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.

Files changed (76) hide show
  1. massgen/__init__.py +94 -0
  2. massgen/agent_config.py +507 -0
  3. massgen/backend/CLAUDE_API_RESEARCH.md +266 -0
  4. massgen/backend/Function calling openai responses.md +1161 -0
  5. massgen/backend/GEMINI_API_DOCUMENTATION.md +410 -0
  6. massgen/backend/OPENAI_RESPONSES_API_FORMAT.md +65 -0
  7. massgen/backend/__init__.py +25 -0
  8. massgen/backend/base.py +180 -0
  9. massgen/backend/chat_completions.py +228 -0
  10. massgen/backend/claude.py +661 -0
  11. massgen/backend/gemini.py +652 -0
  12. massgen/backend/grok.py +187 -0
  13. massgen/backend/response.py +397 -0
  14. massgen/chat_agent.py +440 -0
  15. massgen/cli.py +686 -0
  16. massgen/configs/README.md +293 -0
  17. massgen/configs/creative_team.yaml +53 -0
  18. massgen/configs/gemini_4o_claude.yaml +31 -0
  19. massgen/configs/news_analysis.yaml +51 -0
  20. massgen/configs/research_team.yaml +51 -0
  21. massgen/configs/single_agent.yaml +18 -0
  22. massgen/configs/single_flash2.5.yaml +44 -0
  23. massgen/configs/technical_analysis.yaml +51 -0
  24. massgen/configs/three_agents_default.yaml +31 -0
  25. massgen/configs/travel_planning.yaml +51 -0
  26. massgen/configs/two_agents.yaml +39 -0
  27. massgen/frontend/__init__.py +20 -0
  28. massgen/frontend/coordination_ui.py +945 -0
  29. massgen/frontend/displays/__init__.py +24 -0
  30. massgen/frontend/displays/base_display.py +83 -0
  31. massgen/frontend/displays/rich_terminal_display.py +3497 -0
  32. massgen/frontend/displays/simple_display.py +93 -0
  33. massgen/frontend/displays/terminal_display.py +381 -0
  34. massgen/frontend/logging/__init__.py +9 -0
  35. massgen/frontend/logging/realtime_logger.py +197 -0
  36. massgen/message_templates.py +431 -0
  37. massgen/orchestrator.py +1222 -0
  38. massgen/tests/__init__.py +10 -0
  39. massgen/tests/multi_turn_conversation_design.md +214 -0
  40. massgen/tests/multiturn_llm_input_analysis.md +189 -0
  41. massgen/tests/test_case_studies.md +113 -0
  42. massgen/tests/test_claude_backend.py +310 -0
  43. massgen/tests/test_grok_backend.py +160 -0
  44. massgen/tests/test_message_context_building.py +293 -0
  45. massgen/tests/test_rich_terminal_display.py +378 -0
  46. massgen/tests/test_v3_3agents.py +117 -0
  47. massgen/tests/test_v3_simple.py +216 -0
  48. massgen/tests/test_v3_three_agents.py +272 -0
  49. massgen/tests/test_v3_two_agents.py +176 -0
  50. massgen/utils.py +79 -0
  51. massgen/v1/README.md +330 -0
  52. massgen/v1/__init__.py +91 -0
  53. massgen/v1/agent.py +605 -0
  54. massgen/v1/agents.py +330 -0
  55. massgen/v1/backends/gemini.py +584 -0
  56. massgen/v1/backends/grok.py +410 -0
  57. massgen/v1/backends/oai.py +571 -0
  58. massgen/v1/cli.py +351 -0
  59. massgen/v1/config.py +169 -0
  60. massgen/v1/examples/fast-4o-mini-config.yaml +44 -0
  61. massgen/v1/examples/fast_config.yaml +44 -0
  62. massgen/v1/examples/production.yaml +70 -0
  63. massgen/v1/examples/single_agent.yaml +39 -0
  64. massgen/v1/logging.py +974 -0
  65. massgen/v1/main.py +368 -0
  66. massgen/v1/orchestrator.py +1138 -0
  67. massgen/v1/streaming_display.py +1190 -0
  68. massgen/v1/tools.py +160 -0
  69. massgen/v1/types.py +245 -0
  70. massgen/v1/utils.py +199 -0
  71. massgen-0.0.3.dist-info/METADATA +568 -0
  72. massgen-0.0.3.dist-info/RECORD +76 -0
  73. massgen-0.0.3.dist-info/WHEEL +5 -0
  74. massgen-0.0.3.dist-info/entry_points.txt +2 -0
  75. massgen-0.0.3.dist-info/licenses/LICENSE +204 -0
  76. 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)