374 lines
16 KiB
Python
374 lines
16 KiB
Python
"""
|
|
Workflow execution models for action definitions, AI responses, and workflow-level structures.
|
|
"""
|
|
|
|
from typing import Dict, Any, List, Optional, TYPE_CHECKING
|
|
from pydantic import BaseModel, Field
|
|
from modules.shared.attributeUtils import registerModelLabels
|
|
from modules.shared.jsonUtils import extractJsonString, tryParseJson, repairBrokenJson
|
|
|
|
# Import DocumentReferenceList at runtime (needed for ActionDefinition)
|
|
from modules.datamodels.datamodelDocref import DocumentReferenceList
|
|
|
|
# Forward references for circular imports (use string annotations)
|
|
if TYPE_CHECKING:
|
|
from modules.datamodels.datamodelChat import ChatDocument, ActionResult
|
|
from modules.datamodels.datamodelExtraction import ExtractionOptions
|
|
|
|
|
|
class ActionDefinition(BaseModel):
|
|
"""Action definition with selection and parameters from planning phase"""
|
|
|
|
# Core action selection (Stage 1)
|
|
action: str = Field(description="Compound action name (method.action)")
|
|
actionObjective: str = Field(description="Objective for this action")
|
|
parametersContext: Optional[str] = Field(
|
|
None,
|
|
description="Context for parameter generation"
|
|
)
|
|
learnings: List[str] = Field(
|
|
default_factory=list,
|
|
description="Learnings from previous actions"
|
|
)
|
|
|
|
# Resources (ALWAYS defined in Stage 1 if action needs them)
|
|
documentList: Optional[DocumentReferenceList] = Field(
|
|
None,
|
|
description="Document references (ALWAYS defined in Stage 1 if action needs documents)"
|
|
)
|
|
connectionReference: Optional[str] = Field(
|
|
None,
|
|
description="Connection reference (ALWAYS defined in Stage 1 if action needs connection)"
|
|
)
|
|
|
|
# Parameters (may be defined in Stage 1 OR Stage 2, depending on action and actionObjective)
|
|
parameters: Optional[Dict[str, Any]] = Field(
|
|
None,
|
|
description="Action-specific parameters (generated in Stage 2 for complex actions, or inferred from actionObjective for simple actions)"
|
|
)
|
|
|
|
def hasParameters(self) -> bool:
|
|
"""Check if parameters have been generated (Stage 2 complete or inferred)"""
|
|
return self.parameters is not None
|
|
|
|
def needsStage2(self) -> bool:
|
|
"""Determine if Stage 2 parameter generation is needed (generic, deterministic check)
|
|
|
|
Generic logic (works for any action, dynamically added or removed):
|
|
- If parameters are already set → Stage 2 not needed
|
|
- If parameters are None → Stage 2 needed (to generate parameters from actionObjective and context)
|
|
|
|
Note: Stage 1 always defines documentList and connectionReference if the action needs them.
|
|
Stage 2 only generates the action-specific parameters dictionary.
|
|
"""
|
|
# Generic check: if parameters are not set, Stage 2 is needed
|
|
return self.parameters is None
|
|
|
|
def updateFromStage1StringReferences(self, stringRefs: Optional[List[str]], connectionRef: Optional[str]):
|
|
"""Update documentList and connectionReference from Stage 1 string references
|
|
|
|
Called when Stage 1 AI returns string references that need to be converted to typed models.
|
|
"""
|
|
if stringRefs:
|
|
self.documentList = DocumentReferenceList.from_string_list(stringRefs)
|
|
if connectionRef:
|
|
self.connectionReference = connectionRef
|
|
|
|
|
|
class AiResponseMetadata(BaseModel):
|
|
"""Metadata for AI response (varies by operation type)."""
|
|
|
|
# Document Generation Metadata
|
|
title: Optional[str] = Field(None, description="Document title")
|
|
filename: Optional[str] = Field(None, description="Document filename")
|
|
|
|
# Operation-Specific Metadata
|
|
operationType: Optional[str] = Field(None, description="Type of operation performed")
|
|
schemaVersion: Optional[str] = Field(None, description="Schema version (e.g., 'parameters_v1')", alias="schema")
|
|
extractionMethod: Optional[str] = Field(None, description="Method used for extraction")
|
|
sourceDocuments: Optional[List[str]] = Field(None, description="Source document references")
|
|
|
|
# Additional metadata (for extensibility)
|
|
additionalData: Optional[Dict[str, Any]] = Field(None, description="Additional operation-specific metadata")
|
|
|
|
@classmethod
|
|
def fromDict(cls, data: Optional[Dict[str, Any]]) -> Optional["AiResponseMetadata"]:
|
|
"""Create AiResponseMetadata from dict with camelCase field names"""
|
|
if not data:
|
|
return None
|
|
|
|
# Convert snake_case keys to camelCase if needed (for backward compatibility)
|
|
convertedData = {}
|
|
for key, value in data.items():
|
|
# Keep camelCase as-is, convert snake_case if present
|
|
if '_' in key:
|
|
# Convert snake_case to camelCase
|
|
parts = key.split('_')
|
|
camelKey = parts[0] + ''.join(word.capitalize() for word in parts[1:])
|
|
convertedData[camelKey] = value
|
|
else:
|
|
convertedData[key] = value
|
|
|
|
return cls(**convertedData)
|
|
|
|
|
|
class DocumentData(BaseModel):
|
|
"""Single document in response"""
|
|
documentName: str = Field(description="Document name")
|
|
documentData: Any = Field(description="Document data (can be str, bytes, dict, etc.)")
|
|
mimeType: str = Field(description="MIME type of the document")
|
|
|
|
|
|
class ExtractContentParameters(BaseModel):
|
|
"""Parameters for extraction action.
|
|
|
|
This model is defined together with the `methodAi.extractContent()` action function.
|
|
All action parameter models follow this pattern: defined in the same module as the action.
|
|
However, since this is a workflow-level model used across the system, it's defined here.
|
|
"""
|
|
documentList: DocumentReferenceList = Field(description="Document references to extract content from")
|
|
extractionOptions: Optional[Any] = Field( # ExtractionOptions - forward reference
|
|
None,
|
|
description="Extraction options (determined dynamically based on task and document characteristics)"
|
|
)
|
|
|
|
|
|
class AiResponse(BaseModel):
|
|
"""Unified response from all AI calls (planning, text, documents)"""
|
|
|
|
content: str = Field(description="Response content (JSON string for planning, text for analysis, unified JSON for documents)")
|
|
metadata: Optional[AiResponseMetadata] = Field(
|
|
None,
|
|
description="Response metadata (varies by operation type)"
|
|
)
|
|
documents: Optional[List[DocumentData]] = Field(
|
|
None,
|
|
description="Generated documents (only for document generation operations)"
|
|
)
|
|
|
|
def toJson(self) -> Dict[str, Any]:
|
|
"""
|
|
Convert AI response content to JSON using enhanced stabilizing failsafe conversion methods.
|
|
Centralizes AI result to JSON conversion in one place.
|
|
|
|
Uses methods from jsonUtils:
|
|
- tryParseJson() - Safe parsing with error handling
|
|
- repairBrokenJson() - Repairs broken/incomplete JSON
|
|
- extractJsonString() - Extracts JSON from text with code fences
|
|
|
|
Returns:
|
|
Dict containing the parsed JSON content, or a safe fallback structure if parsing fails.
|
|
- If content is valid JSON dict: returns the dict directly
|
|
- If content is valid JSON list: wraps in {"data": [...]}
|
|
- If content is broken JSON: attempts repair using repairBrokenJson()
|
|
- If all parsing fails: returns {"content": "...", "parseError": True}
|
|
"""
|
|
# If content is already a dict, return it directly
|
|
if isinstance(self.content, dict):
|
|
return self.content
|
|
|
|
# If content is already a list, wrap it
|
|
if isinstance(self.content, list):
|
|
return {"data": self.content}
|
|
|
|
# Convert to string if needed
|
|
contentStr = str(self.content) if not isinstance(self.content, str) else self.content
|
|
|
|
# First, try to extract JSON from text (handles code fences, etc.)
|
|
extractedJson = extractJsonString(contentStr)
|
|
|
|
# Try to parse as JSON (returns tuple: obj, error, cleaned_str)
|
|
parsedJson, parseError, _ = tryParseJson(extractedJson)
|
|
|
|
if parsedJson is not None and parseError is None:
|
|
# If it's a dict, return directly
|
|
if isinstance(parsedJson, dict):
|
|
return parsedJson
|
|
# If it's a list, wrap in dict
|
|
elif isinstance(parsedJson, list):
|
|
return {"data": parsedJson}
|
|
|
|
# Try to repair broken JSON
|
|
repairedJson = repairBrokenJson(contentStr)
|
|
if repairedJson:
|
|
# repairBrokenJson returns Optional[Dict[str, Any]] - always a dict or None
|
|
if isinstance(repairedJson, dict):
|
|
return repairedJson
|
|
|
|
# All parsing failed - return safe fallback
|
|
contentStr = str(self.content) if not isinstance(self.content, str) else self.content
|
|
return {"content": contentStr, "parseError": True}
|
|
|
|
|
|
# Workflow-level models
|
|
|
|
class RequestContext(BaseModel):
|
|
"""Normalized request context from user input"""
|
|
|
|
originalPrompt: str = Field(description="Original user prompt")
|
|
documents: List[Any] = Field( # ChatDocument - forward reference
|
|
default_factory=list,
|
|
description="Documents provided by user"
|
|
)
|
|
userLanguage: str = Field(description="User's language")
|
|
detectedComplexity: str = Field(
|
|
description="Complexity level: simple, moderate, complex"
|
|
)
|
|
requiresDocuments: bool = Field(default=False, description="Whether request requires documents")
|
|
requiresWebResearch: bool = Field(default=False, description="Whether request requires web research")
|
|
requiresAnalysis: bool = Field(default=False, description="Whether request requires analysis")
|
|
expectedOutputFormat: Optional[str] = Field(None, description="Expected output format")
|
|
expectedOutputType: Optional[str] = Field(None, description="Expected output type: answer, document, analysis")
|
|
|
|
|
|
class UnderstandingResult(BaseModel):
|
|
"""Result from initial understanding phase (combined AI call)"""
|
|
|
|
parameters: Dict[str, Any] = Field(
|
|
default_factory=dict,
|
|
description="Basic parameters (language, format, detail level)"
|
|
)
|
|
intention: Dict[str, Any] = Field(
|
|
default_factory=dict,
|
|
description="User intention (primaryGoal, secondaryGoals, intentionType)"
|
|
)
|
|
context: Dict[str, Any] = Field(
|
|
default_factory=dict,
|
|
description="Extracted context (topics, requirements, constraints)"
|
|
)
|
|
documentReferences: List[Dict[str, Any]] = Field(
|
|
default_factory=list,
|
|
description="Document references with purpose and relevance"
|
|
)
|
|
tasks: List["TaskDefinition"] = Field( # Forward reference
|
|
default_factory=list,
|
|
description="Task definitions with deliverables"
|
|
)
|
|
|
|
|
|
class TaskDefinition(BaseModel):
|
|
"""Task definition from understanding phase"""
|
|
|
|
id: str = Field(description="Task identifier")
|
|
objective: str = Field(description="Task objective")
|
|
deliverable: Dict[str, Any] = Field(
|
|
description="Deliverable specification (type, format, style, detailLevel)"
|
|
)
|
|
requiresWebResearch: bool = Field(default=False, description="Whether task requires web research")
|
|
requiresDocumentAnalysis: bool = Field(default=False, description="Whether task requires document analysis")
|
|
requiresContentGeneration: bool = Field(default=True, description="Whether task requires content generation")
|
|
requiredDocuments: List[str] = Field(
|
|
default_factory=list,
|
|
description="Document references needed for this task"
|
|
)
|
|
extractionOptions: Optional[Any] = Field( # ExtractionOptions - forward reference
|
|
None,
|
|
description="Extraction options for document processing (determined dynamically based on task and document characteristics)"
|
|
)
|
|
|
|
|
|
class TaskResult(BaseModel):
|
|
"""Result from task execution"""
|
|
|
|
taskId: str = Field(description="Task identifier")
|
|
actionResult: Any = Field(description="ActionResult from task execution") # ActionResult - forward reference
|
|
|
|
|
|
# Register model labels for UI
|
|
registerModelLabels(
|
|
"ActionDefinition",
|
|
{"en": "Action Definition", "fr": "Définition d'action"},
|
|
{
|
|
"action": {"en": "Action", "fr": "Action"},
|
|
"actionObjective": {"en": "Action Objective", "fr": "Objectif de l'action"},
|
|
"parametersContext": {"en": "Parameters Context", "fr": "Contexte des paramètres"},
|
|
"learnings": {"en": "Learnings", "fr": "Apprentissages"},
|
|
"documentList": {"en": "Document List", "fr": "Liste de documents"},
|
|
"connectionReference": {"en": "Connection Reference", "fr": "Référence de connexion"},
|
|
"parameters": {"en": "Parameters", "fr": "Paramètres"},
|
|
},
|
|
)
|
|
|
|
registerModelLabels(
|
|
"AiResponse",
|
|
{"en": "AI Response", "fr": "Réponse IA"},
|
|
{
|
|
"content": {"en": "Content", "fr": "Contenu"},
|
|
"metadata": {"en": "Metadata", "fr": "Métadonnées"},
|
|
"documents": {"en": "Documents", "fr": "Documents"},
|
|
},
|
|
)
|
|
|
|
registerModelLabels(
|
|
"AiResponseMetadata",
|
|
{"en": "AI Response Metadata", "fr": "Métadonnées de réponse IA"},
|
|
{
|
|
"title": {"en": "Title", "fr": "Titre"},
|
|
"filename": {"en": "Filename", "fr": "Nom de fichier"},
|
|
"operationType": {"en": "Operation Type", "fr": "Type d'opération"},
|
|
"schemaVersion": {"en": "Schema Version", "fr": "Version du schéma"},
|
|
"extractionMethod": {"en": "Extraction Method", "fr": "Méthode d'extraction"},
|
|
"sourceDocuments": {"en": "Source Documents", "fr": "Documents sources"},
|
|
},
|
|
)
|
|
|
|
registerModelLabels(
|
|
"DocumentData",
|
|
{"en": "Document Data", "fr": "Données de document"},
|
|
{
|
|
"documentName": {"en": "Document Name", "fr": "Nom du document"},
|
|
"documentData": {"en": "Document Data", "fr": "Données du document"},
|
|
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
|
|
},
|
|
)
|
|
|
|
registerModelLabels(
|
|
"RequestContext",
|
|
{"en": "Request Context", "fr": "Contexte de requête"},
|
|
{
|
|
"originalPrompt": {"en": "Original Prompt", "fr": "Invite originale"},
|
|
"documents": {"en": "Documents", "fr": "Documents"},
|
|
"userLanguage": {"en": "User Language", "fr": "Langue de l'utilisateur"},
|
|
"detectedComplexity": {"en": "Detected Complexity", "fr": "Complexité détectée"},
|
|
"requiresDocuments": {"en": "Requires Documents", "fr": "Nécessite des documents"},
|
|
"requiresWebResearch": {"en": "Requires Web Research", "fr": "Nécessite une recherche web"},
|
|
"requiresAnalysis": {"en": "Requires Analysis", "fr": "Nécessite une analyse"},
|
|
},
|
|
)
|
|
|
|
registerModelLabels(
|
|
"UnderstandingResult",
|
|
{"en": "Understanding Result", "fr": "Résultat de compréhension"},
|
|
{
|
|
"parameters": {"en": "Parameters", "fr": "Paramètres"},
|
|
"intention": {"en": "Intention", "fr": "Intention"},
|
|
"context": {"en": "Context", "fr": "Contexte"},
|
|
"documentReferences": {"en": "Document References", "fr": "Références de documents"},
|
|
"tasks": {"en": "Tasks", "fr": "Tâches"},
|
|
},
|
|
)
|
|
|
|
registerModelLabels(
|
|
"TaskDefinition",
|
|
{"en": "Task Definition", "fr": "Définition de tâche"},
|
|
{
|
|
"id": {"en": "ID", "fr": "ID"},
|
|
"objective": {"en": "Objective", "fr": "Objectif"},
|
|
"deliverable": {"en": "Deliverable", "fr": "Livrable"},
|
|
"requiresWebResearch": {"en": "Requires Web Research", "fr": "Nécessite une recherche web"},
|
|
"requiresDocumentAnalysis": {"en": "Requires Document Analysis", "fr": "Nécessite une analyse de document"},
|
|
"requiresContentGeneration": {"en": "Requires Content Generation", "fr": "Nécessite une génération de contenu"},
|
|
"requiredDocuments": {"en": "Required Documents", "fr": "Documents requis"},
|
|
"extractionOptions": {"en": "Extraction Options", "fr": "Options d'extraction"},
|
|
},
|
|
)
|
|
|
|
registerModelLabels(
|
|
"TaskResult",
|
|
{"en": "Task Result", "fr": "Résultat de tâche"},
|
|
{
|
|
"taskId": {"en": "Task ID", "fr": "ID de tâche"},
|
|
"actionResult": {"en": "Action Result", "fr": "Résultat d'action"},
|
|
},
|
|
)
|
|
|