streamlined ai calling chain for react and workflow mode

This commit is contained in:
ValueOn AG 2025-11-02 18:41:07 +01:00
parent fa91016e16
commit 53bfe06dbe
16 changed files with 657 additions and 397 deletions

View file

@ -531,8 +531,17 @@ registerModelLabels(
class ObservationPreview(BaseModel): class ObservationPreview(BaseModel):
name: str = Field(description="Document name or URL label") name: str = Field(description="Document name or URL label")
mime: str = Field(description="MIME type or kind") mime: Optional[str] = Field(default=None, description="MIME type or kind (legacy field)")
snippet: str = Field(description="Short snippet or summary") snippet: Optional[str] = Field(default=None, description="Short snippet or summary")
# Extended metadata fields
mimeType: Optional[str] = Field(default=None, description="MIME type")
size: Optional[str] = Field(default=None, description="File size")
created: Optional[str] = Field(default=None, description="Creation timestamp")
modified: Optional[str] = Field(default=None, description="Modification timestamp")
typeGroup: Optional[str] = Field(default=None, description="Document type group")
documentId: Optional[str] = Field(default=None, description="Document ID")
reference: Optional[str] = Field(default=None, description="Document reference")
contentSize: Optional[str] = Field(default=None, description="Content size indicator")
registerModelLabels( registerModelLabels(
@ -556,6 +565,13 @@ class Observation(BaseModel):
notes: List[str] = Field( notes: List[str] = Field(
default_factory=list, description="Short notes or key facts" default_factory=list, description="Short notes or key facts"
) )
# Extended fields for enhanced validation
contentValidation: Optional[Dict[str, Any]] = Field(
default=None, description="Content validation results"
)
contentAnalysis: Optional[Dict[str, Any]] = Field(
default=None, description="Content analysis results"
)
registerModelLabels( registerModelLabels(
@ -751,11 +767,21 @@ class TaskStep(BaseModel):
id: str id: str
objective: str objective: str
dependencies: Optional[list[str]] = Field(default_factory=list) dependencies: Optional[list[str]] = Field(default_factory=list)
success_criteria: Optional[list[str]] = Field(default_factory=list) successCriteria: Optional[list[str]] = Field(default_factory=list)
estimated_complexity: Optional[str] = None estimatedComplexity: Optional[str] = None
userMessage: Optional[str] = Field( userMessage: Optional[str] = Field(
None, description="User-friendly message in user's language" None, description="User-friendly message in user's language"
) )
# Format details extracted from intent analysis
dataType: Optional[str] = Field(
None, description="Expected data type (text, numbers, documents, etc.)"
)
expectedFormat: Optional[str] = Field(
None, description="Expected output format (json, csv, markdown, etc.)"
)
qualityRequirements: Optional[Dict[str, Any]] = Field(
None, description="Quality requirements and constraints"
)
registerModelLabels( registerModelLabels(
@ -765,8 +791,8 @@ registerModelLabels(
"id": {"en": "ID", "fr": "ID"}, "id": {"en": "ID", "fr": "ID"},
"objective": {"en": "Objective", "fr": "Objectif"}, "objective": {"en": "Objective", "fr": "Objectif"},
"dependencies": {"en": "Dependencies", "fr": "Dépendances"}, "dependencies": {"en": "Dependencies", "fr": "Dépendances"},
"success_criteria": {"en": "Success Criteria", "fr": "Critères de succès"}, "successCriteria": {"en": "Success Criteria", "fr": "Critères de succès"},
"estimated_complexity": { "estimatedComplexity": {
"en": "Estimated Complexity", "en": "Estimated Complexity",
"fr": "Complexité estimée", "fr": "Complexité estimée",
}, },
@ -825,27 +851,27 @@ registerModelLabels(
class TaskContext(BaseModel): class TaskContext(BaseModel):
task_step: TaskStep taskStep: TaskStep
workflow: Optional["ChatWorkflow"] = None workflow: Optional["ChatWorkflow"] = None
workflow_id: Optional[str] = None workflowId: Optional[str] = None
available_documents: Optional[str] = "No documents available" availableDocuments: Optional[str] = "No documents available"
available_connections: Optional[list[str]] = Field(default_factory=list) availableConnections: Optional[list[str]] = Field(default_factory=list)
previous_results: Optional[list[str]] = Field(default_factory=list) previousResults: Optional[list[str]] = Field(default_factory=list)
previous_handover: Optional[TaskHandover] = None previousHandover: Optional[TaskHandover] = None
improvements: Optional[list[str]] = Field(default_factory=list) improvements: Optional[list[str]] = Field(default_factory=list)
retry_count: Optional[int] = 0 retryCount: Optional[int] = 0
previous_action_results: Optional[list] = Field(default_factory=list) previousActionResults: Optional[list] = Field(default_factory=list)
previous_review_result: Optional[dict] = None previousReviewResult: Optional[dict] = None
is_regeneration: Optional[bool] = False isRegeneration: Optional[bool] = False
failure_patterns: Optional[list[str]] = Field(default_factory=list) failurePatterns: Optional[list[str]] = Field(default_factory=list)
failed_actions: Optional[list] = Field(default_factory=list) failedActions: Optional[list] = Field(default_factory=list)
successful_actions: Optional[list] = Field(default_factory=list) successfulActions: Optional[list] = Field(default_factory=list)
criteria_progress: Optional[dict] = None criteriaProgress: Optional[dict] = None
def getDocumentReferences(self) -> List[str]: def getDocumentReferences(self) -> List[str]:
docs = [] docs = []
if self.previous_handover: if self.previousHandover:
for doc_exchange in self.previous_handover.inputDocuments: for doc_exchange in self.previousHandover.inputDocuments:
docs.extend(doc_exchange.documents) docs.extend(doc_exchange.documents)
return list(set(docs)) return list(set(docs))
@ -857,22 +883,22 @@ class TaskContext(BaseModel):
class ReviewContext(BaseModel): class ReviewContext(BaseModel):
task_step: TaskStep taskStep: TaskStep
task_actions: Optional[list] = Field(default_factory=list) taskActions: Optional[list] = Field(default_factory=list)
action_results: Optional[list] = Field(default_factory=list) actionResults: Optional[list] = Field(default_factory=list)
step_result: Optional[dict] = Field(default_factory=dict) stepResult: Optional[dict] = Field(default_factory=dict)
workflow_id: Optional[str] = None workflowId: Optional[str] = None
previous_results: Optional[list[str]] = Field(default_factory=list) previousResults: Optional[list[str]] = Field(default_factory=list)
class ReviewResult(BaseModel): class ReviewResult(BaseModel):
status: str status: str
reason: Optional[str] = None reason: Optional[str] = None
improvements: Optional[list[str]] = Field(default_factory=list) improvements: Optional[list[str]] = Field(default_factory=list)
quality_score: Optional[int] = 5 qualityScore: Optional[float] = Field(default=5.0, description="Quality score (0-10)")
missing_outputs: Optional[list[str]] = Field(default_factory=list) missingOutputs: Optional[list[str]] = Field(default_factory=list)
met_criteria: Optional[list[str]] = Field(default_factory=list) metCriteria: Optional[list[str]] = Field(default_factory=list)
unmet_criteria: Optional[list[str]] = Field(default_factory=list) unmetCriteria: Optional[list[str]] = Field(default_factory=list)
confidence: Optional[float] = 0.5 confidence: Optional[float] = 0.5
userMessage: Optional[str] = Field( userMessage: Optional[str] = Field(
None, description="User-friendly message in user's language" None, description="User-friendly message in user's language"
@ -886,10 +912,10 @@ registerModelLabels(
"status": {"en": "Status", "fr": "Statut"}, "status": {"en": "Status", "fr": "Statut"},
"reason": {"en": "Reason", "fr": "Raison"}, "reason": {"en": "Reason", "fr": "Raison"},
"improvements": {"en": "Improvements", "fr": "Améliorations"}, "improvements": {"en": "Improvements", "fr": "Améliorations"},
"quality_score": {"en": "Quality Score", "fr": "Score de qualité"}, "qualityScore": {"en": "Quality Score", "fr": "Score de qualité"},
"missing_outputs": {"en": "Missing Outputs", "fr": "Sorties manquantes"}, "missingOutputs": {"en": "Missing Outputs", "fr": "Sorties manquantes"},
"met_criteria": {"en": "Met Criteria", "fr": "Critères respectés"}, "metCriteria": {"en": "Met Criteria", "fr": "Critères respectés"},
"unmet_criteria": {"en": "Unmet Criteria", "fr": "Critères non respectés"}, "unmetCriteria": {"en": "Unmet Criteria", "fr": "Critères non respectés"},
"confidence": {"en": "Confidence", "fr": "Confiance"}, "confidence": {"en": "Confidence", "fr": "Confiance"},
"userMessage": {"en": "User Message", "fr": "Message utilisateur"}, "userMessage": {"en": "User Message", "fr": "Message utilisateur"},
}, },

View file

@ -7,9 +7,9 @@ class DocumentMetadata(BaseModel):
"""Metadata for the entire document.""" """Metadata for the entire document."""
title: str = Field(description="Document title") title: str = Field(description="Document title")
author: Optional[str] = Field(default=None, description="Document author") author: Optional[str] = Field(default=None, description="Document author")
created_at: datetime = Field(default_factory=datetime.now, description="Creation timestamp") createdAt: datetime = Field(default_factory=datetime.now, description="Creation timestamp")
source_documents: List[str] = Field(default_factory=list, description="Source document IDs") sourceDocuments: List[str] = Field(default_factory=list, description="Source document IDs")
extraction_method: str = Field(default="ai_extraction", description="Method used for extraction") extractionMethod: str = Field(default="ai_extraction", description="Method used for extraction")
version: str = Field(default="1.0", description="Document version") version: str = Field(default="1.0", description="Document version")
@ -31,7 +31,7 @@ class ListItem(BaseModel):
class BulletList(BaseModel): class BulletList(BaseModel):
"""Bulleted or numbered list.""" """Bulleted or numbered list."""
items: List[ListItem] = Field(description="List items") items: List[ListItem] = Field(description="List items")
list_type: Literal["bullet", "numbered", "checklist"] = Field(default="bullet", description="List type") listType: Literal["bullet", "numbered", "checklist"] = Field(default="bullet", description="List type")
metadata: Dict[str, Any] = Field(default_factory=dict, description="List metadata") metadata: Dict[str, Any] = Field(default_factory=dict, description="List metadata")
@ -59,7 +59,7 @@ class CodeBlock(BaseModel):
class Image(BaseModel): class Image(BaseModel):
"""Image with metadata.""" """Image with metadata."""
data: str = Field(description="Base64 encoded image data") data: str = Field(description="Base64 encoded image data")
alt_text: Optional[str] = Field(default=None, description="Alternative text") altText: Optional[str] = Field(default=None, description="Alternative text")
caption: Optional[str] = Field(default=None, description="Image caption") caption: Optional[str] = Field(default=None, description="Image caption")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Image metadata") metadata: Dict[str, Any] = Field(default_factory=dict, description="Image metadata")
@ -68,7 +68,7 @@ class DocumentSection(BaseModel):
"""A section of the document containing one or more content elements.""" """A section of the document containing one or more content elements."""
id: str = Field(description="Unique section identifier") id: str = Field(description="Unique section identifier")
title: Optional[str] = Field(default=None, description="Section title") title: Optional[str] = Field(default=None, description="Section title")
content_type: Literal["table", "list", "paragraph", "heading", "code", "image", "mixed"] = Field(description="Primary content type") contentType: Literal["table", "list", "paragraph", "heading", "code", "image", "mixed"] = Field(description="Primary content type")
elements: List[Union[TableData, BulletList, Paragraph, Heading, CodeBlock, Image]] = Field(description="Content elements in this section") elements: List[Union[TableData, BulletList, Paragraph, Heading, CodeBlock, Image]] = Field(description="Content elements in this section")
order: int = Field(description="Section order in document") order: int = Field(description="Section order in document")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Section metadata") metadata: Dict[str, Any] = Field(default_factory=dict, description="Section metadata")
@ -81,9 +81,9 @@ class StructuredDocument(BaseModel):
summary: Optional[str] = Field(default=None, description="Document summary") summary: Optional[str] = Field(default=None, description="Document summary")
tags: List[str] = Field(default_factory=list, description="Document tags") tags: List[str] = Field(default_factory=list, description="Document tags")
def getSectionsByType(self, content_type: str) -> List[DocumentSection]: def getSectionsByType(self, contentType: str) -> List[DocumentSection]:
"""Get all sections of a specific content type.""" """Get all sections of a specific content type."""
return [section for section in self.sections if section.content_type == content_type] return [section for section in self.sections if section.contentType == contentType]
def getAllTables(self) -> List[TableData]: def getAllTables(self) -> List[TableData]:
"""Get all table data from the document.""" """Get all table data from the document."""
@ -104,22 +104,6 @@ class StructuredDocument(BaseModel):
return lists return lists
class JsonChunkResult(BaseModel):
"""Result from processing a single chunk with JSON output."""
chunk_id: str = Field(description="Chunk identifier")
document_section: DocumentSection = Field(description="Structured content from this chunk")
processing_time: float = Field(description="Processing time in seconds")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Chunk processing metadata")
class JsonMergeResult(BaseModel):
"""Result from merging multiple JSON chunks."""
merged_document: StructuredDocument = Field(description="Merged structured document")
merge_strategy: str = Field(description="Strategy used for merging")
chunks_processed: int = Field(description="Number of chunks processed")
merge_time: float = Field(description="Time taken to merge chunks")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Merge process metadata")
# Update forward references # Update forward references
ListItem.model_rebuild() ListItem.model_rebuild()

View file

@ -22,9 +22,15 @@ class ContentValidator:
self.services = services self.services = services
self.learningEngine = learningEngine self.learningEngine = learningEngine
async def validateContent(self, documents: List[Any], intent: Dict[str, Any]) -> Dict[str, Any]: async def validateContent(self, documents: List[Any], intent: Dict[str, Any], taskStep: Optional[Any] = None) -> Dict[str, Any]:
"""Validates delivered content against user intent using AI (single attempt; parse-or-fail)""" """Validates delivered content against user intent using AI (single attempt; parse-or-fail)
return await self._validateWithAI(documents, intent)
Args:
documents: List of documents to validate
intent: Workflow-level intent dict (for format requirements)
taskStep: Optional TaskStep object (preferred source for objective)
"""
return await self._validateWithAI(documents, intent, taskStep)
def _analyzeDocuments(self, documents: List[Any]) -> List[Dict[str, Any]]: def _analyzeDocuments(self, documents: List[Any]) -> List[Dict[str, Any]]:
"""Generic document analysis - create simple summaries with metadata.""" """Generic document analysis - create simple summaries with metadata."""
@ -242,21 +248,58 @@ class ContentValidator:
return False return False
async def _validateWithAI(self, documents: List[Any], intent: Dict[str, Any]) -> Dict[str, Any]: async def _validateWithAI(self, documents: List[Any], intent: Dict[str, Any], taskStep: Optional[Any] = None) -> Dict[str, Any]:
"""AI-based comprehensive validation - generic approach""" """AI-based comprehensive validation - generic approach"""
try: try:
if not hasattr(self, 'services') or not self.services or not hasattr(self.services, 'ai'): if not hasattr(self, 'services') or not self.services or not hasattr(self.services, 'ai'):
return self._createFailedValidationResult("AI service not available") return self._createFailedValidationResult("AI service not available")
# Use taskStep.objective if available, otherwise fall back to intent.primaryGoal
taskObjective = None
if taskStep and hasattr(taskStep, 'objective'):
taskObjective = taskStep.objective
elif taskStep and isinstance(taskStep, dict):
taskObjective = taskStep.get('objective')
# Use taskStep format fields if available, otherwise fall back to intent
dataType = None
expectedFormat = None
if taskStep:
if hasattr(taskStep, 'dataType') and taskStep.dataType:
dataType = taskStep.dataType
elif isinstance(taskStep, dict):
dataType = taskStep.get('dataType')
if hasattr(taskStep, 'expectedFormat') and taskStep.expectedFormat:
expectedFormat = taskStep.expectedFormat
elif isinstance(taskStep, dict):
expectedFormat = taskStep.get('expectedFormat')
# Fallback to intent if taskStep format fields not available
if not dataType:
dataType = intent.get('dataType', 'unknown')
if not expectedFormat:
expectedFormat = intent.get('expectedFormat', 'unknown')
# Determine objective text and label
objectiveText = taskObjective if taskObjective else intent.get('primaryGoal', 'Unknown')
objectiveLabel = "TASK OBJECTIVE" if taskObjective else "USER REQUEST"
# Build prompt base WITHOUT document summaries first # Build prompt base WITHOUT document summaries first
successCriteria = intent.get('successCriteria', []) # Use success criteria from taskStep if available, otherwise from intent
successCriteria = []
if taskStep and hasattr(taskStep, 'successCriteria') and taskStep.successCriteria:
successCriteria = taskStep.successCriteria
elif taskStep and isinstance(taskStep, dict):
successCriteria = taskStep.get('successCriteria', [])
else:
successCriteria = intent.get('successCriteria', [])
criteriaCount = len(successCriteria) criteriaCount = len(successCriteria)
promptBase = f"""TASK VALIDATION promptBase = f"""TASK VALIDATION
USER REQUEST: '{intent.get('primaryGoal', 'Unknown')}' {objectiveLabel}: '{objectiveText}'
EXPECTED DATA TYPE: {intent.get('dataType', 'unknown')} EXPECTED DATA TYPE: {dataType}
EXPECTED FORMAT: {intent.get('expectedFormat', 'unknown')} EXPECTED FORMAT: {expectedFormat}
SUCCESS CRITERIA ({criteriaCount} items): {successCriteria} SUCCESS CRITERIA ({criteriaCount} items): {successCriteria}
VALIDATION RULES: VALIDATION RULES:

View file

@ -27,12 +27,26 @@ class IntentAnalyzer:
return None return None
# Create AI analysis prompt # Create AI analysis prompt
# Determine if we're in task context (have taskStep) or workflow context
isTaskContext = hasattr(context, 'taskStep') and context.taskStep is not None
contextObjective = getattr(context.taskStep, 'objective', '') if isTaskContext else ''
# Use appropriate label based on context
if isTaskContext:
# Task context: use OBJECTIVE label and only task objective
requestLabel = "OBJECTIVE"
contextInfo = f"OBJECTIVE: {self.services.utils.sanitizePromptContent(contextObjective, 'userinput')}"
else:
# Workflow context: use USER REQUEST label
requestLabel = "USER REQUEST"
contextInfo = f"CONTEXT: {self.services.utils.sanitizePromptContent(contextObjective, 'userinput') if contextObjective else 'None'}"
analysisPrompt = f""" analysisPrompt = f"""
You are an intent analyzer. Analyze the user's request to understand what they want delivered. You are an intent analyzer. Analyze the user's request to understand what they want delivered.
USER REQUEST: {self.services.utils.sanitizePromptContent(userPrompt, 'userinput')} {requestLabel}: {self.services.utils.sanitizePromptContent(userPrompt, 'userinput')}
CONTEXT: {getattr(context.task_step, 'objective', '') if hasattr(context, 'task_step') and context.task_step else ''} {contextInfo}
Analyze the user's intent and determine: Analyze the user's intent and determine:
1. What type of data/content they want (numbers, text, documents, analysis, code, etc.) 1. What type of data/content they want (numbers, text, documents, analysis, code, etc.)

View file

@ -170,9 +170,9 @@ class LearningEngine:
"""Serializes context for storage""" """Serializes context for storage"""
try: try:
return { return {
"taskObjective": getattr(context, 'task_step', {}).get('objective', '') if hasattr(context, 'task_step') else '', "taskObjective": getattr(context, 'taskStep', {}).get('objective', '') if hasattr(context, 'taskStep') else '',
"workflowId": getattr(context, 'workflow_id', ''), "workflowId": getattr(context, 'workflowId', ''),
"availableDocuments": getattr(context, 'available_documents', []) "availableDocuments": getattr(context, 'availableDocuments', [])
} }
except Exception: except Exception:
return {} return {}

View file

@ -201,12 +201,12 @@ class MessageCreator:
completionMessage = f"🎯 **Task {taskProgress}**\n\n{reviewResult.reason or 'Task completed successfully'}" completionMessage = f"🎯 **Task {taskProgress}**\n\n{reviewResult.reason or 'Task completed successfully'}"
# Add criteria status if available # Add criteria status if available
if hasattr(reviewResult, 'met_criteria') and reviewResult.met_criteria: if hasattr(reviewResult, 'metCriteria') and reviewResult.metCriteria:
for criterion in reviewResult.met_criteria: for criterion in reviewResult.metCriteria:
completionMessage += f"\n{criterion}" completionMessage += f"\n{criterion}"
if hasattr(reviewResult, 'quality_score'): if hasattr(reviewResult, 'qualityScore'):
completionMessage += f"\n📊 Score {reviewResult.quality_score}/10" completionMessage += f"\n📊 Score {reviewResult.qualityScore}/10"
taskCompletionMessage = { taskCompletionMessage = {
"workflowId": workflow.id, "workflowId": workflow.id,

View file

@ -52,9 +52,14 @@ class TaskPlanner:
self._checkWorkflowStopped(workflow) self._checkWorkflowStopped(workflow)
# Analyze user intent to obtain cleaned user objective for planning # Analyze user intent to obtain cleaned user objective for planning
# This intent will be reused for workflow-level validation in executeTask
from modules.workflows.processing.adaptive import IntentAnalyzer
intentAnalyzer = IntentAnalyzer(self.services) intentAnalyzer = IntentAnalyzer(self.services)
intent = await intentAnalyzer.analyzeUserIntent(actualUserPrompt, None) workflowIntent = await intentAnalyzer.analyzeUserIntent(actualUserPrompt, None)
cleanedObjective = intent.get('primaryGoal', actualUserPrompt) if isinstance(intent, dict) else actualUserPrompt # Store workflow intent for reuse in executeTask (avoid redundant analysis)
if not hasattr(workflow, '_workflowIntent'):
workflow._workflowIntent = workflowIntent
cleanedObjective = workflowIntent.get('primaryGoal', actualUserPrompt) if isinstance(workflowIntent, dict) else actualUserPrompt
# Create proper context object for task planning using cleaned intent # Create proper context object for task planning using cleaned intent
# For task planning, we need to create a minimal TaskStep since TaskContext requires it # For task planning, we need to create a minimal TaskStep since TaskContext requires it
@ -62,27 +67,27 @@ class TaskPlanner:
id="plan", id="plan",
objective=cleanedObjective, objective=cleanedObjective,
dependencies=[], dependencies=[],
success_criteria=[], successCriteria=[],
estimated_complexity="medium" estimatedComplexity="medium"
) )
taskPlanningContext = TaskContext( taskPlanningContext = TaskContext(
task_step=planningTaskStep, taskStep=planningTaskStep,
workflow=workflow, workflow=workflow,
workflow_id=workflow.id, workflowId=workflow.id,
available_documents=None, availableDocuments=None,
available_connections=None, availableConnections=None,
previous_results=[], previousResults=[],
previous_handover=None, previousHandover=None,
improvements=[], improvements=[],
retry_count=0, retryCount=0,
previous_action_results=[], previousActionResults=[],
previous_review_result=None, previousReviewResult=None,
is_regeneration=False, isRegeneration=False,
failure_patterns=[], failurePatterns=[],
failed_actions=[], failedActions=[],
successful_actions=[], successfulActions=[],
criteria_progress={ criteriaProgress={
'met_criteria': set(), 'met_criteria': set(),
'unmet_criteria': set(), 'unmet_criteria': set(),
'attempt_history': [] 'attempt_history': []
@ -153,6 +158,16 @@ class TaskPlanner:
if 'description' in taskDict and 'objective' not in taskDict: if 'description' in taskDict and 'objective' not in taskDict:
taskDict['objective'] = taskDict.pop('description') taskDict['objective'] = taskDict.pop('description')
# Extract format details from workflow intent and populate TaskStep
# Use workflow-level intent for format requirements (tasks inherit from workflow)
if isinstance(workflowIntent, dict):
if 'dataType' in workflowIntent and 'dataType' not in taskDict:
taskDict['dataType'] = workflowIntent.get('dataType')
if 'expectedFormat' in workflowIntent and 'expectedFormat' not in taskDict:
taskDict['expectedFormat'] = workflowIntent.get('expectedFormat')
if 'qualityRequirements' in workflowIntent and 'qualityRequirements' not in taskDict:
taskDict['qualityRequirements'] = workflowIntent.get('qualityRequirements')
try: try:
task = TaskStep(**taskDict) task = TaskStep(**taskDict)
tasks.append(task) tasks.append(task)
@ -206,7 +221,7 @@ class TaskPlanner:
logger.error(f"Task {i} is not a dictionary: {type(task)}") logger.error(f"Task {i} is not a dictionary: {type(task)}")
return False return False
requiredFields = ['id', 'objective', 'success_criteria'] requiredFields = ['id', 'objective', 'successCriteria']
missingFields = [field for field in requiredFields if field not in task] missingFields = [field for field in requiredFields if field not in task]
if missingFields: if missingFields:
logger.error(f"Task {i} missing required fields: {missingFields}") logger.error(f"Task {i} missing required fields: {missingFields}")

View file

@ -40,7 +40,7 @@ class WorkflowValidator:
logger.error(f"Task {i} is not a dictionary: {type(task)}") logger.error(f"Task {i} is not a dictionary: {type(task)}")
return False return False
requiredFields = ['id', 'objective', 'success_criteria'] requiredFields = ['id', 'objective', 'successCriteria']
missingFields = [field for field in requiredFields if field not in task] missingFields = [field for field in requiredFields if field not in task]
if missingFields: if missingFields:
logger.error(f"Task {i} missing required fields: {missingFields}") logger.error(f"Task {i} missing required fields: {missingFields}")

View file

@ -4,6 +4,7 @@
import json import json
import logging import logging
import uuid import uuid
from datetime import datetime, timezone
from typing import List, Dict, Any from typing import List, Dict, Any
from modules.datamodels.datamodelChat import ( from modules.datamodels.datamodelChat import (
TaskStep, TaskContext, TaskResult, ActionItem, TaskStatus, TaskStep, TaskContext, TaskResult, ActionItem, TaskStatus,
@ -17,6 +18,8 @@ from modules.workflows.processing.shared.promptGenerationActionsActionplan impor
generateActionDefinitionPrompt, generateActionDefinitionPrompt,
generateResultReviewPrompt generateResultReviewPrompt
) )
from modules.workflows.processing.adaptive import IntentAnalyzer, ContentValidator, LearningEngine, ProgressTracker
from modules.workflows.processing.adaptive.adaptiveLearningEngine import AdaptiveLearningEngine
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -25,6 +28,14 @@ class ActionplanMode(BaseMode):
def __init__(self, services, workflow): def __init__(self, services, workflow):
super().__init__(services, workflow) super().__init__(services, workflow)
# Initialize adaptive components for enhanced validation and learning
self.intentAnalyzer = IntentAnalyzer(services)
self.learningEngine = LearningEngine()
self.adaptiveLearningEngine = AdaptiveLearningEngine()
self.contentValidator = ContentValidator(services, self.adaptiveLearningEngine)
self.progressTracker = ProgressTracker()
self.workflowIntent = None
self.taskIntent = None
async def generateActionItems(self, taskStep: TaskStep, workflow: ChatWorkflow, async def generateActionItems(self, taskStep: TaskStep, workflow: ChatWorkflow,
previousResults: List = None, enhancedContext: TaskContext = None) -> List[ActionItem]: previousResults: List = None, enhancedContext: TaskContext = None) -> List[ActionItem]:
@ -33,13 +44,13 @@ class ActionplanMode(BaseMode):
# Check workflow status before generating actions # Check workflow status before generating actions
self._checkWorkflowStopped(workflow) self._checkWorkflowStopped(workflow)
retryInfo = f" (Retry #{enhancedContext.retry_count})" if enhancedContext and enhancedContext.retry_count > 0 else "" retryInfo = f" (Retry #{enhancedContext.retryCount})" if enhancedContext and enhancedContext.retryCount > 0 else ""
logger.info(f"Generating actions for task: {taskStep.objective}{retryInfo}") logger.info(f"Generating actions for task: {taskStep.objective}{retryInfo}")
# Log criteria progress if this is a retry # Log criteria progress if this is a retry
if enhancedContext and hasattr(enhancedContext, 'criteria_progress') and enhancedContext.criteria_progress is not None: if enhancedContext and hasattr(enhancedContext, 'criteriaProgress') and enhancedContext.criteriaProgress is not None:
progress = enhancedContext.criteria_progress progress = enhancedContext.criteriaProgress
logger.info(f"Retry attempt {enhancedContext.retry_count} - Criteria progress:") logger.info(f"Retry attempt {enhancedContext.retryCount} - Criteria progress:")
if progress.get('met_criteria'): if progress.get('met_criteria'):
logger.info(f" Met criteria: {', '.join(progress['met_criteria'])}") logger.info(f" Met criteria: {', '.join(progress['met_criteria'])}")
if progress.get('unmet_criteria'): if progress.get('unmet_criteria'):
@ -59,14 +70,14 @@ class ActionplanMode(BaseMode):
logger.info(f" Quality stable: {currScore}") logger.info(f" Quality stable: {currScore}")
# Enhanced retry context logging # Enhanced retry context logging
if enhancedContext and enhancedContext.retry_count > 0: if enhancedContext and enhancedContext.retryCount > 0:
logger.info("=== RETRY CONTEXT FOR ACTION GENERATION ===") logger.info("=== RETRY CONTEXT FOR ACTION GENERATION ===")
logger.info(f"Retry Count: {enhancedContext.retry_count}") logger.info(f"Retry Count: {enhancedContext.retryCount}")
logger.debug(f"Previous Improvements: {enhancedContext.improvements}") logger.debug(f"Previous Improvements: {enhancedContext.improvements}")
logger.debug(f"Previous Review Result: {enhancedContext.previous_review_result}") logger.debug(f"Previous Review Result: {enhancedContext.previousReviewResult}")
logger.debug(f"Failure Patterns: {enhancedContext.failure_patterns}") logger.debug(f"Failure Patterns: {enhancedContext.failurePatterns}")
logger.debug(f"Failed Actions: {enhancedContext.failed_actions}") logger.debug(f"Failed Actions: {enhancedContext.failedActions}")
logger.debug(f"Successful Actions: {enhancedContext.successful_actions}") logger.debug(f"Successful Actions: {enhancedContext.successfulActions}")
logger.info("=== END RETRY CONTEXT ===") logger.info("=== END RETRY CONTEXT ===")
# Log that we're starting action generation # Log that we're starting action generation
@ -76,42 +87,42 @@ class ActionplanMode(BaseMode):
if enhancedContext and isinstance(enhancedContext, TaskContext): if enhancedContext and isinstance(enhancedContext, TaskContext):
# Use existing TaskContext if provided # Use existing TaskContext if provided
actionContext = TaskContext( actionContext = TaskContext(
task_step=enhancedContext.task_step, taskStep=enhancedContext.taskStep,
workflow=enhancedContext.workflow, workflow=enhancedContext.workflow,
workflow_id=enhancedContext.workflow_id, workflowId=enhancedContext.workflowId,
available_documents=enhancedContext.available_documents, availableDocuments=enhancedContext.availableDocuments,
available_connections=enhancedContext.available_connections, availableConnections=enhancedContext.availableConnections,
previous_results=enhancedContext.previous_results or previousResults or [], previousResults=enhancedContext.previousResults or previousResults or [],
previous_handover=enhancedContext.previous_handover, previousHandover=enhancedContext.previousHandover,
improvements=enhancedContext.improvements or [], improvements=enhancedContext.improvements or [],
retry_count=enhancedContext.retry_count or 0, retryCount=enhancedContext.retryCount or 0,
previous_action_results=enhancedContext.previous_action_results or [], previousActionResults=enhancedContext.previousActionResults or [],
previous_review_result=enhancedContext.previous_review_result, previousReviewResult=enhancedContext.previousReviewResult,
is_regeneration=enhancedContext.is_regeneration or False, isRegeneration=enhancedContext.isRegeneration or False,
failure_patterns=enhancedContext.failure_patterns or [], failurePatterns=enhancedContext.failurePatterns or [],
failed_actions=enhancedContext.failed_actions or [], failedActions=enhancedContext.failedActions or [],
successful_actions=enhancedContext.successful_actions or [], successfulActions=enhancedContext.successfulActions or [],
criteria_progress=enhancedContext.criteria_progress criteriaProgress=enhancedContext.criteriaProgress
) )
else: else:
# Create new context from scratch # Create new context from scratch
actionContext = TaskContext( actionContext = TaskContext(
task_step=taskStep, taskStep=taskStep,
workflow=workflow, workflow=workflow,
workflow_id=workflow.id, workflowId=workflow.id,
available_documents=None, availableDocuments=None,
available_connections=None, availableConnections=None,
previous_results=previousResults or [], previousResults=previousResults or [],
previous_handover=None, previousHandover=None,
improvements=[], improvements=[],
retry_count=0, retryCount=0,
previous_action_results=[], previousActionResults=[],
previous_review_result=None, previousReviewResult=None,
is_regeneration=False, isRegeneration=False,
failure_patterns=[], failurePatterns=[],
failed_actions=[], failedActions=[],
successful_actions=[], successfulActions=[],
criteria_progress=None criteriaProgress=None
) )
# Check workflow status before calling AI service # Check workflow status before calling AI service
@ -220,6 +231,27 @@ class ActionplanMode(BaseMode):
"""Execute all actions for a task step using Actionplan mode""" """Execute all actions for a task step using Actionplan mode"""
logger.info(f"=== STARTING TASK {taskIndex or '?'}: {taskStep.objective} ===") logger.info(f"=== STARTING TASK {taskIndex or '?'}: {taskStep.objective} ===")
# Use workflow-level intent from planning phase (stored in workflow object)
# This avoids redundant intent analysis - intent was already analyzed during task planning
if hasattr(workflow, '_workflowIntent') and workflow._workflowIntent:
self.workflowIntent = workflow._workflowIntent
logger.info(f"Using workflow intent from planning phase")
else:
# Fallback: analyze if not available (shouldn't happen in normal flow)
originalPrompt = self.services.currentUserPrompt if self.services and hasattr(self.services, 'currentUserPrompt') else taskStep.objective
self.workflowIntent = await self.intentAnalyzer.analyzeUserIntent(originalPrompt, context)
logger.warning(f"Workflow intent not found in workflow object, analyzed fresh")
# Task-level intent is NOT needed - use task.objective + task format fields (dataType, expectedFormat, qualityRequirements)
# These format fields are populated from workflow intent during task planning
self.taskIntent = None # Removed redundant task-level intent analysis
logger.info(f"Workflow intent: {self.workflowIntent}")
if taskStep.dataType or taskStep.expectedFormat or taskStep.qualityRequirements:
logger.info(f"Task format info: dataType={taskStep.dataType}, expectedFormat={taskStep.expectedFormat}")
# Reset progress tracking for new task
self.progressTracker.reset()
# Update workflow object before executing task # Update workflow object before executing task
if taskIndex is not None: if taskIndex is not None:
self._updateWorkflowBeforeExecutingTask(taskIndex) self._updateWorkflowBeforeExecutingTask(taskIndex)
@ -243,10 +275,10 @@ class ActionplanMode(BaseMode):
# Update retry context with current attempt information # Update retry context with current attempt information
if retryContext: if retryContext:
retryContext.retry_count = attempt + 1 retryContext.retryCount = attempt + 1
actions = await self.generateActionItems(taskStep, workflow, actions = await self.generateActionItems(taskStep, workflow,
previousResults=retryContext.previous_results, previousResults=retryContext.previousResults,
enhancedContext=retryContext) enhancedContext=retryContext)
# Log total actions count for this task # Log total actions count for this task
@ -302,6 +334,36 @@ class ActionplanMode(BaseMode):
taskIndex, actionNumber, totalActions) taskIndex, actionNumber, totalActions)
actionResults.append(result) actionResults.append(result)
# Enhanced validation: Content validation after each action (like React mode)
if getattr(self, 'workflowIntent', None) and result.documents:
# Pass ALL documents to validator - validator decides what to validate (generic approach)
# Pass taskStep so validator can use task.objective and format fields
validationResult = await self.contentValidator.validateContent(result.documents, self.workflowIntent, taskStep)
qualityScore = validationResult.get('qualityScore', 0.0)
if qualityScore is None:
qualityScore = 0.0
logger.info(f"Content validation for action {actionNumber}: {validationResult['overallSuccess']} (quality: {qualityScore:.2f})")
# Record validation result for adaptive learning
actionContext = {
'actionName': f"{action.execMethod}.{action.execAction}",
'workflowId': context.workflowId
}
self.adaptiveLearningEngine.recordValidationResult(
validationResult,
actionContext,
context.workflowId,
actionNumber
)
# Learn from feedback
feedback = self._collectFeedback(result, validationResult, self.workflowIntent)
self.learningEngine.learnFromFeedback(feedback, context, self.workflowIntent)
# Update progress
self.progressTracker.updateOperation(result, validationResult, self.workflowIntent)
if result.success: if result.success:
state.addSuccessfulAction(result) state.addSuccessfulAction(result)
else: else:
@ -333,47 +395,47 @@ class ActionplanMode(BaseMode):
logger.warning(f"Task step '{taskStep.objective}' requires retry: {reviewResult.improvements}") logger.warning(f"Task step '{taskStep.objective}' requires retry: {reviewResult.improvements}")
# Enhanced logging of criteria status # Enhanced logging of criteria status
if reviewResult.met_criteria: if reviewResult.metCriteria:
logger.info(f"Met criteria: {', '.join(reviewResult.met_criteria)}") logger.info(f"Met criteria: {', '.join(reviewResult.metCriteria)}")
if reviewResult.unmet_criteria: if reviewResult.unmetCriteria:
logger.warning(f"Unmet criteria: {', '.join(reviewResult.unmet_criteria)}") logger.warning(f"Unmet criteria: {', '.join(reviewResult.unmetCriteria)}")
state.incrementRetryCount() state.incrementRetryCount()
# Update retry context with retry information and criteria tracking # Update retry context with retry information and criteria tracking
if retryContext: if retryContext:
retryContext.retry_count = state.retry_count retryContext.retryCount = state.retry_count
retryContext.improvements = reviewResult.improvements retryContext.improvements = reviewResult.improvements
retryContext.previous_action_results = actionResults retryContext.previousActionResults = actionResults
retryContext.previous_review_result = reviewResult retryContext.previousReviewResult = reviewResult
retryContext.is_regeneration = True retryContext.isRegeneration = True
retryContext.failure_patterns = state.getFailurePatterns() retryContext.failurePatterns = state.getFailurePatterns()
retryContext.failed_actions = state.failed_actions retryContext.failedActions = state.failed_actions
retryContext.successful_actions = state.successful_actions retryContext.successfulActions = state.successful_actions
# Track criteria progress across retries # Track criteria progress across retries
if not hasattr(retryContext, 'criteria_progress'): if not hasattr(retryContext, 'criteriaProgress'):
retryContext.criteria_progress = { retryContext.criteriaProgress = {
'met_criteria': set(), 'met_criteria': set(),
'unmet_criteria': set(), 'unmet_criteria': set(),
'attempt_history': [] 'attempt_history': []
} }
# Update criteria progress # Update criteria progress
if reviewResult.met_criteria: if reviewResult.metCriteria:
retryContext.criteria_progress['met_criteria'].update(reviewResult.met_criteria) retryContext.criteriaProgress['met_criteria'].update(reviewResult.metCriteria)
if reviewResult.unmet_criteria: if reviewResult.unmetCriteria:
retryContext.criteria_progress['unmet_criteria'].update(reviewResult.unmet_criteria) retryContext.criteriaProgress['unmet_criteria'].update(reviewResult.unmetCriteria)
# Record this attempt's criteria status # Record this attempt's criteria status
attemptRecord = { attemptRecord = {
'attempt': state.retry_count, 'attempt': state.retry_count,
'met_criteria': reviewResult.met_criteria or [], 'met_criteria': reviewResult.metCriteria or [],
'unmet_criteria': reviewResult.unmet_criteria or [], 'unmet_criteria': reviewResult.unmetCriteria or [],
'quality_score': reviewResult.quality_score, 'quality_score': reviewResult.qualityScore,
'improvements': reviewResult.improvements or [] 'improvements': reviewResult.improvements or []
} }
retryContext.criteria_progress['attempt_history'].append(attemptRecord) retryContext.criteriaProgress['attempt_history'].append(attemptRecord)
# Create retry message # Create retry message
await self.messageCreator.createRetryMessage(taskStep, workflow, taskIndex, reviewResult) await self.messageCreator.createRetryMessage(taskStep, workflow, taskIndex, reviewResult)
@ -420,10 +482,10 @@ class ActionplanMode(BaseMode):
# Create proper context object for result review # Create proper context object for result review
reviewContext = ReviewContext( reviewContext = ReviewContext(
task_step=taskStep, taskStep=taskStep,
task_actions=taskActions, taskActions=taskActions,
action_results=actionResults, actionResults=actionResults,
step_result={ stepResult={
'successful_actions': sum(1 for result in actionResults if result.success), 'successful_actions': sum(1 for result in actionResults if result.success),
'total_actions': len(actionResults), 'total_actions': len(actionResults),
'results': [self._extractResultText(result) for result in actionResults if result.success], 'results': [self._extractResultText(result) for result in actionResults if result.success],
@ -437,8 +499,8 @@ class ActionplanMode(BaseMode):
for i, result in enumerate(actionResults) for i, result in enumerate(actionResults)
] ]
}, },
workflow_id=workflow.id, workflowId=workflow.id,
previous_results=[] previousResults=[]
) )
# Check workflow status before calling AI service # Check workflow status before calling AI service
@ -452,8 +514,8 @@ class ActionplanMode(BaseMode):
# Log result review prompt sent to AI # Log result review prompt sent to AI
logger.info("=== RESULT REVIEW PROMPT SENT TO AI ===") logger.info("=== RESULT REVIEW PROMPT SENT TO AI ===")
logger.info(f"Task: {taskStep.objective}") logger.info(f"Task: {taskStep.objective}")
logger.info(f"Action Results Count: {len(reviewContext.action_results) if reviewContext.action_results else 0}") logger.info(f"Action Results Count: {len(reviewContext.actionResults) if reviewContext.actionResults else 0}")
logger.info(f"Task Actions Count: {len(reviewContext.task_actions) if reviewContext.task_actions else 0}") logger.info(f"Task Actions Count: {len(reviewContext.taskActions) if reviewContext.taskActions else 0}")
# Centralized AI call: Result validation (balanced analysis) with placeholders # Centralized AI call: Result validation (balanced analysis) with placeholders
options = AiCallOptions( options = AiCallOptions(
@ -488,7 +550,7 @@ class ActionplanMode(BaseMode):
raise ValueError("Review response missing 'status' field") raise ValueError("Review response missing 'status' field")
review.setdefault('status', 'unknown') review.setdefault('status', 'unknown')
review.setdefault('reason', 'No reason provided') review.setdefault('reason', 'No reason provided')
review.setdefault('quality_score', 5) review.setdefault('quality_score', 5.0)
# Ensure improvements is a list # Ensure improvements is a list
improvements = review.get('improvements', []) improvements = review.get('improvements', [])
@ -511,31 +573,31 @@ class ActionplanMode(BaseMode):
status=review.get('status', 'unknown'), status=review.get('status', 'unknown'),
reason=review.get('reason', 'No reason provided'), reason=review.get('reason', 'No reason provided'),
improvements=improvements, improvements=improvements,
quality_score=review.get('quality_score', 5), qualityScore=float(review.get('quality_score', review.get('qualityScore', 5.0))),
missing_outputs=[], missingOutputs=[],
met_criteria=metCriteria, metCriteria=metCriteria,
unmet_criteria=unmetCriteria, unmetCriteria=unmetCriteria,
confidence=review.get('confidence', 0.5), confidence=review.get('confidence', 0.5),
# Extract user-friendly message if available # Extract user-friendly message if available
userMessage=review.get('userMessage', None) userMessage=review.get('userMessage', None)
) )
# Enhanced validation logging # Enhanced validation logging
logger.info(f"VALIDATION RESULT - Task: '{taskStep.objective}' - Status: {reviewResult.status.upper()}, Quality: {reviewResult.quality_score}/10") logger.info(f"VALIDATION RESULT - Task: '{taskStep.objective}' - Status: {reviewResult.status.upper()}, Quality: {reviewResult.qualityScore}/10")
if reviewResult.status == 'success': if reviewResult.status == 'success':
logger.info(f"VALIDATION SUCCESS - Task completed successfully") logger.info(f"VALIDATION SUCCESS - Task completed successfully")
if reviewResult.met_criteria: if reviewResult.metCriteria:
logger.info(f"Met criteria: {', '.join(reviewResult.met_criteria)}") logger.info(f"Met criteria: {', '.join(reviewResult.metCriteria)}")
elif reviewResult.status == 'retry': elif reviewResult.status == 'retry':
logger.warning(f"VALIDATION RETRY - Task requires retry: {reviewResult.improvements}") logger.warning(f"VALIDATION RETRY - Task requires retry: {reviewResult.improvements}")
if reviewResult.unmet_criteria: if reviewResult.unmetCriteria:
logger.warning(f"Unmet criteria: {', '.join(reviewResult.unmet_criteria)}") logger.warning(f"Unmet criteria: {', '.join(reviewResult.unmetCriteria)}")
else: else:
logger.error(f"VALIDATION FAILED - Task failed: {reviewResult.reason}") logger.error(f"VALIDATION FAILED - Task failed: {reviewResult.reason}")
logger.info(f"=== TASK COMPLETION REVIEW FINISHED ===") logger.info(f"=== TASK COMPLETION REVIEW FINISHED ===")
logger.info(f"Final Status: {reviewResult.status}") logger.info(f"Final Status: {reviewResult.status}")
logger.info(f"Quality Score: {reviewResult.quality_score}/10") logger.info(f"Quality Score: {reviewResult.qualityScore}/10")
logger.info(f"Improvements: {reviewResult.improvements}") logger.info(f"Improvements: {reviewResult.improvements}")
logger.info("=== END REVIEW ===") logger.info("=== END REVIEW ===")
@ -545,7 +607,7 @@ class ActionplanMode(BaseMode):
return ReviewResult( return ReviewResult(
status='failed', status='failed',
reason=str(e), reason=str(e),
quality_score=0 qualityScore=0.0
) )
def _createActionItem(self, actionData: Dict[str, Any]) -> ActionItem: def _createActionItem(self, actionData: Dict[str, Any]) -> ActionItem:
@ -613,6 +675,47 @@ class ActionplanMode(BaseMode):
# Join all document results with separators # Join all document results with separators
return "\n\n---\n\n".join(resultParts) if resultParts else "" return "\n\n---\n\n".join(resultParts) if resultParts else ""
def _collectFeedback(self, result: Any, validation: Dict[str, Any], intent: Dict[str, Any]) -> Dict[str, Any]:
"""Collects comprehensive feedback from action execution"""
try:
# Extract content summary
contentDelivered = ""
if result.documents:
firstDoc = result.documents[0]
if hasattr(firstDoc, 'documentData'):
data = firstDoc.documentData
if isinstance(data, dict) and 'content' in data:
content = str(data['content'])
contentDelivered = content[:100] + "..." if len(content) > 100 else content
else:
contentDelivered = str(data)[:100] + "..." if len(str(data)) > 100 else str(data)
return {
"actionAttempted": result.resultLabel or "unknown",
"parametersUsed": {}, # Would be extracted from action context
"contentDelivered": contentDelivered,
"intentMatchScore": validation.get('qualityScore', 0),
"qualityScore": validation.get('qualityScore', 0),
"issuesFound": validation.get('improvementSuggestions', []),
"learningOpportunities": validation.get('improvementSuggestions', []),
"userSatisfaction": None, # Would be collected from user feedback
"timestamp": datetime.now(timezone.utc).timestamp()
}
except Exception as e:
logger.error(f"Error collecting feedback: {str(e)}")
return {
"actionAttempted": "unknown",
"parametersUsed": {},
"contentDelivered": "",
"intentMatchScore": 0,
"qualityScore": 0,
"issuesFound": [],
"learningOpportunities": [],
"userSatisfaction": None,
"timestamp": datetime.now(timezone.utc).timestamp()
}
def _updateWorkflowBeforeExecutingTask(self, taskNumber: int): def _updateWorkflowBeforeExecutingTask(self, taskNumber: int):
"""Update workflow object before executing a task""" """Update workflow object before executing a task"""
try: try:
@ -689,57 +792,4 @@ class ActionplanMode(BaseMode):
logger.debug(f"Updated workflow totals: Tasks {self.workflow.totalTasks if hasattr(self.workflow, 'totalTasks') else 'N/A'}, Actions {self.workflow.totalActions if hasattr(self.workflow, 'totalActions') else 'N/A'}") logger.debug(f"Updated workflow totals: Tasks {self.workflow.totalTasks if hasattr(self.workflow, 'totalTasks') else 'N/A'}, Actions {self.workflow.totalActions if hasattr(self.workflow, 'totalActions') else 'N/A'}")
except Exception as e: except Exception as e:
logger.error(f"Error setting workflow totals: {str(e)}") logger.error(f"Error setting workflow totals: {str(e)}")
def _createActionItem(self, actionData: Dict[str, Any]) -> ActionItem:
"""Creates a new task action"""
try:
import uuid
# Ensure ID is present
if "id" not in actionData or not actionData["id"]:
actionData["id"] = f"action_{uuid.uuid4()}"
# Ensure required fields
if "status" not in actionData:
actionData["status"] = TaskStatus.PENDING
if "execMethod" not in actionData:
logger.error("execMethod is required for task action")
return None
if "execAction" not in actionData:
logger.error("execAction is required for task action")
return None
if "execParameters" not in actionData:
actionData["execParameters"] = {}
# Use generic field separation based on ActionItem model
simpleFields, objectFields = self.services.interfaceDbChat._separateObjectFields(ActionItem, actionData)
# Create action in database
createdAction = self.services.interfaceDbChat.db.recordCreate(ActionItem, simpleFields)
# Convert to ActionItem model
return ActionItem(
id=createdAction["id"],
execMethod=createdAction["execMethod"],
execAction=createdAction["execAction"],
execParameters=createdAction.get("execParameters", {}),
execResultLabel=createdAction.get("execResultLabel"),
expectedDocumentFormats=createdAction.get("expectedDocumentFormats"),
status=createdAction.get("status", TaskStatus.PENDING),
error=createdAction.get("error"),
retryCount=createdAction.get("retryCount", 0),
retryMax=createdAction.get("retryMax", 3),
processingTime=createdAction.get("processingTime"),
timestamp=float(createdAction.get("timestamp", self.services.utils.timestampGetUtc())),
result=createdAction.get("result"),
resultDocuments=createdAction.get("resultDocuments", []),
userMessage=createdAction.get("userMessage")
)
except Exception as e:
logger.error(f"Error creating task action: {str(e)}")
return None

View file

@ -9,7 +9,7 @@ from datetime import datetime, timezone
from typing import List, Dict, Any from typing import List, Dict, Any
from modules.datamodels.datamodelChat import ( from modules.datamodels.datamodelChat import (
TaskStep, TaskContext, TaskResult, ActionItem, TaskStatus, TaskStep, TaskContext, TaskResult, ActionItem, TaskStatus,
ActionResult ActionResult, Observation, ObservationPreview, ReviewResult
) )
from modules.datamodels.datamodelChat import ChatWorkflow from modules.datamodels.datamodelChat import ChatWorkflow
from modules.workflows.processing.modes.modeBase import BaseMode from modules.workflows.processing.modes.modeBase import BaseMode
@ -50,14 +50,23 @@ class ReactMode(BaseMode):
"""Execute task using React mode - iterative plan-act-observe-refine loop""" """Execute task using React mode - iterative plan-act-observe-refine loop"""
logger.info(f"=== STARTING TASK {taskIndex or '?'}: {taskStep.objective} ===") logger.info(f"=== STARTING TASK {taskIndex or '?'}: {taskStep.objective} ===")
# NEW: Analyze intents separately for proper validation vs task completion # Use workflow-level intent from planning phase (stored in workflow object)
# Workflow-level intent from cleaned original user prompt # This avoids redundant intent analysis - intent was already analyzed during task planning
original_prompt = self.services.currentUserPrompt if self.services and hasattr(self.services, 'currentUserPrompt') else taskStep.objective if hasattr(workflow, '_workflowIntent') and workflow._workflowIntent:
self.workflowIntent = await self.intentAnalyzer.analyzeUserIntent(original_prompt, context) self.workflowIntent = workflow._workflowIntent
# Task-level intent from current task objective (used only for task-scoped checks) logger.info(f"Using workflow intent from planning phase")
self.taskIntent = await self.intentAnalyzer.analyzeUserIntent(taskStep.objective, context) else:
logger.info(f"Intent analysis — workflow: {self.workflowIntent}") # Fallback: analyze if not available (shouldn't happen in normal flow)
logger.info(f"Intent analysis — task: {self.taskIntent}") original_prompt = self.services.currentUserPrompt if self.services and hasattr(self.services, 'currentUserPrompt') else taskStep.objective
self.workflowIntent = await self.intentAnalyzer.analyzeUserIntent(original_prompt, context)
logger.warning(f"Workflow intent not found in workflow object, analyzed fresh")
# Task-level intent is NOT needed - use task.objective + task format fields (dataType, expectedFormat, qualityRequirements)
# These format fields are populated from workflow intent during task planning
self.taskIntent = None # Removed redundant task-level intent analysis
logger.info(f"Workflow intent: {self.workflowIntent}")
if taskStep.dataType or taskStep.expectedFormat or taskStep.qualityRequirements:
logger.info(f"Task format info: dataType={taskStep.dataType}, expectedFormat={taskStep.expectedFormat}")
# NEW: Reset progress tracking for new task # NEW: Reset progress tracking for new task
self.progressTracker.reset() self.progressTracker.reset()
@ -75,7 +84,7 @@ class ReactMode(BaseMode):
logger.info(f"Using React mode execution with max_steps: {state.max_steps}") logger.info(f"Using React mode execution with max_steps: {state.max_steps}")
step = 1 step = 1
lastReviewDict = None decision = None
while step <= state.max_steps: while step <= state.max_steps:
self._checkWorkflowStopped(workflow) self._checkWorkflowStopped(workflow)
@ -93,14 +102,14 @@ class ReactMode(BaseMode):
result = await self._actExecute(context, selection, taskStep, workflow, step) result = await self._actExecute(context, selection, taskStep, workflow, step)
observation = self._observeBuild(result) observation = self._observeBuild(result)
# Attach deterministic label for clarity # Note: resultLabel is already set correctly in _observeBuild from actionResult.resultLabel
observation['resultLabel'] = result.resultLabel
# Content validation (against original cleaned user prompt / workflow intent) # Content validation (against original cleaned user prompt / workflow intent)
if getattr(self, 'workflowIntent', None) and result.documents: if getattr(self, 'workflowIntent', None) and result.documents:
# Pass ALL documents to validator - validator decides what to validate (generic approach) # Pass ALL documents to validator - validator decides what to validate (generic approach)
validationResult = await self.contentValidator.validateContent(result.documents, self.workflowIntent) # Pass taskStep so validator can use task.objective and format fields
observation['contentValidation'] = validationResult validationResult = await self.contentValidator.validateContent(result.documents, self.workflowIntent, taskStep)
observation.contentValidation = validationResult
quality_score = validationResult.get('qualityScore', 0.0) quality_score = validationResult.get('qualityScore', 0.0)
if quality_score is None: if quality_score is None:
quality_score = 0.0 quality_score = 0.0
@ -110,13 +119,13 @@ class ReactMode(BaseMode):
actionValue = selection.get('action', 'unknown') actionValue = selection.get('action', 'unknown')
actionContext = { actionContext = {
'actionName': actionValue, 'actionName': actionValue,
'workflowId': context.workflow_id 'workflowId': context.workflowId
} }
self.adaptiveLearningEngine.recordValidationResult( self.adaptiveLearningEngine.recordValidationResult(
validationResult, validationResult,
actionContext, actionContext,
context.workflow_id, context.workflowId,
step step
) )
@ -130,18 +139,16 @@ class ReactMode(BaseMode):
decision = await self._refineDecide(context, observation) decision = await self._refineDecide(context, observation)
# Store refinement decision in context for next iteration # Store refinement decision in context for next iteration
if not hasattr(context, 'previous_review_result') or context.previous_review_result is None: if not hasattr(context, 'previousReviewResult') or context.previousReviewResult is None:
context.previous_review_result = [] context.previousReviewResult = []
if decision: # Only append if decision is not None if decision: # Only append if decision is not None
context.previous_review_result.append(decision) context.previousReviewResult.append(decision)
# Update context with learnings from this step # Update context with learnings from this step
if decision and isinstance(decision, dict) and decision.get('reason'): if decision and decision.reason:
if not hasattr(context, 'improvements'): if not hasattr(context, 'improvements'):
context.improvements = [] context.improvements = []
context.improvements.append(f"Step {step}: {decision.get('reason')}") context.improvements.append(f"Step {step}: {decision.reason}")
lastReviewDict = decision if isinstance(decision, dict) else {}
# Create user-friendly message AFTER action execution # Create user-friendly message AFTER action execution
# Action completion message is now handled by the standard message creator in _actExecute # Action completion message is now handled by the standard message creator in _actExecute
@ -152,8 +159,9 @@ class ReactMode(BaseMode):
# NEW: Use adaptive stopping logic # NEW: Use adaptive stopping logic
progressState = self.progressTracker.getCurrentProgress() progressState = self.progressTracker.getCurrentProgress()
continueByProgress = self.progressTracker.shouldContinue(progressState, observation.get('contentValidation', {})) continueByProgress = self.progressTracker.shouldContinue(progressState, observation.contentValidation if observation.contentValidation else {})
continueByReview = shouldContinue(observation, lastReviewDict, step, state.max_steps) # Use Observation Pydantic model directly (decision is ReviewResult model)
continueByReview = shouldContinue(observation, decision, step, state.max_steps)
if not continueByProgress or not continueByReview: if not continueByProgress or not continueByReview:
logger.info(f"Stopping at step {step}: progress={continueByProgress}, review={continueByReview}") logger.info(f"Stopping at step {step}: progress={continueByProgress}, review={continueByReview}")
@ -163,13 +171,23 @@ class ReactMode(BaseMode):
# Summarize task result for react mode # Summarize task result for react mode
status = TaskStatus.COMPLETED status = TaskStatus.COMPLETED
success = True success = True
feedback = lastReviewDict.get('reason') if lastReviewDict and isinstance(lastReviewDict, dict) else 'Completed' # Get feedback from last decision if available
if lastReviewDict and isinstance(lastReviewDict, dict) and lastReviewDict.get('decision') == 'stop': lastDecision = context.previousReviewResult[-1] if hasattr(context, 'previousReviewResult') and context.previousReviewResult else None
feedback = lastDecision.reason if lastDecision and isinstance(lastDecision, ReviewResult) else 'Completed'
if lastDecision and isinstance(lastDecision, ReviewResult) and lastDecision.status == 'success':
success = True success = True
# Create proper ReviewResult for completion message
completionReviewResult = ReviewResult(
status='success',
reason=feedback,
qualityScore=lastDecision.qualityScore if lastDecision and isinstance(lastDecision, ReviewResult) else 8.0,
metCriteria=[],
improvements=[]
)
# Create task completion message # Create task completion message
await self.messageCreator.createTaskCompletionMessage(taskStep, workflow, taskIndex, totalTasks, await self.messageCreator.createTaskCompletionMessage(taskStep, workflow, taskIndex, totalTasks, completionReviewResult)
type('ReviewResult', (), {'reason': feedback, 'met_criteria': [], 'quality_score': 8})())
return TaskResult( return TaskResult(
taskId=taskStep.id, taskId=taskStep.id,
@ -186,6 +204,17 @@ class ReactMode(BaseMode):
placeholders = bundle.placeholders placeholders = bundle.placeholders
# Centralized AI call for plan selection (uses static planning parameters) # Centralized AI call for plan selection (uses static planning parameters)
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
# Create options for documentation/consistency (currently not passed to callAiPlanning API)
options = AiCallOptions(
operationType=OperationTypeEnum.PLAN,
priority=PriorityEnum.QUALITY,
compressPrompt=False,
compressContext=False,
processingMode=ProcessingModeEnum.DETAILED,
maxCost=0.10,
maxProcessingTime=30
)
response = await self.services.ai.callAiPlanning( response = await self.services.ai.callAiPlanning(
prompt=promptTemplate, prompt=promptTemplate,
placeholders=placeholders placeholders=placeholders
@ -264,9 +293,9 @@ class ReactMode(BaseMode):
from types import SimpleNamespace from types import SimpleNamespace
stage2Context = SimpleNamespace() stage2Context = SimpleNamespace()
# Copy essential fields from original context for fallbacks (snake_case for placeholderFactory compatibility) # Copy essential fields from original context for fallbacks
stage2Context.task_step = getattr(context, 'task_step', None) stage2Context.taskStep = getattr(context, 'taskStep', None)
stage2Context.workflow_id = getattr(context, 'workflow_id', None) stage2Context.workflowId = getattr(context, 'workflowId', None)
# Set Stage 1 data directly on the permissive context (snake_case for promptGenerationActionsReact compatibility) # Set Stage 1 data directly on the permissive context (snake_case for promptGenerationActionsReact compatibility)
if isinstance(selection, dict): if isinstance(selection, dict):
@ -284,6 +313,17 @@ class ReactMode(BaseMode):
placeholders = bundle.placeholders placeholders = bundle.placeholders
# Centralized AI call for parameter suggestion (uses static planning parameters) # Centralized AI call for parameter suggestion (uses static planning parameters)
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
# Create options for documentation/consistency (currently not passed to callAiPlanning API)
options = AiCallOptions(
operationType=OperationTypeEnum.PLAN,
priority=PriorityEnum.QUALITY,
compressPrompt=False,
compressContext=False,
processingMode=ProcessingModeEnum.DETAILED,
maxCost=0.10,
maxProcessingTime=30
)
paramsResp = await self.services.ai.callAiPlanning( paramsResp = await self.services.ai.callAiPlanning(
prompt=promptTemplate, prompt=promptTemplate,
placeholders=placeholders placeholders=placeholders
@ -351,7 +391,7 @@ class ReactMode(BaseMode):
return result return result
def _observeBuild(self, actionResult: ActionResult) -> Dict[str, Any]: def _observeBuild(self, actionResult: ActionResult) -> Observation:
"""Observe: build compact observation object from ActionResult with full document metadata""" """Observe: build compact observation object from ActionResult with full document metadata"""
previews = [] previews = []
notes = [] notes = []
@ -359,27 +399,38 @@ class ReactMode(BaseMode):
# Process all documents and show full metadata # Process all documents and show full metadata
for doc in actionResult.documents: for doc in actionResult.documents:
# Extract all available metadata without content # Extract all available metadata without content
docMetadata = { name = getattr(doc, 'fileName', None) or getattr(doc, 'documentName', 'Unknown')
"name": getattr(doc, 'fileName', None) or getattr(doc, 'documentName', 'Unknown'), mimeType = getattr(doc, 'mimeType', None)
"mimeType": getattr(doc, 'mimeType', 'Unknown'), size = getattr(doc, 'size', None)
"size": getattr(doc, 'size', 'Unknown'), created = getattr(doc, 'created', None)
"created": getattr(doc, 'created', 'Unknown'), modified = getattr(doc, 'modified', None)
"modified": getattr(doc, 'modified', 'Unknown'), typeGroup = getattr(doc, 'typeGroup', None)
"typeGroup": getattr(doc, 'typeGroup', 'Unknown'), documentId = getattr(doc, 'documentId', None)
"documentId": getattr(doc, 'documentId', 'Unknown'), reference = getattr(doc, 'reference', None)
"reference": getattr(doc, 'reference', 'Unknown')
}
# Remove 'Unknown' values to keep it clean
docMetadata = {k: v for k, v in docMetadata.items() if v != 'Unknown'}
# Add content size indicator instead of actual content # Add content size indicator instead of actual content
contentSize = None
if hasattr(doc, 'documentData') and doc.documentData: if hasattr(doc, 'documentData') and doc.documentData:
if isinstance(doc.documentData, dict) and 'content' in doc.documentData: if isinstance(doc.documentData, dict) and 'content' in doc.documentData:
contentLength = len(str(doc.documentData['content'])) contentLength = len(str(doc.documentData['content']))
docMetadata['contentSize'] = f"{contentLength} characters" contentSize = f"{contentLength} characters"
else: else:
contentLength = len(str(doc.documentData)) contentLength = len(str(doc.documentData))
docMetadata['contentSize'] = f"{contentLength} characters" contentSize = f"{contentLength} characters"
# Create ObservationPreview with only non-None values
preview = ObservationPreview(
name=name if name != 'Unknown' else 'Unknown Document',
mimeType=mimeType if mimeType and mimeType != 'Unknown' else None,
size=str(size) if size and size != 'Unknown' else None,
created=str(created) if created and created != 'Unknown' else None,
modified=str(modified) if modified and modified != 'Unknown' else None,
typeGroup=str(typeGroup) if typeGroup and typeGroup != 'Unknown' else None,
documentId=str(documentId) if documentId and documentId != 'Unknown' else None,
reference=str(reference) if reference and reference != 'Unknown' else None,
contentSize=contentSize
)
previews.append(preview)
# Extract comment if available # Extract comment if available
if hasattr(doc, 'documentData') and doc.documentData: if hasattr(doc, 'documentData') and doc.documentData:
@ -387,22 +438,21 @@ class ReactMode(BaseMode):
if isinstance(data, dict): if isinstance(data, dict):
comment = data.get("comment", "") comment = data.get("comment", "")
if comment: if comment:
notes.append(f"Document '{docMetadata.get('name', 'Unknown')}': {comment}") notes.append(f"Document '{name}': {comment}")
previews.append(docMetadata)
observation = { # Build observation with optional content analysis
"success": bool(actionResult.success), contentAnalysis = None
"resultLabel": actionResult.resultLabel or "", if self.currentIntent and actionResult and actionResult.documents:
"documentsCount": len(actionResult.documents) if actionResult.documents else 0,
"previews": previews,
"notes": notes
}
# NEW: Add content analysis if intent is available
if self.currentIntent and actionResult.documents:
contentAnalysis = self._analyzeContent(actionResult.documents) contentAnalysis = self._analyzeContent(actionResult.documents)
observation['contentAnalysis'] = contentAnalysis
observation = Observation(
success=bool(actionResult.success) if actionResult else False,
resultLabel=actionResult.resultLabel or "" if actionResult else "",
documentsCount=len(actionResult.documents) if actionResult and actionResult.documents else 0,
previews=previews,
notes=notes,
contentAnalysis=contentAnalysis
)
return observation return observation
@ -529,17 +579,27 @@ class ReactMode(BaseMode):
"timestamp": datetime.now(timezone.utc).timestamp() "timestamp": datetime.now(timezone.utc).timestamp()
} }
async def _refineDecide(self, context: TaskContext, observation: Dict[str, Any]) -> Dict[str, Any]: async def _refineDecide(self, context: TaskContext, observation: Observation) -> ReviewResult:
"""Refine: decide continue or stop, with reason""" """Refine: decide continue or stop, with reason"""
# Create proper ReviewContext for extractReviewContent # Create proper ReviewContext for extractReviewContent
from modules.datamodels.datamodelChat import ReviewContext from modules.datamodels.datamodelChat import ReviewContext
# Convert observation to dict for extractReviewContent (temporary compatibility)
observationDict = {
'success': observation.success,
'resultLabel': observation.resultLabel,
'documentsCount': observation.documentsCount,
'previews': [p.model_dump(exclude_none=True) if hasattr(p, 'model_dump') else p.dict() for p in observation.previews] if observation.previews else [],
'notes': observation.notes,
'contentValidation': observation.contentValidation if observation.contentValidation else {},
'contentAnalysis': observation.contentAnalysis if observation.contentAnalysis else {}
}
reviewContext = ReviewContext( reviewContext = ReviewContext(
task_step=context.task_step, taskStep=context.taskStep,
task_actions=[], taskActions=[],
action_results=[], # React mode doesn't have action results in this context actionResults=[], # React mode doesn't have action results in this context
step_result={'observation': observation}, stepResult={'observation': observationDict},
workflow_id=context.workflow_id, workflowId=context.workflowId,
previous_results=[] previousResults=[]
) )
baseReviewContent = extractReviewContent(reviewContext) baseReviewContent = extractReviewContent(reviewContext)
@ -547,24 +607,24 @@ class ReactMode(BaseMode):
# NEW: Add content validation to review content # NEW: Add content validation to review content
enhancedReviewContent = placeholders.get("REVIEW_CONTENT", "") enhancedReviewContent = placeholders.get("REVIEW_CONTENT", "")
if 'contentValidation' in observation: if observation.contentValidation:
validation = observation['contentValidation'] validation = observation.contentValidation
enhancedReviewContent += f"\n\nCONTENT VALIDATION:\n" enhancedReviewContent += f"\n\nCONTENT VALIDATION:\n"
enhancedReviewContent += f"Overall Success: {validation['overallSuccess']}\n" enhancedReviewContent += f"Overall Success: {validation.get('overallSuccess', False)}\n"
quality_score = validation.get('qualityScore', 0.0) quality_score = validation.get('qualityScore', 0.0)
if quality_score is None: if quality_score is None:
quality_score = 0.0 quality_score = 0.0
enhancedReviewContent += f"Quality Score: {quality_score:.2f}\n" enhancedReviewContent += f"Quality Score: {quality_score:.2f}\n"
if validation['improvementSuggestions']: if validation.get('improvementSuggestions'):
enhancedReviewContent += f"Improvement Suggestions: {', '.join(validation['improvementSuggestions'])}\n" enhancedReviewContent += f"Improvement Suggestions: {', '.join(validation['improvementSuggestions'])}\n"
# NEW: Add content analysis to review content # NEW: Add content analysis to review content
if 'contentAnalysis' in observation: if observation.contentAnalysis:
analysis = observation['contentAnalysis'] analysis = observation.contentAnalysis
enhancedReviewContent += f"\nCONTENT ANALYSIS:\n" enhancedReviewContent += f"\nCONTENT ANALYSIS:\n"
enhancedReviewContent += f"Content Type: {analysis['contentType']}\n" enhancedReviewContent += f"Content Type: {analysis.get('contentType', 'unknown')}\n"
enhancedReviewContent += f"Intent Match: {analysis['intentMatch']}\n" enhancedReviewContent += f"Intent Match: {analysis.get('intentMatch', False)}\n"
if analysis['contentSnippet']: if analysis.get('contentSnippet'):
enhancedReviewContent += f"Content Preview: {analysis['contentSnippet']}\n" enhancedReviewContent += f"Content Preview: {analysis['contentSnippet']}\n"
# NEW: Add progress state to review content # NEW: Add progress state to review content
@ -585,6 +645,17 @@ class ReactMode(BaseMode):
placeholders = bundle.placeholders placeholders = bundle.placeholders
# Centralized AI call for refinement decision (uses static planning parameters) # Centralized AI call for refinement decision (uses static planning parameters)
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
# Create options for documentation/consistency (currently not passed to callAiPlanning API)
options = AiCallOptions(
operationType=OperationTypeEnum.DATA_ANALYSE,
priority=PriorityEnum.BALANCED,
compressPrompt=True,
compressContext=False,
processingMode=ProcessingModeEnum.ADVANCED,
maxCost=0.05,
maxProcessingTime=30
)
resp = await self.services.ai.callAiPlanning( resp = await self.services.ai.callAiPlanning(
prompt=promptTemplate, prompt=promptTemplate,
placeholders=placeholders placeholders=placeholders
@ -592,7 +663,11 @@ class ReactMode(BaseMode):
# More robust JSON extraction # More robust JSON extraction
if not resp: if not resp:
decision = {"decision": "continue", "reason": "default"} return ReviewResult(
status="continue",
reason="default",
qualityScore=5.0
)
else: else:
# Find JSON boundaries more safely # Find JSON boundaries more safely
start_idx = resp.find('{') start_idx = resp.find('{')
@ -607,16 +682,34 @@ class ReactMode(BaseMode):
decision = json.loads(js) decision = json.loads(js)
# Ensure decision is a dictionary # Ensure decision is a dictionary
if not isinstance(decision, dict): if not isinstance(decision, dict):
decision = {"decision": "continue", "reason": "default"} return ReviewResult(
status="continue",
reason="default",
qualityScore=5.0
)
# Convert decision dict to ReviewResult model
decisionValue = decision.get('decision', 'continue')
# Map "stop" to "success" for ReviewResult status
status = 'success' if decisionValue == 'stop' else 'continue'
return ReviewResult(
status=status,
reason=decision.get('reason', 'No reason provided'),
qualityScore=float(decision.get('quality_score', decision.get('qualityScore', 5.0))),
confidence=float(decision.get('confidence', 0.5)),
userMessage=decision.get('userMessage', None)
)
except Exception as e: except Exception as e:
logger.warning(f"Failed to parse refinement decision JSON: {e}") logger.warning(f"Failed to parse refinement decision JSON: {e}")
decision = {"decision": "continue", "reason": "default"} return ReviewResult(
status="continue",
return decision reason="default",
qualityScore=5.0
)
async def _createReactActionMessage(self, workflow: ChatWorkflow, selection: Dict[str, Any], async def _createReactActionMessage(self, workflow: ChatWorkflow, selection: Dict[str, Any],
step: int, maxSteps: int, taskIndex: int, messageType: str, step: int, maxSteps: int, taskIndex: int, messageType: str,
result: ActionResult = None, observation: Dict[str, Any] = None): result: ActionResult = None, observation: Observation = None):
"""Create user-friendly messages for React workflow actions""" """Create user-friendly messages for React workflow actions"""
try: try:
action = selection.get('action', {}) action = selection.get('action', {})
@ -641,7 +734,7 @@ class ReactMode(BaseMode):
messageContent = f"{successIcon} **Step {step} Complete**\n\n{userMessage}" messageContent = f"{successIcon} **Step {step} Complete**\n\n{userMessage}"
status = "step" status = "step"
actionProgress = "success" if result and result.success else "fail" actionProgress = "success" if result and result.success else "fail"
documentsLabel = observation.get('resultLabel') if observation else f"action_{step}_result" documentsLabel = observation.resultLabel if observation else f"action_{step}_result"
else: else:
return return
@ -690,7 +783,7 @@ Return only the user-friendly message, no technical details."""
return f"Executing {method}.{actionName} action..." return f"Executing {method}.{actionName} action..."
async def _generateActionResultMessage(self, method: str, actionName: str, result: ActionResult, async def _generateActionResultMessage(self, method: str, actionName: str, result: ActionResult,
observation: Dict[str, Any], userLanguage: str): observation: Observation, userLanguage: str):
"""Generate user-friendly message explaining action results""" """Generate user-friendly message explaining action results"""
try: try:
# Build result context # Build result context
@ -698,8 +791,8 @@ Return only the user-friendly message, no technical details."""
if result and result.documents: if result and result.documents:
docCount = len(result.documents) docCount = len(result.documents)
resultContext = f"Generated {docCount} document(s)" resultContext = f"Generated {docCount} document(s)"
elif observation and observation.get('documentsCount', 0) > 0: elif observation and observation.documentsCount > 0:
docCount = observation.get('documentsCount', 0) docCount = observation.documentsCount
resultContext = f"Generated {docCount} document(s)" resultContext = f"Generated {docCount} document(s)"
# Create AI prompt for result message # Create AI prompt for result message

View file

@ -2,9 +2,8 @@
# Contains all execution state management logic # Contains all execution state management logic
import logging import logging
from typing import List from typing import List, Optional
from modules.datamodels.datamodelChat import TaskStep from modules.datamodels.datamodelChat import TaskStep, ActionResult, Observation
from modules.datamodels.datamodelChat import ActionResult
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -57,24 +56,48 @@ class TaskExecutionState:
patterns.append("permission_issues") patterns.append("permission_issues")
return list(set(patterns)) return list(set(patterns))
def shouldContinue(observation, review=None, current_step: int = 0, max_steps: int = 5) -> bool: def shouldContinue(observation: Optional[Observation], review=None, current_step: int = 0, max_steps: int = 5) -> bool:
"""Helper to decide if the iterative loop should continue """Helper to decide if the iterative loop should continue
- Stop if review indicates 'stop' or success criteria are met
- Stop on failure with no retry path Args:
observation: Observation Pydantic model with action execution results
review: ReviewResult or dict with review decision (optional)
current_step: Current step number in the iteration
max_steps: Maximum allowed steps
Returns:
bool: True if loop should continue, False if should stop
Logic:
- Stop if max steps reached - Stop if max steps reached
- Stop if review indicates 'stop' or success criteria are met
- Continue if observation indicates failure but allow one more step (caller caps by max_steps)
""" """
try: try:
# Stop if max steps reached
if current_step >= max_steps: if current_step >= max_steps:
return False return False
if review and isinstance(review, dict):
decision = review.get('decision') or review.get('status') # Check review decision (can be ReviewResult model or dict)
if decision in ('stop', 'success'): if review:
return False if hasattr(review, 'status'):
# If observation exists but indicates hard failure with no documents repeatedly # ReviewResult Pydantic model
if observation and isinstance(observation, dict): if review.status in ('stop', 'success'):
if observation.get('success') is False and observation.get('documentsCount', 0) == 0: return False
# allow next step once; the caller can cap by max_steps elif isinstance(review, dict):
# Legacy dict format
decision = review.get('decision') or review.get('status')
if decision in ('stop', 'success'):
return False
# Check observation: if hard failure with no documents, allow one more step
# The caller will enforce max_steps limit
if observation:
if observation.success is False and observation.documentsCount == 0:
# Allow next step once; the caller caps by max_steps
return True return True
return True return True
except Exception: except Exception as e:
logger.warning(f"Error in shouldContinue: {e}")
return False return False

View file

@ -44,12 +44,12 @@ def extractUserPrompt(context: Any) -> str:
try: try:
services = getattr(context, 'services', None) services = getattr(context, 'services', None)
# Determine raw user prompt from services or task_step # Determine raw user prompt from services or taskStep
rawPrompt = None rawPrompt = None
if services and getattr(services, 'currentUserPrompt', None): if services and getattr(services, 'currentUserPrompt', None):
rawPrompt = services.currentUserPrompt rawPrompt = services.currentUserPrompt
elif hasattr(context, 'task_step') and context.task_step: elif hasattr(context, 'taskStep') and context.taskStep:
rawPrompt = context.task_step.objective or 'No request specified' rawPrompt = context.taskStep.objective or 'No request specified'
else: else:
rawPrompt = 'No request specified' rawPrompt = 'No request specified'
@ -60,8 +60,8 @@ def extractUserPrompt(context: Any) -> str:
return rawPrompt return rawPrompt
except Exception: except Exception:
# Robust fallback behavior # Robust fallback behavior
if hasattr(context, 'task_step') and context.task_step: if hasattr(context, 'taskStep') and context.taskStep:
return context.task_step.objective or 'No request specified' return context.taskStep.objective or 'No request specified'
return 'No request specified' return 'No request specified'
def extractWorkflowHistory(service: Any, context: Any) -> str: def extractWorkflowHistory(service: Any, context: Any) -> str:
@ -232,10 +232,10 @@ def getPreviousRoundContext(services, workflow: Any) -> str:
def extractReviewContent(context: Any) -> str: def extractReviewContent(context: Any) -> str:
"""Extract review content for result validation. Maps to {{KEY:REVIEW_CONTENT}}""" """Extract review content for result validation. Maps to {{KEY:REVIEW_CONTENT}}"""
try: try:
if hasattr(context, 'action_results') and context.action_results: if hasattr(context, 'actionResults') and context.actionResults:
# Build result summary # Build result summary
result_summary = "" result_summary = ""
for i, result in enumerate(context.action_results): for i, result in enumerate(context.actionResults):
result_summary += f"\nRESULT {i+1}:\n" result_summary += f"\nRESULT {i+1}:\n"
result_summary += f" Success: {result.success}\n" result_summary += f" Success: {result.success}\n"
if result.error: if result.error:
@ -264,37 +264,49 @@ def extractReviewContent(context: Any) -> str:
return result_summary return result_summary
elif hasattr(context, 'observation') and context.observation: elif hasattr(context, 'observation') and context.observation:
# For observation data, show full content but handle documents specially # For observation data, show full content but handle documents specially
if isinstance(context.observation, dict): # Handle both Pydantic Observation model and dict format
# Create a copy to modify from modules.datamodels.datamodelChat import Observation
obs_copy = context.observation.copy()
if isinstance(context.observation, Observation):
# If there are previews with documents, show only metadata # Convert Pydantic model to dict
if 'previews' in obs_copy and isinstance(obs_copy['previews'], list): obs_dict = context.observation.model_dump(exclude_none=True) if hasattr(context.observation, 'model_dump') else context.observation.dict()
for preview in obs_copy['previews']: elif isinstance(context.observation, dict):
if isinstance(preview, dict) and 'snippet' in preview: obs_dict = context.observation.copy()
# Replace snippet with metadata indicator
preview['snippet'] = f"[Content: {len(preview.get('snippet', ''))} characters]"
return json.dumps(obs_copy, indent=2, ensure_ascii=False)
else: else:
return json.dumps(context.observation, ensure_ascii=False) # Fallback: try to serialize as-is
elif hasattr(context, 'step_result') and context.step_result and 'observation' in context.step_result: obs_dict = context.observation.model_dump(exclude_none=True) if hasattr(context.observation, 'model_dump') else context.observation.dict()
# For observation data in step_result, show full content but handle documents specially
observation = context.step_result['observation'] # If there are previews with documents, show only metadata
if isinstance(observation, dict): if 'previews' in obs_dict and isinstance(obs_dict['previews'], list):
# Create a copy to modify for preview in obs_dict['previews']:
obs_copy = observation.copy() if isinstance(preview, dict) and 'snippet' in preview:
# Replace snippet with metadata indicator
# If there are previews with documents, show only metadata preview['snippet'] = f"[Content: {len(preview.get('snippet', ''))} characters]"
if 'previews' in obs_copy and isinstance(obs_copy['previews'], list):
for preview in obs_copy['previews']: return json.dumps(obs_dict, indent=2, ensure_ascii=False)
if isinstance(preview, dict) and 'snippet' in preview: elif hasattr(context, 'stepResult') and context.stepResult and 'observation' in context.stepResult:
# Replace snippet with metadata indicator # For observation data in stepResult, show full content but handle documents specially
preview['snippet'] = f"[Content: {len(preview.get('snippet', ''))} characters]" observation = context.stepResult['observation']
# Handle both Pydantic Observation model and dict format
return json.dumps(obs_copy, indent=2, ensure_ascii=False) from modules.datamodels.datamodelChat import Observation
if isinstance(observation, Observation):
# Convert Pydantic model to dict
obs_dict = observation.model_dump(exclude_none=True) if hasattr(observation, 'model_dump') else observation.dict()
elif isinstance(observation, dict):
obs_dict = observation.copy()
else: else:
return json.dumps(observation, ensure_ascii=False) # Fallback: try to serialize
obs_dict = observation.model_dump(exclude_none=True) if hasattr(observation, 'model_dump') else observation.dict()
# If there are previews with documents, show only metadata
if 'previews' in obs_dict and isinstance(obs_dict['previews'], list):
for preview in obs_dict['previews']:
if isinstance(preview, dict) and 'snippet' in preview:
# Replace snippet with metadata indicator
preview['snippet'] = f"[Content: {len(preview.get('snippet', ''))} characters]"
return json.dumps(obs_dict, indent=2, ensure_ascii=False)
else: else:
return "No review content available" return "No review content available"
except Exception as e: except Exception as e:
@ -304,11 +316,11 @@ def extractReviewContent(context: Any) -> str:
def extractPreviousActionResults(context: Any) -> str: def extractPreviousActionResults(context: Any) -> str:
"""Extract previous action results for learning context. Maps to {{KEY:PREVIOUS_ACTION_RESULTS}}""" """Extract previous action results for learning context. Maps to {{KEY:PREVIOUS_ACTION_RESULTS}}"""
try: try:
if not hasattr(context, 'previous_action_results') or not context.previous_action_results: if not hasattr(context, 'previousActionResults') or not context.previousActionResults:
return "No previous actions executed yet" return "No previous actions executed yet"
results = [] results = []
for i, result in enumerate(context.previous_action_results[-5:], 1): # Last 5 results for i, result in enumerate(context.previousActionResults[-5:], 1): # Last 5 results
if hasattr(result, 'resultLabel') and hasattr(result, 'status'): if hasattr(result, 'resultLabel') and hasattr(result, 'status'):
status = "SUCCESS" if result.status == "completed" else "FAILED" status = "SUCCESS" if result.status == "completed" else "FAILED"
results.append(f"Action {i}: {result.resultLabel} - {status}") results.append(f"Action {i}: {result.resultLabel} - {status}")
@ -332,15 +344,15 @@ def extractLearningsAndImprovements(context: Any) -> str:
learnings.append(f"- {improvement}") learnings.append(f"- {improvement}")
# Get failure patterns # Get failure patterns
if hasattr(context, 'failure_patterns') and context.failure_patterns and isinstance(context.failure_patterns, list): if hasattr(context, 'failurePatterns') and context.failurePatterns and isinstance(context.failurePatterns, list):
learnings.append("FAILURE PATTERNS TO AVOID:") learnings.append("FAILURE PATTERNS TO AVOID:")
for pattern in context.failure_patterns[-3:]: # Last 3 patterns for pattern in context.failurePatterns[-3:]: # Last 3 patterns
learnings.append(f"- {pattern}") learnings.append(f"- {pattern}")
# Get successful actions # Get successful actions
if hasattr(context, 'successful_actions') and context.successful_actions and isinstance(context.successful_actions, list): if hasattr(context, 'successfulActions') and context.successfulActions and isinstance(context.successfulActions, list):
learnings.append("SUCCESSFUL APPROACHES:") learnings.append("SUCCESSFUL APPROACHES:")
for action in context.successful_actions[-3:]: # Last 3 successful for action in context.successfulActions[-3:]: # Last 3 successful
learnings.append(f"- {action}") learnings.append(f"- {action}")
return "\n".join(learnings) if learnings else "No learnings available yet" return "\n".join(learnings) if learnings else "No learnings available yet"
@ -351,11 +363,11 @@ def extractLearningsAndImprovements(context: Any) -> str:
def extractLatestRefinementFeedback(context: Any) -> str: def extractLatestRefinementFeedback(context: Any) -> str:
"""Extract the latest refinement feedback. Maps to {{KEY:LATEST_REFINEMENT_FEEDBACK}}""" """Extract the latest refinement feedback. Maps to {{KEY:LATEST_REFINEMENT_FEEDBACK}}"""
try: try:
if not hasattr(context, 'previous_review_result') or not context.previous_review_result or not isinstance(context.previous_review_result, list): if not hasattr(context, 'previousReviewResult') or not context.previousReviewResult or not isinstance(context.previousReviewResult, list):
return "No previous refinement feedback available" return "No previous refinement feedback available"
# Get the most recent refinement decision # Get the most recent refinement decision
latest_decision = context.previous_review_result[-1] latest_decision = context.previousReviewResult[-1]
if not isinstance(latest_decision, dict): if not isinstance(latest_decision, dict):
return "No previous refinement feedback available" return "No previous refinement feedback available"

View file

@ -169,8 +169,8 @@ Excludes documents/connections/history entirely.
# determine action objective if available, else fall back to user prompt # determine action objective if available, else fall back to user prompt
if hasattr(context, 'action_objective') and context.action_objective: if hasattr(context, 'action_objective') and context.action_objective:
actionObjective = context.action_objective actionObjective = context.action_objective
elif hasattr(context, 'task_step') and context.task_step and getattr(context.task_step, 'objective', None): elif hasattr(context, 'taskStep') and context.taskStep and getattr(context.taskStep, 'objective', None):
actionObjective = context.task_step.objective actionObjective = context.taskStep.objective
else: else:
actionObjective = extractUserPrompt(context) actionObjective = extractUserPrompt(context)

View file

@ -80,8 +80,8 @@ Break down user requests into logical, executable task steps.
"id": "task_1", "id": "task_1",
"objective": "Clear business objective focusing on what to deliver", "objective": "Clear business objective focusing on what to deliver",
"dependencies": ["task_0"], "dependencies": ["task_0"],
"success_criteria": ["measurable criteria 1", "measurable criteria 2"], "successCriteria": ["measurable criteria 1", "measurable criteria 2"],
"estimated_complexity": "low|medium|high", "estimatedComplexity": "low|medium|high",
"userMessage": "What this task will accomplish in language '{{KEY:USER_LANGUAGE}}'" "userMessage": "What this task will accomplish in language '{{KEY:USER_LANGUAGE}}'"
}} }}
], ],

View file

@ -286,9 +286,9 @@ class WorkflowProcessor:
status = taskResult.status if taskResult else 'unknown' status = taskResult.status if taskResult else 'unknown'
# Handle both TaskResult and ReviewResult objects # Handle both TaskResult and ReviewResult objects
if hasattr(taskResult, 'met_criteria'): if hasattr(taskResult, 'metCriteria'):
# This is a ReviewResult object # This is a ReviewResult object
met = taskResult.met_criteria if taskResult.met_criteria else [] met = taskResult.metCriteria if taskResult.metCriteria else []
reviewResult = taskResult.model_dump() reviewResult = taskResult.model_dump()
else: else:
# This is a TaskResult object # This is a TaskResult object

View file

@ -338,22 +338,22 @@ class WorkflowManager:
# Build TaskContext (mode-specific behavior is inside WorkflowProcessor) # Build TaskContext (mode-specific behavior is inside WorkflowProcessor)
task_context = TaskContext( task_context = TaskContext(
task_step=task_step, taskStep=task_step,
workflow=workflow, workflow=workflow,
workflow_id=workflow.id, workflowId=workflow.id,
available_documents=None, availableDocuments=None,
available_connections=None, availableConnections=None,
previous_results=previous_results, previousResults=previous_results,
previous_handover=None, previousHandover=None,
improvements=[], improvements=[],
retry_count=0, retryCount=0,
previous_action_results=[], previousActionResults=[],
previous_review_result=None, previousReviewResult=None,
is_regeneration=False, isRegeneration=False,
failure_patterns=[], failurePatterns=[],
failed_actions=[], failedActions=[],
successful_actions=[], successfulActions=[],
criteria_progress={ criteriaProgress={
'met_criteria': set(), 'met_criteria': set(),
'unmet_criteria': set(), 'unmet_criteria': set(),
'attempt_history': [] 'attempt_history': []