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::