massgen 0.0.3__py3-none-any.whl
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.
Potentially problematic release.
This version of massgen might be problematic. Click here for more details.
- massgen/__init__.py +94 -0
- massgen/agent_config.py +507 -0
- massgen/backend/CLAUDE_API_RESEARCH.md +266 -0
- massgen/backend/Function calling openai responses.md +1161 -0
- massgen/backend/GEMINI_API_DOCUMENTATION.md +410 -0
- massgen/backend/OPENAI_RESPONSES_API_FORMAT.md +65 -0
- massgen/backend/__init__.py +25 -0
- massgen/backend/base.py +180 -0
- massgen/backend/chat_completions.py +228 -0
- massgen/backend/claude.py +661 -0
- massgen/backend/gemini.py +652 -0
- massgen/backend/grok.py +187 -0
- massgen/backend/response.py +397 -0
- massgen/chat_agent.py +440 -0
- massgen/cli.py +686 -0
- massgen/configs/README.md +293 -0
- massgen/configs/creative_team.yaml +53 -0
- massgen/configs/gemini_4o_claude.yaml +31 -0
- massgen/configs/news_analysis.yaml +51 -0
- massgen/configs/research_team.yaml +51 -0
- massgen/configs/single_agent.yaml +18 -0
- massgen/configs/single_flash2.5.yaml +44 -0
- massgen/configs/technical_analysis.yaml +51 -0
- massgen/configs/three_agents_default.yaml +31 -0
- massgen/configs/travel_planning.yaml +51 -0
- massgen/configs/two_agents.yaml +39 -0
- massgen/frontend/__init__.py +20 -0
- massgen/frontend/coordination_ui.py +945 -0
- massgen/frontend/displays/__init__.py +24 -0
- massgen/frontend/displays/base_display.py +83 -0
- massgen/frontend/displays/rich_terminal_display.py +3497 -0
- massgen/frontend/displays/simple_display.py +93 -0
- massgen/frontend/displays/terminal_display.py +381 -0
- massgen/frontend/logging/__init__.py +9 -0
- massgen/frontend/logging/realtime_logger.py +197 -0
- massgen/message_templates.py +431 -0
- massgen/orchestrator.py +1222 -0
- massgen/tests/__init__.py +10 -0
- massgen/tests/multi_turn_conversation_design.md +214 -0
- massgen/tests/multiturn_llm_input_analysis.md +189 -0
- massgen/tests/test_case_studies.md +113 -0
- massgen/tests/test_claude_backend.py +310 -0
- massgen/tests/test_grok_backend.py +160 -0
- massgen/tests/test_message_context_building.py +293 -0
- massgen/tests/test_rich_terminal_display.py +378 -0
- massgen/tests/test_v3_3agents.py +117 -0
- massgen/tests/test_v3_simple.py +216 -0
- massgen/tests/test_v3_three_agents.py +272 -0
- massgen/tests/test_v3_two_agents.py +176 -0
- massgen/utils.py +79 -0
- massgen/v1/README.md +330 -0
- massgen/v1/__init__.py +91 -0
- massgen/v1/agent.py +605 -0
- massgen/v1/agents.py +330 -0
- massgen/v1/backends/gemini.py +584 -0
- massgen/v1/backends/grok.py +410 -0
- massgen/v1/backends/oai.py +571 -0
- massgen/v1/cli.py +351 -0
- massgen/v1/config.py +169 -0
- massgen/v1/examples/fast-4o-mini-config.yaml +44 -0
- massgen/v1/examples/fast_config.yaml +44 -0
- massgen/v1/examples/production.yaml +70 -0
- massgen/v1/examples/single_agent.yaml +39 -0
- massgen/v1/logging.py +974 -0
- massgen/v1/main.py +368 -0
- massgen/v1/orchestrator.py +1138 -0
- massgen/v1/streaming_display.py +1190 -0
- massgen/v1/tools.py +160 -0
- massgen/v1/types.py +245 -0
- massgen/v1/utils.py +199 -0
- massgen-0.0.3.dist-info/METADATA +568 -0
- massgen-0.0.3.dist-info/RECORD +76 -0
- massgen-0.0.3.dist-info/WHEEL +5 -0
- massgen-0.0.3.dist-info/entry_points.txt +2 -0
- massgen-0.0.3.dist-info/licenses/LICENSE +204 -0
- massgen-0.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,945 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MassGen Coordination UI
|
|
3
|
+
|
|
4
|
+
Main interface for coordinating agents with visual display and logging.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
import asyncio
|
|
9
|
+
from typing import Optional, List, Dict, Any, AsyncGenerator
|
|
10
|
+
from .displays.base_display import BaseDisplay
|
|
11
|
+
from .displays.terminal_display import TerminalDisplay
|
|
12
|
+
from .displays.simple_display import SimpleDisplay
|
|
13
|
+
from .displays.rich_terminal_display import RichTerminalDisplay, is_rich_available
|
|
14
|
+
from .logging.realtime_logger import RealtimeLogger
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CoordinationUI:
|
|
18
|
+
"""Main coordination interface with display and logging capabilities."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
display: Optional[BaseDisplay] = None,
|
|
23
|
+
logger: Optional[RealtimeLogger] = None,
|
|
24
|
+
display_type: str = "terminal",
|
|
25
|
+
logging_enabled: bool = True,
|
|
26
|
+
enable_final_presentation: bool = False,
|
|
27
|
+
**kwargs,
|
|
28
|
+
):
|
|
29
|
+
"""Initialize coordination UI.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
display: Custom display instance (overrides display_type)
|
|
33
|
+
logger: Custom logger instance
|
|
34
|
+
display_type: Type of display ("terminal", "simple", "rich_terminal", "textual_terminal")
|
|
35
|
+
logging_enabled: Whether to enable real-time logging
|
|
36
|
+
enable_final_presentation: Whether to ask winning agent to present final answer
|
|
37
|
+
**kwargs: Additional configuration passed to display/logger
|
|
38
|
+
"""
|
|
39
|
+
self.enable_final_presentation = enable_final_presentation
|
|
40
|
+
self.display = display
|
|
41
|
+
# Filter kwargs for logger (only pass logger-specific params)
|
|
42
|
+
logger_kwargs = {
|
|
43
|
+
k: v for k, v in kwargs.items() if k in ["filename", "update_frequency"]
|
|
44
|
+
}
|
|
45
|
+
self.logger = (
|
|
46
|
+
logger
|
|
47
|
+
if logger is not None
|
|
48
|
+
else (RealtimeLogger(**logger_kwargs) if logging_enabled else None)
|
|
49
|
+
)
|
|
50
|
+
self.display_type = display_type
|
|
51
|
+
self.config = kwargs
|
|
52
|
+
|
|
53
|
+
# Will be set during coordination
|
|
54
|
+
self.agent_ids = []
|
|
55
|
+
self.orchestrator = None
|
|
56
|
+
|
|
57
|
+
# Flush output configuration (matches rich_terminal_display)
|
|
58
|
+
self._flush_char_delay = 0.03 # 30ms between characters
|
|
59
|
+
self._flush_word_delay = 0.08 # 80ms after punctuation
|
|
60
|
+
|
|
61
|
+
# Initialize answer buffer state
|
|
62
|
+
self._answer_buffer = ""
|
|
63
|
+
self._answer_timeout_task = None
|
|
64
|
+
self._final_answer_shown = False
|
|
65
|
+
|
|
66
|
+
def reset(self):
|
|
67
|
+
"""Reset UI state for next coordination session."""
|
|
68
|
+
# Clean up display if exists
|
|
69
|
+
if self.display:
|
|
70
|
+
try:
|
|
71
|
+
self.display.cleanup()
|
|
72
|
+
except Exception:
|
|
73
|
+
pass # Ignore cleanup errors
|
|
74
|
+
self.display = None
|
|
75
|
+
|
|
76
|
+
# Reset all state variables
|
|
77
|
+
self.agent_ids = []
|
|
78
|
+
self.orchestrator = None
|
|
79
|
+
|
|
80
|
+
# Reset answer buffer state if they exist
|
|
81
|
+
if hasattr(self, "_answer_buffer"):
|
|
82
|
+
self._answer_buffer = ""
|
|
83
|
+
if hasattr(self, "_answer_timeout_task") and self._answer_timeout_task:
|
|
84
|
+
self._answer_timeout_task.cancel()
|
|
85
|
+
self._answer_timeout_task = None
|
|
86
|
+
if hasattr(self, "_final_answer_shown"):
|
|
87
|
+
self._final_answer_shown = False
|
|
88
|
+
|
|
89
|
+
async def coordinate(
|
|
90
|
+
self, orchestrator, question: str, agent_ids: Optional[List[str]] = None
|
|
91
|
+
) -> str:
|
|
92
|
+
"""Coordinate agents with visual display and logging.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
orchestrator: MassGen orchestrator instance
|
|
96
|
+
question: Question for coordination
|
|
97
|
+
agent_ids: Optional list of agent IDs (auto-detected if not provided)
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Final coordinated response
|
|
101
|
+
"""
|
|
102
|
+
# Reset display to ensure clean state for each coordination
|
|
103
|
+
if self.display is not None:
|
|
104
|
+
self.display.cleanup()
|
|
105
|
+
self.display = None
|
|
106
|
+
|
|
107
|
+
self.orchestrator = orchestrator
|
|
108
|
+
|
|
109
|
+
# Auto-detect agent IDs if not provided
|
|
110
|
+
if agent_ids is None:
|
|
111
|
+
self.agent_ids = list(orchestrator.agents.keys())
|
|
112
|
+
else:
|
|
113
|
+
self.agent_ids = agent_ids
|
|
114
|
+
|
|
115
|
+
# Initialize display if not provided
|
|
116
|
+
if self.display is None:
|
|
117
|
+
if self.display_type == "terminal":
|
|
118
|
+
self.display = TerminalDisplay(self.agent_ids, **self.config)
|
|
119
|
+
elif self.display_type == "simple":
|
|
120
|
+
self.display = SimpleDisplay(self.agent_ids, **self.config)
|
|
121
|
+
elif self.display_type == "rich_terminal":
|
|
122
|
+
if not is_rich_available():
|
|
123
|
+
print(
|
|
124
|
+
"⚠️ Rich library not available. Falling back to terminal display."
|
|
125
|
+
)
|
|
126
|
+
print(" Install with: pip install rich")
|
|
127
|
+
self.display = TerminalDisplay(self.agent_ids, **self.config)
|
|
128
|
+
else:
|
|
129
|
+
self.display = RichTerminalDisplay(self.agent_ids, **self.config)
|
|
130
|
+
else:
|
|
131
|
+
raise ValueError(f"Unknown display type: {self.display_type}")
|
|
132
|
+
|
|
133
|
+
# Pass orchestrator reference to display for backend info
|
|
134
|
+
self.display.orchestrator = orchestrator
|
|
135
|
+
|
|
136
|
+
# Initialize answer buffering for preventing duplicate show_final_answer calls
|
|
137
|
+
self._answer_buffer = ""
|
|
138
|
+
self._answer_timeout_task = None
|
|
139
|
+
self._final_answer_shown = False
|
|
140
|
+
|
|
141
|
+
# Initialize logger and display
|
|
142
|
+
log_filename = None
|
|
143
|
+
if self.logger:
|
|
144
|
+
log_filename = self.logger.initialize_session(question, self.agent_ids)
|
|
145
|
+
monitoring = self.logger.get_monitoring_commands()
|
|
146
|
+
print(f"📁 Real-time log: {log_filename}")
|
|
147
|
+
print(f"💡 Monitor with: {monitoring['tail']}")
|
|
148
|
+
print()
|
|
149
|
+
|
|
150
|
+
self.display.initialize(question, log_filename)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
# Process coordination stream
|
|
154
|
+
full_response = ""
|
|
155
|
+
final_answer = ""
|
|
156
|
+
|
|
157
|
+
async for chunk in orchestrator.chat_simple(question):
|
|
158
|
+
content = getattr(chunk, "content", "") or ""
|
|
159
|
+
source = getattr(chunk, "source", None)
|
|
160
|
+
chunk_type = getattr(chunk, "type", "")
|
|
161
|
+
|
|
162
|
+
# Handle agent status updates
|
|
163
|
+
if chunk_type == "agent_status":
|
|
164
|
+
status = getattr(chunk, "status", None)
|
|
165
|
+
if source and status:
|
|
166
|
+
self.display.update_agent_status(source, status)
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
# Handle builtin tool results
|
|
170
|
+
elif chunk_type == "builtin_tool_results":
|
|
171
|
+
builtin_results = getattr(chunk, "builtin_tool_results", [])
|
|
172
|
+
if builtin_results and source:
|
|
173
|
+
for result in builtin_results:
|
|
174
|
+
tool_type = result.get("tool_type", "unknown")
|
|
175
|
+
status_result = result.get("status", "unknown")
|
|
176
|
+
tool_msg = (
|
|
177
|
+
f"🔧 [{tool_type.title()}] {status_result.title()}"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if tool_type in ["code_interpreter", "code_execution"]:
|
|
181
|
+
code = result.get("code", "") or result.get(
|
|
182
|
+
"input", {}
|
|
183
|
+
).get("code", "")
|
|
184
|
+
outputs = result.get("outputs")
|
|
185
|
+
if code:
|
|
186
|
+
tool_msg += f" - Code: {code[:50]}{'...' if len(code) > 50 else ''}"
|
|
187
|
+
if outputs:
|
|
188
|
+
tool_msg += f" - Result: {outputs}"
|
|
189
|
+
elif tool_type == "web_search":
|
|
190
|
+
query = result.get("query", "") or result.get(
|
|
191
|
+
"input", {}
|
|
192
|
+
).get("query", "")
|
|
193
|
+
if query:
|
|
194
|
+
tool_msg += f" - Query: '{query}'"
|
|
195
|
+
|
|
196
|
+
# Display as tool content for the specific agent
|
|
197
|
+
await self._process_agent_content(source, tool_msg)
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
if content:
|
|
201
|
+
full_response += content
|
|
202
|
+
|
|
203
|
+
# Log chunk
|
|
204
|
+
if self.logger:
|
|
205
|
+
self.logger.log_chunk(source, content, chunk.type)
|
|
206
|
+
|
|
207
|
+
# Process content by source
|
|
208
|
+
await self._process_content(source, content)
|
|
209
|
+
|
|
210
|
+
# Display vote results and get final presentation
|
|
211
|
+
status = orchestrator.get_status()
|
|
212
|
+
vote_results = status.get("vote_results", {})
|
|
213
|
+
selected_agent = status.get("selected_agent")
|
|
214
|
+
|
|
215
|
+
# if vote_results.get('vote_counts'):
|
|
216
|
+
# self._display_vote_results(vote_results)
|
|
217
|
+
# # Allow time for voting results to be visible
|
|
218
|
+
# import time
|
|
219
|
+
# time.sleep(1.0)
|
|
220
|
+
|
|
221
|
+
# Get final presentation from winning agent
|
|
222
|
+
if (
|
|
223
|
+
self.enable_final_presentation
|
|
224
|
+
and selected_agent
|
|
225
|
+
and vote_results.get("vote_counts")
|
|
226
|
+
):
|
|
227
|
+
print(f"\n🎤 Final Presentation from {selected_agent}:")
|
|
228
|
+
print("=" * 60)
|
|
229
|
+
|
|
230
|
+
presentation_content = ""
|
|
231
|
+
try:
|
|
232
|
+
async for chunk in orchestrator.get_final_presentation(
|
|
233
|
+
selected_agent, vote_results
|
|
234
|
+
):
|
|
235
|
+
content = getattr(chunk, "content", "") or ""
|
|
236
|
+
if content:
|
|
237
|
+
# Ensure content is a string
|
|
238
|
+
if isinstance(content, list):
|
|
239
|
+
content = " ".join(str(item) for item in content)
|
|
240
|
+
elif not isinstance(content, str):
|
|
241
|
+
content = str(content)
|
|
242
|
+
|
|
243
|
+
# Simple content accumulation - let the display handle formatting
|
|
244
|
+
presentation_content += content
|
|
245
|
+
|
|
246
|
+
# Log presentation chunk
|
|
247
|
+
if self.logger:
|
|
248
|
+
self.logger.log_chunk(
|
|
249
|
+
selected_agent,
|
|
250
|
+
content,
|
|
251
|
+
getattr(chunk, "type", "presentation"),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Display the presentation in real-time
|
|
255
|
+
if self.display:
|
|
256
|
+
try:
|
|
257
|
+
await self._process_content(selected_agent, content)
|
|
258
|
+
except Exception as e:
|
|
259
|
+
# Error processing presentation content - continue gracefully
|
|
260
|
+
pass
|
|
261
|
+
# Also print to console with flush using consistent timing with rich display
|
|
262
|
+
self._print_with_flush(content)
|
|
263
|
+
else:
|
|
264
|
+
# Simple print for non-display mode
|
|
265
|
+
print(content, end="", flush=True)
|
|
266
|
+
except AttributeError:
|
|
267
|
+
# get_final_presentation method doesn't exist or failed
|
|
268
|
+
print(
|
|
269
|
+
"Final presentation not available - using coordination result"
|
|
270
|
+
)
|
|
271
|
+
presentation_content = ""
|
|
272
|
+
|
|
273
|
+
final_answer = presentation_content
|
|
274
|
+
print("\n" + "=" * 60)
|
|
275
|
+
# Allow time for final presentation to be fully visible
|
|
276
|
+
time.sleep(1.5)
|
|
277
|
+
|
|
278
|
+
# Get the clean final answer from orchestrator's stored state (avoids token spacing issues)
|
|
279
|
+
orchestrator_final_answer = None
|
|
280
|
+
if (
|
|
281
|
+
selected_agent
|
|
282
|
+
and hasattr(orchestrator, "agent_states")
|
|
283
|
+
and selected_agent in orchestrator.agent_states
|
|
284
|
+
):
|
|
285
|
+
stored_answer = orchestrator.agent_states[selected_agent].answer
|
|
286
|
+
if stored_answer:
|
|
287
|
+
# Clean up the stored answer
|
|
288
|
+
orchestrator_final_answer = (
|
|
289
|
+
stored_answer.replace("\\", "\n").replace("**", "").strip()
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Use orchestrator's clean answer if available, otherwise fall back to presentation
|
|
293
|
+
final_result = (
|
|
294
|
+
orchestrator_final_answer
|
|
295
|
+
if orchestrator_final_answer
|
|
296
|
+
else (final_answer if final_answer else full_response)
|
|
297
|
+
)
|
|
298
|
+
if final_result:
|
|
299
|
+
# print(f"\n🎯 FINAL COORDINATED ANSWER")
|
|
300
|
+
# print("=" * 80)
|
|
301
|
+
# print(f"{final_result.strip()}")
|
|
302
|
+
# print("=" * 80)
|
|
303
|
+
|
|
304
|
+
# Show which agent was selected
|
|
305
|
+
if selected_agent:
|
|
306
|
+
print(f"✅ Selected by: {selected_agent}")
|
|
307
|
+
if vote_results.get("vote_counts"):
|
|
308
|
+
vote_summary = ", ".join(
|
|
309
|
+
[
|
|
310
|
+
f"{agent}: {count}"
|
|
311
|
+
for agent, count in vote_results["vote_counts"].items()
|
|
312
|
+
]
|
|
313
|
+
)
|
|
314
|
+
print(f"🗳️ Vote results: {vote_summary}")
|
|
315
|
+
print()
|
|
316
|
+
|
|
317
|
+
# Finalize session
|
|
318
|
+
if self.logger:
|
|
319
|
+
session_info = self.logger.finalize_session(final_answer, success=True)
|
|
320
|
+
print(f"💾 Session log: {session_info['filename']}")
|
|
321
|
+
print(
|
|
322
|
+
f"⏱️ Duration: {session_info['duration']:.1f}s | Chunks: {session_info['total_chunks']} | Events: {session_info['orchestrator_events']}"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
return final_result
|
|
326
|
+
|
|
327
|
+
except Exception as e:
|
|
328
|
+
if self.logger:
|
|
329
|
+
self.logger.finalize_session("", success=False)
|
|
330
|
+
raise
|
|
331
|
+
finally:
|
|
332
|
+
# Wait for any pending timeout task to complete before cleanup
|
|
333
|
+
if hasattr(self, "_answer_timeout_task") and self._answer_timeout_task:
|
|
334
|
+
try:
|
|
335
|
+
# Give the task a chance to complete
|
|
336
|
+
await asyncio.wait_for(self._answer_timeout_task, timeout=1.0)
|
|
337
|
+
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
338
|
+
# If it takes too long or was cancelled, force flush
|
|
339
|
+
if (
|
|
340
|
+
hasattr(self, "_answer_buffer")
|
|
341
|
+
and self._answer_buffer
|
|
342
|
+
and not self._final_answer_shown
|
|
343
|
+
):
|
|
344
|
+
await self._flush_final_answer()
|
|
345
|
+
self._answer_timeout_task.cancel()
|
|
346
|
+
|
|
347
|
+
# Final check to flush any remaining buffered answer
|
|
348
|
+
if (
|
|
349
|
+
hasattr(self, "_answer_buffer")
|
|
350
|
+
and self._answer_buffer
|
|
351
|
+
and not self._final_answer_shown
|
|
352
|
+
):
|
|
353
|
+
await self._flush_final_answer()
|
|
354
|
+
|
|
355
|
+
# Small delay to ensure display updates are processed
|
|
356
|
+
await asyncio.sleep(0.1)
|
|
357
|
+
|
|
358
|
+
if self.display:
|
|
359
|
+
self.display.cleanup()
|
|
360
|
+
|
|
361
|
+
if selected_agent:
|
|
362
|
+
print(f"✅ Selected by: {selected_agent}")
|
|
363
|
+
if vote_results.get("vote_counts"):
|
|
364
|
+
vote_summary = ", ".join(
|
|
365
|
+
[
|
|
366
|
+
f"{agent}: {count}"
|
|
367
|
+
for agent, count in vote_results["vote_counts"].items()
|
|
368
|
+
]
|
|
369
|
+
)
|
|
370
|
+
print(f"🗳️ Vote results: {vote_summary}")
|
|
371
|
+
print()
|
|
372
|
+
|
|
373
|
+
if self.logger:
|
|
374
|
+
session_info = self.logger.finalize_session(final_answer, success=True)
|
|
375
|
+
print(f"💾 Session log: {session_info['filename']}")
|
|
376
|
+
print(
|
|
377
|
+
f"⏱️ Duration: {session_info['duration']:.1f}s | Chunks: {session_info['total_chunks']} | Events: {session_info['orchestrator_events']}"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
async def coordinate_with_context(
|
|
381
|
+
self,
|
|
382
|
+
orchestrator,
|
|
383
|
+
question: str,
|
|
384
|
+
messages: List[Dict[str, Any]],
|
|
385
|
+
agent_ids: Optional[List[str]] = None,
|
|
386
|
+
) -> str:
|
|
387
|
+
"""Coordinate agents with conversation context and visual display.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
orchestrator: MassGen orchestrator instance
|
|
391
|
+
question: Current question for coordination
|
|
392
|
+
messages: Full conversation message history
|
|
393
|
+
agent_ids: Optional list of agent IDs (auto-detected if not provided)
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
Final coordinated response
|
|
397
|
+
"""
|
|
398
|
+
# Reset display to ensure clean state for each coordination
|
|
399
|
+
if self.display is not None:
|
|
400
|
+
self.display.cleanup()
|
|
401
|
+
self.display = None
|
|
402
|
+
|
|
403
|
+
self.orchestrator = orchestrator
|
|
404
|
+
|
|
405
|
+
# Auto-detect agent IDs if not provided
|
|
406
|
+
if agent_ids is None:
|
|
407
|
+
self.agent_ids = list(orchestrator.agents.keys())
|
|
408
|
+
else:
|
|
409
|
+
self.agent_ids = agent_ids
|
|
410
|
+
|
|
411
|
+
# Initialize display if not provided
|
|
412
|
+
if self.display is None:
|
|
413
|
+
if self.display_type == "terminal":
|
|
414
|
+
self.display = TerminalDisplay(self.agent_ids, **self.config)
|
|
415
|
+
elif self.display_type == "simple":
|
|
416
|
+
self.display = SimpleDisplay(self.agent_ids, **self.config)
|
|
417
|
+
elif self.display_type == "rich_terminal":
|
|
418
|
+
if not is_rich_available():
|
|
419
|
+
print(
|
|
420
|
+
"⚠️ Rich library not available. Falling back to terminal display."
|
|
421
|
+
)
|
|
422
|
+
print(" Install with: pip install rich")
|
|
423
|
+
self.display = TerminalDisplay(self.agent_ids, **self.config)
|
|
424
|
+
else:
|
|
425
|
+
self.display = RichTerminalDisplay(self.agent_ids, **self.config)
|
|
426
|
+
else:
|
|
427
|
+
raise ValueError(f"Unknown display type: {self.display_type}")
|
|
428
|
+
|
|
429
|
+
# Pass orchestrator reference to display for backend info
|
|
430
|
+
self.display.orchestrator = orchestrator
|
|
431
|
+
|
|
432
|
+
# Initialize logger and display with context info
|
|
433
|
+
log_filename = None
|
|
434
|
+
if self.logger:
|
|
435
|
+
# Add context info to session initialization
|
|
436
|
+
context_info = (
|
|
437
|
+
f"(with {len(messages)//2} previous exchanges)"
|
|
438
|
+
if len(messages) > 1
|
|
439
|
+
else ""
|
|
440
|
+
)
|
|
441
|
+
session_question = f"{question} {context_info}"
|
|
442
|
+
log_filename = self.logger.initialize_session(
|
|
443
|
+
session_question, self.agent_ids
|
|
444
|
+
)
|
|
445
|
+
monitoring = self.logger.get_monitoring_commands()
|
|
446
|
+
print(f"📁 Real-time log: {log_filename}")
|
|
447
|
+
print(f"💡 Monitor with: {monitoring['tail']}")
|
|
448
|
+
print()
|
|
449
|
+
|
|
450
|
+
self.display.initialize(question, log_filename)
|
|
451
|
+
|
|
452
|
+
try:
|
|
453
|
+
# Process coordination stream with conversation context
|
|
454
|
+
full_response = ""
|
|
455
|
+
final_answer = ""
|
|
456
|
+
|
|
457
|
+
# Use the orchestrator's chat method with full message context
|
|
458
|
+
async for chunk in orchestrator.chat(messages):
|
|
459
|
+
content = getattr(chunk, "content", "") or ""
|
|
460
|
+
source = getattr(chunk, "source", None)
|
|
461
|
+
chunk_type = getattr(chunk, "type", "")
|
|
462
|
+
|
|
463
|
+
# Handle agent status updates
|
|
464
|
+
if chunk_type == "agent_status":
|
|
465
|
+
status = getattr(chunk, "status", None)
|
|
466
|
+
if source and status:
|
|
467
|
+
self.display.update_agent_status(source, status)
|
|
468
|
+
continue
|
|
469
|
+
|
|
470
|
+
# Handle builtin tool results
|
|
471
|
+
elif chunk_type == "builtin_tool_results":
|
|
472
|
+
builtin_results = getattr(chunk, "builtin_tool_results", [])
|
|
473
|
+
if builtin_results and source:
|
|
474
|
+
for result in builtin_results:
|
|
475
|
+
tool_type = result.get("tool_type", "unknown")
|
|
476
|
+
status_result = result.get("status", "unknown")
|
|
477
|
+
tool_msg = (
|
|
478
|
+
f"🔧 [{tool_type.title()}] {status_result.title()}"
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
if tool_type in ["code_interpreter", "code_execution"]:
|
|
482
|
+
code = result.get("code", "") or result.get(
|
|
483
|
+
"input", {}
|
|
484
|
+
).get("code", "")
|
|
485
|
+
outputs = result.get("outputs")
|
|
486
|
+
if code:
|
|
487
|
+
tool_msg += f" - Code: {code[:50]}{'...' if len(code) > 50 else ''}"
|
|
488
|
+
if outputs:
|
|
489
|
+
tool_msg += f" - Result: {outputs}"
|
|
490
|
+
elif tool_type == "web_search":
|
|
491
|
+
query = result.get("query", "") or result.get(
|
|
492
|
+
"input", {}
|
|
493
|
+
).get("query", "")
|
|
494
|
+
if query:
|
|
495
|
+
tool_msg += f" - Query: '{query}'"
|
|
496
|
+
|
|
497
|
+
# Display as tool content for the specific agent
|
|
498
|
+
await self._process_agent_content(source, tool_msg)
|
|
499
|
+
continue
|
|
500
|
+
|
|
501
|
+
if content:
|
|
502
|
+
full_response += content
|
|
503
|
+
|
|
504
|
+
# Log chunk
|
|
505
|
+
if self.logger:
|
|
506
|
+
self.logger.log_chunk(source, content, chunk.type)
|
|
507
|
+
|
|
508
|
+
# Process content by source
|
|
509
|
+
await self._process_content(source, content)
|
|
510
|
+
|
|
511
|
+
# Display vote results and get final presentation
|
|
512
|
+
status = orchestrator.get_status()
|
|
513
|
+
vote_results = status.get("vote_results", {})
|
|
514
|
+
selected_agent = status.get("selected_agent")
|
|
515
|
+
|
|
516
|
+
# if vote_results.get('vote_counts'):
|
|
517
|
+
# self._display_vote_results(vote_results)
|
|
518
|
+
# # Allow time for voting results to be visible
|
|
519
|
+
# import time
|
|
520
|
+
# time.sleep(1.0)
|
|
521
|
+
|
|
522
|
+
# Get final presentation from winning agent
|
|
523
|
+
if (
|
|
524
|
+
self.enable_final_presentation
|
|
525
|
+
and selected_agent
|
|
526
|
+
and vote_results.get("vote_counts")
|
|
527
|
+
):
|
|
528
|
+
print(f"\n🎤 Final Presentation from {selected_agent}:")
|
|
529
|
+
print("=" * 60)
|
|
530
|
+
|
|
531
|
+
presentation_content = ""
|
|
532
|
+
try:
|
|
533
|
+
async for chunk in orchestrator.get_final_presentation(
|
|
534
|
+
selected_agent, vote_results
|
|
535
|
+
):
|
|
536
|
+
content = getattr(chunk, "content", "") or ""
|
|
537
|
+
if content:
|
|
538
|
+
# Ensure content is a string
|
|
539
|
+
if isinstance(content, list):
|
|
540
|
+
content = " ".join(str(item) for item in content)
|
|
541
|
+
elif not isinstance(content, str):
|
|
542
|
+
content = str(content)
|
|
543
|
+
|
|
544
|
+
# Simple content accumulation - let the display handle formatting
|
|
545
|
+
presentation_content += content
|
|
546
|
+
|
|
547
|
+
# Log presentation chunk
|
|
548
|
+
if self.logger:
|
|
549
|
+
self.logger.log_chunk(
|
|
550
|
+
selected_agent,
|
|
551
|
+
content,
|
|
552
|
+
getattr(chunk, "type", "presentation"),
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
# Stream presentation to console with consistent flush timing
|
|
556
|
+
self._print_with_flush(content)
|
|
557
|
+
|
|
558
|
+
# Update display
|
|
559
|
+
await self._process_content(selected_agent, content)
|
|
560
|
+
|
|
561
|
+
if getattr(chunk, "type", "") == "done":
|
|
562
|
+
break
|
|
563
|
+
|
|
564
|
+
except Exception as e:
|
|
565
|
+
print(f"\n❌ Error during final presentation: {e}")
|
|
566
|
+
presentation_content = full_response # Fallback
|
|
567
|
+
|
|
568
|
+
final_answer = presentation_content
|
|
569
|
+
print("\n" + "=" * 60)
|
|
570
|
+
# Allow time for final presentation to be fully visible
|
|
571
|
+
time.sleep(1.5)
|
|
572
|
+
|
|
573
|
+
# Get the clean final answer from orchestrator's stored state
|
|
574
|
+
orchestrator_final_answer = None
|
|
575
|
+
if (
|
|
576
|
+
selected_agent
|
|
577
|
+
and hasattr(orchestrator, "agent_states")
|
|
578
|
+
and selected_agent in orchestrator.agent_states
|
|
579
|
+
):
|
|
580
|
+
stored_answer = orchestrator.agent_states[selected_agent].answer
|
|
581
|
+
if stored_answer:
|
|
582
|
+
# Clean up the stored answer
|
|
583
|
+
orchestrator_final_answer = (
|
|
584
|
+
stored_answer.replace("\\", "\n").replace("**", "").strip()
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
# Use orchestrator's clean answer if available, otherwise fall back to presentation
|
|
588
|
+
final_result = (
|
|
589
|
+
orchestrator_final_answer
|
|
590
|
+
if orchestrator_final_answer
|
|
591
|
+
else (final_answer if final_answer else full_response)
|
|
592
|
+
)
|
|
593
|
+
if final_result:
|
|
594
|
+
# print(f"\n🎯 FINAL COORDINATED ANSWER")
|
|
595
|
+
# print("=" * 80)
|
|
596
|
+
# print(f"{final_result.strip()}")
|
|
597
|
+
# print("=" * 80)
|
|
598
|
+
|
|
599
|
+
# Show which agent was selected
|
|
600
|
+
if selected_agent:
|
|
601
|
+
print(f"✅ Selected by: {selected_agent}")
|
|
602
|
+
if vote_results.get("vote_counts"):
|
|
603
|
+
vote_summary = ", ".join(
|
|
604
|
+
[
|
|
605
|
+
f"{agent}: {count}"
|
|
606
|
+
for agent, count in vote_results["vote_counts"].items()
|
|
607
|
+
]
|
|
608
|
+
)
|
|
609
|
+
print(f"🗳️ Vote results: {vote_summary}")
|
|
610
|
+
print()
|
|
611
|
+
|
|
612
|
+
# Finalize session
|
|
613
|
+
if self.logger:
|
|
614
|
+
session_info = self.logger.finalize_session(final_answer, success=True)
|
|
615
|
+
print(f"💾 Session log: {session_info['filename']}")
|
|
616
|
+
print(
|
|
617
|
+
f"⏱️ Duration: {session_info['duration']:.1f}s | Chunks: {session_info['total_chunks']} | Events: {session_info['orchestrator_events']}"
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
return final_result
|
|
621
|
+
|
|
622
|
+
except Exception as e:
|
|
623
|
+
if self.logger:
|
|
624
|
+
self.logger.finalize_session("", success=False)
|
|
625
|
+
raise
|
|
626
|
+
finally:
|
|
627
|
+
# Wait for any pending timeout task to complete before cleanup
|
|
628
|
+
if hasattr(self, "_answer_timeout_task") and self._answer_timeout_task:
|
|
629
|
+
try:
|
|
630
|
+
# Give the task a chance to complete
|
|
631
|
+
await asyncio.wait_for(self._answer_timeout_task, timeout=1.0)
|
|
632
|
+
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
633
|
+
# If it takes too long or was cancelled, force flush
|
|
634
|
+
if (
|
|
635
|
+
hasattr(self, "_answer_buffer")
|
|
636
|
+
and self._answer_buffer
|
|
637
|
+
and not self._final_answer_shown
|
|
638
|
+
):
|
|
639
|
+
await self._flush_final_answer()
|
|
640
|
+
self._answer_timeout_task.cancel()
|
|
641
|
+
|
|
642
|
+
# Final check to flush any remaining buffered answer
|
|
643
|
+
if (
|
|
644
|
+
hasattr(self, "_answer_buffer")
|
|
645
|
+
and self._answer_buffer
|
|
646
|
+
and not self._final_answer_shown
|
|
647
|
+
):
|
|
648
|
+
await self._flush_final_answer()
|
|
649
|
+
|
|
650
|
+
# Small delay to ensure display updates are processed
|
|
651
|
+
await asyncio.sleep(0.1)
|
|
652
|
+
|
|
653
|
+
if self.display:
|
|
654
|
+
self.display.cleanup()
|
|
655
|
+
|
|
656
|
+
def _display_vote_results(self, vote_results: Dict[str, Any]):
|
|
657
|
+
"""Display voting results in a formatted table."""
|
|
658
|
+
print(f"\n🗳️ VOTING RESULTS")
|
|
659
|
+
print("=" * 50)
|
|
660
|
+
|
|
661
|
+
vote_counts = vote_results.get("vote_counts", {})
|
|
662
|
+
voter_details = vote_results.get("voter_details", {})
|
|
663
|
+
winner = vote_results.get("winner")
|
|
664
|
+
is_tie = vote_results.get("is_tie", False)
|
|
665
|
+
|
|
666
|
+
# Display vote counts
|
|
667
|
+
if vote_counts:
|
|
668
|
+
print(f"\n📊 Vote Count:")
|
|
669
|
+
for agent_id, count in sorted(
|
|
670
|
+
vote_counts.items(), key=lambda x: x[1], reverse=True
|
|
671
|
+
):
|
|
672
|
+
winner_mark = "🏆" if agent_id == winner else " "
|
|
673
|
+
tie_mark = " (tie-broken)" if is_tie and agent_id == winner else ""
|
|
674
|
+
print(
|
|
675
|
+
f" {winner_mark} {agent_id}: {count} vote{'s' if count != 1 else ''}{tie_mark}"
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
# Display voter details
|
|
679
|
+
if voter_details:
|
|
680
|
+
print(f"\n🔍 Vote Details:")
|
|
681
|
+
for voted_for, voters in voter_details.items():
|
|
682
|
+
print(f" → {voted_for}:")
|
|
683
|
+
for voter_info in voters:
|
|
684
|
+
voter = voter_info["voter"]
|
|
685
|
+
reason = voter_info["reason"]
|
|
686
|
+
print(f' • {voter}: "{reason}"')
|
|
687
|
+
|
|
688
|
+
# Display tie-breaking info
|
|
689
|
+
if is_tie:
|
|
690
|
+
print(
|
|
691
|
+
f"\n⚖️ Tie broken by agent registration order (orchestrator setup order)"
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
# Display summary stats
|
|
695
|
+
total_votes = vote_results.get("total_votes", 0)
|
|
696
|
+
agents_voted = vote_results.get("agents_voted", 0)
|
|
697
|
+
print(f"\n📈 Summary: {agents_voted}/{total_votes} agents voted")
|
|
698
|
+
print("=" * 50)
|
|
699
|
+
|
|
700
|
+
async def _process_content(self, source: Optional[str], content: str):
|
|
701
|
+
"""Process content from coordination stream."""
|
|
702
|
+
# Handle agent content
|
|
703
|
+
if source in self.agent_ids:
|
|
704
|
+
await self._process_agent_content(source, content)
|
|
705
|
+
|
|
706
|
+
# Handle orchestrator content
|
|
707
|
+
elif source in ["coordination_hub", "orchestrator"] or source is None:
|
|
708
|
+
await self._process_orchestrator_content(content)
|
|
709
|
+
|
|
710
|
+
# Capture coordination events from any source (orchestrator or agents)
|
|
711
|
+
if any(marker in content for marker in ["✅", "🗳️", "🔄", "❌"]):
|
|
712
|
+
clean_line = content.replace("**", "").replace("##", "").strip()
|
|
713
|
+
if clean_line and not any(
|
|
714
|
+
skip in clean_line
|
|
715
|
+
for skip in [
|
|
716
|
+
"result ignored",
|
|
717
|
+
"Starting",
|
|
718
|
+
"Agents Coordinating",
|
|
719
|
+
"Coordinating agents, please wait",
|
|
720
|
+
]
|
|
721
|
+
):
|
|
722
|
+
event = (
|
|
723
|
+
f"🔄 {source}: {clean_line}"
|
|
724
|
+
if source and source not in ["coordination_hub", "orchestrator"]
|
|
725
|
+
else f"🔄 {clean_line}"
|
|
726
|
+
)
|
|
727
|
+
self.display.add_orchestrator_event(event)
|
|
728
|
+
if self.logger:
|
|
729
|
+
self.logger.log_orchestrator_event(event)
|
|
730
|
+
|
|
731
|
+
async def _process_agent_content(self, agent_id: str, content: str):
|
|
732
|
+
"""Process content from a specific agent."""
|
|
733
|
+
# Update agent status - if agent is streaming content, they're working
|
|
734
|
+
# But don't override "completed" status
|
|
735
|
+
current_status = self.display.get_agent_status(agent_id)
|
|
736
|
+
if current_status not in ["working", "completed"]:
|
|
737
|
+
self.display.update_agent_status(agent_id, "working")
|
|
738
|
+
|
|
739
|
+
# Determine content type and process
|
|
740
|
+
if "🔧" in content or "🔄 Vote invalid" in content:
|
|
741
|
+
# Tool usage or status messages
|
|
742
|
+
content_type = "tool" if "🔧" in content else "status"
|
|
743
|
+
self.display.update_agent_content(agent_id, content, content_type)
|
|
744
|
+
|
|
745
|
+
# Update status on completion
|
|
746
|
+
if "new_answer" in content or "vote" in content:
|
|
747
|
+
self.display.update_agent_status(agent_id, "completed")
|
|
748
|
+
|
|
749
|
+
# Log to detailed logger
|
|
750
|
+
if self.logger:
|
|
751
|
+
self.logger.log_agent_content(agent_id, content, content_type)
|
|
752
|
+
|
|
753
|
+
else:
|
|
754
|
+
# Thinking content
|
|
755
|
+
self.display.update_agent_content(agent_id, content, "thinking")
|
|
756
|
+
if self.logger:
|
|
757
|
+
self.logger.log_agent_content(agent_id, content, "thinking")
|
|
758
|
+
|
|
759
|
+
async def _flush_final_answer(self):
|
|
760
|
+
"""Flush the buffered final answer after a timeout to prevent duplicate calls."""
|
|
761
|
+
if self._final_answer_shown or not self._answer_buffer.strip():
|
|
762
|
+
return
|
|
763
|
+
|
|
764
|
+
# Get orchestrator status for voting results and winner
|
|
765
|
+
status = self.orchestrator.get_status()
|
|
766
|
+
selected_agent = status.get("selected_agent", "Unknown")
|
|
767
|
+
vote_results = status.get("vote_results", {})
|
|
768
|
+
|
|
769
|
+
# Mark as shown to prevent duplicate calls
|
|
770
|
+
self._final_answer_shown = True
|
|
771
|
+
|
|
772
|
+
# Show the final answer
|
|
773
|
+
self.display.show_final_answer(
|
|
774
|
+
self._answer_buffer.strip(),
|
|
775
|
+
vote_results=vote_results,
|
|
776
|
+
selected_agent=selected_agent,
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
async def _process_orchestrator_content(self, content: str):
|
|
780
|
+
"""Process content from orchestrator."""
|
|
781
|
+
# Handle final answer - merge with voting info
|
|
782
|
+
if "Final Coordinated Answer" in content:
|
|
783
|
+
# Don't create event yet - wait for actual answer content to merge
|
|
784
|
+
pass
|
|
785
|
+
|
|
786
|
+
# Handle coordination events (provided answer, votes)
|
|
787
|
+
elif any(marker in content for marker in ["✅", "🗳️", "🔄", "❌"]):
|
|
788
|
+
clean_line = content.replace("**", "").replace("##", "").strip()
|
|
789
|
+
if clean_line and not any(
|
|
790
|
+
skip in clean_line
|
|
791
|
+
for skip in [
|
|
792
|
+
"result ignored",
|
|
793
|
+
"Starting",
|
|
794
|
+
"Agents Coordinating",
|
|
795
|
+
"Coordinating agents, please wait",
|
|
796
|
+
]
|
|
797
|
+
):
|
|
798
|
+
event = f"🔄 {clean_line}"
|
|
799
|
+
self.display.add_orchestrator_event(event)
|
|
800
|
+
if self.logger:
|
|
801
|
+
self.logger.log_orchestrator_event(event)
|
|
802
|
+
|
|
803
|
+
# Handle final answer content - buffer it to prevent duplicate calls
|
|
804
|
+
elif "Final Coordinated Answer" not in content and not any(
|
|
805
|
+
marker in content
|
|
806
|
+
for marker in [
|
|
807
|
+
"✅",
|
|
808
|
+
"🗳️",
|
|
809
|
+
"🎯",
|
|
810
|
+
"Starting",
|
|
811
|
+
"Agents Coordinating",
|
|
812
|
+
"🔄",
|
|
813
|
+
"**",
|
|
814
|
+
"result ignored",
|
|
815
|
+
"restart pending",
|
|
816
|
+
]
|
|
817
|
+
):
|
|
818
|
+
# Extract clean final answer content
|
|
819
|
+
clean_content = content.strip()
|
|
820
|
+
if (
|
|
821
|
+
clean_content
|
|
822
|
+
and not clean_content.startswith("---")
|
|
823
|
+
and not clean_content.startswith("*Coordinated by")
|
|
824
|
+
):
|
|
825
|
+
# Add to buffer
|
|
826
|
+
if self._answer_buffer:
|
|
827
|
+
self._answer_buffer += " " + clean_content
|
|
828
|
+
else:
|
|
829
|
+
self._answer_buffer = clean_content
|
|
830
|
+
|
|
831
|
+
# Cancel previous timeout if it exists
|
|
832
|
+
if self._answer_timeout_task:
|
|
833
|
+
self._answer_timeout_task.cancel()
|
|
834
|
+
|
|
835
|
+
# Set a timeout to flush the answer (in case streaming stops)
|
|
836
|
+
self._answer_timeout_task = asyncio.create_task(
|
|
837
|
+
self._schedule_final_answer_flush()
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
# Create event for this chunk but don't call show_final_answer yet
|
|
841
|
+
status = self.orchestrator.get_status()
|
|
842
|
+
selected_agent = status.get("selected_agent", "Unknown")
|
|
843
|
+
vote_results = status.get("vote_results", {})
|
|
844
|
+
vote_counts = vote_results.get("vote_counts", {})
|
|
845
|
+
is_tie = vote_results.get("is_tie", False)
|
|
846
|
+
|
|
847
|
+
# Only create final event for first chunk to avoid spam
|
|
848
|
+
if self._answer_buffer == clean_content: # First chunk
|
|
849
|
+
if vote_counts:
|
|
850
|
+
vote_summary = ", ".join(
|
|
851
|
+
[
|
|
852
|
+
f"{agent}: {count} vote{'s' if count != 1 else ''}"
|
|
853
|
+
for agent, count in vote_counts.items()
|
|
854
|
+
]
|
|
855
|
+
)
|
|
856
|
+
tie_info = (
|
|
857
|
+
" (tie-broken by registration order)" if is_tie else ""
|
|
858
|
+
)
|
|
859
|
+
event = f"🎯 FINAL: {selected_agent} selected ({vote_summary}{tie_info}) → [buffering...]"
|
|
860
|
+
else:
|
|
861
|
+
event = f"🎯 FINAL: {selected_agent} selected → [buffering...]"
|
|
862
|
+
|
|
863
|
+
self.display.add_orchestrator_event(event)
|
|
864
|
+
if self.logger:
|
|
865
|
+
self.logger.log_orchestrator_event(event)
|
|
866
|
+
|
|
867
|
+
async def _schedule_final_answer_flush(self):
|
|
868
|
+
"""Schedule the final answer flush after a delay to collect all chunks."""
|
|
869
|
+
await asyncio.sleep(0.5) # Wait 0.5 seconds for more chunks
|
|
870
|
+
await self._flush_final_answer()
|
|
871
|
+
|
|
872
|
+
def _print_with_flush(self, content: str):
|
|
873
|
+
"""Print content chunks directly without character-by-character flushing."""
|
|
874
|
+
try:
|
|
875
|
+
# Display the entire chunk immediately
|
|
876
|
+
print(content, end="", flush=True)
|
|
877
|
+
except Exception:
|
|
878
|
+
# On any error, fallback to immediate display
|
|
879
|
+
print(content, end="", flush=True)
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
# Convenience functions for common use cases
|
|
883
|
+
async def coordinate_with_terminal_ui(
|
|
884
|
+
orchestrator, question: str, enable_final_presentation: bool = False, **kwargs
|
|
885
|
+
) -> str:
|
|
886
|
+
"""Quick coordination with terminal UI and logging.
|
|
887
|
+
|
|
888
|
+
Args:
|
|
889
|
+
orchestrator: MassGen orchestrator instance
|
|
890
|
+
question: Question for coordination
|
|
891
|
+
enable_final_presentation: Whether to ask winning agent to present final answer
|
|
892
|
+
**kwargs: Additional configuration
|
|
893
|
+
|
|
894
|
+
Returns:
|
|
895
|
+
Final coordinated response
|
|
896
|
+
"""
|
|
897
|
+
ui = CoordinationUI(
|
|
898
|
+
display_type="terminal",
|
|
899
|
+
enable_final_presentation=enable_final_presentation,
|
|
900
|
+
**kwargs,
|
|
901
|
+
)
|
|
902
|
+
return await ui.coordinate(orchestrator, question)
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
async def coordinate_with_simple_ui(
|
|
906
|
+
orchestrator, question: str, enable_final_presentation: bool = False, **kwargs
|
|
907
|
+
) -> str:
|
|
908
|
+
"""Quick coordination with simple UI and logging.
|
|
909
|
+
|
|
910
|
+
Args:
|
|
911
|
+
orchestrator: MassGen orchestrator instance
|
|
912
|
+
question: Question for coordination
|
|
913
|
+
**kwargs: Additional configuration
|
|
914
|
+
|
|
915
|
+
Returns:
|
|
916
|
+
Final coordinated response
|
|
917
|
+
"""
|
|
918
|
+
ui = CoordinationUI(
|
|
919
|
+
display_type="simple",
|
|
920
|
+
enable_final_presentation=enable_final_presentation,
|
|
921
|
+
**kwargs,
|
|
922
|
+
)
|
|
923
|
+
return await ui.coordinate(orchestrator, question)
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
async def coordinate_with_rich_ui(
|
|
927
|
+
orchestrator, question: str, enable_final_presentation: bool = False, **kwargs
|
|
928
|
+
) -> str:
|
|
929
|
+
"""Quick coordination with rich terminal UI and logging.
|
|
930
|
+
|
|
931
|
+
Args:
|
|
932
|
+
orchestrator: MassGen orchestrator instance
|
|
933
|
+
question: Question for coordination
|
|
934
|
+
enable_final_presentation: Whether to ask winning agent to present final answer
|
|
935
|
+
**kwargs: Additional configuration (theme, refresh_rate, etc.)
|
|
936
|
+
|
|
937
|
+
Returns:
|
|
938
|
+
Final coordinated response
|
|
939
|
+
"""
|
|
940
|
+
ui = CoordinationUI(
|
|
941
|
+
display_type="rich_terminal",
|
|
942
|
+
enable_final_presentation=enable_final_presentation,
|
|
943
|
+
**kwargs,
|
|
944
|
+
)
|
|
945
|
+
return await ui.coordinate(orchestrator, question)
|