@aj-archipelago/cortex 1.4.2 → 1.4.3

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.
Files changed (86) hide show
  1. package/README.md +1 -0
  2. package/config.js +1 -1
  3. package/helper-apps/cortex-autogen2/.dockerignore +1 -0
  4. package/helper-apps/cortex-autogen2/Dockerfile +6 -10
  5. package/helper-apps/cortex-autogen2/Dockerfile.worker +2 -0
  6. package/helper-apps/cortex-autogen2/agents.py +203 -2
  7. package/helper-apps/cortex-autogen2/main.py +1 -1
  8. package/helper-apps/cortex-autogen2/pyproject.toml +12 -0
  9. package/helper-apps/cortex-autogen2/requirements.txt +14 -0
  10. package/helper-apps/cortex-autogen2/services/redis_publisher.py +1 -1
  11. package/helper-apps/cortex-autogen2/services/run_analyzer.py +1 -1
  12. package/helper-apps/cortex-autogen2/task_processor.py +431 -229
  13. package/helper-apps/cortex-autogen2/test_entity_fetcher.py +305 -0
  14. package/helper-apps/cortex-autogen2/tests/README.md +240 -0
  15. package/helper-apps/cortex-autogen2/tests/TEST_REPORT.md +342 -0
  16. package/helper-apps/cortex-autogen2/tests/__init__.py +8 -0
  17. package/helper-apps/cortex-autogen2/tests/analysis/__init__.py +1 -0
  18. package/helper-apps/cortex-autogen2/tests/analysis/improvement_suggester.py +224 -0
  19. package/helper-apps/cortex-autogen2/tests/analysis/trend_analyzer.py +211 -0
  20. package/helper-apps/cortex-autogen2/tests/cli/__init__.py +1 -0
  21. package/helper-apps/cortex-autogen2/tests/cli/run_tests.py +296 -0
  22. package/helper-apps/cortex-autogen2/tests/collectors/__init__.py +1 -0
  23. package/helper-apps/cortex-autogen2/tests/collectors/log_collector.py +252 -0
  24. package/helper-apps/cortex-autogen2/tests/collectors/progress_collector.py +182 -0
  25. package/helper-apps/cortex-autogen2/tests/conftest.py +15 -0
  26. package/helper-apps/cortex-autogen2/tests/database/__init__.py +1 -0
  27. package/helper-apps/cortex-autogen2/tests/database/repository.py +501 -0
  28. package/helper-apps/cortex-autogen2/tests/database/schema.sql +108 -0
  29. package/helper-apps/cortex-autogen2/tests/evaluators/__init__.py +1 -0
  30. package/helper-apps/cortex-autogen2/tests/evaluators/llm_scorer.py +294 -0
  31. package/helper-apps/cortex-autogen2/tests/evaluators/prompts.py +250 -0
  32. package/helper-apps/cortex-autogen2/tests/evaluators/wordcloud_validator.py +168 -0
  33. package/helper-apps/cortex-autogen2/tests/metrics/__init__.py +1 -0
  34. package/helper-apps/cortex-autogen2/tests/metrics/collector.py +155 -0
  35. package/helper-apps/cortex-autogen2/tests/orchestrator.py +576 -0
  36. package/helper-apps/cortex-autogen2/tests/test_cases.yaml +279 -0
  37. package/helper-apps/cortex-autogen2/tests/test_data.db +0 -0
  38. package/helper-apps/cortex-autogen2/tests/utils/__init__.py +3 -0
  39. package/helper-apps/cortex-autogen2/tests/utils/connectivity.py +112 -0
  40. package/helper-apps/cortex-autogen2/tools/azure_blob_tools.py +74 -24
  41. package/helper-apps/cortex-autogen2/tools/entity_api_registry.json +38 -0
  42. package/helper-apps/cortex-autogen2/tools/file_tools.py +1 -1
  43. package/helper-apps/cortex-autogen2/tools/search_tools.py +436 -238
  44. package/helper-apps/cortex-file-handler/package-lock.json +2 -2
  45. package/helper-apps/cortex-file-handler/package.json +1 -1
  46. package/helper-apps/cortex-file-handler/scripts/setup-test-containers.js +4 -5
  47. package/helper-apps/cortex-file-handler/src/blobHandler.js +36 -144
  48. package/helper-apps/cortex-file-handler/src/services/FileConversionService.js +5 -3
  49. package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +34 -1
  50. package/helper-apps/cortex-file-handler/src/services/storage/GCSStorageProvider.js +22 -0
  51. package/helper-apps/cortex-file-handler/src/services/storage/LocalStorageProvider.js +28 -1
  52. package/helper-apps/cortex-file-handler/src/services/storage/StorageFactory.js +29 -4
  53. package/helper-apps/cortex-file-handler/src/services/storage/StorageProvider.js +11 -0
  54. package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +1 -1
  55. package/helper-apps/cortex-file-handler/tests/blobHandler.test.js +3 -2
  56. package/helper-apps/cortex-file-handler/tests/checkHashShortLived.test.js +8 -1
  57. package/helper-apps/cortex-file-handler/tests/containerConversionFlow.test.js +5 -2
  58. package/helper-apps/cortex-file-handler/tests/containerNameParsing.test.js +14 -7
  59. package/helper-apps/cortex-file-handler/tests/containerParameterFlow.test.js +5 -2
  60. package/helper-apps/cortex-file-handler/tests/storage/StorageFactory.test.js +31 -19
  61. package/package.json +1 -1
  62. package/server/modelExecutor.js +4 -0
  63. package/server/plugins/claude4VertexPlugin.js +540 -0
  64. package/server/plugins/openAiWhisperPlugin.js +43 -2
  65. package/tests/integration/rest/vendors/claude_streaming.test.js +121 -0
  66. package/tests/unit/plugins/claude4VertexPlugin.test.js +462 -0
  67. package/tests/unit/plugins/claude4VertexToolConversion.test.js +413 -0
  68. package/helper-apps/cortex-autogen/.funcignore +0 -8
  69. package/helper-apps/cortex-autogen/Dockerfile +0 -10
  70. package/helper-apps/cortex-autogen/OAI_CONFIG_LIST +0 -6
  71. package/helper-apps/cortex-autogen/agents.py +0 -493
  72. package/helper-apps/cortex-autogen/agents_extra.py +0 -14
  73. package/helper-apps/cortex-autogen/config.py +0 -18
  74. package/helper-apps/cortex-autogen/data_operations.py +0 -29
  75. package/helper-apps/cortex-autogen/function_app.py +0 -44
  76. package/helper-apps/cortex-autogen/host.json +0 -15
  77. package/helper-apps/cortex-autogen/main.py +0 -38
  78. package/helper-apps/cortex-autogen/prompts.py +0 -196
  79. package/helper-apps/cortex-autogen/prompts_extra.py +0 -5
  80. package/helper-apps/cortex-autogen/requirements.txt +0 -9
  81. package/helper-apps/cortex-autogen/search.py +0 -85
  82. package/helper-apps/cortex-autogen/test.sh +0 -40
  83. package/helper-apps/cortex-autogen/tools/sasfileuploader.py +0 -66
  84. package/helper-apps/cortex-autogen/utils.py +0 -88
  85. package/helper-apps/cortex-autogen2/DigiCertGlobalRootCA.crt.pem +0 -22
  86. package/helper-apps/cortex-autogen2/poetry.lock +0 -3652
@@ -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 # 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"""Transform this agent activity into a delightful, crystal-clear progress update (8-15 words) that makes non-technical users feel excited about what's happening. Start with a perfect emoji.
167
-
168
- Context: This appears in a live progress indicator for end users who aren't coders.
169
-
170
- Current Activity: {cleaned_content}
171
- Agent Role: {source if source else "Unknown"}
172
-
173
- 🎨 Emoji Guide (pick the most fitting):
174
- Planning/Thinking: 🧭 🗺️ 💡 🎯 🤔
175
- Research/Search: 🔎 🔍 🌐 📚 🕵️
176
- Data/Analysis: 📊 📈 📉 🧮 💹
177
- Writing/Creating: ✍️ 📝 🖊️ 🎨
178
- Images/Media: 🖼️ 📸 🎬 🌈 🖌️
179
- Code/Technical: 💻 ⚙️ 🛠️ 🔧
180
- Files/Upload: 📁 ☁️ 📤 💾 🗂️
181
- Success/Done: ✅ 🎉 🏆 🎊 ⭐
182
-
183
- Writing Style:
184
- - ENGAGING: Use vivid, active verbs that paint a picture (discovering, crafting, weaving, building, hunting)
185
- - HUMAN: Conversational and warm, like a helpful colleague updating you
186
- - CLEAR: Zero jargon, no technical terms, no agent/tool names
187
- - SPECIFIC: Say what's actually being created/found (not just "processing data")
188
- - UPBEAT: Positive energy, but not over-the-top
189
- - SHORT: 8-15 words max - every word must earn its place
190
-
191
- 🌟 Great Examples (follow these patterns):
192
- - "🔍 Hunting down the perfect images for your presentation"
193
- - "📊 Crunching numbers to reveal hidden trends"
194
- - " Weaving everything together into a polished report"
195
- - "🎨 Designing eye-catching charts that tell the story"
196
- - "📚 Diving deep into research to find golden insights"
197
- - "🖼️ Gathering stunning visuals to bring ideas to life"
198
- - "💡 Mapping out the smartest approach to tackle this"
199
- - "☁️ Packaging everything up for easy download"
200
- - "🔎 Exploring databases to uncover the answers"
201
- - "✍️ Crafting a compelling narrative from the data"
202
-
203
- Avoid These (too boring/technical):
204
- - "Processing data" (vague)
205
- - "Executing SQL query" (jargon)
206
- - "Running code" (technical)
207
- - "Your report is ready" (premature/addressing user)
208
- - "Task terminated" (robotic)
209
-
210
- Return ONLY the update line with emoji - nothing else:"""
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", "file_cloud_uploader_agent", "aj_sql_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.07, "🧭 Using lessons from similar past tasks")
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
- self.gpt41_model_client,
414
- self.o3_model_client,
415
- self.gpt41_model_client,
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=self.gpt41_model_client,
635
+ model_client=wrapped_gpt41_client,
423
636
  termination_condition=termination,
424
- max_turns=200
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 guard for repeating provider schema errors (e.g., tool_calls/MultiMessage)
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 += 0.01
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
- # Loop-guard detection: break early if the same schema error repeats
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 in a clean, professional Markdown/HTML that contains ONLY what the task requested. This will be shown in a React app.
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
- RAW_AGENT_COMMUNICATIONS:
817
+ AGENT_WORK_COMPLETED:
646
818
  {result_limited_to_fit}
647
819
 
648
- UPLOADED_FILES_SAS_URLS:
649
- {json.dumps(uploaded_file_urls, indent=2)}
650
-
651
- EXTERNAL_MEDIA_URLS:
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
- presenter_stream = presenter_agent.run_stream(task=presenter_task)
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
- pass
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
- # Run terminator agent once presenter has produced final text
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.warning(f"⚠️ Failed to decode as base64, trying as raw JSON: {e}")
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 (first 100 chars): {task_content[:100]}...")
884
- logger.info(f"📩 Processing task: {task_content[:100]}...")
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