From b44c46be2312251423713ddfa2fc1192f71b1857 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Sun, 5 Oct 2025 15:01:22 +0200
Subject: [PATCH] cleanup prompts and placeholders
---
modules/datamodels/datamodelWorkflow.py | 35 ++
modules/services/__init__.py | 3 +-
modules/services/serviceAi/mainServiceAi.py | 78 +++-
modules/workflows/methods/methodOutlook.py | 16 +-
modules/workflows/methods/methodSharepoint.py | 8 +-
.../processing/modes/modeActionplan.py | 68 +---
.../workflows/processing/modes/modeReact.py | 64 +--
.../processing/shared/placeholderFactory.py | 383 ++----------------
.../shared/placeholderFactoryReactOnly.py | 189 ---------
.../promptGenerationActionsActionplan.py | 62 ++-
.../shared/promptGenerationActionsReact.py | 92 ++++-
.../shared/promptGenerationTaskplan.py | 24 +-
modules/workflows/workflowManager.py | 127 +++++-
13 files changed, 461 insertions(+), 688 deletions(-)
delete mode 100644 modules/workflows/processing/shared/placeholderFactoryReactOnly.py
diff --git a/modules/datamodels/datamodelWorkflow.py b/modules/datamodels/datamodelWorkflow.py
index 6cf6ab18..c1da40c4 100644
--- a/modules/datamodels/datamodelWorkflow.py
+++ b/modules/datamodels/datamodelWorkflow.py
@@ -409,3 +409,38 @@ register_model_labels(
# Resolve forward references
from modules.datamodels.datamodelChat import ChatWorkflow
TaskContext.update_forward_refs()
+
+# -----------------------------
+# Prompt helper datamodels
+# -----------------------------
+
+class PromptPlaceholder(BaseModel, ModelMixin):
+ label: str
+ content: str
+ summaryAllowed: bool = Field(default=False, description="Whether host may summarize content before sending to AI")
+
+
+register_model_labels(
+ "PromptPlaceholder",
+ {"en": "Prompt Placeholder", "fr": "Espace réservé d'invite"},
+ {
+ "label": {"en": "Label", "fr": "Libellé"},
+ "content": {"en": "Content", "fr": "Contenu"},
+ "summaryAllowed": {"en": "Summary Allowed", "fr": "Résumé autorisé"},
+ },
+)
+
+
+class PromptBundle(BaseModel, ModelMixin):
+ prompt: str
+ placeholders: List[PromptPlaceholder] = Field(default_factory=list)
+
+
+register_model_labels(
+ "PromptBundle",
+ {"en": "Prompt Bundle", "fr": "Lot d'invite"},
+ {
+ "prompt": {"en": "Prompt", "fr": "Invite"},
+ "placeholders": {"en": "Placeholders", "fr": "Espaces réservés"},
+ },
+)
diff --git a/modules/services/__init__.py b/modules/services/__init__.py
index 589286f2..384ae6af 100644
--- a/modules/services/__init__.py
+++ b/modules/services/__init__.py
@@ -41,7 +41,8 @@ class Services:
def __init__(self, user: User, workflow: ChatWorkflow = None):
self.user: User = user
self.workflow: ChatWorkflow = workflow
- self.currentUserPrompt: str = "" # Store the current user prompt for easy access
+ self.currentUserPrompt: str = "" # Cleaned/normalized user intent for the current round
+ self.rawUserPrompt: str = "" # Original raw user message for the current round
# Initialize interfaces
diff --git a/modules/services/serviceAi/mainServiceAi.py b/modules/services/serviceAi/mainServiceAi.py
index ccea4c36..19e2e1f0 100644
--- a/modules/services/serviceAi/mainServiceAi.py
+++ b/modules/services/serviceAi/mainServiceAi.py
@@ -1,5 +1,6 @@
import logging
from typing import Dict, Any, List, Optional, Tuple, Union
+from modules.datamodels.datamodelWorkflow import PromptPlaceholder
from modules.datamodels.datamodelChat import ChatDocument
from modules.services.serviceExtraction.mainServiceExtraction import ExtractionService
@@ -704,7 +705,7 @@ class AiService:
self,
prompt: str,
documents: Optional[List[ChatDocument]] = None,
- placeholders: Optional[Dict[str, str]] = None,
+ placeholders: Optional[List[PromptPlaceholder]] = None,
options: Optional[AiCallOptions] = None
) -> str:
"""
@@ -713,7 +714,7 @@ class AiService:
Args:
prompt: The main prompt for the AI call
documents: Optional list of documents to process
- placeholders: Optional dictionary of placeholder replacements for planning calls
+ placeholders: Optional list of placeholder replacements for planning calls
options: AI call configuration options
Returns:
@@ -728,12 +729,19 @@ class AiService:
if options is None:
options = AiCallOptions()
+ # Normalize placeholders from List[PromptPlaceholder]
+ placeholders_dict: Dict[str, str] = {}
+ placeholders_meta: Dict[str, bool] = {}
+ if placeholders:
+ placeholders_dict = {p.label: p.content for p in placeholders}
+ placeholders_meta = {p.label: bool(getattr(p, 'summaryAllowed', False)) for p in placeholders}
+
# Auto-determine call type based on documents and operation type
call_type = self._determineCallType(documents, options.operationType)
options.callType = call_type
if call_type == "planning":
- return await self._callAiPlanning(prompt, placeholders, options)
+ return await self._callAiPlanning(prompt, placeholders_dict, placeholders_meta, options)
else:
# Set processDocumentsIndividually from the legacy parameter if not set in options
if options.processDocumentsIndividually is None and documents:
@@ -758,6 +766,7 @@ class AiService:
self,
prompt: str,
placeholders: Optional[Dict[str, str]],
+ placeholdersMeta: Optional[Dict[str, bool]],
options: AiCallOptions
) -> str:
"""
@@ -766,8 +775,67 @@ class AiService:
# Ensure aiObjects is initialized
await self._ensureAiObjectsInitialized()
- # Build full prompt with placeholders
- full_prompt = self._buildPromptWithPlaceholders(prompt, placeholders)
+ # Build full prompt with placeholders; if too large, summarize summaryAllowed placeholders proportionally
+ effective_placeholders = placeholders or {}
+ full_prompt = self._buildPromptWithPlaceholders(prompt, effective_placeholders)
+
+ if options.compressPrompt and placeholdersMeta:
+ # Determine model capacity
+ try:
+ caps = self._getModelCapabilitiesForContent(full_prompt, None, options)
+ max_bytes = caps.get("maxContextBytes", len(full_prompt.encode("utf-8")))
+ except Exception:
+ max_bytes = len(full_prompt.encode("utf-8"))
+
+ current_bytes = len(full_prompt.encode("utf-8"))
+ if current_bytes > max_bytes:
+ # Compute total bytes contributed by allowed placeholders (approximate by content length)
+ allowed_labels = [l for l, allow in placeholdersMeta.items() if allow]
+ allowed_sizes = {l: len((effective_placeholders.get(l) or "").encode("utf-8")) for l in allowed_labels}
+ total_allowed = sum(allowed_sizes.values())
+
+ overage = current_bytes - max_bytes
+ if total_allowed > 0 and overage > 0:
+ # Target total for allowed after reduction
+ target_allowed = max(total_allowed - overage, 0)
+ # Global ratio to apply across allowed placeholders
+ ratio = target_allowed / total_allowed if total_allowed > 0 else 1.0
+ ratio = max(0.0, min(1.0, ratio))
+
+ reduced: Dict[str, str] = {}
+ for label, content in effective_placeholders.items():
+ if label in allowed_labels and isinstance(content, str) and len(content) > 0:
+ old_len = len(content)
+ # Reduce by proportional ratio on characters (fallback if empty)
+ reduction_factor = ratio if old_len > 0 else 1.0
+ reduced[label] = self._reduceText(content, reduction_factor)
+ else:
+ reduced[label] = content
+
+ effective_placeholders = reduced
+ full_prompt = self._buildPromptWithPlaceholders(prompt, effective_placeholders)
+
+ # If still slightly over, perform a second-pass fine adjustment with updated ratio
+ current_bytes = len(full_prompt.encode("utf-8"))
+ if current_bytes > max_bytes and total_allowed > 0:
+ overage2 = current_bytes - max_bytes
+ # Recompute allowed sizes after first reduction
+ allowed_sizes2 = {l: len((effective_placeholders.get(l) or "").encode("utf-8")) for l in allowed_labels}
+ total_allowed2 = sum(allowed_sizes2.values())
+ if total_allowed2 > 0 and overage2 > 0:
+ target_allowed2 = max(total_allowed2 - overage2, 0)
+ ratio2 = target_allowed2 / total_allowed2
+ ratio2 = max(0.0, min(1.0, ratio2))
+ reduced2: Dict[str, str] = {}
+ for label, content in effective_placeholders.items():
+ if label in allowed_labels and isinstance(content, str) and len(content) > 0:
+ old_len = len(content)
+ reduction_factor = ratio2 if old_len > 0 else 1.0
+ reduced2[label] = self._reduceText(content, reduction_factor)
+ else:
+ reduced2[label] = content
+ effective_placeholders = reduced2
+ full_prompt = self._buildPromptWithPlaceholders(prompt, effective_placeholders)
# Make AI call using AiObjects (let it handle model selection)
diff --git a/modules/workflows/methods/methodOutlook.py b/modules/workflows/methods/methodOutlook.py
index 9f39dc52..2593b9cd 100644
--- a/modules/workflows/methods/methodOutlook.py
+++ b/modules/workflows/methods/methodOutlook.py
@@ -303,7 +303,7 @@ class MethodOutlook(MethodBase):
WORKFLOW POSITION: Use for email analysis, before composing responses
Parameters:
- connectionReference (str): Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS list)
+ connectionReference (str): Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS_INDEX list)
folder (str, optional): Email folder to read from (default: "Inbox")
limit (int, optional): Maximum number of emails to read (default: 10)
filter (str, optional): Filter criteria for emails. Supports: Email address (e.g., "user@domain.com") - filters by sender, Search queries (e.g., "from:user@domain.com", "subject:meeting"), Text content (e.g., "project update") - searches in subject
@@ -462,7 +462,7 @@ class MethodOutlook(MethodBase):
WORKFLOW POSITION: Use for finding specific emails, before reading or responding
Parameters:
- connectionReference (str): Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS list)
+ connectionReference (str): Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS_INDEX list)
query (str): Search query
folder (str, optional): Folder to search in (default: "All")
limit (int, optional): Maximum number of results (default: 20)
@@ -669,7 +669,7 @@ class MethodOutlook(MethodBase):
List email drafts in Outlook
Parameters:
- connectionReference (str): Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS list)
+ connectionReference (str): Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS_INDEX list)
folder (str, optional): Folder to search for drafts (default: "Drafts")
limit (int, optional): Maximum number of drafts to list (default: 20)
expectedDocumentFormats (list, optional): Expected document formats with extension, mimeType, description
@@ -792,7 +792,7 @@ class MethodOutlook(MethodBase):
Find email drafts across all folders in Outlook
Parameters:
- connectionReference (str): Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS list)
+ connectionReference (str): Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS_INDEX list)
limit (int, optional): Maximum number of drafts to find (default: 50)
expectedDocumentFormats (list, optional): Expected document formats with extension, mimeType, description
"""
@@ -929,7 +929,7 @@ class MethodOutlook(MethodBase):
Check the contents of the Drafts folder directly
Parameters:
- connectionReference (str): Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS list)
+ connectionReference (str): Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS_INDEX list)
limit (int, optional): Maximum number of drafts to check (default: 20)
expectedDocumentFormats (list, optional): Expected document formats with extension, mimeType, description
"""
@@ -1052,7 +1052,7 @@ class MethodOutlook(MethodBase):
WORKFLOW POSITION: Use when you have complete email information ready to send
Parameters:
- connectionReference (str): REQUIRED - Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS list)
+ connectionReference (str): REQUIRED - Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS_INDEX list)
to (List[str]): REQUIRED - Email recipient addresses
subject (str): REQUIRED - Email subject line
body (str): REQUIRED - Email body content
@@ -1216,7 +1216,7 @@ class MethodOutlook(MethodBase):
WORKFLOW POSITION: Use when you need AI to generate email content from available information
Parameters:
- connectionReference (str): REQUIRED - Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS list)
+ connectionReference (str): REQUIRED - Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS_INDEX list)
to (List[str]): REQUIRED - Email recipient addresses
context (str): REQUIRED - Context information for email composition
documentList (List[str], optional): Document references to include as context and attachments
@@ -1460,7 +1460,7 @@ Make sure the email is:
Check if the current Microsoft connection has the necessary permissions for Outlook operations.
Parameters:
- connectionReference (str): Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS list) to check
+ connectionReference (str): Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS_INDEX list) to check
"""
try:
connectionReference = parameters.get("connectionReference")
diff --git a/modules/workflows/methods/methodSharepoint.py b/modules/workflows/methods/methodSharepoint.py
index d7aac499..c58cf89d 100644
--- a/modules/workflows/methods/methodSharepoint.py
+++ b/modules/workflows/methods/methodSharepoint.py
@@ -454,7 +454,7 @@ class MethodSharepoint(MethodBase):
WORKFLOW POSITION: Use first to locate documents, before readDocuments or uploadDocument
Parameters:
- connectionReference (str): Microsoft connection reference (must be a connection label from AVAILABLE_CONNECTIONS list)
+ connectionReference (str): Microsoft connection reference (must be a connection label from AVAILABLE_CONNECTIONS_INDEX list)
site (str, optional): Site hint (e.g., "SSS", "KM XYZ")
searchQuery (str): Search query - "budget", "folders:alpha", "files:budget", "/Documents/Project1", "namepart1 namepart2 namepart3". Use "folders:" prefix when user wants to store files or find folders
maxResults (int, optional): Max results (default: 100)
@@ -834,7 +834,7 @@ class MethodSharepoint(MethodBase):
Parameters:
documentList (list): Reference(s) to the document list to read
- connectionReference (str): Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS list)
+ connectionReference (str): Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS_INDEX list)
pathObject (str, optional): Path object to locate documents. This can ONLY be a reference to a result from sharepoint.findDocumentPath action
pathQuery (str, optional): Path query to locate documents, only if no pathObject is provided (e.g., "/Documents/Project1", "*" for all sites)
includeMetadata (bool, optional): Whether to include metadata (default: True)
@@ -1117,7 +1117,7 @@ class MethodSharepoint(MethodBase):
WORKFLOW POSITION: Use after document generation, as final storage step
Parameters:
- connectionReference (str): Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS list)
+ connectionReference (str): Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS_INDEX list)
pathObject (str, optional): Path object to locate documents. This can ONLY be a reference to a result from sharepoint.findDocumentPath action
pathQuery (str, optional): Path query to locate documents, only if no pathObject is provided (e.g., "/Documents/Project1", "*" for all sites)
documentList (list): Reference(s) to the document list to upload
@@ -1477,7 +1477,7 @@ class MethodSharepoint(MethodBase):
WORKFLOW POSITION: Use for exploring SharePoint content, before findDocumentPath
Parameters:
- connectionReference (str): Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS list)
+ connectionReference (str): Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS_INDEX list)
pathObject (str, optional): Path object to locate documents. This can ONLY be a reference to a result from sharepoint.findDocumentPath action
pathQuery (str, optional): Path query to locate documents, only if no pathObject is provided (e.g., "/Documents/Project1", "*" for all sites)
includeSubfolders (bool, optional): Whether to include subfolders (default: False)
diff --git a/modules/workflows/processing/modes/modeActionplan.py b/modules/workflows/processing/modes/modeActionplan.py
index b3a8cb76..547c266b 100644
--- a/modules/workflows/processing/modes/modeActionplan.py
+++ b/modules/workflows/processing/modes/modeActionplan.py
@@ -14,17 +14,8 @@ from modules.datamodels.datamodelAi import AiCallOptions, OperationType, Process
from modules.workflows.processing.modes.modeBase import BaseMode
from modules.workflows.processing.shared.executionState import TaskExecutionState
from modules.workflows.processing.shared.promptGenerationActionsActionplan import (
- createActionDefinitionPromptTemplate,
- createResultReviewPromptTemplate
-)
-from modules.workflows.processing.shared.placeholderFactory import (
- extractUserPrompt,
- extractAvailableDocuments,
- extractWorkflowHistory,
- extractAvailableMethods,
- extractUserLanguage,
- extractAvailableConnections,
- extractReviewContent
+ generateActionDefinitionPrompt,
+ generateResultReviewPrompt
)
logger = logging.getLogger(__name__)
@@ -126,29 +117,10 @@ class ActionplanMode(BaseMode):
# Check workflow status before calling AI service
self._checkWorkflowStopped(workflow)
- # Generate the action definition prompt with placeholders
- actionPromptTemplate = createActionDefinitionPromptTemplate()
-
- # Extract content for placeholders
- userPrompt = extractUserPrompt(actionContext)
- # Populate context.available_documents with formatted document string (like old system)
- actionContext.available_documents = self.services.workflow.getAvailableDocuments(workflow)
- availableDocuments = extractAvailableDocuments(actionContext)
- workflowHistory = extractWorkflowHistory(self.services, actionContext)
- availableMethods = extractAvailableMethods(self.services)
- userLanguage = extractUserLanguage(self.services)
- # Action planner also needs connections for parameter generation (like old system)
- availableConnectionsStr = extractAvailableConnections(self.services)
-
- # Create placeholders dictionary
- placeholders = {
- "USER_PROMPT": userPrompt,
- "AVAILABLE_DOCUMENTS": availableDocuments,
- "WORKFLOW_HISTORY": workflowHistory,
- "AVAILABLE_METHODS": availableMethods,
- "USER_LANGUAGE": userLanguage,
- "AVAILABLE_CONNECTIONS": availableConnectionsStr
- }
+ # Build prompt bundle (template + placeholders)
+ bundle = generateActionDefinitionPrompt(self.services, actionContext)
+ actionPromptTemplate = bundle.prompt
+ placeholders = bundle.placeholders
# Trace action planning prompt
self._writeTraceLog("Action Plan Prompt", actionPromptTemplate)
@@ -165,11 +137,7 @@ class ActionplanMode(BaseMode):
maxProcessingTime=30
)
- prompt = await self.services.ai.callAi(
- prompt=actionPromptTemplate,
- placeholders=placeholders,
- options=options
- )
+ prompt = await self.services.ai.callAi(prompt=actionPromptTemplate, placeholders=placeholders, options=options)
# Check if AI response is valid
if not prompt:
@@ -485,18 +453,10 @@ class ActionplanMode(BaseMode):
# Check workflow status before calling AI service
self._checkWorkflowStopped(workflow)
- # Use placeholder-based review prompt
- promptTemplate = createResultReviewPromptTemplate()
-
- # Extract content for placeholders
- userPrompt = extractUserPrompt(reviewContext)
- reviewContent = extractReviewContent(reviewContext)
-
- # Create placeholders dictionary
- placeholders = {
- "USER_PROMPT": userPrompt,
- "REVIEW_CONTENT": reviewContent
- }
+ # Build prompt bundle for result review
+ bundle = generateResultReviewPrompt(reviewContext)
+ promptTemplate = bundle.prompt
+ placeholders = bundle.placeholders
# Log result review prompt sent to AI
logger.info("=== RESULT REVIEW PROMPT SENT TO AI ===")
@@ -518,11 +478,7 @@ class ActionplanMode(BaseMode):
maxProcessingTime=30
)
- response = await self.services.ai.callAi(
- prompt=promptTemplate,
- placeholders=placeholders,
- options=options
- )
+ response = await self.services.ai.callAi(prompt=promptTemplate, placeholders=placeholders, options=options)
# Log result review response received
logger.info("=== RESULT REVIEW AI RESPONSE RECEIVED ===")
diff --git a/modules/workflows/processing/modes/modeReact.py b/modules/workflows/processing/modes/modeReact.py
index 82f6ccf6..107022a5 100644
--- a/modules/workflows/processing/modes/modeReact.py
+++ b/modules/workflows/processing/modes/modeReact.py
@@ -15,15 +15,12 @@ from modules.datamodels.datamodelChat import ChatWorkflow
from modules.datamodels.datamodelAi import AiCallOptions, OperationType, ProcessingMode, Priority
from modules.workflows.processing.modes.modeBase import BaseMode
from modules.workflows.processing.shared.executionState import TaskExecutionState, shouldContinue
-from modules.workflows.processing.shared.placeholderFactoryReactOnly import (
- ContextAwarePlaceholders,
- WorkflowPhase
-)
from modules.workflows.processing.shared.promptGenerationActionsReact import (
- createReactPlanSelectionPromptTemplate,
- createReactParametersPromptTemplate,
- createReactRefinementPromptTemplate
+ generateReactPlanSelectionPrompt,
+ generateReactParametersPrompt,
+ generateReactRefinementPrompt
)
+from modules.workflows.processing.shared.placeholderFactory import extractReviewContent
from modules.workflows.processing.adaptive import IntentAnalyzer, ContentValidator, LearningEngine, ProgressTracker
logger = logging.getLogger(__name__)
@@ -39,8 +36,7 @@ class ReactMode(BaseMode):
self.learningEngine = LearningEngine()
self.progressTracker = ProgressTracker()
self.currentIntent = None
- # Initialize context-aware placeholder service
- self.placeholderService = ContextAwarePlaceholders(services)
+ # Placeholder service no longer used; prompts are generated directly
async def generateTaskActions(self, taskStep: TaskStep, workflow: ChatWorkflow,
previousResults: List = None, enhancedContext: TaskContext = None) -> List[TaskAction]:
@@ -176,13 +172,9 @@ class ReactMode(BaseMode):
async def _planSelect(self, context: TaskContext) -> Dict[str, Any]:
"""Plan: select exactly one action. Returns {"action": {method, name}}"""
- promptTemplate = createReactPlanSelectionPromptTemplate()
-
- # Use context-aware placeholders for React plan selection (minimal context)
- placeholders = await self.placeholderService.getPlaceholders(
- WorkflowPhase.REACT_PLAN_SELECTION,
- context
- )
+ bundle = generateReactPlanSelectionPrompt(self.services, context)
+ promptTemplate = bundle.prompt
+ placeholders = bundle.placeholders
self._writeTraceLog("React Plan Selection Prompt", promptTemplate)
self._writeTraceLog("React Plan Selection Placeholders", placeholders)
@@ -230,24 +222,9 @@ class ReactMode(BaseMode):
parameters = selection['parameters']
else:
logger.info("No parameters in action selection, requesting from AI")
- promptTemplate = createReactParametersPromptTemplate()
-
- # Get action parameter description (not function signature)
- from modules.workflows.processing.shared.methodDiscovery import methods, getActionParameterSignature
- actionParameters = getActionParameterSignature(methodName, actionName, methods)
-
- selectedAction = compoundActionName
-
- # Use context-aware placeholders for React parameters (full context)
- additional_data = {
- "SELECTED_ACTION": selectedAction,
- "ACTION_SIGNATURE": actionParameters
- }
- placeholders = await self.placeholderService.getPlaceholders(
- WorkflowPhase.REACT_PARAMETERS,
- context,
- additional_data
- )
+ bundle = generateReactParametersPrompt(self.services, context, compoundActionName)
+ promptTemplate = bundle.prompt
+ placeholders = bundle.placeholders
self._writeTraceLog("React Parameters Prompt", promptTemplate)
self._writeTraceLog("React Parameters Placeholders", placeholders)
@@ -485,8 +462,6 @@ class ReactMode(BaseMode):
async def _refineDecide(self, context: TaskContext, observation: Dict[str, Any]) -> Dict[str, Any]:
"""Refine: decide continue or stop, with reason"""
- promptTemplate = createReactRefinementPromptTemplate()
-
# Create proper ReviewContext for extractReviewContent
from modules.datamodels.datamodelWorkflow import ReviewContext
reviewContext = ReviewContext(
@@ -497,14 +472,9 @@ class ReactMode(BaseMode):
workflow_id=context.workflow_id,
previous_results=[]
)
-
- # Use context-aware placeholders for React refinement
- additional_data = {"REVIEW_CONTENT": self.placeholderService._getReviewContent(reviewContext)}
- placeholders = await self.placeholderService.getPlaceholders(
- WorkflowPhase.RESULT_REVIEW,
- context,
- additional_data
- )
+
+ baseReviewContent = extractReviewContent(reviewContext)
+ placeholders = {"REVIEW_CONTENT": baseReviewContent}
# NEW: Add content validation to review content
enhancedReviewContent = placeholders.get("REVIEW_CONTENT", "")
@@ -538,9 +508,13 @@ class ReactMode(BaseMode):
# Update placeholders with enhanced review content
placeholders["REVIEW_CONTENT"] = enhancedReviewContent
+ bundle = generateReactRefinementPrompt(self.services, context, enhancedReviewContent)
+ promptTemplate = bundle.prompt
+ placeholders = bundle.placeholders
+
self._writeTraceLog("React Refinement Prompt", promptTemplate)
self._writeTraceLog("React Refinement Placeholders", placeholders)
-
+
# Centralized AI call for refinement decision (balanced analysis)
options = AiCallOptions(
operationType=OperationType.ANALYSE_CONTENT,
diff --git a/modules/workflows/processing/shared/placeholderFactory.py b/modules/workflows/processing/shared/placeholderFactory.py
index e4dfd224..e79c75a2 100644
--- a/modules/workflows/processing/shared/placeholderFactory.py
+++ b/modules/workflows/processing/shared/placeholderFactory.py
@@ -8,21 +8,24 @@ NAMING CONVENTION:
- Placeholder names are in UPPER_CASE with underscores
- Function names are in camelCase
-MAPPING TABLE:
-{{KEY:USER_PROMPT}} -> extractUserPrompt()
-{{KEY:AVAILABLE_DOCUMENTS}} -> extractAvailableDocuments()
-{{KEY:WORKFLOW_HISTORY}} -> extractWorkflowHistory()
-{{KEY:AVAILABLE_METHODS}} -> extractAvailableMethods()
-{{KEY:AVAILABLE_CONNECTIONS}} -> extractAvailableConnections()
-{{KEY:USER_LANGUAGE}} -> extractUserLanguage()
-{{KEY:REVIEW_CONTENT}} -> extractReviewContent()
-{{KEY:ACTION_OBJECTIVE}} -> extractActionObjective()
-{{KEY:PREVIOUS_ACTION_RESULTS}} -> extractPreviousActionResults()
-{{KEY:LEARNINGS_AND_IMPROVEMENTS}} -> extractLearningsAndImprovements()
-{{KEY:LATEST_REFINEMENT_FEEDBACK}} -> extractLatestRefinementFeedback()
-{{KEY:SELECTED_ACTION}} -> extractSelectedAction()
-{{KEY:ACTION_SIGNATURE}} -> extractActionSignature()
-{{KEY:ENHANCED_DOCUMENTS}} -> extractEnhancedDocumentContext()
+MAPPING TABLE (keys → function) with usage [global | react | actionplan]:
+{{KEY:USER_PROMPT}} -> extractUserPrompt() [global, react, actionplan]
+{{KEY:USER_LANGUAGE}} -> extractUserLanguage() [react, actionplan]
+{{KEY:AVAILABLE_DOCUMENTS_SUMMARY}} -> extractAvailableDocumentsSummary() [react, actionplan]
+{{KEY:AVAILABLE_DOCUMENTS_INDEX}} -> extractAvailableDocumentsIndex() [react]
+{{KEY:AVAILABLE_CONNECTIONS_INDEX}} -> extractAvailableConnectionsIndex() [react, actionplan]
+{{KEY:AVAILABLE_CONNECTIONS_SUMMARY}} -> extractAvailableConnectionsSummary() [unused]
+{{KEY:WORKFLOW_HISTORY}} -> extractWorkflowHistory() [actionplan]
+{{KEY:AVAILABLE_METHODS}} -> extractAvailableMethods() [react, actionplan]
+{{KEY:REVIEW_CONTENT}} -> extractReviewContent() [react, actionplan]
+{{KEY:PREVIOUS_ACTION_RESULTS}} -> extractPreviousActionResults() [react]
+{{KEY:LEARNINGS_AND_IMPROVEMENTS}} -> extractLearningsAndImprovements() [react]
+{{KEY:LATEST_REFINEMENT_FEEDBACK}} -> extractLatestRefinementFeedback() [react]
+
+Following placeholders are populated directly by prompt builders with according context in promptGenerationActionsReact module:
+- ACTION_OBJECTIVE,
+- SELECTED_ACTION,
+- ACTION_SIGNATURE
"""
import json
@@ -31,39 +34,31 @@ from typing import Dict, Any, List
from modules.datamodels.datamodelChat import ChatDocument
logger = logging.getLogger(__name__)
-from modules.workflows.processing.shared.methodDiscovery import (
- getAvailableDocuments,
- getMethodsList,
- methods,
- discoverMethods
-)
-
-
-# ============================================================================
-# CORE PLACEHOLDER EXTRACTION FUNCTIONS
-# ============================================================================
+from modules.workflows.processing.shared.methodDiscovery import (methods, discoverMethods)
def extractUserPrompt(context: Any) -> str:
- """Extract user prompt from context. Maps to {{KEY:USER_PROMPT}}"""
+ """Extract user prompt from context. Maps to {{KEY:USER_PROMPT}}.
+ Prefer the cleaned intent stored on the services object if available via context.
+ Fallback to the task_step objective.
+ """
+ try:
+ # Prefer services.currentUserPrompt when accessible through context
+ services = getattr(context, 'services', None)
+ if services and getattr(services, 'currentUserPrompt', None):
+ return services.currentUserPrompt
+ except Exception:
+ pass
+
if hasattr(context, 'task_step') and context.task_step:
return context.task_step.objective or 'No request specified'
return 'No request specified'
-
-def extractAvailableDocuments(context: Any) -> str:
- """Extract available documents from context. Maps to {{KEY:AVAILABLE_DOCUMENTS}}"""
- if hasattr(context, 'available_documents') and context.available_documents:
- return context.available_documents
- return "No documents available"
-
-
def extractWorkflowHistory(service: Any, context: Any) -> str:
"""Extract workflow history from context. Maps to {{KEY:WORKFLOW_HISTORY}}"""
if hasattr(context, 'workflow') and context.workflow:
return getPreviousRoundContext(service, context.workflow) or "No previous workflow rounds - this is the first round."
return "No previous workflow rounds - this is the first round."
-
def extractAvailableMethods(service: Any) -> str:
"""Extract available methods for action planning. Maps to {{KEY:AVAILABLE_METHODS}}"""
try:
@@ -89,24 +84,10 @@ def extractAvailableMethods(service: Any) -> str:
logger.error(f"Error extracting available methods: {str(e)}")
return json.dumps({}, indent=2, ensure_ascii=False)
-
def extractUserLanguage(service: Any) -> str:
"""Extract user language from service. Maps to {{KEY:USER_LANGUAGE}}"""
return service.user.language if service and service.user else 'en'
-
-def extractAvailableConnections(service: Any) -> str:
- """Extract available connections. Maps to {{KEY:AVAILABLE_CONNECTIONS}}"""
- try:
- connections = getConnectionReferenceList(service)
- if connections:
- return '\n'.join(f"- {conn}" for conn in connections)
- return "No connections available"
- except Exception as e:
- logger.error(f"Error extracting available connections: {str(e)}")
- return "No connections available"
-
-
def getConnectionReferenceList(services) -> List[str]:
"""Get list of available connections"""
try:
@@ -128,7 +109,6 @@ def getConnectionReferenceList(services) -> List[str]:
logger.error(f"Error getting connection reference list: {str(e)}")
return []
-
def getPreviousRoundContext(services, context: Any) -> str:
"""Get previous round context for prompt"""
try:
@@ -161,7 +141,6 @@ def getPreviousRoundContext(services, context: Any) -> str:
logger.error(f"Error getting previous round context: {str(e)}")
return "Error retrieving previous round context"
-
def extractReviewContent(context: Any) -> str:
"""Extract review content for result validation. Maps to {{KEY:REVIEW_CONTENT}}"""
try:
@@ -234,18 +213,6 @@ def extractReviewContent(context: Any) -> str:
logger.error(f"Error extracting review content: {str(e)}")
return "No review content available"
-
-# ============================================================================
-# REACT MODE SPECIFIC PLACEHOLDERS
-# ============================================================================
-
-def extractActionObjective(context: Any, current_task: str, original_prompt: str, additional_data: Dict[str, Any] = None) -> str:
- """Extract action objective for React mode. Maps to {{KEY:ACTION_OBJECTIVE}}"""
- # This is a placeholder - the actual implementation will be in placeholderFactoryReactOnly
- # since it requires AI generation
- return current_task or original_prompt
-
-
def extractPreviousActionResults(context: Any) -> str:
"""Extract previous action results for learning context. Maps to {{KEY:PREVIOUS_ACTION_RESULTS}}"""
try:
@@ -265,7 +232,6 @@ def extractPreviousActionResults(context: Any) -> str:
logger.error(f"Error extracting previous action results: {str(e)}")
return "No previous actions executed yet"
-
def extractLearningsAndImprovements(context: Any) -> str:
"""Extract learnings and improvements from previous actions. Maps to {{KEY:LEARNINGS_AND_IMPROVEMENTS}}"""
try:
@@ -294,7 +260,6 @@ def extractLearningsAndImprovements(context: Any) -> str:
logger.error(f"Error extracting learnings and improvements: {str(e)}")
return "No learnings available yet"
-
def extractLatestRefinementFeedback(context: Any) -> str:
"""Extract the latest refinement feedback. Maps to {{KEY:LATEST_REFINEMENT_FEEDBACK}}"""
try:
@@ -326,308 +291,48 @@ def extractLatestRefinementFeedback(context: Any) -> str:
logger.error(f"Error extracting latest refinement feedback: {str(e)}")
return "No previous refinement feedback available"
-
-def extractSelectedAction(additional_data: Dict[str, Any]) -> str:
- """Extract selected action from additional data. Maps to {{KEY:SELECTED_ACTION}}"""
- return additional_data.get('SELECTED_ACTION', '') if additional_data else ''
-
-
-def extractActionSignature(additional_data: Dict[str, Any]) -> str:
- """Extract action signature from additional data. Maps to {{KEY:ACTION_SIGNATURE}}"""
- return additional_data.get('ACTION_SIGNATURE', '') if additional_data else ''
-
-
-# ============================================================================
-# CONTEXT-AWARE PLACEHOLDER FUNCTIONS (for React mode)
-# ============================================================================
-
-def extractMinimalDocumentContext(service: Any, context: Any) -> str:
- """Extract minimal document context (counts only) for React plan selection."""
+def extractAvailableDocumentsSummary(service: Any, context: Any) -> str:
+ """Summary of available documents (count only)."""
try:
if hasattr(context, 'workflow') and context.workflow:
- # Get document count from workflow
documents = service.workflow.getAvailableDocuments(context.workflow)
if documents and documents != "No documents available":
- # Count documents by counting docList and docItem references
doc_count = documents.count("docList:") + documents.count("docItem:")
return f"{doc_count} documents available from previous tasks"
- else:
- return "No documents available"
+ return "No documents available"
return "No documents available"
except Exception as e:
- logger.error(f"Error getting minimal document context: {str(e)}")
+ logger.error(f"Error getting document summary: {str(e)}")
return "No documents available"
-
-def extractFullDocumentContext(service: Any, context: Any) -> str:
- """Extract full document context with detailed references for parameter generation."""
+def extractAvailableDocumentsIndex(service: Any, context: Any) -> str:
+ """Index of available documents with detailed references for parameter generation."""
try:
if hasattr(context, 'workflow') and context.workflow:
return service.workflow.getAvailableDocuments(context.workflow)
return "No documents available"
except Exception as e:
- logger.error(f"Error getting full document context: {str(e)}")
+ logger.error(f"Error getting document index: {str(e)}")
return "No documents available"
-
-def extractMinimalConnectionContext(service: Any) -> str:
- """Extract minimal connection context (count only) for React plan selection."""
+def extractAvailableConnectionsSummary(service: Any) -> str:
+ """Summary of available connections (count only)."""
try:
connections = getConnectionReferenceList(service)
if connections:
return f"{len(connections)} connections available"
return "No connections available"
except Exception as e:
- logger.error(f"Error getting minimal connection context: {str(e)}")
+ logger.error(f"Error getting connection summary: {str(e)}")
return "No connections available"
-
-def extractFullConnectionContext(service: Any) -> str:
- """Extract full connection context with detailed references for parameter generation."""
+def extractAvailableConnectionsIndex(service: Any) -> str:
+ """Index of available connections with detailed references for parameter generation."""
try:
connections = getConnectionReferenceList(service)
if connections:
return '\n'.join(f"- {conn}" for conn in connections)
return "No connections available"
except Exception as e:
- logger.error(f"Error getting full connection context: {str(e)}")
+ logger.error(f"Error getting connection index: {str(e)}")
return "No connections available"
-
-
-def extractUserPromptFromService(service: Any) -> str:
- """Extract user prompt from service (clean and reliable)."""
- # Get the current user prompt from services (clean and reliable)
- if service and hasattr(service, 'currentUserPrompt') and service.currentUserPrompt:
- return service.currentUserPrompt
-
- # Fallback to task step objective if no current prompt found
- return 'No request specified'
-
-
-def extractUserLanguageFromService(service: Any) -> str:
- """Extract user language from service."""
- return service.user.language if service and service.user else 'en'
-
-
-# ============================================================================
-# ADDITIONAL PLACEHOLDER EXTRACTION FUNCTIONS (moved from methodDiscovery.py)
-# ============================================================================
-
-def extractAvailableDocumentsFromList(context: Any) -> str:
- """Extract available documents from context list. Maps to {{KEY:AVAILABLE_DOCUMENTS}} (alternative implementation)"""
- try:
- if not context or not hasattr(context, 'available_documents') or not context.available_documents:
- return "No documents available"
-
- documents = context.available_documents
- if not isinstance(documents, list):
- return "No documents available"
-
- docList = []
- for i, doc in enumerate(documents, 1):
- if isinstance(doc, ChatDocument):
- docInfo = f"{i}. **{doc.fileName}**"
- if hasattr(doc, 'mimeType') and doc.mimeType:
- docInfo += f" ({doc.mimeType})"
- if hasattr(doc, 'size') and doc.size:
- docInfo += f" - {doc.size} bytes"
- docList.append(docInfo)
- elif isinstance(doc, dict):
- docInfo = f"{i}. **{doc.get('fileName', 'Unknown')}**"
- if doc.get('mimeType'):
- docInfo += f" ({doc['mimeType']})"
- if doc.get('size'):
- docInfo += f" - {doc['size']} bytes"
- docList.append(docInfo)
- else:
- docList.append(f"{i}. {str(doc)}")
-
- return "\n".join(docList) if docList else "No documents available"
- except Exception as e:
- logger.error(f"Error getting available documents: {str(e)}")
- return "Error retrieving documents"
-
-
-def extractWorkflowHistoryFromMessages(services: Any, context: Any) -> str:
- """Extract workflow history from messages. Maps to {{KEY:WORKFLOW_HISTORY}} (alternative implementation)"""
- try:
- if not context or not hasattr(context, 'workflow_id'):
- return "No workflow history available"
-
- workflowId = context.workflow_id
- if not workflowId:
- return "No workflow history available"
-
- # Get workflow messages
- messages = services.interfaceDbChat.getWorkflowMessages(workflowId)
- if not messages:
- return "No workflow history available"
-
- # Filter for relevant messages (last 10)
- recentMessages = messages[-10:] if len(messages) > 10 else messages
-
- historyList = []
- for msg in recentMessages:
- if hasattr(msg, 'role') and hasattr(msg, 'message'):
- role = "User" if msg.role == "user" else "Assistant"
- message = msg.message[:200] + "..." if len(msg.message) > 200 else msg.message
- historyList.append(f"**{role}**: {message}")
-
- return "\n".join(historyList) if historyList else "No workflow history available"
- except Exception as e:
- logger.error(f"Error getting workflow history: {str(e)}")
- return "Error retrieving workflow history"
-
-
-def extractAvailableMethodsFromList(services: Any) -> str:
- """Extract available methods as formatted list. Maps to {{KEY:AVAILABLE_METHODS}} (alternative implementation)"""
- try:
- if not methods:
- discoverMethods(services)
-
- return getMethodsList(services)
- except Exception as e:
- logger.error(f"Error getting available methods: {str(e)}")
- return "Error retrieving available methods"
-
-
-def extractUserLanguageFromServices(services: Any) -> str:
- """Extract user language from services. Maps to {{KEY:USER_LANGUAGE}} (alternative implementation)"""
- try:
- if hasattr(services, 'user') and hasattr(services.user, 'language'):
- return services.user.language or 'en'
- return 'en'
- except Exception as e:
- logger.error(f"Error getting user language: {str(e)}")
- return 'en'
-
-
-def extractReviewContentFromObservation(context: Any) -> str:
- """Extract review content from observation. Maps to {{KEY:REVIEW_CONTENT}} (alternative implementation)"""
- try:
- if not context or not hasattr(context, 'observation'):
- return "No review content available"
-
- observation = context.observation
- if not isinstance(observation, dict):
- return "No review content available"
-
- reviewParts = []
-
- # Add success status
- if 'success' in observation:
- reviewParts.append(f"Success: {observation['success']}")
-
- # Add documents count
- if 'documentsCount' in observation:
- reviewParts.append(f"Documents generated: {observation['documentsCount']}")
-
- # Add previews
- if 'previews' in observation and observation['previews']:
- reviewParts.append("Document previews:")
- for preview in observation['previews']:
- if isinstance(preview, dict):
- name = preview.get('name', 'Unknown')
- mimeType = preview.get('mimeType', 'Unknown')
- size = preview.get('contentSize', 'Unknown size')
- reviewParts.append(f" - {name} ({mimeType}) - {size}")
-
- # Add notes
- if 'notes' in observation and observation['notes']:
- reviewParts.append("Notes:")
- for note in observation['notes']:
- reviewParts.append(f" - {note}")
-
- return "\n".join(reviewParts) if reviewParts else "No review content available"
- except Exception as e:
- logger.error(f"Error getting review content: {str(e)}")
- return "Error retrieving review content"
-
-
-def extractEnhancedDocumentContext(services: Any) -> str:
- """Extract enhanced document context with full metadata. Maps to {{KEY:ENHANCED_DOCUMENTS}}"""
- try:
- # Get all documents from the current workflow
- workflow = getattr(services, 'currentWorkflow', None)
- if not workflow or not hasattr(workflow, 'id'):
- return "No workflow context available"
-
- # Get workflow documents from messages
- if not hasattr(workflow, 'messages') or not workflow.messages:
- return "No documents available"
-
- # Collect all documents from all messages
- all_documents = []
- for message in workflow.messages:
- if hasattr(message, 'documents') and message.documents:
- all_documents.extend(message.documents)
-
- if not all_documents:
- return "No documents available"
-
- # Group documents by round/task/action for better organization
- docGroups = {}
- for message in workflow.messages:
- if hasattr(message, 'documents') and message.documents:
- round_num = getattr(message, 'roundNumber', 0)
- task_num = getattr(message, 'taskNumber', 0)
- action_num = getattr(message, 'actionNumber', 0)
- label = getattr(message, 'documentsLabel', 'results')
-
- group_key = f"round{round_num}_task{task_num}_action{action_num}_{label}"
- if group_key not in docGroups:
- docGroups[group_key] = []
- docGroups[group_key].extend(message.documents)
-
- # Format documents by groups with proper docList references
- docList = []
- for group_key, group_docs in docGroups.items():
- # Find the message that contains these documents to get the message ID
- message_id = None
- for message in workflow.messages:
- if hasattr(message, 'documents') and message.documents:
- round_num = getattr(message, 'roundNumber', 0)
- task_num = getattr(message, 'taskNumber', 0)
- action_num = getattr(message, 'actionNumber', 0)
- label = getattr(message, 'documentsLabel', 'results')
- msg_group_key = f"round{round_num}_task{task_num}_action{action_num}_{label}"
-
- if msg_group_key == group_key:
- message_id = str(message.id)
- break
-
- # Generate proper docList reference
- if message_id:
- docListRef = f"docList:{message_id}:{group_key}"
- else:
- # Fallback to direct label reference
- docListRef = group_key
-
- docList.append(f"\n**{group_key}:**")
- docList.append(f"Reference: {docListRef}")
- for i, doc in enumerate(group_docs, 1):
- if isinstance(doc, ChatDocument):
- docInfo = f" {i}. **{doc.fileName}**"
- if hasattr(doc, 'mimeType') and doc.mimeType:
- docInfo += f" ({doc.mimeType})"
- if hasattr(doc, 'size') and doc.size:
- docInfo += f" - {doc.size} bytes"
- if hasattr(doc, 'created') and doc.created:
- docInfo += f" - Created: {doc.created}"
- docList.append(docInfo)
- elif isinstance(doc, dict):
- docInfo = f" {i}. **{doc.get('fileName', 'Unknown')}**"
- if doc.get('mimeType'):
- docInfo += f" ({doc['mimeType']})"
- if doc.get('size'):
- docInfo += f" - {doc['size']} bytes"
- if doc.get('created'):
- docInfo += f" - Created: {doc['created']}"
- docList.append(docInfo)
- else:
- docList.append(f" {i}. {str(doc)}")
-
- return "\n".join(docList) if docList else "No documents available"
- except Exception as e:
- logger.error(f"Error getting enhanced document context: {str(e)}")
- return "Error retrieving document context"
diff --git a/modules/workflows/processing/shared/placeholderFactoryReactOnly.py b/modules/workflows/processing/shared/placeholderFactoryReactOnly.py
deleted file mode 100644
index d2675fe2..00000000
--- a/modules/workflows/processing/shared/placeholderFactoryReactOnly.py
+++ /dev/null
@@ -1,189 +0,0 @@
-"""
-Context-aware placeholder service for different workflow phases.
-This module provides different levels of context based on the workflow phase.
-"""
-
-import json
-import logging
-from typing import Dict, Any, Optional
-from enum import Enum
-from modules.workflows.processing.shared.placeholderFactory import (
- extractUserPromptFromService, extractFullDocumentContext,
- extractWorkflowHistory, extractUserLanguageFromService,
- extractMinimalDocumentContext, extractAvailableMethods,
- extractMinimalConnectionContext, extractFullConnectionContext,
- extractReviewContent, extractPreviousActionResults,
- extractLearningsAndImprovements, extractLatestRefinementFeedback
-)
-from modules.datamodels.datamodelAi import AiCallOptions, OperationType, Priority, ProcessingMode
-
-logger = logging.getLogger(__name__)
-
-class WorkflowPhase(Enum):
- """Different phases of workflow execution requiring different context levels."""
- TASK_PLANNING = "task_planning" # Needs full context for planning
- REACT_PLAN_SELECTION = "react_plan_selection" # Needs minimal context for action selection
- REACT_PARAMETERS = "react_parameters" # Needs full context for parameter generation
- ACTION_PLANNING = "action_planning" # Needs full context for action planning
- RESULT_REVIEW = "result_review" # Needs full context for review
-
-class ContextAwarePlaceholders:
- """Context-aware placeholder service that provides different context levels based on workflow phase."""
-
- def __init__(self, services):
- self.services = services
-
- async def getPlaceholders(self, phase: WorkflowPhase, context: Any, additional_data: Dict[str, Any] = None) -> Dict[str, str]:
- """
- Get placeholders based on workflow phase and context.
-
- Args:
- phase: The workflow phase determining context level
- context: The workflow context object
- additional_data: Additional data for specific phases (e.g., selected action)
-
- Returns:
- Dictionary of placeholder key-value pairs
- """
- if phase == WorkflowPhase.TASK_PLANNING:
- return {
- "USER_PROMPT": extractUserPromptFromService(self.services),
- "AVAILABLE_DOCUMENTS": extractFullDocumentContext(self.services, context),
- "WORKFLOW_HISTORY": extractWorkflowHistory(self.services, context),
- "USER_LANGUAGE": extractUserLanguageFromService(self.services),
- }
- elif phase == WorkflowPhase.REACT_PLAN_SELECTION:
- return {
- "USER_PROMPT": extractUserPromptFromService(self.services),
- "AVAILABLE_DOCUMENTS": extractMinimalDocumentContext(self.services, context),
- "USER_LANGUAGE": extractUserLanguageFromService(self.services),
- "AVAILABLE_METHODS": extractAvailableMethods(self.services),
- "AVAILABLE_CONNECTIONS": extractMinimalConnectionContext(self.services),
- }
- elif phase == WorkflowPhase.REACT_PARAMETERS:
- # Get both original user prompt and current task objective
- original_prompt = extractUserPromptFromService(self.services)
- current_task = ""
- if hasattr(context, 'task_step') and context.task_step and context.task_step.objective:
- current_task = context.task_step.objective
-
- # Combine original prompt and current task for better context
- combined_prompt = f"Original request: {original_prompt}"
- if current_task and current_task != original_prompt:
- combined_prompt += f"\n\nCurrent task: {current_task}"
-
- # Generate intelligent action objective
- action_objective = await self._generateActionObjective(context, current_task, original_prompt, additional_data)
-
- placeholders = {
- "USER_PROMPT": combined_prompt,
- "ACTION_OBJECTIVE": action_objective, # AI-generated intelligent objective
- "AVAILABLE_DOCUMENTS": extractFullDocumentContext(self.services, context),
- "USER_LANGUAGE": extractUserLanguageFromService(self.services),
- "AVAILABLE_CONNECTIONS": extractFullConnectionContext(self.services),
- "PREVIOUS_ACTION_RESULTS": extractPreviousActionResults(context),
- "LEARNINGS_AND_IMPROVEMENTS": extractLearningsAndImprovements(context),
- "LATEST_REFINEMENT_FEEDBACK": extractLatestRefinementFeedback(context),
- }
-
- # Add additional data if provided (e.g., selected action, action signature)
- if additional_data:
- placeholders.update(additional_data)
-
- return placeholders
- elif phase == WorkflowPhase.ACTION_PLANNING:
- return {
- "USER_PROMPT": extractUserPromptFromService(self.services),
- "AVAILABLE_DOCUMENTS": extractFullDocumentContext(self.services, context),
- "WORKFLOW_HISTORY": extractWorkflowHistory(self.services, context),
- "AVAILABLE_METHODS": extractAvailableMethods(self.services),
- "AVAILABLE_CONNECTIONS": extractFullConnectionContext(self.services),
- "USER_LANGUAGE": extractUserLanguageFromService(self.services),
- }
- elif phase == WorkflowPhase.RESULT_REVIEW:
- return {
- "USER_PROMPT": extractUserPromptFromService(self.services),
- "REVIEW_CONTENT": extractReviewContent(context),
- }
- else:
- logger.warning(f"Unknown workflow phase: {phase}")
- return {
- "USER_PROMPT": extractUserPromptFromService(self.services),
- "USER_LANGUAGE": extractUserLanguageFromService(self.services),
- }
-
- async def _generateActionObjective(self, context: Any, current_task: str, original_prompt: str, additional_data: Dict[str, Any] = None) -> str:
- """Generate intelligent, context-aware action objective using AI."""
- try:
- # Get the selected action from additional_data
- selected_action = additional_data.get('SELECTED_ACTION', '') if additional_data else ''
-
- # Build context for AI objective generation
- context_info = {
- "original_prompt": original_prompt,
- "current_task": current_task,
- "selected_action": selected_action,
- "available_documents": extractFullDocumentContext(self.services, context),
- "available_connections": extractFullConnectionContext(self.services),
- "previous_results": extractPreviousActionResults(context),
- "learnings": extractLearningsAndImprovements(context),
- "refinement_feedback": extractLatestRefinementFeedback(context),
- "user_language": extractUserLanguageFromService(self.services)
- }
-
- # Create AI prompt for objective generation
- objective_prompt = f"""Generate a specific, actionable objective for the selected action.
-
- CONTEXT:
- - Original User Request: {context_info['original_prompt']}
- - Current Task: {context_info['current_task']}
- - Selected Action: {context_info['selected_action']}
- - Available Documents: {context_info['available_documents']}
- - Available Connections: {context_info['available_connections']}
- - Previous Action Results: {context_info['previous_results']}
- - Learnings and Improvements: {context_info['learnings']}
- - Latest Refinement Feedback: {context_info['refinement_feedback']}
- - User Language: {context_info['user_language']}
-
- REQUIREMENTS:
- 1. Create a SPECIFIC objective that tells the action exactly what to accomplish
- 2. Include relevant details about documents, connections, recipients, etc.
- 3. Learn from previous attempts and refinement feedback
- 4. Make it actionable and concrete
- 5. Focus on the user's actual intent, not just the task description
- 6. If this is a retry, incorporate learnings from previous failures
-
- RESPONSE FORMAT:
- Return ONLY the objective text, no explanations or formatting.
-
- OBJECTIVE:"""
-
- # Call AI to generate the objective
- if self.services and hasattr(self.services, 'ai'):
- options = AiCallOptions(
- operationType=OperationType.ANALYSE_CONTENT,
- priority=Priority.BALANCED,
- compressPrompt=False,
- compressContext=False,
- processingMode=ProcessingMode.ADVANCED,
- maxCost=0.01,
- maxProcessingTime=10
- )
-
- response = await self.services.ai.callAi(
- prompt=objective_prompt,
- placeholders={},
- options=options
- )
-
- # Extract objective from response
- if response and response.strip():
- return response.strip()
-
- # Fallback to current task if AI fails
- return current_task or original_prompt
-
- except Exception as e:
- logger.error(f"Error generating action objective: {str(e)}")
- # Fallback to current task
- return current_task or original_prompt
diff --git a/modules/workflows/processing/shared/promptGenerationActionsActionplan.py b/modules/workflows/processing/shared/promptGenerationActionsActionplan.py
index fc07edee..e0d29ea4 100644
--- a/modules/workflows/processing/shared/promptGenerationActionsActionplan.py
+++ b/modules/workflows/processing/shared/promptGenerationActionsActionplan.py
@@ -5,13 +5,32 @@ Handles prompt templates and extraction functions for actionplan mode action han
import json
import logging
-from typing import Dict, Any
+from typing import Dict, Any, List
+from modules.datamodels.datamodelWorkflow import PromptBundle, PromptPlaceholder
+from modules.workflows.processing.shared.placeholderFactory import (
+ extractUserPrompt,
+ extractAvailableDocumentsSummary,
+ extractWorkflowHistory,
+ extractAvailableMethods,
+ extractUserLanguage,
+ extractAvailableConnectionsIndex,
+ extractReviewContent,
+)
logger = logging.getLogger(__name__)
-def createActionDefinitionPromptTemplate() -> str:
- """Create action definition prompt template with placeholders."""
- return """# Action Definition
+def generateActionDefinitionPrompt(services, context: Any) -> PromptBundle:
+ """Define placeholders first, then the template; return PromptBundle."""
+ placeholders: List[PromptPlaceholder] = [
+ PromptPlaceholder(label="USER_PROMPT", content=extractUserPrompt(context), summaryAllowed=False),
+ PromptPlaceholder(label="AVAILABLE_DOCUMENTS_SUMMARY", content=extractAvailableDocumentsSummary(services, context), summaryAllowed=True),
+ PromptPlaceholder(label="AVAILABLE_CONNECTIONS_INDEX", content=extractAvailableConnectionsIndex(services), summaryAllowed=False),
+ PromptPlaceholder(label="WORKFLOW_HISTORY", content=extractWorkflowHistory(services, context), summaryAllowed=True),
+ PromptPlaceholder(label="AVAILABLE_METHODS", content=extractAvailableMethods(services), summaryAllowed=False),
+ PromptPlaceholder(label="USER_LANGUAGE", content=extractUserLanguage(services), summaryAllowed=False),
+ ]
+
+ template = """# Action Definition
Generate the next action to advance toward completing the task objective.
@@ -21,20 +40,20 @@ def createActionDefinitionPromptTemplate() -> str:
{{KEY:USER_PROMPT}}
### Available Documents
- {{KEY:AVAILABLE_DOCUMENTS}}
+ {{KEY:AVAILABLE_DOCUMENTS_SUMMARY}}
+ ### Available Connections
+ {{KEY:AVAILABLE_CONNECTIONS_INDEX}}
+
+ ### User Language
+ {{KEY:USER_LANGUAGE}}
+
### Workflow History
{{KEY:WORKFLOW_HISTORY}}
### Available Methods
{{KEY:AVAILABLE_METHODS}}
- ### Available Connections
- {{KEY:AVAILABLE_CONNECTIONS}}
-
- ### User Language
- {{KEY:USER_LANGUAGE}}
-
## ⚠️ RULES
### Action Names
@@ -43,8 +62,8 @@ def createActionDefinitionPromptTemplate() -> str:
- **DO NOT separate** method and action names - use the full compound name
### Parameter Guidelines
- - **Use exact document references** from AVAILABLE_DOCUMENTS
- - **Use exact connection references** from AVAILABLE_CONNECTIONS
+ - **Use exact document references** from AVAILABLE_DOCUMENTS_INDEX
+ - **Use exact connection references** from AVAILABLE_CONNECTIONS_INDEX
- **Include user language** if relevant
- **Avoid unnecessary fields** - host applies defaults
@@ -106,9 +125,16 @@ def createActionDefinitionPromptTemplate() -> str:
## 🚀 Response Format
Return ONLY the JSON object."""
-def createResultReviewPromptTemplate() -> str:
- """Create result review prompt template with placeholders."""
- return """# Result Review & Validation
+ return PromptBundle(prompt=template, placeholders=placeholders)
+
+def generateResultReviewPrompt(context: Any) -> PromptBundle:
+ """Define placeholders first, then the template; return PromptBundle."""
+ placeholders: List[PromptPlaceholder] = [
+ PromptPlaceholder(label="USER_PROMPT", content=extractUserPrompt(context), summaryAllowed=False),
+ PromptPlaceholder(label="REVIEW_CONTENT", content=extractReviewContent(context), summaryAllowed=True),
+ ]
+
+ template = """# Result Review & Validation
Review task execution outcomes and determine success, retry needs, or failure.
@@ -198,7 +224,7 @@ def createResultReviewPromptTemplate() -> str:
- **Technical fixes** - Address specific technical issues
### Examples
- - "Use more specific document references from AVAILABLE_DOCUMENTS"
+ - "Use more specific document references from AVAILABLE_DOCUMENTS_INDEX"
- "Include user language parameter for better localization"
- "Break down complex objective into smaller, focused actions"
- "Verify document references before processing"
@@ -206,3 +232,5 @@ def createResultReviewPromptTemplate() -> str:
## 🚀 Response Format
Return ONLY the JSON object. Do not include any explanatory text."""
+ return PromptBundle(prompt=template, placeholders=placeholders)
+
diff --git a/modules/workflows/processing/shared/promptGenerationActionsReact.py b/modules/workflows/processing/shared/promptGenerationActionsReact.py
index e4f65b5e..c922ae71 100644
--- a/modules/workflows/processing/shared/promptGenerationActionsReact.py
+++ b/modules/workflows/processing/shared/promptGenerationActionsReact.py
@@ -3,15 +3,36 @@ React Mode Prompt Generation
Handles prompt templates for react mode action handling.
"""
-def createReactPlanSelectionPromptTemplate() -> str:
- """Create action selection prompt template for React mode with minimal placeholders."""
- return """Select one action to advance the task.
+from typing import Any, List
+from modules.datamodels.datamodelWorkflow import PromptBundle, PromptPlaceholder
+from modules.workflows.processing.shared.placeholderFactory import (
+ extractUserPrompt,
+ extractUserLanguage,
+ extractAvailableMethods,
+ extractAvailableDocumentsSummary,
+ extractAvailableDocumentsIndex,
+ extractAvailableConnectionsIndex,
+ extractPreviousActionResults,
+ extractLearningsAndImprovements,
+ extractLatestRefinementFeedback,
+)
+from modules.workflows.processing.shared.methodDiscovery import methods, getActionParameterSignature
+
+def generateReactPlanSelectionPrompt(services, context: Any) -> PromptBundle:
+ """Define placeholders first, then the template; return PromptBundle."""
+ placeholders: List[PromptPlaceholder] = [
+ PromptPlaceholder(label="USER_PROMPT", content=extractUserPrompt(context), summaryAllowed=False),
+ PromptPlaceholder(label="AVAILABLE_DOCUMENTS_SUMMARY", content=extractAvailableDocumentsSummary(services, context), summaryAllowed=True),
+ PromptPlaceholder(label="AVAILABLE_METHODS", content=extractAvailableMethods(services), summaryAllowed=False),
+ ]
+
+ template = """Select one action to advance the task.
OBJECTIVE:
{{KEY:USER_PROMPT}}
- AVAILABLE_DOCUMENTS:
- {{KEY:AVAILABLE_DOCUMENTS}}
+ AVAILABLE_DOCUMENTS_SUMMARY:
+ {{KEY:AVAILABLE_DOCUMENTS_SUMMARY}}
AVAILABLE_METHODS:
{{KEY:AVAILABLE_METHODS}}
@@ -28,9 +49,37 @@ def createReactPlanSelectionPromptTemplate() -> str:
4. Do NOT add explanations
"""
-def createReactParametersPromptTemplate() -> str:
- """Create comprehensive action parameter prompt template for React mode with all available context."""
- return """Generate parameters for this action.
+ return PromptBundle(prompt=template, placeholders=placeholders)
+
+def generateReactParametersPrompt(services, context: Any, compoundActionName: str) -> PromptBundle:
+ """Define placeholders first, then the template; return PromptBundle."""
+ # derive method/action and signature
+ methodName, actionName = (compoundActionName.split('.', 1) if '.' in compoundActionName else (compoundActionName, ''))
+ actionSignature = getActionParameterSignature(methodName, actionName, methods)
+
+ # determine action objective if available, else fall back to user prompt
+ actionObjective = None
+ if hasattr(context, 'action_objective') and context.action_objective:
+ actionObjective = context.action_objective
+ elif hasattr(context, 'task_step') and context.task_step and getattr(context.task_step, 'objective', None):
+ actionObjective = context.task_step.objective
+ else:
+ actionObjective = extractUserPrompt(context)
+
+ placeholders: List[PromptPlaceholder] = [
+ PromptPlaceholder(label="ACTION_OBJECTIVE", content=actionObjective, summaryAllowed=False),
+ PromptPlaceholder(label="ACTION_SIGNATURE", content=actionSignature, summaryAllowed=False),
+ PromptPlaceholder(label="AVAILABLE_DOCUMENTS_INDEX", content=extractAvailableDocumentsIndex(services, context), summaryAllowed=True),
+ PromptPlaceholder(label="AVAILABLE_CONNECTIONS_INDEX", content=extractAvailableConnectionsIndex(services), summaryAllowed=False),
+ PromptPlaceholder(label="USER_PROMPT", content=extractUserPrompt(context), summaryAllowed=False),
+ PromptPlaceholder(label="USER_LANGUAGE", content=extractUserLanguage(services), summaryAllowed=False),
+ PromptPlaceholder(label="PREVIOUS_ACTION_RESULTS", content=extractPreviousActionResults(context), summaryAllowed=True),
+ PromptPlaceholder(label="LEARNINGS_AND_IMPROVEMENTS", content=extractLearningsAndImprovements(context), summaryAllowed=True),
+ PromptPlaceholder(label="LATEST_REFINEMENT_FEEDBACK", content=extractLatestRefinementFeedback(context), summaryAllowed=True),
+ PromptPlaceholder(label="SELECTED_ACTION", content=compoundActionName, summaryAllowed=False),
+ ]
+
+ template = """Generate parameters for this action.
ACTION_OBJECTIVE (the objective for this action to fulfill):
{{KEY:ACTION_OBJECTIVE}}
@@ -38,11 +87,11 @@ def createReactParametersPromptTemplate() -> str:
ACTION_SIGNATURE (the signature of the action to generate parameters for):
{{KEY:ACTION_SIGNATURE}}
- AVAILABLE_DOCUMENTS:
- {{KEY:AVAILABLE_DOCUMENTS}}
+ AVAILABLE_DOCUMENTS_INDEX:
+ {{KEY:AVAILABLE_DOCUMENTS_INDEX}}
- AVAILABLE_CONNECTIONS:
- {{KEY:AVAILABLE_CONNECTIONS}}
+ AVAILABLE_CONNECTIONS_INDEX:
+ {{KEY:AVAILABLE_CONNECTIONS_INDEX}}
USER_REQUEST (final user prompt to deliver):
{{KEY:USER_PROMPT}}
@@ -72,8 +121,8 @@ def createReactParametersPromptTemplate() -> str:
RULES:
1. Use ONLY parameter names from ACTION_SIGNATURE
- 2. Use exact connection references from AVAILABLE_CONNECTIONS for connectionReference parameters
- 3. Use exact document references from AVAILABLE_DOCUMENTS for documentList parameters
+ 2. Use exact connection references from AVAILABLE_CONNECTIONS_INDEX for connectionReference parameters
+ 3. Use exact document references from AVAILABLE_DOCUMENTS_INDEX for documentList parameters
4. Learn from PREVIOUS_ACTION_RESULTS and LEARNINGS_AND_IMPROVEMENTS to avoid repeating mistakes
5. Consider LATEST_REFINEMENT_FEEDBACK when generating parameters
6. Use the ACTION_OBJECTIVE to understand the specific goal for this action
@@ -83,9 +132,16 @@ def createReactParametersPromptTemplate() -> str:
10. Do NOT add explanations
"""
-def createReactRefinementPromptTemplate() -> str:
- """Create refinement prompt template for React mode with full context placeholders."""
- return """Decide the next step based on the observation.
+ return PromptBundle(prompt=template, placeholders=placeholders)
+
+def generateReactRefinementPrompt(services, context: Any, reviewContent: str) -> PromptBundle:
+ """Define placeholders first, then the template; return PromptBundle."""
+ placeholders: List[PromptPlaceholder] = [
+ PromptPlaceholder(label="USER_PROMPT", content=extractUserPrompt(context), summaryAllowed=False),
+ PromptPlaceholder(label="REVIEW_CONTENT", content=reviewContent, summaryAllowed=True),
+ ]
+
+ template = """Decide the next step based on the observation.
OBJECTIVE:
{{KEY:USER_PROMPT}}
@@ -106,3 +162,5 @@ def createReactRefinementPromptTemplate() -> str:
4. Do NOT use markdown code blocks
5. Do NOT add explanations
"""
+
+ return PromptBundle(prompt=template, placeholders=placeholders)
diff --git a/modules/workflows/processing/shared/promptGenerationTaskplan.py b/modules/workflows/processing/shared/promptGenerationTaskplan.py
index 80481a74..c0b7bbb9 100644
--- a/modules/workflows/processing/shared/promptGenerationTaskplan.py
+++ b/modules/workflows/processing/shared/promptGenerationTaskplan.py
@@ -5,14 +5,26 @@ Handles prompt templates and extraction functions for task planning phase.
import json
import logging
-from typing import Dict, Any
+from typing import Dict, Any, List
+from modules.datamodels.datamodelWorkflow import PromptBundle, PromptPlaceholder
+from modules.workflows.processing.shared.placeholderFactory import (
+ extractUserPrompt,
+ extractAvailableDocumentsSummary,
+ extractWorkflowHistory,
+)
logger = logging.getLogger(__name__)
-def createTaskPlanningPromptTemplate() -> str:
- """Create task planning prompt template with placeholders."""
- return """# Task Planning
+def generateTaskPlanningPrompt(services, context: Any) -> PromptBundle:
+ """Define placeholders first, then the template; return PromptBundle."""
+ placeholders: List[PromptPlaceholder] = [
+ PromptPlaceholder(label="USER_PROMPT", content=extractUserPrompt(context), summaryAllowed=False),
+ PromptPlaceholder(label="AVAILABLE_DOCUMENTS_SUMMARY", content=extractAvailableDocumentsSummary(services, context), summaryAllowed=True),
+ PromptPlaceholder(label="WORKFLOW_HISTORY", content=extractWorkflowHistory(services, context), summaryAllowed=True),
+ ]
+
+ template = """# Task Planning
Break down user requests into logical, executable task steps.
@@ -22,7 +34,7 @@ def createTaskPlanningPromptTemplate() -> str:
{{KEY:USER_PROMPT}}
### Available Documents
- {{KEY:AVAILABLE_DOCUMENTS}}
+ {{KEY:AVAILABLE_DOCUMENTS_SUMMARY}}
### Previous Workflow Rounds
{{KEY:WORKFLOW_HISTORY}}
@@ -105,3 +117,5 @@ def createTaskPlanningPromptTemplate() -> str:
## 🚀 Response Format
Return ONLY the JSON object."""
+
+ return PromptBundle(prompt=template, placeholders=placeholders)
diff --git a/modules/workflows/workflowManager.py b/modules/workflows/workflowManager.py
index 7137aa8a..db0d6652 100644
--- a/modules/workflows/workflowManager.py
+++ b/modules/workflows/workflowManager.py
@@ -155,6 +155,7 @@ class WorkflowManager:
"""Process a workflow with user input"""
try:
# Store the current user prompt in services for easy access throughout the workflow
+ self.services.rawUserPrompt = userInput.prompt
self.services.currentUserPrompt = userInput.prompt
self.workflowProcessor = WorkflowProcessor(self.services, workflow)
message = await self._sendFirstMessage(userInput, workflow)
@@ -176,11 +177,11 @@ class WorkflowManager:
self.workflowProcessor._checkWorkflowStopped(workflow)
# Create initial message using interface
- # Generate the correct documentsLabel that matches what getDocumentReferenceString will create
+ # For first user message, include round info in the user context label
round_num = workflow.currentRound
task_num = 0
action_num = 0
- context_label = f"round{round_num}_task{task_num}_action{action_num}_context"
+ context_label = f"round{round_num}_usercontext"
messageData = {
"workflowId": workflow.id,
@@ -215,6 +216,128 @@ class WorkflowManager:
message.documents = documents
# Update the message with documents in database
self.services.workflow.updateMessage(message.id, {"documents": [doc.to_dict() for doc in documents]})
+
+ # Analyze the user's input to extract intent and offload bulky context into documents
+ try:
+ analyzerPrompt = (
+ "You are an input analyzer. Split the user's message into:\n"
+ "1) intent: the user's core request in one concise paragraph, normalized to the user's language.\n"
+ "2) contextItems: supportive data to attach as separate documents if significantly larger than the intent. "
+ "Include large literal data blocks, long lists/tables, code/JSON blocks, quoted transcripts, CSV fragments, or detailed specs. "
+ "Keep URLs in the intent unless they include large pasted content.\n\n"
+ "Rules:\n"
+ "- If total content length (intent + data) is less than 10% of the model's max tokens, do not extract; "
+ "return an empty contextItems and keep a compact, self-contained intent.\n"
+ "- If content exceeds that, move bulky parts into contextItems, keeping the intent short and clear.\n"
+ "- Preserve critical references (URLs, filenames) in the intent.\n"
+ "- Normalize the intent to the detected language. If mixed-language, use the primary detected language and normalize.\n\n"
+ "Output JSON only (no markdown):\n"
+ "{\n"
+ " \"detectedLanguage\": \"en\",\n"
+ " \"intent\": \"Concise normalized request...\",\n"
+ " \"contextItems\": [\n"
+ " {\n"
+ " \"title\": \"User context 1\",\n"
+ " \"mimeType\": \"text/plain\",\n"
+ " \"content\": \"Full extracted content block here\"\n"
+ " }\n"
+ " ]\n"
+ "}\n\n"
+ f"User message:\n{userInput.prompt}"
+ )
+
+ # Call AI analyzer
+ aiResponse = await self.services.ai.callAi(prompt=analyzerPrompt)
+
+ detectedLanguage = None
+ intentText = userInput.prompt
+ contextItems = []
+
+ # Parse analyzer response (JSON expected)
+ try:
+ import json
+ jsonStart = aiResponse.find('{') if aiResponse else -1
+ jsonEnd = aiResponse.rfind('}') + 1 if aiResponse else 0
+ if jsonStart != -1 and jsonEnd > jsonStart:
+ parsed = json.loads(aiResponse[jsonStart:jsonEnd])
+ detectedLanguage = parsed.get('detectedLanguage') or None
+ if parsed.get('intent'):
+ intentText = parsed.get('intent')
+ contextItems = parsed.get('contextItems') or []
+ except Exception:
+ contextItems = []
+
+ # Update services state
+ if detectedLanguage and isinstance(detectedLanguage, str):
+ self._setUserLanguage(detectedLanguage)
+ self.services.currentUserPrompt = intentText or userInput.prompt
+
+ # Telemetry (sizes and counts)
+ try:
+ inputSize = len(userInput.prompt.encode('utf-8')) if userInput and userInput.prompt else 0
+ outputSize = len(aiResponse.encode('utf-8')) if aiResponse else 0
+ self.services.workflow.createLog({
+ "workflowId": workflow.id,
+ "message": f"User prompt analyzed (input {inputSize} bytes, output {outputSize} bytes, items {len(contextItems)})",
+ "type": "info",
+ "status": "running",
+ "progress": 0
+ })
+ except Exception:
+ pass
+
+ # Create and attach documents for context items
+ if contextItems and isinstance(contextItems, list):
+ created_docs = []
+ for idx, item in enumerate(contextItems):
+ try:
+ title = item.get('title') if isinstance(item, dict) else None
+ mime = item.get('mimeType') if isinstance(item, dict) else None
+ content = item.get('content') if isinstance(item, dict) else None
+ if not content:
+ continue
+ fileName = (title or f"user_context_{idx+1}.txt").strip()
+ mimeType = (mime or "text/plain").strip()
+
+ # Create file in component storage
+ content_bytes = content.encode('utf-8')
+ file_item = self.services.interfaceDbComponent.createFile(
+ name=fileName,
+ mimeType=mimeType,
+ content=content_bytes
+ )
+ # Persist file data
+ self.services.interfaceDbComponent.createFileData(file_item.id, content_bytes)
+
+ # Collect file info
+ file_info = self.services.workflow.getFileInfo(file_item.id)
+ from modules.datamodels.datamodelChat import ChatDocument as _ChatDocument
+ doc = _ChatDocument(
+ messageId=message.id,
+ fileId=file_item.id,
+ fileName=file_info.get("fileName", fileName) if file_info else fileName,
+ fileSize=file_info.get("size", len(content_bytes)) if file_info else len(content_bytes),
+ mimeType=file_info.get("mimeType", mimeType) if file_info else mimeType
+ )
+ # Persist document record
+ self.services.interfaceDbChat.createDocument(doc.to_dict())
+ created_docs.append(doc)
+ except Exception:
+ continue
+
+ if created_docs:
+ # Attach to message and persist
+ if not message.documents:
+ message.documents = []
+ message.documents.extend(created_docs)
+ # Ensure label is user_context for discoverability
+ message.documentsLabel = context_label
+ self.services.workflow.updateMessage(message.id, {
+ "documents": [d.to_dict() for d in message.documents],
+ "documentsLabel": context_label
+ })
+ except Exception as e:
+ logger.warning(f"Prompt analysis failed or skipped: {str(e)}")
return message
else: