397 lines
20 KiB
Python
397 lines
20 KiB
Python
"""
|
|
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
|