535 lines
No EOL
24 KiB
Python
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'
|
|
} |