diff --git a/modules/features/automation/mainAutomation.py b/modules/features/automation/mainAutomation.py index 239b1c15..a7e381e1 100644 --- a/modules/features/automation/mainAutomation.py +++ b/modules/features/automation/mainAutomation.py @@ -6,7 +6,7 @@ Handles feature initialization and RBAC catalog registration. """ import logging -from typing import Dict, List, Any +from typing import Dict, List, Any, Optional logger = logging.getLogger(__name__) @@ -121,6 +121,106 @@ TEMPLATE_ROLES = [ }, ] +# Service requirements - services this feature needs from the service center +REQUIRED_SERVICES = [ + {"serviceKey": "chat", "meta": {"usage": "Workflow CRUD, messages, logs"}}, + {"serviceKey": "ai", "meta": {"usage": "AI planning for workflow execution"}}, + {"serviceKey": "utils", "meta": {"usage": "Timestamps, utilities"}}, + {"serviceKey": "billing", "meta": {"usage": "AI call billing"}}, + {"serviceKey": "extraction", "meta": {"usage": "Workflow method actions"}}, + {"serviceKey": "sharepoint", "meta": {"usage": "SharePoint actions (listDocuments, uploadDocument, etc.)"}}, +] + + +def getRequiredServiceKeys() -> List[str]: + """Return list of service keys this feature requires.""" + return [s["serviceKey"] for s in REQUIRED_SERVICES] + + +def getAutomationServices( + user, + mandateId: Optional[str] = None, + featureInstanceId: Optional[str] = None, + workflow=None, +) -> "_AutomationServiceHub": + """ + Get a service hub for the automation feature using the service center. + Resolves only the services declared in REQUIRED_SERVICES. + No legacy fallback - service center only. + + Returns a hub-like object with: chat, ai, utils, billing, extraction, + sharepoint, rbac, interfaceDbApp, interfaceDbComponent, interfaceDbChat, + interfaceDbAutomation. + """ + from modules.serviceCenter import getService + from modules.serviceCenter.context import ServiceCenterContext + from modules.features.automation.interfaceFeatureAutomation import getInterface as getAutomationInterface + + _workflow = workflow + if _workflow is None: + _workflow = type("_Placeholder", (), {"featureCode": FEATURE_CODE})() + ctx = ServiceCenterContext( + user=user, + mandate_id=mandateId, + feature_instance_id=featureInstanceId, + workflow=_workflow, + ) + + hub = _AutomationServiceHub() + hub.user = user + hub.mandateId = mandateId + hub.featureInstanceId = featureInstanceId + hub.workflow = workflow + hub.featureCode = FEATURE_CODE + hub.allowedProviders = None + + for spec in REQUIRED_SERVICES: + key = spec["serviceKey"] + try: + svc = getService(key, ctx, legacy_hub=None) + setattr(hub, key, svc) + except Exception as e: + logger.warning(f"Could not resolve service '{key}' for automation: {e}") + setattr(hub, key, None) + + # Copy interfaces from chat service for WorkflowManager compatibility + if hub.chat: + hub.interfaceDbApp = getattr(hub.chat, "interfaceDbApp", None) + hub.interfaceDbComponent = getattr(hub.chat, "interfaceDbComponent", None) + hub.interfaceDbChat = getattr(hub.chat, "interfaceDbChat", None) + + # RBAC for MethodBase action permission checks (workflow methods) + hub.rbac = getattr(hub.interfaceDbApp, "rbac", None) if hub.interfaceDbApp else None + + # Set interfaceDbAutomation from feature interface + hub.interfaceDbAutomation = getAutomationInterface( + user, mandateId=mandateId, featureInstanceId=featureInstanceId + ) + + return hub + + +class _AutomationServiceHub: + """Lightweight hub exposing only services required by the automation feature.""" + + user = None + mandateId = None + featureInstanceId = None + workflow = None + featureCode = "automation" + allowedProviders = None + interfaceDbApp = None + interfaceDbComponent = None + interfaceDbChat = None + interfaceDbAutomation = None + rbac = None + chat = None + ai = None + utils = None + billing = None + extraction = None + sharepoint = None + def getFeatureDefinition() -> Dict[str, Any]: """Return the feature definition for registration.""" diff --git a/modules/features/automation/routeFeatureAutomation.py b/modules/features/automation/routeFeatureAutomation.py index 4c008301..1e548bba 100644 --- a/modules/features/automation/routeFeatureAutomation.py +++ b/modules/features/automation/routeFeatureAutomation.py @@ -14,6 +14,7 @@ import json # Import interfaces and models from modules.features.automation.interfaceFeatureAutomation import getInterface as getAutomationInterface +from modules.features.automation.mainAutomation import getAutomationServices from modules.auth import limiter, getRequestContext, RequestContext from modules.features.automation.datamodelFeatureAutomation import AutomationDefinition, AutomationTemplate from modules.datamodels.datamodelChat import ChatWorkflow @@ -147,12 +148,14 @@ def get_available_actions( """ try: from modules.workflows.processing.shared.methodDiscovery import methods, discoverMethods - from modules.services import getInterface as getServices - # Ensure methods are discovered (need a service center for discovery) + # Ensure methods are discovered (need a service hub for discovery) if not methods: - # Create a lightweight service center for method discovery - services = getServices(context.user, mandateId=context.mandateId) + services = getAutomationServices( + context.user, + mandateId=context.mandateId, + featureInstanceId=context.featureInstanceId, + ) discoverMethods(services) actionsList = [] @@ -365,8 +368,11 @@ async def execute_automation_route( ) -> ChatWorkflow: """Execute an automation immediately (test mode)""" try: - from modules.services import getInterface as getServices - services = getServices(context.user, mandateId=context.mandateId, featureInstanceId=context.featureInstanceId) + services = getAutomationServices( + context.user, + mandateId=context.mandateId, + featureInstanceId=context.featureInstanceId, + ) # Load automation with current user's context (user has RBAC permissions via UI) automation = services.interfaceDbAutomation.getAutomationDefinition(automationId, includeSystemFields=True) diff --git a/modules/routes/routeAdminAutomationEvents.py b/modules/routes/routeAdminAutomationEvents.py index 867390df..d184ae76 100644 --- a/modules/routes/routeAdminAutomationEvents.py +++ b/modules/routes/routeAdminAutomationEvents.py @@ -45,8 +45,8 @@ def get_all_automation_events( try: from modules.shared.eventManagement import eventManager from modules.interfaces.interfaceDbApp import getRootInterface - from modules.services import getInterface as getServices - + from modules.features.automation.mainAutomation import getAutomationServices + if not eventManager.scheduler: return [] @@ -74,7 +74,7 @@ def get_all_automation_events( rootInterface = getRootInterface() eventUser = rootInterface.getUserByUsername("event") if eventUser: - services = getServices(currentUser, None) + services = getAutomationServices(currentUser, mandateId=None, featureInstanceId=None) allAutomations = services.interfaceDbAutomation.getAllAutomationDefinitionsWithRBAC(eventUser) # Build lookup by automation ID @@ -171,8 +171,8 @@ async def sync_all_automation_events( detail="Event user not available" ) - from modules.services import getInterface as getServices - services = getServices(currentUser, None) + from modules.features.automation.mainAutomation import getAutomationServices + services = getAutomationServices(currentUser, mandateId=None, featureInstanceId=None) result = syncAutomationEvents(services, eventUser) return { "success": True, diff --git a/modules/workflows/automation/mainWorkflow.py b/modules/workflows/automation/mainWorkflow.py index 1fc4b0cf..1296f3fe 100644 --- a/modules/workflows/automation/mainWorkflow.py +++ b/modules/workflows/automation/mainWorkflow.py @@ -17,7 +17,7 @@ from modules.features.automation.datamodelFeatureAutomation import AutomationDef from modules.datamodels.datamodelUam import User from modules.shared.timeUtils import getUtcTimestamp from modules.shared.eventManagement import eventManager -from modules.services import getInterface as getServices +from modules.features.automation.mainAutomation import getAutomationServices from modules.workflows.workflowManager import WorkflowManager from .subAutomationUtils import parseScheduleToCron, planToPrompt, replacePlaceholders @@ -38,7 +38,7 @@ async def chatStart(currentUser: User, userInput: UserInputRequest, workflowMode featureCode: Feature code (e.g., 'chatplayground', 'automation') """ try: - services = getServices(currentUser, mandateId=mandateId, featureInstanceId=featureInstanceId) + services = getAutomationServices(currentUser, mandateId=mandateId, featureInstanceId=featureInstanceId) # Store allowedProviders in services context for model selection if hasattr(userInput, 'allowedProviders') and userInput.allowedProviders: @@ -59,7 +59,7 @@ async def chatStart(currentUser: User, userInput: UserInputRequest, workflowMode async def chatStop(currentUser: User, workflowId: str, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None, featureCode: Optional[str] = None) -> ChatWorkflow: """Stops a running chat.""" try: - services = getServices(currentUser, mandateId=mandateId, featureInstanceId=featureInstanceId) + services = getAutomationServices(currentUser, mandateId=mandateId, featureInstanceId=featureInstanceId) if featureCode: services.featureCode = featureCode workflowManager = WorkflowManager(services) @@ -163,7 +163,7 @@ async def executeAutomation(automationId: str, automation, creatorUser: User, se # Set workflow name with "automated" prefix — use creatorUser's Services # (services parameter is eventServices with eventUser context, must use creatorUser context) - creatorServices = getServices(creatorUser, mandateId=automationMandateId, featureInstanceId=automationFeatureInstanceId) + creatorServices = getAutomationServices(creatorUser, mandateId=automationMandateId, featureInstanceId=automationFeatureInstanceId) automationLabel = automation.label or "Unknown Automation" workflowName = f"automated: {automationLabel}" creatorServices.interfaceDbChat.updateWorkflow(workflow.id, {"name": workflowName}) @@ -292,7 +292,7 @@ def createAutomationEventHandler(automationId: str, eventUser): return # Load automation using SysAdmin eventUser (has unrestricted access) - eventServices = getServices(eventUser, None) + eventServices = getAutomationServices(eventUser, mandateId=None, featureInstanceId=None) automation = eventServices.interfaceDbAutomation.getAutomationDefinition(automationId, includeSystemFields=True) if not automation or not getattr(automation, "active", False): logger.warning(f"Automation {automationId} not found or not active, skipping execution") diff --git a/modules/workflows/automation/subAutomationSchedule.py b/modules/workflows/automation/subAutomationSchedule.py index 9db1f3fa..18cc4245 100644 --- a/modules/workflows/automation/subAutomationSchedule.py +++ b/modules/workflows/automation/subAutomationSchedule.py @@ -9,7 +9,7 @@ the automation scheduler (loading/syncing scheduled automation events). """ import logging -from modules.services import getInterface as getServices +from modules.features.automation.mainAutomation import getAutomationServices logger = logging.getLogger(__name__) @@ -31,12 +31,12 @@ def start(eventUser) -> bool: from modules.shared.callbackRegistry import callbackRegistry # Get services for event user (provides access to interfaces) - services = getServices(eventUser, None) + services = getAutomationServices(eventUser, mandateId=None, featureInstanceId=None) # Register callback for automation changes def onAutomationChanged(chatInterface): """Callback triggered when automations are created/updated/deleted.""" - eventServices = getServices(eventUser, None) + eventServices = getAutomationServices(eventUser, mandateId=None, featureInstanceId=None) syncAutomationEvents(eventServices, eventUser) callbackRegistry.register('automation.changed', onAutomationChanged)