@aj-archipelago/cortex 1.4.2 → 1.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/config.js +1 -1
- package/helper-apps/cortex-autogen2/.dockerignore +1 -0
- package/helper-apps/cortex-autogen2/Dockerfile +6 -10
- package/helper-apps/cortex-autogen2/Dockerfile.worker +2 -0
- package/helper-apps/cortex-autogen2/agents.py +203 -2
- package/helper-apps/cortex-autogen2/main.py +1 -1
- package/helper-apps/cortex-autogen2/pyproject.toml +12 -0
- package/helper-apps/cortex-autogen2/requirements.txt +14 -0
- package/helper-apps/cortex-autogen2/services/redis_publisher.py +1 -1
- package/helper-apps/cortex-autogen2/services/run_analyzer.py +1 -1
- package/helper-apps/cortex-autogen2/task_processor.py +431 -229
- package/helper-apps/cortex-autogen2/test_entity_fetcher.py +305 -0
- package/helper-apps/cortex-autogen2/tests/README.md +240 -0
- package/helper-apps/cortex-autogen2/tests/TEST_REPORT.md +342 -0
- package/helper-apps/cortex-autogen2/tests/__init__.py +8 -0
- package/helper-apps/cortex-autogen2/tests/analysis/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/tests/analysis/improvement_suggester.py +224 -0
- package/helper-apps/cortex-autogen2/tests/analysis/trend_analyzer.py +211 -0
- package/helper-apps/cortex-autogen2/tests/cli/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/tests/cli/run_tests.py +296 -0
- package/helper-apps/cortex-autogen2/tests/collectors/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/tests/collectors/log_collector.py +252 -0
- package/helper-apps/cortex-autogen2/tests/collectors/progress_collector.py +182 -0
- package/helper-apps/cortex-autogen2/tests/conftest.py +15 -0
- package/helper-apps/cortex-autogen2/tests/database/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/tests/database/repository.py +501 -0
- package/helper-apps/cortex-autogen2/tests/database/schema.sql +108 -0
- package/helper-apps/cortex-autogen2/tests/evaluators/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/tests/evaluators/llm_scorer.py +294 -0
- package/helper-apps/cortex-autogen2/tests/evaluators/prompts.py +250 -0
- package/helper-apps/cortex-autogen2/tests/evaluators/wordcloud_validator.py +168 -0
- package/helper-apps/cortex-autogen2/tests/metrics/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/tests/metrics/collector.py +155 -0
- package/helper-apps/cortex-autogen2/tests/orchestrator.py +576 -0
- package/helper-apps/cortex-autogen2/tests/test_cases.yaml +279 -0
- package/helper-apps/cortex-autogen2/tests/test_data.db +0 -0
- package/helper-apps/cortex-autogen2/tests/utils/__init__.py +3 -0
- package/helper-apps/cortex-autogen2/tests/utils/connectivity.py +112 -0
- package/helper-apps/cortex-autogen2/tools/azure_blob_tools.py +74 -24
- package/helper-apps/cortex-autogen2/tools/entity_api_registry.json +38 -0
- package/helper-apps/cortex-autogen2/tools/file_tools.py +1 -1
- package/helper-apps/cortex-autogen2/tools/search_tools.py +436 -238
- package/helper-apps/cortex-file-handler/package-lock.json +2 -2
- package/helper-apps/cortex-file-handler/package.json +1 -1
- package/helper-apps/cortex-file-handler/scripts/setup-test-containers.js +4 -5
- package/helper-apps/cortex-file-handler/src/blobHandler.js +36 -144
- package/helper-apps/cortex-file-handler/src/services/FileConversionService.js +5 -3
- package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +34 -1
- package/helper-apps/cortex-file-handler/src/services/storage/GCSStorageProvider.js +22 -0
- package/helper-apps/cortex-file-handler/src/services/storage/LocalStorageProvider.js +28 -1
- package/helper-apps/cortex-file-handler/src/services/storage/StorageFactory.js +29 -4
- package/helper-apps/cortex-file-handler/src/services/storage/StorageProvider.js +11 -0
- package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +1 -1
- package/helper-apps/cortex-file-handler/tests/blobHandler.test.js +3 -2
- package/helper-apps/cortex-file-handler/tests/checkHashShortLived.test.js +8 -1
- package/helper-apps/cortex-file-handler/tests/containerConversionFlow.test.js +5 -2
- package/helper-apps/cortex-file-handler/tests/containerNameParsing.test.js +14 -7
- package/helper-apps/cortex-file-handler/tests/containerParameterFlow.test.js +5 -2
- package/helper-apps/cortex-file-handler/tests/storage/StorageFactory.test.js +31 -19
- package/lib/entityConstants.js +3 -0
- package/package.json +2 -2
- package/pathways/system/entity/sys_entity_agent.js +2 -1
- package/pathways/system/entity/tools/sys_tool_codingagent.js +2 -2
- package/pathways/system/workspaces/workspace_applet_edit.js +551 -29
- package/server/modelExecutor.js +4 -0
- package/server/plugins/claude4VertexPlugin.js +540 -0
- package/server/plugins/openAiWhisperPlugin.js +43 -2
- package/tests/integration/rest/vendors/claude_streaming.test.js +121 -0
- package/tests/unit/plugins/claude4VertexPlugin.test.js +462 -0
- package/tests/unit/plugins/claude4VertexToolConversion.test.js +413 -0
- package/helper-apps/cortex-autogen/.funcignore +0 -8
- package/helper-apps/cortex-autogen/Dockerfile +0 -10
- package/helper-apps/cortex-autogen/OAI_CONFIG_LIST +0 -6
- package/helper-apps/cortex-autogen/agents.py +0 -493
- package/helper-apps/cortex-autogen/agents_extra.py +0 -14
- package/helper-apps/cortex-autogen/config.py +0 -18
- package/helper-apps/cortex-autogen/data_operations.py +0 -29
- package/helper-apps/cortex-autogen/function_app.py +0 -44
- package/helper-apps/cortex-autogen/host.json +0 -15
- package/helper-apps/cortex-autogen/main.py +0 -38
- package/helper-apps/cortex-autogen/prompts.py +0 -196
- package/helper-apps/cortex-autogen/prompts_extra.py +0 -5
- package/helper-apps/cortex-autogen/requirements.txt +0 -9
- package/helper-apps/cortex-autogen/search.py +0 -85
- package/helper-apps/cortex-autogen/test.sh +0 -40
- package/helper-apps/cortex-autogen/tools/sasfileuploader.py +0 -66
- package/helper-apps/cortex-autogen/utils.py +0 -88
- package/helper-apps/cortex-autogen2/DigiCertGlobalRootCA.crt.pem +0 -22
- package/helper-apps/cortex-autogen2/poetry.lock +0 -3652
- package/testrun.log +0 -35371
|
@@ -3,9 +3,9 @@ import json
|
|
|
3
3
|
import base64
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
|
-
from typing import Optional, Dict, Any, List
|
|
6
|
+
from typing import Optional, Dict, Any, List, Tuple, Union
|
|
7
7
|
from autogen_ext.models.openai import OpenAIChatCompletionClient
|
|
8
|
-
from autogen_core.models import ModelInfo
|
|
8
|
+
from autogen_core.models import ModelInfo, UserMessage, AssistantMessage, SystemMessage
|
|
9
9
|
from autogen_agentchat.teams import SelectorGroupChat
|
|
10
10
|
from autogen_core.models import UserMessage
|
|
11
11
|
from autogen_agentchat.conditions import TextMentionTermination, HandoffTermination
|
|
@@ -26,6 +26,167 @@ from tools.azure_blob_tools import upload_file_to_azure_blob
|
|
|
26
26
|
logger = logging.getLogger(__name__)
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def _message_to_dict(msg: Any) -> Optional[Dict[str, Any]]:
|
|
30
|
+
"""Best-effort conversion of chat message objects to a plain dict."""
|
|
31
|
+
if isinstance(msg, dict):
|
|
32
|
+
return dict(msg)
|
|
33
|
+
|
|
34
|
+
for attr in ("model_dump", "dict", "to_dict", "as_dict"):
|
|
35
|
+
if hasattr(msg, attr):
|
|
36
|
+
try:
|
|
37
|
+
candidate = getattr(msg, attr)()
|
|
38
|
+
if isinstance(candidate, dict):
|
|
39
|
+
return dict(candidate)
|
|
40
|
+
except TypeError:
|
|
41
|
+
try:
|
|
42
|
+
candidate = getattr(msg, attr)(exclude_none=False)
|
|
43
|
+
if isinstance(candidate, dict):
|
|
44
|
+
return dict(candidate)
|
|
45
|
+
except Exception:
|
|
46
|
+
continue
|
|
47
|
+
except Exception:
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
if hasattr(msg, "__dict__"):
|
|
51
|
+
try:
|
|
52
|
+
return {k: v for k, v in vars(msg).items() if not k.startswith("__")}
|
|
53
|
+
except Exception:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class RoleFixingModelClientWrapper:
|
|
60
|
+
"""Wraps an OpenAI model client to fix agent message roles before API calls."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, wrapped_client: OpenAIChatCompletionClient):
|
|
63
|
+
self.wrapped_client = wrapped_client
|
|
64
|
+
|
|
65
|
+
async def create(self, messages=None, **kwargs):
|
|
66
|
+
"""Intercept create calls to fix message roles before sending to API."""
|
|
67
|
+
if messages:
|
|
68
|
+
normalized_messages: List[Dict[str, Any]] = []
|
|
69
|
+
first_user_seen = False
|
|
70
|
+
for raw_msg in messages:
|
|
71
|
+
normalized, first_user_seen = _normalize_single_message(raw_msg, first_user_seen)
|
|
72
|
+
normalized_messages.append(normalized)
|
|
73
|
+
messages = normalized_messages
|
|
74
|
+
return await self.wrapped_client.create(messages=messages, **kwargs)
|
|
75
|
+
|
|
76
|
+
def __getattr__(self, name):
|
|
77
|
+
"""Delegate all other attributes/methods to wrapped client."""
|
|
78
|
+
return getattr(self.wrapped_client, name)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _stringify_content(content: Any) -> str:
|
|
82
|
+
import json
|
|
83
|
+
|
|
84
|
+
if content is None:
|
|
85
|
+
return ""
|
|
86
|
+
|
|
87
|
+
if isinstance(content, str):
|
|
88
|
+
return content
|
|
89
|
+
|
|
90
|
+
if isinstance(content, list):
|
|
91
|
+
parts: List[str] = []
|
|
92
|
+
for item in content:
|
|
93
|
+
if isinstance(item, dict) and item.get("type") == "text":
|
|
94
|
+
parts.append(str(item.get("text", "")))
|
|
95
|
+
elif isinstance(item, dict):
|
|
96
|
+
try:
|
|
97
|
+
parts.append(json.dumps(item, ensure_ascii=False))
|
|
98
|
+
except Exception:
|
|
99
|
+
parts.append(str(item))
|
|
100
|
+
else:
|
|
101
|
+
parts.append(str(item))
|
|
102
|
+
return "\n".join(parts)
|
|
103
|
+
|
|
104
|
+
if isinstance(content, dict):
|
|
105
|
+
try:
|
|
106
|
+
return json.dumps(content, ensure_ascii=False)
|
|
107
|
+
except Exception:
|
|
108
|
+
return str(content)
|
|
109
|
+
|
|
110
|
+
return str(content)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _wrap_json_if_needed(text: str) -> str:
|
|
114
|
+
import json
|
|
115
|
+
|
|
116
|
+
if not isinstance(text, str):
|
|
117
|
+
text = str(text)
|
|
118
|
+
|
|
119
|
+
stripped = text.strip()
|
|
120
|
+
if stripped.startswith("```"):
|
|
121
|
+
return text
|
|
122
|
+
|
|
123
|
+
looks_like_json = False
|
|
124
|
+
if (stripped.startswith("{") and stripped.endswith("}")) or (
|
|
125
|
+
stripped.startswith("[") and stripped.endswith("]")
|
|
126
|
+
):
|
|
127
|
+
try:
|
|
128
|
+
json.loads(stripped)
|
|
129
|
+
looks_like_json = True
|
|
130
|
+
except Exception:
|
|
131
|
+
looks_like_json = False
|
|
132
|
+
|
|
133
|
+
if looks_like_json:
|
|
134
|
+
return f"```json\n{stripped}\n```"
|
|
135
|
+
|
|
136
|
+
return text
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _normalize_single_message(raw_message: Any, first_user_seen: bool) -> Tuple[Dict[str, Any], bool]:
|
|
140
|
+
import json
|
|
141
|
+
|
|
142
|
+
msg = _message_to_dict(raw_message) or {}
|
|
143
|
+
|
|
144
|
+
# Determine role
|
|
145
|
+
if msg.get("name"):
|
|
146
|
+
msg["role"] = "assistant"
|
|
147
|
+
elif not msg.get("role"):
|
|
148
|
+
if not first_user_seen:
|
|
149
|
+
msg["role"] = "user"
|
|
150
|
+
first_user_seen = True
|
|
151
|
+
else:
|
|
152
|
+
msg["role"] = "assistant"
|
|
153
|
+
elif msg.get("role") == "user" and first_user_seen:
|
|
154
|
+
msg["role"] = "assistant"
|
|
155
|
+
elif msg.get("role") == "user" and not first_user_seen:
|
|
156
|
+
first_user_seen = True
|
|
157
|
+
elif msg.get("role") not in {"assistant", "system"}:
|
|
158
|
+
msg["role"] = "assistant"
|
|
159
|
+
|
|
160
|
+
role = msg.get("role", "assistant")
|
|
161
|
+
name = msg.get("name") or msg.get("source") or ("user" if role == "user" else "assistant")
|
|
162
|
+
|
|
163
|
+
base_content = _stringify_content(msg.get("content"))
|
|
164
|
+
|
|
165
|
+
tool_calls = msg.get("tool_calls") if isinstance(msg.get("tool_calls"), list) else None
|
|
166
|
+
if tool_calls:
|
|
167
|
+
role = "assistant"
|
|
168
|
+
try:
|
|
169
|
+
tool_json = json.dumps(tool_calls, ensure_ascii=False)
|
|
170
|
+
except Exception:
|
|
171
|
+
tool_json = str(tool_calls)
|
|
172
|
+
tool_text = _wrap_json_if_needed(tool_json)
|
|
173
|
+
if base_content:
|
|
174
|
+
base_content = f"{base_content}\n\nTool calls:\n{tool_text}"
|
|
175
|
+
else:
|
|
176
|
+
base_content = f"Tool calls:\n{tool_text}"
|
|
177
|
+
|
|
178
|
+
content_text = _wrap_json_if_needed(base_content) if role != "system" else base_content
|
|
179
|
+
|
|
180
|
+
if role == "system":
|
|
181
|
+
message_obj = SystemMessage(content=content_text)
|
|
182
|
+
elif role == "user":
|
|
183
|
+
message_obj = UserMessage(content=content_text, source=str(name))
|
|
184
|
+
else:
|
|
185
|
+
message_obj = AssistantMessage(content=content_text, source=str(name))
|
|
186
|
+
|
|
187
|
+
return message_obj, first_user_seen
|
|
188
|
+
|
|
189
|
+
|
|
29
190
|
class TaskProcessor:
|
|
30
191
|
"""
|
|
31
192
|
Core task processing logic that can be used by both worker and Azure Function App.
|
|
@@ -163,51 +324,89 @@ class TaskProcessor:
|
|
|
163
324
|
if not cleaned_content:
|
|
164
325
|
return None
|
|
165
326
|
|
|
166
|
-
prompt = f"""
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
- "
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
- "
|
|
207
|
-
- "
|
|
208
|
-
- "
|
|
209
|
-
|
|
210
|
-
|
|
327
|
+
prompt = f"""Create a professional progress update (8-12 words) showing expert work in action. User is watching a skilled professional handle their task.
|
|
328
|
+
|
|
329
|
+
Activity: {cleaned_content}
|
|
330
|
+
Role: {source if source else "Unknown"}
|
|
331
|
+
|
|
332
|
+
═══════════════════════════════════════════════════════════════════
|
|
333
|
+
🎯 CORE PRINCIPLES
|
|
334
|
+
═══════════════════════════════════════════════════════════════════
|
|
335
|
+
|
|
336
|
+
1. **SHOW CRAFT, NOT OUTCOME** - User watches expertise, not receives results
|
|
337
|
+
❌ "Report ready for download"
|
|
338
|
+
✅ "Compiling insights into executive summary"
|
|
339
|
+
|
|
340
|
+
2. **PRESENT CONTINUOUS** - Always -ing verbs (happening right now)
|
|
341
|
+
✅ "Analyzing... Designing... Building... Processing..."
|
|
342
|
+
|
|
343
|
+
3. **NEVER ADDRESS USER** - No "you/your", no "for you", no promises
|
|
344
|
+
❌ "Gathering images for your presentation"
|
|
345
|
+
❌ "Preparing your report"
|
|
346
|
+
❌ "Finding what you need"
|
|
347
|
+
✅ "Assembling presentation materials"
|
|
348
|
+
|
|
349
|
+
4. **PROFESSIONAL BUSINESS TONE** - Confident expert, not friendly helper
|
|
350
|
+
✅ "Processing financial data across quarterly reports"
|
|
351
|
+
❌ "Crunching numbers to find cool insights!"
|
|
352
|
+
|
|
353
|
+
5. **SPECIFIC = CREDIBLE** - What exactly is happening?
|
|
354
|
+
✅ "Structuring analysis across 6 data dimensions"
|
|
355
|
+
❌ "Processing information"
|
|
356
|
+
|
|
357
|
+
═══════════════════════════════════════════════════════════════════
|
|
358
|
+
📋 EMOJI + PATTERNS
|
|
359
|
+
═══════════════════════════════════════════════════════════════════
|
|
360
|
+
|
|
361
|
+
🧭 Planning/Strategy:
|
|
362
|
+
- "Architecting multi-phase analysis framework"
|
|
363
|
+
- "Structuring comprehensive research methodology"
|
|
364
|
+
- "Mapping data relationships across sources"
|
|
365
|
+
|
|
366
|
+
📊 Data/Analysis:
|
|
367
|
+
- "Processing statistical patterns in time-series data"
|
|
368
|
+
- "Analyzing trends across historical datasets"
|
|
369
|
+
- "Computing correlations between key metrics"
|
|
370
|
+
|
|
371
|
+
🖼️ Images/Media:
|
|
372
|
+
- "Sourcing high-resolution assets from verified collections"
|
|
373
|
+
- "Curating professional imagery meeting brand standards"
|
|
374
|
+
- "Selecting licensed graphics from premium libraries"
|
|
375
|
+
|
|
376
|
+
✨ Creating/Designing:
|
|
377
|
+
- "Designing presentation with executive-level polish"
|
|
378
|
+
- "Building interactive visualizations from raw data"
|
|
379
|
+
- "Crafting report layout with professional typography"
|
|
380
|
+
|
|
381
|
+
📝 Writing/Content:
|
|
382
|
+
- "Synthesizing findings into coherent narrative"
|
|
383
|
+
- "Structuring content with logical flow"
|
|
384
|
+
- "Composing analysis with supporting evidence"
|
|
385
|
+
|
|
386
|
+
🔍 Research/Search:
|
|
387
|
+
- "Scanning authoritative sources for verified information"
|
|
388
|
+
- "Cross-referencing multiple knowledge bases"
|
|
389
|
+
- "Extracting relevant data from extensive archives"
|
|
390
|
+
|
|
391
|
+
📦 Finalizing/Delivery:
|
|
392
|
+
- "Applying final quality checks to deliverables"
|
|
393
|
+
- "Packaging complete analysis suite"
|
|
394
|
+
- "Validating output against requirements"
|
|
395
|
+
|
|
396
|
+
═══════════════════════════════════════════════════════════════════
|
|
397
|
+
❌ FORBIDDEN PATTERNS
|
|
398
|
+
═══════════════════════════════════════════════════════════════════
|
|
399
|
+
|
|
400
|
+
NEVER use:
|
|
401
|
+
- "for you" / "your" / addressing user
|
|
402
|
+
- "ready" / "complete" / "done" (premature)
|
|
403
|
+
- "downloading" / "uploading" (technical mechanics)
|
|
404
|
+
- "perfect" / "awesome" / "amazing" (overhype)
|
|
405
|
+
- "just" / "simply" / "quickly" (undermines expertise)
|
|
406
|
+
- Technical terms: SQL, API, database names, code
|
|
407
|
+
- Vague verbs: "working on", "getting", "making"
|
|
408
|
+
|
|
409
|
+
Return ONLY: [emoji] [professional update text]"""
|
|
211
410
|
|
|
212
411
|
messages = [UserMessage(content=str(prompt), source="summarize_progress_function")]
|
|
213
412
|
|
|
@@ -221,25 +420,34 @@ Return ONLY the update line with emoji - nothing else:"""
|
|
|
221
420
|
"""Determine if a progress update should be skipped."""
|
|
222
421
|
if not content:
|
|
223
422
|
return True
|
|
224
|
-
|
|
423
|
+
|
|
225
424
|
content_str = str(content).strip().upper()
|
|
226
|
-
|
|
425
|
+
|
|
227
426
|
# Skip internal selector prompts or bare role names
|
|
228
427
|
if self._is_internal_selector_message(content):
|
|
229
428
|
return True
|
|
230
429
|
|
|
430
|
+
# Skip HandoffMessage (agent transfers)
|
|
431
|
+
if message_type == "HandoffMessage":
|
|
432
|
+
return True
|
|
433
|
+
|
|
434
|
+
# Skip messages containing agent handoff keywords (internal coordination)
|
|
435
|
+
handoff_keywords = ["TRANSFERRED TO", "ADOPTING THE ROLE", "HANDOFF TO", "TRANSFER_TO_", "ASSUMING", "ROLE AND INITIATING"]
|
|
436
|
+
if any(keyword in content_str for keyword in handoff_keywords):
|
|
437
|
+
return True
|
|
438
|
+
|
|
231
439
|
# Skip termination messages
|
|
232
440
|
if content_str == "TERMINATE" or "TERMINATE" in content_str:
|
|
233
441
|
return True
|
|
234
|
-
|
|
442
|
+
|
|
235
443
|
# Skip empty or whitespace-only content
|
|
236
444
|
if not content_str or content_str.isspace():
|
|
237
445
|
return True
|
|
238
|
-
|
|
446
|
+
|
|
239
447
|
# Skip technical tool execution messages
|
|
240
448
|
if message_type == "ToolCallExecutionEvent":
|
|
241
449
|
return True
|
|
242
|
-
|
|
450
|
+
|
|
243
451
|
# Skip messages from terminator agent
|
|
244
452
|
if source == "terminator_agent":
|
|
245
453
|
return True
|
|
@@ -299,7 +507,7 @@ Return ONLY the update line with emoji - nothing else:"""
|
|
|
299
507
|
|
|
300
508
|
role_names = {
|
|
301
509
|
"planner_agent", "coder_agent", "code_executor", "terminator_agent",
|
|
302
|
-
"presenter_agent", "
|
|
510
|
+
"presenter_agent", "aj_sql_agent",
|
|
303
511
|
"aj_article_writer_agent", "cognitive_search_agent", "web_search_agent"
|
|
304
512
|
}
|
|
305
513
|
# If the entire content is just a role name, treat as internal
|
|
@@ -392,7 +600,7 @@ Return ONLY the update line with emoji - nothing else:"""
|
|
|
392
600
|
if similar_docs:
|
|
393
601
|
planner_learnings = await summarize_prior_learnings(similar_docs, self.gpt41_model_client)
|
|
394
602
|
if planner_learnings:
|
|
395
|
-
await self.progress_tracker.set_transient_update(task_id, 0.
|
|
603
|
+
await self.progress_tracker.set_transient_update(task_id, 0.05, "🧭 Using lessons from similar past tasks")
|
|
396
604
|
except Exception as e:
|
|
397
605
|
logger.debug(f"Pre-run retrieval failed: {e}")
|
|
398
606
|
|
|
@@ -409,19 +617,24 @@ Return ONLY the update line with emoji - nothing else:"""
|
|
|
409
617
|
except Exception:
|
|
410
618
|
merged_planner_learnings = locals().get('planner_learnings')
|
|
411
619
|
|
|
620
|
+
# CRITICAL: Wrap model clients to fix agent message roles before API calls
|
|
621
|
+
wrapped_gpt41_client = RoleFixingModelClientWrapper(self.gpt41_model_client)
|
|
622
|
+
wrapped_o3_client = RoleFixingModelClientWrapper(self.o3_model_client)
|
|
623
|
+
|
|
412
624
|
agents, presenter_agent, terminator_agent = await get_agents(
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
625
|
+
wrapped_gpt41_client,
|
|
626
|
+
wrapped_o3_client,
|
|
627
|
+
wrapped_gpt41_client,
|
|
416
628
|
request_work_dir=request_work_dir_for_agents if 'request_work_dir_for_agents' in locals() else None,
|
|
417
|
-
planner_learnings=merged_planner_learnings
|
|
629
|
+
planner_learnings=merged_planner_learnings,
|
|
630
|
+
task_context=task if 'task' in locals() else None
|
|
418
631
|
)
|
|
419
632
|
|
|
420
633
|
team = SelectorGroupChat(
|
|
421
634
|
participants=agents,
|
|
422
|
-
model_client=
|
|
635
|
+
model_client=wrapped_gpt41_client,
|
|
423
636
|
termination_condition=termination,
|
|
424
|
-
max_turns=
|
|
637
|
+
max_turns=500 # Increased to 500 - very complex tasks (word clouds, multi-database queries, extensive processing) need more turns
|
|
425
638
|
)
|
|
426
639
|
|
|
427
640
|
messages = []
|
|
@@ -437,23 +650,29 @@ Return ONLY the update line with emoji - nothing else:"""
|
|
|
437
650
|
"""
|
|
438
651
|
|
|
439
652
|
stream = team.run_stream(task=task)
|
|
440
|
-
# Loop
|
|
653
|
+
# Loop guards for detecting stuck workflows
|
|
441
654
|
repeated_schema_error_count = 0
|
|
442
655
|
last_schema_error_seen = False
|
|
656
|
+
no_files_found_count = 0
|
|
657
|
+
no_code_blocks_count = 0
|
|
658
|
+
task_not_completed_count = 0
|
|
659
|
+
|
|
443
660
|
async for message in stream:
|
|
444
661
|
messages.append(message)
|
|
445
662
|
source = message.source if hasattr(message, 'source') else None
|
|
446
|
-
content = message.content if hasattr(message, 'content') else None
|
|
663
|
+
content = message.content if hasattr(message, 'content') else None
|
|
447
664
|
created_at = message.created_at if hasattr(message, 'created_at') else None
|
|
448
665
|
logger.info(f"\n\n#SOURCE: {source}\n#CONTENT: {content}\n#CREATED_AT: {created_at}\n")
|
|
449
|
-
|
|
450
|
-
task_completed_percentage
|
|
666
|
+
|
|
667
|
+
task_completed_percentage = round(task_completed_percentage + 0.01, 2)
|
|
451
668
|
if task_completed_percentage >= 1.0:
|
|
452
669
|
task_completed_percentage = 0.99
|
|
453
|
-
|
|
454
|
-
#
|
|
670
|
+
|
|
671
|
+
# Circuit breaker: detect infinite loops
|
|
455
672
|
try:
|
|
456
673
|
ctext = str(content) if content is not None else ""
|
|
674
|
+
|
|
675
|
+
# Schema error loop guard
|
|
457
676
|
is_schema_err = ("tool_calls" in ctext) and ("MultiMessage" in ctext)
|
|
458
677
|
if is_schema_err:
|
|
459
678
|
if last_schema_error_seen:
|
|
@@ -461,13 +680,34 @@ Return ONLY the update line with emoji - nothing else:"""
|
|
|
461
680
|
else:
|
|
462
681
|
repeated_schema_error_count = 1
|
|
463
682
|
last_schema_error_seen = True
|
|
464
|
-
# If schema error repeats too many times, stop the loop to avoid getting stuck
|
|
465
683
|
if repeated_schema_error_count >= 3:
|
|
466
684
|
logger.warning("Breaking team.run_stream due to repeated MultiMessage/tool_calls schema errors.")
|
|
467
685
|
break
|
|
468
686
|
else:
|
|
469
687
|
last_schema_error_seen = False
|
|
470
688
|
repeated_schema_error_count = 0
|
|
689
|
+
|
|
690
|
+
# File uploader stuck loop guard
|
|
691
|
+
if "No files found" in ctext or "No output files" in ctext or "No files matching" in ctext:
|
|
692
|
+
no_files_found_count += 1
|
|
693
|
+
if no_files_found_count >= 5:
|
|
694
|
+
logger.warning(f"Breaking: file_uploader repeated 'No files found' {no_files_found_count} times. Likely issue with coder agent file paths.")
|
|
695
|
+
break
|
|
696
|
+
|
|
697
|
+
# Code executor stuck loop guard
|
|
698
|
+
if "No code blocks found" in ctext:
|
|
699
|
+
no_code_blocks_count += 1
|
|
700
|
+
if no_code_blocks_count >= 5:
|
|
701
|
+
logger.warning(f"Breaking: code_executor repeated 'No code blocks' {no_code_blocks_count} times. Coder agent not handing off properly.")
|
|
702
|
+
break
|
|
703
|
+
|
|
704
|
+
# Terminator stuck loop guard
|
|
705
|
+
if "TASK NOT COMPLETED" in ctext:
|
|
706
|
+
task_not_completed_count += 1
|
|
707
|
+
if task_not_completed_count >= 3:
|
|
708
|
+
logger.warning(f"Breaking: terminator said 'TASK NOT COMPLETED' {task_not_completed_count} times. Workflow stuck.")
|
|
709
|
+
break
|
|
710
|
+
|
|
471
711
|
except Exception:
|
|
472
712
|
pass
|
|
473
713
|
|
|
@@ -484,8 +724,35 @@ Return ONLY the update line with emoji - nothing else:"""
|
|
|
484
724
|
try:
|
|
485
725
|
json_content = json.loads(content)
|
|
486
726
|
if isinstance(json_content, dict):
|
|
727
|
+
# Handle upload_recent_deliverables format: {"uploads": [{blob_name, download_url}]}
|
|
728
|
+
if "uploads" in json_content and isinstance(json_content["uploads"], list):
|
|
729
|
+
upload_count_before = len(uploaded_file_urls)
|
|
730
|
+
for upload_item in json_content["uploads"]:
|
|
731
|
+
if isinstance(upload_item, dict) and "download_url" in upload_item and "blob_name" in upload_item:
|
|
732
|
+
uploaded_file_urls[upload_item["blob_name"]] = upload_item["download_url"]
|
|
733
|
+
# Progress update when files are uploaded
|
|
734
|
+
new_uploads = len(uploaded_file_urls) - upload_count_before
|
|
735
|
+
if new_uploads > 0:
|
|
736
|
+
try:
|
|
737
|
+
asyncio.create_task(self.progress_tracker.set_transient_update(
|
|
738
|
+
task_id,
|
|
739
|
+
min(0.90, task_completed_percentage + 0.05),
|
|
740
|
+
f"📤 Uploaded {new_uploads} file{'s' if new_uploads > 1 else ''} to cloud storage"
|
|
741
|
+
))
|
|
742
|
+
except Exception:
|
|
743
|
+
pass
|
|
744
|
+
# Handle direct format: {blob_name, download_url}
|
|
487
745
|
if "download_url" in json_content and "blob_name" in json_content:
|
|
488
746
|
uploaded_file_urls[json_content["blob_name"]] = json_content["download_url"]
|
|
747
|
+
# Progress update for single file upload
|
|
748
|
+
try:
|
|
749
|
+
asyncio.create_task(self.progress_tracker.set_transient_update(
|
|
750
|
+
task_id,
|
|
751
|
+
min(0.90, task_completed_percentage + 0.05),
|
|
752
|
+
f"📤 Uploaded {json_content.get('blob_name', 'file')} to cloud storage"
|
|
753
|
+
))
|
|
754
|
+
except Exception:
|
|
755
|
+
pass
|
|
489
756
|
# collect external media from known keys
|
|
490
757
|
for k in ("images", "image_urls", "media", "videos", "thumbnails", "assets"):
|
|
491
758
|
try:
|
|
@@ -539,152 +806,114 @@ Return ONLY the update line with emoji - nothing else:"""
|
|
|
539
806
|
# Catch-all for the outer deliverables-referencing try block
|
|
540
807
|
pass
|
|
541
808
|
|
|
542
|
-
# Per-request auto-upload: select best deliverables (avoid multiple near-identical PPTX)
|
|
543
|
-
try:
|
|
544
|
-
deliverable_exts = {".pptx", ".ppt", ".csv", ".png", ".jpg", ".jpeg", ".pdf", ".zip"}
|
|
545
|
-
req_dir = os.getenv("CORTEX_WORK_DIR", "/tmp/coding")
|
|
546
|
-
selected_paths: List[str] = []
|
|
547
|
-
if os.path.isdir(req_dir):
|
|
548
|
-
# Gather candidates by extension
|
|
549
|
-
candidates_by_ext: Dict[str, List[Dict[str, Any]]] = {}
|
|
550
|
-
for root, _, files in os.walk(req_dir):
|
|
551
|
-
for name in files:
|
|
552
|
-
try:
|
|
553
|
-
_, ext = os.path.splitext(name)
|
|
554
|
-
ext = ext.lower()
|
|
555
|
-
if ext not in deliverable_exts:
|
|
556
|
-
continue
|
|
557
|
-
fp = os.path.join(root, name)
|
|
558
|
-
size = 0
|
|
559
|
-
mtime = 0.0
|
|
560
|
-
try:
|
|
561
|
-
st = os.stat(fp)
|
|
562
|
-
size = int(getattr(st, 'st_size', 0))
|
|
563
|
-
mtime = float(getattr(st, 'st_mtime', 0.0))
|
|
564
|
-
except Exception:
|
|
565
|
-
pass
|
|
566
|
-
lst = candidates_by_ext.setdefault(ext, [])
|
|
567
|
-
lst.append({"path": fp, "size": size, "mtime": mtime})
|
|
568
|
-
except Exception:
|
|
569
|
-
continue
|
|
570
|
-
|
|
571
|
-
# Selection policy:
|
|
572
|
-
# - For .pptx and .ppt: choose the single largest file (assume most complete)
|
|
573
|
-
# - For other ext: include all
|
|
574
|
-
for ext, items in candidates_by_ext.items():
|
|
575
|
-
if ext in (".pptx", ".ppt"):
|
|
576
|
-
if items:
|
|
577
|
-
best = max(items, key=lambda x: (x.get("size", 0), x.get("mtime", 0.0)))
|
|
578
|
-
selected_paths.append(best["path"])
|
|
579
|
-
else:
|
|
580
|
-
for it in items:
|
|
581
|
-
selected_paths.append(it["path"])
|
|
582
|
-
|
|
583
|
-
# Upload only selected paths
|
|
584
|
-
for fp in selected_paths:
|
|
585
|
-
try:
|
|
586
|
-
up_json = upload_file_to_azure_blob(fp, blob_name=None)
|
|
587
|
-
up = json.loads(up_json)
|
|
588
|
-
if "download_url" in up and "blob_name" in up:
|
|
589
|
-
uploaded_file_urls[up["blob_name"]] = up["download_url"]
|
|
590
|
-
try:
|
|
591
|
-
bname = os.path.basename(str(up.get("blob_name") or ""))
|
|
592
|
-
extl = os.path.splitext(bname)[1].lower()
|
|
593
|
-
is_img = extl in (".png", ".jpg", ".jpeg", ".webp", ".gif")
|
|
594
|
-
uploaded_files_list.append({
|
|
595
|
-
"file_name": bname,
|
|
596
|
-
"url": up["download_url"],
|
|
597
|
-
"ext": extl,
|
|
598
|
-
"is_image": is_img,
|
|
599
|
-
})
|
|
600
|
-
if is_img:
|
|
601
|
-
external_media_urls.append(up["download_url"])
|
|
602
|
-
except Exception:
|
|
603
|
-
pass
|
|
604
|
-
except Exception:
|
|
605
|
-
continue
|
|
606
|
-
except Exception:
|
|
607
|
-
pass
|
|
608
|
-
|
|
609
|
-
# Deduplicate and cap external media to a reasonable number
|
|
610
|
-
try:
|
|
611
|
-
dedup_media = []
|
|
612
|
-
seen = set()
|
|
613
|
-
for u in external_media_urls:
|
|
614
|
-
if u in seen:
|
|
615
|
-
continue
|
|
616
|
-
seen.add(u)
|
|
617
|
-
dedup_media.append(u)
|
|
618
|
-
external_media_urls = dedup_media[:24]
|
|
619
|
-
except Exception:
|
|
620
|
-
pass
|
|
621
|
-
|
|
622
809
|
result_limited_to_fit = "\n".join(final_result_content)
|
|
623
810
|
|
|
624
|
-
# Provide the presenter with explicit file list to avoid duplication and downloads sections
|
|
625
|
-
uploaded_files_list = []
|
|
626
|
-
try:
|
|
627
|
-
for blob_name, url in (uploaded_file_urls.items() if isinstance(uploaded_file_urls, dict) else []):
|
|
628
|
-
try:
|
|
629
|
-
fname = os.path.basename(str(blob_name))
|
|
630
|
-
except Exception:
|
|
631
|
-
fname = str(blob_name)
|
|
632
|
-
extl = os.path.splitext(fname)[1].lower()
|
|
633
|
-
is_image = extl in (".png", ".jpg", ".jpeg", ".webp", ".gif")
|
|
634
|
-
uploaded_files_list.append({"file_name": fname, "url": url, "ext": extl, "is_image": is_image})
|
|
635
|
-
except Exception:
|
|
636
|
-
pass
|
|
637
|
-
|
|
638
811
|
presenter_task = f"""
|
|
639
|
-
Present the task result
|
|
640
|
-
Use only the information provided.
|
|
812
|
+
Present the final task result to the user.
|
|
641
813
|
|
|
642
814
|
TASK:
|
|
643
815
|
{task}
|
|
644
816
|
|
|
645
|
-
|
|
817
|
+
AGENT_WORK_COMPLETED:
|
|
646
818
|
{result_limited_to_fit}
|
|
647
819
|
|
|
648
|
-
|
|
649
|
-
{
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
{json.dumps(external_media_urls, indent=2)}
|
|
653
|
-
|
|
654
|
-
UPLOADED_FILES_LIST:
|
|
655
|
-
{json.dumps(uploaded_files_list, indent=2)}
|
|
656
|
-
|
|
657
|
-
STRICT OUTPUT RULES:
|
|
658
|
-
- Use UPLOADED_FILES_LIST (SAS URLs) and EXTERNAL_MEDIA_URLS to present assets. Always use the SAS URL provided in UPLOADED_FILES_LIST for any uploaded file.
|
|
659
|
-
- Images (png, jpg, jpeg, webp, gif): embed inline in a Visuals section using <figure><img/></figure> with captions. Do NOT provide links for images.
|
|
660
|
-
- Non-image files (pptx, pdf, csv): insert a SINGLE inline anchor (<a href=\"...\">filename</a>) at the first natural mention; do NOT create a 'Downloads' section; do NOT repeat links.
|
|
661
|
-
- For media: do NOT use grid or containers.
|
|
662
|
-
- SINGLE media: wrap in <figure style=\"margin: 12px 0;\"> with <img style=\"display:block;width:100%;max-width:960px;height:auto;margin:0 auto;border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,0.12)\"> and a <figcaption style=\"margin-top:8px;font-size:0.92em;color:inherit;opacity:0.8;text-align:center;\">.
|
|
663
|
-
- MULTIPLE media: output consecutive <figure> elements, one per row; no wrapping <div>.
|
|
664
|
-
- Avoid framework classes in HTML; rely on inline styles only. Do NOT include any class attributes. Use color: inherit for captions to respect dark/light mode.
|
|
665
|
-
- Never fabricate URLs, images, or content; use only links present in UPLOADED_FILES_LIST or EXTERNAL_MEDIA_URLS.
|
|
666
|
-
- Present each uploaded non-image file ONCE only (no duplicate links), using its filename as the link text.
|
|
667
|
-
- For links, prefer HTML anchor tags: <a href=\"URL\" target=\"_blank\" rel=\"noopener noreferrer\" download>FILENAME</a>.
|
|
668
|
-
- Do NOT include code, tool usage, or internal logs.
|
|
669
|
-
- Be detailed and user-facing. Include Overview, Visuals, Key Takeaways, and Next Actions sections. Do not create a Downloads section.
|
|
820
|
+
WORK_DIRECTORY:
|
|
821
|
+
{request_work_dir}
|
|
822
|
+
|
|
823
|
+
Remember: Users cannot access local file paths. Upload any deliverable files to get SAS URLs, then present with those URLs.
|
|
670
824
|
"""
|
|
671
825
|
|
|
672
|
-
|
|
673
|
-
presenter_messages = []
|
|
674
|
-
async for message in presenter_stream:
|
|
675
|
-
logger.info(f"#PRESENTER MESSAGE: {message.content if hasattr(message, 'content') else ''}")
|
|
676
|
-
presenter_messages.append(message)
|
|
677
|
-
|
|
678
|
-
task_result = presenter_messages[-1]
|
|
679
|
-
last_message = task_result.messages[-1]
|
|
680
|
-
text_result = last_message.content if hasattr(last_message, 'content') else None
|
|
681
|
-
|
|
682
|
-
# No presenter normalization or auto-upload based on text; rely on strict prompts
|
|
826
|
+
# Add progress update for file upload/presentation phase
|
|
683
827
|
try:
|
|
684
|
-
|
|
828
|
+
await self.progress_tracker.set_transient_update(task_id, 0.92, "📤 Uploading deliverable files to cloud storage...")
|
|
685
829
|
except Exception:
|
|
686
830
|
pass
|
|
687
831
|
|
|
832
|
+
# Run presenter with explicit tool call handling
|
|
833
|
+
from autogen_core import CancellationToken
|
|
834
|
+
from autogen_agentchat.messages import TextMessage
|
|
835
|
+
from autogen_agentchat.base import Response
|
|
836
|
+
|
|
837
|
+
text_result = None
|
|
838
|
+
|
|
839
|
+
# Build initial messages
|
|
840
|
+
conversation_messages = [TextMessage(source="user", content=presenter_task)]
|
|
841
|
+
|
|
842
|
+
max_turns = 5
|
|
843
|
+
for turn_num in range(max_turns):
|
|
844
|
+
logger.info(f"🎭 PRESENTER TURN {turn_num + 1}/{max_turns}")
|
|
845
|
+
|
|
846
|
+
# Update progress during presenter turns (file uploads happening)
|
|
847
|
+
try:
|
|
848
|
+
if turn_num == 0:
|
|
849
|
+
await self.progress_tracker.set_transient_update(task_id, 0.94, "📤 Processing file uploads...")
|
|
850
|
+
elif turn_num == 1:
|
|
851
|
+
await self.progress_tracker.set_transient_update(task_id, 0.95, "🎨 Preparing final presentation...")
|
|
852
|
+
except Exception:
|
|
853
|
+
pass
|
|
854
|
+
|
|
855
|
+
try:
|
|
856
|
+
response: Response = await presenter_agent.on_messages(conversation_messages, CancellationToken())
|
|
857
|
+
|
|
858
|
+
if not response or not hasattr(response, 'chat_message'):
|
|
859
|
+
logger.warning(f"No response from presenter on turn {turn_num + 1}")
|
|
860
|
+
break
|
|
861
|
+
|
|
862
|
+
# Add the response to conversation
|
|
863
|
+
response_msg = response.chat_message
|
|
864
|
+
conversation_messages.append(response_msg)
|
|
865
|
+
|
|
866
|
+
# Check what type of response we got
|
|
867
|
+
has_function_calls = (hasattr(response_msg, 'content') and
|
|
868
|
+
isinstance(response_msg.content, list) and
|
|
869
|
+
any(hasattr(item, 'call_id') for item in response_msg.content if hasattr(item, 'call_id')))
|
|
870
|
+
|
|
871
|
+
# If it's a text response (not function calls)
|
|
872
|
+
if hasattr(response_msg, 'content') and isinstance(response_msg.content, str):
|
|
873
|
+
text_content = response_msg.content.strip()
|
|
874
|
+
# Make sure it's not just raw JSON from tool
|
|
875
|
+
if text_content and not text_content.startswith('```json') and not text_content.startswith('{"blob_name"'):
|
|
876
|
+
text_result = text_content
|
|
877
|
+
logger.info(f"✅ Got final presentation text ({len(text_result)} chars)")
|
|
878
|
+
break
|
|
879
|
+
|
|
880
|
+
# Don't manually add inner_messages - on_messages() handles tool execution internally
|
|
881
|
+
# Just continue to next turn which will process the tool results
|
|
882
|
+
|
|
883
|
+
except Exception as e:
|
|
884
|
+
logger.error(f"Error in presenter turn {turn_num + 1}: {e}")
|
|
885
|
+
break
|
|
886
|
+
|
|
887
|
+
if not text_result:
|
|
888
|
+
logger.warning("⚠️ Presenter didn't generate final text after all turns")
|
|
889
|
+
text_result = "Task completed. Please check uploaded files."
|
|
890
|
+
|
|
891
|
+
# Auto-upload files marked as "Ready for upload" by code_executor
|
|
892
|
+
uploaded_files = {}
|
|
893
|
+
if presenter_agent and hasattr(presenter_agent, '_tools'):
|
|
894
|
+
# Scan all conversation messages for "Ready for upload" markers
|
|
895
|
+
for message in conversation_messages:
|
|
896
|
+
content = str(getattr(message, 'content', ''))
|
|
897
|
+
import re
|
|
898
|
+
upload_markers = re.findall(r'📁 Ready for upload: ([^\s]+)', content)
|
|
899
|
+
for file_path in upload_markers:
|
|
900
|
+
if file_path not in uploaded_files and os.path.exists(file_path):
|
|
901
|
+
try:
|
|
902
|
+
# Use the enhanced upload function directly
|
|
903
|
+
from tools.azure_blob_tools import upload_file_to_azure_blob
|
|
904
|
+
upload_result = upload_file_to_azure_blob(file_path)
|
|
905
|
+
parsed_result = json.loads(upload_result) if isinstance(upload_result, str) else upload_result
|
|
906
|
+
if 'sas_url' in parsed_result:
|
|
907
|
+
uploaded_files[file_path] = parsed_result['sas_url']
|
|
908
|
+
logger.info(f"✅ Auto-uploaded: {file_path} -> {parsed_result['sas_url']}")
|
|
909
|
+
else:
|
|
910
|
+
logger.warning(f"❌ Upload failed for: {file_path} - {parsed_result}")
|
|
911
|
+
except Exception as e:
|
|
912
|
+
logger.error(f"❌ Upload error for {file_path}: {e}")
|
|
913
|
+
|
|
914
|
+
if uploaded_files:
|
|
915
|
+
logger.info(f"📁 Auto-uploaded {len(uploaded_files)} files from code_executor output")
|
|
916
|
+
|
|
688
917
|
# No post-sanitization here; enforce via presenter prompt only per user request
|
|
689
918
|
|
|
690
919
|
logger.info(f"🔍 TASK RESULT:\n{text_result}")
|
|
@@ -745,34 +974,7 @@ Return ONLY the update line with emoji - nothing else:"""
|
|
|
745
974
|
except Exception as e:
|
|
746
975
|
logger.debug(f"Post-run indexing failed or skipped: {e}")
|
|
747
976
|
|
|
748
|
-
#
|
|
749
|
-
try:
|
|
750
|
-
term_messages = []
|
|
751
|
-
term_task = f"""
|
|
752
|
-
Check if the task is completed and output TERMINATE if and only if done.
|
|
753
|
-
Latest presenter output:
|
|
754
|
-
{text_result}
|
|
755
|
-
|
|
756
|
-
Uploaded files (SAS URLs):
|
|
757
|
-
{json.dumps(uploaded_file_urls, indent=2)}
|
|
758
|
-
|
|
759
|
-
TASK:
|
|
760
|
-
{task}
|
|
761
|
-
|
|
762
|
-
Reminder:
|
|
763
|
-
- If the TASK explicitly requires downloadable files, ensure at least one clickable download URL is present.
|
|
764
|
-
- If the TASK does not require files (e.g., simple answer, calculation, summary, troubleshooting), terminate when the presenter has clearly delivered the requested content. Do not require downloads in that case.
|
|
765
|
-
"""
|
|
766
|
-
term_stream = terminator_agent.run_stream(task=term_task)
|
|
767
|
-
async for message in term_stream:
|
|
768
|
-
term_messages.append(message)
|
|
769
|
-
if term_messages:
|
|
770
|
-
t_last = term_messages[-1].messages[-1]
|
|
771
|
-
t_text = t_last.content if hasattr(t_last, 'content') else ''
|
|
772
|
-
logger.info(f"🛑 TERMINATOR: {t_text}")
|
|
773
|
-
# If it didn't say TERMINATE but we already have presenter output, proceed anyway
|
|
774
|
-
except Exception as e:
|
|
775
|
-
logger.warning(f"⚠️ Terminator agent failed or unavailable: {e}")
|
|
977
|
+
# Publish final result
|
|
776
978
|
final_data = text_result or "🎉 Your task is complete!"
|
|
777
979
|
await self.progress_tracker.publish_progress(task_id, 1.0, "🎉 Your task is complete!", data=final_data)
|
|
778
980
|
try:
|
|
@@ -857,7 +1059,7 @@ async def process_queue_message(message_data: Dict[str, Any]) -> Optional[str]:
|
|
|
857
1059
|
task_data = json.loads(decoded_content)
|
|
858
1060
|
logger.debug(f"🔍 DEBUG: process_queue_message - Successfully base64 decoded and JSON parsed. Keys: {list(task_data.keys())}")
|
|
859
1061
|
except (json.JSONDecodeError, TypeError, ValueError) as e:
|
|
860
|
-
logger.
|
|
1062
|
+
logger.debug(f"Base64 decode failed; falling back to raw JSON: {e}")
|
|
861
1063
|
try:
|
|
862
1064
|
task_data = json.loads(raw_content)
|
|
863
1065
|
logger.debug(f"🔍 DEBUG: process_queue_message - Successfully JSON parsed raw content. Keys: {list(task_data.keys())}")
|
|
@@ -880,8 +1082,8 @@ async def process_queue_message(message_data: Dict[str, Any]) -> Optional[str]:
|
|
|
880
1082
|
await processor.publish_final(task_id or "", "⚠️ No actionable task content found. Processing has ended.")
|
|
881
1083
|
return None
|
|
882
1084
|
|
|
883
|
-
logger.debug(f"🔍 DEBUG: process_queue_message - Extracted task_content
|
|
884
|
-
logger.info(f"📩 Processing task: {task_content
|
|
1085
|
+
logger.debug(f"🔍 DEBUG: process_queue_message - Extracted task_content: {task_content}...")
|
|
1086
|
+
logger.info(f"📩 Processing task: {task_content}...")
|
|
885
1087
|
|
|
886
1088
|
result = await processor.process_task(task_id, task_content)
|
|
887
1089
|
return result
|