chat trace file log ready
This commit is contained in:
parent
bf1100410c
commit
0cb1e75daf
13 changed files with 526 additions and 608 deletions
|
|
@ -1053,7 +1053,7 @@ class ChatObjects:
|
||||||
def _storeDebugMessageAndDocuments(self, message: ChatMessage) -> None:
|
def _storeDebugMessageAndDocuments(self, message: ChatMessage) -> None:
|
||||||
"""
|
"""
|
||||||
Store message and documents for debugging purposes in fileshare.
|
Store message and documents for debugging purposes in fileshare.
|
||||||
Structure: gateway/test-chat/obj/m_round_task_action_timestamp/documentlist_label/documents
|
Structure: gateway/test-chat/messages/m_round_task_action_timestamp/documentlist_label/documents
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
message: ChatMessage object to store
|
message: ChatMessage object to store
|
||||||
|
|
@ -1061,21 +1061,21 @@ class ChatObjects:
|
||||||
try:
|
try:
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime, UTC
|
||||||
|
|
||||||
# Create base debug directory
|
# Create base debug directory
|
||||||
debug_root = "./test-chat/obj"
|
debug_root = "./test-chat/messages"
|
||||||
os.makedirs(debug_root, exist_ok=True)
|
os.makedirs(debug_root, exist_ok=True)
|
||||||
|
|
||||||
# Generate timestamp
|
# Generate timestamp
|
||||||
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
timestamp = datetime.now(UTC).strftime('%Y%m%d-%H%M%S-%f')[:-3]
|
||||||
|
|
||||||
# Create message folder name: m_round_task_action_timestamp
|
# Create message folder name: m_round_task_action_timestamp
|
||||||
# Use actual values from message, not defaults
|
# Use actual values from message, not defaults
|
||||||
round_str = str(message.roundNumber) if message.roundNumber is not None else "0"
|
round_str = str(message.roundNumber) if message.roundNumber is not None else "0"
|
||||||
task_str = str(message.taskNumber) if message.taskNumber is not None else "0"
|
task_str = str(message.taskNumber) if message.taskNumber is not None else "0"
|
||||||
action_str = str(message.actionNumber) if message.actionNumber is not None else "0"
|
action_str = str(message.actionNumber) if message.actionNumber is not None else "0"
|
||||||
message_folder = f"m{timestamp}_{round_str}_{task_str}_{action_str}"
|
message_folder = f"{timestamp}_m_{round_str}_{task_str}_{action_str}"
|
||||||
|
|
||||||
message_path = os.path.join(debug_root, message_folder)
|
message_path = os.path.join(debug_root, message_folder)
|
||||||
os.makedirs(message_path, exist_ok=True)
|
os.makedirs(message_path, exist_ok=True)
|
||||||
|
|
|
||||||
|
|
@ -559,10 +559,10 @@ class AiService:
|
||||||
|
|
||||||
# Prepare debug directory TODO TO REMOVE
|
# Prepare debug directory TODO TO REMOVE
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime, UTC
|
||||||
debug_root = "./test-chat/extraction"
|
debug_root = "./test-chat/ai"
|
||||||
ts = datetime.now().strftime("%Y%m%d-%H%M%S")
|
ts = datetime.now(UTC).strftime('%Y%m%d-%H%M%S-%f')[:-3]
|
||||||
debug_dir = os.path.join(debug_root, f"per_chunk_{ts}")
|
debug_dir = os.path.join(debug_root, f"{ts}_extraction_per_chunk")
|
||||||
try:
|
try:
|
||||||
os.makedirs(debug_dir, exist_ok=True)
|
os.makedirs(debug_dir, exist_ok=True)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -744,26 +744,93 @@ class AiService:
|
||||||
call_type = self._determineCallType(documents, options.operationType)
|
call_type = self._determineCallType(documents, options.operationType)
|
||||||
options.callType = call_type
|
options.callType = call_type
|
||||||
|
|
||||||
|
# Log the prompt being sent to AI for debugging (before routing) TODO TO REMOVE
|
||||||
|
try:
|
||||||
|
# Build the full prompt that will be sent to AI
|
||||||
|
if placeholders:
|
||||||
|
full_prompt = prompt
|
||||||
|
for p in placeholders:
|
||||||
|
placeholder = f"{{{{KEY:{p.label}}}}}"
|
||||||
|
full_prompt = full_prompt.replace(placeholder, p.content)
|
||||||
|
else:
|
||||||
|
full_prompt = prompt
|
||||||
|
|
||||||
|
self._writeAiResponseDebug(
|
||||||
|
label='ai_prompt_debug',
|
||||||
|
content=full_prompt,
|
||||||
|
partIndex=1,
|
||||||
|
modelName=None,
|
||||||
|
continuation=False
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Handle document generation with specific output format
|
# Handle document generation with specific output format
|
||||||
if outputFormat:
|
if outputFormat:
|
||||||
return await self._callAiWithDocumentGeneration(prompt, documents, options, outputFormat, title)
|
result = await self._callAiWithDocumentGeneration(prompt, documents, options, outputFormat, title)
|
||||||
|
# Log AI response for debugging TODO TO REMOVE
|
||||||
|
try:
|
||||||
|
if isinstance(result, dict) and 'content' in result:
|
||||||
|
self._writeAiResponseDebug(
|
||||||
|
label='ai_document_generation',
|
||||||
|
content=result['content'],
|
||||||
|
partIndex=1,
|
||||||
|
modelName=None, # Document generation doesn't return model info
|
||||||
|
continuation=False
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
if call_type == "planning":
|
if call_type == "planning":
|
||||||
return await self._callAiPlanning(prompt, placeholders_dict, placeholders_meta, options)
|
result = await self._callAiPlanning(prompt, placeholders_dict, placeholders_meta, options)
|
||||||
|
# Log AI response for debugging TODO TO REMOVE
|
||||||
|
try:
|
||||||
|
self._writeAiResponseDebug(
|
||||||
|
label='ai_planning',
|
||||||
|
content=result or "",
|
||||||
|
partIndex=1,
|
||||||
|
modelName=None, # Planning doesn't return model info
|
||||||
|
continuation=False
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
else:
|
else:
|
||||||
# Set processDocumentsIndividually from the legacy parameter if not set in options
|
# Set processDocumentsIndividually from the legacy parameter if not set in options
|
||||||
if options.processDocumentsIndividually is None and documents:
|
if options.processDocumentsIndividually is None and documents:
|
||||||
options.processDocumentsIndividually = False # Default to batch processing
|
options.processDocumentsIndividually = False # Default to batch processing
|
||||||
return await self._callAiText(prompt, documents, options)
|
|
||||||
|
# For text calls, we need to build the full prompt with placeholders here
|
||||||
|
# since _callAiText doesn't handle placeholders directly
|
||||||
|
if placeholders_dict:
|
||||||
|
full_prompt = self._buildPromptWithPlaceholders(prompt, placeholders_dict)
|
||||||
|
else:
|
||||||
|
full_prompt = prompt
|
||||||
|
|
||||||
|
result = await self._callAiText(full_prompt, documents, options)
|
||||||
|
# Log AI response for debugging (additional logging for text calls) TODO TO REMOVE
|
||||||
|
try:
|
||||||
|
self._writeAiResponseDebug(
|
||||||
|
label='ai_text_main',
|
||||||
|
content=result or "",
|
||||||
|
partIndex=1,
|
||||||
|
modelName=None, # Text calls already log internally
|
||||||
|
continuation=False
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
def _determineCallType(self, documents: Optional[List[ChatDocument]], operation_type: str) -> str:
|
def _determineCallType(self, documents: Optional[List[ChatDocument]], operation_type: str) -> str:
|
||||||
"""
|
"""
|
||||||
Determine call type based on documents and operation type.
|
Determine call type based on documents and operation type.
|
||||||
|
|
||||||
Criteria: no documents AND (operationType is "generate_plan" or "analyse_content") -> planning
|
Criteria: no documents AND operationType is "generate_plan" -> planning
|
||||||
|
All other cases -> text
|
||||||
"""
|
"""
|
||||||
has_documents = documents is not None and len(documents) > 0
|
has_documents = documents is not None and len(documents) > 0
|
||||||
is_planning_operation = operation_type in [OperationType.GENERATE_PLAN, OperationType.ANALYSE_CONTENT]
|
is_planning_operation = operation_type == OperationType.GENERATE_PLAN
|
||||||
|
|
||||||
if not has_documents and is_planning_operation:
|
if not has_documents and is_planning_operation:
|
||||||
return "planning"
|
return "planning"
|
||||||
|
|
@ -857,24 +924,6 @@ class AiService:
|
||||||
logger.debug(f"AI model selected (planning): {getattr(response, 'modelName', 'unknown')}")
|
logger.debug(f"AI model selected (planning): {getattr(response, 'modelName', 'unknown')}")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
# Write full planning response as JSON dump when possible (no duplicates)
|
|
||||||
try:
|
|
||||||
import json
|
|
||||||
content = response.content
|
|
||||||
cleaned = content.strip()
|
|
||||||
if cleaned.startswith('```json'):
|
|
||||||
cleaned = cleaned[7:]
|
|
||||||
if cleaned.endswith('```'):
|
|
||||||
cleaned = cleaned[:-3]
|
|
||||||
cleaned = cleaned.strip()
|
|
||||||
obj = json.loads(cleaned)
|
|
||||||
self._writeTraceLog("AI Planning Raw Response", obj)
|
|
||||||
except Exception:
|
|
||||||
# Fallback to plain text once
|
|
||||||
try:
|
|
||||||
self._writeTraceLog("AI Planning Raw Response", response.content)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return response.content
|
return response.content
|
||||||
|
|
||||||
async def _callAiText(
|
async def _callAiText(
|
||||||
|
|
@ -1027,16 +1076,6 @@ class AiService:
|
||||||
pass
|
pass
|
||||||
content_first = response.content or ""
|
content_first = response.content or ""
|
||||||
merged_content, needs_more = _split_content_and_flag(content_first)
|
merged_content, needs_more = _split_content_and_flag(content_first)
|
||||||
try:
|
|
||||||
self._writeAiResponseDebug(
|
|
||||||
label='ai_text',
|
|
||||||
content=content_first,
|
|
||||||
partIndex=1,
|
|
||||||
modelName=getattr(response, 'modelName', None),
|
|
||||||
continuation=needs_more
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Iteratively request next parts if flagged
|
# Iteratively request next parts if flagged
|
||||||
# Allow configurable max parts via options; default = 1000
|
# Allow configurable max parts via options; default = 1000
|
||||||
|
|
@ -1064,16 +1103,6 @@ class AiService:
|
||||||
next_response = await self.aiObjects.call(next_request)
|
next_response = await self.aiObjects.call(next_request)
|
||||||
part_text = next_response.content or ""
|
part_text = next_response.content or ""
|
||||||
part_clean, needs_more = _split_content_and_flag(part_text)
|
part_clean, needs_more = _split_content_and_flag(part_text)
|
||||||
try:
|
|
||||||
self._writeAiResponseDebug(
|
|
||||||
label='ai_text',
|
|
||||||
content=part_text,
|
|
||||||
partIndex=part_index,
|
|
||||||
modelName=getattr(next_response, 'modelName', None),
|
|
||||||
continuation=needs_more
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if part_clean:
|
if part_clean:
|
||||||
# Separate parts clearly
|
# Separate parts clearly
|
||||||
merged_content = (merged_content + "\n\n" + part_clean).strip()
|
merged_content = (merged_content + "\n\n" + part_clean).strip()
|
||||||
|
|
@ -1247,14 +1276,14 @@ class AiService:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _writeAiResponseDebug(self, label: str, content: str, partIndex: int = 1, modelName: str = None, continuation: bool = None) -> None:
|
def _writeAiResponseDebug(self, label: str, content: str, partIndex: int = 1, modelName: str = None, continuation: bool = None) -> None:
|
||||||
"""Persist raw AI response parts for debugging under test-chat/ai-responses."""
|
"""Persist raw AI response parts for debugging under test-chat/ai."""
|
||||||
try:
|
try:
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, UTC
|
from datetime import datetime, UTC
|
||||||
# Base dir: gateway/test-chat/ai-responses (go up 4 levels from this file)
|
# Base dir: gateway/test-chat/ai (go up 4 levels from this file)
|
||||||
# .../gateway/modules/services/serviceAi/mainServiceAi.py -> up to gateway root
|
# .../gateway/modules/services/serviceAi/mainServiceAi.py -> up to gateway root
|
||||||
gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||||
outDir = os.path.join(gatewayDir, 'test-chat', 'ai-responses')
|
outDir = os.path.join(gatewayDir, 'test-chat', 'ai')
|
||||||
os.makedirs(outDir, exist_ok=True)
|
os.makedirs(outDir, exist_ok=True)
|
||||||
ts = datetime.now(UTC).strftime('%Y%m%d-%H%M%S-%f')[:-3]
|
ts = datetime.now(UTC).strftime('%Y%m%d-%H%M%S-%f')[:-3]
|
||||||
suffix = []
|
suffix = []
|
||||||
|
|
@ -1266,7 +1295,7 @@ class AiService:
|
||||||
safeModel = ''.join(c if c.isalnum() or c in ('-', '_') else '-' for c in modelName)
|
safeModel = ''.join(c if c.isalnum() or c in ('-', '_') else '-' for c in modelName)
|
||||||
suffix.append(safeModel)
|
suffix.append(safeModel)
|
||||||
suffixStr = ('_' + '_'.join(suffix)) if suffix else ''
|
suffixStr = ('_' + '_'.join(suffix)) if suffix else ''
|
||||||
fname = f"{label}_{ts}{suffixStr}.txt"
|
fname = f"{ts}_{label}{suffixStr}.txt"
|
||||||
fpath = os.path.join(outDir, fname)
|
fpath = os.path.join(outDir, fname)
|
||||||
with open(fpath, 'w', encoding='utf-8') as f:
|
with open(fpath, 'w', encoding='utf-8') as f:
|
||||||
f.write(content or '')
|
f.write(content or '')
|
||||||
|
|
|
||||||
|
|
@ -92,11 +92,15 @@ def runExtraction(extractorRegistry: ExtractorRegistry, chunkerRegistry: Chunker
|
||||||
parts = non_chunk_parts + chunk_parts
|
parts = non_chunk_parts + chunk_parts
|
||||||
|
|
||||||
logger.debug(f"runExtraction: Final parts after merging: {len(parts)} (chunks: {len(chunk_parts)})")
|
logger.debug(f"runExtraction: Final parts after merging: {len(parts)} (chunks: {len(chunk_parts)})")
|
||||||
# DEBUG: dump parts and chunks to files under @testing_extraction/ TODO TO REMOVE
|
# DEBUG: dump parts and chunks to files TODO TO REMOVE
|
||||||
try:
|
try:
|
||||||
base_dir = "./test-chat/extraction"
|
base_dir = "./test-chat/ai"
|
||||||
doc_dir = os.path.join(base_dir, f"extraction_{fileName}")
|
os.makedirs(base_dir, exist_ok=True)
|
||||||
os.makedirs(doc_dir, exist_ok=True)
|
|
||||||
|
# Generate timestamp for consistent naming
|
||||||
|
from datetime import datetime, UTC
|
||||||
|
ts = datetime.now(UTC).strftime('%Y%m%d-%H%M%S-%f')[:-3]
|
||||||
|
|
||||||
# Write a summary file
|
# Write a summary file
|
||||||
summary_lines: List[str] = [f"fileName: {fileName}", f"mimeType: {mimeType}", f"totalParts: {len(parts)}"]
|
summary_lines: List[str] = [f"fileName: {fileName}", f"mimeType: {mimeType}", f"totalParts: {len(parts)}"]
|
||||||
text_index = 0
|
text_index = 0
|
||||||
|
|
@ -109,12 +113,16 @@ def runExtraction(extractorRegistry: ExtractorRegistry, chunkerRegistry: Chunker
|
||||||
)
|
)
|
||||||
if is_texty and getattr(part, "data", None):
|
if is_texty and getattr(part, "data", None):
|
||||||
text_index += 1
|
text_index += 1
|
||||||
fname = f"part_{idx:03d}_{'chunk' if is_chunk else 'full'}_{text_index:03d}.txt"
|
fname = f"{ts}_extract_{fileName}_part_{idx:03d}_{'chunk' if is_chunk else 'full'}_{text_index:03d}.txt"
|
||||||
fpath = os.path.join(doc_dir, fname)
|
fpath = os.path.join(base_dir, fname)
|
||||||
with open(fpath, "w", encoding="utf-8") as f:
|
with open(fpath, "w", encoding="utf-8") as f:
|
||||||
f.write(f"# typeGroup: {part.typeGroup}\n# label: {part.label}\n# chunk: {is_chunk}\n# size: {size}\n\n")
|
f.write(f"# typeGroup: {part.typeGroup}\n# label: {part.label}\n# chunk: {is_chunk}\n# size: {size}\n\n")
|
||||||
f.write(str(part.data))
|
f.write(str(part.data))
|
||||||
with open(os.path.join(doc_dir, "summary.txt"), "w", encoding="utf-8") as f:
|
|
||||||
|
# Write summary file
|
||||||
|
summary_fname = f"{ts}_extract_{fileName}_summary.txt"
|
||||||
|
summary_fpath = os.path.join(base_dir, summary_fname)
|
||||||
|
with open(summary_fpath, "w", encoding="utf-8") as f:
|
||||||
f.write("\n".join(summary_lines))
|
f.write("\n".join(summary_lines))
|
||||||
except Exception as _e:
|
except Exception as _e:
|
||||||
logger.debug(f"Debug dump skipped: {_e}")
|
logger.debug(f"Debug dump skipped: {_e}")
|
||||||
|
|
|
||||||
|
|
@ -309,11 +309,11 @@ class GenerationService:
|
||||||
tuple: (rendered_content, mime_type)
|
tuple: (rendered_content, mime_type)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# DEBUG: dump renderer input to @testing_extraction to diagnose JSON+HTML mixtures TODO REMOVE
|
# DEBUG: dump renderer input to diagnose JSON+HTML mixtures TODO REMOVE
|
||||||
try:
|
try:
|
||||||
import os
|
import os
|
||||||
ts = datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
|
ts = datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
|
||||||
debug_root = "./test-chat/extraction"
|
debug_root = "./test-chat/ai"
|
||||||
debug_dir = os.path.join(debug_root, f"render_input_{ts}")
|
debug_dir = os.path.join(debug_root, f"render_input_{ts}")
|
||||||
os.makedirs(debug_dir, exist_ok=True)
|
os.makedirs(debug_dir, exist_ok=True)
|
||||||
with open(os.path.join(debug_dir, "meta.txt"), "w", encoding="utf-8") as f:
|
with open(os.path.join(debug_dir, "meta.txt"), "w", encoding="utf-8") as f:
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
```json
|
|
||||||
{
|
|
||||||
"detectedLanguage": "de",
|
|
||||||
"intent": "Erstelle ein Word-Dokument mit den ersten 1000 Primzahlen.",
|
|
||||||
"contextItems": [],
|
|
||||||
"CONTINUATION": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
FILENAME: first-1000-primes.docx
|
|
||||||
|
|
||||||
AI Generated Document
|
|
||||||
|
|
||||||
Title Page
|
|
||||||
|
|
||||||
AI Generated Document
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Page 1
|
|
||||||
|
|
||||||
**List of the First 1000 Prime Numbers**
|
|
||||||
|
|
||||||
This document contains the first 1000 prime numbers, organized into sections of 100 numbers each. Each section is presented in a table format with 5 columns and column headers for clarity and readability. Page numbers are included at the bottom of each page.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Section 1: Primes 1 to 100
|
|
||||||
|
|
||||||
| Index | Prime Number | Index | Prime Number | Index | Prime Number | Index | Prime Number | Index | Prime Number |
|
|
||||||
|-------|--------------|-------|--------------|-------|--------------|-------|--------------|-------|--------------|
|
|
||||||
| 1 | 2 | 2 | 3 | 3 | 5 | 4 | 7 | 5 | 11 |
|
|
||||||
| 6 | 13 | 7 | 17 | 8 | 19 | 9 | 23 | 10 | 29 |
|
|
||||||
| 11 | 31 | 12 | 37 | 13 | 41 | 14 | 43 | 15 | 47 |
|
|
||||||
| 16 | 53 | 17 | 59 | 18 | 61 | 19 | 67 | 20 | 71 |
|
|
||||||
| 21 | 73 | 22 | 79 | 23 | 83 | 24 | 89 | 25 | 97 |
|
|
||||||
| 26 | 101 | 27 | 103 | 28 | 107 | 29 | 109 | 30 | 113 |
|
|
||||||
| 31 | 127 | 32 | 131 | 33 | 137 | 34 | 139 | 35 | 149 |
|
|
||||||
| 36 | 151 | 37 | 157 | 38 | 163 | 39 | 167 | 40 | 173 |
|
|
||||||
| 41 | 179 | 42 | 181 | 43 | 191 | 44 | 193 | 45 | 197 |
|
|
||||||
| 46 | 199 | 47 | 211 | 48 | 223 | 49 | 227 | 50 | 229 |
|
|
||||||
| 51 | 233 | 52 | 239 | 53 | 241 | 54 | 251 | 55 | 257 |
|
|
||||||
| 56 | 263 | 57 | 269 | 58 | 271 | 59 | 277 | 60 | 281 |
|
|
||||||
| 61 | 283 | 62 | 293 | 63 | 307 | 64 | 311 | 65 | 313 |
|
|
||||||
| 66 | 317 | 67 | 331 | 68 | 337 | 69 | 347 | 70 | 349 |
|
|
||||||
| 71 | 353 | 72 | 359 | 73 | 367 | 74 | 373 | 75 | 379 |
|
|
||||||
| 76 | 383 | 77 | 389 | 78 | 397 | 79 | 401 | 80 | 409 |
|
|
||||||
| 81 | 419 | 82 | 421 | 83 | 431 | 84 | 433 | 85 | 439 |
|
|
||||||
| 86 | 443 | 87 | 449 | 88 | 457 | 89 | 461 | 90 | 463 |
|
|
||||||
| 91 | 467 | 92 | 479 | 93 | 487 | 94 | 491 | 95 | 499 |
|
|
||||||
| 96 | 503 | 97 | 509 | 98 | 521 | 99 | 523 | 100 | 541 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Section 2: Primes 101 to 200
|
|
||||||
|
|
||||||
| Index | Prime Number | Index | Prime Number | Index | Prime Number | Index | Prime Number | Index | Prime Number |
|
|
||||||
|-------|--------------|-------|--------------|-------|--------------|-------|--------------|-------|--------------|
|
|
||||||
| 101 | 547 | 102 | 557 | 103 | 563 | 104 | 569 | 105 | 571 |
|
|
||||||
| 106 | 577 | 107 | 587 | 108 | 593 | 109 | 599 | 110 | 601 |
|
|
||||||
| 111 | 607 | 112 | 613 | 113 | 617 | 114 | 619 | 115 | 631 |
|
|
||||||
| 116 | 641 | 117 | 643 | 118 | 647 | 119 | 653 | 120 | 659 |
|
|
||||||
| 121 | 661 | 122 | 673 | 123 | 677 | 124 | 683 | 125 | 691 |
|
|
||||||
| 126 | 701 | 127 | 709 | 128 | 719 | 129 | 727 | 130 | 733 |
|
|
||||||
| 131 | 739 | 132 | 743 | 133 | 751 | 134 | 757 | 135 | 761 |
|
|
||||||
| 136 | 769 | 137 | 773 | 138 | 787 | 139 | 797 | 140 | 809 |
|
|
||||||
| 141 | 811 | 142 | 821 | 143 | 823 | 144 | 827 | 145 | 829 |
|
|
||||||
| 146 | 839 | 147 | 853 | 148 | 857 | 149 | 859 | 150 | 863 |
|
|
||||||
| 151 | 877 | 152 | 881 | 153 | 883 | 154 | 887 | 155 | 907 |
|
|
||||||
| 156 | 911 | 157 | 919 | 158 | 929 | 159 | 937 | 160 | 941 |
|
|
||||||
| 161 | 947 | 162 | 953 | 163 | 967 | 164 | 971 | 165 | 977 |
|
|
||||||
| 166 | 983 | 167 | 991 | 168 | 997 | 169 | 1009 | 170 | 1013 |
|
|
||||||
| 171 | 1019 | 172 | 1021 | 173 | 1031 | 174 | 1033 | 175 | 1039 |
|
|
||||||
| 176 | 1049 | 177 | 1051 | 178 | 1061 | 179 | 1063 | 180 | 1069 |
|
|
||||||
| 181 | 1087 | 182 | 1091 | 183 | 1093 | 184 | 1097 | 185 | 1103 |
|
|
||||||
| 186 | 1109 | 187 | 1117 | 188 | 1123 | 189 | 1129 | 190 | 1151 |
|
|
||||||
| 191 | 1153 | 192 | 1163 | 193 | 1171 | 194 | 1181 | 195 | 1187 |
|
|
||||||
| 196 | 1193 | 197 | 1201 | 198 | 1213 | 199 | 1217 | 200 | 1223 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Section 3: Primes 201 to 300
|
|
||||||
|
|
||||||
| Index | Prime Number | Index | Prime Number | Index | Prime Number | Index | Prime Number | Index | Prime Number |
|
|
||||||
|-------|--------------|-------|--------------|-------|--------------|-------|--------------|-------|--------------|
|
|
||||||
| 201 | 1229 | 202 | 1231 | 203 | 1237 | 204 | 1249 | 205 | 1259 |
|
|
||||||
| 206 | 1277 | 207
|
|
||||||
|
|
@ -127,7 +127,9 @@ class MethodOutlook(MethodBase):
|
||||||
clean_query = clean_query.replace('"', '')
|
clean_query = clean_query.replace('"', '')
|
||||||
|
|
||||||
# Handle common search operators
|
# Handle common search operators
|
||||||
if any(op in clean_query.lower() for op in ['from:', 'to:', 'subject:', 'received:', 'hasattachment:']):
|
# Recognize Graph operators including both singular and plural forms for hasAttachments
|
||||||
|
lowered = clean_query.lower()
|
||||||
|
if any(op in lowered for op in ['from:', 'to:', 'subject:', 'received:', 'hasattachment:', 'hasattachments:']):
|
||||||
# This is an advanced search query, return as-is
|
# This is an advanced search query, return as-is
|
||||||
return clean_query
|
return clean_query
|
||||||
|
|
||||||
|
|
@ -170,7 +172,9 @@ class MethodOutlook(MethodBase):
|
||||||
return params
|
return params
|
||||||
|
|
||||||
# Check if this is a complex search query with multiple operators
|
# Check if this is a complex search query with multiple operators
|
||||||
if any(op in clean_query.lower() for op in ['from:', 'to:', 'subject:', 'received:', 'hasattachment:']):
|
# Recognize Graph operators including both singular and plural forms for hasAttachments
|
||||||
|
lowered = clean_query.lower()
|
||||||
|
if any(op in lowered for op in ['from:', 'to:', 'subject:', 'received:', 'hasattachment:', 'hasattachments:']):
|
||||||
# This is an advanced search query, use $search
|
# This is an advanced search query, use $search
|
||||||
# Microsoft Graph API supports complex search syntax
|
# Microsoft Graph API supports complex search syntax
|
||||||
params["$search"] = f'"{clean_query}"'
|
params["$search"] = f'"{clean_query}"'
|
||||||
|
|
@ -222,7 +226,9 @@ class MethodOutlook(MethodBase):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# Handle search queries (from:, to:, subject:, etc.) - check this FIRST
|
# Handle search queries (from:, to:, subject:, etc.) - check this FIRST
|
||||||
if any(filter_text.startswith(prefix) for prefix in ['from:', 'to:', 'subject:', 'received:', 'hasattachment:']):
|
# Support both singular and plural forms for hasAttachments
|
||||||
|
lt = filter_text.lower()
|
||||||
|
if any(lt.startswith(prefix) for prefix in ['from:', 'to:', 'subject:', 'received:', 'hasattachment:', 'hasattachments:']):
|
||||||
return {"$search": f'"{filter_text}"'}
|
return {"$search": f'"{filter_text}"'}
|
||||||
|
|
||||||
# Handle email address filters (only if it's NOT a search query)
|
# Handle email address filters (only if it's NOT a search query)
|
||||||
|
|
@ -1037,165 +1043,6 @@ class MethodOutlook(MethodBase):
|
||||||
logger.error(f"Error checking Drafts folder: {str(e)}")
|
logger.error(f"Error checking Drafts folder: {str(e)}")
|
||||||
return ActionResult.isFailure(error=str(e))
|
return ActionResult.isFailure(error=str(e))
|
||||||
|
|
||||||
@action
|
|
||||||
async def composeAndSendEmailDirect(self, parameters: Dict[str, Any]) -> ActionResult:
|
|
||||||
"""
|
|
||||||
GENERAL:
|
|
||||||
- Purpose: Create and send/prepare email using provided subject, body, and recipients.
|
|
||||||
- Input requirements: connectionReference (required); to (required); subject (required); body (required); optional cc, bcc, attachmentDocumentList.
|
|
||||||
- Output format: JSON confirmation with draft/send metadata.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- connectionReference (str, required): Microsoft connection label.
|
|
||||||
- to (list, required): Recipient email addresses.
|
|
||||||
- subject (str, required): Email subject.
|
|
||||||
- body (str, required): Email body (plain text or HTML).
|
|
||||||
- cc (list, optional): CC recipients.
|
|
||||||
- bcc (list, optional): BCC recipients.
|
|
||||||
- attachmentDocumentList (list, optional): Attachment document references.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
connectionReference = parameters.get("connectionReference")
|
|
||||||
to = parameters.get("to")
|
|
||||||
subject = parameters.get("subject")
|
|
||||||
body = parameters.get("body")
|
|
||||||
cc = parameters.get("cc", [])
|
|
||||||
bcc = parameters.get("bcc", [])
|
|
||||||
attachmentDocumentList = parameters.get("attachmentDocumentList", [])
|
|
||||||
|
|
||||||
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 for all recipient parameters
|
|
||||||
if isinstance(to, str):
|
|
||||||
to = [to]
|
|
||||||
if isinstance(cc, str):
|
|
||||||
cc = [cc]
|
|
||||||
if isinstance(bcc, str):
|
|
||||||
bcc = [bcc]
|
|
||||||
if isinstance(attachmentDocumentList, str):
|
|
||||||
attachmentDocumentList = [attachmentDocumentList]
|
|
||||||
|
|
||||||
# Get Microsoft connection
|
|
||||||
connection = self._getMicrosoftConnection(connectionReference)
|
|
||||||
if not connection:
|
|
||||||
return ActionResult.isFailure(error="No valid Microsoft connection found")
|
|
||||||
|
|
||||||
# Check permissions
|
|
||||||
permissions_ok = await self._checkPermissions(connection)
|
|
||||||
if not permissions_ok:
|
|
||||||
return ActionResult.isFailure(error="Connection lacks necessary permissions for Outlook operations")
|
|
||||||
|
|
||||||
# Create and send the email message
|
|
||||||
try:
|
|
||||||
graph_url = "https://graph.microsoft.com/v1.0"
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {connection['accessToken']}",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Clean and format body content
|
|
||||||
cleaned_body = body.strip()
|
|
||||||
|
|
||||||
# Check if body is already HTML
|
|
||||||
if cleaned_body.startswith('<html>') or cleaned_body.startswith('<body>') or '<br>' in cleaned_body:
|
|
||||||
html_body = cleaned_body
|
|
||||||
else:
|
|
||||||
# Convert plain text to proper HTML formatting
|
|
||||||
html_body = cleaned_body.replace('\n', '<br>')
|
|
||||||
html_body = f"<html><body>{html_body}</body></html>"
|
|
||||||
|
|
||||||
# Build the email message
|
|
||||||
message = {
|
|
||||||
"subject": subject,
|
|
||||||
"body": {
|
|
||||||
"contentType": "HTML",
|
|
||||||
"content": html_body
|
|
||||||
},
|
|
||||||
"toRecipients": [{"emailAddress": {"address": email}} for email in to],
|
|
||||||
"ccRecipients": [{"emailAddress": {"address": email}} for email in cc] if cc else [],
|
|
||||||
"bccRecipients": [{"emailAddress": {"address": email}} for email in bcc] if bcc else []
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add attachments if provided
|
|
||||||
if attachmentDocumentList:
|
|
||||||
message["attachments"] = []
|
|
||||||
for attachment_ref in attachmentDocumentList:
|
|
||||||
# Get attachment document from service center
|
|
||||||
attachment_docs = self.services.workflow.getChatDocumentsFromDocumentList([attachment_ref])
|
|
||||||
if attachment_docs:
|
|
||||||
for doc in attachment_docs:
|
|
||||||
file_id = getattr(doc, 'fileId', None)
|
|
||||||
if file_id:
|
|
||||||
try:
|
|
||||||
file_content = self.services.workflow.getFileData(file_id)
|
|
||||||
if file_content:
|
|
||||||
if isinstance(file_content, bytes):
|
|
||||||
content_bytes = file_content
|
|
||||||
else:
|
|
||||||
content_bytes = str(file_content).encode('utf-8')
|
|
||||||
|
|
||||||
base64_content = base64.b64encode(content_bytes).decode('utf-8')
|
|
||||||
|
|
||||||
attachment = {
|
|
||||||
"@odata.type": "#microsoft.graph.fileAttachment",
|
|
||||||
"name": doc.fileName,
|
|
||||||
"contentType": doc.mimeType or "application/octet-stream",
|
|
||||||
"contentBytes": base64_content
|
|
||||||
}
|
|
||||||
message["attachments"].append(attachment)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error reading attachment file {doc.fileName}: {str(e)}")
|
|
||||||
|
|
||||||
# Create the draft message
|
|
||||||
drafts_folder_id = self._getFolderId("Drafts", connection)
|
|
||||||
|
|
||||||
if drafts_folder_id:
|
|
||||||
api_url = f"{graph_url}/me/mailFolders/{drafts_folder_id}/messages"
|
|
||||||
else:
|
|
||||||
api_url = f"{graph_url}/me/messages"
|
|
||||||
logger.warning("Could not find Drafts folder, creating draft in default location")
|
|
||||||
|
|
||||||
response = requests.post(api_url, headers=headers, json=message)
|
|
||||||
|
|
||||||
if response.status_code in [200, 201]:
|
|
||||||
draft_data = response.json()
|
|
||||||
draft_id = draft_data.get("id", "Unknown")
|
|
||||||
|
|
||||||
result_data = {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Email draft created successfully",
|
|
||||||
"draftId": draft_id,
|
|
||||||
"folder": "Drafts (Entwürfe)",
|
|
||||||
"mailbox": connection.get('userEmail', 'Unknown'),
|
|
||||||
"subject": subject,
|
|
||||||
"recipients": to,
|
|
||||||
"cc": cc,
|
|
||||||
"bcc": bcc,
|
|
||||||
"attachments": len(attachmentDocumentList),
|
|
||||||
"timestamp": self.services.utils.getUtcTimestamp()
|
|
||||||
}
|
|
||||||
|
|
||||||
return ActionResult(
|
|
||||||
success=True,
|
|
||||||
documents=[ActionDocument(
|
|
||||||
documentName=f"email_draft_created_{self._format_timestamp_for_filename()}.json",
|
|
||||||
documentData=json.dumps(result_data, indent=2),
|
|
||||||
mimeType="application/json"
|
|
||||||
)]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.error(f"Failed to create draft. Status: {response.status_code}, Response: {response.text}")
|
|
||||||
return ActionResult.isFailure(error=f"Failed to create email draft: {response.status_code} - {response.text}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error creating email via Microsoft Graph API: {str(e)}")
|
|
||||||
return ActionResult.isFailure(error=f"Failed to create email: {str(e)}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in composeAndSendEmailDirect: {str(e)}")
|
|
||||||
return ActionResult.isFailure(error=str(e))
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async def composeAndSendEmailWithContext(self, parameters: Dict[str, Any]) -> ActionResult:
|
async def composeAndSendEmailWithContext(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||||
"""
|
"""
|
||||||
|
|
@ -1207,7 +1054,7 @@ class MethodOutlook(MethodBase):
|
||||||
Parameters:
|
Parameters:
|
||||||
- connectionReference (str, required): Microsoft connection label.
|
- connectionReference (str, required): Microsoft connection label.
|
||||||
- to (list, required): Recipient email addresses.
|
- to (list, required): Recipient email addresses.
|
||||||
- context (str, required): Context for composing the email.
|
- context (str, required): Detailled context for composing the email.
|
||||||
- documentList (list, optional): Document references for context/attachments.
|
- documentList (list, optional): Document references for context/attachments.
|
||||||
- cc (list, optional): CC recipients.
|
- cc (list, optional): CC recipients.
|
||||||
- bcc (list, optional): BCC recipients.
|
- bcc (list, optional): BCC recipients.
|
||||||
|
|
@ -1253,6 +1100,14 @@ class MethodOutlook(MethodBase):
|
||||||
chatDocuments = self.services.workflow.getChatDocumentsFromDocumentList(documentList)
|
chatDocuments = self.services.workflow.getChatDocumentsFromDocumentList(documentList)
|
||||||
|
|
||||||
# Create AI prompt for email composition
|
# Create AI prompt for email composition
|
||||||
|
# Build document reference list for AI
|
||||||
|
doc_references = documentList
|
||||||
|
doc_list_text = ""
|
||||||
|
if doc_references:
|
||||||
|
doc_list_text = f"Available_Document_References: {', '.join(doc_references)}"
|
||||||
|
else:
|
||||||
|
doc_list_text = "Available_Document_References: (No documents available for attachment)"
|
||||||
|
|
||||||
ai_prompt = f"""
|
ai_prompt = f"""
|
||||||
Compose a professional email based on the following context and requirements:
|
Compose a professional email based on the following context and requirements:
|
||||||
|
|
||||||
|
|
@ -1263,15 +1118,19 @@ RECIPIENT: {to}
|
||||||
EMAIL STYLE: {emailStyle}
|
EMAIL STYLE: {emailStyle}
|
||||||
MAX LENGTH: {maxLength} characters
|
MAX LENGTH: {maxLength} characters
|
||||||
|
|
||||||
|
{doc_list_text}
|
||||||
|
|
||||||
Please generate:
|
Please generate:
|
||||||
1. A clear, professional subject line
|
1. A clear, professional subject line
|
||||||
2. A well-structured email body that addresses the context appropriately
|
2. A well-structured email body that addresses the context appropriately
|
||||||
3. Use the {emailStyle} tone throughout
|
3. Use the {emailStyle} tone throughout
|
||||||
|
4. Decide which documents from Available_Document_References (if any) should be attached to the email
|
||||||
|
|
||||||
Return your response in the following JSON format:
|
Return your response in the following JSON format:
|
||||||
{{
|
{{
|
||||||
"subject": "Your generated subject line here",
|
"subject": "Your generated subject line here",
|
||||||
"body": "Your generated email body here (can include HTML formatting like <br> for line breaks)"
|
"body": "Your generated email body here (can include HTML formatting like <br> for line breaks)",
|
||||||
|
"attachments": ["document_reference", "document_reference", ...]
|
||||||
}}
|
}}
|
||||||
|
|
||||||
Make sure the email is:
|
Make sure the email is:
|
||||||
|
|
@ -1279,6 +1138,7 @@ Make sure the email is:
|
||||||
- Clear and concise
|
- Clear and concise
|
||||||
- Well-structured with proper greeting and closing
|
- Well-structured with proper greeting and closing
|
||||||
- Relevant to the provided context
|
- Relevant to the provided context
|
||||||
|
- Include only relevant documents as attachments (use EXACT document references from the Available_Document_References)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Call AI service to generate email content
|
# Call AI service to generate email content
|
||||||
|
|
@ -1291,7 +1151,7 @@ Make sure the email is:
|
||||||
priority="normal",
|
priority="normal",
|
||||||
compressPrompt=False,
|
compressPrompt=False,
|
||||||
compressContext=True,
|
compressContext=True,
|
||||||
processDocumentsIndividually=True,
|
processDocumentsIndividually=False, # Process all documents together for email composition
|
||||||
processingMode="detailed",
|
processingMode="detailed",
|
||||||
resultFormat="json",
|
resultFormat="json",
|
||||||
maxCost=0.50,
|
maxCost=0.50,
|
||||||
|
|
@ -1317,9 +1177,22 @@ Make sure the email is:
|
||||||
email_data = json.loads(json_content)
|
email_data = json.loads(json_content)
|
||||||
subject = email_data.get("subject", "")
|
subject = email_data.get("subject", "")
|
||||||
body = email_data.get("body", "")
|
body = email_data.get("body", "")
|
||||||
|
ai_attachments = email_data.get("attachments", [])
|
||||||
|
|
||||||
if not subject or not body:
|
if not subject or not body:
|
||||||
return ActionResult.isFailure(error="AI did not generate valid subject and body")
|
return ActionResult.isFailure(error="AI did not generate valid subject and body")
|
||||||
|
|
||||||
|
# Use AI-selected attachments if provided, otherwise use all documents
|
||||||
|
if ai_attachments:
|
||||||
|
# Filter documentList to only include AI-selected attachments
|
||||||
|
selected_docs = [doc_ref for doc_ref in documentList if doc_ref in ai_attachments]
|
||||||
|
if selected_docs:
|
||||||
|
documentList = selected_docs
|
||||||
|
logger.info(f"AI selected {len(selected_docs)} documents for attachment: {selected_docs}")
|
||||||
|
else:
|
||||||
|
logger.warning("AI selected attachments not found in available documents, using all documents")
|
||||||
|
else:
|
||||||
|
logger.info("AI did not specify attachments, using all available documents")
|
||||||
|
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
logger.error(f"Failed to parse AI response as JSON: {str(e)}")
|
logger.error(f"Failed to parse AI response as JSON: {str(e)}")
|
||||||
|
|
@ -1418,6 +1291,7 @@ Make sure the email is:
|
||||||
"cc": cc,
|
"cc": cc,
|
||||||
"bcc": bcc,
|
"bcc": bcc,
|
||||||
"attachments": len(documentList),
|
"attachments": len(documentList),
|
||||||
|
"aiSelectedAttachments": ai_attachments if ai_attachments else "all documents",
|
||||||
"aiGenerated": True,
|
"aiGenerated": True,
|
||||||
"context": context,
|
"context": context,
|
||||||
"emailStyle": emailStyle,
|
"emailStyle": emailStyle,
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,12 @@ class MessageCreator:
|
||||||
messageText += f"❌ {taskObjective}\n\n"
|
messageText += f"❌ {taskObjective}\n\n"
|
||||||
messageText += f"{errorDetails}\n\n"
|
messageText += f"{errorDetails}\n\n"
|
||||||
|
|
||||||
|
# Build concise summary to persist for history context
|
||||||
|
doc_count = len(createdDocuments) if createdDocuments else 0
|
||||||
|
trimmed_msg = (messageText or "").strip().replace("\n", " ")
|
||||||
|
if len(trimmed_msg) > 160:
|
||||||
|
trimmed_msg = trimmed_msg[:157] + "..."
|
||||||
|
|
||||||
messageData = {
|
messageData = {
|
||||||
"workflowId": workflow.id,
|
"workflowId": workflow.id,
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
|
|
@ -171,7 +177,8 @@ class MessageCreator:
|
||||||
"roundNumber": currentRound,
|
"roundNumber": currentRound,
|
||||||
"taskNumber": currentTask,
|
"taskNumber": currentTask,
|
||||||
"actionNumber": currentAction,
|
"actionNumber": currentAction,
|
||||||
"actionProgress": "success" if result.success else "fail"
|
"actionProgress": "success" if result.success else "fail",
|
||||||
|
"summary": f"{action.execMethod}.{action.execAction}: {doc_count} docs | msg='{trimmed_msg}'"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add debugging for error messages
|
# Add debugging for error messages
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,13 @@ class ReactMode(BaseMode):
|
||||||
selection = json.loads(response[jsonStart:jsonEnd])
|
selection = json.loads(response[jsonStart:jsonEnd])
|
||||||
if 'action' not in selection or not isinstance(selection['action'], str):
|
if 'action' not in selection or not isinstance(selection['action'], str):
|
||||||
raise ValueError("Selection missing 'action' as string")
|
raise ValueError("Selection missing 'action' as string")
|
||||||
|
# Enforce spec: Stage 1 must NOT include 'parameters'
|
||||||
|
if 'parameters' in selection:
|
||||||
|
# Remove to avoid accidental carryover
|
||||||
|
try:
|
||||||
|
del selection['parameters']
|
||||||
|
except Exception:
|
||||||
|
selection['parameters'] = None
|
||||||
return selection
|
return selection
|
||||||
|
|
||||||
async def _actExecute(self, context: TaskContext, selection: Dict[str, Any], taskStep: TaskStep,
|
async def _actExecute(self, context: TaskContext, selection: Dict[str, Any], taskStep: TaskStep,
|
||||||
|
|
@ -217,57 +224,101 @@ class ReactMode(BaseMode):
|
||||||
|
|
||||||
methodName, actionName = compoundActionName.split('.', 1)
|
methodName, actionName = compoundActionName.split('.', 1)
|
||||||
|
|
||||||
# Check if parameters are already provided in the selection
|
# Always request parameters in Stage 2 (spec: Stage 1 must not provide them)
|
||||||
if 'parameters' in selection and selection['parameters']:
|
logger.info("Requesting parameters in Stage 2 based on Stage 1 outputs")
|
||||||
logger.info("Using parameters from action selection")
|
|
||||||
parameters = selection['parameters']
|
# Create a permissive Stage 2 context to avoid TaskContext attribute restrictions
|
||||||
|
from types import SimpleNamespace
|
||||||
|
stage2Context = SimpleNamespace()
|
||||||
|
|
||||||
|
# Copy essential fields from original context for fallbacks (snake_case for placeholderFactory compatibility)
|
||||||
|
stage2Context.task_step = getattr(context, 'task_step', None)
|
||||||
|
stage2Context.workflow_id = getattr(context, 'workflow_id', None)
|
||||||
|
|
||||||
|
# Set Stage 1 data directly on the permissive context (snake_case for promptGenerationActionsReact compatibility)
|
||||||
|
if isinstance(selection, dict):
|
||||||
|
stage2Context.action_objective = selection.get('actionObjective', '')
|
||||||
|
stage2Context.parameters_context = selection.get('parametersContext', '')
|
||||||
|
stage2Context.learnings = selection.get('learnings', [])
|
||||||
else:
|
else:
|
||||||
logger.info("No parameters in action selection, requesting from AI")
|
stage2Context.action_objective = ''
|
||||||
bundle = generateReactParametersPrompt(self.services, context, compoundActionName)
|
stage2Context.parameters_context = ''
|
||||||
promptTemplate = bundle.prompt
|
stage2Context.learnings = []
|
||||||
placeholders = bundle.placeholders
|
|
||||||
|
# Build and send the Stage 2 parameters prompt (always)
|
||||||
self._writeTraceLog("React Parameters Prompt", promptTemplate)
|
bundle = generateReactParametersPrompt(self.services, stage2Context, compoundActionName)
|
||||||
self._writeTraceLog("React Parameters Placeholders", placeholders)
|
promptTemplate = bundle.prompt
|
||||||
|
placeholders = bundle.placeholders
|
||||||
# Centralized AI call for parameter suggestion (balanced analysis)
|
|
||||||
options = AiCallOptions(
|
self._writeTraceLog("React Parameters Prompt", promptTemplate)
|
||||||
operationType=OperationType.ANALYSE_CONTENT,
|
self._writeTraceLog("React Parameters Placeholders", placeholders)
|
||||||
priority=Priority.BALANCED,
|
|
||||||
compressPrompt=True,
|
# Centralized AI call for parameter suggestion (balanced analysis)
|
||||||
compressContext=False,
|
options = AiCallOptions(
|
||||||
processingMode=ProcessingMode.ADVANCED,
|
operationType=OperationType.ANALYSE_CONTENT,
|
||||||
maxCost=0.05,
|
priority=Priority.BALANCED,
|
||||||
maxProcessingTime=30,
|
compressPrompt=True,
|
||||||
temperature=0.3, # Slightly higher temperature for better instruction following
|
compressContext=False,
|
||||||
# maxTokens not set - use model's maximum for big JSON responses
|
processingMode=ProcessingMode.ADVANCED,
|
||||||
resultFormat="json" # Explicitly request JSON format
|
maxCost=0.05,
|
||||||
)
|
maxProcessingTime=30,
|
||||||
|
temperature=0.3, # Slightly higher temperature for better instruction following
|
||||||
paramsResp = await self.services.ai.callAi(
|
# maxTokens not set - use model's maximum for big JSON responses
|
||||||
prompt=promptTemplate,
|
resultFormat="json" # Explicitly request JSON format
|
||||||
placeholders=placeholders,
|
)
|
||||||
options=options
|
|
||||||
)
|
paramsResp = await self.services.ai.callAi(
|
||||||
# Parse JSON response
|
prompt=promptTemplate,
|
||||||
js = paramsResp[paramsResp.find('{'):paramsResp.rfind('}')+1] if paramsResp else '{}'
|
placeholders=placeholders,
|
||||||
try:
|
options=options
|
||||||
paramObj = json.loads(js)
|
)
|
||||||
parameters = paramObj.get('parameters', {}) if isinstance(paramObj, dict) else {}
|
# Parse JSON response
|
||||||
# Log only the parsed JSON object to avoid duplicated raw text
|
js = paramsResp[paramsResp.find('{'):paramsResp.rfind('}')+1] if paramsResp else '{}'
|
||||||
try:
|
try:
|
||||||
self._writeTraceLog("React Parameters Response", paramObj)
|
paramObj = json.loads(js)
|
||||||
except Exception:
|
parameters = paramObj.get('parameters', {}) if isinstance(paramObj, dict) else {}
|
||||||
pass
|
except Exception as e:
|
||||||
except Exception as e:
|
logger.error(f"Failed to parse AI parameters response as JSON: {str(e)}")
|
||||||
logger.error(f"Failed to parse AI parameters response as JSON: {str(e)}")
|
logger.error(f"Response was: {paramsResp}")
|
||||||
logger.error(f"Response was: {paramsResp}")
|
parameters = {}
|
||||||
parameters = {}
|
|
||||||
|
# Merge Stage 1 resource selections into Stage 2 parameters (only if action expects them)
|
||||||
|
try:
|
||||||
|
requiredDocs = selection.get('requiredInputDocuments')
|
||||||
|
if requiredDocs:
|
||||||
|
# Ensure list
|
||||||
|
if isinstance(requiredDocs, list):
|
||||||
|
# Only attach if target action defines 'documentList'
|
||||||
|
methodName, actionName = compoundActionName.split('.', 1)
|
||||||
|
from modules.workflows.processing.shared.methodDiscovery import getActionParameterList, methods as _methods
|
||||||
|
expectedParams = getActionParameterList(methodName, actionName, _methods)
|
||||||
|
if 'documentList' in expectedParams:
|
||||||
|
parameters['documentList'] = requiredDocs
|
||||||
|
requiredConn = selection.get('requiredConnection')
|
||||||
|
if requiredConn:
|
||||||
|
# Only attach if target action defines 'connectionReference'
|
||||||
|
methodName, actionName = compoundActionName.split('.', 1)
|
||||||
|
from modules.workflows.processing.shared.methodDiscovery import getActionParameterList, methods as _methods
|
||||||
|
expectedParams = getActionParameterList(methodName, actionName, _methods)
|
||||||
|
if 'connectionReference' in expectedParams:
|
||||||
|
parameters['connectionReference'] = requiredConn
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Apply minimal defaults in-code (language)
|
# Apply minimal defaults in-code (language)
|
||||||
if 'language' not in parameters and hasattr(self.services, 'user') and getattr(self.services.user, 'language', None):
|
if 'language' not in parameters and hasattr(self.services, 'user') and getattr(self.services.user, 'language', None):
|
||||||
parameters['language'] = self.services.user.language
|
parameters['language'] = self.services.user.language
|
||||||
|
|
||||||
|
# Write merged parameters to trace BEFORE continuing
|
||||||
|
try:
|
||||||
|
mergedParamObj = {
|
||||||
|
"schema": (paramObj.get('schema') if isinstance(paramObj, dict) else 'parameters_v1'),
|
||||||
|
"parameters": parameters
|
||||||
|
}
|
||||||
|
self._writeTraceLog("React Parameters Response", mergedParamObj)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Build a synthetic ActionItem for execution routing and labels
|
# Build a synthetic ActionItem for execution routing and labels
|
||||||
currentRound = getattr(self.workflow, 'currentRound', 0)
|
currentRound = getattr(self.workflow, 'currentRound', 0)
|
||||||
currentTask = getattr(self.workflow, 'currentTask', 0)
|
currentTask = getattr(self.workflow, 'currentTask', 0)
|
||||||
|
|
@ -295,7 +346,7 @@ class ReactMode(BaseMode):
|
||||||
for doc in actionResult.documents:
|
for doc in actionResult.documents:
|
||||||
# Extract all available metadata without content
|
# Extract all available metadata without content
|
||||||
docMetadata = {
|
docMetadata = {
|
||||||
"name": getattr(doc, 'documentName', 'Unknown'),
|
"name": getattr(doc, 'fileName', None) or getattr(doc, 'documentName', 'Unknown'),
|
||||||
"mimeType": getattr(doc, 'mimeType', 'Unknown'),
|
"mimeType": getattr(doc, 'mimeType', 'Unknown'),
|
||||||
"size": getattr(doc, 'size', 'Unknown'),
|
"size": getattr(doc, 'size', 'Unknown'),
|
||||||
"created": getattr(doc, 'created', 'Unknown'),
|
"created": getattr(doc, 'created', 'Unknown'),
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ NAMING CONVENTION:
|
||||||
MAPPING TABLE (keys → function) with usage [taskplan | actionplan | react]:
|
MAPPING TABLE (keys → function) with usage [taskplan | actionplan | react]:
|
||||||
{{KEY:USER_PROMPT}} -> extractUserPrompt() [taskplan, actionplan, react]
|
{{KEY:USER_PROMPT}} -> extractUserPrompt() [taskplan, actionplan, react]
|
||||||
{{KEY:USER_LANGUAGE}} -> extractUserLanguage() [actionplan, react]
|
{{KEY:USER_LANGUAGE}} -> extractUserLanguage() [actionplan, react]
|
||||||
{{KEY:WORKFLOW_HISTORY}} -> extractWorkflowHistory() [taskplan, actionplan]
|
{{KEY:WORKFLOW_HISTORY}} -> extractWorkflowHistory() [taskplan, actionplan, react]
|
||||||
{{KEY:AVAILABLE_CONNECTIONS_INDEX}} -> extractAvailableConnectionsIndex() [actionplan, react]
|
{{KEY:AVAILABLE_CONNECTIONS_INDEX}} -> extractAvailableConnectionsIndex() [actionplan, react]
|
||||||
{{KEY:AVAILABLE_CONNECTIONS_SUMMARY}} -> extractAvailableConnectionsSummary() []
|
{{KEY:AVAILABLE_CONNECTIONS_SUMMARY}} -> extractAvailableConnectionsSummary() []
|
||||||
{{KEY:AVAILABLE_DOCUMENTS_SUMMARY}} -> extractAvailableDocumentsSummary() [taskplan, actionplan, react]
|
{{KEY:AVAILABLE_DOCUMENTS_SUMMARY}} -> extractAvailableDocumentsSummary() [taskplan, actionplan, react]
|
||||||
|
|
@ -54,7 +54,9 @@ def extractUserPrompt(context: Any) -> str:
|
||||||
return 'No request specified'
|
return 'No request specified'
|
||||||
|
|
||||||
def extractWorkflowHistory(service: Any, context: Any) -> str:
|
def extractWorkflowHistory(service: Any, context: Any) -> str:
|
||||||
"""Extract workflow history from context. Maps to {{KEY:WORKFLOW_HISTORY}}"""
|
"""Extract workflow history from context. Maps to {{KEY:WORKFLOW_HISTORY}}
|
||||||
|
Reverse-chronological, enriched with message summaries and document labels.
|
||||||
|
"""
|
||||||
# Prefer explicit workflow on context; else fall back to services.workflow
|
# Prefer explicit workflow on context; else fall back to services.workflow
|
||||||
workflow = None
|
workflow = None
|
||||||
try:
|
try:
|
||||||
|
|
@ -99,30 +101,10 @@ def extractUserLanguage(service: Any) -> str:
|
||||||
"""Extract user language from service. Maps to {{KEY:USER_LANGUAGE}}"""
|
"""Extract user language from service. Maps to {{KEY:USER_LANGUAGE}}"""
|
||||||
return service.user.language if service and service.user else 'en'
|
return service.user.language if service and service.user else 'en'
|
||||||
|
|
||||||
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 _computeMessageSummary(msg) -> str:
|
def _computeMessageSummary(msg) -> str:
|
||||||
"""Create a concise summary for a ChatMessage with documents only.
|
"""Create a concise summary for a ChatMessage with documents only.
|
||||||
Fields: documentCount, roundNumber, documentsLabel, document names, message (trimmed), success flag.
|
Fields: documentCount, roundNumber, documentsLabel, document names, message (full), success flag.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
docs = getattr(msg, 'documents', []) or []
|
docs = getattr(msg, 'documents', []) or []
|
||||||
|
|
@ -131,26 +113,39 @@ def _computeMessageSummary(msg) -> str:
|
||||||
document_count = len(docs)
|
document_count = len(docs)
|
||||||
round_number = getattr(msg, 'roundNumber', None) or 0
|
round_number = getattr(msg, 'roundNumber', None) or 0
|
||||||
label = getattr(msg, 'documentsLabel', None) or ""
|
label = getattr(msg, 'documentsLabel', None) or ""
|
||||||
# Collect up to 3 document names (supports dicts or objects)
|
# Collect ALL document names (supports ChatDocument objects and dicts)
|
||||||
doc_names = []
|
doc_names = []
|
||||||
for d in docs[:3]:
|
for d in docs:
|
||||||
name = None
|
name = None
|
||||||
try:
|
try:
|
||||||
if isinstance(d, dict):
|
if isinstance(d, dict):
|
||||||
name = d.get('documentName') or d.get('name') or d.get('filename')
|
# For dict objects, try multiple possible field names
|
||||||
|
name = d.get('fileName') or d.get('documentName') or d.get('name') or d.get('filename')
|
||||||
else:
|
else:
|
||||||
name = getattr(d, 'documentName', None) or getattr(d, 'name', None) or getattr(d, 'filename', None)
|
# For ChatDocument objects, use fileName field
|
||||||
|
name = getattr(d, 'fileName', None) or getattr(d, 'documentName', None) or getattr(d, 'name', None) or getattr(d, 'filename', None)
|
||||||
except Exception:
|
except Exception:
|
||||||
name = None
|
name = None
|
||||||
doc_names.append(name or "(unnamed)")
|
doc_names.append(name or "(unnamed)")
|
||||||
names_part = ", ".join(doc_names) + (" +more" if document_count > 3 else "")
|
# Format document names in brackets
|
||||||
|
if doc_names:
|
||||||
|
names_part = f"({', '.join(doc_names)})"
|
||||||
|
else:
|
||||||
|
names_part = "(no documents)"
|
||||||
|
|
||||||
|
# Don't truncate the message - show full content
|
||||||
user_message = (getattr(msg, 'message', '') or '').strip().replace("\n", " ")
|
user_message = (getattr(msg, 'message', '') or '').strip().replace("\n", " ")
|
||||||
if len(user_message) > 120:
|
# Read success from ChatMessage.success field
|
||||||
user_message = user_message[:117] + "..."
|
|
||||||
success_flag = getattr(msg, 'success', None)
|
success_flag = getattr(msg, 'success', None)
|
||||||
success_text = "success=True" if success_flag is True else ("success=False" if success_flag is False else "success=Unknown")
|
success_text = "success=True" if success_flag is True else ("success=False" if success_flag is False else "success=Unknown")
|
||||||
label_part = f" label='{label}'" if label else ""
|
label_part = f" label='{label}'" if label else ""
|
||||||
return f"Round {round_number}: {document_count} docs - {names_part}{label_part} | {success_text} | msg='{user_message}'"
|
|
||||||
|
# Add learning/feedback if available
|
||||||
|
learning_part = ""
|
||||||
|
if hasattr(msg, 'summary') and msg.summary and 'learnings' in msg.summary.lower():
|
||||||
|
learning_part = " | learnings available"
|
||||||
|
|
||||||
|
return f"Round {round_number}: {document_count} docs {names_part}{label_part} | {success_text}{learning_part} | msg='{user_message}'"
|
||||||
except Exception:
|
except Exception:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
@ -171,17 +166,35 @@ def getMessageSummary(msg) -> str:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def getPreviousRoundContext(services, workflow: Any) -> str:
|
def getPreviousRoundContext(services, workflow: Any) -> str:
|
||||||
"""Get previous round context listing only messages that produced documents, using summaries (full history)."""
|
"""Get enriched context:
|
||||||
|
- Reverse-chronological ordering
|
||||||
|
- Current round first (newest → oldest), then older rounds
|
||||||
|
- Only messages with documents summarized
|
||||||
|
- Include available documents snapshot at end
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
if not workflow:
|
if not workflow:
|
||||||
return "No previous round context available"
|
return "No previous round context available"
|
||||||
|
|
||||||
lines: List[str] = []
|
lines: List[str] = []
|
||||||
|
|
||||||
# Summarize ALL messages WITH documents only, in chronological order
|
# Reverse-chronological, current round first
|
||||||
try:
|
try:
|
||||||
msgs = getattr(workflow, 'messages', []) or []
|
msgs = getattr(workflow, 'messages', []) or []
|
||||||
|
current_round = getattr(workflow, 'currentRound', None)
|
||||||
|
current_round_msgs: List[Any] = []
|
||||||
|
previous_round_msgs: List[Any] = []
|
||||||
for m in msgs:
|
for m in msgs:
|
||||||
|
if current_round is not None and getattr(m, 'roundNumber', None) == current_round:
|
||||||
|
current_round_msgs.append(m)
|
||||||
|
else:
|
||||||
|
previous_round_msgs.append(m)
|
||||||
|
|
||||||
|
for m in reversed(current_round_msgs):
|
||||||
|
s = getMessageSummary(m)
|
||||||
|
if s:
|
||||||
|
lines.append(f"- {s}")
|
||||||
|
for m in reversed(previous_round_msgs):
|
||||||
s = getMessageSummary(m)
|
s = getMessageSummary(m)
|
||||||
if s:
|
if s:
|
||||||
lines.append(f"- {s}")
|
lines.append(f"- {s}")
|
||||||
|
|
@ -222,7 +235,7 @@ def extractReviewContent(context: Any) -> str:
|
||||||
for doc in result.documents:
|
for doc in result.documents:
|
||||||
# Extract all available metadata without content
|
# Extract all available metadata without content
|
||||||
doc_metadata = {
|
doc_metadata = {
|
||||||
"name": getattr(doc, 'documentName', 'Unknown'),
|
"name": getattr(doc, 'fileName', None) or getattr(doc, 'documentName', 'Unknown'),
|
||||||
"mimeType": getattr(doc, 'mimeType', 'Unknown'),
|
"mimeType": getattr(doc, 'mimeType', 'Unknown'),
|
||||||
"size": getattr(doc, 'size', 'Unknown'),
|
"size": getattr(doc, 'size', 'Unknown'),
|
||||||
"created": getattr(doc, 'created', 'Unknown'),
|
"created": getattr(doc, 'created', 'Unknown'),
|
||||||
|
|
@ -358,12 +371,10 @@ def extractLatestRefinementFeedback(context: Any) -> str:
|
||||||
def extractAvailableDocumentsSummary(service: Any, context: Any) -> str:
|
def extractAvailableDocumentsSummary(service: Any, context: Any) -> str:
|
||||||
"""Summary of available documents (count only)."""
|
"""Summary of available documents (count only)."""
|
||||||
try:
|
try:
|
||||||
if hasattr(context, 'workflow') and context.workflow:
|
documents = service.workflow.getAvailableDocuments(context.workflow)
|
||||||
documents = service.workflow.getAvailableDocuments(context.workflow)
|
if documents and documents != "No documents available":
|
||||||
if documents and documents != "No documents available":
|
doc_count = documents.count("docList:") + documents.count("docItem:")
|
||||||
doc_count = documents.count("docList:") + documents.count("docItem:")
|
return f"{doc_count} documents available from previous tasks"
|
||||||
return f"{doc_count} documents available from previous tasks"
|
|
||||||
return "No documents available"
|
|
||||||
return "No documents available"
|
return "No documents available"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting document summary: {str(e)}")
|
logger.error(f"Error getting document summary: {str(e)}")
|
||||||
|
|
@ -372,9 +383,7 @@ def extractAvailableDocumentsSummary(service: Any, context: Any) -> str:
|
||||||
def extractAvailableDocumentsIndex(service: Any, context: Any) -> str:
|
def extractAvailableDocumentsIndex(service: Any, context: Any) -> str:
|
||||||
"""Index of available documents with detailed references for parameter generation."""
|
"""Index of available documents with detailed references for parameter generation."""
|
||||||
try:
|
try:
|
||||||
if hasattr(context, 'workflow') and context.workflow:
|
return service.workflow.getAvailableDocuments(context.workflow)
|
||||||
return service.workflow.getAvailableDocuments(context.workflow)
|
|
||||||
return "No documents available"
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting document index: {str(e)}")
|
logger.error(f"Error getting document index: {str(e)}")
|
||||||
return "No documents available"
|
return "No documents available"
|
||||||
|
|
@ -382,7 +391,7 @@ def extractAvailableDocumentsIndex(service: Any, context: Any) -> str:
|
||||||
def extractAvailableConnectionsSummary(service: Any) -> str:
|
def extractAvailableConnectionsSummary(service: Any) -> str:
|
||||||
"""Summary of available connections (count only)."""
|
"""Summary of available connections (count only)."""
|
||||||
try:
|
try:
|
||||||
connections = getConnectionReferenceList(service)
|
connections = service.workflow.getConnectionReferenceList()
|
||||||
if connections:
|
if connections:
|
||||||
return f"{len(connections)} connections available"
|
return f"{len(connections)} connections available"
|
||||||
return "No connections available"
|
return "No connections available"
|
||||||
|
|
@ -393,7 +402,7 @@ def extractAvailableConnectionsSummary(service: Any) -> str:
|
||||||
def extractAvailableConnectionsIndex(service: Any) -> str:
|
def extractAvailableConnectionsIndex(service: Any) -> str:
|
||||||
"""Index of available connections with detailed references for parameter generation."""
|
"""Index of available connections with detailed references for parameter generation."""
|
||||||
try:
|
try:
|
||||||
connections = getConnectionReferenceList(service)
|
connections = service.workflow.getConnectionReferenceList()
|
||||||
if connections:
|
if connections:
|
||||||
return '\n'.join(f"- {conn}" for conn in connections)
|
return '\n'.join(f"- {conn}" for conn in connections)
|
||||||
return "No connections available"
|
return "No connections available"
|
||||||
|
|
|
||||||
|
|
@ -25,41 +25,112 @@ def generateReactPlanSelectionPrompt(services, context: Any) -> PromptBundle:
|
||||||
PromptPlaceholder(label="USER_PROMPT", content=extractUserPrompt(context), summaryAllowed=False),
|
PromptPlaceholder(label="USER_PROMPT", content=extractUserPrompt(context), summaryAllowed=False),
|
||||||
PromptPlaceholder(label="AVAILABLE_DOCUMENTS_SUMMARY", content=extractAvailableDocumentsSummary(services, context), summaryAllowed=True),
|
PromptPlaceholder(label="AVAILABLE_DOCUMENTS_SUMMARY", content=extractAvailableDocumentsSummary(services, context), summaryAllowed=True),
|
||||||
PromptPlaceholder(label="AVAILABLE_METHODS", content=extractAvailableMethods(services), summaryAllowed=False),
|
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 one action to advance the task.
|
template = """Select exactly one action to advance the task.
|
||||||
|
|
||||||
OBJECTIVE:
|
OBJECTIVE:
|
||||||
{{KEY:USER_PROMPT}}
|
{{KEY:USER_PROMPT}}
|
||||||
|
|
||||||
AVAILABLE_DOCUMENTS_SUMMARY:
|
AVAILABLE_DOCUMENTS_SUMMARY:
|
||||||
{{KEY:AVAILABLE_DOCUMENTS_SUMMARY}}
|
{{KEY:AVAILABLE_DOCUMENTS_SUMMARY}}
|
||||||
|
|
||||||
AVAILABLE_METHODS:
|
AVAILABLE_METHODS:
|
||||||
{{KEY:AVAILABLE_METHODS}}
|
{{KEY:AVAILABLE_METHODS}}
|
||||||
|
|
||||||
REPLY: Return only a JSON object with the selected action:
|
WORKFLOW_HISTORY (reverse-chronological, enriched):
|
||||||
{{
|
{{KEY:WORKFLOW_HISTORY}}
|
||||||
"action": "method.action_name"
|
|
||||||
}}
|
|
||||||
|
|
||||||
RULES:
|
AVAILABLE_DOCUMENTS_INDEX:
|
||||||
1. Use EXACT action names from AVAILABLE_METHODS
|
{{KEY:AVAILABLE_DOCUMENTS_INDEX}}
|
||||||
2. Return ONLY JSON - no other text
|
|
||||||
3. Do NOT use markdown code blocks
|
AVAILABLE_CONNECTIONS_INDEX:
|
||||||
4. Do NOT add explanations
|
{{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)
|
return PromptBundle(prompt=template, placeholders=placeholders)
|
||||||
|
|
||||||
def generateReactParametersPrompt(services, context: Any, compoundActionName: str) -> PromptBundle:
|
def generateReactParametersPrompt(services, context: Any, compoundActionName: str) -> PromptBundle:
|
||||||
"""Define placeholders first, then the template; return 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
|
# derive method/action and parameter list
|
||||||
methodName, actionName = (compoundActionName.split('.', 1) if '.' in compoundActionName else (compoundActionName, ''))
|
methodName, actionName = (compoundActionName.split('.', 1) if '.' in compoundActionName else (compoundActionName, ''))
|
||||||
actionParameterList = getActionParameterList(methodName, actionName, methods)
|
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
|
# determine action objective if available, else fall back to user prompt
|
||||||
actionObjective = None
|
|
||||||
if hasattr(context, 'action_objective') and context.action_objective:
|
if hasattr(context, 'action_objective') and context.action_objective:
|
||||||
actionObjective = context.action_objective
|
actionObjective = context.action_objective
|
||||||
elif hasattr(context, 'task_step') and context.task_step and getattr(context.task_step, 'objective', None):
|
elif hasattr(context, 'task_step') and context.task_step and getattr(context.task_step, 'objective', None):
|
||||||
|
|
@ -67,107 +138,62 @@ def generateReactParametersPrompt(services, context: Any, compoundActionName: st
|
||||||
else:
|
else:
|
||||||
actionObjective = extractUserPrompt(context)
|
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] = [
|
placeholders: List[PromptPlaceholder] = [
|
||||||
PromptPlaceholder(label="ACTION_OBJECTIVE", content=actionObjective, summaryAllowed=False),
|
PromptPlaceholder(label="ACTION_OBJECTIVE", content=actionObjective, summaryAllowed=False),
|
||||||
PromptPlaceholder(label="ACTION_PARAMETER_LIST", content=actionParameterList, summaryAllowed=False),
|
|
||||||
PromptPlaceholder(label="AVAILABLE_DOCUMENTS_INDEX", content=extractAvailableDocumentsIndex(services, context), summaryAllowed=True),
|
|
||||||
PromptPlaceholder(label="AVAILABLE_CONNECTIONS_INDEX", content=extractAvailableConnectionsIndex(services), summaryAllowed=False),
|
|
||||||
PromptPlaceholder(label="USER_PROMPT", content=extractUserPrompt(context), summaryAllowed=False),
|
|
||||||
PromptPlaceholder(label="USER_LANGUAGE", content=extractUserLanguage(services), summaryAllowed=False),
|
|
||||||
PromptPlaceholder(label="PREVIOUS_ACTION_RESULTS", content=extractPreviousActionResults(context), summaryAllowed=True),
|
|
||||||
PromptPlaceholder(label="LEARNINGS_AND_IMPROVEMENTS", content=extractLearningsAndImprovements(context), summaryAllowed=True),
|
|
||||||
PromptPlaceholder(label="LATEST_REFINEMENT_FEEDBACK", content=extractLatestRefinementFeedback(context), summaryAllowed=True),
|
|
||||||
PromptPlaceholder(label="WORKFLOW_HISTORY", content=extractWorkflowHistory(services, context), summaryAllowed=True),
|
|
||||||
PromptPlaceholder(label="SELECTED_ACTION", content=compoundActionName, 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 = """Generate parameters for this action.
|
template = """You are a parameter generator. Set the parameters for this specific action.
|
||||||
|
|
||||||
## Return ONLY a JSON RESPONSEOBJECT without comments.
|
|
||||||
|
|
||||||
ACTION_OBJECTIVE (the objective for this action to fulfill):
|
|
||||||
{{KEY:ACTION_OBJECTIVE}}
|
|
||||||
|
|
||||||
SELECTED_ACTION:
|
|
||||||
{{KEY:SELECTED_ACTION}}
|
|
||||||
|
|
||||||
JSON RESPONSEOBJECT:
|
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",
|
"schema": "parameters_v1",
|
||||||
"parameters": {{
|
"parameters": {{
|
||||||
"paramName": "value"
|
"paramName": "value"
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
EXAMPLE of the result format to deliver:
|
RULES:
|
||||||
{{
|
- Return ONLY JSON (no markdown, no prose)
|
||||||
"schema": "parameters_v1",
|
- Use only the parameters listed in REQUIRED PARAMETERS FOR THIS ACTION
|
||||||
"parameters": {{
|
"""
|
||||||
"aiPrompt": "...",
|
|
||||||
"resultType": "docx",
|
|
||||||
"processingMode": "detailed"
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
## RULES:
|
|
||||||
1. Use ONLY parameter names from ACTION_PARAMETER_LIST
|
|
||||||
2. For connectionReference, use an EXACT label from AVAILABLE_CONNECTIONS_INDEX (do NOT invent labels)
|
|
||||||
3. Use exact document references from AVAILABLE_DOCUMENTS_INDEX for documentList parameters (do NOT invent names like "doc1"): pick specific docItem references; to include all from a list, use its docList reference
|
|
||||||
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
|
|
||||||
|
|
||||||
## ACTION_PARAMETER_LIST:
|
|
||||||
|
|
||||||
{{KEY:ACTION_PARAMETER_LIST}}
|
|
||||||
|
|
||||||
|
|
||||||
## AVAILABLE_DOCUMENTS_INDEX:
|
|
||||||
|
|
||||||
(Use these references in parameter "documentList" if given; to include all docs from a list, pass its docList reference)
|
|
||||||
{{KEY:AVAILABLE_DOCUMENTS_INDEX}}
|
|
||||||
|
|
||||||
|
|
||||||
## AVAILABLE_CONNECTIONS_INDEX:
|
|
||||||
|
|
||||||
{{KEY:AVAILABLE_CONNECTIONS_INDEX}}
|
|
||||||
(Use an EXACT label here for parameter "connectionReference")
|
|
||||||
|
|
||||||
|
|
||||||
## Example how to assign references from AVAILABLE_DOCUMENTS_INDEX and AVAILABLE_CONNECTIONS_INDEX:
|
|
||||||
|
|
||||||
{{
|
|
||||||
"schema": "parameters_v1",
|
|
||||||
"parameters": {{
|
|
||||||
"documentList": ["docList:msg_47a7a578-e8f2-4ba8-ac66-0dbff40605e0:round8_task1_action1_results", "docItem:5d8b7aee-b546-4487-b6a8-835c86f7b186:AI_Generated_Document_20251006-104256.docx"],
|
|
||||||
"connectionReference": "conn_msft_1ae8b8e5-128b-49b8-b1cb-7c632669eeae",
|
|
||||||
"aiPrompt": "...",
|
|
||||||
"resultType": "xlsx",
|
|
||||||
"processingMode": "basic"
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
## CONTEXT
|
|
||||||
|
|
||||||
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}}
|
|
||||||
|
|
||||||
WORKFLOW_HISTORY:
|
|
||||||
{{KEY:WORKFLOW_HISTORY}}
|
|
||||||
"""
|
|
||||||
|
|
||||||
return PromptBundle(prompt=template, placeholders=placeholders)
|
return PromptBundle(prompt=template, placeholders=placeholders)
|
||||||
|
|
||||||
|
|
@ -180,24 +206,24 @@ def generateReactRefinementPrompt(services, context: Any, reviewContent: str) ->
|
||||||
|
|
||||||
template = """Decide the next step based on the observation.
|
template = """Decide the next step based on the observation.
|
||||||
|
|
||||||
OBJECTIVE:
|
OBJECTIVE:
|
||||||
{{KEY:USER_PROMPT}}
|
{{KEY:USER_PROMPT}}
|
||||||
|
|
||||||
OBSERVATION:
|
OBSERVATION:
|
||||||
{{KEY:REVIEW_CONTENT}}
|
{{KEY:REVIEW_CONTENT}}
|
||||||
|
|
||||||
REPLY: Return only a JSON object with your decision:
|
REPLY: Return only a JSON object with your decision:
|
||||||
{{
|
{{
|
||||||
"decision": "continue|stop",
|
"decision": "continue|stop",
|
||||||
"reason": "brief explanation"
|
"reason": "brief explanation"
|
||||||
}}
|
}}
|
||||||
|
|
||||||
RULES:
|
RULES:
|
||||||
1. Use "continue" if objective NOT fulfilled
|
1. Use "continue" if objective NOT fulfilled
|
||||||
2. Use "stop" if objective fulfilled
|
2. Use "stop" if objective fulfilled
|
||||||
3. Return ONLY JSON - no other text
|
3. Return ONLY JSON - no other text
|
||||||
4. Do NOT use markdown code blocks
|
4. Do NOT use markdown code blocks
|
||||||
5. Do NOT add explanations
|
5. Do NOT add explanations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return PromptBundle(prompt=template, placeholders=placeholders)
|
return PromptBundle(prompt=template, placeholders=placeholders)
|
||||||
|
|
|
||||||
|
|
@ -26,96 +26,96 @@ def generateTaskPlanningPrompt(services, context: Any) -> PromptBundle:
|
||||||
|
|
||||||
template = """# Task Planning
|
template = """# Task Planning
|
||||||
|
|
||||||
Break down user requests into logical, executable task steps.
|
Break down user requests into logical, executable task steps.
|
||||||
|
|
||||||
## 📋 Context
|
## 📋 Context
|
||||||
|
|
||||||
### User Request
|
### User Request
|
||||||
{{KEY:USER_PROMPT}}
|
{{KEY:USER_PROMPT}}
|
||||||
|
|
||||||
### Available Documents
|
### Available Documents
|
||||||
{{KEY:AVAILABLE_DOCUMENTS_SUMMARY}}
|
{{KEY:AVAILABLE_DOCUMENTS_SUMMARY}}
|
||||||
|
|
||||||
### Previous Workflow Rounds
|
### Previous Workflow Rounds
|
||||||
{{KEY:WORKFLOW_HISTORY}}
|
{{KEY:WORKFLOW_HISTORY}}
|
||||||
|
|
||||||
## 📝 Task Planning Rules
|
## 📝 Task Planning Rules
|
||||||
|
|
||||||
### Strategic Task Grouping
|
### Strategic Task Grouping
|
||||||
- **GROUP RELATED ACTIONS** - Combine all actions for the same business topic into ONE task
|
- **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
|
- **ONE TOPIC PER TASK** - Each task should handle one complete business objective
|
||||||
- **HIGH-LEVEL FOCUS** - Plan strategic outcomes, not implementation steps
|
- **HIGH-LEVEL FOCUS** - Plan strategic outcomes, not implementation steps
|
||||||
- **AVOID MICRO-TASKS** - Don't create separate tasks for each small action
|
- **AVOID MICRO-TASKS** - Don't create separate tasks for each small action
|
||||||
|
|
||||||
### Task Grouping Examples
|
### Task Grouping Examples
|
||||||
- **Research + Analysis + Report** → ONE task: "Web research report"
|
- **Research + Analysis + Report** → ONE task: "Web research report"
|
||||||
- **Data Collection + Processing + Visualization** → ONE task: "Collect and present data"
|
- **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"
|
- **Different topics** (email + flowers) → SEPARATE tasks: "Send formal email..." + "Order flowers from Fleurop for delivery to 123 Main St, include card message"
|
||||||
|
|
||||||
### Retry Handling
|
### Retry Handling
|
||||||
- **If retry request**: Analyze previous rounds to understand what failed
|
- **If retry request**: Analyze previous rounds to understand what failed
|
||||||
- **Learn from mistakes**: Improve the plan based on previous failures
|
- **Learn from mistakes**: Improve the plan based on previous failures
|
||||||
|
|
||||||
## 📊 Required JSON Structure
|
## 📊 Required JSON Structure
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"overview": "Brief description of the overall plan",
|
"overview": "Brief description of the overall plan",
|
||||||
"languageUserDetected": "en",
|
"languageUserDetected": "en",
|
||||||
"userMessage": "User-friendly message explaining the task plan",
|
"userMessage": "User-friendly message explaining the task plan",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"id": "task_1",
|
"id": "task_1",
|
||||||
"objective": "Clear business objective focusing on what to deliver",
|
"objective": "Clear business objective focusing on what to deliver",
|
||||||
"dependencies": ["task_0"],
|
"dependencies": ["task_0"],
|
||||||
"success_criteria": ["measurable criteria 1", "measurable criteria 2"],
|
"success_criteria": ["measurable criteria 1", "measurable criteria 2"],
|
||||||
"estimated_complexity": "low|medium|high",
|
"estimated_complexity": "low|medium|high",
|
||||||
"userMessage": "What this task will accomplish"
|
"userMessage": "What this task will accomplish"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎯 Task Structure Guidelines
|
## 🎯 Task Structure Guidelines
|
||||||
|
|
||||||
### Task ID Format
|
### Task ID Format
|
||||||
- Use sequential numbering: `task_1`, `task_2`, `task_3`
|
- Use sequential numbering: `task_1`, `task_2`, `task_3`
|
||||||
- Keep IDs simple and clear
|
- Keep IDs simple and clear
|
||||||
|
|
||||||
### Objective Writing
|
### Objective Writing
|
||||||
- **Be VERY SPECIFIC** - Include exact details needed for action planning
|
- **Be VERY SPECIFIC** - Include exact details needed for action planning
|
||||||
- **Include all requirements** - recipient, attachments, format, recipients, etc.
|
- **Include all requirements** - recipient, attachments, format, recipients, etc.
|
||||||
- **State the complete deliverable** - What exactly will be produced
|
- **State the complete deliverable** - What exactly will be produced
|
||||||
- **Include context and constraints** - When, where, how, with what
|
- **Include context and constraints** - When, where, how, with what
|
||||||
- **Make it actionable** - Clear enough to plan specific actions
|
- **Make it actionable** - Clear enough to plan specific actions
|
||||||
|
|
||||||
### Specific Objective Examples
|
### Specific Objective Examples
|
||||||
- **Good**: "Send formal email to ceo and board of directors with annual report as attachment"
|
- **Good**: "Send formal email to ceo and board of directors with annual report as attachment"
|
||||||
- **Bad**: "Handle email communication"
|
- **Bad**: "Handle email communication"
|
||||||
- **Good**: "Order flowers from Fleurop for delivery to 123 Main St, include card message 'Happy Birthday', deliver on March 15th"
|
- **Good**: "Order flowers from Fleurop for delivery to 123 Main St, include card message 'Happy Birthday', deliver on March 15th"
|
||||||
- **Bad**: "Order flowers"
|
- **Bad**: "Order flowers"
|
||||||
|
|
||||||
### Action Planning Requirements
|
### Action Planning Requirements
|
||||||
- **Include all necessary details** - The objective must contain everything needed to plan actions
|
- **Include all necessary details** - The objective must contain everything needed to plan actions
|
||||||
- **Specify recipients and destinations** - Who should receive what
|
- **Specify recipients and destinations** - Who should receive what
|
||||||
- **Include file names and formats** - What documents to use/create
|
- **Include file names and formats** - What documents to use/create
|
||||||
- **State timing and deadlines** - When things need to be done
|
- **State timing and deadlines** - When things need to be done
|
||||||
- **Include context and constraints** - Any special requirements or limitations
|
- **Include context and constraints** - Any special requirements or limitations
|
||||||
|
|
||||||
### Success Criteria
|
### Success Criteria
|
||||||
- **Make them measurable** - specific, quantifiable outcomes
|
- **Make them measurable** - specific, quantifiable outcomes
|
||||||
- **Focus on deliverables** - what the user will receive
|
- **Focus on deliverables** - what the user will receive
|
||||||
- **Keep criteria realistic** - achievable within the task scope
|
- **Keep criteria realistic** - achievable within the task scope
|
||||||
- **Include all related actions** - success means completing the entire business objective
|
- **Include all related actions** - success means completing the entire business objective
|
||||||
- **Be specific about requirements** - Include exact details like recipients, formats, deadlines
|
- **Be specific about requirements** - Include exact details like recipients, formats, deadlines
|
||||||
- **State clear completion criteria** - How to know the task is fully done
|
- **State clear completion criteria** - How to know the task is fully done
|
||||||
|
|
||||||
### Complexity Estimation
|
### Complexity Estimation
|
||||||
- **Low**: Simple, single-action tasks (1-2 actions)
|
- **Low**: Simple, single-action tasks (1-2 actions)
|
||||||
- **Medium**: Multi-action tasks for one topic (3-5 actions)
|
- **Medium**: Multi-action tasks for one topic (3-5 actions)
|
||||||
- **High**: Complex strategic tasks (6+ actions)
|
- **High**: Complex strategic tasks (6+ actions)
|
||||||
|
|
||||||
## 🚀 Response Format
|
## 🚀 Response Format
|
||||||
Return ONLY the JSON object."""
|
Return ONLY the JSON object."""
|
||||||
|
|
||||||
return PromptBundle(prompt=template, placeholders=placeholders)
|
return PromptBundle(prompt=template, placeholders=placeholders)
|
||||||
|
|
|
||||||
|
|
@ -524,7 +524,7 @@ class WorkflowManager:
|
||||||
# Add failed log entry
|
# Add failed log entry
|
||||||
self.services.workflow.createLog({
|
self.services.workflow.createLog({
|
||||||
"workflowId": workflow.id,
|
"workflowId": workflow.id,
|
||||||
"message": f"Workflow failed: {workflow_result.error or 'Unknown error'}",
|
"message": "Workflow failed: Unknown error",
|
||||||
"type": "error",
|
"type": "error",
|
||||||
"status": "failed",
|
"status": "failed",
|
||||||
"progress": 100
|
"progress": 100
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue