From 08959754787cb782681bdc0689d7782c7f175218 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Mon, 25 Aug 2025 22:36:21 +0200
Subject: [PATCH] enhanced document handover with primary search over full
string and secondary search over round/task/action number only
---
modules/chat/documents/documentGeneration.py | 16 +-
modules/chat/handling/handlingTasks.py | 572 ++++++++++++-------
modules/chat/handling/promptFactory.py | 63 +-
modules/chat/managerChat.py | 7 +-
modules/chat/serviceCenter.py | 209 ++++---
modules/interfaces/interfaceAppObjects.py | 81 ++-
modules/interfaces/interfaceChatModel.py | 18 +
modules/interfaces/interfaceChatObjects.py | 24 +-
modules/methods/methodOutlook.py | 14 +-
modules/routes/routeSecurityGoogle.py | 3 +-
modules/routes/routeSecurityMsft.py | 3 +-
modules/security/tokenManager.py | 15 +-
notes/changelog.txt | 3 +-
test_documentExtraction.py | 70 +--
test_excel_processing.py | 10 +-
15 files changed, 726 insertions(+), 382 deletions(-)
diff --git a/modules/chat/documents/documentGeneration.py b/modules/chat/documents/documentGeneration.py
index 9907d0b0..32b33507 100644
--- a/modules/chat/documents/documentGeneration.py
+++ b/modules/chat/documents/documentGeneration.py
@@ -18,7 +18,8 @@ class DocumentGenerator:
def processActionResultDocuments(self, action_result, action, workflow) -> List[Dict[str, Any]]:
"""
- Main function to process documents from an action result.
+ Process documents produced by AI actions and convert them to ChatDocument format.
+ This function handles AI-generated document data, not document references.
Returns a list of processed document dictionaries.
"""
try:
@@ -31,18 +32,7 @@ class DocumentGenerator:
logger.info(f"Processing {len(documents)} documents from action_result.documents")
- # Check if documents are references (strings starting with "docItem:") or actual document objects
- if documents and isinstance(documents[0], str) and documents[0].startswith("docItem:"):
- # These are document references, resolve them to actual documents
- logger.info(f"Resolving {len(documents)} document references to actual documents")
- try:
- actual_documents = self.service.getChatDocumentsFromDocumentList(documents)
- logger.info(f"Resolved {len(actual_documents)} actual documents from references")
- documents = actual_documents
- except Exception as e:
- logger.error(f"Error resolving document references: {str(e)}")
- return []
-
+ # Process each document from the AI action result
processed_documents = []
for doc in documents:
processed_doc = self.processSingleDocument(doc, action)
diff --git a/modules/chat/handling/handlingTasks.py b/modules/chat/handling/handlingTasks.py
index 8a0bc21f..361b6fff 100644
--- a/modules/chat/handling/handlingTasks.py
+++ b/modules/chat/handling/handlingTasks.py
@@ -60,7 +60,9 @@ class HandlingTasks:
# Check workflow status before generating task plan
self._checkWorkflowStopped()
- logger.info(f"Generating task plan for workflow {workflow.id}")
+ logger.info(f"=== STARTING TASK PLAN GENERATION ===")
+ logger.info(f"Workflow ID: {workflow.id}")
+ logger.info(f"User Input: {userInput}")
available_docs = self.service.getAvailableDocuments(workflow)
# Set initial workflow context
@@ -95,12 +97,39 @@ class HandlingTasks:
is_regeneration=False,
failure_patterns=[],
failed_actions=[],
- successful_actions=[]
+ successful_actions=[],
+ criteria_progress={
+ 'met_criteria': set(),
+ 'unmet_criteria': set(),
+ 'attempt_history': []
+ }
)
- prompt = await self.service.callAiTextAdvanced(
- createTaskPlanningPrompt(task_planning_context, self.service)
- )
+ # Generate the task planning prompt
+ task_planning_prompt = createTaskPlanningPrompt(task_planning_context, self.service)
+
+ # Log the full task planning prompt being sent to AI for debugging
+ logger.info("=== TASK PLANNING PROMPT SENT TO AI ===")
+ logger.info(f"User Input: {userInput}")
+ logger.info(f"Available Documents: {len(available_docs) if available_docs else 0}")
+ logger.info("=== FULL TASK PLANNING PROMPT ===")
+ logger.info(task_planning_prompt)
+ logger.info("=== END TASK PLANNING PROMPT ===")
+
+ prompt = await self.service.callAiTextAdvanced(task_planning_prompt)
+
+ # Check if AI response is valid
+ if not prompt:
+ raise ValueError("AI service returned no response for task planning")
+
+ # Log the full AI response for task planning
+ logger.info("=== TASK PLANNING AI RESPONSE RECEIVED ===")
+ logger.info(f"Response length: {len(prompt) if prompt else 0}")
+ logger.info(f"Response preview: {prompt[:500] if prompt else 'None'}...")
+ logger.info("=== FULL TASK PLANNING AI RESPONSE ===")
+ logger.info(prompt)
+ logger.info("=== END TASK PLANNING AI RESPONSE ===")
+
# Inline _parseTaskPlanResponse logic
try:
json_start = prompt.find('{')
@@ -109,6 +138,7 @@ class HandlingTasks:
raise ValueError("No JSON found in response")
json_str = prompt[json_start:json_end]
task_plan_dict = json.loads(json_str)
+
if 'tasks' not in task_plan_dict:
raise ValueError("Task plan missing 'tasks' field")
except Exception as e:
@@ -121,12 +151,29 @@ class HandlingTasks:
logger.error(f"Parsed Task Plan: {json.dumps(task_plan_dict, indent=2)}")
raise Exception("AI-generated task plan failed validation - AI is required for task planning")
+ if not task_plan_dict.get('tasks'):
+ raise ValueError("Task plan contains no tasks")
+
tasks = []
- for task_dict in task_plan_dict.get('tasks', []):
+ for i, task_dict in enumerate(task_plan_dict.get('tasks', [])):
+ if not isinstance(task_dict, dict):
+ logger.warning(f"Skipping invalid task {i+1}: not a dictionary")
+ continue
+
# Map old 'description' field to new 'objective' field
if 'description' in task_dict and 'objective' not in task_dict:
task_dict['objective'] = task_dict.pop('description')
- tasks.append(TaskStep(**task_dict))
+
+ try:
+ task = TaskStep(**task_dict)
+ tasks.append(task)
+ except Exception as e:
+ logger.warning(f"Skipping invalid task {i+1}: {str(e)}")
+ continue
+
+ if not tasks:
+ raise ValueError("No valid tasks could be created from AI response")
+
task_plan = TaskPlan(
overview=task_plan_dict.get('overview', ''),
tasks=tasks
@@ -134,26 +181,13 @@ class HandlingTasks:
# Set workflow totals for progress tracking
total_tasks = len(tasks)
+ if total_tasks == 0:
+ raise ValueError("Task plan contains no valid tasks")
+
self.service.setWorkflowTotals(total_tasks=total_tasks)
logger.info(f"Task plan generated successfully with {len(tasks)} tasks")
- # Log the generated tasks
- for i, task in enumerate(tasks):
- logger.info(f" Task {i+1}: {task.objective}")
- if task.success_criteria:
- logger.info(f" Success criteria: {task.success_criteria}")
-
- # Log the complete task plan
- logger.info("=== GENERATED TASK PLAN ===")
- logger.info(f"Overview: {task_plan.overview}")
- logger.info(f"Total tasks: {len(tasks)}")
-
- # Log the RAW AI-generated task plan JSON for debugging
- logger.info("=== RAW AI TASK PLAN JSON ===")
- logger.info(f"AI Response with task plan: {prompt}")
- logger.info("=== END RAW AI TASK PLAN JSON ===")
-
# PHASE 3: Create chat message containing the task plan
await self.createTaskPlanMessage(task_plan, workflow)
@@ -212,135 +246,45 @@ class HandlingTasks:
logger.error(f"Error creating task plan message: {str(e)}")
async def createDocumentContextMessage(self, documents: List, workflow):
- """Create a chat message with enhanced document context and workflow labeling"""
+ """Create a chat message with document context and workflow labeling"""
try:
- from .promptFactory import createDocumentContextPrompt
-
- # Get user language from service
- user_language = self.service.user.language if self.service and self.service.user else 'en'
-
# Get current workflow context and stats
workflow_context = self.service.getWorkflowContext()
workflow_stats = self.service.getWorkflowStats()
- # Build context for the document context prompt
- context = {
- 'documents': documents,
- 'workflow_context': {
- 'currentRound': workflow_context.get('currentRound', 1),
- 'totalTasks': workflow_stats.get('totalTasks', 0),
- 'currentTask': workflow_context.get('currentTask', 0),
- 'totalActions': workflow_stats.get('totalActions', 0),
- 'currentAction': workflow_context.get('currentAction', 0),
- 'workflowStatus': workflow_stats.get('workflowStatus', 'unknown'),
- 'workflowId': workflow_stats.get('workflowId', 'unknown')
- },
- 'user_language': user_language
- }
+ # Create a simple document context message without AI dependency
+ message_text = f"📄 **Document Context**\n\n"
+ message_text += f"**Total Documents:** {len(documents)}\n\n"
- # Generate enhanced document context using AI
- prompt = createDocumentContextPrompt(context)
- response = await self.service.callAiTextAdvanced(prompt)
+ # Add workflow context information
+ current_round = workflow_context.get('currentRound', 1)
+ current_task = workflow_context.get('currentTask', 0)
+ total_tasks = workflow_stats.get('totalTasks', 0)
+ current_action = workflow_context.get('currentAction', 0)
+ total_actions = workflow_stats.get('totalActions', 0)
- # Parse the AI response
- try:
- json_start = response.find('{')
- json_end = response.find('}') + 1
- if json_start != -1 and json_end > 0:
- json_str = response[json_start:json_end]
- doc_context = json.loads(json_str)
-
- # Build message from AI response
- message_text = f"📄 **Document Context**\n\n"
- message_text += f"**Summary:** {doc_context.get('documentSummary', 'No summary available')}\n\n"
- message_text += f"**Workflow Progress:** {doc_context.get('workflowProgress', 'No progress info')}\n\n"
-
- # Add workflow context information
- current_round = workflow_context.get('currentRound', 1)
- current_task = workflow_context.get('currentTask', 0)
- total_tasks = workflow_stats.get('totalTasks', 0)
- current_action = workflow_context.get('currentAction', 0)
- total_actions = workflow_stats.get('totalActions', 0)
-
- message_text += f"**Workflow Context:**\n"
- message_text += f"- Round: {current_round}\n"
- if total_tasks > 0:
- message_text += f"- Task: {current_task}/{total_tasks}\n"
- else:
- message_text += f"- Task: {current_task}\n"
- if total_actions > 0:
- message_text += f"- Action: {current_action}/{total_actions}\n"
- else:
- message_text += f"- Action: {current_action}\n"
- message_text += f"- Status: {workflow_stats.get('workflowStatus', 'unknown')}\n\n"
-
- # Add overall user message if available
- overall_message = doc_context.get('overallUserMessage')
- if overall_message:
- message_text += f"💬 {overall_message}\n\n"
-
- # Add document details
- document_details = doc_context.get('documentDetails', [])
- if document_details:
- message_text += "**Document Details:**\n"
- for doc_detail in document_details:
- message_text += f"- {doc_detail.get('workflowLabel', 'Unknown')}: {doc_detail.get('fileName', 'Unknown file')}\n"
- user_msg = doc_detail.get('userMessage')
- if user_msg:
- message_text += f" 💬 {user_msg}\n"
- message_text += "\n"
- else:
- # Fallback if AI response parsing fails
- message_text = f"📄 **Document Context**\n\n"
- message_text += f"**Total Documents:** {len(documents)}\n\n"
-
- # Add workflow context information even in fallback
- current_round = workflow_context.get('currentRound', 1)
- current_task = workflow_context.get('currentTask', 0)
- total_tasks = workflow_stats.get('totalTasks', 0)
- current_action = workflow_context.get('currentAction', 0)
- total_actions = workflow_stats.get('totalActions', 0)
-
- message_text += f"**Workflow Context:**\n"
- message_text += f"- Round: {current_round}\n"
- if total_tasks > 0:
- message_text += f"- Task: {current_task}/{total_tasks}\n"
- else:
- message_text += f"- Task: {current_task}\n"
- if total_actions > 0:
- message_text += f"- Action: {current_action}/{total_actions}\n"
- else:
- message_text += f"- Action: {current_action}\n"
- message_text += f"- Status: {workflow_stats.get('workflowStatus', 'unknown')}\n\n"
-
- message_text += "Document context information is available for processing."
-
- except Exception as e:
- logger.error(f"Error parsing document context AI response: {str(e)}")
- # Fallback message with workflow context
- message_text = f"📄 **Document Context**\n\n"
- message_text += f"**Total Documents:** {len(documents)}\n\n"
-
- # Add workflow context information in fallback
- current_round = workflow_context.get('currentRound', 1)
- current_task = workflow_context.get('currentTask', 0)
- total_tasks = workflow_stats.get('totalTasks', 0)
- current_action = workflow_context.get('currentAction', 0)
- total_actions = workflow_stats.get('totalActions', 0)
-
- message_text += f"**Workflow Context:**\n"
- message_text += f"- Round: {current_round}\n"
- if total_tasks > 0:
- message_text += f"- Task: {current_task}/{total_tasks}\n"
- else:
- message_text += f"- Task: {current_task}\n"
- if total_actions > 0:
- message_text += f"- Action: {current_action}/{total_actions}\n"
- else:
- message_text += f"- Action: {current_action}\n"
- message_text += f"- Status: {workflow_stats.get('workflowStatus', 'unknown')}\n\n"
-
- message_text += "Document context information is available for processing."
+ message_text += f"**Workflow Context:**\n"
+ message_text += f"- Round: {current_round}\n"
+ if total_tasks > 0:
+ message_text += f"- Task: {current_task}/{total_tasks}\n"
+ else:
+ message_text += f"- Task: {current_task}\n"
+ if total_actions > 0:
+ message_text += f"- Action: {current_action}/{total_actions}\n"
+ else:
+ message_text += f"- Action: {current_action}\n"
+ message_text += f"- Status: {workflow_stats.get('workflowStatus', 'unknown')}\n\n"
+
+ # Add document list
+ if documents:
+ message_text += "**Available Documents:**\n"
+ for i, doc in enumerate(documents[:5]): # Show first 5 documents
+ message_text += f"- {doc.fileName if hasattr(doc, 'fileName') else f'Document {i+1}'}\n"
+ if len(documents) > 5:
+ message_text += f"- ... and {len(documents) - 5} more documents\n"
+ message_text += "\n"
+
+ message_text += "Document context information is available for processing."
# Create workflow message
message_data = {
@@ -351,7 +295,7 @@ class HandlingTasks:
"sequenceNr": len(workflow.messages) + 1,
"publishedAt": get_utc_timestamp(),
"documentsLabel": "document_context",
- "documents": documents,
+ "documents": [], # Empty documents for context message
# Add workflow context fields
"roundNumber": workflow_context.get('currentRound', 1),
"taskNumber": workflow_context.get('currentTask', 0),
@@ -374,11 +318,61 @@ class HandlingTasks:
# Check workflow status before generating actions
self._checkWorkflowStopped()
- logger.info(f"Generating actions for task: {task_step.objective}")
+ retry_info = f" (Retry #{enhanced_context.retry_count})" if enhanced_context and enhanced_context.retry_count > 0 else ""
+ logger.info(f"Generating actions for task: {task_step.objective}{retry_info}")
+
+ # Log criteria progress if this is a retry
+ if enhanced_context and hasattr(enhanced_context, 'criteria_progress') and enhanced_context.criteria_progress is not None:
+ progress = enhanced_context.criteria_progress
+ logger.info(f"Retry attempt {enhanced_context.retry_count} - Criteria progress:")
+ if progress.get('met_criteria'):
+ logger.info(f" Met criteria: {', '.join(progress['met_criteria'])}")
+ if progress.get('unmet_criteria'):
+ logger.warning(f" Unmet criteria: {', '.join(progress['unmet_criteria'])}")
+
+ # Show improvement trends
+ if progress.get('attempt_history'):
+ recent_attempts = progress['attempt_history'][-2:] # Last 2 attempts
+ if len(recent_attempts) >= 2:
+ prev_score = recent_attempts[0].get('quality_score', 0)
+ curr_score = recent_attempts[1].get('quality_score', 0)
+ if curr_score > prev_score:
+ logger.info(f" Quality improving: {prev_score} -> {curr_score}")
+ elif curr_score < prev_score:
+ logger.warning(f" Quality declining: {prev_score} -> {curr_score}")
+ else:
+ logger.info(f" Quality stable: {curr_score}")
+
+ # Enhanced retry context logging
+ if enhanced_context and enhanced_context.retry_count > 0:
+ logger.info("=== RETRY CONTEXT FOR ACTION GENERATION ===")
+ logger.info(f"Retry Count: {enhanced_context.retry_count}")
+ logger.info(f"Previous Improvements: {enhanced_context.improvements}")
+ logger.info(f"Previous Review Result: {enhanced_context.previous_review_result}")
+ logger.info(f"Failure Patterns: {enhanced_context.failure_patterns}")
+ logger.info(f"Failed Actions: {enhanced_context.failed_actions}")
+ logger.info(f"Successful Actions: {enhanced_context.successful_actions}")
+ logger.info("=== END RETRY CONTEXT ===")
available_docs = self.service.getAvailableDocuments(workflow)
available_connections = self.service.getConnectionReferenceList()
+ # Log available resources for debugging
+ logger.info("=== AVAILABLE RESOURCES FOR ACTION GENERATION ===")
+ logger.info(f"Available Documents: {len(available_docs) if available_docs else 0}")
+ if available_docs:
+ for i, doc in enumerate(available_docs[:5]): # Show first 5
+ logger.info(f" Doc {i+1}: {doc}")
+ if len(available_docs) > 5:
+ logger.info(f" ... and {len(available_docs) - 5} more documents")
+ logger.info(f"Available Connections: {len(available_connections) if available_connections else 0}")
+ if available_connections:
+ for i, conn in enumerate(available_connections[:5]): # Show first 5
+ logger.info(f" Conn {i+1}: {conn}")
+ if len(available_connections) > 5:
+ logger.info(f" ... and {len(available_connections) - 5} more connections")
+ logger.info("=== END AVAILABLE RESOURCES ===")
+
# Create proper context object for action definition
if enhanced_context and isinstance(enhanced_context, TaskContext):
# Use existing TaskContext if provided
@@ -397,7 +391,8 @@ class HandlingTasks:
is_regeneration=enhanced_context.is_regeneration or False,
failure_patterns=enhanced_context.failure_patterns or [],
failed_actions=enhanced_context.failed_actions or [],
- successful_actions=enhanced_context.successful_actions or []
+ successful_actions=enhanced_context.successful_actions or [],
+ criteria_progress=enhanced_context.criteria_progress
)
else:
# Create new context from scratch
@@ -416,66 +411,106 @@ class HandlingTasks:
is_regeneration=False,
failure_patterns=[],
failed_actions=[],
- successful_actions=[]
+ successful_actions=[],
+ criteria_progress=None
)
# Check workflow status before calling AI service
self._checkWorkflowStopped()
- prompt = await self.service.callAiTextAdvanced(
- await createActionDefinitionPrompt(action_context, self.service)
- )
+ # Log the final action context being sent to AI
+ logger.info("=== FINAL ACTION CONTEXT FOR AI ===")
+ logger.info(f"Task Step ID: {action_context.task_step.id if action_context.task_step else 'None'}")
+ logger.info(f"Task Step Objective: {action_context.task_step.objective if action_context.task_step else 'None'}")
+ logger.info(f"Workflow ID: {action_context.workflow_id}")
+ logger.info(f"Available Documents Count: {len(action_context.available_documents) if action_context.available_documents else 0}")
+ logger.info(f"Available Connections Count: {len(action_context.available_connections) if action_context.available_connections else 0}")
+ logger.info(f"Previous Results Count: {len(action_context.previous_results) if action_context.previous_results else 0}")
+ logger.info(f"Retry Count: {action_context.retry_count}")
+ logger.info(f"Is Regeneration: {action_context.is_regeneration}")
+ logger.info("=== END ACTION CONTEXT ===")
+
+ # Generate the action definition prompt
+ action_prompt = await createActionDefinitionPrompt(action_context, self.service)
+
+ # Log the full prompt being sent to AI for debugging
+ logger.info("=== ACTION DEFINITION PROMPT SENT TO AI ===")
+ logger.info(f"Task: {task_step.objective}")
+ logger.info(f"Retry Count: {action_context.retry_count}")
+ logger.info(f"Previous Results: {action_context.previous_results}")
+ logger.info(f"Improvements: {action_context.improvements}")
+ logger.info(f"Previous Review Result: {action_context.previous_review_result}")
+ logger.info(f"Criteria Progress: {action_context.criteria_progress}")
+ logger.info("=== FULL PROMPT ===")
+ logger.info(action_prompt)
+ logger.info("=== END PROMPT ===")
+
+ prompt = await self.service.callAiTextAdvanced(action_prompt)
+
+ # Check if AI response is valid
+ if not prompt:
+ raise ValueError("AI service returned no response")
+
+ # Log the full AI response for debugging
+ logger.info("=== FULL AI RESPONSE ===")
+ logger.info(prompt)
+ logger.info("=== END AI RESPONSE ===")
+
# Inline parseActionResponse logic here
json_start = prompt.find('{')
json_end = prompt.rfind('}') + 1
if json_start == -1 or json_end == 0:
raise ValueError("No JSON found in response")
json_str = prompt[json_start:json_end]
+
try:
action_data = json.loads(json_str)
except Exception as e:
logger.error(f"Error parsing action response JSON: {str(e)}")
action_data = {}
+
if 'actions' not in action_data:
raise ValueError("Action response missing 'actions' field")
+
actions = action_data['actions']
+ if not actions:
+ raise ValueError("Action response contains empty actions list")
+
+ if not isinstance(actions, list):
+ raise ValueError(f"Action response 'actions' field is not a list: {type(actions)}")
+
if not self._validateActions(actions, action_context):
logger.error("Generated actions failed validation")
raise Exception("AI-generated actions failed validation - AI is required for action generation")
# Convert to TaskAction objects
- task_actions = [self.createTaskAction({
- "execMethod": a.get('method', 'unknown'),
- "execAction": a.get('action', 'unknown'),
- "execParameters": a.get('parameters', {}),
- "execResultLabel": a.get('resultLabel', ''),
- "expectedDocumentFormats": a.get('expectedDocumentFormats', None),
- "status": TaskStatus.PENDING,
- # Extract user-friendly message if available
- "userMessage": a.get('userMessage', None)
- }) for a in actions]
+ task_actions = []
+ for i, a in enumerate(actions):
+ if not isinstance(a, dict):
+ logger.warning(f"Skipping invalid action {i+1}: not a dictionary")
+ continue
+
+ task_action = self.createTaskAction({
+ "execMethod": a.get('method', 'unknown'),
+ "execAction": a.get('action', 'unknown'),
+ "execParameters": a.get('parameters', {}),
+ "execResultLabel": a.get('resultLabel', ''),
+ "expectedDocumentFormats": a.get('expectedDocumentFormats', None),
+ "status": TaskStatus.PENDING,
+ # Extract user-friendly message if available
+ "userMessage": a.get('userMessage', None)
+ })
+
+ if task_action:
+ task_actions.append(task_action)
+ else:
+ logger.warning(f"Skipping invalid action {i+1}: failed to create TaskAction")
valid_actions = [ta for ta in task_actions if ta]
- logger.info(f"Generated {len(valid_actions)} actions for task: {task_step.objective}")
-
- # Log the generated actions
- for i, action in enumerate(valid_actions):
- logger.info(f" Action {i+1}: {action.execMethod}.{action.execAction}")
- if action.expectedDocumentFormats:
- logger.info(f" Expected formats: {action.expectedDocumentFormats}")
- if action.execParameters.get('documentList'):
- logger.info(f" Input documents: {action.execParameters['documentList']}")
-
- # Log the complete action plan
- logger.info("=== GENERATED ACTION PLAN ===")
- logger.info(f"Task: {task_step.objective}")
- logger.info(f"Total actions: {len(valid_actions)}")
-
- # Log the RAW AI-generated action plan JSON for debugging
- logger.info("=== RAW AI ACTION PLAN JSON ===")
- logger.info(f"AI Response with parsed actions: {prompt}")
- logger.info("=== END RAW AI ACTION PLAN JSON ===")
+ if not valid_actions:
+ raise ValueError("No valid actions could be created from AI response")
+
return valid_actions
except Exception as e:
logger.error(f"Error in generateTaskActions: {str(e)}")
@@ -488,7 +523,7 @@ class HandlingTasks:
# Update workflow context for this task
if task_index is not None:
self.service.setWorkflowContext(task_number=task_index)
- self.service.incrementWorkflowContext('task')
+ # Remove the increment call that causes double-increment bug
# Create database log entry for task start in format expected by frontend
if task_index is not None:
@@ -569,7 +604,7 @@ class HandlingTasks:
# Update workflow context for this action
action_number = action_idx + 1
self.service.setWorkflowContext(action_number=action_number)
- self.service.incrementWorkflowContext('action')
+ # Remove the increment call that causes double-increment bug
# Log action start in format expected by frontend
logger.info(f"Task {task_index} - Starting action {action_number}/{total_actions}")
@@ -643,10 +678,23 @@ class HandlingTasks:
# Create a task completion message for the user
task_progress = f"{task_index}/{total_tasks}" if total_tasks is not None else str(task_index)
+
+ # Enhanced completion message with criteria details
+ completion_message = f"🎯 Task {task_progress} Completed Successfully!\n\nObjective: {task_step.objective}\n\nFeedback: {feedback or 'Task completed successfully'}"
+
+ # Add criteria status if available
+ if hasattr(review_result, 'met_criteria') and review_result.met_criteria:
+ completion_message += f"\n\n✅ **Success Criteria Met:**\n"
+ for criterion in review_result.met_criteria:
+ completion_message += f"• {criterion}\n"
+
+ if hasattr(review_result, 'quality_score'):
+ completion_message += f"\n📊 **Quality Score:** {review_result.quality_score}/10"
+
task_completion_message = {
"workflowId": workflow.id,
"role": "assistant",
- "message": f"🎯 Task {task_progress} Completed Successfully!\n\nObjective: {task_step.objective}\n\nFeedback: {feedback or 'Task completed successfully'}",
+ "message": completion_message,
"status": "step",
"sequenceNr": len(workflow.messages) + 1,
"publishedAt": get_utc_timestamp(),
@@ -674,11 +722,19 @@ class HandlingTasks:
feedback=feedback,
error=None
)
+
elif review_result.status == 'retry' and state.canRetry():
logger.warning(f"Task step '{task_step.objective}' requires retry: {review_result.improvements}")
+
+ # Enhanced logging of criteria status
+ if review_result.met_criteria:
+ logger.info(f"Met criteria: {', '.join(review_result.met_criteria)}")
+ if review_result.unmet_criteria:
+ logger.warning(f"Unmet criteria: {', '.join(review_result.unmet_criteria)}")
+
state.incrementRetryCount()
- # Update retry context with retry information
+ # Update retry context with retry information and criteria tracking
if retry_context:
retry_context.retry_count = state.retry_count
retry_context.improvements = review_result.improvements
@@ -688,6 +744,47 @@ class HandlingTasks:
retry_context.failure_patterns = state.getFailurePatterns()
retry_context.failed_actions = state.failed_actions
retry_context.successful_actions = state.successful_actions
+
+ # Track criteria progress across retries
+ if not hasattr(retry_context, 'criteria_progress'):
+ retry_context.criteria_progress = {
+ 'met_criteria': set(),
+ 'unmet_criteria': set(),
+ 'attempt_history': []
+ }
+
+ # Update criteria progress - convert lists to sets for deduplication
+ if review_result.met_criteria:
+ retry_context.criteria_progress['met_criteria'].update(review_result.met_criteria)
+ if review_result.unmet_criteria:
+ retry_context.criteria_progress['unmet_criteria'].update(review_result.unmet_criteria)
+
+ # Record this attempt's criteria status
+ attempt_record = {
+ 'attempt': state.retry_count,
+ 'met_criteria': review_result.met_criteria or [],
+ 'unmet_criteria': review_result.unmet_criteria or [],
+ 'quality_score': review_result.quality_score,
+ 'improvements': review_result.improvements or []
+ }
+ retry_context.criteria_progress['attempt_history'].append(attempt_record)
+
+ logger.info(f"Criteria progress after {state.retry_count} attempts:")
+ logger.info(f" Total met: {len(retry_context.criteria_progress['met_criteria'])}")
+ logger.info(f" Total unmet: {len(retry_context.criteria_progress['unmet_criteria'])}")
+ if retry_context.criteria_progress['met_criteria']:
+ logger.info(f" Met criteria: {', '.join(retry_context.criteria_progress['met_criteria'])}")
+ if retry_context.criteria_progress['unmet_criteria']:
+ logger.info(f" Unmet criteria: {', '.join(retry_context.criteria_progress['unmet_criteria'])}")
+
+ # Log retry summary for debugging
+ logger.info(f"=== RETRY #{state.retry_count} SUMMARY ===")
+ logger.info(f"Task: {task_step.objective}")
+ logger.info(f"Quality Score: {review_result.quality_score}/10")
+ logger.info(f"Status: {review_result.status}")
+ logger.info(f"Improvements Needed: {review_result.improvements}")
+ logger.info(f"Reason: {review_result.reason}")
+ logger.info("=== END RETRY SUMMARY ===")
continue
else:
@@ -698,8 +795,18 @@ class HandlingTasks:
error_message += f"Objective: {task_step.objective}\n\n"
# Add specific error details if available
- if error:
- error_message += f"Error: {error}\n\n"
+ if review_result and hasattr(review_result, 'reason') and review_result.reason:
+ error_message += f"Reason: {review_result.reason}\n\n"
+
+ # Add criteria progress information if available
+ if retry_context and hasattr(retry_context, 'criteria_progress'):
+ progress = retry_context.criteria_progress
+ error_message += f"📊 **Progress Summary:**\n"
+ if progress.get('met_criteria'):
+ error_message += f"✅ Met criteria: {', '.join(progress['met_criteria'])}\n"
+ if progress.get('unmet_criteria'):
+ error_message += f"❌ Unmet criteria: {', '.join(progress['unmet_criteria'])}\n"
+ error_message += "\n"
# Add retry information
error_message += f"Attempts: {attempt+1}\n"
@@ -733,14 +840,14 @@ class HandlingTasks:
else:
logger.error(f"Failed to create user-facing retry message for failed task: {task_step.objective}")
except Exception as e:
- logger.error(f"Error creating user-facing retry message: {str(e)}")
+ logger.error(f"Error creating user-facing retry message: {str(e)}")
return TaskResult(
taskId=task_step.id,
status=TaskStatus.FAILED,
success=False,
feedback=feedback,
- error=error
+ error=review_result.reason if review_result and hasattr(review_result, 'reason') else "Task failed after retry attempts"
)
logger.error(f"=== TASK {task_index or '?'} FAILED AFTER ALL RETRIES: {task_step.objective} ===")
@@ -749,8 +856,10 @@ class HandlingTasks:
error_message += f"Objective: {task_step.objective}\n\n"
# Add specific error details if available
- if error and error != "Task failed after all retries.":
- error_message += f"Error: {error}\n\n"
+ if retry_context and hasattr(retry_context, 'previous_review_result') and retry_context.previous_review_result:
+ reason = retry_context.previous_review_result.get('reason', '')
+ if reason and reason != "Task failed after all retries.":
+ error_message += f"Reason: {reason}\n\n"
# Add retry information
error_message += f"Retries attempted: {retry_context.retry_count if retry_context else 'Unknown'}\n"
@@ -799,6 +908,11 @@ class HandlingTasks:
# Check workflow status before reviewing task completion
self._checkWorkflowStopped()
+ logger.info(f"=== STARTING TASK COMPLETION REVIEW ===")
+ logger.info(f"Task: {task_step.objective}")
+ logger.info(f"Actions executed: {len(task_actions) if task_actions else 0}")
+ logger.info(f"Action results: {len(action_results) if action_results else 0}")
+
# Create proper context object for result review
review_context = ReviewContext(
task_step=task_step,
@@ -827,13 +941,32 @@ class HandlingTasks:
# Use promptFactory for review prompt
prompt = createResultReviewPrompt(review_context, self.service)
+
+ # Log the full result review prompt being sent to AI for debugging
+ logger.info("=== RESULT REVIEW PROMPT SENT TO AI ===")
+ logger.info(f"Task: {task_step.objective}")
+ logger.info(f"Action Results Count: {len(review_context.action_results) if review_context.action_results else 0}")
+ logger.info(f"Task Actions Count: {len(review_context.task_actions) if review_context.task_actions else 0}")
+ logger.info("=== FULL RESULT REVIEW PROMPT ===")
+ logger.info(prompt)
+ logger.info("=== END RESULT REVIEW PROMPT ===")
+
response = await self.service.callAiTextAdvanced(prompt)
+
+ # Log the full AI response for result review
+ logger.info("=== RESULT REVIEW AI RESPONSE RECEIVED ===")
+ logger.info(f"Response length: {len(response) if response else 0}")
+ logger.info("=== FULL RESULT REVIEW AI RESPONSE ===")
+ logger.info(response)
+ logger.info("=== END RESULT REVIEW AI RESPONSE ===")
+
# Inline parseReviewResponse logic here
json_start = response.find('{')
json_end = response.rfind('}') + 1
if json_start == -1 or json_end == 0:
raise ValueError("No JSON found in review response")
json_str = response[json_start:json_end]
+
try:
review = json.loads(json_str)
except Exception as e:
@@ -888,6 +1021,12 @@ class HandlingTasks:
else:
logger.error(f"VALIDATION FAILED - Task failed: {review_result.reason}")
+ logger.info(f"=== TASK COMPLETION REVIEW FINISHED ===")
+ logger.info(f"Final Status: {review_result.status}")
+ logger.info(f"Quality Score: {review_result.quality_score}/10")
+ logger.info(f"Improvements: {review_result.improvements}")
+ logger.info("=== END REVIEW ===")
+
return review_result
except Exception as e:
logger.error(f"Error in reviewTaskCompletion: {str(e)}")
@@ -897,20 +1036,33 @@ class HandlingTasks:
quality_score=0
)
- async def prepareTaskHandover(self, task_step, task_actions, review_result, workflow):
+ async def prepareTaskHandover(self, task_step, task_actions, task_result, workflow):
try:
# Check workflow status before preparing task handover
self._checkWorkflowStopped()
# Log handover status summary
- status = review_result.status if review_result else 'unknown'
- met = review_result.met_criteria if review_result and review_result.met_criteria else []
+ status = task_result.status if task_result else 'unknown'
+
+ # Handle both TaskResult and ReviewResult objects
+ if hasattr(task_result, 'met_criteria'):
+ # This is a ReviewResult object
+ met = task_result.met_criteria if task_result.met_criteria else []
+ review_result = task_result.to_dict()
+ else:
+ # This is a TaskResult object
+ met = []
+ review_result = {
+ 'status': task_result.status if task_result else 'unknown',
+ 'reason': task_result.error if task_result and hasattr(task_result, 'error') else None,
+ 'success': task_result.success if task_result else False
+ }
handover_data = {
'task_id': task_step.id,
'task_description': task_step.objective,
'actions': [action.to_dict() for action in task_actions],
- 'review_result': review_result.to_dict(),
+ 'review_result': review_result,
'workflow_id': workflow.id,
'handover_time': get_utc_timestamp()
}
@@ -1029,7 +1181,7 @@ class HandlingTasks:
await self.createActionMessage(action, result, workflow, message_result_label, created_documents, task_step, task_index)
# Log action results
- logger.info(f"✓ Action completed successfully")
+ logger.info(f"Action completed successfully")
# Create database log entry for action completion
if total_actions is not None:
@@ -1060,7 +1212,7 @@ class HandlingTasks:
logger.info("Output: No documents created")
else:
action.setError(result.error or "Action execution failed")
- logger.error(f"✗ Action failed: {result.error}")
+ logger.error(f"Action failed: {result.error}")
# ⚠️ IMPORTANT: Create error message for failed actions so user can see what went wrong
await self.createActionMessage(action, result, workflow, result_label, [], task_step, task_index)
@@ -1114,10 +1266,6 @@ class HandlingTasks:
if result_label is None:
result_label = action.execResultLabel
- # Use provided documents or process them if not provided
- if created_documents is None:
- created_documents = self.documentGenerator.createDocumentsFromActionResult(result, action, workflow)
-
# Log delivered documents
if created_documents:
logger.info(f"Result label: {result_label} - {len(created_documents)} documents")
@@ -1226,10 +1374,10 @@ class HandlingTasks:
"actionName": action.execAction,
"documentsLabel": result_label,
"documents": created_documents,
- # Add workflow context fields
+ # Add workflow context fields - extract from result_label to match document reference
"roundNumber": workflow_context.get('currentRound', 1),
"taskNumber": task_index,
- "actionNumber": workflow_context.get('currentAction', 0)
+ "actionNumber": self._extractActionNumberFromLabel(result_label) if result_label else workflow_context.get('currentAction', 0)
}
# Add user-friendly message if available
@@ -1314,6 +1462,24 @@ class HandlingTasks:
logger.error(f"Error validating task plan: {str(e)}")
return False
+ def _extractActionNumberFromLabel(self, label: str) -> int:
+ """Extract action number from a document label like 'round1_task1_action1_diagram_analysis'"""
+ try:
+ if not label or not isinstance(label, str):
+ return 0
+
+ # Parse label format: round{round}_task{task}_action{action}_{context}
+ if '_action' in label:
+ action_part = label.split('_action')[1]
+ if action_part and '_' in action_part:
+ action_number = action_part.split('_')[0]
+ return int(action_number)
+
+ return 0
+ except Exception as e:
+ logger.warning(f"Could not extract action number from label '{label}': {str(e)}")
+ return 0
+
def _validateActions(self, actions: List[Dict[str, Any]], context) -> bool:
try:
if not isinstance(actions, list):
diff --git a/modules/chat/handling/promptFactory.py b/modules/chat/handling/promptFactory.py
index 1bf55c9b..71569556 100644
--- a/modules/chat/handling/promptFactory.py
+++ b/modules/chat/handling/promptFactory.py
@@ -152,18 +152,19 @@ Previous review feedback:
You are an action generation AI that creates specific actions to accomplish a task step with user-friendly messages.
DOCUMENT REFERENCE TYPES:
-- docItem: Reference to a single document. Format: "docItem::