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"""