massgen 0.0.3__py3-none-any.whl → 0.1.0a1__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 +142 -8
- massgen/adapters/__init__.py +29 -0
- massgen/adapters/ag2_adapter.py +483 -0
- massgen/adapters/base.py +183 -0
- massgen/adapters/tests/__init__.py +0 -0
- massgen/adapters/tests/test_ag2_adapter.py +439 -0
- massgen/adapters/tests/test_agent_adapter.py +128 -0
- massgen/adapters/utils/__init__.py +2 -0
- massgen/adapters/utils/ag2_utils.py +236 -0
- massgen/adapters/utils/tests/__init__.py +0 -0
- massgen/adapters/utils/tests/test_ag2_utils.py +138 -0
- massgen/agent_config.py +329 -55
- massgen/api_params_handler/__init__.py +10 -0
- massgen/api_params_handler/_api_params_handler_base.py +99 -0
- massgen/api_params_handler/_chat_completions_api_params_handler.py +176 -0
- massgen/api_params_handler/_claude_api_params_handler.py +113 -0
- massgen/api_params_handler/_response_api_params_handler.py +130 -0
- massgen/backend/__init__.py +39 -4
- massgen/backend/azure_openai.py +385 -0
- massgen/backend/base.py +341 -69
- massgen/backend/base_with_mcp.py +1102 -0
- massgen/backend/capabilities.py +386 -0
- massgen/backend/chat_completions.py +577 -130
- massgen/backend/claude.py +1033 -537
- massgen/backend/claude_code.py +1203 -0
- massgen/backend/cli_base.py +209 -0
- massgen/backend/docs/BACKEND_ARCHITECTURE.md +126 -0
- massgen/backend/{CLAUDE_API_RESEARCH.md → docs/CLAUDE_API_RESEARCH.md} +18 -18
- massgen/backend/{GEMINI_API_DOCUMENTATION.md → docs/GEMINI_API_DOCUMENTATION.md} +9 -9
- massgen/backend/docs/Gemini MCP Integration Analysis.md +1050 -0
- massgen/backend/docs/MCP_IMPLEMENTATION_CLAUDE_BACKEND.md +177 -0
- massgen/backend/docs/MCP_INTEGRATION_RESPONSE_BACKEND.md +352 -0
- massgen/backend/docs/OPENAI_GPT5_MODELS.md +211 -0
- massgen/backend/{OPENAI_RESPONSES_API_FORMAT.md → docs/OPENAI_RESPONSE_API_TOOL_CALLS.md} +3 -3
- massgen/backend/docs/OPENAI_response_streaming.md +20654 -0
- massgen/backend/docs/inference_backend.md +257 -0
- massgen/backend/docs/permissions_and_context_files.md +1085 -0
- massgen/backend/external.py +126 -0
- massgen/backend/gemini.py +1850 -241
- massgen/backend/grok.py +40 -156
- massgen/backend/inference.py +156 -0
- massgen/backend/lmstudio.py +171 -0
- massgen/backend/response.py +1095 -322
- massgen/chat_agent.py +131 -113
- massgen/cli.py +1504 -287
- massgen/config_builder.py +2165 -0
- massgen/configs/BACKEND_CONFIGURATION.md +458 -0
- massgen/configs/README.md +559 -216
- massgen/configs/ag2/ag2_case_study.yaml +27 -0
- massgen/configs/ag2/ag2_coder.yaml +34 -0
- massgen/configs/ag2/ag2_coder_case_study.yaml +36 -0
- massgen/configs/ag2/ag2_gemini.yaml +27 -0
- massgen/configs/ag2/ag2_groupchat.yaml +108 -0
- massgen/configs/ag2/ag2_groupchat_gpt.yaml +118 -0
- massgen/configs/ag2/ag2_single_agent.yaml +21 -0
- massgen/configs/basic/multi/fast_timeout_example.yaml +37 -0
- massgen/configs/basic/multi/gemini_4o_claude.yaml +31 -0
- massgen/configs/basic/multi/gemini_gpt5nano_claude.yaml +36 -0
- massgen/configs/{gemini_4o_claude.yaml → basic/multi/geminicode_4o_claude.yaml} +3 -3
- massgen/configs/basic/multi/geminicode_gpt5nano_claude.yaml +36 -0
- massgen/configs/basic/multi/glm_gemini_claude.yaml +25 -0
- massgen/configs/basic/multi/gpt4o_audio_generation.yaml +30 -0
- massgen/configs/basic/multi/gpt4o_image_generation.yaml +31 -0
- massgen/configs/basic/multi/gpt5nano_glm_qwen.yaml +26 -0
- massgen/configs/basic/multi/gpt5nano_image_understanding.yaml +26 -0
- massgen/configs/{three_agents_default.yaml → basic/multi/three_agents_default.yaml} +8 -4
- massgen/configs/basic/multi/three_agents_opensource.yaml +27 -0
- massgen/configs/basic/multi/three_agents_vllm.yaml +20 -0
- massgen/configs/basic/multi/two_agents_gemini.yaml +19 -0
- massgen/configs/{two_agents.yaml → basic/multi/two_agents_gpt5.yaml} +14 -6
- massgen/configs/basic/multi/two_agents_opensource_lmstudio.yaml +31 -0
- massgen/configs/basic/multi/two_qwen_vllm_sglang.yaml +28 -0
- massgen/configs/{single_agent.yaml → basic/single/single_agent.yaml} +1 -1
- massgen/configs/{single_flash2.5.yaml → basic/single/single_flash2.5.yaml} +1 -2
- massgen/configs/basic/single/single_gemini2.5pro.yaml +16 -0
- massgen/configs/basic/single/single_gpt4o_audio_generation.yaml +22 -0
- massgen/configs/basic/single/single_gpt4o_image_generation.yaml +22 -0
- massgen/configs/basic/single/single_gpt4o_video_generation.yaml +24 -0
- massgen/configs/basic/single/single_gpt5nano.yaml +20 -0
- massgen/configs/basic/single/single_gpt5nano_file_search.yaml +18 -0
- massgen/configs/basic/single/single_gpt5nano_image_understanding.yaml +17 -0
- massgen/configs/basic/single/single_gptoss120b.yaml +15 -0
- massgen/configs/basic/single/single_openrouter_audio_understanding.yaml +15 -0
- massgen/configs/basic/single/single_qwen_video_understanding.yaml +15 -0
- massgen/configs/debug/code_execution/command_filtering_blacklist.yaml +29 -0
- massgen/configs/debug/code_execution/command_filtering_whitelist.yaml +28 -0
- massgen/configs/debug/code_execution/docker_verification.yaml +29 -0
- massgen/configs/debug/skip_coordination_test.yaml +27 -0
- massgen/configs/debug/test_sdk_migration.yaml +17 -0
- massgen/configs/docs/DISCORD_MCP_SETUP.md +208 -0
- massgen/configs/docs/TWITTER_MCP_ENESCINAR_SETUP.md +82 -0
- massgen/configs/providers/azure/azure_openai_multi.yaml +21 -0
- massgen/configs/providers/azure/azure_openai_single.yaml +19 -0
- massgen/configs/providers/claude/claude.yaml +14 -0
- massgen/configs/providers/gemini/gemini_gpt5nano.yaml +28 -0
- massgen/configs/providers/local/lmstudio.yaml +11 -0
- massgen/configs/providers/openai/gpt5.yaml +46 -0
- massgen/configs/providers/openai/gpt5_nano.yaml +46 -0
- massgen/configs/providers/others/grok_single_agent.yaml +19 -0
- massgen/configs/providers/others/zai_coding_team.yaml +108 -0
- massgen/configs/providers/others/zai_glm45.yaml +12 -0
- massgen/configs/{creative_team.yaml → teams/creative/creative_team.yaml} +16 -6
- massgen/configs/{travel_planning.yaml → teams/creative/travel_planning.yaml} +16 -6
- massgen/configs/{news_analysis.yaml → teams/research/news_analysis.yaml} +16 -6
- massgen/configs/{research_team.yaml → teams/research/research_team.yaml} +15 -7
- massgen/configs/{technical_analysis.yaml → teams/research/technical_analysis.yaml} +16 -6
- massgen/configs/tools/code-execution/basic_command_execution.yaml +25 -0
- massgen/configs/tools/code-execution/code_execution_use_case_simple.yaml +41 -0
- massgen/configs/tools/code-execution/docker_claude_code.yaml +32 -0
- massgen/configs/tools/code-execution/docker_multi_agent.yaml +32 -0
- massgen/configs/tools/code-execution/docker_simple.yaml +29 -0
- massgen/configs/tools/code-execution/docker_with_resource_limits.yaml +32 -0
- massgen/configs/tools/code-execution/multi_agent_playwright_automation.yaml +57 -0
- massgen/configs/tools/filesystem/cc_gpt5_gemini_filesystem.yaml +34 -0
- massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +68 -0
- massgen/configs/tools/filesystem/claude_code_flash2.5.yaml +43 -0
- massgen/configs/tools/filesystem/claude_code_flash2.5_gptoss.yaml +49 -0
- massgen/configs/tools/filesystem/claude_code_gpt5nano.yaml +31 -0
- massgen/configs/tools/filesystem/claude_code_single.yaml +40 -0
- massgen/configs/tools/filesystem/fs_permissions_test.yaml +87 -0
- massgen/configs/tools/filesystem/gemini_gemini_workspace_cleanup.yaml +54 -0
- massgen/configs/tools/filesystem/gemini_gpt5_filesystem_casestudy.yaml +30 -0
- massgen/configs/tools/filesystem/gemini_gpt5nano_file_context_path.yaml +43 -0
- massgen/configs/tools/filesystem/gemini_gpt5nano_protected_paths.yaml +45 -0
- massgen/configs/tools/filesystem/gpt5mini_cc_fs_context_path.yaml +31 -0
- massgen/configs/tools/filesystem/grok4_gpt5_gemini_filesystem.yaml +32 -0
- massgen/configs/tools/filesystem/multiturn/grok4_gpt5_claude_code_filesystem_multiturn.yaml +58 -0
- massgen/configs/tools/filesystem/multiturn/grok4_gpt5_gemini_filesystem_multiturn.yaml +58 -0
- massgen/configs/tools/filesystem/multiturn/two_claude_code_filesystem_multiturn.yaml +47 -0
- massgen/configs/tools/filesystem/multiturn/two_gemini_flash_filesystem_multiturn.yaml +48 -0
- massgen/configs/tools/mcp/claude_code_discord_mcp_example.yaml +27 -0
- massgen/configs/tools/mcp/claude_code_simple_mcp.yaml +35 -0
- massgen/configs/tools/mcp/claude_code_twitter_mcp_example.yaml +32 -0
- massgen/configs/tools/mcp/claude_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/claude_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/five_agents_travel_mcp_test.yaml +157 -0
- massgen/configs/tools/mcp/five_agents_weather_mcp_test.yaml +103 -0
- massgen/configs/tools/mcp/gemini_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test.yaml +23 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test_sharing.yaml +23 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test_single_agent.yaml +17 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test_with_claude_code.yaml +24 -0
- massgen/configs/tools/mcp/gemini_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/gemini_notion_mcp.yaml +52 -0
- massgen/configs/tools/mcp/gpt5_nano_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/gpt5_nano_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/gpt5mini_claude_code_discord_mcp_example.yaml +38 -0
- massgen/configs/tools/mcp/gpt_oss_mcp_example.yaml +25 -0
- massgen/configs/tools/mcp/gpt_oss_mcp_test.yaml +28 -0
- massgen/configs/tools/mcp/grok3_mini_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/grok3_mini_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/multimcp_gemini.yaml +111 -0
- massgen/configs/tools/mcp/qwen_api_mcp_example.yaml +25 -0
- massgen/configs/tools/mcp/qwen_api_mcp_test.yaml +28 -0
- massgen/configs/tools/mcp/qwen_local_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/qwen_local_mcp_test.yaml +27 -0
- massgen/configs/tools/planning/five_agents_discord_mcp_planning_mode.yaml +140 -0
- massgen/configs/tools/planning/five_agents_filesystem_mcp_planning_mode.yaml +151 -0
- massgen/configs/tools/planning/five_agents_notion_mcp_planning_mode.yaml +151 -0
- massgen/configs/tools/planning/five_agents_twitter_mcp_planning_mode.yaml +155 -0
- massgen/configs/tools/planning/gpt5_mini_case_study_mcp_planning_mode.yaml +73 -0
- massgen/configs/tools/web-search/claude_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/gemini_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/gpt5_mini_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/gpt_oss_streamable_http_test.yaml +44 -0
- massgen/configs/tools/web-search/grok3_mini_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/qwen_api_streamable_http_test.yaml +44 -0
- massgen/configs/tools/web-search/qwen_local_streamable_http_test.yaml +43 -0
- massgen/coordination_tracker.py +708 -0
- massgen/docker/README.md +462 -0
- massgen/filesystem_manager/__init__.py +21 -0
- massgen/filesystem_manager/_base.py +9 -0
- massgen/filesystem_manager/_code_execution_server.py +545 -0
- massgen/filesystem_manager/_docker_manager.py +477 -0
- massgen/filesystem_manager/_file_operation_tracker.py +248 -0
- massgen/filesystem_manager/_filesystem_manager.py +813 -0
- massgen/filesystem_manager/_path_permission_manager.py +1261 -0
- massgen/filesystem_manager/_workspace_tools_server.py +1815 -0
- massgen/formatter/__init__.py +10 -0
- massgen/formatter/_chat_completions_formatter.py +284 -0
- massgen/formatter/_claude_formatter.py +235 -0
- massgen/formatter/_formatter_base.py +156 -0
- massgen/formatter/_response_formatter.py +263 -0
- massgen/frontend/__init__.py +1 -2
- massgen/frontend/coordination_ui.py +471 -286
- massgen/frontend/displays/base_display.py +56 -11
- massgen/frontend/displays/create_coordination_table.py +1956 -0
- massgen/frontend/displays/rich_terminal_display.py +1259 -619
- massgen/frontend/displays/simple_display.py +9 -4
- massgen/frontend/displays/terminal_display.py +27 -68
- massgen/logger_config.py +681 -0
- massgen/mcp_tools/README.md +232 -0
- massgen/mcp_tools/__init__.py +105 -0
- massgen/mcp_tools/backend_utils.py +1035 -0
- massgen/mcp_tools/circuit_breaker.py +195 -0
- massgen/mcp_tools/client.py +894 -0
- massgen/mcp_tools/config_validator.py +138 -0
- massgen/mcp_tools/docs/circuit_breaker.md +646 -0
- massgen/mcp_tools/docs/client.md +950 -0
- massgen/mcp_tools/docs/config_validator.md +478 -0
- massgen/mcp_tools/docs/exceptions.md +1165 -0
- massgen/mcp_tools/docs/security.md +854 -0
- massgen/mcp_tools/exceptions.py +338 -0
- massgen/mcp_tools/hooks.py +212 -0
- massgen/mcp_tools/security.py +780 -0
- massgen/message_templates.py +342 -64
- massgen/orchestrator.py +1515 -241
- massgen/stream_chunk/__init__.py +35 -0
- massgen/stream_chunk/base.py +92 -0
- massgen/stream_chunk/multimodal.py +237 -0
- massgen/stream_chunk/text.py +162 -0
- massgen/tests/mcp_test_server.py +150 -0
- massgen/tests/multi_turn_conversation_design.md +0 -8
- massgen/tests/test_azure_openai_backend.py +156 -0
- massgen/tests/test_backend_capabilities.py +262 -0
- massgen/tests/test_backend_event_loop_all.py +179 -0
- massgen/tests/test_chat_completions_refactor.py +142 -0
- massgen/tests/test_claude_backend.py +15 -28
- massgen/tests/test_claude_code.py +268 -0
- massgen/tests/test_claude_code_context_sharing.py +233 -0
- massgen/tests/test_claude_code_orchestrator.py +175 -0
- massgen/tests/test_cli_backends.py +180 -0
- massgen/tests/test_code_execution.py +679 -0
- massgen/tests/test_external_agent_backend.py +134 -0
- massgen/tests/test_final_presentation_fallback.py +237 -0
- massgen/tests/test_gemini_planning_mode.py +351 -0
- massgen/tests/test_grok_backend.py +7 -10
- massgen/tests/test_http_mcp_server.py +42 -0
- massgen/tests/test_integration_simple.py +198 -0
- massgen/tests/test_mcp_blocking.py +125 -0
- massgen/tests/test_message_context_building.py +29 -47
- massgen/tests/test_orchestrator_final_presentation.py +48 -0
- massgen/tests/test_path_permission_manager.py +2087 -0
- massgen/tests/test_rich_terminal_display.py +14 -13
- massgen/tests/test_timeout.py +133 -0
- massgen/tests/test_v3_3agents.py +11 -12
- massgen/tests/test_v3_simple.py +8 -13
- massgen/tests/test_v3_three_agents.py +11 -18
- massgen/tests/test_v3_two_agents.py +8 -13
- massgen/token_manager/__init__.py +7 -0
- massgen/token_manager/token_manager.py +400 -0
- massgen/utils.py +52 -16
- massgen/v1/agent.py +45 -91
- massgen/v1/agents.py +18 -53
- massgen/v1/backends/gemini.py +50 -153
- massgen/v1/backends/grok.py +21 -54
- massgen/v1/backends/oai.py +39 -111
- massgen/v1/cli.py +36 -93
- massgen/v1/config.py +8 -12
- massgen/v1/logging.py +43 -127
- massgen/v1/main.py +18 -32
- massgen/v1/orchestrator.py +68 -209
- massgen/v1/streaming_display.py +62 -163
- massgen/v1/tools.py +8 -12
- massgen/v1/types.py +9 -23
- massgen/v1/utils.py +5 -23
- massgen-0.1.0a1.dist-info/METADATA +1287 -0
- massgen-0.1.0a1.dist-info/RECORD +273 -0
- massgen-0.1.0a1.dist-info/entry_points.txt +2 -0
- massgen/frontend/logging/__init__.py +0 -9
- massgen/frontend/logging/realtime_logger.py +0 -197
- massgen-0.0.3.dist-info/METADATA +0 -568
- massgen-0.0.3.dist-info/RECORD +0 -76
- massgen-0.0.3.dist-info/entry_points.txt +0 -2
- /massgen/backend/{Function calling openai responses.md → docs/Function calling openai responses.md} +0 -0
- {massgen-0.0.3.dist-info → massgen-0.1.0a1.dist-info}/WHEEL +0 -0
- {massgen-0.0.3.dist-info → massgen-0.1.0a1.dist-info}/licenses/LICENSE +0 -0
- {massgen-0.0.3.dist-info → massgen-0.1.0a1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1956 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Multi-Agent Coordination Event Table Generator
|
|
5
|
+
|
|
6
|
+
Parses coordination_events.json and generates a formatted table showing
|
|
7
|
+
the progression of agent interactions across rounds.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any, Dict, List, Optional, Union
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from rich import box
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
|
|
21
|
+
RICH_AVAILABLE = True
|
|
22
|
+
except ImportError:
|
|
23
|
+
RICH_AVAILABLE = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def display_scrollable_content_macos(
|
|
27
|
+
console: Console,
|
|
28
|
+
content_items: List[Any],
|
|
29
|
+
title: str = "",
|
|
30
|
+
) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Display scrollable content with macOS-compatible navigation.
|
|
33
|
+
Works around macOS Terminal's issues with Rich's pager.
|
|
34
|
+
"""
|
|
35
|
+
if not content_items:
|
|
36
|
+
console.print("[dim]No content to display[/dim]")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
# Clear screen and move cursor to top
|
|
40
|
+
console.clear()
|
|
41
|
+
|
|
42
|
+
# Move cursor to top-left corner to ensure we start at the beginning
|
|
43
|
+
console.print("\033[H", end="")
|
|
44
|
+
|
|
45
|
+
# Print title if provided
|
|
46
|
+
if title:
|
|
47
|
+
console.print(f"\n[bold bright_green]{title}[/bold bright_green]\n")
|
|
48
|
+
|
|
49
|
+
# Print content
|
|
50
|
+
for item in content_items:
|
|
51
|
+
console.print(item)
|
|
52
|
+
|
|
53
|
+
# Show instructions and wait for input
|
|
54
|
+
console.print("\n" + "=" * 80)
|
|
55
|
+
console.print(
|
|
56
|
+
"[bright_cyan]Press Enter to return to agent selector...[/bright_cyan]",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
input() # Wait for Enter key
|
|
61
|
+
except (KeyboardInterrupt, EOFError):
|
|
62
|
+
pass # Handle Ctrl+C gracefully
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def display_with_native_pager(
|
|
66
|
+
console: Console,
|
|
67
|
+
content_items: List[Any],
|
|
68
|
+
title: str = "",
|
|
69
|
+
) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Use the system's native pager (less/more) for better scrolling support.
|
|
72
|
+
Falls back to simple display if pager is not available.
|
|
73
|
+
"""
|
|
74
|
+
import subprocess
|
|
75
|
+
import tempfile
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
# Create temporary file with content
|
|
79
|
+
with tempfile.NamedTemporaryFile(
|
|
80
|
+
mode="w",
|
|
81
|
+
suffix=".txt",
|
|
82
|
+
delete=False,
|
|
83
|
+
) as tmp_file:
|
|
84
|
+
if title:
|
|
85
|
+
tmp_file.write(f"{title}\n")
|
|
86
|
+
tmp_file.write("=" * len(title) + "\n\n")
|
|
87
|
+
|
|
88
|
+
# Convert Rich content to plain text
|
|
89
|
+
for item in content_items:
|
|
90
|
+
if hasattr(item, "__rich_console__"):
|
|
91
|
+
# For Rich objects, render to plain text
|
|
92
|
+
with console.capture() as capture:
|
|
93
|
+
console.print(item)
|
|
94
|
+
tmp_file.write(capture.get() + "\n")
|
|
95
|
+
else:
|
|
96
|
+
tmp_file.write(str(item) + "\n")
|
|
97
|
+
|
|
98
|
+
tmp_file.write("\n" + "=" * 80 + "\n")
|
|
99
|
+
tmp_file.write("Press 'q' to quit, arrow keys or j/k to scroll\n")
|
|
100
|
+
tmp_file_path = tmp_file.name
|
|
101
|
+
|
|
102
|
+
# Use system pager
|
|
103
|
+
if sys.platform == "darwin": # macOS
|
|
104
|
+
pager_cmd = [
|
|
105
|
+
"less",
|
|
106
|
+
"-R",
|
|
107
|
+
"-S",
|
|
108
|
+
] # -R for colors, -S for no wrap, start at top
|
|
109
|
+
else:
|
|
110
|
+
pager_cmd = ["less", "-R"]
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
subprocess.run(pager_cmd + [tmp_file_path], check=True)
|
|
114
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
115
|
+
# Fallback to 'more' if 'less' is not available
|
|
116
|
+
try:
|
|
117
|
+
subprocess.run(["more", tmp_file_path], check=True)
|
|
118
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
119
|
+
# Final fallback to simple display
|
|
120
|
+
display_scrollable_content_macos(console, content_items, title)
|
|
121
|
+
|
|
122
|
+
# Clean up temporary file
|
|
123
|
+
try:
|
|
124
|
+
os.unlink(tmp_file_path)
|
|
125
|
+
except OSError:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
except Exception:
|
|
129
|
+
# Fallback to simple display on any error
|
|
130
|
+
display_scrollable_content_macos(console, content_items, title)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def is_macos_terminal() -> bool:
|
|
134
|
+
"""Check if running in macOS Terminal or similar."""
|
|
135
|
+
if sys.platform != "darwin":
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
term_program = os.environ.get("TERM_PROGRAM", "").lower()
|
|
139
|
+
return term_program in ["apple_terminal", "terminal", "iterm.app", ""]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_optimal_display_method() -> Any:
|
|
143
|
+
"""Get the optimal display method for the current platform."""
|
|
144
|
+
if sys.platform == "darwin":
|
|
145
|
+
# Try native pager first on all macOS terminals since less works well
|
|
146
|
+
return "native_pager"
|
|
147
|
+
else:
|
|
148
|
+
return "rich_pager" # Use Rich's pager on Linux/Windows
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass
|
|
152
|
+
class AgentState:
|
|
153
|
+
"""Track state for a single agent"""
|
|
154
|
+
|
|
155
|
+
status: str = "idle"
|
|
156
|
+
current_answer: Optional[str] = None
|
|
157
|
+
answer_preview: Optional[str] = None
|
|
158
|
+
vote: Optional[str] = None
|
|
159
|
+
vote_reason: Optional[str] = None
|
|
160
|
+
context: List[str] = field(default_factory=list)
|
|
161
|
+
round: int = 0
|
|
162
|
+
is_final: bool = False
|
|
163
|
+
has_final_answer: bool = False
|
|
164
|
+
is_selected_winner: bool = False
|
|
165
|
+
has_voted: bool = False # Track if agent has already voted
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass
|
|
169
|
+
class RoundData:
|
|
170
|
+
"""Data for a single round"""
|
|
171
|
+
|
|
172
|
+
round_num: int
|
|
173
|
+
round_type: str # "R0", "R1", "R2", ... "FINAL"
|
|
174
|
+
agent_states: Dict[str, AgentState]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class CoordinationTableBuilder:
|
|
178
|
+
def __init__(self, data: Union[List[Dict[str, Any]], Dict[str, Any]]):
|
|
179
|
+
# Handle both old format (list of events) and new format (dict with
|
|
180
|
+
# metadata)
|
|
181
|
+
if isinstance(data, dict) and "events" in data:
|
|
182
|
+
self.events = data["events"]
|
|
183
|
+
self.session_metadata = data.get("session_metadata", {})
|
|
184
|
+
else:
|
|
185
|
+
self.events = data if isinstance(data, list) else []
|
|
186
|
+
self.session_metadata = {}
|
|
187
|
+
|
|
188
|
+
self.agents = self._extract_agents()
|
|
189
|
+
self.agent_mapping = self._create_agent_mapping()
|
|
190
|
+
self.agent_answers = self._extract_answer_previews()
|
|
191
|
+
self.final_winner = self._find_final_winner()
|
|
192
|
+
self.final_round_num = self._find_final_round_number()
|
|
193
|
+
self.agent_vote_rounds = self._track_vote_rounds()
|
|
194
|
+
self.rounds = self._process_events()
|
|
195
|
+
self.user_question = self._extract_user_question()
|
|
196
|
+
|
|
197
|
+
def _extract_agents(self) -> List[str]:
|
|
198
|
+
"""Extract unique agent IDs from events using original orchestrator order"""
|
|
199
|
+
# First try to get agent order from session metadata
|
|
200
|
+
metadata_agents = self.session_metadata.get("agent_ids", [])
|
|
201
|
+
if metadata_agents:
|
|
202
|
+
return list(metadata_agents)
|
|
203
|
+
|
|
204
|
+
# Fallback: extract from events and sort for consistency
|
|
205
|
+
agents = set()
|
|
206
|
+
for event in self.events:
|
|
207
|
+
agent_id = event.get("agent_id")
|
|
208
|
+
if agent_id and agent_id not in [None, "null"]:
|
|
209
|
+
agents.add(agent_id)
|
|
210
|
+
return sorted(list(agents))
|
|
211
|
+
|
|
212
|
+
def _create_agent_mapping(self) -> Dict[str, str]:
|
|
213
|
+
"""Create explicit mapping from agent_id to agent_number for answer labels"""
|
|
214
|
+
mapping = {}
|
|
215
|
+
for i, agent_id in enumerate(self.agents, 1):
|
|
216
|
+
mapping[agent_id] = str(i)
|
|
217
|
+
return mapping
|
|
218
|
+
|
|
219
|
+
def _extract_user_question(self) -> str:
|
|
220
|
+
"""Extract the user question from session metadata"""
|
|
221
|
+
return str(
|
|
222
|
+
self.session_metadata.get("user_prompt", "No user prompt found"),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def _extract_answer_previews(self) -> Dict[str, str]:
|
|
226
|
+
"""Extract the actual answer text for each agent using explicit mapping"""
|
|
227
|
+
answers = {}
|
|
228
|
+
|
|
229
|
+
# Try to get from final_agent_selected event
|
|
230
|
+
for event in self.events:
|
|
231
|
+
if event["event_type"] == "final_agent_selected":
|
|
232
|
+
context = event.get("context", {})
|
|
233
|
+
answers_for_context = context.get("answers_for_context", {})
|
|
234
|
+
|
|
235
|
+
# Map answers to agents using explicit agent mapping
|
|
236
|
+
for label, answer in answers_for_context.items():
|
|
237
|
+
# Direct match: label is an agent_id
|
|
238
|
+
if label in self.agents:
|
|
239
|
+
answers[label] = answer
|
|
240
|
+
else:
|
|
241
|
+
# Map answer label to agent using our explicit mapping
|
|
242
|
+
# For labels like "agent1.1", extract the number and
|
|
243
|
+
# find matching agent
|
|
244
|
+
if label.startswith("agent") and "." in label:
|
|
245
|
+
try:
|
|
246
|
+
# Extract agent number from label (e.g.,
|
|
247
|
+
# "agent1.1" -> "1")
|
|
248
|
+
agent_num = label.split(".")[0][5:] # Remove "agent" prefix
|
|
249
|
+
# Find agent with this number in our mapping
|
|
250
|
+
for (
|
|
251
|
+
agent_id,
|
|
252
|
+
mapped_num,
|
|
253
|
+
) in self.agent_mapping.items():
|
|
254
|
+
if mapped_num == agent_num:
|
|
255
|
+
answers[agent_id] = answer
|
|
256
|
+
break
|
|
257
|
+
except (IndexError, ValueError):
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
return answers
|
|
261
|
+
|
|
262
|
+
def _find_final_winner(self) -> Optional[str]:
|
|
263
|
+
"""Find which agent was selected as the final winner"""
|
|
264
|
+
for event in self.events:
|
|
265
|
+
if event["event_type"] == "final_agent_selected":
|
|
266
|
+
agent_id = event.get("agent_id")
|
|
267
|
+
return agent_id if agent_id is not None else None
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
def _find_final_round_number(self) -> Optional[int]:
|
|
271
|
+
"""Find which round number is the final round"""
|
|
272
|
+
for event in self.events:
|
|
273
|
+
if event["event_type"] == "final_round_start":
|
|
274
|
+
context = event.get("context", {})
|
|
275
|
+
round_num = context.get("round", context.get("final_round"))
|
|
276
|
+
return int(round_num) if round_num is not None else None
|
|
277
|
+
|
|
278
|
+
# If no explicit final round, check for final_answer events
|
|
279
|
+
for event in self.events:
|
|
280
|
+
if event["event_type"] == "final_answer":
|
|
281
|
+
context = event.get("context", {})
|
|
282
|
+
round_num = context.get("round")
|
|
283
|
+
return int(round_num) if round_num is not None else None
|
|
284
|
+
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
def _track_vote_rounds(self) -> Dict[str, int]:
|
|
288
|
+
"""Track which round each agent cast their vote"""
|
|
289
|
+
vote_rounds = {}
|
|
290
|
+
for event in self.events:
|
|
291
|
+
if event["event_type"] == "vote_cast":
|
|
292
|
+
agent_id = event.get("agent_id")
|
|
293
|
+
context = event.get("context", {})
|
|
294
|
+
round_num = context.get("round", 0)
|
|
295
|
+
if agent_id:
|
|
296
|
+
vote_rounds[agent_id] = round_num
|
|
297
|
+
return vote_rounds
|
|
298
|
+
|
|
299
|
+
def _process_events(self) -> List[RoundData]:
|
|
300
|
+
"""Process events into rounds with proper organization"""
|
|
301
|
+
# Find all unique rounds
|
|
302
|
+
all_rounds = set()
|
|
303
|
+
for event in self.events:
|
|
304
|
+
context = event.get("context", {})
|
|
305
|
+
round_num = context.get("round", 0)
|
|
306
|
+
all_rounds.add(round_num)
|
|
307
|
+
|
|
308
|
+
# Exclude final round from regular rounds if it exists
|
|
309
|
+
regular_rounds = sorted(
|
|
310
|
+
(all_rounds - {self.final_round_num} if self.final_round_num else all_rounds),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Initialize round states
|
|
314
|
+
rounds = {}
|
|
315
|
+
for r in regular_rounds:
|
|
316
|
+
rounds[r] = {agent: AgentState(round=r) for agent in self.agents}
|
|
317
|
+
|
|
318
|
+
# Add final round if exists
|
|
319
|
+
if self.final_round_num is not None:
|
|
320
|
+
rounds[self.final_round_num] = {agent: AgentState(round=self.final_round_num) for agent in self.agents}
|
|
321
|
+
|
|
322
|
+
# Process events
|
|
323
|
+
for event in self.events:
|
|
324
|
+
event_type = event["event_type"]
|
|
325
|
+
agent_id = event.get("agent_id")
|
|
326
|
+
context = event.get("context", {})
|
|
327
|
+
|
|
328
|
+
if agent_id and agent_id in self.agents:
|
|
329
|
+
# Determine the round for this event
|
|
330
|
+
round_num = context.get("round", 0)
|
|
331
|
+
|
|
332
|
+
# Special handling for votes and answers that specify rounds
|
|
333
|
+
if event_type == "vote_cast":
|
|
334
|
+
round_num = context.get("round", 0)
|
|
335
|
+
elif event_type == "new_answer":
|
|
336
|
+
round_num = context.get("round", 0)
|
|
337
|
+
elif event_type == "restart_completed":
|
|
338
|
+
round_num = context.get(
|
|
339
|
+
"agent_round",
|
|
340
|
+
context.get("round", 0),
|
|
341
|
+
)
|
|
342
|
+
elif event_type == "final_answer":
|
|
343
|
+
round_num = self.final_round_num if self.final_round_num else context.get("round", 0)
|
|
344
|
+
|
|
345
|
+
if round_num in rounds:
|
|
346
|
+
agent_state = rounds[round_num][agent_id]
|
|
347
|
+
|
|
348
|
+
if event_type == "context_received":
|
|
349
|
+
labels = context.get("available_answer_labels", [])
|
|
350
|
+
agent_state.context = labels
|
|
351
|
+
|
|
352
|
+
elif event_type == "new_answer":
|
|
353
|
+
label = context.get("label")
|
|
354
|
+
if label:
|
|
355
|
+
agent_state.current_answer = label
|
|
356
|
+
# Get preview from saved answers
|
|
357
|
+
if agent_id in self.agent_answers:
|
|
358
|
+
agent_state.answer_preview = self.agent_answers[agent_id]
|
|
359
|
+
|
|
360
|
+
elif event_type == "vote_cast":
|
|
361
|
+
agent_state.vote = context.get("voted_for_label")
|
|
362
|
+
agent_state.vote_reason = context.get("reason")
|
|
363
|
+
agent_state.has_voted = True
|
|
364
|
+
|
|
365
|
+
elif event_type == "final_answer":
|
|
366
|
+
agent_state.has_final_answer = True
|
|
367
|
+
label = context.get("label")
|
|
368
|
+
agent_state.current_answer = f"Final answer provided ({label})"
|
|
369
|
+
agent_state.is_final = True
|
|
370
|
+
# Try to get the actual answer content if available
|
|
371
|
+
if agent_id in self.agent_answers:
|
|
372
|
+
agent_state.answer_preview = self.agent_answers[agent_id]
|
|
373
|
+
agent_state.current_answer = self.agent_answers[agent_id]
|
|
374
|
+
|
|
375
|
+
elif event_type == "final_agent_selected":
|
|
376
|
+
agent_state.is_selected_winner = True
|
|
377
|
+
|
|
378
|
+
elif event_type == "status_change":
|
|
379
|
+
status = event.get("details", "").replace(
|
|
380
|
+
"Changed to status: ",
|
|
381
|
+
"",
|
|
382
|
+
)
|
|
383
|
+
agent_state.status = status
|
|
384
|
+
|
|
385
|
+
# Mark non-winner as completed in FINAL round
|
|
386
|
+
if self.final_winner and self.final_round_num in rounds:
|
|
387
|
+
for agent in self.agents:
|
|
388
|
+
if agent != self.final_winner:
|
|
389
|
+
rounds[self.final_round_num][agent].status = "completed"
|
|
390
|
+
|
|
391
|
+
# Build final round list
|
|
392
|
+
round_list = []
|
|
393
|
+
|
|
394
|
+
# Add regular rounds
|
|
395
|
+
for r in regular_rounds:
|
|
396
|
+
round_type = f"R{r}"
|
|
397
|
+
round_list.append(
|
|
398
|
+
RoundData(
|
|
399
|
+
r,
|
|
400
|
+
round_type,
|
|
401
|
+
rounds.get(
|
|
402
|
+
r,
|
|
403
|
+
{agent: AgentState() for agent in self.agents},
|
|
404
|
+
),
|
|
405
|
+
),
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# Add FINAL round if exists
|
|
409
|
+
if self.final_round_num is not None and self.final_round_num in rounds:
|
|
410
|
+
round_list.append(
|
|
411
|
+
RoundData(
|
|
412
|
+
self.final_round_num,
|
|
413
|
+
"FINAL",
|
|
414
|
+
rounds[self.final_round_num],
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
return round_list
|
|
419
|
+
|
|
420
|
+
def _format_cell(self, content: str, width: int) -> str:
|
|
421
|
+
"""Format content to fit within cell width, centered"""
|
|
422
|
+
if not content:
|
|
423
|
+
return " " * width
|
|
424
|
+
|
|
425
|
+
if len(content) <= width:
|
|
426
|
+
return content.center(width)
|
|
427
|
+
else:
|
|
428
|
+
# Truncate if too long
|
|
429
|
+
truncated = content[: width - 3] + "..."
|
|
430
|
+
return truncated.center(width)
|
|
431
|
+
|
|
432
|
+
def _build_agent_cell_content(
|
|
433
|
+
self,
|
|
434
|
+
agent_state: AgentState,
|
|
435
|
+
round_type: str,
|
|
436
|
+
agent_id: str,
|
|
437
|
+
round_num: int,
|
|
438
|
+
) -> List[str]:
|
|
439
|
+
"""Build the content for an agent's cell in a round"""
|
|
440
|
+
lines = []
|
|
441
|
+
|
|
442
|
+
# Determine if we should show context (but not for voting agents)
|
|
443
|
+
# Show context only if agent is doing something meaningful with it (but
|
|
444
|
+
# not voting)
|
|
445
|
+
show_context = (
|
|
446
|
+
(agent_state.current_answer and not agent_state.vote)
|
|
447
|
+
or agent_state.has_final_answer # Agent answered (but didn't vote)
|
|
448
|
+
or agent_state.status in ["streaming", "answering"] # Agent has final answer # Agent is actively working
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Don't show context for completed agents in FINAL round
|
|
452
|
+
if round_type == "FINAL" and agent_state.status == "completed":
|
|
453
|
+
show_context = False
|
|
454
|
+
|
|
455
|
+
# Add context if appropriate
|
|
456
|
+
if show_context:
|
|
457
|
+
if agent_state.context:
|
|
458
|
+
context_str = f"Context: [{', '.join(agent_state.context)}]"
|
|
459
|
+
else:
|
|
460
|
+
context_str = "Context: []"
|
|
461
|
+
lines.append(context_str)
|
|
462
|
+
|
|
463
|
+
# Add content based on what happened in this round
|
|
464
|
+
# Check for votes first, regardless of round type
|
|
465
|
+
if agent_state.vote:
|
|
466
|
+
# Agent voted in this round - show Context first, then vote
|
|
467
|
+
if agent_state.context:
|
|
468
|
+
lines.append(f"Context: [{', '.join(agent_state.context)}]")
|
|
469
|
+
lines.append(f"VOTE: {agent_state.vote}")
|
|
470
|
+
if agent_state.vote_reason:
|
|
471
|
+
reason = agent_state.vote_reason[:47] + "..." if len(agent_state.vote_reason) > 50 else agent_state.vote_reason
|
|
472
|
+
lines.append(f"Reason: {reason}")
|
|
473
|
+
|
|
474
|
+
elif round_type == "FINAL":
|
|
475
|
+
# Final presentation round
|
|
476
|
+
if agent_state.has_final_answer:
|
|
477
|
+
lines.append(f"FINAL ANSWER: {agent_state.current_answer}")
|
|
478
|
+
if agent_state.answer_preview:
|
|
479
|
+
clean_preview = agent_state.answer_preview.replace(
|
|
480
|
+
"\n",
|
|
481
|
+
" ",
|
|
482
|
+
).strip()
|
|
483
|
+
lines.append(f"Preview: {clean_preview}")
|
|
484
|
+
else:
|
|
485
|
+
lines.append("Preview: [Answer not available]")
|
|
486
|
+
elif agent_state.status == "completed":
|
|
487
|
+
lines.append("(completed)")
|
|
488
|
+
else:
|
|
489
|
+
lines.append("(waiting)")
|
|
490
|
+
|
|
491
|
+
elif agent_state.current_answer and not agent_state.vote:
|
|
492
|
+
# Agent provided an answer in this round
|
|
493
|
+
lines.append(f"NEW ANSWER: {agent_state.current_answer}")
|
|
494
|
+
if agent_state.answer_preview:
|
|
495
|
+
clean_preview = agent_state.answer_preview.replace(
|
|
496
|
+
"\n",
|
|
497
|
+
" ",
|
|
498
|
+
).strip()
|
|
499
|
+
lines.append(f"Preview: {clean_preview}")
|
|
500
|
+
else:
|
|
501
|
+
lines.append("Preview: [Answer not available]")
|
|
502
|
+
|
|
503
|
+
elif agent_state.status in ["streaming", "answering"]:
|
|
504
|
+
lines.append("(answering)")
|
|
505
|
+
|
|
506
|
+
elif agent_state.status == "voted":
|
|
507
|
+
lines.append("(voted)")
|
|
508
|
+
|
|
509
|
+
elif agent_state.status == "answered":
|
|
510
|
+
lines.append("(answered)")
|
|
511
|
+
|
|
512
|
+
else:
|
|
513
|
+
lines.append("(waiting)")
|
|
514
|
+
|
|
515
|
+
return lines
|
|
516
|
+
|
|
517
|
+
def generate_event_table(self) -> str:
|
|
518
|
+
"""Generate an event-driven formatted table"""
|
|
519
|
+
num_agents = len(self.agents)
|
|
520
|
+
# Dynamic cell width based on number of agents
|
|
521
|
+
if num_agents <= 2:
|
|
522
|
+
cell_width = 60
|
|
523
|
+
elif num_agents == 3:
|
|
524
|
+
cell_width = 40
|
|
525
|
+
elif num_agents == 4:
|
|
526
|
+
cell_width = 30
|
|
527
|
+
else: # 5+ agents
|
|
528
|
+
cell_width = 25
|
|
529
|
+
total_width = 10 + (cell_width + 1) * num_agents + 1
|
|
530
|
+
|
|
531
|
+
lines = []
|
|
532
|
+
|
|
533
|
+
# Helper function to add separator
|
|
534
|
+
def add_separator(style: str = "-") -> None:
|
|
535
|
+
lines.append(
|
|
536
|
+
"|" + style * 10 + "+" + (style * cell_width + "+") * num_agents,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
# Add legend/explanation section
|
|
540
|
+
lines.extend(self._create_legend_section(cell_width))
|
|
541
|
+
|
|
542
|
+
# Top border
|
|
543
|
+
lines.append("+" + "-" * (total_width - 2) + "+")
|
|
544
|
+
|
|
545
|
+
# Header row
|
|
546
|
+
header = "| Event |"
|
|
547
|
+
for agent in self.agents:
|
|
548
|
+
# Use format "Agent 1 (full_agent_id)"
|
|
549
|
+
agent_num = self.agent_mapping.get(agent, "?")
|
|
550
|
+
agent_name = f"Agent {agent_num} ({agent})"
|
|
551
|
+
header += self._format_cell(agent_name, cell_width) + "|"
|
|
552
|
+
lines.append(header)
|
|
553
|
+
|
|
554
|
+
# Header separator
|
|
555
|
+
lines.append(
|
|
556
|
+
"|" + "-" * 10 + "+" + ("-" * cell_width + "+") * num_agents,
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
# User question row
|
|
560
|
+
question_row = "| USER |"
|
|
561
|
+
question_width = cell_width * num_agents + (num_agents - 1)
|
|
562
|
+
question_text = self.user_question.center(question_width)
|
|
563
|
+
question_row += question_text + "|"
|
|
564
|
+
lines.append(question_row)
|
|
565
|
+
|
|
566
|
+
# Double separator
|
|
567
|
+
lines.append(
|
|
568
|
+
"|" + "=" * 10 + "+" + ("=" * cell_width + "+") * num_agents,
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
# Process events chronologically
|
|
572
|
+
agent_states: Dict[str, Dict[str, Any]] = {
|
|
573
|
+
agent: {
|
|
574
|
+
"status": "idle",
|
|
575
|
+
"context": [],
|
|
576
|
+
"answer": None,
|
|
577
|
+
"vote": None,
|
|
578
|
+
"preview": None,
|
|
579
|
+
"last_streaming_logged": False,
|
|
580
|
+
}
|
|
581
|
+
for agent in self.agents
|
|
582
|
+
}
|
|
583
|
+
event_num = 1
|
|
584
|
+
|
|
585
|
+
for event in self.events:
|
|
586
|
+
event_type = event["event_type"]
|
|
587
|
+
agent_id = event.get("agent_id")
|
|
588
|
+
context = event.get("context", {})
|
|
589
|
+
|
|
590
|
+
# Skip session-level events - just show the actual coordination
|
|
591
|
+
# work
|
|
592
|
+
|
|
593
|
+
# Skip iteration_start events - we already have session_start
|
|
594
|
+
|
|
595
|
+
# Skip system-level events without agent_id
|
|
596
|
+
if not agent_id or agent_id not in self.agents:
|
|
597
|
+
continue
|
|
598
|
+
|
|
599
|
+
# Update agent state and create table row
|
|
600
|
+
|
|
601
|
+
if event_type == "status_change":
|
|
602
|
+
status = event.get("details", "").replace(
|
|
603
|
+
"Changed to status: ",
|
|
604
|
+
"",
|
|
605
|
+
)
|
|
606
|
+
old_status = agent_states[agent_id]["status"]
|
|
607
|
+
agent_states[agent_id]["status"] = status
|
|
608
|
+
|
|
609
|
+
# Only log the FIRST streaming status for each agent, not
|
|
610
|
+
# repetitive ones
|
|
611
|
+
if status in ["streaming", "answering"]:
|
|
612
|
+
# Skip streaming that happens after voting - we'll show
|
|
613
|
+
# final_answer directly
|
|
614
|
+
if old_status == "voted":
|
|
615
|
+
# Just update status but don't show this event
|
|
616
|
+
pass
|
|
617
|
+
else:
|
|
618
|
+
# Only show if this is a meaningful transition (not
|
|
619
|
+
# streaming -> streaming)
|
|
620
|
+
if old_status not in ["streaming", "answering"] or not agent_states[agent_id]["last_streaming_logged"]:
|
|
621
|
+
# Create multi-line event with context and
|
|
622
|
+
# streaming start
|
|
623
|
+
event_lines = []
|
|
624
|
+
# Show context when starting to stream
|
|
625
|
+
context = agent_states[agent_id]["context"]
|
|
626
|
+
if context:
|
|
627
|
+
if isinstance(context, list):
|
|
628
|
+
context_str = ", ".join(str(c) for c in context)
|
|
629
|
+
else:
|
|
630
|
+
context_str = str(context)
|
|
631
|
+
event_lines.append(
|
|
632
|
+
f"📋 Context: [{context_str}]",
|
|
633
|
+
)
|
|
634
|
+
else:
|
|
635
|
+
event_lines.append("📋 Context: []")
|
|
636
|
+
event_lines.append(f"💭 Started {status}")
|
|
637
|
+
|
|
638
|
+
lines.extend(
|
|
639
|
+
self._create_multi_line_event_row(
|
|
640
|
+
event_num,
|
|
641
|
+
agent_id,
|
|
642
|
+
event_lines,
|
|
643
|
+
agent_states,
|
|
644
|
+
cell_width,
|
|
645
|
+
),
|
|
646
|
+
)
|
|
647
|
+
add_separator("-") # Add separator after event
|
|
648
|
+
agent_states[agent_id]["last_streaming_logged"] = True
|
|
649
|
+
event_num += 1
|
|
650
|
+
elif status not in ["streaming", "answering"]:
|
|
651
|
+
# Reset the flag when status changes to something else
|
|
652
|
+
agent_states[agent_id]["last_streaming_logged"] = False
|
|
653
|
+
|
|
654
|
+
elif event_type == "context_received":
|
|
655
|
+
labels = context.get("available_answer_labels", [])
|
|
656
|
+
agent_states[agent_id]["context"] = labels
|
|
657
|
+
# Don't create a separate row for context, it will be shown
|
|
658
|
+
# with answers/votes
|
|
659
|
+
|
|
660
|
+
elif event_type == "restart_triggered":
|
|
661
|
+
# Show restart trigger event spanning both columns (it's a
|
|
662
|
+
# coordination event)
|
|
663
|
+
agent_num = self.agent_mapping.get(agent_id, "?")
|
|
664
|
+
agent_name = f"Agent {agent_num}"
|
|
665
|
+
lines.extend(
|
|
666
|
+
self._create_system_row(
|
|
667
|
+
f"🔁 {agent_name} RESTART TRIGGERED",
|
|
668
|
+
cell_width,
|
|
669
|
+
),
|
|
670
|
+
)
|
|
671
|
+
event_num += 1
|
|
672
|
+
|
|
673
|
+
elif event_type == "restart_completed":
|
|
674
|
+
# Show restart completion
|
|
675
|
+
agent_round = context.get(
|
|
676
|
+
"agent_round",
|
|
677
|
+
context.get("round", 0),
|
|
678
|
+
)
|
|
679
|
+
lines.extend(
|
|
680
|
+
self._create_event_row(
|
|
681
|
+
event_num,
|
|
682
|
+
agent_id,
|
|
683
|
+
f"✅ RESTART COMPLETED (Restart {agent_round})",
|
|
684
|
+
agent_states,
|
|
685
|
+
cell_width,
|
|
686
|
+
),
|
|
687
|
+
)
|
|
688
|
+
add_separator("-")
|
|
689
|
+
event_num += 1
|
|
690
|
+
# Reset streaming flag so next streaming will be shown
|
|
691
|
+
agent_states[agent_id]["last_streaming_logged"] = False
|
|
692
|
+
|
|
693
|
+
elif event_type == "new_answer":
|
|
694
|
+
label = context.get("label")
|
|
695
|
+
if label:
|
|
696
|
+
agent_states[agent_id]["answer"] = label
|
|
697
|
+
agent_states[agent_id]["status"] = "answered"
|
|
698
|
+
agent_states[agent_id]["last_streaming_logged"] = False # Reset for next round
|
|
699
|
+
# Get preview from saved answers
|
|
700
|
+
preview = ""
|
|
701
|
+
if agent_id in self.agent_answers:
|
|
702
|
+
preview = self.agent_answers[agent_id]
|
|
703
|
+
agent_states[agent_id]["preview"] = preview
|
|
704
|
+
|
|
705
|
+
# Create multi-line event with answer and preview
|
|
706
|
+
event_lines = []
|
|
707
|
+
# Context already shown when streaming started
|
|
708
|
+
event_lines.append(f"✨ NEW ANSWER: {label}")
|
|
709
|
+
if preview:
|
|
710
|
+
clean_preview = preview.replace("\n", " ").strip()
|
|
711
|
+
event_lines.append(f"👁️ Preview: {clean_preview}")
|
|
712
|
+
|
|
713
|
+
lines.extend(
|
|
714
|
+
self._create_multi_line_event_row(
|
|
715
|
+
event_num,
|
|
716
|
+
agent_id,
|
|
717
|
+
event_lines,
|
|
718
|
+
agent_states,
|
|
719
|
+
cell_width,
|
|
720
|
+
),
|
|
721
|
+
)
|
|
722
|
+
add_separator("-") # Add separator after event
|
|
723
|
+
event_num += 1
|
|
724
|
+
|
|
725
|
+
elif event_type == "vote_cast":
|
|
726
|
+
vote = context.get("voted_for_label")
|
|
727
|
+
reason = context.get("reason", "")
|
|
728
|
+
if vote:
|
|
729
|
+
agent_states[agent_id]["vote"] = vote
|
|
730
|
+
agent_states[agent_id]["status"] = "voted"
|
|
731
|
+
agent_states[agent_id]["last_streaming_logged"] = False # Reset for next round
|
|
732
|
+
|
|
733
|
+
# Create multi-line event with vote and reason
|
|
734
|
+
event_lines = []
|
|
735
|
+
# Context already shown when streaming started
|
|
736
|
+
event_lines.append(f"🗳️ VOTE: {vote}")
|
|
737
|
+
if reason:
|
|
738
|
+
clean_reason = reason.replace("\n", " ").strip()
|
|
739
|
+
reason_str = clean_reason[:50] + "..." if len(clean_reason) > 50 else clean_reason
|
|
740
|
+
event_lines.append(f"💭 Reason: {reason_str}")
|
|
741
|
+
|
|
742
|
+
lines.extend(
|
|
743
|
+
self._create_multi_line_event_row(
|
|
744
|
+
event_num,
|
|
745
|
+
agent_id,
|
|
746
|
+
event_lines,
|
|
747
|
+
agent_states,
|
|
748
|
+
cell_width,
|
|
749
|
+
),
|
|
750
|
+
)
|
|
751
|
+
add_separator("-") # Add separator after event
|
|
752
|
+
event_num += 1
|
|
753
|
+
|
|
754
|
+
elif event_type == "final_agent_selected":
|
|
755
|
+
# Show winner selection using agent mapping
|
|
756
|
+
agent_num = self.agent_mapping.get(agent_id, "?")
|
|
757
|
+
winner_name = f"Agent {agent_num}"
|
|
758
|
+
lines.extend(
|
|
759
|
+
self._create_system_row(
|
|
760
|
+
f"🏆 {winner_name} selected as winner",
|
|
761
|
+
cell_width,
|
|
762
|
+
),
|
|
763
|
+
)
|
|
764
|
+
# Update other agents to completed status
|
|
765
|
+
for other_agent in self.agents:
|
|
766
|
+
if other_agent != agent_id:
|
|
767
|
+
agent_states[other_agent]["status"] = "completed"
|
|
768
|
+
|
|
769
|
+
elif event_type == "final_answer":
|
|
770
|
+
label = context.get("label")
|
|
771
|
+
if label:
|
|
772
|
+
agent_states[agent_id]["status"] = "final"
|
|
773
|
+
|
|
774
|
+
# Ensure preview is available for final answer
|
|
775
|
+
if not agent_states[agent_id]["preview"] and agent_id in self.agent_answers:
|
|
776
|
+
agent_states[agent_id]["preview"] = self.agent_answers[agent_id]
|
|
777
|
+
|
|
778
|
+
# Create multi-line event with final answer
|
|
779
|
+
event_lines = []
|
|
780
|
+
# Context already shown when streaming started
|
|
781
|
+
event_lines.append(f"🎯 FINAL ANSWER: {label}")
|
|
782
|
+
if agent_states[agent_id]["preview"]:
|
|
783
|
+
preview_text = str(agent_states[agent_id]["preview"])
|
|
784
|
+
clean_preview = preview_text.replace("\n", " ").strip()
|
|
785
|
+
event_lines.append(f"👁️ Preview: {clean_preview}")
|
|
786
|
+
|
|
787
|
+
lines.extend(
|
|
788
|
+
self._create_multi_line_event_row(
|
|
789
|
+
event_num,
|
|
790
|
+
agent_id,
|
|
791
|
+
event_lines,
|
|
792
|
+
agent_states,
|
|
793
|
+
cell_width,
|
|
794
|
+
),
|
|
795
|
+
)
|
|
796
|
+
add_separator("-") # Add separator after event
|
|
797
|
+
event_num += 1
|
|
798
|
+
|
|
799
|
+
# Add summary statistics
|
|
800
|
+
lines.extend(self._create_summary_section(agent_states, cell_width))
|
|
801
|
+
|
|
802
|
+
# Bottom border
|
|
803
|
+
lines.append("+" + "-" * (total_width - 2) + "+")
|
|
804
|
+
|
|
805
|
+
return "\n".join(lines)
|
|
806
|
+
|
|
807
|
+
def _create_event_row(
|
|
808
|
+
self,
|
|
809
|
+
event_num: int,
|
|
810
|
+
active_agent: str,
|
|
811
|
+
event_description: str,
|
|
812
|
+
agent_states: dict,
|
|
813
|
+
cell_width: int,
|
|
814
|
+
) -> list:
|
|
815
|
+
"""Create a table row for a single event"""
|
|
816
|
+
row = "|"
|
|
817
|
+
|
|
818
|
+
# Event number
|
|
819
|
+
event_label = f" E{event_num} "
|
|
820
|
+
row += event_label[-10:].rjust(10) + "|"
|
|
821
|
+
|
|
822
|
+
# Agent cells
|
|
823
|
+
for agent in self.agents:
|
|
824
|
+
if agent == active_agent:
|
|
825
|
+
# This agent is performing the event
|
|
826
|
+
cell_content = event_description
|
|
827
|
+
else:
|
|
828
|
+
# Show current status for other agents - prioritize active
|
|
829
|
+
# states
|
|
830
|
+
status = agent_states[agent]["status"]
|
|
831
|
+
if status in ["streaming", "answering"]:
|
|
832
|
+
cell_content = f"🔄 ({status})"
|
|
833
|
+
elif status == "voted":
|
|
834
|
+
# Just show voted status without the value to avoid
|
|
835
|
+
# confusion
|
|
836
|
+
cell_content = "✅ (voted)"
|
|
837
|
+
elif status == "answered":
|
|
838
|
+
if agent_states[agent]["answer"]:
|
|
839
|
+
cell_content = f"✅ Answered: {agent_states[agent]['answer']}"
|
|
840
|
+
else:
|
|
841
|
+
cell_content = "✅ (answered)"
|
|
842
|
+
elif status == "completed":
|
|
843
|
+
cell_content = "✅ (completed)"
|
|
844
|
+
elif status == "final":
|
|
845
|
+
cell_content = "🎯 (final answer given)"
|
|
846
|
+
elif status == "idle":
|
|
847
|
+
cell_content = "⏳ (waiting)"
|
|
848
|
+
else:
|
|
849
|
+
cell_content = f"({status})"
|
|
850
|
+
|
|
851
|
+
row += self._format_cell(cell_content, cell_width) + "|"
|
|
852
|
+
|
|
853
|
+
return [row]
|
|
854
|
+
|
|
855
|
+
def _create_multi_line_event_row(
|
|
856
|
+
self,
|
|
857
|
+
event_num: int,
|
|
858
|
+
active_agent: str,
|
|
859
|
+
event_lines: list,
|
|
860
|
+
agent_states: dict,
|
|
861
|
+
cell_width: int,
|
|
862
|
+
) -> list:
|
|
863
|
+
"""Create multiple table rows for a single event with multiple lines of content"""
|
|
864
|
+
rows = []
|
|
865
|
+
|
|
866
|
+
for line_idx, event_line in enumerate(event_lines):
|
|
867
|
+
row = "|"
|
|
868
|
+
|
|
869
|
+
# Event number (only on first line)
|
|
870
|
+
if line_idx == 0:
|
|
871
|
+
event_label = f" E{event_num} "
|
|
872
|
+
row += event_label[-10:].rjust(10) + "|"
|
|
873
|
+
else:
|
|
874
|
+
row += " " * 10 + "|"
|
|
875
|
+
|
|
876
|
+
# Agent cells
|
|
877
|
+
for agent in self.agents:
|
|
878
|
+
if agent == active_agent:
|
|
879
|
+
# This agent is performing the event
|
|
880
|
+
cell_content = event_line
|
|
881
|
+
else:
|
|
882
|
+
# Show current status for other agents (only on first line)
|
|
883
|
+
# - prioritize active states
|
|
884
|
+
if line_idx == 0:
|
|
885
|
+
status = agent_states[agent]["status"]
|
|
886
|
+
if status in ["streaming", "answering"]:
|
|
887
|
+
cell_content = f"🔄 ({status})"
|
|
888
|
+
elif status == "voted":
|
|
889
|
+
# Just show voted status without the value to avoid
|
|
890
|
+
# confusion
|
|
891
|
+
cell_content = "✅ (voted)"
|
|
892
|
+
elif status == "answered":
|
|
893
|
+
if agent_states[agent]["answer"]:
|
|
894
|
+
cell_content = f"✅ Answered: {agent_states[agent]['answer']}"
|
|
895
|
+
else:
|
|
896
|
+
cell_content = "✅ (answered)"
|
|
897
|
+
elif status == "completed":
|
|
898
|
+
cell_content = "✅ (completed)"
|
|
899
|
+
elif status == "final":
|
|
900
|
+
cell_content = "🎯 (final answer given)"
|
|
901
|
+
elif status == "idle":
|
|
902
|
+
cell_content = "⏳ (waiting)"
|
|
903
|
+
else:
|
|
904
|
+
cell_content = f"({status})"
|
|
905
|
+
else:
|
|
906
|
+
cell_content = ""
|
|
907
|
+
|
|
908
|
+
row += self._format_cell(cell_content, cell_width) + "|"
|
|
909
|
+
|
|
910
|
+
rows.append(row)
|
|
911
|
+
|
|
912
|
+
return rows
|
|
913
|
+
|
|
914
|
+
def _create_system_row(self, message: str, cell_width: int) -> list:
|
|
915
|
+
"""Create a system announcement row that spans all columns"""
|
|
916
|
+
total_width = 10 + (cell_width + 1) * len(self.agents) + 1
|
|
917
|
+
|
|
918
|
+
# Separator line
|
|
919
|
+
separator = "|" + "-" * 10 + "+" + ("-" * cell_width + "+") * len(self.agents)
|
|
920
|
+
|
|
921
|
+
# Message row
|
|
922
|
+
message_width = total_width - 3 # Account for borders
|
|
923
|
+
message_row = "|" + message.center(message_width) + "|"
|
|
924
|
+
|
|
925
|
+
# Another separator
|
|
926
|
+
separator2 = "|" + "-" * 10 + "+" + ("-" * cell_width + "+") * len(self.agents)
|
|
927
|
+
|
|
928
|
+
return [separator, message_row, separator2]
|
|
929
|
+
|
|
930
|
+
def _create_summary_section(
|
|
931
|
+
self,
|
|
932
|
+
agent_states: dict,
|
|
933
|
+
cell_width: int,
|
|
934
|
+
) -> list:
|
|
935
|
+
"""Create summary statistics section"""
|
|
936
|
+
lines = []
|
|
937
|
+
|
|
938
|
+
# Calculate statistics
|
|
939
|
+
total_answers = sum(1 for agent in self.agents if agent_states[agent]["answer"])
|
|
940
|
+
total_votes = sum(1 for agent in self.agents if agent_states[agent]["vote"])
|
|
941
|
+
total_restarts = len(
|
|
942
|
+
[e for e in self.events if e["event_type"] == "restart_completed"],
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
# Count per-agent stats
|
|
946
|
+
agent_stats = {}
|
|
947
|
+
for agent in self.agents:
|
|
948
|
+
agent_num = self.agent_mapping.get(agent, "?")
|
|
949
|
+
agent_name = f"Agent {agent_num}"
|
|
950
|
+
agent_stats[agent_name] = {
|
|
951
|
+
"answers": 1 if agent_states[agent]["answer"] else 0,
|
|
952
|
+
"votes": 1 if agent_states[agent]["vote"] else 0,
|
|
953
|
+
"final_status": agent_states[agent]["status"],
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
# Count restarts per agent
|
|
957
|
+
for event in self.events:
|
|
958
|
+
if event["event_type"] == "restart_completed" and event.get("agent_id") in self.agents:
|
|
959
|
+
agent_id = event["agent_id"]
|
|
960
|
+
agent_num = self.agent_mapping.get(agent_id, "?")
|
|
961
|
+
agent_name = f"Agent {agent_num}"
|
|
962
|
+
if agent_name not in agent_stats:
|
|
963
|
+
agent_stats[agent_name] = {"restarts": 0}
|
|
964
|
+
if "restarts" not in agent_stats[agent_name]:
|
|
965
|
+
agent_stats[agent_name]["restarts"] = 0
|
|
966
|
+
agent_stats[agent_name]["restarts"] += 1
|
|
967
|
+
|
|
968
|
+
# Create separator
|
|
969
|
+
separator = "|" + "=" * 10 + "+" + ("=" * cell_width + "+") * len(self.agents)
|
|
970
|
+
lines.append(separator)
|
|
971
|
+
|
|
972
|
+
# Summary header
|
|
973
|
+
summary_header = "| SUMMARY |"
|
|
974
|
+
for agent in self.agents:
|
|
975
|
+
agent_num = self.agent_mapping.get(agent, "?")
|
|
976
|
+
agent_name = f"Agent {agent_num}"
|
|
977
|
+
summary_header += self._format_cell(agent_name, cell_width) + "|"
|
|
978
|
+
lines.append(summary_header)
|
|
979
|
+
|
|
980
|
+
# Separator
|
|
981
|
+
lines.append(
|
|
982
|
+
"|" + "-" * 10 + "+" + ("-" * cell_width + "+") * len(self.agents),
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
# Answers row
|
|
986
|
+
answers_row = "| Answers |"
|
|
987
|
+
for agent in self.agents:
|
|
988
|
+
agent_num = self.agent_mapping.get(agent, "?")
|
|
989
|
+
agent_name = f"Agent {agent_num}"
|
|
990
|
+
count = agent_stats.get(agent_name, {}).get("answers", 0)
|
|
991
|
+
answers_row += (
|
|
992
|
+
self._format_cell(
|
|
993
|
+
f"{count} answer{'s' if count != 1 else ''}",
|
|
994
|
+
cell_width,
|
|
995
|
+
)
|
|
996
|
+
+ "|"
|
|
997
|
+
)
|
|
998
|
+
lines.append(answers_row)
|
|
999
|
+
|
|
1000
|
+
# Votes row
|
|
1001
|
+
votes_row = "| Votes |"
|
|
1002
|
+
for agent in self.agents:
|
|
1003
|
+
agent_num = self.agent_mapping.get(agent, "?")
|
|
1004
|
+
agent_name = f"Agent {agent_num}"
|
|
1005
|
+
count = agent_stats.get(agent_name, {}).get("votes", 0)
|
|
1006
|
+
votes_row += (
|
|
1007
|
+
self._format_cell(
|
|
1008
|
+
f"{count} vote{'s' if count != 1 else ''}",
|
|
1009
|
+
cell_width,
|
|
1010
|
+
)
|
|
1011
|
+
+ "|"
|
|
1012
|
+
)
|
|
1013
|
+
lines.append(votes_row)
|
|
1014
|
+
|
|
1015
|
+
# Restarts row
|
|
1016
|
+
restarts_row = "| Restarts |"
|
|
1017
|
+
for agent in self.agents:
|
|
1018
|
+
agent_num = self.agent_mapping.get(agent, "?")
|
|
1019
|
+
agent_name = f"Agent {agent_num}"
|
|
1020
|
+
count = agent_stats.get(agent_name, {}).get("restarts", 0)
|
|
1021
|
+
restarts_row += (
|
|
1022
|
+
self._format_cell(
|
|
1023
|
+
f"{count} restart{'s' if count != 1 else ''}",
|
|
1024
|
+
cell_width,
|
|
1025
|
+
)
|
|
1026
|
+
+ "|"
|
|
1027
|
+
)
|
|
1028
|
+
lines.append(restarts_row)
|
|
1029
|
+
|
|
1030
|
+
# Final status row
|
|
1031
|
+
status_row = "| Status |"
|
|
1032
|
+
for agent in self.agents:
|
|
1033
|
+
agent_num = self.agent_mapping.get(agent, "?")
|
|
1034
|
+
agent_name = f"Agent {agent_num}"
|
|
1035
|
+
status = agent_states[agent]["status"]
|
|
1036
|
+
if status == "final":
|
|
1037
|
+
display = "🏆 Winner"
|
|
1038
|
+
elif status == "completed":
|
|
1039
|
+
display = "✅ Completed"
|
|
1040
|
+
elif status == "voted":
|
|
1041
|
+
display = "✅ Voted"
|
|
1042
|
+
else:
|
|
1043
|
+
display = f"({status})"
|
|
1044
|
+
status_row += self._format_cell(display, cell_width) + "|"
|
|
1045
|
+
lines.append(status_row)
|
|
1046
|
+
|
|
1047
|
+
# Overall totals row
|
|
1048
|
+
lines.append(
|
|
1049
|
+
"|" + "-" * 10 + "+" + ("-" * cell_width + "+") * len(self.agents),
|
|
1050
|
+
)
|
|
1051
|
+
totals_row = "| TOTALS |"
|
|
1052
|
+
total_width = cell_width * len(self.agents) + (len(self.agents) - 1)
|
|
1053
|
+
totals_content = f"{total_answers} answers, {total_votes} votes, {total_restarts} restarts"
|
|
1054
|
+
winner_name = None
|
|
1055
|
+
for agent in self.agents:
|
|
1056
|
+
if agent_states[agent]["status"] == "final":
|
|
1057
|
+
winner_name = f"Agent{agent.split('_')[-1]}" if "_" in agent else agent
|
|
1058
|
+
break
|
|
1059
|
+
if winner_name:
|
|
1060
|
+
totals_content += f" → {winner_name} selected"
|
|
1061
|
+
totals_row += totals_content.center(total_width) + "|"
|
|
1062
|
+
lines.append(totals_row)
|
|
1063
|
+
|
|
1064
|
+
return lines
|
|
1065
|
+
|
|
1066
|
+
def _get_legend_content(self) -> dict:
|
|
1067
|
+
"""Get legend content as structured data to be formatted by different displays"""
|
|
1068
|
+
return {
|
|
1069
|
+
"event_symbols": [
|
|
1070
|
+
("💭 Started streaming", "Agent begins thinking/processing"),
|
|
1071
|
+
("✨ NEW ANSWER", "Agent provides a labeled answer"),
|
|
1072
|
+
("🗳️ VOTE", "Agent votes for an answer"),
|
|
1073
|
+
("💭 Reason", "Reasoning behind the vote"),
|
|
1074
|
+
("👁️ Preview", "Content of the answer"),
|
|
1075
|
+
("🔁 RESTART TRIGGERED", "Agent requests to restart"),
|
|
1076
|
+
("✅ RESTART COMPLETED", "Agent finishes restart"),
|
|
1077
|
+
("🎯 FINAL ANSWER", "Winner provides final response"),
|
|
1078
|
+
("🏆 Winner selected", "System announces winner"),
|
|
1079
|
+
],
|
|
1080
|
+
"status_symbols": [
|
|
1081
|
+
("💭 (streaming)", "Currently thinking/processing"),
|
|
1082
|
+
("⏳ (waiting)", "Idle, waiting for turn"),
|
|
1083
|
+
("✅ (answered)", "Has provided an answer"),
|
|
1084
|
+
("✅ (voted)", "Has cast a vote"),
|
|
1085
|
+
("✅ (completed)", "Task completed"),
|
|
1086
|
+
("🎯 (final answer given)", "Winner completed final answer"),
|
|
1087
|
+
],
|
|
1088
|
+
"terms": [
|
|
1089
|
+
("Context", "Available answer options agent can see"),
|
|
1090
|
+
("Restart", "Agent starts over (clears memory)"),
|
|
1091
|
+
("Event", "Chronological action in the coordination"),
|
|
1092
|
+
(
|
|
1093
|
+
"Answer Labels",
|
|
1094
|
+
"Each answer gets a unique ID (agent1.1, agent2.1, etc.)\n"
|
|
1095
|
+
" Format: agent{N}.{attempt} where N=agent number, attempt=new answer number\n"
|
|
1096
|
+
" Example: agent1.1 = Agent1's 1st answer, agent2.1 = Agent2's 1st answer",
|
|
1097
|
+
),
|
|
1098
|
+
(
|
|
1099
|
+
"agent1.final",
|
|
1100
|
+
"Special label for the winner's final answer",
|
|
1101
|
+
),
|
|
1102
|
+
],
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
def _create_legend_section(self, cell_width: int) -> list:
|
|
1106
|
+
"""Create legend/explanation section at the top for plain text"""
|
|
1107
|
+
lines = []
|
|
1108
|
+
legend_data = self._get_legend_content()
|
|
1109
|
+
|
|
1110
|
+
# Title
|
|
1111
|
+
lines.append("")
|
|
1112
|
+
lines.append("Multi-Agent Coordination Events Log")
|
|
1113
|
+
lines.append("=" * 50)
|
|
1114
|
+
lines.append("")
|
|
1115
|
+
|
|
1116
|
+
# Event symbols
|
|
1117
|
+
lines.append("📋 EVENT SYMBOLS:")
|
|
1118
|
+
for symbol, description in legend_data["event_symbols"]:
|
|
1119
|
+
# Pad symbol to consistent width (24 chars) for alignment
|
|
1120
|
+
padded = f" {symbol}".ljust(28)
|
|
1121
|
+
lines.append(f"{padded}- {description}")
|
|
1122
|
+
lines.append("")
|
|
1123
|
+
|
|
1124
|
+
# Status symbols
|
|
1125
|
+
lines.append("📊 STATUS SYMBOLS:")
|
|
1126
|
+
for symbol, description in legend_data["status_symbols"]:
|
|
1127
|
+
padded = f" {symbol}".ljust(28)
|
|
1128
|
+
lines.append(f"{padded}- {description}")
|
|
1129
|
+
lines.append("")
|
|
1130
|
+
|
|
1131
|
+
# Terms
|
|
1132
|
+
lines.append("📖 TERMS:")
|
|
1133
|
+
for term, description in legend_data["terms"]:
|
|
1134
|
+
if "\n" in description:
|
|
1135
|
+
# Handle multi-line descriptions
|
|
1136
|
+
first_line = description.split("\n")[0]
|
|
1137
|
+
lines.append(f" {term.ljust(13)} - {first_line}")
|
|
1138
|
+
for line in description.split("\n")[1:]:
|
|
1139
|
+
lines.append(f" {line}")
|
|
1140
|
+
else:
|
|
1141
|
+
lines.append(f" {term.ljust(13)} - {description}")
|
|
1142
|
+
lines.append("")
|
|
1143
|
+
|
|
1144
|
+
return lines
|
|
1145
|
+
|
|
1146
|
+
def generate_table(self) -> str:
|
|
1147
|
+
"""Generate the formatted table"""
|
|
1148
|
+
num_agents = len(self.agents)
|
|
1149
|
+
# Dynamic cell width based on number of agents
|
|
1150
|
+
if num_agents <= 2:
|
|
1151
|
+
cell_width = 60
|
|
1152
|
+
elif num_agents == 3:
|
|
1153
|
+
cell_width = 40
|
|
1154
|
+
elif num_agents == 4:
|
|
1155
|
+
cell_width = 30
|
|
1156
|
+
else: # 5+ agents
|
|
1157
|
+
cell_width = 25
|
|
1158
|
+
total_width = 10 + (cell_width + 1) * num_agents + 1
|
|
1159
|
+
|
|
1160
|
+
lines = []
|
|
1161
|
+
|
|
1162
|
+
# Top border
|
|
1163
|
+
lines.append("+" + "-" * (total_width - 2) + "+")
|
|
1164
|
+
|
|
1165
|
+
# Header row
|
|
1166
|
+
header = "| Round |"
|
|
1167
|
+
for agent in self.agents:
|
|
1168
|
+
# Try to create readable agent names
|
|
1169
|
+
# Use the full agent name as provided by user configuration
|
|
1170
|
+
agent_name = agent
|
|
1171
|
+
header += self._format_cell(agent_name, cell_width) + "|"
|
|
1172
|
+
lines.append(header)
|
|
1173
|
+
|
|
1174
|
+
# Header separator
|
|
1175
|
+
lines.append(
|
|
1176
|
+
"|" + "-" * 10 + "+" + ("-" * cell_width + "+") * num_agents,
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
# User question row
|
|
1180
|
+
question_row = "| USER |"
|
|
1181
|
+
question_width = cell_width * num_agents + (num_agents - 1)
|
|
1182
|
+
question_text = self.user_question.center(question_width)
|
|
1183
|
+
question_row += question_text + "|"
|
|
1184
|
+
lines.append(question_row)
|
|
1185
|
+
|
|
1186
|
+
# Double separator
|
|
1187
|
+
lines.append(
|
|
1188
|
+
"|" + "=" * 10 + "+" + ("=" * cell_width + "+") * num_agents,
|
|
1189
|
+
)
|
|
1190
|
+
|
|
1191
|
+
# Process each round
|
|
1192
|
+
for i, round_data in enumerate(self.rounds):
|
|
1193
|
+
# Get content for each agent
|
|
1194
|
+
agent_contents = {}
|
|
1195
|
+
max_lines = 0
|
|
1196
|
+
|
|
1197
|
+
for agent in self.agents:
|
|
1198
|
+
content = self._build_agent_cell_content(
|
|
1199
|
+
round_data.agent_states[agent],
|
|
1200
|
+
round_data.round_type,
|
|
1201
|
+
agent,
|
|
1202
|
+
round_data.round_num,
|
|
1203
|
+
)
|
|
1204
|
+
agent_contents[agent] = content
|
|
1205
|
+
max_lines = max(max_lines, len(content))
|
|
1206
|
+
|
|
1207
|
+
# Build round rows
|
|
1208
|
+
for line_idx in range(max_lines):
|
|
1209
|
+
row = "|"
|
|
1210
|
+
|
|
1211
|
+
# Round label (only on first line)
|
|
1212
|
+
if line_idx == 0:
|
|
1213
|
+
if round_data.round_type == "FINAL":
|
|
1214
|
+
round_label = " FINAL "
|
|
1215
|
+
else:
|
|
1216
|
+
round_label = f" {round_data.round_type} "
|
|
1217
|
+
row += round_label[-10:].rjust(10) + "|"
|
|
1218
|
+
else:
|
|
1219
|
+
row += " " * 10 + "|"
|
|
1220
|
+
|
|
1221
|
+
# Agent cells
|
|
1222
|
+
for agent in self.agents:
|
|
1223
|
+
content_lines = agent_contents[agent]
|
|
1224
|
+
if line_idx < len(content_lines):
|
|
1225
|
+
row += self._format_cell(
|
|
1226
|
+
content_lines[line_idx],
|
|
1227
|
+
cell_width,
|
|
1228
|
+
)
|
|
1229
|
+
else:
|
|
1230
|
+
row += " " * cell_width
|
|
1231
|
+
row += "|"
|
|
1232
|
+
|
|
1233
|
+
lines.append(row)
|
|
1234
|
+
|
|
1235
|
+
# Round separator
|
|
1236
|
+
if i < len(self.rounds) - 1:
|
|
1237
|
+
next_round = self.rounds[i + 1]
|
|
1238
|
+
if next_round.round_type == "FINAL":
|
|
1239
|
+
# Add winner announcement before FINAL round
|
|
1240
|
+
lines.append(
|
|
1241
|
+
"|" + "-" * 10 + "+" + ("-" * cell_width + "+") * num_agents,
|
|
1242
|
+
)
|
|
1243
|
+
|
|
1244
|
+
# Winner announcement row
|
|
1245
|
+
if self.final_winner:
|
|
1246
|
+
# Use agent mapping for consistent naming
|
|
1247
|
+
agent_number = self.agent_mapping.get(
|
|
1248
|
+
self.final_winner,
|
|
1249
|
+
)
|
|
1250
|
+
if agent_number:
|
|
1251
|
+
winner_name = f"Agent {agent_number}"
|
|
1252
|
+
else:
|
|
1253
|
+
winner_name = self.final_winner
|
|
1254
|
+
|
|
1255
|
+
winner_text = f"{winner_name} selected as winner"
|
|
1256
|
+
winner_width = total_width - 1 # Full table width minus the outer borders
|
|
1257
|
+
winner_row = "|" + winner_text.center(winner_width) + "|"
|
|
1258
|
+
lines.append(winner_row)
|
|
1259
|
+
|
|
1260
|
+
# Solid line before FINAL
|
|
1261
|
+
lines.append(
|
|
1262
|
+
"|" + "-" * 10 + "+" + ("-" * cell_width + "+") * num_agents,
|
|
1263
|
+
)
|
|
1264
|
+
else:
|
|
1265
|
+
# Wavy line between regular rounds
|
|
1266
|
+
lines.append(
|
|
1267
|
+
"|" + "~" * 10 + "+" + ("~" * cell_width + "+") * num_agents,
|
|
1268
|
+
)
|
|
1269
|
+
|
|
1270
|
+
# Bottom separator
|
|
1271
|
+
lines.append(
|
|
1272
|
+
"|" + "-" * 10 + "+" + ("-" * cell_width + "+") * num_agents,
|
|
1273
|
+
)
|
|
1274
|
+
|
|
1275
|
+
# Bottom border
|
|
1276
|
+
lines.append("+" + "-" * (total_width - 2) + "+")
|
|
1277
|
+
|
|
1278
|
+
return "\n".join(lines)
|
|
1279
|
+
|
|
1280
|
+
def _create_rich_legend(self) -> Optional[Any]:
|
|
1281
|
+
"""Create Rich legend panel using shared legend content"""
|
|
1282
|
+
try:
|
|
1283
|
+
from rich import box
|
|
1284
|
+
from rich.panel import Panel
|
|
1285
|
+
from rich.text import Text
|
|
1286
|
+
except ImportError:
|
|
1287
|
+
return None
|
|
1288
|
+
|
|
1289
|
+
legend_data = self._get_legend_content()
|
|
1290
|
+
content = Text()
|
|
1291
|
+
|
|
1292
|
+
# Event symbols
|
|
1293
|
+
content.append("📋 EVENT SYMBOLS:\n", style="bold bright_blue")
|
|
1294
|
+
for symbol, description in legend_data["event_symbols"]:
|
|
1295
|
+
padded = f" {symbol}".ljust(28)
|
|
1296
|
+
content.append(f"{padded}- {description}\n", style="dim white")
|
|
1297
|
+
content.append("\n")
|
|
1298
|
+
|
|
1299
|
+
# Status symbols
|
|
1300
|
+
content.append("📊 STATUS SYMBOLS:\n", style="bold bright_green")
|
|
1301
|
+
for symbol, description in legend_data["status_symbols"]:
|
|
1302
|
+
padded = f" {symbol}".ljust(28)
|
|
1303
|
+
content.append(f"{padded}- {description}\n", style="dim white")
|
|
1304
|
+
content.append("\n")
|
|
1305
|
+
|
|
1306
|
+
# Terms
|
|
1307
|
+
content.append("📖 TERMS:\n", style="bold bright_yellow")
|
|
1308
|
+
for term, description in legend_data["terms"]:
|
|
1309
|
+
if "\n" in description:
|
|
1310
|
+
# Handle multi-line descriptions
|
|
1311
|
+
lines = description.split("\n")
|
|
1312
|
+
content.append(
|
|
1313
|
+
f" {term.ljust(13)} - {lines[0]}\n",
|
|
1314
|
+
style="dim white",
|
|
1315
|
+
)
|
|
1316
|
+
for line in lines[1:]:
|
|
1317
|
+
content.append(f" {line}\n", style="dim white")
|
|
1318
|
+
else:
|
|
1319
|
+
content.append(
|
|
1320
|
+
f" {term.ljust(13)} - {description}\n",
|
|
1321
|
+
style="dim white",
|
|
1322
|
+
)
|
|
1323
|
+
|
|
1324
|
+
return Panel(
|
|
1325
|
+
content,
|
|
1326
|
+
title="[bold bright_cyan]📋 COORDINATION GUIDE[/bold bright_cyan]",
|
|
1327
|
+
border_style="bright_cyan",
|
|
1328
|
+
box=box.ROUNDED,
|
|
1329
|
+
padding=(1, 2),
|
|
1330
|
+
)
|
|
1331
|
+
|
|
1332
|
+
def generate_rich_event_table(self) -> Optional[tuple]:
|
|
1333
|
+
"""Generate a rich event-driven table with legend
|
|
1334
|
+
|
|
1335
|
+
Returns:
|
|
1336
|
+
Tuple of (legend_panel, table) or None if Rich not available
|
|
1337
|
+
"""
|
|
1338
|
+
try:
|
|
1339
|
+
from rich import box
|
|
1340
|
+
from rich.table import Table
|
|
1341
|
+
from rich.text import Text
|
|
1342
|
+
except ImportError:
|
|
1343
|
+
return None
|
|
1344
|
+
|
|
1345
|
+
# Create legend first
|
|
1346
|
+
legend = self._create_rich_legend()
|
|
1347
|
+
|
|
1348
|
+
# Create the main table
|
|
1349
|
+
table = Table(
|
|
1350
|
+
title="[bold cyan]Multi-Agent Coordination Events[/bold cyan]",
|
|
1351
|
+
box=box.DOUBLE_EDGE,
|
|
1352
|
+
expand=True,
|
|
1353
|
+
show_lines=True,
|
|
1354
|
+
)
|
|
1355
|
+
|
|
1356
|
+
# Add columns
|
|
1357
|
+
table.add_column(
|
|
1358
|
+
"Event",
|
|
1359
|
+
style="bold yellow",
|
|
1360
|
+
width=8,
|
|
1361
|
+
justify="center",
|
|
1362
|
+
)
|
|
1363
|
+
for agent in self.agents:
|
|
1364
|
+
# Use format "Agent 1 (full_agent_id)"
|
|
1365
|
+
agent_num = self.agent_mapping.get(agent, "?")
|
|
1366
|
+
agent_name = f"Agent {agent_num} ({agent})"
|
|
1367
|
+
table.add_column(
|
|
1368
|
+
agent_name,
|
|
1369
|
+
style="white",
|
|
1370
|
+
width=45,
|
|
1371
|
+
justify="center",
|
|
1372
|
+
)
|
|
1373
|
+
|
|
1374
|
+
# Add user question as header
|
|
1375
|
+
question_row = ["[bold cyan]USER[/bold cyan]"]
|
|
1376
|
+
question_text = f"[bold white]{self.user_question}[/bold white]"
|
|
1377
|
+
for _ in range(len(self.agents)):
|
|
1378
|
+
question_row.append(question_text)
|
|
1379
|
+
table.add_row(*question_row)
|
|
1380
|
+
|
|
1381
|
+
# Process events chronologically
|
|
1382
|
+
agent_states: Dict[str, Dict[str, Any]] = {
|
|
1383
|
+
agent: {
|
|
1384
|
+
"status": "idle",
|
|
1385
|
+
"context": [],
|
|
1386
|
+
"answer": None,
|
|
1387
|
+
"vote": None,
|
|
1388
|
+
"preview": None,
|
|
1389
|
+
"last_streaming_logged": False,
|
|
1390
|
+
}
|
|
1391
|
+
for agent in self.agents
|
|
1392
|
+
}
|
|
1393
|
+
event_num = 1
|
|
1394
|
+
|
|
1395
|
+
for event in self.events:
|
|
1396
|
+
event_type = event["event_type"]
|
|
1397
|
+
agent_id = event.get("agent_id")
|
|
1398
|
+
context = event.get("context", {})
|
|
1399
|
+
|
|
1400
|
+
# Handle system events that span both columns
|
|
1401
|
+
if event_type == "final_agent_selected":
|
|
1402
|
+
agent_num = self.agent_mapping.get(agent_id, "?")
|
|
1403
|
+
winner_name = f"Agent {agent_num}"
|
|
1404
|
+
winner_row = ["[bold green]🏆[/bold green]"]
|
|
1405
|
+
winner_text = Text(
|
|
1406
|
+
f"🏆 {winner_name} selected as winner 🏆",
|
|
1407
|
+
style="bold green",
|
|
1408
|
+
justify="center",
|
|
1409
|
+
)
|
|
1410
|
+
for _ in range(len(self.agents)):
|
|
1411
|
+
winner_row.append(winner_text)
|
|
1412
|
+
table.add_row(*winner_row)
|
|
1413
|
+
continue
|
|
1414
|
+
elif event_type == "restart_triggered" and agent_id and agent_id in self.agents:
|
|
1415
|
+
agent_num = self.agent_mapping.get(agent_id, "?")
|
|
1416
|
+
agent_name = f"Agent {agent_num}"
|
|
1417
|
+
restart_row = ["[bold yellow]🔁[/bold yellow]"]
|
|
1418
|
+
restart_text = Text(
|
|
1419
|
+
f"🔁 {agent_name} RESTART TRIGGERED",
|
|
1420
|
+
style="bold yellow",
|
|
1421
|
+
justify="center",
|
|
1422
|
+
)
|
|
1423
|
+
for _ in range(len(self.agents)):
|
|
1424
|
+
restart_row.append(restart_text)
|
|
1425
|
+
table.add_row(*restart_row)
|
|
1426
|
+
continue
|
|
1427
|
+
|
|
1428
|
+
# Skip session-level events
|
|
1429
|
+
if not agent_id or agent_id not in self.agents:
|
|
1430
|
+
continue
|
|
1431
|
+
|
|
1432
|
+
# Handle agent events
|
|
1433
|
+
if event_type == "status_change":
|
|
1434
|
+
status = event.get("details", "").replace(
|
|
1435
|
+
"Changed to status: ",
|
|
1436
|
+
"",
|
|
1437
|
+
)
|
|
1438
|
+
old_status = agent_states[agent_id]["status"]
|
|
1439
|
+
agent_states[agent_id]["status"] = status
|
|
1440
|
+
|
|
1441
|
+
# Only log first streaming
|
|
1442
|
+
if status in ["streaming", "answering"]:
|
|
1443
|
+
if old_status == "voted":
|
|
1444
|
+
pass # Skip post-vote streaming
|
|
1445
|
+
elif old_status not in ["streaming", "answering"] or not agent_states[agent_id]["last_streaming_logged"]:
|
|
1446
|
+
row = self._create_rich_event_row(
|
|
1447
|
+
event_num,
|
|
1448
|
+
agent_id,
|
|
1449
|
+
agent_states,
|
|
1450
|
+
"streaming_start",
|
|
1451
|
+
)
|
|
1452
|
+
if row:
|
|
1453
|
+
table.add_row(*row)
|
|
1454
|
+
event_num += 1
|
|
1455
|
+
agent_states[agent_id]["last_streaming_logged"] = True
|
|
1456
|
+
|
|
1457
|
+
elif event_type == "context_received":
|
|
1458
|
+
labels = context.get("available_answer_labels", [])
|
|
1459
|
+
agent_states[agent_id]["context"] = labels
|
|
1460
|
+
|
|
1461
|
+
elif event_type == "restart_completed":
|
|
1462
|
+
agent_round = context.get(
|
|
1463
|
+
"agent_round",
|
|
1464
|
+
context.get("round", 0),
|
|
1465
|
+
)
|
|
1466
|
+
row = self._create_rich_event_row(
|
|
1467
|
+
event_num,
|
|
1468
|
+
agent_id,
|
|
1469
|
+
agent_states,
|
|
1470
|
+
"restart_completed",
|
|
1471
|
+
agent_round,
|
|
1472
|
+
)
|
|
1473
|
+
if row:
|
|
1474
|
+
table.add_row(*row)
|
|
1475
|
+
event_num += 1
|
|
1476
|
+
agent_states[agent_id]["last_streaming_logged"] = False
|
|
1477
|
+
|
|
1478
|
+
elif event_type == "new_answer":
|
|
1479
|
+
label = context.get("label")
|
|
1480
|
+
if label:
|
|
1481
|
+
agent_states[agent_id]["answer"] = label
|
|
1482
|
+
agent_states[agent_id]["status"] = "answered"
|
|
1483
|
+
agent_states[agent_id]["last_streaming_logged"] = False
|
|
1484
|
+
preview = self.agent_answers.get(agent_id, "")
|
|
1485
|
+
agent_states[agent_id]["preview"] = preview
|
|
1486
|
+
row = self._create_rich_event_row(
|
|
1487
|
+
event_num,
|
|
1488
|
+
agent_id,
|
|
1489
|
+
agent_states,
|
|
1490
|
+
"new_answer",
|
|
1491
|
+
label,
|
|
1492
|
+
preview,
|
|
1493
|
+
)
|
|
1494
|
+
if row:
|
|
1495
|
+
table.add_row(*row)
|
|
1496
|
+
event_num += 1
|
|
1497
|
+
|
|
1498
|
+
elif event_type == "vote_cast":
|
|
1499
|
+
vote = context.get("voted_for_label")
|
|
1500
|
+
reason = context.get("reason", "")
|
|
1501
|
+
if vote:
|
|
1502
|
+
agent_states[agent_id]["vote"] = vote
|
|
1503
|
+
agent_states[agent_id]["status"] = "voted"
|
|
1504
|
+
agent_states[agent_id]["last_streaming_logged"] = False
|
|
1505
|
+
row = self._create_rich_event_row(
|
|
1506
|
+
event_num,
|
|
1507
|
+
agent_id,
|
|
1508
|
+
agent_states,
|
|
1509
|
+
"vote",
|
|
1510
|
+
vote,
|
|
1511
|
+
reason,
|
|
1512
|
+
)
|
|
1513
|
+
if row:
|
|
1514
|
+
table.add_row(*row)
|
|
1515
|
+
event_num += 1
|
|
1516
|
+
|
|
1517
|
+
elif event_type == "final_answer":
|
|
1518
|
+
label = context.get("label")
|
|
1519
|
+
if label:
|
|
1520
|
+
agent_states[agent_id]["status"] = "final"
|
|
1521
|
+
preview = agent_states[agent_id].get("preview", "")
|
|
1522
|
+
row = self._create_rich_event_row(
|
|
1523
|
+
event_num,
|
|
1524
|
+
agent_id,
|
|
1525
|
+
agent_states,
|
|
1526
|
+
"final_answer",
|
|
1527
|
+
label,
|
|
1528
|
+
preview,
|
|
1529
|
+
)
|
|
1530
|
+
if row:
|
|
1531
|
+
table.add_row(*row)
|
|
1532
|
+
event_num += 1
|
|
1533
|
+
|
|
1534
|
+
# Add summary section
|
|
1535
|
+
self._add_rich_summary(table, agent_states)
|
|
1536
|
+
|
|
1537
|
+
# Return both legend and table
|
|
1538
|
+
return (legend, table)
|
|
1539
|
+
|
|
1540
|
+
def _create_rich_event_row(
|
|
1541
|
+
self,
|
|
1542
|
+
event_num: int,
|
|
1543
|
+
active_agent: str,
|
|
1544
|
+
agent_states: Dict[str, Any],
|
|
1545
|
+
event_type: str,
|
|
1546
|
+
*args: Any,
|
|
1547
|
+
) -> list:
|
|
1548
|
+
"""Create a rich table row for an event"""
|
|
1549
|
+
row = [f"[bold yellow]E{event_num}[/bold yellow]"]
|
|
1550
|
+
|
|
1551
|
+
for agent in self.agents:
|
|
1552
|
+
if agent == active_agent:
|
|
1553
|
+
# Active agent performing the event
|
|
1554
|
+
if event_type == "streaming_start":
|
|
1555
|
+
context = agent_states[agent]["context"]
|
|
1556
|
+
context_str = f"[dim blue]📋 Context: \\[{', '.join(context)}][/dim blue]\n" if context else "[dim blue]📋 Context: \\[][/dim blue]\n"
|
|
1557
|
+
cell = context_str + "[bold cyan]💭 Started streaming[/bold cyan]"
|
|
1558
|
+
elif event_type == "restart_completed":
|
|
1559
|
+
cell = f"[bold green]✅ RESTART COMPLETED (Restart {args[0]})[/bold green]"
|
|
1560
|
+
elif event_type == "new_answer":
|
|
1561
|
+
label, preview = args[0], args[1] if len(args) > 1 else ""
|
|
1562
|
+
cell = f"[bold green]✨ NEW ANSWER: {label}[/bold green]"
|
|
1563
|
+
if preview:
|
|
1564
|
+
clean_preview = preview.replace("\n", " ").strip()
|
|
1565
|
+
preview_truncated = clean_preview[:80] + "..." if len(clean_preview) > 80 else clean_preview
|
|
1566
|
+
cell += f"\n[dim white]👁️ Preview: {preview_truncated}[/dim white]"
|
|
1567
|
+
elif event_type == "vote":
|
|
1568
|
+
vote, reason = args[0], args[1] if len(args) > 1 else ""
|
|
1569
|
+
cell = f"[bold cyan]🗳️ VOTE: {vote}[/bold cyan]"
|
|
1570
|
+
if reason:
|
|
1571
|
+
clean_reason = reason.replace("\n", " ").strip()
|
|
1572
|
+
reason_preview = clean_reason[:50] + "..." if len(clean_reason) > 50 else clean_reason
|
|
1573
|
+
cell += f"\n[italic dim]💭 Reason: {reason_preview}[/italic dim]"
|
|
1574
|
+
elif event_type == "final_answer":
|
|
1575
|
+
label, preview = args[0], args[1] if len(args) > 1 else ""
|
|
1576
|
+
cell = f"[bold green]🎯 FINAL ANSWER: {label}[/bold green]"
|
|
1577
|
+
if preview:
|
|
1578
|
+
clean_preview = preview.replace("\n", " ").strip()
|
|
1579
|
+
preview_truncated = clean_preview[:80] + "..." if len(clean_preview) > 80 else clean_preview
|
|
1580
|
+
cell += f"\n[dim white]👁️ Preview: {preview_truncated}[/dim white]"
|
|
1581
|
+
else:
|
|
1582
|
+
cell = ""
|
|
1583
|
+
row.append(cell)
|
|
1584
|
+
else:
|
|
1585
|
+
# Other agents showing status - prioritize active states
|
|
1586
|
+
status = agent_states[agent]["status"]
|
|
1587
|
+
if status in ["streaming", "answering"]:
|
|
1588
|
+
cell = f"[cyan]🔄 ({status})[/cyan]"
|
|
1589
|
+
elif status == "voted":
|
|
1590
|
+
cell = "[green]✅ (voted)[/green]"
|
|
1591
|
+
elif status == "answered":
|
|
1592
|
+
if agent_states[agent]["answer"]:
|
|
1593
|
+
cell = f"[green]✅ Answered: {agent_states[agent]['answer']}[/green]"
|
|
1594
|
+
else:
|
|
1595
|
+
cell = "[green]✅ (answered)[/green]"
|
|
1596
|
+
elif status == "completed":
|
|
1597
|
+
cell = "[green]✅ (completed)[/green]"
|
|
1598
|
+
elif status == "final":
|
|
1599
|
+
cell = "[bold green]🎯 (final answer given)[/bold green]"
|
|
1600
|
+
elif status == "idle":
|
|
1601
|
+
cell = "[dim]⏳ (waiting)[/dim]"
|
|
1602
|
+
else:
|
|
1603
|
+
cell = f"[dim]({status})[/dim]"
|
|
1604
|
+
row.append(cell)
|
|
1605
|
+
|
|
1606
|
+
return row
|
|
1607
|
+
|
|
1608
|
+
def _add_rich_summary(self, table: Any, agent_states: dict) -> None:
|
|
1609
|
+
"""Add summary statistics to the rich table"""
|
|
1610
|
+
# Calculate statistics
|
|
1611
|
+
total_answers = sum(1 for agent in self.agents if agent_states[agent]["answer"])
|
|
1612
|
+
total_votes = sum(1 for agent in self.agents if agent_states[agent]["vote"])
|
|
1613
|
+
total_restarts = len(
|
|
1614
|
+
[e for e in self.events if e["event_type"] == "restart_completed"],
|
|
1615
|
+
)
|
|
1616
|
+
|
|
1617
|
+
# Summary header
|
|
1618
|
+
summary_row = ["[bold magenta]SUMMARY[/bold magenta]"]
|
|
1619
|
+
for agent in self.agents:
|
|
1620
|
+
agent_num = self.agent_mapping.get(agent, "?")
|
|
1621
|
+
agent_name = f"Agent {agent_num}"
|
|
1622
|
+
summary_row.append(f"[bold magenta]{agent_name}[/bold magenta]")
|
|
1623
|
+
table.add_row(*summary_row)
|
|
1624
|
+
|
|
1625
|
+
# Stats for each agent
|
|
1626
|
+
stats_row = ["[bold]Stats[/bold]"]
|
|
1627
|
+
for agent in self.agents:
|
|
1628
|
+
answer_count = 1 if agent_states[agent]["answer"] else 0
|
|
1629
|
+
vote_count = 1 if agent_states[agent]["vote"] else 0
|
|
1630
|
+
restart_count = len(
|
|
1631
|
+
[e for e in self.events if e["event_type"] == "restart_completed" and e.get("agent_id") == agent],
|
|
1632
|
+
)
|
|
1633
|
+
|
|
1634
|
+
status = agent_states[agent]["status"]
|
|
1635
|
+
if status == "final":
|
|
1636
|
+
status_str = "[bold green]🏆 Winner[/bold green]"
|
|
1637
|
+
elif status == "completed":
|
|
1638
|
+
status_str = "[green]✅ Completed[/green]"
|
|
1639
|
+
else:
|
|
1640
|
+
status_str = f"[dim]{status}[/dim]"
|
|
1641
|
+
|
|
1642
|
+
stats = f"{answer_count} answer, {vote_count} vote, {restart_count} restarts\n{status_str}"
|
|
1643
|
+
stats_row.append(stats)
|
|
1644
|
+
table.add_row(*stats_row)
|
|
1645
|
+
|
|
1646
|
+
# Overall totals
|
|
1647
|
+
totals_row = ["[bold]TOTALS[/bold]"]
|
|
1648
|
+
totals_text = f"[bold cyan]{total_answers} answers, {total_votes} votes, {total_restarts} restarts[/bold cyan]"
|
|
1649
|
+
for _ in range(len(self.agents)):
|
|
1650
|
+
totals_row.append(totals_text)
|
|
1651
|
+
table.add_row(*totals_row)
|
|
1652
|
+
|
|
1653
|
+
def generate_rich_table(self) -> Optional["Table"]:
|
|
1654
|
+
"""Generate a Rich table with proper formatting and colors."""
|
|
1655
|
+
if not RICH_AVAILABLE:
|
|
1656
|
+
return None
|
|
1657
|
+
|
|
1658
|
+
# Create main table with individual agent columns
|
|
1659
|
+
table = Table(
|
|
1660
|
+
box=box.DOUBLE_EDGE,
|
|
1661
|
+
show_header=True,
|
|
1662
|
+
header_style="bold bright_white on blue",
|
|
1663
|
+
expand=True,
|
|
1664
|
+
padding=(0, 1),
|
|
1665
|
+
title="[bold bright_cyan]Multi-Agent Coordination Flow[/bold bright_cyan]",
|
|
1666
|
+
title_style="bold bright_cyan",
|
|
1667
|
+
)
|
|
1668
|
+
|
|
1669
|
+
# Add columns with individual agents
|
|
1670
|
+
table.add_column(
|
|
1671
|
+
"Round",
|
|
1672
|
+
style="bold bright_white",
|
|
1673
|
+
width=14,
|
|
1674
|
+
justify="center",
|
|
1675
|
+
)
|
|
1676
|
+
for agent in self.agents:
|
|
1677
|
+
# Create readable agent names
|
|
1678
|
+
# Use the full agent name as provided by user configuration
|
|
1679
|
+
agent_name = agent
|
|
1680
|
+
# Use fixed width instead of ratio to prevent truncation
|
|
1681
|
+
table.add_column(
|
|
1682
|
+
agent_name,
|
|
1683
|
+
style="white",
|
|
1684
|
+
justify="center",
|
|
1685
|
+
width=40,
|
|
1686
|
+
overflow="fold",
|
|
1687
|
+
)
|
|
1688
|
+
|
|
1689
|
+
# Add user question row - create a nested table to achieve true
|
|
1690
|
+
# spanning
|
|
1691
|
+
from rich.table import Table as InnerTable
|
|
1692
|
+
|
|
1693
|
+
inner_question_table = InnerTable(
|
|
1694
|
+
box=None,
|
|
1695
|
+
show_header=False,
|
|
1696
|
+
expand=True,
|
|
1697
|
+
padding=(0, 0),
|
|
1698
|
+
)
|
|
1699
|
+
inner_question_table.add_column("Question", justify="center", ratio=1)
|
|
1700
|
+
inner_question_table.add_row(
|
|
1701
|
+
f"[bold bright_yellow]{self.user_question}[/bold bright_yellow]",
|
|
1702
|
+
)
|
|
1703
|
+
|
|
1704
|
+
question_cells = [""] # Empty round column
|
|
1705
|
+
question_cells.append(inner_question_table)
|
|
1706
|
+
# Fill remaining columns with empty strings - Rich will merge them
|
|
1707
|
+
# visually
|
|
1708
|
+
for i in range(len(self.agents) - 1):
|
|
1709
|
+
question_cells.append("")
|
|
1710
|
+
table.add_row(*question_cells)
|
|
1711
|
+
|
|
1712
|
+
# Add separator row
|
|
1713
|
+
separator_cells = [
|
|
1714
|
+
"[dim bright_blue]════════════[/dim bright_blue]",
|
|
1715
|
+
] + ["[dim bright_blue]" + "═" * 88 + "[/dim bright_blue]" for _ in self.agents]
|
|
1716
|
+
table.add_row(*separator_cells)
|
|
1717
|
+
|
|
1718
|
+
# Process each round
|
|
1719
|
+
for i, round_data in enumerate(self.rounds):
|
|
1720
|
+
# Get content for each agent
|
|
1721
|
+
agent_contents = {}
|
|
1722
|
+
max_lines = 0
|
|
1723
|
+
|
|
1724
|
+
for agent in self.agents:
|
|
1725
|
+
content = self._build_rich_agent_cell_content(
|
|
1726
|
+
round_data.agent_states[agent],
|
|
1727
|
+
round_data.round_type,
|
|
1728
|
+
agent,
|
|
1729
|
+
round_data.round_num,
|
|
1730
|
+
)
|
|
1731
|
+
agent_contents[agent] = content
|
|
1732
|
+
max_lines = max(max_lines, len(content))
|
|
1733
|
+
|
|
1734
|
+
# Build round rows
|
|
1735
|
+
for line_idx in range(max_lines):
|
|
1736
|
+
row_cells = []
|
|
1737
|
+
|
|
1738
|
+
# Round label (only on first line)
|
|
1739
|
+
if line_idx == 0:
|
|
1740
|
+
if round_data.round_type == "FINAL":
|
|
1741
|
+
round_label = "[bold green]🏁 FINAL 🏁[/bold green]"
|
|
1742
|
+
else:
|
|
1743
|
+
round_label = f"[bold cyan]🔄 {round_data.round_type} 🔄[/bold cyan]"
|
|
1744
|
+
row_cells.append(round_label)
|
|
1745
|
+
else:
|
|
1746
|
+
row_cells.append("")
|
|
1747
|
+
|
|
1748
|
+
# Agent cells (individual columns)
|
|
1749
|
+
for agent in self.agents:
|
|
1750
|
+
content_lines = agent_contents[agent]
|
|
1751
|
+
if line_idx < len(content_lines):
|
|
1752
|
+
row_cells.append(content_lines[line_idx])
|
|
1753
|
+
else:
|
|
1754
|
+
row_cells.append("")
|
|
1755
|
+
|
|
1756
|
+
table.add_row(*row_cells)
|
|
1757
|
+
|
|
1758
|
+
# Round separator
|
|
1759
|
+
if i < len(self.rounds) - 1:
|
|
1760
|
+
next_round = self.rounds[i + 1]
|
|
1761
|
+
if next_round.round_type == "FINAL":
|
|
1762
|
+
# Winner announcement - simulate spanning
|
|
1763
|
+
if self.final_winner:
|
|
1764
|
+
# Use agent mapping for consistent naming
|
|
1765
|
+
agent_number = self.agent_mapping.get(
|
|
1766
|
+
self.final_winner,
|
|
1767
|
+
)
|
|
1768
|
+
if agent_number:
|
|
1769
|
+
winner_name = f"Agent {agent_number}"
|
|
1770
|
+
else:
|
|
1771
|
+
winner_name = self.final_winner
|
|
1772
|
+
|
|
1773
|
+
winner_announcement = f"🏆 {winner_name} selected as winner 🏆"
|
|
1774
|
+
# Create nested table for winner announcement spanning
|
|
1775
|
+
inner_winner_table = InnerTable(
|
|
1776
|
+
box=None,
|
|
1777
|
+
show_header=False,
|
|
1778
|
+
expand=True,
|
|
1779
|
+
padding=(0, 0),
|
|
1780
|
+
)
|
|
1781
|
+
inner_winner_table.add_column(
|
|
1782
|
+
"Winner",
|
|
1783
|
+
justify="center",
|
|
1784
|
+
ratio=1,
|
|
1785
|
+
)
|
|
1786
|
+
inner_winner_table.add_row(
|
|
1787
|
+
f"[bold bright_green]{winner_announcement}[/bold bright_green]",
|
|
1788
|
+
)
|
|
1789
|
+
|
|
1790
|
+
winner_cells = [""] # Empty round column
|
|
1791
|
+
winner_cells.append(inner_winner_table)
|
|
1792
|
+
# Fill remaining columns with empty strings
|
|
1793
|
+
for j in range(len(self.agents) - 1):
|
|
1794
|
+
winner_cells.append("")
|
|
1795
|
+
table.add_row(*winner_cells)
|
|
1796
|
+
|
|
1797
|
+
# Solid line before FINAL
|
|
1798
|
+
separator_cells = [
|
|
1799
|
+
"[dim green]────────────[/dim green]",
|
|
1800
|
+
] + ["[dim green]" + "─" * 88 + "[/dim green]" for _ in self.agents]
|
|
1801
|
+
table.add_row(*separator_cells)
|
|
1802
|
+
else:
|
|
1803
|
+
# Wavy line between regular rounds
|
|
1804
|
+
separator_cells = ["[dim cyan]~~~~~~~~~~~~[/dim cyan]"] + ["[dim cyan]" + "~" * 88 + "[/dim cyan]" for _ in self.agents]
|
|
1805
|
+
table.add_row(*separator_cells)
|
|
1806
|
+
|
|
1807
|
+
return table
|
|
1808
|
+
|
|
1809
|
+
def _build_rich_agent_cell_content(
|
|
1810
|
+
self,
|
|
1811
|
+
agent_state: AgentState,
|
|
1812
|
+
round_type: str,
|
|
1813
|
+
agent_id: str,
|
|
1814
|
+
round_num: int,
|
|
1815
|
+
) -> List[str]:
|
|
1816
|
+
"""Build Rich-formatted content for an agent's cell in a round."""
|
|
1817
|
+
lines = []
|
|
1818
|
+
|
|
1819
|
+
# Determine if we should show context (for non-voting scenarios)
|
|
1820
|
+
show_context = (agent_state.current_answer and not agent_state.vote) or agent_state.has_final_answer or agent_state.status in ["streaming", "answering"]
|
|
1821
|
+
|
|
1822
|
+
# Don't show context for completed agents in FINAL round
|
|
1823
|
+
if round_type == "FINAL" and agent_state.status == "completed":
|
|
1824
|
+
show_context = False
|
|
1825
|
+
|
|
1826
|
+
# Add context with better styling (but not for voting agents)
|
|
1827
|
+
if show_context and not agent_state.vote:
|
|
1828
|
+
if agent_state.context:
|
|
1829
|
+
context_items = ", ".join(agent_state.context)
|
|
1830
|
+
# Escape brackets for Rich
|
|
1831
|
+
context_str = f"📋 Context: \\[{context_items}]"
|
|
1832
|
+
else:
|
|
1833
|
+
context_str = "📋 Context: \\[]" # Escape brackets for Rich
|
|
1834
|
+
lines.append(f"[dim blue]{context_str}[/dim blue]")
|
|
1835
|
+
|
|
1836
|
+
# Add content based on what happened in this round with enhanced
|
|
1837
|
+
# styling
|
|
1838
|
+
if agent_state.vote:
|
|
1839
|
+
# Agent voted in this round - always show context when voting
|
|
1840
|
+
if agent_state.context:
|
|
1841
|
+
context_items = ", ".join(agent_state.context)
|
|
1842
|
+
# Escape brackets for Rich
|
|
1843
|
+
context_str = f"📋 Context: \\[{context_items}]"
|
|
1844
|
+
lines.append(f"[dim blue]{context_str}[/dim blue]")
|
|
1845
|
+
vote_str = f"🗳️ VOTE: {agent_state.vote}"
|
|
1846
|
+
lines.append(f"[bold cyan]{vote_str}[/bold cyan]")
|
|
1847
|
+
if agent_state.vote_reason:
|
|
1848
|
+
# Clean up newlines and truncate
|
|
1849
|
+
clean_reason = agent_state.vote_reason.replace(
|
|
1850
|
+
"\n",
|
|
1851
|
+
" ",
|
|
1852
|
+
).strip()
|
|
1853
|
+
reason = clean_reason[:65] + "..." if len(clean_reason) > 68 else clean_reason
|
|
1854
|
+
reason_str = f"💭 Reason: {reason}"
|
|
1855
|
+
lines.append(f"[italic dim]{reason_str}[/italic dim]")
|
|
1856
|
+
|
|
1857
|
+
elif round_type == "FINAL":
|
|
1858
|
+
# Final presentation round
|
|
1859
|
+
if agent_state.has_final_answer:
|
|
1860
|
+
final_str = f"🎯 FINAL ANSWER: {agent_state.current_answer}"
|
|
1861
|
+
lines.append(f"[bold green]{final_str}[/bold green]")
|
|
1862
|
+
if agent_state.answer_preview:
|
|
1863
|
+
# Clean up newlines in preview
|
|
1864
|
+
clean_preview = agent_state.answer_preview.replace(
|
|
1865
|
+
"\n",
|
|
1866
|
+
" ",
|
|
1867
|
+
).strip()
|
|
1868
|
+
preview_truncated = clean_preview[:80] + "..." if len(clean_preview) > 80 else clean_preview
|
|
1869
|
+
preview_str = f"👁️ Preview: {preview_truncated}"
|
|
1870
|
+
lines.append(f"[dim white]{preview_str}[/dim white]")
|
|
1871
|
+
else:
|
|
1872
|
+
lines.append(
|
|
1873
|
+
"[dim red]👁️ Preview: [Answer not available][/dim red]",
|
|
1874
|
+
)
|
|
1875
|
+
elif agent_state.status == "completed":
|
|
1876
|
+
lines.append("[dim green]✅ (completed)[/dim green]")
|
|
1877
|
+
else:
|
|
1878
|
+
lines.append("[dim yellow]⏳ (waiting)[/dim yellow]")
|
|
1879
|
+
|
|
1880
|
+
elif agent_state.current_answer and not agent_state.vote:
|
|
1881
|
+
# Agent provided an answer in this round
|
|
1882
|
+
answer_str = f"✨ NEW ANSWER: {agent_state.current_answer}"
|
|
1883
|
+
lines.append(f"[bold green]{answer_str}[/bold green]")
|
|
1884
|
+
if agent_state.answer_preview:
|
|
1885
|
+
# Clean up newlines in preview
|
|
1886
|
+
clean_preview = agent_state.answer_preview.replace(
|
|
1887
|
+
"\n",
|
|
1888
|
+
" ",
|
|
1889
|
+
).strip()
|
|
1890
|
+
preview_truncated = clean_preview[:80] + "..." if len(clean_preview) > 80 else clean_preview
|
|
1891
|
+
preview_str = f"👁️ Preview: {preview_truncated}"
|
|
1892
|
+
lines.append(f"[dim white]{preview_str}[/dim white]")
|
|
1893
|
+
else:
|
|
1894
|
+
lines.append(
|
|
1895
|
+
"[dim red]👁️ Preview: [Answer not available][/dim red]",
|
|
1896
|
+
)
|
|
1897
|
+
|
|
1898
|
+
elif agent_state.status in ["streaming", "answering"]:
|
|
1899
|
+
lines.append("[bold yellow]🔄 (answering)[/bold yellow]")
|
|
1900
|
+
|
|
1901
|
+
elif agent_state.status == "voted":
|
|
1902
|
+
lines.append("[dim bright_cyan]✅ (voted)[/dim bright_cyan]")
|
|
1903
|
+
|
|
1904
|
+
elif agent_state.status == "answered":
|
|
1905
|
+
lines.append("[dim bright_green]✅ (answered)[/dim bright_green]")
|
|
1906
|
+
|
|
1907
|
+
else:
|
|
1908
|
+
lines.append("[dim]⏳ (waiting)[/dim]")
|
|
1909
|
+
|
|
1910
|
+
return lines
|
|
1911
|
+
|
|
1912
|
+
|
|
1913
|
+
def main() -> None:
|
|
1914
|
+
"""Main entry point"""
|
|
1915
|
+
# Check for input file
|
|
1916
|
+
if len(sys.argv) > 1:
|
|
1917
|
+
filename = sys.argv[1]
|
|
1918
|
+
else:
|
|
1919
|
+
filename = "coordination_events.json"
|
|
1920
|
+
|
|
1921
|
+
try:
|
|
1922
|
+
# Load events
|
|
1923
|
+
with open(filename, "r") as f:
|
|
1924
|
+
events = json.load(f)
|
|
1925
|
+
|
|
1926
|
+
# Build and print table
|
|
1927
|
+
builder = CoordinationTableBuilder(events)
|
|
1928
|
+
|
|
1929
|
+
# Try to use Rich table first, fallback to plain text
|
|
1930
|
+
if RICH_AVAILABLE:
|
|
1931
|
+
rich_table = builder.generate_rich_event_table()
|
|
1932
|
+
if rich_table:
|
|
1933
|
+
console = Console()
|
|
1934
|
+
console.print(rich_table)
|
|
1935
|
+
else:
|
|
1936
|
+
# Fallback to plain event table
|
|
1937
|
+
table = builder.generate_event_table()
|
|
1938
|
+
print(table)
|
|
1939
|
+
else:
|
|
1940
|
+
# Use event-driven plain table as default
|
|
1941
|
+
table = builder.generate_event_table()
|
|
1942
|
+
print(table)
|
|
1943
|
+
|
|
1944
|
+
except FileNotFoundError:
|
|
1945
|
+
print(f"Error: Could not find file '{filename}'")
|
|
1946
|
+
sys.exit(1)
|
|
1947
|
+
except json.JSONDecodeError as e:
|
|
1948
|
+
print(f"Error: Invalid JSON in file '{filename}': {e}")
|
|
1949
|
+
sys.exit(1)
|
|
1950
|
+
except Exception as e:
|
|
1951
|
+
print(f"Error: {e}")
|
|
1952
|
+
sys.exit(1)
|
|
1953
|
+
|
|
1954
|
+
|
|
1955
|
+
if __name__ == "__main__":
|
|
1956
|
+
main()
|