from typing import Dict, Any, List, Optional import logging from datetime import datetime, UTC import uuid import asyncio from modules.interfaces.interfaceAppObjects import User from modules.interfaces.interfaceChatModel import (UserInputRequest, ChatMessage, ChatWorkflow, TaskItem, TaskStatus) from modules.interfaces.interfaceChatObjects import ChatObjects from modules.workflow.managerChat import ChatManager logger = logging.getLogger(__name__) class WorkflowStoppedException(Exception): """Exception raised when workflow is stopped by user""" pass class WorkflowManager: """Manager for workflow processing and coordination""" def __init__(self, chatInterface: ChatObjects, currentUser: User): self.chatInterface = chatInterface self.chatManager = ChatManager(currentUser, chatInterface) self.currentUser = currentUser def _checkWorkflowStopped(self, workflow: ChatWorkflow) -> None: """Check if workflow has been stopped""" if workflow.status == "stopped": raise WorkflowStoppedException("Workflow was stopped by user") async def workflowProcess(self, userInput: UserInputRequest, workflow: ChatWorkflow) -> TaskItem: """Enhanced workflow process with proper task planning and handover review""" try: logger.info(f"Processing workflow: {workflow.id}") # Phase 1: Create initial message with user request and documents initial_message = await self._createInitialMessage(userInput, workflow) if not initial_message: raise Exception("Failed to create initial message") # Phase 2: Generate task plan through AI analysis task_plan = await self._generateTaskPlan(userInput, workflow, initial_message) if not task_plan: raise Exception("Failed to generate task plan") # Phase 3: Execute tasks with handover review task_result = await self._executeTaskPlan(task_plan, workflow, userInput) return task_result except Exception as e: logger.error(f"Error in workflowProcess: {str(e)}") raise async def _createInitialMessage(self, userInput: UserInputRequest, workflow: ChatWorkflow) -> ChatMessage: """Create initial message with user request and processed documents""" try: # Initialize chat manager with workflow await self.chatManager.initialize(workflow) # Process file IDs into ChatDocument objects documents = await self.chatManager.processFileIds(userInput.listFileId) # Create message data message_data = { "id": f"msg_{uuid.uuid4()}", "workflowId": workflow.id, "role": "user", "agentName": "", "message": userInput.prompt, "documents": documents, "status": "step", "publishedAt": self._getCurrentTimestamp() } # Create message in database message = self.chatInterface.createWorkflowMessage(message_data) if not message: raise Exception("Failed to create workflow message") logger.info(f"Created initial message: {message.id} with {len(documents)} documents") return message except Exception as e: logger.error(f"Error creating initial message: {str(e)}") return None async def _generateTaskPlan(self, userInput: UserInputRequest, workflow: ChatWorkflow, initial_message: ChatMessage) -> Dict[str, Any]: """Generate task plan through AI analysis""" try: # Prepare context for AI analysis context = { "user_request": userInput.prompt, "available_documents": [doc.filename for doc in initial_message.documents], "workflow_id": workflow.id, "message_id": initial_message.id } # Generate task plan using AI task_plan = await self.chatManager.generateTaskPlan(context) logger.info(f"Generated task plan with {len(task_plan.get('tasks', []))} tasks") return task_plan except Exception as e: logger.error(f"Error generating task plan: {str(e)}") return None async def _executeTaskPlan(self, task_plan: Dict[str, Any], workflow: ChatWorkflow, userInput: UserInputRequest) -> TaskItem: """Execute task plan with handover review and enhanced error recovery""" try: tasks = task_plan.get('tasks', []) if not tasks: raise Exception("No tasks in task plan") # Create main task item task_data = { "id": f"task_{uuid.uuid4()}", "workflowId": workflow.id, "userInput": userInput.prompt, "status": TaskStatus.RUNNING, "feedback": task_plan.get('overview', 'Executing task plan'), "startedAt": self._getCurrentTimestamp(), "actionList": [], "taskPlan": task_plan } task = self.chatInterface.createTask(task_data) if not task: raise Exception("Failed to create task") # Ensure task is saved to database logger.info(f"Created task with ID: {task.id}") # Execute each task with enhanced error recovery for i, task_step in enumerate(tasks): logger.info(f"Executing task {i+1}/{len(tasks)}: {task_step.get('description', 'Unknown')}") # Execute task step with retry mechanism step_result = await self._executeTaskStepWithRetry(task_step, workflow, task) # Enhanced handover review review_result = await self._performEnhancedHandoverReview(step_result, task_step, workflow, task) if review_result['status'] == 'failed': # Try alternative approach before giving up alternative_result = await self._tryAlternativeApproach(task_step, workflow, task, review_result) if alternative_result['status'] == 'failed': # Update task status with detailed feedback update_result = self.chatInterface.updateTask(task.id, { "status": TaskStatus.FAILED, "feedback": f"Task failed at step {i+1}: {review_result['reason']}. Alternative approach also failed.", "errorDetails": { "failedStep": task_step, "originalError": review_result['reason'], "suggestions": self._generateFailureSuggestions(task_step, review_result) } }) if not update_result: logger.error(f"Failed to update task {task.id} status to FAILED") return task else: step_result = alternative_result review_result = {'status': 'success'} elif review_result['status'] == 'retry': # Retry with improved approach logger.info(f"Retrying task step {i+1} with improved approach") step_result = await self._executeTaskStepWithRetry(task_step, workflow, task, improvements=review_result.get('improvements')) review_result = await self._performEnhancedHandoverReview(step_result, task_step, workflow, task) if review_result['status'] == 'failed': update_result = self.chatInterface.updateTask(task.id, { "status": TaskStatus.FAILED, "feedback": f"Task failed after retry at step {i+1}: {review_result['reason']}" }) if not update_result: logger.error(f"Failed to update task {task.id} status to FAILED after retry") return task # Add step result to task if step_result and step_result.get('actions'): for action in step_result['actions']: # Convert action format to TaskAction format task_action_data = { "execMethod": action.get('method', 'unknown'), "execAction": action.get('action', 'unknown'), "execParameters": action.get('parameters', {}), "execResultLabel": action.get('resultLabel', ''), "status": TaskStatus.PENDING } task_action = self.chatInterface.createTaskAction(task_action_data) if task_action: task.actionList.append(task_action) logger.info(f"Created task action: {task_action.execMethod}.{task_action.execAction}") else: logger.error(f"Failed to create task action: {action}") # Update progress self._updateTaskProgress(task, i + 1, len(tasks)) # Update task as completed update_result = self.chatInterface.updateTask(task.id, { "status": TaskStatus.COMPLETED, "feedback": f"Successfully completed {len(tasks)} tasks with {len(task.actionList)} total actions", "finishedAt": self._getCurrentTimestamp(), "successMetrics": { "totalTasks": len(tasks), "totalActions": len(task.actionList), "executionTime": self._calculateExecutionTime(task.startedAt) } }) if not update_result: logger.error(f"Failed to update task {task.id} status to COMPLETED") return task except Exception as e: logger.error(f"Error executing task plan: {str(e)}") raise async def _executeTaskStepWithRetry(self, task_step: Dict[str, Any], workflow: ChatWorkflow, task: TaskItem, max_retries: int = 3, improvements: str = None) -> Dict[str, Any]: """Execute task step with exponential backoff retry mechanism""" last_error = None for attempt in range(max_retries + 1): try: # Add exponential backoff delay for retries if attempt > 0: delay = min(2 ** attempt, 30) # Max 30 seconds await asyncio.sleep(delay) logger.info(f"Retry attempt {attempt} for task step: {task_step.get('description', 'Unknown')}") # Execute task step step_result = await self._executeTaskStep(task_step, workflow, task, improvements) # Quick validation if step_result.get('status') == 'completed': return step_result else: last_error = step_result.get('error', 'Unknown error') except Exception as e: last_error = str(e) logger.warning(f"Attempt {attempt + 1} failed for task step: {str(e)}") # All retries exhausted return { 'task_step': task_step, 'error': f"All {max_retries + 1} attempts failed. Last error: {last_error}", 'status': 'failed', 'retryAttempts': max_retries + 1 } async def _performEnhancedHandoverReview(self, step_result: Dict[str, Any], task_step: Dict[str, Any], workflow: ChatWorkflow, task: TaskItem) -> Dict[str, Any]: """Enhanced handover review with quality assessment""" try: # Prepare enhanced review context review_context = { 'task_step': task_step, 'step_result': step_result, 'workflow_id': workflow.id, 'task_id': task.id, 'previous_results': self._getPreviousResults(task) } # Use AI to review the results review = await self.chatManager.reviewTaskStepResults(review_context) # Add quality metrics review['quality_metrics'] = await self._calculateQualityMetrics(step_result, task_step) return review except Exception as e: logger.error(f"Error in enhanced handover review: {str(e)}") return { 'status': 'failed', 'reason': f'Review failed: {str(e)}', 'quality_metrics': {'score': 0, 'confidence': 0} } async def _tryAlternativeApproach(self, task_step: Dict[str, Any], workflow: ChatWorkflow, task: TaskItem, original_review: Dict[str, Any]) -> Dict[str, Any]: """Try alternative approach when original method fails""" try: logger.info(f"Trying alternative approach for task step: {task_step.get('description', 'Unknown')}") # Generate alternative approach based on failure analysis alternative_prompt = self._createAlternativeApproachPrompt(task_step, original_review) alternative_response = await self.chatManager._callAI(alternative_prompt, "alternative_approach") # Parse alternative approach alternative_approach = self._parseAlternativeApproach(alternative_response) if alternative_approach: # Execute alternative approach step_result = await self._executeTaskStep(task_step, workflow, task, alternative_approach) return step_result else: return { 'task_step': task_step, 'error': 'Could not generate alternative approach', 'status': 'failed' } except Exception as e: logger.error(f"Error trying alternative approach: {str(e)}") return { 'task_step': task_step, 'error': f'Alternative approach failed: {str(e)}', 'status': 'failed' } def _generateFailureSuggestions(self, task_step: Dict[str, Any], review_result: Dict[str, Any]) -> List[str]: """Generate helpful suggestions when tasks fail""" suggestions = [] if 'missing_outputs' in review_result: suggestions.append(f"Ensure all expected outputs are produced: {', '.join(review_result['missing_outputs'])}") if 'unmet_criteria' in review_result: suggestions.append(f"Address unmet success criteria: {', '.join(review_result['unmet_criteria'])}") suggestions.append("Check if all required documents are available and accessible") suggestions.append("Verify that the task step has all necessary dependencies completed") return suggestions async def _calculateQualityMetrics(self, step_result: Dict[str, Any], task_step: Dict[str, Any]) -> Dict[str, Any]: """Calculate quality metrics for task step results""" try: quality_score = 0 confidence = 0 if step_result.get('status') == 'completed': quality_score = 8 # Base score for completion # Check if all expected outputs were produced expected_outputs = task_step.get('expected_outputs', []) produced_outputs = step_result.get('outputs', []) output_coverage = len(set(produced_outputs) & set(expected_outputs)) / len(expected_outputs) if expected_outputs else 1 quality_score += output_coverage * 2 confidence = min(quality_score / 10, 1.0) return { 'score': min(quality_score, 10), 'confidence': confidence } except Exception as e: logger.error(f"Error calculating quality metrics: {str(e)}") return {'score': 0, 'confidence': 0} def _updateTaskProgress(self, task: TaskItem, current_step: int, total_steps: int): """Update task progress information""" progress = (current_step / total_steps) * 100 logger.info(f"Task progress: {progress:.1f}% ({current_step}/{total_steps})") def _calculateExecutionTime(self, started_at: str) -> float: """Calculate execution time in seconds""" try: start_time = datetime.fromisoformat(started_at.replace('Z', '+00:00')) end_time = datetime.now(UTC) return (end_time - start_time).total_seconds() except Exception: return 0.0 def _getPreviousResults(self, task: TaskItem) -> List[str]: """Get list of previous results from completed actions""" results = [] for action in task.actionList: if action.execResultLabel: results.append(action.execResultLabel) return results def _createAlternativeApproachPrompt(self, task_step: Dict[str, Any], original_review: Dict[str, Any]) -> str: """Create prompt for generating alternative approaches""" return f"""The original approach for this task step failed. Please suggest an alternative approach. TASK STEP: {task_step.get('description', 'Unknown')} ORIGINAL FAILURE: {original_review.get('reason', 'Unknown error')} MISSING OUTPUTS: {', '.join(original_review.get('missing_outputs', []))} Please provide an alternative approach that addresses these issues.""" def _parseAlternativeApproach(self, response: str) -> Optional[str]: """Parse alternative approach from AI response""" try: # Simple parsing - extract the approach description if "approach:" in response.lower(): lines = response.split('\n') for line in lines: if "approach:" in line.lower(): return line.split(":", 1)[1].strip() return None except Exception: return None def _getCurrentTimestamp(self) -> str: """Get current timestamp in ISO format""" return datetime.now(UTC).isoformat() async def workflowProcess_ORIGINAL_TEMPORARY_DEACTIVATED(self, userInput: UserInputRequest, workflow: ChatWorkflow) -> None: """Process a workflow with user input""" try: # Initialize chat manager await self.chatManager.initialize(workflow) # Set user language self.chatManager.setUserLanguage(userInput.userLanguage) # Send first message message = await self._sendFirstMessage(userInput, workflow) # Create initial task task = await self.chatManager.createInitialTask(workflow, message) # Process workflow while True: # Check if workflow is stopped self._checkWorkflowStopped(workflow) # Execute task result = await self.chatManager.executeTask(task) # Process result await self.chatManager.parseTaskResult(workflow, result) # Check if workflow should continue if not await self.chatManager.shouldContinue(workflow): break # Identify next task nextTaskResult = await self.chatManager.identifyNextTask(workflow) # Create next task task = await self.chatManager.createNextTask(workflow, nextTaskResult) if not task: break # Send last message await self._sendLastMessage(workflow) except WorkflowStoppedException: logger.info("Workflow stopped by user") except Exception as e: logger.error(f"Workflow processing error: {str(e)}") raise async def _sendFirstMessage(self, userInput: UserInputRequest, workflow: ChatWorkflow) -> ChatMessage: """Send first message to start workflow""" try: # Create initial message using interface messageData = { "workflowId": workflow.id, "role": "user", "message": userInput.prompt, "status": "first", "sequenceNr": 1, "publishedAt": datetime.now(UTC).isoformat() } # Add documents if any if userInput.listFileId: # Process file IDs and add to message data documents = await self.chatManager.processFileIds(userInput.listFileId) messageData["documents"] = documents # Create message using interface message = self.chatInterface.createWorkflowMessage(messageData) if message: workflow.messages.append(message) return message else: raise Exception("Failed to create first message") except Exception as e: logger.error(f"Error sending first message: {str(e)}") raise async def _sendLastMessage(self, workflow: ChatWorkflow) -> None: """Send last message to complete workflow""" try: # Generate feedback feedback = await self.chatManager.generateWorkflowFeedback(workflow) # Create last message using interface messageData = { "workflowId": workflow.id, "role": "assistant", "message": feedback, "status": "last", "sequenceNr": len(workflow.messages) + 1, "publishedAt": datetime.now(UTC).isoformat() } # Create message using interface message = self.chatInterface.createWorkflowMessage(messageData) if message: workflow.messages.append(message) except Exception as e: logger.error(f"Error sending last message: {str(e)}") raise async def _executeTaskStep(self, task_step: Dict[str, Any], workflow: ChatWorkflow, task: TaskItem, improvements: str = None) -> Dict[str, Any]: """Execute a single task step and generate actions""" try: # Generate actions for this task step actions = await self.chatManager.generateActionsForTask(task_step, workflow, task, improvements) # Execute actions results = [] for action in actions: action_result = await self.chatManager.executeAction(action, workflow) results.append(action_result) return { 'task_step': task_step, 'actions': actions, 'results': results, 'status': 'completed' } except Exception as e: logger.error(f"Error executing task step: {str(e)}") return { 'task_step': task_step, 'error': str(e), 'status': 'failed' }