diff --git a/modules/features/chatplayground/mainChatplayground.py b/modules/features/chatplayground/mainChatplayground.py index 246236a1..d01c1c23 100644 --- a/modules/features/chatplayground/mainChatplayground.py +++ b/modules/features/chatplayground/mainChatplayground.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__) @@ -48,6 +48,16 @@ RESOURCE_OBJECTS = [ }, ] +# Service requirements - services this feature needs from the service center +# Same as automation: chatplayground runs the same WorkflowManager and workflow methods +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.)"}}, +] # Template roles for this feature # Role names MUST follow convention: {featureCode}-{roleName} TEMPLATE_ROLES = [ @@ -104,6 +114,88 @@ TEMPLATE_ROLES = [ ] +def getRequiredServiceKeys() -> List[str]: + """Return list of service keys this feature requires.""" + return [s["serviceKey"] for s in REQUIRED_SERVICES] + + +def getChatplaygroundServices( + user, + mandateId: Optional[str] = None, + featureInstanceId: Optional[str] = None, + workflow=None, +) -> "_ChatplaygroundServiceHub": + """ + Get a service hub for the chatplayground 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. + """ + from modules.serviceCenter import getService + from modules.serviceCenter.context import ServiceCenterContext + + _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 = _ChatplaygroundServiceHub() + 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 chatplayground: {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 + + return hub + + +class _ChatplaygroundServiceHub: + """Lightweight hub exposing only services required by the chatplayground feature.""" + + user = None + mandateId = None + featureInstanceId = None + workflow = None + featureCode = "chatplayground" + allowedProviders = None + interfaceDbApp = None + interfaceDbComponent = None + interfaceDbChat = 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.""" return { diff --git a/modules/features/chatplayground/routeFeatureChatplayground.py b/modules/features/chatplayground/routeFeatureChatplayground.py index e3787904..c08f4b62 100644 --- a/modules/features/chatplayground/routeFeatureChatplayground.py +++ b/modules/features/chatplayground/routeFeatureChatplayground.py @@ -20,6 +20,7 @@ from modules.datamodels.datamodelChat import ChatWorkflow, UserInputRequest, Wor # Import workflow control functions from modules.workflows.automation import chatStart, chatStop +from modules.features.chatplayground.mainChatplayground import getChatplaygroundServices # Configure logger logger = logging.getLogger(__name__) @@ -95,15 +96,26 @@ async def start_workflow( # Validate access and get mandate ID mandateId = _validateInstanceAccess(instanceId, context) - # Start or continue workflow - workflow = await chatStart( - context.user, - userInput, - workflowMode, - workflowId, + # Get chatplayground services from service center (not automation) + services = getChatplaygroundServices( + context.user, mandateId=mandateId, featureInstanceId=instanceId, - featureCode='chatplayground' + ) + services.featureCode = 'chatplayground' + if hasattr(userInput, 'allowedProviders') and userInput.allowedProviders: + services.allowedProviders = userInput.allowedProviders + + # Start or continue workflow + workflow = await chatStart( + context.user, + userInput, + workflowMode, + workflowId, + mandateId=mandateId, + featureInstanceId=instanceId, + featureCode='chatplayground', + services=services, ) return workflow @@ -132,12 +144,22 @@ async def stop_workflow( # Validate access and get mandate ID mandateId = _validateInstanceAccess(instanceId, context) + # Get chatplayground services from service center (not automation) + services = getChatplaygroundServices( + context.user, + mandateId=mandateId, + featureInstanceId=instanceId, + ) + services.featureCode = 'chatplayground' + # Stop workflow (pass featureInstanceId for proper RBAC filtering) workflow = await chatStop( - context.user, - workflowId, + context.user, + workflowId, mandateId=mandateId, - featureInstanceId=instanceId + featureInstanceId=instanceId, + featureCode='chatplayground', + services=services, ) return workflow diff --git a/modules/workflows/automation/mainWorkflow.py b/modules/workflows/automation/mainWorkflow.py index 1296f3fe..625384d7 100644 --- a/modules/workflows/automation/mainWorkflow.py +++ b/modules/workflows/automation/mainWorkflow.py @@ -24,7 +24,7 @@ from .subAutomationUtils import parseScheduleToCron, planToPrompt, replacePlaceh logger = logging.getLogger(__name__) -async def chatStart(currentUser: User, userInput: UserInputRequest, workflowMode: WorkflowModeEnum, workflowId: Optional[str] = None, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None, featureCode: Optional[str] = None) -> ChatWorkflow: +async def chatStart(currentUser: User, userInput: UserInputRequest, workflowMode: WorkflowModeEnum, workflowId: Optional[str] = None, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None, featureCode: Optional[str] = None, services=None) -> ChatWorkflow: """ Starts a new chat or continues an existing one, then launches processing asynchronously. @@ -36,9 +36,11 @@ async def chatStart(currentUser: User, userInput: UserInputRequest, workflowMode mandateId: Mandate ID (required for billing) featureInstanceId: Feature instance ID (required for billing) featureCode: Feature code (e.g., 'chatplayground', 'automation') + services: Pre-built service hub from the calling feature (required). Each feature must pass its own services. """ + if services is None: + raise ValueError("services is required: each feature must pass its own service hub (e.g. getChatplaygroundServices, getAutomationServices)") try: - services = getAutomationServices(currentUser, mandateId=mandateId, featureInstanceId=featureInstanceId) # Store allowedProviders in services context for model selection if hasattr(userInput, 'allowedProviders') and userInput.allowedProviders: @@ -56,10 +58,11 @@ async def chatStart(currentUser: User, userInput: UserInputRequest, workflowMode logger.error(f"Error starting chat: {str(e)}") raise -async def chatStop(currentUser: User, workflowId: str, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None, featureCode: Optional[str] = None) -> ChatWorkflow: - """Stops a running chat.""" +async def chatStop(currentUser: User, workflowId: str, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None, featureCode: Optional[str] = None, services=None) -> ChatWorkflow: + """Stops a running chat. Caller must pass services from the owning feature.""" + if services is None: + raise ValueError("services is required: each feature must pass its own service hub (e.g. getChatplaygroundServices, getAutomationServices)") try: - services = getAutomationServices(currentUser, mandateId=mandateId, featureInstanceId=featureInstanceId) if featureCode: services.featureCode = featureCode workflowManager = WorkflowManager(services) @@ -146,6 +149,12 @@ async def executeAutomation(automationId: str, automation, creatorUser: User, se # 3. Start workflow using chatStart with creator's context # mandateId and featureInstanceId come from the automation definition + # Each feature must pass its own services - no fallback + creatorServices = getAutomationServices( + creatorUser, + mandateId=automationMandateId, + featureInstanceId=automationFeatureInstanceId, + ) workflow = await chatStart( currentUser=creatorUser, userInput=userInput, @@ -153,7 +162,8 @@ async def executeAutomation(automationId: str, automation, creatorUser: User, se workflowId=None, mandateId=automationMandateId, featureInstanceId=automationFeatureInstanceId, - featureCode='automation' + featureCode='automation', + services=creatorServices, ) executionLog["workflowId"] = workflow.id @@ -161,9 +171,7 @@ async def executeAutomation(automationId: str, automation, creatorUser: User, se executionLog["messages"].append(f"Workflow {workflow.id} started successfully") logger.info(f"Started workflow {workflow.id} with plan containing {len(plan.get('tasks', []))} tasks (plan embedded in userInput)") - # Set workflow name with "automated" prefix — use creatorUser's Services - # (services parameter is eventServices with eventUser context, must use creatorUser context) - creatorServices = getAutomationServices(creatorUser, mandateId=automationMandateId, featureInstanceId=automationFeatureInstanceId) + # Set workflow name with "automated" prefix — use creatorServices from chatStart automationLabel = automation.label or "Unknown Automation" workflowName = f"automated: {automationLabel}" creatorServices.interfaceDbChat.updateWorkflow(workflow.id, {"name": workflowName})