# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Interface for Automation2 feature - Workflows, Runs, Human Tasks. Uses PostgreSQL poweron_automation2 database. """ import logging import uuid from typing import Dict, Any, List, Optional from modules.datamodels.datamodelUam import User from modules.features.automation2.datamodelFeatureAutomation2 import ( Automation2Workflow, Automation2WorkflowRun, Automation2HumanTask, ) from modules.connectors.connectorDbPostgre import DatabaseConnector from modules.shared.configuration import APP_CONFIG logger = logging.getLogger(__name__) def getAutomation2Interface( currentUser: User, mandateId: str, featureInstanceId: str, ) -> "Automation2Objects": """Factory for Automation2 interface with user context.""" return Automation2Objects( currentUser=currentUser, mandateId=mandateId, featureInstanceId=featureInstanceId, ) class Automation2Objects: """Interface for Automation2 database operations.""" def __init__( self, currentUser: User, mandateId: str, featureInstanceId: str, ): self.currentUser = currentUser self.mandateId = mandateId self.featureInstanceId = featureInstanceId self.userId = currentUser.id if currentUser else None self._init_db() if hasattr(self.db, "updateContext") and self.userId: self.db.updateContext(self.userId) def _init_db(self): """Initialize database connection to poweron_automation2.""" dbHost = APP_CONFIG.get("DB_HOST", "localhost") dbDatabase = "poweron_automation2" dbUser = APP_CONFIG.get("DB_USER") dbPassword = APP_CONFIG.get("DB_PASSWORD_SECRET") or APP_CONFIG.get("DB_PASSWORD") dbPort = int(APP_CONFIG.get("DB_PORT", 5432)) self.db = DatabaseConnector( dbHost=dbHost, dbDatabase=dbDatabase, dbUser=dbUser, dbPassword=dbPassword, dbPort=dbPort, userId=self.userId, ) logger.debug("Automation2 database initialized for user %s", self.userId) # ------------------------------------------------------------------------- # Workflow CRUD # ------------------------------------------------------------------------- def getWorkflows(self) -> List[Dict[str, Any]]: """Get all workflows for this mandate and feature instance.""" if not self.db._ensureTableExists(Automation2Workflow): return [] records = self.db.getRecordset( Automation2Workflow, recordFilter={ "mandateId": self.mandateId, "featureInstanceId": self.featureInstanceId, }, ) return [dict(r) for r in records] if records else [] def getWorkflow(self, workflowId: str) -> Optional[Dict[str, Any]]: """Get a single workflow by ID.""" if not self.db._ensureTableExists(Automation2Workflow): return None records = self.db.getRecordset( Automation2Workflow, recordFilter={ "id": workflowId, "mandateId": self.mandateId, "featureInstanceId": self.featureInstanceId, }, ) if not records: return None return dict(records[0]) def createWorkflow(self, data: Dict[str, Any]) -> Dict[str, Any]: """Create a new workflow.""" if "id" not in data or not data.get("id"): data["id"] = str(uuid.uuid4()) data["mandateId"] = self.mandateId data["featureInstanceId"] = self.featureInstanceId created = self.db.recordCreate(Automation2Workflow, data) return dict(created) def updateWorkflow(self, workflowId: str, data: Dict[str, Any]) -> Optional[Dict[str, Any]]: """Update an existing workflow.""" existing = self.getWorkflow(workflowId) if not existing: return None # Don't overwrite mandateId/featureInstanceId data.pop("mandateId", None) data.pop("featureInstanceId", None) updated = self.db.recordModify(Automation2Workflow, workflowId, data) return dict(updated) def deleteWorkflow(self, workflowId: str) -> bool: """Delete a workflow.""" existing = self.getWorkflow(workflowId) if not existing: return False self.db.recordDelete(Automation2Workflow, workflowId) return True # ------------------------------------------------------------------------- # Workflow Runs # ------------------------------------------------------------------------- def createRun(self, workflowId: str, nodeOutputs: Dict = None, context: Dict = None) -> Dict[str, Any]: """Create a new workflow run.""" data = { "id": str(uuid.uuid4()), "workflowId": workflowId, "status": "running", "nodeOutputs": nodeOutputs or {}, "currentNodeId": None, "context": context or {}, } created = self.db.recordCreate(Automation2WorkflowRun, data) return dict(created) def getRun(self, runId: str) -> Optional[Dict[str, Any]]: """Get a run by ID.""" if not self.db._ensureTableExists(Automation2WorkflowRun): return None records = self.db.getRecordset( Automation2WorkflowRun, recordFilter={"id": runId}, ) if not records: return None return dict(records[0]) def updateRun( self, runId: str, status: str = None, nodeOutputs: Dict = None, currentNodeId: str = None, context: Dict = None, ) -> Optional[Dict[str, Any]]: """Update a run.""" run = self.getRun(runId) if not run: return None updates = {} if status is not None: updates["status"] = status if nodeOutputs is not None: updates["nodeOutputs"] = nodeOutputs if currentNodeId is not None: updates["currentNodeId"] = currentNodeId if context is not None: updates["context"] = context if not updates: return run updated = self.db.recordModify(Automation2WorkflowRun, runId, updates) return dict(updated) def getRunsByWorkflow(self, workflowId: str) -> List[Dict[str, Any]]: """Get all runs for a workflow.""" if not self.db._ensureTableExists(Automation2WorkflowRun): return [] records = self.db.getRecordset( Automation2WorkflowRun, recordFilter={"workflowId": workflowId}, ) return [dict(r) for r in records] if records else [] def getRunsWaitingForEmail(self) -> List[Dict[str, Any]]: """Get all paused runs waiting for a new email (for background poller).""" if not self.db._ensureTableExists(Automation2WorkflowRun): return [] records = self.db.getRecordset( Automation2WorkflowRun, recordFilter={"status": "paused"}, ) if not records: return [] result = [] for r in records: rec = dict(r) ctx = rec.get("context") or {} if ctx.get("waitReason") == "email": result.append(rec) return result # ------------------------------------------------------------------------- # Human Tasks # ------------------------------------------------------------------------- def createTask( self, runId: str, workflowId: str, nodeId: str, nodeType: str, config: Dict[str, Any], assigneeId: str = None, ) -> Dict[str, Any]: """Create a human task.""" data = { "id": str(uuid.uuid4()), "runId": runId, "workflowId": workflowId, "nodeId": nodeId, "nodeType": nodeType, "config": config, "assigneeId": assigneeId, "status": "pending", "result": None, } created = self.db.recordCreate(Automation2HumanTask, data) return dict(created) def getTask(self, taskId: str) -> Optional[Dict[str, Any]]: """Get a task by ID.""" if not self.db._ensureTableExists(Automation2HumanTask): return None records = self.db.getRecordset( Automation2HumanTask, recordFilter={"id": taskId}, ) if not records: return None return dict(records[0]) def updateTask(self, taskId: str, status: str = None, result: Dict = None) -> Optional[Dict[str, Any]]: """Update a task (e.g. complete with result).""" task = self.getTask(taskId) if not task: return None updates = {} if status is not None: updates["status"] = status if result is not None: updates["result"] = result if not updates: return task updated = self.db.recordModify(Automation2HumanTask, taskId, updates) return dict(updated) def getTasks( self, workflowId: str = None, runId: str = None, status: str = None, assigneeId: str = None, ) -> List[Dict[str, Any]]: """Get tasks with optional filters. AssigneeId filters to that user; None returns all.""" if not self.db._ensureTableExists(Automation2HumanTask): return [] rf = {} if workflowId: rf["workflowId"] = workflowId if runId: rf["runId"] = runId if status: rf["status"] = status if assigneeId: rf["assigneeId"] = assigneeId records = self.db.getRecordset( Automation2HumanTask, recordFilter=rf if rf else None, ) items = [dict(r) for r in records] if records else [] workflows = {w["id"]: w for w in self.getWorkflows()} filtered = [t for t in items if t.get("workflowId") in workflows] return filtered