gateway/modules/workflows/processing/shared/promptGenerationActionsReact.py
2025-10-07 00:17:11 +02:00

229 lines
9.5 KiB
Python

"""
React Mode Prompt Generation
Handles prompt templates for react mode action handling.
"""
from typing import Any, List
from modules.datamodels.datamodelChat import PromptBundle, PromptPlaceholder
from modules.workflows.processing.shared.placeholderFactory import (
extractUserPrompt,
extractUserLanguage,
extractAvailableMethods,
extractAvailableDocumentsSummary,
extractAvailableDocumentsIndex,
extractAvailableConnectionsIndex,
extractPreviousActionResults,
extractLearningsAndImprovements,
extractLatestRefinementFeedback,
extractWorkflowHistory,
)
from modules.workflows.processing.shared.methodDiscovery import methods, getActionParameterList
def generateReactPlanSelectionPrompt(services, context: Any) -> PromptBundle:
"""Define placeholders first, then the template; return PromptBundle."""
placeholders: List[PromptPlaceholder] = [
PromptPlaceholder(label="USER_PROMPT", content=extractUserPrompt(context), summaryAllowed=False),
PromptPlaceholder(label="AVAILABLE_DOCUMENTS_SUMMARY", content=extractAvailableDocumentsSummary(services, context), summaryAllowed=True),
PromptPlaceholder(label="AVAILABLE_METHODS", content=extractAvailableMethods(services), summaryAllowed=False),
# Provide enriched history context for Stage 1 to craft parametersContext
PromptPlaceholder(label="WORKFLOW_HISTORY", content=extractWorkflowHistory(services, context), summaryAllowed=True),
# Provide deterministic indexes so the planner can choose exact labels
PromptPlaceholder(label="AVAILABLE_DOCUMENTS_INDEX", content=extractAvailableDocumentsIndex(services, context), summaryAllowed=True),
PromptPlaceholder(label="AVAILABLE_CONNECTIONS_INDEX", content=extractAvailableConnectionsIndex(services), summaryAllowed=False),
]
template = """Select exactly one action to advance the task.
OBJECTIVE:
{{KEY:USER_PROMPT}}
AVAILABLE_DOCUMENTS_SUMMARY:
{{KEY:AVAILABLE_DOCUMENTS_SUMMARY}}
AVAILABLE_METHODS:
{{KEY:AVAILABLE_METHODS}}
WORKFLOW_HISTORY (reverse-chronological, enriched):
{{KEY:WORKFLOW_HISTORY}}
AVAILABLE_DOCUMENTS_INDEX:
{{KEY:AVAILABLE_DOCUMENTS_INDEX}}
AVAILABLE_CONNECTIONS_INDEX:
{{KEY:AVAILABLE_CONNECTIONS_INDEX}}
REPLY: Return ONLY a JSON object with the following structure (no comments, no extra text):
{{
"action": "method.action_name",
"actionObjective": "...",
"learnings": ["..."],
"requiredInputDocuments": ["docList:..."],
"requiredConnection": "connection:..." | null,
"parametersContext": "concise text that Stage 2 will use to set business parameters"
}}
EXAMPLE how to assign references from AVAILABLE_DOCUMENTS_INDEX and AVAILABLE_CONNECTIONS_INDEX:
"requiredInputDocuments": ["docList:msg_47a7a578-e8f2-4ba8-ac66-0dbff40605e0:round8_task1_action1_results","docItem:5d8b7aee-b546-4487-b6a8-835c86f7b186:AI_Generated_Document_20251006-104256.docx"],
"requiredConnection": "connection:msft:p.motsch@valueon.ch:1ae8b8e5-128b-49b8-b1cb-7c632669eeae",
RULES:
1. Use EXACT action names from AVAILABLE_METHODS
2. Do NOT output a "parameters" object
3. parametersContext must be short and sufficient for Stage 2
4. Return ONLY JSON - no markdown, no explanations
5. For requiredInputDocuments, use ONLY exact references from AVAILABLE_DOCUMENTS_INDEX (docList:... or docItem:...)
6. For requiredConnection, use ONLY an exact label from AVAILABLE_CONNECTIONS_INDEX
"""
return PromptBundle(prompt=template, placeholders=placeholders)
def generateReactParametersPrompt(services, context: Any, compoundActionName: str) -> PromptBundle:
"""Define placeholders first, then the template; return PromptBundle.
Minimal Stage 2 (no fallback): consumes actionObjective, selectedAction, parametersContext only.
Excludes documents/connections/history entirely.
"""
# derive method/action and parameter list
methodName, actionName = (compoundActionName.split('.', 1) if '.' in compoundActionName else (compoundActionName, ''))
actionParameterList = getActionParameterList(methodName, actionName, methods)
def _formatBusinessParameters(params) -> str:
excluded = {"documentList", "connectionReference"}
# Case 1: params is a list of dicts or objects with 'name'
if isinstance(params, (list, tuple)):
entries = []
for p in params:
try:
if isinstance(p, dict):
name = p.get("name")
if not name or name in excluded:
continue
ptype = p.get("type") or p.get("dataType") or ""
req = p.get("required")
reqTxt = "required" if (req is True or str(req).lower() == "true") else "optional"
desc = p.get("description") or p.get("desc") or ""
entry = f"- {name} ({ptype}, {reqTxt})" + (f": {desc}" if desc else "")
entries.append(entry)
else:
# Try attribute access
name = getattr(p, "name", None)
if not name or name in excluded:
continue
ptype = getattr(p, "type", "") or getattr(p, "dataType", "")
req = getattr(p, "required", False)
reqTxt = "required" if (req is True or str(req).lower() == "true") else "optional"
desc = getattr(p, "description", None) or getattr(p, "desc", None) or ""
entry = f"- {name} ({ptype}, {reqTxt})" + (f": {desc}" if desc else "")
entries.append(entry)
except Exception:
continue
return "\n".join(entries)
# Case 2: params is a string description: filter out lines mentioning excluded names
if isinstance(params, str):
lines = [ln for ln in params.splitlines() if not any(ex in ln for ex in excluded)]
return "\n".join(lines).strip()
# Fallback: plain string
try:
return str(params)
except Exception:
return ""
actionParametersText = _formatBusinessParameters(actionParameterList)
# determine action objective if available, else fall back to user prompt
if hasattr(context, 'action_objective') and context.action_objective:
actionObjective = context.action_objective
elif hasattr(context, 'task_step') and context.task_step and getattr(context.task_step, 'objective', None):
actionObjective = context.task_step.objective
else:
actionObjective = extractUserPrompt(context)
# Minimal Stage 2 (no fallback)
parametersContext = getattr(context, 'parameters_context', None)
learningsText = ""
try:
# If Stage 1 learnings were attached to context, pass them textually
if hasattr(context, 'learnings') and context.learnings:
if isinstance(context.learnings, (list, tuple)):
learningsText = "\n".join(f"- {str(x)}" for x in context.learnings)
else:
learningsText = str(context.learnings)
except Exception:
learningsText = ""
placeholders: List[PromptPlaceholder] = [
PromptPlaceholder(label="ACTION_OBJECTIVE", content=actionObjective, summaryAllowed=False),
PromptPlaceholder(label="SELECTED_ACTION", content=compoundActionName, summaryAllowed=False),
PromptPlaceholder(label="PARAMETERS_CONTEXT", content=(parametersContext or ""), summaryAllowed=True),
PromptPlaceholder(label="ACTION_PARAMETERS", content=actionParametersText, summaryAllowed=False),
PromptPlaceholder(label="LEARNINGS", content=learningsText, summaryAllowed=True),
]
template = """You are a parameter generator. Set the parameters for this specific action.
CONTEXT AND OBJECTIVE:
{{KEY:ACTION_OBJECTIVE}}
SELECTED_ACTION:
{{KEY:SELECTED_ACTION}}
CONTEXT FOR PARAMETER VALUES:
{{KEY:PARAMETERS_CONTEXT}}
LEARNINGS (from prior attempts, if any):
{{KEY:LEARNINGS}}
REQUIRED PARAMETERS FOR THIS ACTION (use these exact parameter names):
{{KEY:ACTION_PARAMETERS}}
INSTRUCTIONS:
- Use ONLY the parameter names listed above
- Fill in appropriate values based on the context and objective
- Do NOT invent new parameters
- Do NOT include: documentList, connectionReference, history, documents, connections
REPLY (ONLY JSON):
{{
"schema": "parameters_v1",
"parameters": {{
"paramName": "value"
}}
}}
RULES:
- Return ONLY JSON (no markdown, no prose)
- Use only the parameters listed in REQUIRED PARAMETERS FOR THIS ACTION
"""
return PromptBundle(prompt=template, placeholders=placeholders)
def generateReactRefinementPrompt(services, context: Any, reviewContent: str) -> PromptBundle:
"""Define placeholders first, then the template; return PromptBundle."""
placeholders: List[PromptPlaceholder] = [
PromptPlaceholder(label="USER_PROMPT", content=extractUserPrompt(context), summaryAllowed=False),
PromptPlaceholder(label="REVIEW_CONTENT", content=reviewContent, summaryAllowed=True),
]
template = """Decide the next step based on the observation.
OBJECTIVE:
{{KEY:USER_PROMPT}}
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
"""
return PromptBundle(prompt=template, placeholders=placeholders)