From 310e6d3f8b62c783a44904c47be00351c281bfe4 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Mon, 3 Nov 2025 09:58:15 +0100 Subject: [PATCH] fixes automation --- modules/connectors/connectorDbPostgre.py | 17 +- modules/datamodels/datamodelChat.py | 8 + modules/interfaces/interfaceDbChatObjects.py | 194 +++++++++++++------ 3 files changed, 163 insertions(+), 56 deletions(-) diff --git a/modules/connectors/connectorDbPostgre.py b/modules/connectors/connectorDbPostgre.py index 58d17b66..78ab5e36 100644 --- a/modules/connectors/connectorDbPostgre.py +++ b/modules/connectors/connectorDbPostgre.py @@ -612,11 +612,24 @@ class DatabaseConnector: # Add metadata currentTime = getUtcTimestamp() + # Set _createdAt and _createdBy if this is a new record (record doesn't have _createdAt) if "_createdAt" not in record: record["_createdAt"] = currentTime - record["_createdBy"] = self.userId + # Only set _createdBy if userId is valid (not None or empty string) + if self.userId: + record["_createdBy"] = self.userId + else: + logger.warning(f"Attempting to create record with empty userId - _createdBy will not be set") + # Also ensure _createdBy is set even if _createdAt exists but _createdBy is missing/empty + elif "_createdBy" not in record or not record.get("_createdBy"): + if self.userId: + record["_createdBy"] = self.userId + else: + logger.warning(f"Attempting to set _createdBy with empty userId for record {recordId}") + # Always update modification metadata record["_modifiedAt"] = currentTime - record["_modifiedBy"] = self.userId + if self.userId: + record["_modifiedBy"] = self.userId with self.connection.cursor() as cursor: self._save_record(cursor, table, recordId, record, model_class) diff --git a/modules/datamodels/datamodelChat.py b/modules/datamodels/datamodelChat.py index 437eacb1..9acbf464 100644 --- a/modules/datamodels/datamodelChat.py +++ b/modules/datamodels/datamodelChat.py @@ -1071,6 +1071,13 @@ class AutomationDefinition(BaseModel): frontend_readonly=True, frontend_required=False ) + executionLogs: List[Dict[str, Any]] = Field( + default_factory=list, + description="List of execution logs, each containing timestamp, workflowId, status, and messages", + frontend_type="text", + frontend_readonly=True, + frontend_required=False + ) registerModelLabels( @@ -1086,5 +1093,6 @@ registerModelLabels( "active": {"en": "Active", "fr": "Actif"}, "eventId": {"en": "Event ID", "fr": "ID de l'événement"}, "status": {"en": "Status", "fr": "Statut"}, + "executionLogs": {"en": "Execution Logs", "fr": "Journaux d'exécution"}, }, ) diff --git a/modules/interfaces/interfaceDbChatObjects.py b/modules/interfaces/interfaceDbChatObjects.py index e6cb9aa0..08d0ee88 100644 --- a/modules/interfaces/interfaceDbChatObjects.py +++ b/modules/interfaces/interfaceDbChatObjects.py @@ -107,7 +107,11 @@ class ChatObjects: objectFields[fieldName] = value else: # Field not in model - treat as scalar if simple, otherwise filter out - if isinstance(value, (str, int, float, bool, type(None))): + # BUT: always include metadata fields (_createdBy, _createdAt, etc.) as they're handled by connector + if fieldName.startswith("_"): + # Metadata fields should be passed through to connector + simpleFields[fieldName] = value + elif isinstance(value, (str, int, float, bool, type(None))): simpleFields[fieldName] = value else: objectFields[fieldName] = value @@ -1284,6 +1288,16 @@ class ChatObjects: if "mandateId" not in automationData: automationData["mandateId"] = self.mandateId + # Ensure database connector has correct userId context + # The connector should have been initialized with userId, but ensure it's updated + if self.userId and hasattr(self.db, 'updateContext'): + try: + self.db.updateContext(self.userId) + except Exception as e: + logger.warning(f"Could not update database context: {e}") + + # Note: _createdBy will be set automatically by connector's _saveRecord method + # when _createdAt is not present. We don't need to set it manually here. # Use generic field separation simpleFields, objectFields = self._separateObjectFields(AutomationDefinition, automationData) @@ -1383,60 +1397,132 @@ class ChatObjects: async def executeAutomation(self, automationId: str) -> ChatWorkflow: """Execute automation workflow immediately (test mode) with placeholder replacement""" - # 1. Load automation definition - automation = self.getAutomationDefinition(automationId) - if not automation: - raise ValueError(f"Automation {automationId} not found") + executionStartTime = getUtcTimestamp() + executionLog = { + "timestamp": executionStartTime, + "workflowId": None, + "status": "running", + "messages": [] + } - # 2. Replace placeholders in template to generate plan - template = automation.get("template", "") - placeholders = automation.get("placeholders", {}) - planJson = self._replacePlaceholders(template, placeholders) - plan = json.loads(planJson) - - # 3. Get user who created automation - creator_user_id = automation.get("_createdBy") - if not creator_user_id: - raise ValueError(f"Automation {automationId} has no creator user") - - # Get user from database - from modules.interfaces.interfaceDbAppObjects import getInterface as getAppInterface - appInterface = getAppInterface(self.currentUser) - creator_user = appInterface.getUser(creator_user_id) - if not creator_user: - raise ValueError(f"Creator user {creator_user_id} not found") - - # 4. Create UserInputRequest from plan - # Embed plan JSON in prompt for TemplateMode to extract - promptText = self._planToPrompt(plan) - planJson = json.dumps(plan) - # Embed plan as JSON comment so TemplateMode can extract it - promptWithPlan = f"{promptText}\n\n\n{planJson}\n" - - userInput = UserInputRequest( - prompt=promptWithPlan, - listFileId=[], - userLanguage=creator_user.language or "en" - ) - - # 5. Start workflow using chatStart - from modules.features.chatPlayground.mainChatPlayground import chatStart - - workflow = await chatStart( - currentUser=creator_user, - userInput=userInput, - workflowMode=WorkflowModeEnum.WORKFLOW_AUTOMATION, - workflowId=None - ) - - # Also store plan in module-level cache as backup (keyed by workflow ID) - from modules.workflows.processing.modes import modeAutomation - if not hasattr(modeAutomation, '_templatePlanCache'): - modeAutomation._templatePlanCache = {} - modeAutomation._templatePlanCache[workflow.id] = plan - logger.info(f"Stored template plan for workflow {workflow.id} (cache + prompt) with {len(plan.get('tasks', []))} tasks") - - return workflow + try: + # 1. Load automation definition + automation = self.getAutomationDefinition(automationId) + if not automation: + raise ValueError(f"Automation {automationId} not found") + + executionLog["messages"].append(f"Started execution at {executionStartTime}") + + # 2. Replace placeholders in template to generate plan + template = automation.get("template", "") + placeholders = automation.get("placeholders", {}) + planJson = self._replacePlaceholders(template, placeholders) + plan = json.loads(planJson) + executionLog["messages"].append("Template placeholders replaced successfully") + + # 3. Get user who created automation + creator_user_id = automation.get("_createdBy") + + # If _createdBy is missing, try to fix it by setting it to current user + # This handles automations created before _createdBy was required + if not creator_user_id: + logger.warning(f"Automation {automationId} has no creator user, setting to current user {self.userId}") + try: + # Update the automation to set _createdBy + self.db.recordModify( + AutomationDefinition, + automationId, + {"_createdBy": self.userId} + ) + creator_user_id = self.userId + automation["_createdBy"] = self.userId + logger.info(f"Fixed automation {automationId} by setting _createdBy to {self.userId}") + executionLog["messages"].append(f"Fixed missing _createdBy field, set to user {self.userId}") + except Exception as e: + logger.error(f"Error fixing automation {automationId}: {str(e)}") + raise ValueError(f"Automation {automationId} has no creator user and could not be fixed") + + # Get user from database + from modules.interfaces.interfaceDbAppObjects import getInterface as getAppInterface + appInterface = getAppInterface(self.currentUser) + creator_user = appInterface.getUser(creator_user_id) + if not creator_user: + raise ValueError(f"Creator user {creator_user_id} not found") + + executionLog["messages"].append(f"Using creator user: {creator_user_id}") + + # 4. Create UserInputRequest from plan + # Embed plan JSON in prompt for TemplateMode to extract + promptText = self._planToPrompt(plan) + planJson = json.dumps(plan) + # Embed plan as JSON comment so TemplateMode can extract it + promptWithPlan = f"{promptText}\n\n\n{planJson}\n" + + userInput = UserInputRequest( + prompt=promptWithPlan, + listFileId=[], + userLanguage=creator_user.language or "en" + ) + + executionLog["messages"].append("Starting workflow execution") + + # 5. Start workflow using chatStart + from modules.features.chatPlayground.mainChatPlayground import chatStart + + workflow = await chatStart( + currentUser=creator_user, + userInput=userInput, + workflowMode=WorkflowModeEnum.WORKFLOW_AUTOMATION, + workflowId=None + ) + + executionLog["workflowId"] = workflow.id + executionLog["status"] = "completed" + executionLog["messages"].append(f"Workflow {workflow.id} started successfully") + + # Also store plan in module-level cache as backup (keyed by workflow ID) + from modules.workflows.processing.modes import modeAutomation + if not hasattr(modeAutomation, '_templatePlanCache'): + modeAutomation._templatePlanCache = {} + modeAutomation._templatePlanCache[workflow.id] = plan + logger.info(f"Stored template plan for workflow {workflow.id} (cache + prompt) with {len(plan.get('tasks', []))} tasks") + + # Update automation with execution log + executionLogs = automation.get("executionLogs", []) + executionLogs.append(executionLog) + # Keep only last 50 executions + if len(executionLogs) > 50: + executionLogs = executionLogs[-50:] + + self.db.recordModify( + AutomationDefinition, + automationId, + {"executionLogs": executionLogs} + ) + + return workflow + except Exception as e: + # Log error to execution log + executionLog["status"] = "error" + executionLog["messages"].append(f"Error: {str(e)}") + + # Update automation with execution log even on error + try: + automation = self.getAutomationDefinition(automationId) + if automation: + executionLogs = automation.get("executionLogs", []) + executionLogs.append(executionLog) + if len(executionLogs) > 50: + executionLogs = executionLogs[-50:] + self.db.recordModify( + AutomationDefinition, + automationId, + {"executionLogs": executionLogs} + ) + except Exception as logError: + logger.error(f"Error saving execution log: {str(logError)}") + + raise def _planToPrompt(self, plan: Dict) -> str: """Convert plan structure to prompt string for workflow execution"""