gateway/modules/workflow/managerWorkflow.py
2025-07-04 15:10:26 +02:00

535 lines
No EOL
24 KiB
Python

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'
}