From 9b8510bfd003a416d32e17e2212e5b55a044821e Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Sun, 5 Oct 2025 11:34:40 +0200
Subject: [PATCH] refactored workflow engine
---
modules/workflows/methods/methodOutlook.py | 12 +-
.../processing/core/actionExecutor.py | 2 +-
.../workflows/processing/core/taskPlanner.py | 10 +-
.../{actionplanMode.py => modeActionplan.py} | 14 +-
.../modes/{baseMode.py => modeBase.py} | 2 +-
.../modes/{reactMode.py => modeReact.py} | 35 +-
.../shared/contextAwarePlaceholders.py | 397 -----------
.../processing/shared/executionState.py | 2 +-
.../processing/shared/methodDiscovery.py | 130 ++++
.../processing/shared/placeholderFactory.py | 633 ++++++++++++++++
.../shared/placeholderFactoryReactOnly.py | 189 +++++
.../processing/shared/promptFactory.py | 371 ----------
.../shared/promptFactoryPlaceholders.py | 673 ------------------
.../promptGenerationActionsActionplan.py | 208 ++++++
.../shared/promptGenerationActionsReact.py | 108 +++
.../shared/promptGenerationTaskplan.py | 107 +++
.../processing/shared/reactPromptTemplates.py | 87 ---
.../workflows/processing/workflowProcessor.py | 6 +-
.../obj/m20251005-100001_2_0_0/message.json | 19 +
.../m20251005-100001_2_0_0/message_text.txt | 1 +
.../obj/m20251005-100006_2_1_0/message.json | 19 +
.../m20251005-100006_2_1_0/message_text.txt | 6 +
.../obj/m20251005-100007_2_1_0/message.json | 19 +
.../m20251005-100007_2_1_0/message_text.txt | 3 +
.../obj/m20251005-100020_2_1_1/message.json | 19 +
.../m20251005-100020_2_1_1/message_text.txt | 4 +
.../document_001_metadata.json | 12 +
.../obj/m20251005-100021_2_0_0/message.json | 19 +
.../m20251005-100021_2_0_0/message_text.txt | 4 +
.../obj/m20251005-100021_2_1_0/message.json | 19 +
.../m20251005-100021_2_1_0/message_text.txt | 4 +
31 files changed, 1559 insertions(+), 1575 deletions(-)
rename modules/workflows/processing/modes/{actionplanMode.py => modeActionplan.py} (98%)
rename modules/workflows/processing/modes/{baseMode.py => modeBase.py} (99%)
rename modules/workflows/processing/modes/{reactMode.py => modeReact.py} (96%)
delete mode 100644 modules/workflows/processing/shared/contextAwarePlaceholders.py
create mode 100644 modules/workflows/processing/shared/methodDiscovery.py
create mode 100644 modules/workflows/processing/shared/placeholderFactory.py
create mode 100644 modules/workflows/processing/shared/placeholderFactoryReactOnly.py
delete mode 100644 modules/workflows/processing/shared/promptFactory.py
delete mode 100644 modules/workflows/processing/shared/promptFactoryPlaceholders.py
create mode 100644 modules/workflows/processing/shared/promptGenerationActionsActionplan.py
create mode 100644 modules/workflows/processing/shared/promptGenerationActionsReact.py
create mode 100644 modules/workflows/processing/shared/promptGenerationTaskplan.py
delete mode 100644 modules/workflows/processing/shared/reactPromptTemplates.py
create mode 100644 test-chat/obj/m20251005-100001_2_0_0/message.json
create mode 100644 test-chat/obj/m20251005-100001_2_0_0/message_text.txt
create mode 100644 test-chat/obj/m20251005-100006_2_1_0/message.json
create mode 100644 test-chat/obj/m20251005-100006_2_1_0/message_text.txt
create mode 100644 test-chat/obj/m20251005-100007_2_1_0/message.json
create mode 100644 test-chat/obj/m20251005-100007_2_1_0/message_text.txt
create mode 100644 test-chat/obj/m20251005-100020_2_1_1/message.json
create mode 100644 test-chat/obj/m20251005-100020_2_1_1/message_text.txt
create mode 100644 test-chat/obj/m20251005-100020_2_1_1/round2_task1_action1_results/document_001_metadata.json
create mode 100644 test-chat/obj/m20251005-100021_2_0_0/message.json
create mode 100644 test-chat/obj/m20251005-100021_2_0_0/message_text.txt
create mode 100644 test-chat/obj/m20251005-100021_2_1_0/message.json
create mode 100644 test-chat/obj/m20251005-100021_2_1_0/message_text.txt
diff --git a/modules/workflows/methods/methodOutlook.py b/modules/workflows/methods/methodOutlook.py
index ac01ec64..9f39dc52 100644
--- a/modules/workflows/methods/methodOutlook.py
+++ b/modules/workflows/methods/methodOutlook.py
@@ -1053,7 +1053,7 @@ class MethodOutlook(MethodBase):
Parameters:
connectionReference (str): REQUIRED - Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS list)
- to (str): REQUIRED - Email recipient address
+ to (List[str]): REQUIRED - Email recipient addresses
subject (str): REQUIRED - Email subject line
body (str): REQUIRED - Email body content
cc (List[str], optional): CC recipients
@@ -1072,7 +1072,9 @@ class MethodOutlook(MethodBase):
if not connectionReference or not to or not subject or not body:
return ActionResult.isFailure(error="connectionReference, to, subject, and body are required")
- # Convert single values to lists
+ # Convert single values to lists for all recipient parameters
+ if isinstance(to, str):
+ to = [to]
if isinstance(cc, str):
cc = [cc]
if isinstance(bcc, str):
@@ -1215,7 +1217,7 @@ class MethodOutlook(MethodBase):
Parameters:
connectionReference (str): REQUIRED - Reference to the Microsoft connection (must be a connection label from AVAILABLE_CONNECTIONS list)
- to (str): REQUIRED - Email recipient address
+ 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
cc (List[str], optional): CC recipients
@@ -1236,7 +1238,9 @@ class MethodOutlook(MethodBase):
if not connectionReference or not to or not context:
return ActionResult.isFailure(error="connectionReference, to, and context are required")
- # Convert single values to lists
+ # Convert single values to lists for all recipient parameters
+ if isinstance(to, str):
+ to = [to]
if isinstance(cc, str):
cc = [cc]
if isinstance(bcc, str):
diff --git a/modules/workflows/processing/core/actionExecutor.py b/modules/workflows/processing/core/actionExecutor.py
index cd293369..7ac012f3 100644
--- a/modules/workflows/processing/core/actionExecutor.py
+++ b/modules/workflows/processing/core/actionExecutor.py
@@ -5,7 +5,7 @@ import logging
from typing import Dict, Any, List
from modules.datamodels.datamodelWorkflow import ActionResult, TaskAction, TaskStep
from modules.datamodels.datamodelChat import ChatWorkflow
-from modules.workflows.processing.shared.promptFactory import methods
+from modules.workflows.processing.shared.methodDiscovery import methods
logger = logging.getLogger(__name__)
diff --git a/modules/workflows/processing/core/taskPlanner.py b/modules/workflows/processing/core/taskPlanner.py
index 26b000fb..237a3711 100644
--- a/modules/workflows/processing/core/taskPlanner.py
+++ b/modules/workflows/processing/core/taskPlanner.py
@@ -6,11 +6,11 @@ import logging
from typing import Dict, Any
from modules.datamodels.datamodelWorkflow import TaskStep, TaskContext, TaskPlan
from modules.datamodels.datamodelAi import AiCallOptions, OperationType, ProcessingMode, Priority
-from modules.workflows.processing.shared.promptFactoryPlaceholders import (
- createTaskPlanningPromptTemplate,
- extractUserPrompt,
- extractAvailableDocuments,
- extractWorkflowHistory
+from modules.workflows.processing.shared.promptGenerationTaskplan import (
+ createTaskPlanningPromptTemplate
+)
+from modules.workflows.processing.shared.placeholderFactory import (
+ extractUserPrompt
)
logger = logging.getLogger(__name__)
diff --git a/modules/workflows/processing/modes/actionplanMode.py b/modules/workflows/processing/modes/modeActionplan.py
similarity index 98%
rename from modules/workflows/processing/modes/actionplanMode.py
rename to modules/workflows/processing/modes/modeActionplan.py
index 3ba4a07c..b3a8cb76 100644
--- a/modules/workflows/processing/modes/actionplanMode.py
+++ b/modules/workflows/processing/modes/modeActionplan.py
@@ -1,4 +1,4 @@
-# actionplanMode.py
+# modeActionplan.py
# Actionplan mode implementation for workflows
import json
@@ -11,16 +11,19 @@ from modules.datamodels.datamodelWorkflow import (
)
from modules.datamodels.datamodelChat import ChatWorkflow
from modules.datamodels.datamodelAi import AiCallOptions, OperationType, ProcessingMode, Priority
-from modules.workflows.processing.modes.baseMode import BaseMode
+from modules.workflows.processing.modes.modeBase import BaseMode
from modules.workflows.processing.shared.executionState import TaskExecutionState
-from modules.workflows.processing.shared.promptFactoryPlaceholders import (
+from modules.workflows.processing.shared.promptGenerationActionsActionplan import (
createActionDefinitionPromptTemplate,
- createResultReviewPromptTemplate,
+ createResultReviewPromptTemplate
+)
+from modules.workflows.processing.shared.placeholderFactory import (
extractUserPrompt,
extractAvailableDocuments,
extractWorkflowHistory,
extractAvailableMethods,
extractUserLanguage,
+ extractAvailableConnections,
extractReviewContent
)
@@ -135,8 +138,7 @@ class ActionplanMode(BaseMode):
availableMethods = extractAvailableMethods(self.services)
userLanguage = extractUserLanguage(self.services)
# Action planner also needs connections for parameter generation (like old system)
- availableConnections = self.services.workflow.getConnectionReferenceList()
- availableConnectionsStr = '\n'.join(f"- {conn}" for conn in availableConnections) if availableConnections else "No connections available"
+ availableConnectionsStr = extractAvailableConnections(self.services)
# Create placeholders dictionary
placeholders = {
diff --git a/modules/workflows/processing/modes/baseMode.py b/modules/workflows/processing/modes/modeBase.py
similarity index 99%
rename from modules/workflows/processing/modes/baseMode.py
rename to modules/workflows/processing/modes/modeBase.py
index 173bd98b..611be32a 100644
--- a/modules/workflows/processing/modes/baseMode.py
+++ b/modules/workflows/processing/modes/modeBase.py
@@ -1,4 +1,4 @@
-# baseMode.py
+# modeBase.py
# Abstract base class for workflow modes
from abc import ABC, abstractmethod
diff --git a/modules/workflows/processing/modes/reactMode.py b/modules/workflows/processing/modes/modeReact.py
similarity index 96%
rename from modules/workflows/processing/modes/reactMode.py
rename to modules/workflows/processing/modes/modeReact.py
index b5eb94aa..82f6ccf6 100644
--- a/modules/workflows/processing/modes/reactMode.py
+++ b/modules/workflows/processing/modes/modeReact.py
@@ -1,4 +1,4 @@
-# reactMode.py
+# modeReact.py
# React mode implementation for workflows
import json
@@ -13,13 +13,13 @@ from modules.datamodels.datamodelWorkflow import (
)
from modules.datamodels.datamodelChat import ChatWorkflow
from modules.datamodels.datamodelAi import AiCallOptions, OperationType, ProcessingMode, Priority
-from modules.workflows.processing.modes.baseMode import BaseMode
-from modules.workflows.processing.shared.executionState import TaskExecutionState, should_continue
-from modules.workflows.processing.shared.contextAwarePlaceholders import (
+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.reactPromptTemplates import (
+from modules.workflows.processing.shared.promptGenerationActionsReact import (
createReactPlanSelectionPromptTemplate,
createReactParametersPromptTemplate,
createReactRefinementPromptTemplate
@@ -150,8 +150,8 @@ class ReactMode(BaseMode):
progressState = self.progressTracker.getCurrentProgress()
shouldContinue = self.progressTracker.shouldContinue(progressState, observation.get('contentValidation', {}))
- if not shouldContinue or not should_continue(observation, lastReviewDict, step, state.max_steps):
- logger.info(f"Stopping at step {step}: shouldContinue={shouldContinue}, should_continue={should_continue(observation, lastReviewDict, step, state.max_steps)}")
+ if not shouldContinue or not shouldContinue(observation, lastReviewDict, step, state.max_steps):
+ logger.info(f"Stopping at step {step}: shouldContinue={shouldContinue}, shouldContinue={shouldContinue(observation, lastReviewDict, step, state.max_steps)}")
break
step += 1
@@ -233,25 +233,8 @@ class ReactMode(BaseMode):
promptTemplate = createReactParametersPromptTemplate()
# Get action parameter description (not function signature)
- actionParameters = ""
- from modules.workflows.processing.shared.promptFactory import methods
- if self.services and methodName in methods:
- methodInstance = methods[methodName]['instance']
- if actionName in methodInstance.actions:
- action_info = methodInstance.actions[actionName]
- # Extract parameter descriptions from docstring
- docstring = action_info.get('description', '')
- paramDescriptions, paramTypes = methodInstance._extractParameterDetails(docstring)
-
- param_list = []
- for paramName, paramDesc in paramDescriptions.items():
- paramType = paramTypes.get(paramName, 'Any')
- if paramDesc:
- param_list.append(f"- {paramName} ({paramType}): {paramDesc}")
- else:
- param_list.append(f"- {paramName} ({paramType})")
-
- actionParameters = "Required parameters:\n" + "\n".join(param_list)
+ from modules.workflows.processing.shared.methodDiscovery import methods, getActionParameterSignature
+ actionParameters = getActionParameterSignature(methodName, actionName, methods)
selectedAction = compoundActionName
diff --git a/modules/workflows/processing/shared/contextAwarePlaceholders.py b/modules/workflows/processing/shared/contextAwarePlaceholders.py
deleted file mode 100644
index 104082a3..00000000
--- a/modules/workflows/processing/shared/contextAwarePlaceholders.py
+++ /dev/null
@@ -1,397 +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
-
-logger = logging.getLogger(__name__)
-
-class WorkflowPhase(Enum):
- """Different phases of workflow execution requiring different context levels."""
- TASK_PLANNING = "task_planning" # Needs full context for planning
- REACT_PLAN_SELECTION = "react_plan_selection" # Needs minimal context for action selection
- REACT_PARAMETERS = "react_parameters" # Needs full context for parameter generation
- ACTION_PLANNING = "action_planning" # Needs full context for action planning
- RESULT_REVIEW = "result_review" # Needs full context for review
-
-class ContextAwarePlaceholders:
- """Context-aware placeholder service that provides different context levels based on workflow phase."""
-
- def __init__(self, services):
- self.services = services
-
- async def getPlaceholders(self, phase: WorkflowPhase, context: Any, additional_data: Dict[str, Any] = None) -> Dict[str, str]:
- """
- Get placeholders based on workflow phase and context.
-
- Args:
- phase: The workflow phase determining context level
- context: The workflow context object
- additional_data: Additional data for specific phases (e.g., selected action)
-
- Returns:
- Dictionary of placeholder key-value pairs
- """
- if phase == WorkflowPhase.TASK_PLANNING:
- return self._getTaskPlanningPlaceholders(context)
- elif phase == WorkflowPhase.REACT_PLAN_SELECTION:
- return self._getReactPlanSelectionPlaceholders(context)
- elif phase == WorkflowPhase.REACT_PARAMETERS:
- return await self._getReactParametersPlaceholders(context, additional_data)
- elif phase == WorkflowPhase.ACTION_PLANNING:
- return self._getActionPlanningPlaceholders(context)
- elif phase == WorkflowPhase.RESULT_REVIEW:
- return self._getResultReviewPlaceholders(context)
- else:
- logger.warning(f"Unknown workflow phase: {phase}")
- return self._getMinimalPlaceholders(context)
-
- def _getTaskPlanningPlaceholders(self, context: Any) -> Dict[str, str]:
- """Get full context placeholders for task planning."""
- return {
- "USER_PROMPT": self._extractUserPrompt(context),
- "AVAILABLE_DOCUMENTS": self._getFullDocumentContext(context),
- "WORKFLOW_HISTORY": self._getWorkflowHistory(context),
- "USER_LANGUAGE": self._extractUserLanguage(),
- }
-
- def _getReactPlanSelectionPlaceholders(self, context: Any) -> Dict[str, str]:
- """Get minimal context placeholders for React plan selection."""
- return {
- "USER_PROMPT": self._extractUserPrompt(context),
- "AVAILABLE_DOCUMENTS": self._getMinimalDocumentContext(context),
- "USER_LANGUAGE": self._extractUserLanguage(),
- "AVAILABLE_METHODS": self._getAvailableMethods(),
- "AVAILABLE_CONNECTIONS": self._getMinimalConnectionContext(),
- }
-
- async def _getReactParametersPlaceholders(self, context: Any, additional_data: Dict[str, Any] = None) -> Dict[str, str]:
- """Get full context placeholders for React parameter generation."""
- # Get both original user prompt and current task objective
- original_prompt = self._extractUserPrompt(context)
- current_task = ""
- if hasattr(context, 'task_step') and context.task_step and context.task_step.objective:
- current_task = context.task_step.objective
-
- # Combine original prompt and current task for better context
- combined_prompt = f"Original request: {original_prompt}"
- if current_task and current_task != original_prompt:
- combined_prompt += f"\n\nCurrent task: {current_task}"
-
- # Generate intelligent action objective
- action_objective = await self._generateActionObjective(context, current_task, original_prompt, additional_data)
-
- placeholders = {
- "USER_PROMPT": combined_prompt,
- "ACTION_OBJECTIVE": action_objective, # AI-generated intelligent objective
- "AVAILABLE_DOCUMENTS": self._getFullDocumentContext(context),
- "USER_LANGUAGE": self._extractUserLanguage(),
- "AVAILABLE_CONNECTIONS": self._getFullConnectionContext(),
- "PREVIOUS_ACTION_RESULTS": self._getPreviousActionResults(context),
- "LEARNINGS_AND_IMPROVEMENTS": self._getLearningsAndImprovements(context),
- "LATEST_REFINEMENT_FEEDBACK": self._getLatestRefinementFeedback(context),
- }
-
- # Add additional data if provided (e.g., selected action, action signature)
- if additional_data:
- placeholders.update(additional_data)
-
- return placeholders
-
- def _getActionPlanningPlaceholders(self, context: Any) -> Dict[str, str]:
- """Get full context placeholders for action planning."""
- return {
- "USER_PROMPT": self._extractUserPrompt(context),
- "AVAILABLE_DOCUMENTS": self._getFullDocumentContext(context),
- "WORKFLOW_HISTORY": self._getWorkflowHistory(context),
- "AVAILABLE_METHODS": self._getAvailableMethods(),
- "AVAILABLE_CONNECTIONS": self._getFullConnectionContext(),
- "USER_LANGUAGE": self._extractUserLanguage(),
- }
-
- def _getResultReviewPlaceholders(self, context: Any) -> Dict[str, str]:
- """Get full context placeholders for result review."""
- return {
- "USER_PROMPT": self._extractUserPrompt(context),
- "REVIEW_CONTENT": self._getReviewContent(context),
- }
-
- def _getMinimalPlaceholders(self, context: Any) -> Dict[str, str]:
- """Get minimal placeholders as fallback."""
- return {
- "USER_PROMPT": self._extractUserPrompt(context),
- "USER_LANGUAGE": self._extractUserLanguage(),
- }
-
- # Helper methods for extracting different context levels
-
- def _extractUserPrompt(self, context: Any) -> str:
- """Extract user prompt from context."""
- # Get the current user prompt from services (clean and reliable)
- if self.services and hasattr(self.services, 'currentUserPrompt') and self.services.currentUserPrompt:
- return self.services.currentUserPrompt
-
- # Fallback to task step objective if no current prompt found
- if hasattr(context, 'task_step') and context.task_step:
- return context.task_step.objective or 'No request specified'
- return 'No request specified'
-
- def _extractUserLanguage(self) -> str:
- """Extract user language from service."""
- return self.services.user.language if self.services and self.services.user else 'en'
-
- def _getMinimalDocumentContext(self, context: Any) -> str:
- """Get minimal document context (counts only) for React plan selection."""
- try:
- if hasattr(context, 'workflow') and context.workflow:
- # Get document count from workflow
- documents = self.services.workflow.getAvailableDocuments(context.workflow)
- if documents and documents != "No documents available":
- # Count documents by counting docList and docItem references
- doc_count = documents.count("docList:") + documents.count("docItem:")
- return f"{doc_count} documents available from previous tasks"
- else:
- return "No documents available"
- return "No documents available"
- except Exception as e:
- logger.error(f"Error getting minimal document context: {str(e)}")
- return "No documents available"
-
- def _getFullDocumentContext(self, context: Any) -> str:
- """Get full document context with detailed references for parameter generation."""
- try:
- if hasattr(context, 'workflow') and context.workflow:
- return self.services.workflow.getAvailableDocuments(context.workflow)
- return "No documents available"
- except Exception as e:
- logger.error(f"Error getting full document context: {str(e)}")
- return "No documents available"
-
- def _getMinimalConnectionContext(self) -> str:
- """Get minimal connection context (count only) for React plan selection."""
- try:
- connections = self.services.workflow.getConnectionReferenceList()
- if connections:
- return f"{len(connections)} connections available"
- return "No connections available"
- except Exception as e:
- logger.error(f"Error getting minimal connection context: {str(e)}")
- return "No connections available"
-
- def _getFullConnectionContext(self) -> str:
- """Get full connection context with detailed references for parameter generation."""
- try:
- connections = self.services.workflow.getConnectionReferenceList()
- if connections:
- return '\n'.join(f"- {conn}" for conn in connections)
- return "No connections available"
- except Exception as e:
- logger.error(f"Error getting full connection context: {str(e)}")
- return "No connections available"
-
- def _getWorkflowHistory(self, context: Any) -> str:
- """Get workflow history for task planning."""
- try:
- if hasattr(context, 'workflow') and context.workflow:
- from modules.workflows.processing.shared.promptFactory import getPreviousRoundContext
- return getPreviousRoundContext(self.services, context.workflow) or "No previous workflow rounds - this is the first round."
- return "No previous workflow rounds - this is the first round."
- except Exception as e:
- logger.error(f"Error getting workflow history: {str(e)}")
- return "No previous workflow rounds - this is the first round."
-
- def _getAvailableMethods(self) -> str:
- """Get available methods for action selection and planning using compound action names."""
- try:
- from modules.workflows.processing.shared.promptFactory import methods, discoverMethods
-
- # Get the methods dictionary
- if not methods:
- discoverMethods(self.services)
-
- # Create a flat JSON format with compound action names for better AI parsing
- available_actions_json = {}
- for methodName, methodInfo in methods.items():
- # Convert MethodAi -> ai, MethodDocument -> document, etc.
- shortName = methodName.replace('Method', '').lower()
-
- for actionName, actionInfo in methodInfo['actions'].items():
- # Create compound action name: method.action
- compoundActionName = f"{shortName}.{actionName}"
- # Get the action description
- action_description = actionInfo.get('description', f"Execute {actionName} action")
- available_actions_json[compoundActionName] = action_description
-
- return json.dumps(available_actions_json, indent=2, ensure_ascii=False)
- except Exception as e:
- logger.error(f"Error extracting available methods: {str(e)}")
- return json.dumps({}, indent=2, ensure_ascii=False)
-
- def _getReviewContent(self, context: Any) -> str:
- """Get review content for result validation."""
- try:
- from modules.workflows.processing.shared.promptFactoryPlaceholders import extractReviewContent
- return extractReviewContent(context)
- except Exception as e:
- logger.error(f"Error getting review content: {str(e)}")
- return "No review content available"
-
- def _getPreviousActionResults(self, context: Any) -> str:
- """Get previous action results for learning context."""
- try:
- if not hasattr(context, 'previous_action_results') or not context.previous_action_results:
- return "No previous actions executed yet"
-
- results = []
- for i, result in enumerate(context.previous_action_results[-5:], 1): # Last 5 results
- if hasattr(result, 'resultLabel') and hasattr(result, 'status'):
- status = "SUCCESS" if result.status == "completed" else "FAILED"
- results.append(f"Action {i}: {result.resultLabel} - {status}")
- if hasattr(result, 'error') and result.error:
- results.append(f" Error: {result.error}")
-
- return "\n".join(results) if results else "No previous actions executed yet"
- except Exception as e:
- logger.error(f"Error getting previous action results: {str(e)}")
- return "No previous actions executed yet"
-
- def _getLearningsAndImprovements(self, context: Any) -> str:
- """Get learnings and improvements from previous actions."""
- try:
- learnings = []
-
- # Get improvements from context
- if hasattr(context, 'improvements') and context.improvements and isinstance(context.improvements, list):
- learnings.append("IMPROVEMENTS:")
- for improvement in context.improvements[-3:]: # Last 3 improvements
- learnings.append(f"- {improvement}")
-
- # Get failure patterns
- if hasattr(context, 'failure_patterns') and context.failure_patterns and isinstance(context.failure_patterns, list):
- learnings.append("FAILURE PATTERNS TO AVOID:")
- for pattern in context.failure_patterns[-3:]: # Last 3 patterns
- learnings.append(f"- {pattern}")
-
- # Get successful actions
- if hasattr(context, 'successful_actions') and context.successful_actions and isinstance(context.successful_actions, list):
- learnings.append("SUCCESSFUL APPROACHES:")
- for action in context.successful_actions[-3:]: # Last 3 successful
- learnings.append(f"- {action}")
-
- return "\n".join(learnings) if learnings else "No learnings available yet"
- except Exception as e:
- logger.error(f"Error getting learnings and improvements: {str(e)}")
- return "No learnings available yet"
-
- def _getLatestRefinementFeedback(self, context: Any) -> str:
- """Get the latest refinement feedback to influence next action planning."""
- try:
- if not hasattr(context, 'previous_review_result') or not context.previous_review_result or not isinstance(context.previous_review_result, list):
- return "No previous refinement feedback available"
-
- # Get the most recent refinement decision
- latest_decision = context.previous_review_result[-1]
- if not isinstance(latest_decision, dict):
- return "No previous refinement feedback available"
-
- feedback_parts = []
-
- # Add decision and reason
- decision = latest_decision.get('decision', 'unknown')
- reason = latest_decision.get('reason', 'No reason provided')
- feedback_parts.append(f"Latest Decision: {decision}")
- feedback_parts.append(f"Reason: {reason}")
-
- # Add any specific feedback or suggestions
- if 'feedback' in latest_decision:
- feedback_parts.append(f"Feedback: {latest_decision['feedback']}")
-
- if 'suggestions' in latest_decision:
- feedback_parts.append(f"Suggestions: {latest_decision['suggestions']}")
-
- return "\n".join(feedback_parts)
- except Exception as e:
- logger.error(f"Error getting latest refinement feedback: {str(e)}")
- return "No previous refinement feedback available"
-
- async def _generateActionObjective(self, context: Any, current_task: str, original_prompt: str, additional_data: Dict[str, Any] = None) -> str:
- """Generate intelligent, context-aware action objective using AI."""
- try:
- # Get the selected action from additional_data
- selected_action = additional_data.get('SELECTED_ACTION', '') if additional_data else ''
-
- # Build context for AI objective generation
- context_info = {
- "original_prompt": original_prompt,
- "current_task": current_task,
- "selected_action": selected_action,
- "available_documents": self._getFullDocumentContext(context),
- "available_connections": self._getFullConnectionContext(),
- "previous_results": self._getPreviousActionResults(context),
- "learnings": self._getLearningsAndImprovements(context),
- "refinement_feedback": self._getLatestRefinementFeedback(context),
- "user_language": self._extractUserLanguage()
- }
-
- # Create AI prompt for objective generation
- objective_prompt = f"""Generate a specific, actionable objective for the selected action.
-
-CONTEXT:
-- Original User Request: {context_info['original_prompt']}
-- Current Task: {context_info['current_task']}
-- Selected Action: {context_info['selected_action']}
-- Available Documents: {context_info['available_documents']}
-- Available Connections: {context_info['available_connections']}
-- Previous Action Results: {context_info['previous_results']}
-- Learnings and Improvements: {context_info['learnings']}
-- Latest Refinement Feedback: {context_info['refinement_feedback']}
-- User Language: {context_info['user_language']}
-
-REQUIREMENTS:
-1. Create a SPECIFIC objective that tells the action exactly what to accomplish
-2. Include relevant details about documents, connections, recipients, etc.
-3. Learn from previous attempts and refinement feedback
-4. Make it actionable and concrete
-5. Focus on the user's actual intent, not just the task description
-6. If this is a retry, incorporate learnings from previous failures
-
-RESPONSE FORMAT:
-Return ONLY the objective text, no explanations or formatting.
-
-OBJECTIVE:"""
-
- # Call AI to generate the objective
- if self.services and hasattr(self.services, 'ai'):
- from modules.datamodels.datamodelAi import AiCallOptions, OperationType, Priority, ProcessingMode
-
- options = AiCallOptions(
- operationType=OperationType.ANALYSE_CONTENT,
- priority=Priority.BALANCED,
- compressPrompt=False,
- compressContext=False,
- processingMode=ProcessingMode.ADVANCED,
- maxCost=0.01,
- maxProcessingTime=10
- )
-
- response = await self.services.ai.callAi(
- prompt=objective_prompt,
- placeholders={},
- options=options
- )
-
- # Extract objective from response
- if response and response.strip():
- return response.strip()
-
- # Fallback to current task if AI fails
- return current_task or original_prompt
-
- except Exception as e:
- logger.error(f"Error generating action objective: {str(e)}")
- # Fallback to current task
- return current_task or original_prompt
diff --git a/modules/workflows/processing/shared/executionState.py b/modules/workflows/processing/shared/executionState.py
index 3c506326..d6368f5f 100644
--- a/modules/workflows/processing/shared/executionState.py
+++ b/modules/workflows/processing/shared/executionState.py
@@ -58,7 +58,7 @@ class TaskExecutionState:
patterns.append("permission_issues")
return list(set(patterns))
-def should_continue(observation, review=None, current_step: int = 0, max_steps: int = 5) -> bool:
+def shouldContinue(observation, review=None, current_step: int = 0, max_steps: int = 5) -> bool:
"""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
diff --git a/modules/workflows/processing/shared/methodDiscovery.py b/modules/workflows/processing/shared/methodDiscovery.py
new file mode 100644
index 00000000..16b4b30c
--- /dev/null
+++ b/modules/workflows/processing/shared/methodDiscovery.py
@@ -0,0 +1,130 @@
+# methodDiscovery.py
+# Method discovery and management for workflow execution
+
+import json
+import logging
+import importlib
+import pkgutil
+import inspect
+from typing import Any, Dict, List
+from modules.datamodels.datamodelWorkflow import TaskContext, ReviewContext, DocumentExchange
+from modules.workflows.methods.methodBase import MethodBase
+
+# Set up logger
+logger = logging.getLogger(__name__)
+
+# Global methods catalog - moved from serviceCenter
+methods = {}
+
+def discoverMethods(serviceCenter):
+ """Dynamically discover all method classes and their actions in modules methods package"""
+ try:
+ # Import the methods package
+ methodsPackage = importlib.import_module('modules.workflows.methods')
+
+ # Discover all modules in the package
+ for _, name, isPkg in pkgutil.iter_modules(methodsPackage.__path__):
+ if not isPkg and name.startswith('method'):
+ try:
+ # Import the module
+ module = importlib.import_module(f'modules.workflows.methods.{name}')
+
+ # Find all classes in the module that inherit from MethodBase
+ for itemName, item in inspect.getmembers(module):
+ if (inspect.isclass(item) and
+ issubclass(item, MethodBase) and
+ item != MethodBase):
+ # Instantiate the method
+ methodInstance = item(serviceCenter)
+
+ # Use the actions property from MethodBase which handles @action decorator
+ actions = methodInstance.actions
+
+ # Create method info
+ methodInfo = {
+ 'instance': methodInstance,
+ 'actions': actions,
+ 'description': item.__doc__ or f"Method {itemName}"
+ }
+
+ # Store the method with full class name
+ methods[itemName] = methodInfo
+
+ # Also store with short name for action executor access
+ shortName = itemName.replace('Method', '').lower()
+ methods[shortName] = methodInfo
+
+ logger.info(f"Discovered method {itemName} (short: {shortName}) with {len(actions)} actions")
+
+ except Exception as e:
+ logger.error(f"Error discovering method {name}: {str(e)}")
+ continue
+
+ logger.info(f"Discovered {len(methods)} method entries total")
+
+ except Exception as e:
+ logger.error(f"Error discovering methods: {str(e)}")
+
+def getMethodsList(serviceCenter):
+ """Get a list of available methods with their signatures"""
+ if not methods:
+ discoverMethods(serviceCenter)
+
+ methodsList = []
+ for methodName, methodInfo in methods.items():
+ methodDescription = methodInfo['description']
+ actionsList = []
+
+ for actionName, actionInfo in methodInfo['actions'].items():
+ actionDescription = actionInfo['description']
+ parameters = actionInfo['parameters']
+
+ # Build parameter signature
+ paramSig = []
+ for paramName, paramInfo in parameters.items():
+ paramType = paramInfo['type']
+ paramRequired = paramInfo['required']
+ paramDefault = paramInfo['default']
+
+ if paramRequired:
+ paramSig.append(f"{paramName}: {paramType}")
+ else:
+ defaultStr = f" = {paramDefault}" if paramDefault is not None else " = None"
+ paramSig.append(f"{paramName}: {paramType}{defaultStr}")
+
+ paramSignature = f"({', '.join(paramSig)})" if paramSig else "()"
+ actionsList.append(f"- {actionName}{paramSignature}: {actionDescription}")
+
+ actionsStr = "\n".join(actionsList)
+ methodsList.append(f"**{methodName}**: {methodDescription}\n{actionsStr}")
+
+ return "\n\n".join(methodsList)
+
+def getActionParameterSignature(methodName: str, actionName: str, methods: Dict[str, Any]) -> str:
+ """Get action parameter signature from method docstring for AI parameter generation"""
+ try:
+ if not methods or methodName not in methods:
+ return ""
+
+ methodInstance = methods[methodName]['instance']
+ if actionName not in methodInstance.actions:
+ return ""
+
+ action_info = methodInstance.actions[actionName]
+ # Extract parameter descriptions from docstring
+ docstring = action_info.get('description', '')
+ paramDescriptions, paramTypes = methodInstance._extractParameterDetails(docstring)
+
+ param_list = []
+ for paramName, paramDesc in paramDescriptions.items():
+ paramType = paramTypes.get(paramName, 'Any')
+ if paramDesc:
+ param_list.append(f"- {paramName} ({paramType}): {paramDesc}")
+ else:
+ param_list.append(f"- {paramName} ({paramType})")
+
+ return "Required parameters:\n" + "\n".join(param_list)
+ except Exception as e:
+ logger.error(f"Error getting action parameter signature for {methodName}.{actionName}: {str(e)}")
+ return ""
+
diff --git a/modules/workflows/processing/shared/placeholderFactory.py b/modules/workflows/processing/shared/placeholderFactory.py
new file mode 100644
index 00000000..e4dfd224
--- /dev/null
+++ b/modules/workflows/processing/shared/placeholderFactory.py
@@ -0,0 +1,633 @@
+"""
+Placeholder Factory
+Centralized placeholder extraction functions for all workflow modes.
+Each function corresponds to a {{KEY:PLACEHOLDER_NAME}} in prompt templates.
+
+NAMING CONVENTION:
+- All functions follow pattern: extract{PlaceholderName}()
+- 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()
+"""
+
+import json
+import logging
+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
+# ============================================================================
+
+def extractUserPrompt(context: Any) -> str:
+ """Extract user prompt from context. Maps to {{KEY:USER_PROMPT}}"""
+ 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:
+ # Get the methods dictionary directly from the global methods variable
+ if not methods:
+ discoverMethods(service)
+
+ # Create a flat JSON format with compound action names for better AI parsing
+ available_actions_json = {}
+ for methodName, methodInfo in methods.items():
+ # Convert MethodAi -> ai, MethodDocument -> document, etc.
+ shortName = methodName.replace('Method', '').lower()
+
+ for actionName, actionInfo in methodInfo['actions'].items():
+ # Create compound action name: method.action
+ compoundActionName = f"{shortName}.{actionName}"
+ # Get the action description
+ action_description = actionInfo.get('description', f"Execute {actionName} action")
+ available_actions_json[compoundActionName] = action_description
+
+ return json.dumps(available_actions_json, indent=2, ensure_ascii=False)
+ except Exception as e:
+ logger.error(f"Error extracting available methods: {str(e)}")
+ return json.dumps({}, indent=2, ensure_ascii=False)
+
+
+def 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:
+ # Get connections from the database
+ if hasattr(services, 'interfaceDbApp') and hasattr(services, 'user'):
+ userId = services.user.id
+ connections = services.interfaceDbApp.getUserConnections(userId)
+ if connections:
+ # Format connections as reference strings
+ connectionRefs = []
+ for conn in connections:
+ # Create reference string in format: conn_{authority}_{id}
+ ref = f"conn_{conn.authority.value}_{conn.id}"
+ connectionRefs.append(ref)
+ return connectionRefs
+
+ return []
+ except Exception as e:
+ logger.error(f"Error getting connection reference list: {str(e)}")
+ return []
+
+
+def getPreviousRoundContext(services, context: Any) -> str:
+ """Get previous round context for prompt"""
+ try:
+ if not context or not hasattr(context, 'workflow_id'):
+ return "No previous round context available"
+
+ workflowId = context.workflow_id
+ if not workflowId:
+ return "No previous round context available"
+
+ # Get previous round results
+ previousResults = getattr(context, 'previous_results', [])
+ if not previousResults:
+ return "No previous round context available"
+
+ contextList = []
+ for i, result in enumerate(previousResults, 1):
+ if hasattr(result, 'success') and hasattr(result, 'resultLabel'):
+ status = "Success" if result.success else "Failed"
+ contextList.append(f"{i}. {result.resultLabel} - {status}")
+ elif isinstance(result, dict):
+ status = "Success" if result.get('success', False) else "Failed"
+ label = result.get('resultLabel', 'Unknown')
+ contextList.append(f"{i}. {label} - {status}")
+ else:
+ contextList.append(f"{i}. {str(result)}")
+
+ return "\n".join(contextList) if contextList else "No previous round context available"
+ except Exception as e:
+ 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:
+ if hasattr(context, 'action_results') and context.action_results:
+ # Build result summary
+ result_summary = ""
+ for i, result in enumerate(context.action_results):
+ result_summary += f"\nRESULT {i+1}:\n"
+ result_summary += f" Success: {result.success}\n"
+ if result.error:
+ result_summary += f" Error: {result.error}\n"
+
+ if result.documents:
+ result_summary += f" Documents: {len(result.documents)} document(s)\n"
+ for doc in result.documents:
+ # Extract all available metadata without content
+ doc_metadata = {
+ "name": getattr(doc, 'documentName', 'Unknown'),
+ "mimeType": getattr(doc, 'mimeType', 'Unknown'),
+ "size": getattr(doc, 'size', 'Unknown'),
+ "created": getattr(doc, 'created', 'Unknown'),
+ "modified": getattr(doc, 'modified', 'Unknown'),
+ "typeGroup": getattr(doc, 'typeGroup', 'Unknown'),
+ "documentId": getattr(doc, 'documentId', 'Unknown'),
+ "reference": getattr(doc, 'reference', 'Unknown')
+ }
+ # Remove 'Unknown' values to keep it clean
+ doc_metadata = {k: v for k, v in doc_metadata.items() if v != 'Unknown'}
+ result_summary += f" - {json.dumps(doc_metadata, indent=6, ensure_ascii=False)}\n"
+ else:
+ result_summary += f" Documents: None\n"
+
+ return result_summary
+ elif hasattr(context, 'observation') and context.observation:
+ # For observation data, show full content but handle documents specially
+ if isinstance(context.observation, dict):
+ # Create a copy to modify
+ obs_copy = context.observation.copy()
+
+ # If there are previews with documents, show only metadata
+ if 'previews' in obs_copy and isinstance(obs_copy['previews'], list):
+ for preview in obs_copy['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_copy, indent=2, ensure_ascii=False)
+ else:
+ return json.dumps(context.observation, ensure_ascii=False)
+ elif hasattr(context, 'step_result') and context.step_result and 'observation' in context.step_result:
+ # For observation data in step_result, show full content but handle documents specially
+ observation = context.step_result['observation']
+ if isinstance(observation, dict):
+ # Create a copy to modify
+ obs_copy = observation.copy()
+
+ # If there are previews with documents, show only metadata
+ if 'previews' in obs_copy and isinstance(obs_copy['previews'], list):
+ for preview in obs_copy['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_copy, indent=2, ensure_ascii=False)
+ else:
+ return json.dumps(observation, ensure_ascii=False)
+ else:
+ return "No review content available"
+ except Exception as e:
+ 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:
+ if not hasattr(context, 'previous_action_results') or not context.previous_action_results:
+ return "No previous actions executed yet"
+
+ results = []
+ for i, result in enumerate(context.previous_action_results[-5:], 1): # Last 5 results
+ if hasattr(result, 'resultLabel') and hasattr(result, 'status'):
+ status = "SUCCESS" if result.status == "completed" else "FAILED"
+ results.append(f"Action {i}: {result.resultLabel} - {status}")
+ if hasattr(result, 'error') and result.error:
+ results.append(f" Error: {result.error}")
+
+ return "\n".join(results) if results else "No previous actions executed yet"
+ except Exception as e:
+ logger.error(f"Error 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:
+ learnings = []
+
+ # Get improvements from context
+ if hasattr(context, 'improvements') and context.improvements and isinstance(context.improvements, list):
+ learnings.append("IMPROVEMENTS:")
+ for improvement in context.improvements[-3:]: # Last 3 improvements
+ learnings.append(f"- {improvement}")
+
+ # Get failure patterns
+ if hasattr(context, 'failure_patterns') and context.failure_patterns and isinstance(context.failure_patterns, list):
+ learnings.append("FAILURE PATTERNS TO AVOID:")
+ for pattern in context.failure_patterns[-3:]: # Last 3 patterns
+ learnings.append(f"- {pattern}")
+
+ # Get successful actions
+ if hasattr(context, 'successful_actions') and context.successful_actions and isinstance(context.successful_actions, list):
+ learnings.append("SUCCESSFUL APPROACHES:")
+ for action in context.successful_actions[-3:]: # Last 3 successful
+ learnings.append(f"- {action}")
+
+ return "\n".join(learnings) if learnings else "No learnings available yet"
+ except Exception as e:
+ logger.error(f"Error 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:
+ if not hasattr(context, 'previous_review_result') or not context.previous_review_result or not isinstance(context.previous_review_result, list):
+ return "No previous refinement feedback available"
+
+ # Get the most recent refinement decision
+ latest_decision = context.previous_review_result[-1]
+ if not isinstance(latest_decision, dict):
+ return "No previous refinement feedback available"
+
+ feedback_parts = []
+
+ # Add decision and reason
+ decision = latest_decision.get('decision', 'unknown')
+ reason = latest_decision.get('reason', 'No reason provided')
+ feedback_parts.append(f"Latest Decision: {decision}")
+ feedback_parts.append(f"Reason: {reason}")
+
+ # Add any specific feedback or suggestions
+ if 'feedback' in latest_decision:
+ feedback_parts.append(f"Feedback: {latest_decision['feedback']}")
+
+ if 'suggestions' in latest_decision:
+ feedback_parts.append(f"Suggestions: {latest_decision['suggestions']}")
+
+ return "\n".join(feedback_parts)
+ except Exception as e:
+ logger.error(f"Error 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."""
+ 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"
+ except Exception as e:
+ logger.error(f"Error getting minimal document context: {str(e)}")
+ return "No documents available"
+
+
+def extractFullDocumentContext(service: Any, context: Any) -> str:
+ """Extract full document context 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)}")
+ return "No documents available"
+
+
+def extractMinimalConnectionContext(service: Any) -> str:
+ """Extract minimal connection context (count only) for React plan selection."""
+ 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)}")
+ return "No connections available"
+
+
+def extractFullConnectionContext(service: Any) -> str:
+ """Extract full connection context 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)}")
+ 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
new file mode 100644
index 00000000..d2675fe2
--- /dev/null
+++ b/modules/workflows/processing/shared/placeholderFactoryReactOnly.py
@@ -0,0 +1,189 @@
+"""
+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/promptFactory.py b/modules/workflows/processing/shared/promptFactory.py
deleted file mode 100644
index cc9e733a..00000000
--- a/modules/workflows/processing/shared/promptFactory.py
+++ /dev/null
@@ -1,371 +0,0 @@
-# promptFactory.py
-# Enhanced prompt factory with reusable functions
-
-import json
-import logging
-import importlib
-import pkgutil
-import inspect
-from typing import Any, Dict, List
-from modules.datamodels.datamodelWorkflow import TaskContext, ReviewContext, DocumentExchange
-from modules.datamodels.datamodelChat import ChatDocument
-from modules.services.serviceGeneration.subDocumentUtility import getFileExtension
-from modules.workflows.methods.methodBase import MethodBase
-
-# Set up logger
-logger = logging.getLogger(__name__)
-
-# Global methods catalog - moved from serviceCenter
-methods = {}
-
-def discoverMethods(serviceCenter):
- """Dynamically discover all method classes and their actions in modules methods package"""
- try:
- # Import the methods package
- methodsPackage = importlib.import_module('modules.workflows.methods')
-
- # Discover all modules in the package
- for _, name, isPkg in pkgutil.iter_modules(methodsPackage.__path__):
- if not isPkg and name.startswith('method'):
- try:
- # Import the module
- module = importlib.import_module(f'modules.workflows.methods.{name}')
-
- # Find all classes in the module that inherit from MethodBase
- for itemName, item in inspect.getmembers(module):
- if (inspect.isclass(item) and
- issubclass(item, MethodBase) and
- item != MethodBase):
- # Instantiate the method
- methodInstance = item(serviceCenter)
-
- # Use the actions property from MethodBase which handles @action decorator
- actions = methodInstance.actions
-
- # Create method info
- methodInfo = {
- 'instance': methodInstance,
- 'actions': actions,
- 'description': item.__doc__ or f"Method {itemName}"
- }
-
- # Store the method with full class name
- methods[itemName] = methodInfo
-
- # Also store with short name for action executor access
- shortName = itemName.replace('Method', '').lower()
- methods[shortName] = methodInfo
-
- logger.info(f"Discovered method {itemName} (short: {shortName}) with {len(actions)} actions")
-
- except Exception as e:
- logger.error(f"Error discovering method {name}: {str(e)}")
- continue
-
- logger.info(f"Discovered {len(methods)} method entries total")
-
- except Exception as e:
- logger.error(f"Error discovering methods: {str(e)}")
-
-def getMethodsList(serviceCenter):
- """Get a list of available methods with their signatures"""
- if not methods:
- discoverMethods(serviceCenter)
-
- methodsList = []
- for methodName, methodInfo in methods.items():
- methodDescription = methodInfo['description']
- actionsList = []
-
- for actionName, actionInfo in methodInfo['actions'].items():
- actionDescription = actionInfo['description']
- parameters = actionInfo['parameters']
-
- # Build parameter signature
- paramSig = []
- for paramName, paramInfo in parameters.items():
- paramType = paramInfo['type']
- paramRequired = paramInfo['required']
- paramDefault = paramInfo['default']
-
- if paramRequired:
- paramSig.append(f"{paramName}: {paramType}")
- else:
- defaultStr = f" = {paramDefault}" if paramDefault is not None else " = None"
- paramSig.append(f"{paramName}: {paramType}{defaultStr}")
-
- paramSignature = f"({', '.join(paramSig)})" if paramSig else "()"
- actionsList.append(f"- {actionName}{paramSignature}: {actionDescription}")
-
- actionsStr = "\n".join(actionsList)
- methodsList.append(f"**{methodName}**: {methodDescription}\n{actionsStr}")
-
- return "\n\n".join(methodsList)
-
-# Reusable prompt element functions
-def getAvailableDocuments(context: Any) -> str:
- """Get available documents for prompt context"""
- 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 getWorkflowHistory(services, context: Any) -> str:
- """Get workflow history for prompt context"""
- 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 getAvailableMethods(services) -> str:
- """Get available methods for prompt context"""
- 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 getEnhancedDocumentContext(services) -> str:
- """Get enhanced document context with full metadata"""
- 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"
-
-def getConnectionReferenceList(services) -> List[str]:
- """Get list of available connections"""
- try:
- # Get connections from the database
- if hasattr(services, 'interfaceDbApp') and hasattr(services, 'user'):
- userId = services.user.id
- connections = services.interfaceDbApp.getUserConnections(userId)
- if connections:
- # Format connections as reference strings
- connectionRefs = []
- for conn in connections:
- # Create reference string in format: conn_{authority}_{id}
- ref = f"conn_{conn.authority.value}_{conn.id}"
- connectionRefs.append(ref)
- return connectionRefs
-
- return []
- except Exception as e:
- logger.error(f"Error getting connection reference list: {str(e)}")
- return []
-
-def getUserLanguage(services) -> str:
- """Get user language from services"""
- 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 getReviewContent(context: Any) -> str:
- """Get review content for prompt context"""
- 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 getPreviousRoundContext(services, context: Any) -> str:
- """Get previous round context for prompt"""
- try:
- if not context or not hasattr(context, 'workflow_id'):
- return "No previous round context available"
-
- workflowId = context.workflow_id
- if not workflowId:
- return "No previous round context available"
-
- # Get previous round results
- previousResults = getattr(context, 'previous_results', [])
- if not previousResults:
- return "No previous round context available"
-
- contextList = []
- for i, result in enumerate(previousResults, 1):
- if hasattr(result, 'success') and hasattr(result, 'resultLabel'):
- status = "Success" if result.success else "Failed"
- contextList.append(f"{i}. {result.resultLabel} - {status}")
- elif isinstance(result, dict):
- status = "Success" if result.get('success', False) else "Failed"
- label = result.get('resultLabel', 'Unknown')
- contextList.append(f"{i}. {label} - {status}")
- else:
- contextList.append(f"{i}. {str(result)}")
-
- return "\n".join(contextList) if contextList else "No previous round context available"
- except Exception as e:
- logger.error(f"Error getting previous round context: {str(e)}")
- return "Error retrieving previous round context"
diff --git a/modules/workflows/processing/shared/promptFactoryPlaceholders.py b/modules/workflows/processing/shared/promptFactoryPlaceholders.py
deleted file mode 100644
index c33e3a42..00000000
--- a/modules/workflows/processing/shared/promptFactoryPlaceholders.py
+++ /dev/null
@@ -1,673 +0,0 @@
-"""
-Placeholder-based prompt factory for dynamic AI calls.
-This module provides prompt templates with placeholders that can be filled dynamically.
-"""
-
-import json
-import logging
-from typing import Dict, Any
-
-logger = logging.getLogger(__name__)
-from modules.workflows.processing.shared.promptFactory import (
- getAvailableDocuments,
- getPreviousRoundContext,
- getMethodsList,
- getEnhancedDocumentContext,
- getConnectionReferenceList,
- methods,
- discoverMethods
-)
-
-
-def createTaskPlanningPromptTemplate() -> str:
- """Create task planning prompt template with placeholders."""
- return """# Task Planning
-
-Break down user requests into logical, executable task steps.
-
-## š Context
-
-### User Request
-{{KEY:USER_PROMPT}}
-
-### Available Documents
-{{KEY:AVAILABLE_DOCUMENTS}}
-
-### Previous Workflow Rounds
-{{KEY:WORKFLOW_HISTORY}}
-
-## š Task Planning Rules
-
-### Strategic Task Grouping
-- **GROUP RELATED ACTIONS** - Combine all actions for the same business topic into ONE task
-- **ONE TOPIC PER TASK** - Each task should handle one complete business objective
-- **HIGH-LEVEL FOCUS** - Plan strategic outcomes, not implementation steps
-- **AVOID MICRO-TASKS** - Don't create separate tasks for each small action
-
-### Task Grouping Examples
-- **Research + Analysis + Report** ā ONE task: "Web research report"
-- **Data Collection + Processing + Visualization** ā ONE task: "Collect and present data"
-- **Different topics** (email + flowers) ā SEPARATE tasks: "Send formal email..." + "Order flowers from Fleurop for delivery to 123 Main St, include card message"
-
-### Retry Handling
-- **If retry request**: Analyze previous rounds to understand what failed
-- **Learn from mistakes**: Improve the plan based on previous failures
-
-## š Required JSON Structure
-
-```json
-{
- "overview": "Brief description of the overall plan",
- "languageUserDetected": "en",
- "userMessage": "User-friendly message explaining the task plan",
- "tasks": [
- {
- "id": "task_1",
- "objective": "Clear business objective focusing on what to deliver",
- "dependencies": ["task_0"],
- "success_criteria": ["measurable criteria 1", "measurable criteria 2"],
- "estimated_complexity": "low|medium|high",
- "userMessage": "What this task will accomplish"
- }
- ]
-}
-```
-
-## šÆ Task Structure Guidelines
-
-### Task ID Format
-- Use sequential numbering: `task_1`, `task_2`, `task_3`
-- Keep IDs simple and clear
-
-### Objective Writing
-- **Be VERY SPECIFIC** - Include exact details needed for action planning
-- **Include all requirements** - recipient, attachments, format, recipients, etc.
-- **State the complete deliverable** - What exactly will be produced
-- **Include context and constraints** - When, where, how, with what
-- **Make it actionable** - Clear enough to plan specific actions
-
-### Specific Objective Examples
-- **Good**: "Send formal email to ceo and board of directors with annual report as attachment"
-- **Bad**: "Handle email communication"
-- **Good**: "Order flowers from Fleurop for delivery to 123 Main St, include card message 'Happy Birthday', deliver on March 15th"
-- **Bad**: "Order flowers"
-
-### Action Planning Requirements
-- **Include all necessary details** - The objective must contain everything needed to plan actions
-- **Specify recipients and destinations** - Who should receive what
-- **Include file names and formats** - What documents to use/create
-- **State timing and deadlines** - When things need to be done
-- **Include context and constraints** - Any special requirements or limitations
-
-### Success Criteria
-- **Make them measurable** - specific, quantifiable outcomes
-- **Focus on deliverables** - what the user will receive
-- **Keep criteria realistic** - achievable within the task scope
-- **Include all related actions** - success means completing the entire business objective
-- **Be specific about requirements** - Include exact details like recipients, formats, deadlines
-- **State clear completion criteria** - How to know the task is fully done
-
-### Complexity Estimation
-- **Low**: Simple, single-action tasks (1-2 actions)
-- **Medium**: Multi-action tasks for one topic (3-5 actions)
-- **High**: Complex strategic tasks (6+ actions)
-
-## š Response Format
-Return ONLY the JSON object."""
-
-
-def createActionDefinitionPromptTemplate() -> str:
- """Create action definition prompt template with placeholders."""
- return """# Action Definition
-
-Generate the next action to advance toward completing the task objective.
-
-## š Context
-
-### Task Objective
-{{KEY:USER_PROMPT}}
-
-### Available Documents
-{{KEY:AVAILABLE_DOCUMENTS}}
-
-### Workflow History
-{{KEY:WORKFLOW_HISTORY}}
-
-### Available Methods
-{{KEY:AVAILABLE_METHODS}}
-
-### Available Connections
-{{KEY:AVAILABLE_CONNECTIONS}}
-
-### User Language
-{{KEY:USER_LANGUAGE}}
-
-## ā ļø RULES
-
-### Action Names
-- **Use EXACT compound action names** from AVAILABLE_METHODS (e.g., "ai.process", "document.extract", "web.search")
-- **DO NOT create** new action names - only use those listed in AVAILABLE_METHODS
-- **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
-- **Include user language** if relevant
-- **Avoid unnecessary fields** - host applies defaults
-
-## š Required JSON Structure
-
-```json
-{
- "actions": [
- {
- "action": "method.action_name",
- "parameters": {},
- "resultLabel": "round{current_round}_task{current_task}_action{action_number}_{descriptive_label}",
- "description": "What this action accomplishes",
- "userMessage": "User-friendly message in {{KEY:USER_LANGUAGE}}"
- }
- ]
-}
-```
-
-## ā
Correct Example
-
-```json
-{
- "actions": [
- {
- "action": "document.extract",
- "parameters": {"documentList": ["docList:msg_123:results"]},
- "resultLabel": "round1_task1_action1_extract_results",
- "description": "Extract data from documents",
- "userMessage": "Extracting data from documents"
- }
- ]
-}
-```
-
-
-## šÆ Action Planning Guidelines
-
-### Method Selection
-- **Choose appropriate method** based on task requirements
-- **Consider available resources** (documents, connections)
-- **Match method capabilities** to task objectives
-
-### Parameter Design
-- **Use ACTION SIGNATURE** to understand required parameters
-- **Convert objective** into appropriate parameter values
-- **Include all required parameters** for the action
-
-### Result Labeling
-- **Use descriptive labels** that explain what the action produces
-- **Follow naming convention**: `round{round}_task{task}_action{action}_{label}`
-- **Make labels meaningful** for future reference
-
-### User Messages
-- **Write in user language** ({{KEY:USER_LANGUAGE}})
-- **Explain what's happening** in user-friendly terms
-- **Keep messages concise** but informative
-
-## š Response Format
-Return ONLY the JSON object."""
-
-
-def createActionSelectionPromptTemplate() -> str:
- """Create action selection prompt template with placeholders."""
- return """# Action Selection
-
-Select exactly one action to advance the task.
-
-## š Context
-
-### Objective
-{{KEY:USER_PROMPT}}
-
-### Available Documents
-{{KEY:AVAILABLE_DOCUMENTS}}
-
-### User Language
-{{KEY:USER_LANGUAGE}}
-
-### Available Methods
-{{KEY:AVAILABLE_METHODS}}
-
-## ā ļø CRITICAL RULES
-
-### Selection Requirements
-- **Return ONLY the compound action name**
-- **Do NOT include parameters or prompts**
-- **Use EXACT compound action names** from AVAILABLE_METHODS above
-- **DO NOT create** new action names
-
-### Action Format
-- **Compound action names**: Use exact names from AVAILABLE_METHODS (e.g., "ai.process", "document.extract", "web.search")
-- **Single field format**: Use the full compound action name as a single string
-
-## š Required JSON Format
-
-```json
-{"action":"method.action_name"}
-```
-
-## ā
Correct Examples
-
-```json
-{"action":"ai.process"}
-{"action":"document.extract"}
-{"action":"web.search"}
-```
-
-
-## šÆ Selection Guidelines
-
-### Choose Appropriate Action
-- **Match action to objective** - select the most relevant action
-- **Consider available resources** - ensure required documents/connections are available
-- **Think about the next step** - what action will advance the task
-
-### Method Selection
-- **AI methods**: For processing, analysis, or generation tasks
-- **Document methods**: For document operations (extract, generate, etc.)
-- **Web methods**: For web searches or external data retrieval
-- **Other methods**: Based on specific requirements
-
-## š Response Format
-Return ONLY the JSON object."""
-
-
-def createActionParameterPromptTemplate() -> str:
- """Create action parameter prompt template with placeholders."""
- return """# Action Parameter Generation
-
-You are an AI assistant tasked with generating parameters for a selected action.
-
-## šÆ Your Goal
-Provide the EXACT parameters required by the ACTION SIGNATURE, using information from the OBJECTIVE, AVAILABLE DOCUMENTS, and AVAILABLE CONNECTIONS.
-
-## ā ļø CRITICAL RULES
-- **MUST respond with a JSON object**
-- **All parameters MUST be wrapped in a "parameters" object**
-- **ONLY include parameters listed in the ACTION SIGNATURE**
-- **Do NOT use code blocks or markdown in your response**
-- **Return ONLY the JSON object**
-
-## š Document & Connection References
-- **Document references**: Copy the EXACT reference string from AVAILABLE DOCUMENTS (e.g., `docList:msg_UUID:label`)
-- **Connection references**: Copy the EXACT reference string from AVAILABLE CONNECTIONS (e.g., `connection:msft:user@domain.com:uuid [status:active, token:valid]`)
-- **Do NOT invent, shorten, or modify any references**
-- **If unsure**: Use "UNCLEAR_REFERENCE" or "UNCLEAR_OBJECTIVE" and explain in a comment
-
-## š Input Context
-
-### Selected Action
-{{KEY:SELECTED_ACTION}}
-
-### Objective
-{{KEY:USER_PROMPT}}
-
-### Available Documents
-{{KEY:AVAILABLE_DOCUMENTS}}
-
-### Available Connections
-{{KEY:AVAILABLE_CONNECTIONS}}
-
-### User Language
-{{KEY:USER_LANGUAGE}}
-
-### Action Requirements
-{{KEY:ACTION_SIGNATURE}}
-
-## š Reference Types
-
-### Document References
-- **docItem**: Reference to a single document (e.g., "docItem:uuid:filename.pdf")
-- **docList**: Reference to a group of documents (e.g., "docList:msg_123:AnalysisResults")
-- **Use EXACT reference strings** shown in AVAILABLE_DOCUMENTS
-
-### Connection References
-- **Use exact connection references** from AVAILABLE CONNECTIONS
-- **Examples**: "connection:msft:user@domain.com:uuid [status:active, token:valid]", "connection:sp:user@domain.com:uuid [status:active, token:valid]"
-
-## š” Basic Examples
-
-```json
-{"parameters":{"aiPrompt": "Summarize the document"}}
-{"parameters":{"documentList": ["docList:msg_UUID:label"]}}
-{"parameters":{"connectionReference": "connection:msft:user@domain.com:uuid [status:active, token:valid]"}}
-```
-
-## ā Wrong Format (DO NOT USE)
-
-```json
-{"aiPrompt": "Your prompt here"}
-```
-
-```json
-{"parameters":{"aiPrompt": "Your prompt here"}}
-```
-
-## šÆ Parameter Guidelines
-
-### Required Parameters
-- **Use ACTION SIGNATURE** to understand what parameters are required
-- **Convert objective** into appropriate parameter values
-- **Include user language** if relevant
-- **Avoid unnecessary fields** - host applies defaults
-
-### Document Reference Rules
-- **ONLY use exact document reference strings** from AVAILABLE_DOCUMENTS
-- **DO NOT add file paths** or individual filenames to document references
-- **For documentList parameters**: Use the EXACT reference strings shown in AVAILABLE_DOCUMENTS
-
-### Connection Reference Rules
-- **ONLY use exact connection references** from AVAILABLE CONNECTIONS
-- **For connectionReference parameters**: Use the exact connection reference from AVAILABLE CONNECTIONS
-
-## š Response Format
-Return your JSON response immediately after this prompt."""
-
-
-def createRefinementPromptTemplate() -> str:
- """Create refinement prompt template with placeholders."""
- return """# Workflow Refinement Decision
-
-Decide the next step based on the observation.
-
-## š Context
-
-### Objective
-{{KEY:USER_PROMPT}}
-
-### Observation
-{{KEY:REVIEW_CONTENT}}
-
-## ā ļø CRITICAL RULES
-
-### Data Requirements
-- **If user wants DATA** (numbers, lists, calculations): Ensure AI delivers the actual data, not code
-- **If user wants DOCUMENTS** (Word, PDF, Excel): Ensure appropriate method is used to create the document
-- **If user wants ANALYSIS**: Ensure AI analyzes and delivers insights
-- **NEVER accept code when user wants data** - demand the actual data
-- **NEVER accept algorithms when user wants results** - demand the actual results
-
-## š¤ Decision Rules
-
-### Continue Conditions
-- The objective is **NOT fulfilled** (user didn't get what they asked for)
-- More data or processing is needed
-- The current result is incomplete
-
-### Stop Conditions
-- The objective is **fulfilled** (user got what they asked for)
-- All required data has been delivered
-- The task is complete
-
-### Focus
-- Focus on what the user actually wants, not what was delivered
-- Consider the user's original request carefully
-
-## š Response Format
-
-```json
-{"decision":"continue","reason":"Need more data"}
-```
-
-### Decision Options
-- `"continue"` - Keep working on the objective
-- `"stop"` - Objective has been fulfilled
-
-### Reason Examples
-- `"Need more data"`
-- `"Objective fulfilled"`
-- `"User got the requested document"`
-- `"Analysis complete"`
-
-## šÆ Decision Guidelines
-
-### When to Continue
-- **Incomplete results** - User didn't get what they asked for
-- **Missing data** - Need to gather more information
-- **Partial success** - Some but not all requirements met
-- **Technical issues** - Action failed and needs retry
-
-### When to Stop
-- **Complete success** - User got exactly what they asked for
-- **All criteria met** - Success criteria have been achieved
-- **Document created** - Required document has been generated
-- **Data delivered** - All requested data has been provided
-
-### Quality Assessment
-- **Check completeness** - Is the result complete?
-- **Verify accuracy** - Is the data correct?
-- **Assess usefulness** - Does it meet the user's needs?
-- **Consider format** - Is it in the requested format?
-
-## š Response Format
-Return your JSON response immediately after this prompt."""
-
-
-def createResultReviewPromptTemplate() -> str:
- """Create result review prompt template with placeholders."""
- return """# Result Review & Validation
-
-Review task execution outcomes and determine success, retry needs, or failure.
-
-## š Context
-
-### Task Objective
-{{KEY:USER_PROMPT}}
-
-### Execution Results
-{{KEY:REVIEW_CONTENT}}
-
-## š Validation Criteria
-
-### Action Assessment
-- **Review each action's success/failure status**
-- **Check if required documents were produced**
-- **Validate document quality and completeness**
-- **Assess if success criteria were met**
-- **Identify any missing or incomplete outputs**
-
-### Decision Making
-- **Determine if retry would help** or if task should be marked as failed
-- **Consider business value** and user satisfaction
-- **Evaluate technical execution** and results quality
-
-## š Required JSON Structure
-
-```json
-{
- "status": "success|retry|failed",
- "reason": "Detailed explanation of the validation decision",
- "improvements": ["specific improvement 1", "specific improvement 2"],
- "quality_score": 8,
- "met_criteria": ["criteria1", "criteria2"],
- "unmet_criteria": ["criteria3", "criteria4"],
- "confidence": 0.85,
- "userMessage": "User-friendly message explaining the validation result"
-}
-```
-
-## šÆ Validation Principles
-
-### Assessment Approach
-- **Be thorough but fair** in assessment
-- **Focus on business value** and outcomes
-- **Consider both technical execution** and business results
-- **Provide specific, actionable** improvement suggestions
-
-### Quality Scoring
-- **Use quality scores** to track progress across retries
-- **Scale 1-10**: 1 = Poor, 5 = Average, 10 = Excellent
-- **Consider completeness, accuracy, and usefulness**
-
-### Criteria Evaluation
-- **Clearly identify** which success criteria were met vs. unmet
-- **List specific criteria** that were achieved
-- **Note missing requirements** that need attention
-
-### Confidence Levels
-- **Set appropriate confidence levels** based on evidence quality
-- **Scale 0.0-1.0**: 0.0 = No confidence, 1.0 = Complete confidence
-- **Consider data quality** and result reliability
-
-## š Status Definitions
-
-### Success
-- **All objectives met** - User got what they asked for
-- **Quality standards met** - Results are complete and accurate
-- **No retry needed** - Task is fully complete
-
-### Retry
-- **Partial success** - Some but not all objectives met
-- **Improvement possible** - Retry could lead to better results
-- **Technical issues** - Action failures that can be resolved
-
-### Failed
-- **No progress made** - Objectives not achieved
-- **Technical limitations** - Cannot be resolved with retry
-- **Resource constraints** - Missing required inputs
-
-## š” Improvement Suggestions
-
-### Actionable Improvements
-- **Be specific** - Don't just say "improve quality"
-- **Focus on process** - How to do better next time
-- **Consider resources** - What additional inputs might help
-- **Technical fixes** - Address specific technical issues
-
-### Examples
-- "Use more specific document references from AVAILABLE_DOCUMENTS"
-- "Include user language parameter for better localization"
-- "Break down complex objective into smaller, focused actions"
-- "Verify document references before processing"
-
-## š Response Format
-Return ONLY the JSON object. Do not include any explanatory text."""
-
-
-# Helper functions to extract content for placeholders
-
-def extractUserPrompt(context) -> str:
- """Extract user prompt from context."""
- 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) -> str:
- """Extract available documents from context."""
- if hasattr(context, 'available_documents') and context.available_documents:
- return context.available_documents
- return "No documents available"
-
-
-def extractWorkflowHistory(service, context) -> str:
- """Extract workflow history from context."""
- 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) -> str:
- """Extract available methods for action planning using compound action names."""
- try:
- # Get the methods dictionary directly from the global methods variable
- if not methods:
- discoverMethods(service)
-
- # Create a flat JSON format with compound action names for better AI parsing
- available_actions_json = {}
- for methodName, methodInfo in methods.items():
- # Convert MethodAi -> ai, MethodDocument -> document, etc.
- shortName = methodName.replace('Method', '').lower()
-
- for actionName, actionInfo in methodInfo['actions'].items():
- # Create compound action name: method.action
- compoundActionName = f"{shortName}.{actionName}"
- # Get the action description
- action_description = actionInfo.get('description', f"Execute {actionName} action")
- available_actions_json[compoundActionName] = action_description
-
- return json.dumps(available_actions_json, indent=2, ensure_ascii=False)
- except Exception as e:
- logger.error(f"Error extracting available methods: {str(e)}")
- return json.dumps({}, indent=2, ensure_ascii=False)
-
-
-def extractUserLanguage(service) -> str:
- """Extract user language from service."""
- return service.user.language if service and service.user else 'en'
-
-
-def extractReviewContent(context) -> str:
- """Extract review content from context with full document metadata."""
- if hasattr(context, 'action_results') and context.action_results:
- # Build result summary
- result_summary = ""
- for i, result in enumerate(context.action_results):
- result_summary += f"\nRESULT {i+1}:\n"
- result_summary += f" Success: {result.success}\n"
- if result.error:
- result_summary += f" Error: {result.error}\n"
-
- if result.documents:
- result_summary += f" Documents: {len(result.documents)} document(s)\n"
- for doc in result.documents:
- # Extract all available metadata without content
- doc_metadata = {
- "name": getattr(doc, 'documentName', 'Unknown'),
- "mimeType": getattr(doc, 'mimeType', 'Unknown'),
- "size": getattr(doc, 'size', 'Unknown'),
- "created": getattr(doc, 'created', 'Unknown'),
- "modified": getattr(doc, 'modified', 'Unknown'),
- "typeGroup": getattr(doc, 'typeGroup', 'Unknown'),
- "documentId": getattr(doc, 'documentId', 'Unknown'),
- "reference": getattr(doc, 'reference', 'Unknown')
- }
- # Remove 'Unknown' values to keep it clean
- doc_metadata = {k: v for k, v in doc_metadata.items() if v != 'Unknown'}
- result_summary += f" - {json.dumps(doc_metadata, indent=6, ensure_ascii=False)}\n"
- else:
- result_summary += f" Documents: None\n"
-
- return result_summary
- elif hasattr(context, 'observation') and context.observation:
- # For observation data, show full content but handle documents specially
- if isinstance(context.observation, dict):
- # Create a copy to modify
- obs_copy = context.observation.copy()
-
- # If there are previews with documents, show only metadata
- if 'previews' in obs_copy and isinstance(obs_copy['previews'], list):
- for preview in obs_copy['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_copy, indent=2, ensure_ascii=False)
- else:
- return json.dumps(context.observation, ensure_ascii=False)
- elif hasattr(context, 'step_result') and context.step_result and 'observation' in context.step_result:
- # For observation data in step_result, show full content but handle documents specially
- observation = context.step_result['observation']
- if isinstance(observation, dict):
- # Create a copy to modify
- obs_copy = observation.copy()
-
- # If there are previews with documents, show only metadata
- if 'previews' in obs_copy and isinstance(obs_copy['previews'], list):
- for preview in obs_copy['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_copy, indent=2, ensure_ascii=False)
- else:
- return json.dumps(observation, ensure_ascii=False)
- else:
- return "No review content available"
diff --git a/modules/workflows/processing/shared/promptGenerationActionsActionplan.py b/modules/workflows/processing/shared/promptGenerationActionsActionplan.py
new file mode 100644
index 00000000..fc07edee
--- /dev/null
+++ b/modules/workflows/processing/shared/promptGenerationActionsActionplan.py
@@ -0,0 +1,208 @@
+"""
+Actionplan Mode Prompt Generation
+Handles prompt templates and extraction functions for actionplan mode action handling.
+"""
+
+import json
+import logging
+from typing import Dict, Any
+
+logger = logging.getLogger(__name__)
+
+def createActionDefinitionPromptTemplate() -> str:
+ """Create action definition prompt template with placeholders."""
+ return """# Action Definition
+
+ Generate the next action to advance toward completing the task objective.
+
+ ## š Context
+
+ ### Task Objective
+ {{KEY:USER_PROMPT}}
+
+ ### Available Documents
+ {{KEY:AVAILABLE_DOCUMENTS}}
+
+ ### Workflow History
+ {{KEY:WORKFLOW_HISTORY}}
+
+ ### Available Methods
+ {{KEY:AVAILABLE_METHODS}}
+
+ ### Available Connections
+ {{KEY:AVAILABLE_CONNECTIONS}}
+
+ ### User Language
+ {{KEY:USER_LANGUAGE}}
+
+ ## ā ļø RULES
+
+ ### Action Names
+ - **Use EXACT compound action names** from AVAILABLE_METHODS (e.g., "ai.process", "document.extract", "web.search")
+ - **DO NOT create** new action names - only use those listed in AVAILABLE_METHODS
+ - **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
+ - **Include user language** if relevant
+ - **Avoid unnecessary fields** - host applies defaults
+
+ ## š Required JSON Structure
+
+ ```json
+ {
+ "actions": [
+ {
+ "action": "method.action_name",
+ "parameters": {},
+ "resultLabel": "round{current_round}_task{current_task}_action{action_number}_{descriptive_label}",
+ "description": "What this action accomplishes",
+ "userMessage": "User-friendly message in {{KEY:USER_LANGUAGE}}"
+ }
+ ]
+ }
+ ```
+
+ ## ā
Correct Example
+
+ ```json
+ {
+ "actions": [
+ {
+ "action": "document.extract",
+ "parameters": {"documentList": ["docList:msg_123:results"]},
+ "resultLabel": "round1_task1_action1_extract_results",
+ "description": "Extract data from documents",
+ "userMessage": "Extracting data from documents"
+ }
+ ]
+ }
+ ```
+
+
+ ## šÆ Action Planning Guidelines
+
+ ### Method Selection
+ - **Choose appropriate method** based on task requirements
+ - **Consider available resources** (documents, connections)
+ - **Match method capabilities** to task objectives
+
+ ### Parameter Design
+ - **Use ACTION SIGNATURE** to understand required parameters
+ - **Convert objective** into appropriate parameter values
+ - **Include all required parameters** for the action
+
+ ### Result Labeling
+ - **Use descriptive labels** that explain what the action produces
+ - **Follow naming convention**: `round{round}_task{task}_action{action}_{label}`
+ - **Make labels meaningful** for future reference
+
+ ### User Messages
+ - **Write in user language** ({{KEY:USER_LANGUAGE}})
+ - **Explain what's happening** in user-friendly terms
+ - **Keep messages concise** but informative
+
+ ## š Response Format
+ Return ONLY the JSON object."""
+
+def createResultReviewPromptTemplate() -> str:
+ """Create result review prompt template with placeholders."""
+ return """# Result Review & Validation
+
+ Review task execution outcomes and determine success, retry needs, or failure.
+
+ ## š Context
+
+ ### Task Objective
+ {{KEY:USER_PROMPT}}
+
+ ### Execution Results
+ {{KEY:REVIEW_CONTENT}}
+
+ ## š Validation Criteria
+
+ ### Action Assessment
+ - **Review each action's success/failure status**
+ - **Check if required documents were produced**
+ - **Validate document quality and completeness**
+ - **Assess if success criteria were met**
+ - **Identify any missing or incomplete outputs**
+
+ ### Decision Making
+ - **Determine if retry would help** or if task should be marked as failed
+ - **Consider business value** and user satisfaction
+ - **Evaluate technical execution** and results quality
+
+ ## š Required JSON Structure
+
+ ```json
+ {
+ "status": "success|retry|failed",
+ "reason": "Detailed explanation of the validation decision",
+ "improvements": ["specific improvement 1", "specific improvement 2"],
+ "quality_score": 8,
+ "met_criteria": ["criteria1", "criteria2"],
+ "unmet_criteria": ["criteria3", "criteria4"],
+ "confidence": 0.85,
+ "userMessage": "User-friendly message explaining the validation result"
+ }
+ ```
+
+ ## šÆ Validation Principles
+
+ ### Assessment Approach
+ - **Be thorough but fair** in assessment
+ - **Focus on business value** and outcomes
+ - **Consider both technical execution** and business results
+ - **Provide specific, actionable** improvement suggestions
+
+ ### Quality Scoring
+ - **Use quality scores** to track progress across retries
+ - **Scale 1-10**: 1 = Poor, 5 = Average, 10 = Excellent
+ - **Consider completeness, accuracy, and usefulness**
+
+ ### Criteria Evaluation
+ - **Clearly identify** which success criteria were met vs. unmet
+ - **List specific criteria** that were achieved
+ - **Note missing requirements** that need attention
+
+ ### Confidence Levels
+ - **Set appropriate confidence levels** based on evidence quality
+ - **Scale 0.0-1.0**: 0.0 = No confidence, 1.0 = Complete confidence
+ - **Consider data quality** and result reliability
+
+ ## š Status Definitions
+
+ ### Success
+ - **All objectives met** - User got what they asked for
+ - **Quality standards met** - Results are complete and accurate
+ - **No retry needed** - Task is fully complete
+
+ ### Retry
+ - **Partial success** - Some but not all objectives met
+ - **Improvement possible** - Retry could lead to better results
+ - **Technical issues** - Action failures that can be resolved
+
+ ### Failed
+ - **No progress made** - Objectives not achieved
+ - **Technical limitations** - Cannot be resolved with retry
+ - **Resource constraints** - Missing required inputs
+
+ ## š” Improvement Suggestions
+
+ ### Actionable Improvements
+ - **Be specific** - Don't just say "improve quality"
+ - **Focus on process** - How to do better next time
+ - **Consider resources** - What additional inputs might help
+ - **Technical fixes** - Address specific technical issues
+
+ ### Examples
+ - "Use more specific document references from AVAILABLE_DOCUMENTS"
+ - "Include user language parameter for better localization"
+ - "Break down complex objective into smaller, focused actions"
+ - "Verify document references before processing"
+
+ ## š Response Format
+ Return ONLY the JSON object. Do not include any explanatory text."""
+
diff --git a/modules/workflows/processing/shared/promptGenerationActionsReact.py b/modules/workflows/processing/shared/promptGenerationActionsReact.py
new file mode 100644
index 00000000..e4f65b5e
--- /dev/null
+++ b/modules/workflows/processing/shared/promptGenerationActionsReact.py
@@ -0,0 +1,108 @@
+"""
+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.
+
+ OBJECTIVE:
+ {{KEY:USER_PROMPT}}
+
+ AVAILABLE_DOCUMENTS:
+ {{KEY:AVAILABLE_DOCUMENTS}}
+
+ AVAILABLE_METHODS:
+ {{KEY:AVAILABLE_METHODS}}
+
+ REPLY: Return only a JSON object with the selected action:
+ {{
+ "action": "method.action_name"
+ }}
+
+ RULES:
+ 1. Use EXACT action names from AVAILABLE_METHODS
+ 2. Return ONLY JSON - no other text
+ 3. Do NOT use markdown code blocks
+ 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.
+
+ ACTION_OBJECTIVE (the objective for this action to fulfill):
+ {{KEY:ACTION_OBJECTIVE}}
+
+ ACTION_SIGNATURE (the signature of the action to generate parameters for):
+ {{KEY:ACTION_SIGNATURE}}
+
+ AVAILABLE_DOCUMENTS:
+ {{KEY:AVAILABLE_DOCUMENTS}}
+
+ AVAILABLE_CONNECTIONS:
+ {{KEY:AVAILABLE_CONNECTIONS}}
+
+ USER_REQUEST (final user prompt to deliver):
+ {{KEY:USER_PROMPT}}
+
+ USER_LANGUAGE:
+ {{KEY:USER_LANGUAGE}}
+
+ PREVIOUS_ACTION_RESULTS:
+ {{KEY:PREVIOUS_ACTION_RESULTS}}
+
+ LEARNINGS_AND_IMPROVEMENTS:
+ {{KEY:LEARNINGS_AND_IMPROVEMENTS}}
+
+ LATEST_REFINEMENT_FEEDBACK:
+ {{KEY:LATEST_REFINEMENT_FEEDBACK}}
+
+ SELECTED_ACTION:
+ {{KEY:SELECTED_ACTION}}
+
+ REPLY: Return only a JSON object with the parameters according to the ACTION_SIGNATURE without any comments in the structure below:
+ {{
+ "parameters": {{
+ "parameter": "value",
+ }},
+ "signature": [List of all signatures, you see in the ACTION_SIGNATURE]
+ }}
+
+ 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
+ 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
+ 7. Generate parameters that align with the USER_LANGUAGE when applicable
+ 8. Return ONLY JSON - no other text
+ 9. Do NOT use markdown code blocks
+ 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.
+
+ OBJECTIVE:
+ {{KEY:USER_PROMPT}}
+
+ OBSERVATION:
+ {{KEY:REVIEW_CONTENT}}
+
+ REPLY: Return only a JSON object with your decision:
+ {{
+ "decision": "continue|stop",
+ "reason": "brief explanation"
+ }}
+
+ RULES:
+ 1. Use "continue" if objective NOT fulfilled
+ 2. Use "stop" if objective fulfilled
+ 3. Return ONLY JSON - no other text
+ 4. Do NOT use markdown code blocks
+ 5. Do NOT add explanations
+ """
diff --git a/modules/workflows/processing/shared/promptGenerationTaskplan.py b/modules/workflows/processing/shared/promptGenerationTaskplan.py
new file mode 100644
index 00000000..80481a74
--- /dev/null
+++ b/modules/workflows/processing/shared/promptGenerationTaskplan.py
@@ -0,0 +1,107 @@
+"""
+Task Planning Prompt Generation
+Handles prompt templates and extraction functions for task planning phase.
+"""
+
+import json
+import logging
+from typing import Dict, Any
+
+logger = logging.getLogger(__name__)
+
+
+def createTaskPlanningPromptTemplate() -> str:
+ """Create task planning prompt template with placeholders."""
+ return """# Task Planning
+
+ Break down user requests into logical, executable task steps.
+
+ ## š Context
+
+ ### User Request
+ {{KEY:USER_PROMPT}}
+
+ ### Available Documents
+ {{KEY:AVAILABLE_DOCUMENTS}}
+
+ ### Previous Workflow Rounds
+ {{KEY:WORKFLOW_HISTORY}}
+
+ ## š Task Planning Rules
+
+ ### Strategic Task Grouping
+ - **GROUP RELATED ACTIONS** - Combine all actions for the same business topic into ONE task
+ - **ONE TOPIC PER TASK** - Each task should handle one complete business objective
+ - **HIGH-LEVEL FOCUS** - Plan strategic outcomes, not implementation steps
+ - **AVOID MICRO-TASKS** - Don't create separate tasks for each small action
+
+ ### Task Grouping Examples
+ - **Research + Analysis + Report** ā ONE task: "Web research report"
+ - **Data Collection + Processing + Visualization** ā ONE task: "Collect and present data"
+ - **Different topics** (email + flowers) ā SEPARATE tasks: "Send formal email..." + "Order flowers from Fleurop for delivery to 123 Main St, include card message"
+
+ ### Retry Handling
+ - **If retry request**: Analyze previous rounds to understand what failed
+ - **Learn from mistakes**: Improve the plan based on previous failures
+
+ ## š Required JSON Structure
+
+ ```json
+ {
+ "overview": "Brief description of the overall plan",
+ "languageUserDetected": "en",
+ "userMessage": "User-friendly message explaining the task plan",
+ "tasks": [
+ {
+ "id": "task_1",
+ "objective": "Clear business objective focusing on what to deliver",
+ "dependencies": ["task_0"],
+ "success_criteria": ["measurable criteria 1", "measurable criteria 2"],
+ "estimated_complexity": "low|medium|high",
+ "userMessage": "What this task will accomplish"
+ }
+ ]
+ }
+ ```
+
+ ## šÆ Task Structure Guidelines
+
+ ### Task ID Format
+ - Use sequential numbering: `task_1`, `task_2`, `task_3`
+ - Keep IDs simple and clear
+
+ ### Objective Writing
+ - **Be VERY SPECIFIC** - Include exact details needed for action planning
+ - **Include all requirements** - recipient, attachments, format, recipients, etc.
+ - **State the complete deliverable** - What exactly will be produced
+ - **Include context and constraints** - When, where, how, with what
+ - **Make it actionable** - Clear enough to plan specific actions
+
+ ### Specific Objective Examples
+ - **Good**: "Send formal email to ceo and board of directors with annual report as attachment"
+ - **Bad**: "Handle email communication"
+ - **Good**: "Order flowers from Fleurop for delivery to 123 Main St, include card message 'Happy Birthday', deliver on March 15th"
+ - **Bad**: "Order flowers"
+
+ ### Action Planning Requirements
+ - **Include all necessary details** - The objective must contain everything needed to plan actions
+ - **Specify recipients and destinations** - Who should receive what
+ - **Include file names and formats** - What documents to use/create
+ - **State timing and deadlines** - When things need to be done
+ - **Include context and constraints** - Any special requirements or limitations
+
+ ### Success Criteria
+ - **Make them measurable** - specific, quantifiable outcomes
+ - **Focus on deliverables** - what the user will receive
+ - **Keep criteria realistic** - achievable within the task scope
+ - **Include all related actions** - success means completing the entire business objective
+ - **Be specific about requirements** - Include exact details like recipients, formats, deadlines
+ - **State clear completion criteria** - How to know the task is fully done
+
+ ### Complexity Estimation
+ - **Low**: Simple, single-action tasks (1-2 actions)
+ - **Medium**: Multi-action tasks for one topic (3-5 actions)
+ - **High**: Complex strategic tasks (6+ actions)
+
+ ## š Response Format
+ Return ONLY the JSON object."""
diff --git a/modules/workflows/processing/shared/reactPromptTemplates.py b/modules/workflows/processing/shared/reactPromptTemplates.py
deleted file mode 100644
index bfe44d51..00000000
--- a/modules/workflows/processing/shared/reactPromptTemplates.py
+++ /dev/null
@@ -1,87 +0,0 @@
-"""
-React-specific prompt templates for dynamic AI calls.
-These templates are tailored for the React mode's iterative process.
-"""
-
-def createReactPlanSelectionPromptTemplate() -> str:
- """Create action selection prompt template for React mode with minimal placeholders."""
- return """Select one action to advance the task.
-
-OBJECTIVE:
-{{KEY:USER_PROMPT}}
-
-AVAILABLE_DOCUMENTS:
-{{KEY:AVAILABLE_DOCUMENTS}}
-
-AVAILABLE_METHODS:
-{{KEY:AVAILABLE_METHODS}}
-
-REPLY: Return only a JSON object with the selected action:
-{{
- "action": "method.action_name"
-}}
-
-RULES:
-1. Use EXACT action names from AVAILABLE_METHODS
-2. Return ONLY JSON - no other text
-3. Do NOT use markdown code blocks
-4. Do NOT add explanations
-"""
-
-
-def createReactParametersPromptTemplate() -> str:
- """Create ultra-simple action parameter prompt template for React mode."""
- return """Generate parameters for this action.
-
-ACTION_SIGNATURE:
-{{KEY:ACTION_SIGNATURE}}
-
-AVAILABLE_DOCUMENTS:
-{{KEY:AVAILABLE_DOCUMENTS}}
-
-AVAILABLE_CONNECTIONS:
-{{KEY:AVAILABLE_CONNECTIONS}}
-
-USER_REQUEST:
-{{KEY:USER_PROMPT}}
-
-REPLY: Return only a JSON object with the parameters according to the ACTION_SIGNATURE without any comments in the structure below:
-{{
- "parameters": {{
- "parameter": "value",
- }},
- "signature": [List of all signatures, you see in the ACTION_SIGNATURE]
-}}
-
-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
-4. Return ONLY JSON - no other text
-5. Do NOT use markdown code blocks
-6. 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.
-
-OBJECTIVE:
-{{KEY:USER_PROMPT}}
-
-OBSERVATION:
-{{KEY:REVIEW_CONTENT}}
-
-REPLY: Return only a JSON object with your decision:
-{{
- "decision": "continue|stop",
- "reason": "brief explanation"
-}}
-
-RULES:
-1. Use "continue" if objective NOT fulfilled
-2. Use "stop" if objective fulfilled
-3. Return ONLY JSON - no other text
-4. Do NOT use markdown code blocks
-5. Do NOT add explanations
-"""
\ No newline at end of file
diff --git a/modules/workflows/processing/workflowProcessor.py b/modules/workflows/processing/workflowProcessor.py
index 9031ac30..5041bb2f 100644
--- a/modules/workflows/processing/workflowProcessor.py
+++ b/modules/workflows/processing/workflowProcessor.py
@@ -5,9 +5,9 @@ import logging
from typing import Dict, Any, Optional, List
from modules.datamodels.datamodelWorkflow import TaskStep, TaskContext, TaskPlan, TaskResult, ReviewResult
from modules.datamodels.datamodelChat import ChatWorkflow
-from modules.workflows.processing.modes.baseMode import BaseMode
-from modules.workflows.processing.modes.actionplanMode import ActionplanMode
-from modules.workflows.processing.modes.reactMode import ReactMode
+from modules.workflows.processing.modes.modeBase import BaseMode
+from modules.workflows.processing.modes.modeActionplan import ActionplanMode
+from modules.workflows.processing.modes.modeReact import ReactMode
logger = logging.getLogger(__name__)
diff --git a/test-chat/obj/m20251005-100001_2_0_0/message.json b/test-chat/obj/m20251005-100001_2_0_0/message.json
new file mode 100644
index 00000000..ab6be3c8
--- /dev/null
+++ b/test-chat/obj/m20251005-100001_2_0_0/message.json
@@ -0,0 +1,19 @@
+{
+ "id": "msg_4fce3b47-1595-4190-a09a-7b8c2483d9dd",
+ "workflowId": "8630c862-d9f3-4332-9d6c-6664a39edd73",
+ "parentMessageId": null,
+ "message": "Sende eine formelle E-Mail an peter.muster@domain.com von meinem valueon account aus, um meinen Termin von 10 Uhr auf Freitag zu scheiben. lege diese datei im mail als anhang bei und erfasse eine zusammenfasung im mail.",
+ "role": "user",
+ "status": "first",
+ "sequenceNr": 7,
+ "publishedAt": 1759651201.5949264,
+ "roundNumber": 2,
+ "taskNumber": 0,
+ "actionNumber": 0,
+ "documentsLabel": "round2_task0_action0_context",
+ "actionId": null,
+ "actionMethod": null,
+ "actionName": null,
+ "success": null,
+ "documents": []
+}
\ No newline at end of file
diff --git a/test-chat/obj/m20251005-100001_2_0_0/message_text.txt b/test-chat/obj/m20251005-100001_2_0_0/message_text.txt
new file mode 100644
index 00000000..c3ae568d
--- /dev/null
+++ b/test-chat/obj/m20251005-100001_2_0_0/message_text.txt
@@ -0,0 +1 @@
+Sende eine formelle E-Mail an peter.muster@domain.com von meinem valueon account aus, um meinen Termin von 10 Uhr auf Freitag zu scheiben. lege diese datei im mail als anhang bei und erfasse eine zusammenfasung im mail.
\ No newline at end of file
diff --git a/test-chat/obj/m20251005-100006_2_1_0/message.json b/test-chat/obj/m20251005-100006_2_1_0/message.json
new file mode 100644
index 00000000..6ae75e90
--- /dev/null
+++ b/test-chat/obj/m20251005-100006_2_1_0/message.json
@@ -0,0 +1,19 @@
+{
+ "id": "msg_b85c3e84-c119-4634-ad19-11a735e1039d",
+ "workflowId": "8630c862-d9f3-4332-9d6c-6664a39edd73",
+ "parentMessageId": null,
+ "message": "š **Task Plan**\n\nI will help you send a formal email to reschedule your appointment, including the specified file and a summary.\n\nš¬ I will compose and send a formal email to reschedule your appointment, ensuring all required elements are included.\n\n",
+ "role": "assistant",
+ "status": "step",
+ "sequenceNr": 8,
+ "publishedAt": 1759651206.7063708,
+ "roundNumber": 2,
+ "taskNumber": 1,
+ "actionNumber": 0,
+ "documentsLabel": "task_plan",
+ "actionId": null,
+ "actionMethod": null,
+ "actionName": null,
+ "success": null,
+ "documents": []
+}
\ No newline at end of file
diff --git a/test-chat/obj/m20251005-100006_2_1_0/message_text.txt b/test-chat/obj/m20251005-100006_2_1_0/message_text.txt
new file mode 100644
index 00000000..4b48d457
--- /dev/null
+++ b/test-chat/obj/m20251005-100006_2_1_0/message_text.txt
@@ -0,0 +1,6 @@
+š **Task Plan**
+
+I will help you send a formal email to reschedule your appointment, including the specified file and a summary.
+
+š¬ I will compose and send a formal email to reschedule your appointment, ensuring all required elements are included.
+
diff --git a/test-chat/obj/m20251005-100007_2_1_0/message.json b/test-chat/obj/m20251005-100007_2_1_0/message.json
new file mode 100644
index 00000000..cdd5f5c6
--- /dev/null
+++ b/test-chat/obj/m20251005-100007_2_1_0/message.json
@@ -0,0 +1,19 @@
+{
+ "id": "msg_cb9e3372-52b5-4254-98e6-7552efb2b248",
+ "workflowId": "8630c862-d9f3-4332-9d6c-6664a39edd73",
+ "parentMessageId": null,
+ "message": "š **Task 1/1**\n\nš¬ I will compose and send a formal email to reschedule your appointment, ensuring all required elements are included.",
+ "role": "assistant",
+ "status": "step",
+ "sequenceNr": 9,
+ "publishedAt": 1759651207.017333,
+ "roundNumber": 2,
+ "taskNumber": 1,
+ "actionNumber": 0,
+ "documentsLabel": "task_1_start",
+ "actionId": null,
+ "actionMethod": null,
+ "actionName": null,
+ "success": null,
+ "documents": []
+}
\ No newline at end of file
diff --git a/test-chat/obj/m20251005-100007_2_1_0/message_text.txt b/test-chat/obj/m20251005-100007_2_1_0/message_text.txt
new file mode 100644
index 00000000..95783a76
--- /dev/null
+++ b/test-chat/obj/m20251005-100007_2_1_0/message_text.txt
@@ -0,0 +1,3 @@
+š **Task 1/1**
+
+š¬ I will compose and send a formal email to reschedule your appointment, ensuring all required elements are included.
\ No newline at end of file
diff --git a/test-chat/obj/m20251005-100020_2_1_1/message.json b/test-chat/obj/m20251005-100020_2_1_1/message.json
new file mode 100644
index 00000000..bc8042f4
--- /dev/null
+++ b/test-chat/obj/m20251005-100020_2_1_1/message.json
@@ -0,0 +1,19 @@
+{
+ "id": "msg_dafc2f81-528f-4c61-991e-53fbb863a9a8",
+ "workflowId": "8630c862-d9f3-4332-9d6c-6664a39edd73",
+ "parentMessageId": null,
+ "message": "**Action 1/1 (outlook.composeAndSendEmailWithContext)**\n\nā
Compose and send formal email from valueon account to peter.muster@domain.com to reschedule 10am appointment to Friday, including file attachment and appointment summary\n\n",
+ "role": "assistant",
+ "status": "step",
+ "sequenceNr": 10,
+ "publishedAt": 1759651220.387675,
+ "roundNumber": 2,
+ "taskNumber": 1,
+ "actionNumber": 1,
+ "documentsLabel": "round2_task1_action1_results",
+ "actionId": "action_4a3eb40f-a97d-4043-94c3-4fedfc5b1c8d",
+ "actionMethod": "outlook",
+ "actionName": "composeAndSendEmailWithContext",
+ "success": null,
+ "documents": []
+}
\ No newline at end of file
diff --git a/test-chat/obj/m20251005-100020_2_1_1/message_text.txt b/test-chat/obj/m20251005-100020_2_1_1/message_text.txt
new file mode 100644
index 00000000..13a4f901
--- /dev/null
+++ b/test-chat/obj/m20251005-100020_2_1_1/message_text.txt
@@ -0,0 +1,4 @@
+**Action 1/1 (outlook.composeAndSendEmailWithContext)**
+
+ā
Compose and send formal email from valueon account to peter.muster@domain.com to reschedule 10am appointment to Friday, including file attachment and appointment summary
+
diff --git a/test-chat/obj/m20251005-100020_2_1_1/round2_task1_action1_results/document_001_metadata.json b/test-chat/obj/m20251005-100020_2_1_1/round2_task1_action1_results/document_001_metadata.json
new file mode 100644
index 00000000..8b46896d
--- /dev/null
+++ b/test-chat/obj/m20251005-100020_2_1_1/round2_task1_action1_results/document_001_metadata.json
@@ -0,0 +1,12 @@
+{
+ "id": "94fc49c9-55e5-4b03-a437-e79c26483651",
+ "messageId": "msg_dafc2f81-528f-4c61-991e-53fbb863a9a8",
+ "fileId": "a0196528-6ba3-4bc9-abef-7ae25aad0c76",
+ "fileName": "ai_generated_email_draft_20251005-080020.json",
+ "fileSize": 1173,
+ "mimeType": "application/json",
+ "roundNumber": 2,
+ "taskNumber": 1,
+ "actionNumber": 1,
+ "actionId": "action_4a3eb40f-a97d-4043-94c3-4fedfc5b1c8d"
+}
\ No newline at end of file
diff --git a/test-chat/obj/m20251005-100021_2_0_0/message.json b/test-chat/obj/m20251005-100021_2_0_0/message.json
new file mode 100644
index 00000000..d4c5f818
--- /dev/null
+++ b/test-chat/obj/m20251005-100021_2_0_0/message.json
@@ -0,0 +1,19 @@
+{
+ "id": "msg_998654a7-b4b3-444a-9d25-ecabd0117735",
+ "workflowId": "8630c862-d9f3-4332-9d6c-6664a39edd73",
+ "parentMessageId": null,
+ "message": "Workflow completed.\n\nProcessed 2 user inputs and generated 9 responses.\nWorkflow status: running",
+ "role": "assistant",
+ "status": "last",
+ "sequenceNr": 12,
+ "publishedAt": 1759651221.848871,
+ "roundNumber": 2,
+ "taskNumber": 0,
+ "actionNumber": 0,
+ "documentsLabel": "workflow_feedback",
+ "actionId": null,
+ "actionMethod": null,
+ "actionName": null,
+ "success": null,
+ "documents": []
+}
\ No newline at end of file
diff --git a/test-chat/obj/m20251005-100021_2_0_0/message_text.txt b/test-chat/obj/m20251005-100021_2_0_0/message_text.txt
new file mode 100644
index 00000000..f57715a1
--- /dev/null
+++ b/test-chat/obj/m20251005-100021_2_0_0/message_text.txt
@@ -0,0 +1,4 @@
+Workflow completed.
+
+Processed 2 user inputs and generated 9 responses.
+Workflow status: running
\ No newline at end of file
diff --git a/test-chat/obj/m20251005-100021_2_1_0/message.json b/test-chat/obj/m20251005-100021_2_1_0/message.json
new file mode 100644
index 00000000..ef04f40e
--- /dev/null
+++ b/test-chat/obj/m20251005-100021_2_1_0/message.json
@@ -0,0 +1,19 @@
+{
+ "id": "msg_402e3653-1500-441f-adf0-d4ea90980d4e",
+ "workflowId": "8630c862-d9f3-4332-9d6c-6664a39edd73",
+ "parentMessageId": null,
+ "message": "šÆ **Task 1/1**\n\nā
Email draft successfully created with attachment and summary as requested\nš Score 8/10",
+ "role": "assistant",
+ "status": "step",
+ "sequenceNr": 11,
+ "publishedAt": 1759651221.6639369,
+ "roundNumber": 2,
+ "taskNumber": 1,
+ "actionNumber": 0,
+ "documentsLabel": "task_1_completion",
+ "actionId": null,
+ "actionMethod": null,
+ "actionName": null,
+ "success": null,
+ "documents": []
+}
\ No newline at end of file
diff --git a/test-chat/obj/m20251005-100021_2_1_0/message_text.txt b/test-chat/obj/m20251005-100021_2_1_0/message_text.txt
new file mode 100644
index 00000000..89e8e75e
--- /dev/null
+++ b/test-chat/obj/m20251005-100021_2_1_0/message_text.txt
@@ -0,0 +1,4 @@
+šÆ **Task 1/1**
+
+ā
Email draft successfully created with attachment and summary as requested
+š Score 8/10
\ No newline at end of file