""" Context-aware placeholder service for different workflow phases. This module provides different levels of context based on the workflow phase. """ import json import logging from typing import Dict, Any, Optional from enum import Enum logger = logging.getLogger(__name__) class WorkflowPhase(Enum): """Different phases of workflow execution requiring different context levels.""" TASK_PLANNING = "task_planning" # Needs full context for planning REACT_PLAN_SELECTION = "react_plan_selection" # Needs minimal context for action selection REACT_PARAMETERS = "react_parameters" # Needs full context for parameter generation ACTION_PLANNING = "action_planning" # Needs full context for action planning RESULT_REVIEW = "result_review" # Needs full context for review class ContextAwarePlaceholders: """Context-aware placeholder service that provides different context levels based on workflow phase.""" def __init__(self, services): self.services = services async def getPlaceholders(self, phase: WorkflowPhase, context: Any, additional_data: Dict[str, Any] = None) -> Dict[str, str]: """ Get placeholders based on workflow phase and context. Args: phase: The workflow phase determining context level context: The workflow context object additional_data: Additional data for specific phases (e.g., selected action) Returns: Dictionary of placeholder key-value pairs """ if phase == WorkflowPhase.TASK_PLANNING: return self._getTaskPlanningPlaceholders(context) elif phase == WorkflowPhase.REACT_PLAN_SELECTION: return self._getReactPlanSelectionPlaceholders(context) elif phase == WorkflowPhase.REACT_PARAMETERS: return await self._getReactParametersPlaceholders(context, additional_data) elif phase == WorkflowPhase.ACTION_PLANNING: return self._getActionPlanningPlaceholders(context) elif phase == WorkflowPhase.RESULT_REVIEW: return self._getResultReviewPlaceholders(context) else: logger.warning(f"Unknown workflow phase: {phase}") return self._getMinimalPlaceholders(context) def _getTaskPlanningPlaceholders(self, context: Any) -> Dict[str, str]: """Get full context placeholders for task planning.""" return { "USER_PROMPT": self._extractUserPrompt(context), "AVAILABLE_DOCUMENTS": self._getFullDocumentContext(context), "WORKFLOW_HISTORY": self._getWorkflowHistory(context), "USER_LANGUAGE": self._extractUserLanguage(), } def _getReactPlanSelectionPlaceholders(self, context: Any) -> Dict[str, str]: """Get minimal context placeholders for React plan selection.""" return { "USER_PROMPT": self._extractUserPrompt(context), "AVAILABLE_DOCUMENTS": self._getMinimalDocumentContext(context), "USER_LANGUAGE": self._extractUserLanguage(), "AVAILABLE_METHODS": self._getAvailableMethods(), "AVAILABLE_CONNECTIONS": self._getMinimalConnectionContext(), } async def _getReactParametersPlaceholders(self, context: Any, additional_data: Dict[str, Any] = None) -> Dict[str, str]: """Get full context placeholders for React parameter generation.""" # Get both original user prompt and current task objective original_prompt = self._extractUserPrompt(context) current_task = "" if hasattr(context, 'task_step') and context.task_step and context.task_step.objective: current_task = context.task_step.objective # Combine original prompt and current task for better context combined_prompt = f"Original request: {original_prompt}" if current_task and current_task != original_prompt: combined_prompt += f"\n\nCurrent task: {current_task}" # Generate intelligent action objective action_objective = await self._generateActionObjective(context, current_task, original_prompt, additional_data) placeholders = { "USER_PROMPT": combined_prompt, "ACTION_OBJECTIVE": action_objective, # AI-generated intelligent objective "AVAILABLE_DOCUMENTS": self._getFullDocumentContext(context), "USER_LANGUAGE": self._extractUserLanguage(), "AVAILABLE_CONNECTIONS": self._getFullConnectionContext(), "PREVIOUS_ACTION_RESULTS": self._getPreviousActionResults(context), "LEARNINGS_AND_IMPROVEMENTS": self._getLearningsAndImprovements(context), "LATEST_REFINEMENT_FEEDBACK": self._getLatestRefinementFeedback(context), } # Add additional data if provided (e.g., selected action, action signature) if additional_data: placeholders.update(additional_data) return placeholders def _getActionPlanningPlaceholders(self, context: Any) -> Dict[str, str]: """Get full context placeholders for action planning.""" return { "USER_PROMPT": self._extractUserPrompt(context), "AVAILABLE_DOCUMENTS": self._getFullDocumentContext(context), "WORKFLOW_HISTORY": self._getWorkflowHistory(context), "AVAILABLE_METHODS": self._getAvailableMethods(), "AVAILABLE_CONNECTIONS": self._getFullConnectionContext(), "USER_LANGUAGE": self._extractUserLanguage(), } def _getResultReviewPlaceholders(self, context: Any) -> Dict[str, str]: """Get full context placeholders for result review.""" return { "USER_PROMPT": self._extractUserPrompt(context), "REVIEW_CONTENT": self._getReviewContent(context), } def _getMinimalPlaceholders(self, context: Any) -> Dict[str, str]: """Get minimal placeholders as fallback.""" return { "USER_PROMPT": self._extractUserPrompt(context), "USER_LANGUAGE": self._extractUserLanguage(), } # Helper methods for extracting different context levels def _extractUserPrompt(self, context: Any) -> str: """Extract user prompt from context.""" # Get the current user prompt from services (clean and reliable) if self.services and hasattr(self.services, 'currentUserPrompt') and self.services.currentUserPrompt: return self.services.currentUserPrompt # Fallback to task step objective if no current prompt found if hasattr(context, 'task_step') and context.task_step: return context.task_step.objective or 'No request specified' return 'No request specified' def _extractUserLanguage(self) -> str: """Extract user language from service.""" return self.services.user.language if self.services and self.services.user else 'en' def _getMinimalDocumentContext(self, context: Any) -> str: """Get minimal document context (counts only) for React plan selection.""" try: if hasattr(context, 'workflow') and context.workflow: # Get document count from workflow documents = self.services.workflow.getAvailableDocuments(context.workflow) if documents and documents != "No documents available": # Count documents by counting docList and docItem references doc_count = documents.count("docList:") + documents.count("docItem:") return f"{doc_count} documents available from previous tasks" else: return "No documents available" return "No documents available" except Exception as e: logger.error(f"Error getting minimal document context: {str(e)}") return "No documents available" def _getFullDocumentContext(self, context: Any) -> str: """Get full document context with detailed references for parameter generation.""" try: if hasattr(context, 'workflow') and context.workflow: return self.services.workflow.getAvailableDocuments(context.workflow) return "No documents available" except Exception as e: logger.error(f"Error getting full document context: {str(e)}") return "No documents available" def _getMinimalConnectionContext(self) -> str: """Get minimal connection context (count only) for React plan selection.""" try: connections = self.services.workflow.getConnectionReferenceList() if connections: return f"{len(connections)} connections available" return "No connections available" except Exception as e: logger.error(f"Error getting minimal connection context: {str(e)}") return "No connections available" def _getFullConnectionContext(self) -> str: """Get full connection context with detailed references for parameter generation.""" try: connections = self.services.workflow.getConnectionReferenceList() if connections: return '\n'.join(f"- {conn}" for conn in connections) return "No connections available" except Exception as e: logger.error(f"Error getting full connection context: {str(e)}") return "No connections available" def _getWorkflowHistory(self, context: Any) -> str: """Get workflow history for task planning.""" try: if hasattr(context, 'workflow') and context.workflow: from modules.workflows.processing.shared.promptFactory import getPreviousRoundContext return getPreviousRoundContext(self.services, context.workflow) or "No previous workflow rounds - this is the first round." return "No previous workflow rounds - this is the first round." except Exception as e: logger.error(f"Error getting workflow history: {str(e)}") return "No previous workflow rounds - this is the first round." def _getAvailableMethods(self) -> str: """Get available methods for action selection and planning using compound action names.""" try: from modules.workflows.processing.shared.promptFactory import methods, discoverMethods # Get the methods dictionary if not methods: discoverMethods(self.services) # Create a flat JSON format with compound action names for better AI parsing available_actions_json = {} for methodName, methodInfo in methods.items(): # Convert MethodAi -> ai, MethodDocument -> document, etc. shortName = methodName.replace('Method', '').lower() for actionName, actionInfo in methodInfo['actions'].items(): # Create compound action name: method.action compoundActionName = f"{shortName}.{actionName}" # Get the action description action_description = actionInfo.get('description', f"Execute {actionName} action") available_actions_json[compoundActionName] = action_description return json.dumps(available_actions_json, indent=2, ensure_ascii=False) except Exception as e: logger.error(f"Error extracting available methods: {str(e)}") return json.dumps({}, indent=2, ensure_ascii=False) def _getReviewContent(self, context: Any) -> str: """Get review content for result validation.""" try: from modules.workflows.processing.shared.promptFactoryPlaceholders import extractReviewContent return extractReviewContent(context) except Exception as e: logger.error(f"Error getting review content: {str(e)}") return "No review content available" def _getPreviousActionResults(self, context: Any) -> str: """Get previous action results for learning context.""" try: if not hasattr(context, 'previous_action_results') or not context.previous_action_results: return "No previous actions executed yet" results = [] for i, result in enumerate(context.previous_action_results[-5:], 1): # Last 5 results if hasattr(result, 'resultLabel') and hasattr(result, 'status'): status = "SUCCESS" if result.status == "completed" else "FAILED" results.append(f"Action {i}: {result.resultLabel} - {status}") if hasattr(result, 'error') and result.error: results.append(f" Error: {result.error}") return "\n".join(results) if results else "No previous actions executed yet" except Exception as e: logger.error(f"Error getting previous action results: {str(e)}") return "No previous actions executed yet" def _getLearningsAndImprovements(self, context: Any) -> str: """Get learnings and improvements from previous actions.""" try: learnings = [] # Get improvements from context if hasattr(context, 'improvements') and context.improvements and isinstance(context.improvements, list): learnings.append("IMPROVEMENTS:") for improvement in context.improvements[-3:]: # Last 3 improvements learnings.append(f"- {improvement}") # Get failure patterns if hasattr(context, 'failure_patterns') and context.failure_patterns and isinstance(context.failure_patterns, list): learnings.append("FAILURE PATTERNS TO AVOID:") for pattern in context.failure_patterns[-3:]: # Last 3 patterns learnings.append(f"- {pattern}") # Get successful actions if hasattr(context, 'successful_actions') and context.successful_actions and isinstance(context.successful_actions, list): learnings.append("SUCCESSFUL APPROACHES:") for action in context.successful_actions[-3:]: # Last 3 successful learnings.append(f"- {action}") return "\n".join(learnings) if learnings else "No learnings available yet" except Exception as e: logger.error(f"Error getting learnings and improvements: {str(e)}") return "No learnings available yet" def _getLatestRefinementFeedback(self, context: Any) -> str: """Get the latest refinement feedback to influence next action planning.""" try: if not hasattr(context, 'previous_review_result') or not context.previous_review_result or not isinstance(context.previous_review_result, list): return "No previous refinement feedback available" # Get the most recent refinement decision latest_decision = context.previous_review_result[-1] if not isinstance(latest_decision, dict): return "No previous refinement feedback available" feedback_parts = [] # Add decision and reason decision = latest_decision.get('decision', 'unknown') reason = latest_decision.get('reason', 'No reason provided') feedback_parts.append(f"Latest Decision: {decision}") feedback_parts.append(f"Reason: {reason}") # Add any specific feedback or suggestions if 'feedback' in latest_decision: feedback_parts.append(f"Feedback: {latest_decision['feedback']}") if 'suggestions' in latest_decision: feedback_parts.append(f"Suggestions: {latest_decision['suggestions']}") return "\n".join(feedback_parts) except Exception as e: logger.error(f"Error getting latest refinement feedback: {str(e)}") return "No previous refinement feedback available" async def _generateActionObjective(self, context: Any, current_task: str, original_prompt: str, additional_data: Dict[str, Any] = None) -> str: """Generate intelligent, context-aware action objective using AI.""" try: # Get the selected action from additional_data selected_action = additional_data.get('SELECTED_ACTION', '') if additional_data else '' # Build context for AI objective generation context_info = { "original_prompt": original_prompt, "current_task": current_task, "selected_action": selected_action, "available_documents": self._getFullDocumentContext(context), "available_connections": self._getFullConnectionContext(), "previous_results": self._getPreviousActionResults(context), "learnings": self._getLearningsAndImprovements(context), "refinement_feedback": self._getLatestRefinementFeedback(context), "user_language": self._extractUserLanguage() } # Create AI prompt for objective generation objective_prompt = f"""Generate a specific, actionable objective for the selected action. CONTEXT: - Original User Request: {context_info['original_prompt']} - Current Task: {context_info['current_task']} - Selected Action: {context_info['selected_action']} - Available Documents: {context_info['available_documents']} - Available Connections: {context_info['available_connections']} - Previous Action Results: {context_info['previous_results']} - Learnings and Improvements: {context_info['learnings']} - Latest Refinement Feedback: {context_info['refinement_feedback']} - User Language: {context_info['user_language']} REQUIREMENTS: 1. Create a SPECIFIC objective that tells the action exactly what to accomplish 2. Include relevant details about documents, connections, recipients, etc. 3. Learn from previous attempts and refinement feedback 4. Make it actionable and concrete 5. Focus on the user's actual intent, not just the task description 6. If this is a retry, incorporate learnings from previous failures RESPONSE FORMAT: Return ONLY the objective text, no explanations or formatting. OBJECTIVE:""" # Call AI to generate the objective if self.services and hasattr(self.services, 'ai'): from modules.datamodels.datamodelAi import AiCallOptions, OperationType, Priority, ProcessingMode options = AiCallOptions( operationType=OperationType.ANALYSE_CONTENT, priority=Priority.BALANCED, compressPrompt=False, compressContext=False, processingMode=ProcessingMode.ADVANCED, maxCost=0.01, maxProcessingTime=10 ) response = await self.services.ai.callAi( prompt=objective_prompt, placeholders={}, options=options ) # Extract objective from response if response and response.strip(): return response.strip() # Fallback to current task if AI fails return current_task or original_prompt except Exception as e: logger.error(f"Error generating action objective: {str(e)}") # Fallback to current task return current_task or original_prompt