massgen 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of massgen might be problematic. Click here for more details.
- massgen/__init__.py +94 -0
- massgen/agent_config.py +507 -0
- massgen/backend/CLAUDE_API_RESEARCH.md +266 -0
- massgen/backend/Function calling openai responses.md +1161 -0
- massgen/backend/GEMINI_API_DOCUMENTATION.md +410 -0
- massgen/backend/OPENAI_RESPONSES_API_FORMAT.md +65 -0
- massgen/backend/__init__.py +25 -0
- massgen/backend/base.py +180 -0
- massgen/backend/chat_completions.py +228 -0
- massgen/backend/claude.py +661 -0
- massgen/backend/gemini.py +652 -0
- massgen/backend/grok.py +187 -0
- massgen/backend/response.py +397 -0
- massgen/chat_agent.py +440 -0
- massgen/cli.py +686 -0
- massgen/configs/README.md +293 -0
- massgen/configs/creative_team.yaml +53 -0
- massgen/configs/gemini_4o_claude.yaml +31 -0
- massgen/configs/news_analysis.yaml +51 -0
- massgen/configs/research_team.yaml +51 -0
- massgen/configs/single_agent.yaml +18 -0
- massgen/configs/single_flash2.5.yaml +44 -0
- massgen/configs/technical_analysis.yaml +51 -0
- massgen/configs/three_agents_default.yaml +31 -0
- massgen/configs/travel_planning.yaml +51 -0
- massgen/configs/two_agents.yaml +39 -0
- massgen/frontend/__init__.py +20 -0
- massgen/frontend/coordination_ui.py +945 -0
- massgen/frontend/displays/__init__.py +24 -0
- massgen/frontend/displays/base_display.py +83 -0
- massgen/frontend/displays/rich_terminal_display.py +3497 -0
- massgen/frontend/displays/simple_display.py +93 -0
- massgen/frontend/displays/terminal_display.py +381 -0
- massgen/frontend/logging/__init__.py +9 -0
- massgen/frontend/logging/realtime_logger.py +197 -0
- massgen/message_templates.py +431 -0
- massgen/orchestrator.py +1222 -0
- massgen/tests/__init__.py +10 -0
- massgen/tests/multi_turn_conversation_design.md +214 -0
- massgen/tests/multiturn_llm_input_analysis.md +189 -0
- massgen/tests/test_case_studies.md +113 -0
- massgen/tests/test_claude_backend.py +310 -0
- massgen/tests/test_grok_backend.py +160 -0
- massgen/tests/test_message_context_building.py +293 -0
- massgen/tests/test_rich_terminal_display.py +378 -0
- massgen/tests/test_v3_3agents.py +117 -0
- massgen/tests/test_v3_simple.py +216 -0
- massgen/tests/test_v3_three_agents.py +272 -0
- massgen/tests/test_v3_two_agents.py +176 -0
- massgen/utils.py +79 -0
- massgen/v1/README.md +330 -0
- massgen/v1/__init__.py +91 -0
- massgen/v1/agent.py +605 -0
- massgen/v1/agents.py +330 -0
- massgen/v1/backends/gemini.py +584 -0
- massgen/v1/backends/grok.py +410 -0
- massgen/v1/backends/oai.py +571 -0
- massgen/v1/cli.py +351 -0
- massgen/v1/config.py +169 -0
- massgen/v1/examples/fast-4o-mini-config.yaml +44 -0
- massgen/v1/examples/fast_config.yaml +44 -0
- massgen/v1/examples/production.yaml +70 -0
- massgen/v1/examples/single_agent.yaml +39 -0
- massgen/v1/logging.py +974 -0
- massgen/v1/main.py +368 -0
- massgen/v1/orchestrator.py +1138 -0
- massgen/v1/streaming_display.py +1190 -0
- massgen/v1/tools.py +160 -0
- massgen/v1/types.py +245 -0
- massgen/v1/utils.py +199 -0
- massgen-0.0.3.dist-info/METADATA +568 -0
- massgen-0.0.3.dist-info/RECORD +76 -0
- massgen-0.0.3.dist-info/WHEEL +5 -0
- massgen-0.0.3.dist-info/entry_points.txt +2 -0
- massgen-0.0.3.dist-info/licenses/LICENSE +204 -0
- massgen-0.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simple Display for MassGen Coordination
|
|
3
|
+
|
|
4
|
+
Basic text output display for minimal use cases and debugging.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from .base_display import BaseDisplay
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SimpleDisplay(BaseDisplay):
|
|
12
|
+
"""Simple text-based display with minimal formatting."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, agent_ids, **kwargs):
|
|
15
|
+
"""Initialize simple display."""
|
|
16
|
+
super().__init__(agent_ids, **kwargs)
|
|
17
|
+
self.show_agent_prefixes = kwargs.get("show_agent_prefixes", True)
|
|
18
|
+
self.show_events = kwargs.get("show_events", True)
|
|
19
|
+
|
|
20
|
+
def initialize(self, question: str, log_filename: Optional[str] = None):
|
|
21
|
+
"""Initialize the display."""
|
|
22
|
+
print(f"🎯 MassGen Coordination: {question}")
|
|
23
|
+
if log_filename:
|
|
24
|
+
print(f"📁 Log file: {log_filename}")
|
|
25
|
+
print(f"👥 Agents: {', '.join(self.agent_ids)}")
|
|
26
|
+
print("=" * 50)
|
|
27
|
+
|
|
28
|
+
def update_agent_content(
|
|
29
|
+
self, agent_id: str, content: str, content_type: str = "thinking"
|
|
30
|
+
):
|
|
31
|
+
"""Update content for a specific agent."""
|
|
32
|
+
if agent_id not in self.agent_ids:
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
# Clean content - remove any legacy agent prefixes to avoid duplication
|
|
36
|
+
clean_content = content.strip()
|
|
37
|
+
if clean_content.startswith(f"[{agent_id}]"):
|
|
38
|
+
clean_content = clean_content[len(f"[{agent_id}]") :].strip()
|
|
39
|
+
|
|
40
|
+
# Remove any legacy ** prefixes from orchestrator messages (kept for compatibility)
|
|
41
|
+
if clean_content.startswith(f"🤖 **{agent_id}**"):
|
|
42
|
+
clean_content = clean_content.replace(f"🤖 **{agent_id}**", "🤖").strip()
|
|
43
|
+
|
|
44
|
+
# Store cleaned content
|
|
45
|
+
self.agent_outputs[agent_id].append(clean_content)
|
|
46
|
+
|
|
47
|
+
# Display immediately
|
|
48
|
+
if self.show_agent_prefixes:
|
|
49
|
+
prefix = f"[{agent_id}] "
|
|
50
|
+
else:
|
|
51
|
+
prefix = ""
|
|
52
|
+
|
|
53
|
+
if content_type == "tool":
|
|
54
|
+
# Filter out noise "Tool result" messages
|
|
55
|
+
if "Tool result:" in clean_content:
|
|
56
|
+
return # Skip tool result messages as they're just noise
|
|
57
|
+
print(f"{prefix}🔧 {clean_content}")
|
|
58
|
+
elif content_type == "status":
|
|
59
|
+
print(f"{prefix}📊 {clean_content}")
|
|
60
|
+
elif content_type == "presentation":
|
|
61
|
+
print(f"{prefix}🎤 {clean_content}")
|
|
62
|
+
else:
|
|
63
|
+
print(f"{prefix}{clean_content}")
|
|
64
|
+
|
|
65
|
+
def update_agent_status(self, agent_id: str, status: str):
|
|
66
|
+
"""Update status for a specific agent."""
|
|
67
|
+
if agent_id not in self.agent_ids:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
self.agent_status[agent_id] = status
|
|
71
|
+
if self.show_agent_prefixes:
|
|
72
|
+
print(f"[{agent_id}] Status: {status}")
|
|
73
|
+
else:
|
|
74
|
+
print(f"Status: {status}")
|
|
75
|
+
|
|
76
|
+
def add_orchestrator_event(self, event: str):
|
|
77
|
+
"""Add an orchestrator coordination event."""
|
|
78
|
+
self.orchestrator_events.append(event)
|
|
79
|
+
if self.show_events:
|
|
80
|
+
print(f"🎭 {event}")
|
|
81
|
+
|
|
82
|
+
def show_final_answer(self, answer: str):
|
|
83
|
+
"""Display the final coordinated answer."""
|
|
84
|
+
print("\n" + "=" * 50)
|
|
85
|
+
print(f"🎯 FINAL ANSWER: {answer}")
|
|
86
|
+
print("=" * 50)
|
|
87
|
+
|
|
88
|
+
def cleanup(self):
|
|
89
|
+
"""Clean up resources."""
|
|
90
|
+
print(f"\n✅ Coordination completed with {len(self.agent_ids)} agents")
|
|
91
|
+
print(f"📊 Total orchestrator events: {len(self.orchestrator_events)}")
|
|
92
|
+
for agent_id in self.agent_ids:
|
|
93
|
+
print(f"📝 {agent_id}: {len(self.agent_outputs[agent_id])} content items")
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Terminal Display for MassGen Coordination
|
|
3
|
+
|
|
4
|
+
Rich terminal interface with live updates, agent columns, and coordination events.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
from .base_display import BaseDisplay
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TerminalDisplay(BaseDisplay):
|
|
13
|
+
"""Rich terminal display with live agent columns and coordination events."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, agent_ids: List[str], **kwargs):
|
|
16
|
+
"""Initialize terminal display.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
agent_ids: List of agent IDs to display
|
|
20
|
+
**kwargs: Additional configuration options
|
|
21
|
+
- terminal_width: Override terminal width (default: auto-detect)
|
|
22
|
+
- max_events: Max coordination events to show (default: 5)
|
|
23
|
+
"""
|
|
24
|
+
super().__init__(agent_ids, **kwargs)
|
|
25
|
+
self.terminal_width = kwargs.get("terminal_width", self._get_terminal_width())
|
|
26
|
+
self.max_events = kwargs.get("max_events", 5)
|
|
27
|
+
self.num_agents = len(agent_ids)
|
|
28
|
+
self.log_filename = None
|
|
29
|
+
self._last_refresh_time = 0
|
|
30
|
+
|
|
31
|
+
# Calculate column layout
|
|
32
|
+
if self.num_agents == 1:
|
|
33
|
+
self.col_width = self.terminal_width - 4
|
|
34
|
+
self.separators = ""
|
|
35
|
+
elif self.num_agents == 2:
|
|
36
|
+
self.col_width = (self.terminal_width - 3) // 2
|
|
37
|
+
self.separators = " │ "
|
|
38
|
+
else:
|
|
39
|
+
self.col_width = (
|
|
40
|
+
self.terminal_width - (self.num_agents - 1) * 3
|
|
41
|
+
) // self.num_agents
|
|
42
|
+
self.separators = " │ "
|
|
43
|
+
|
|
44
|
+
def _get_terminal_width(self) -> int:
|
|
45
|
+
"""Get terminal width with fallback."""
|
|
46
|
+
try:
|
|
47
|
+
return min(os.get_terminal_size().columns, 120)
|
|
48
|
+
except (OSError, AttributeError):
|
|
49
|
+
return 80
|
|
50
|
+
|
|
51
|
+
def initialize(self, question: str, log_filename: Optional[str] = None):
|
|
52
|
+
"""Initialize the display with column headers."""
|
|
53
|
+
self.log_filename = log_filename
|
|
54
|
+
|
|
55
|
+
# Clear screen and show initial layout
|
|
56
|
+
import os
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
os.system("clear" if os.name == "posix" else "cls")
|
|
60
|
+
except:
|
|
61
|
+
print("\033[2J\033[H", end="")
|
|
62
|
+
|
|
63
|
+
title = f"🚀 {'Multi' if self.num_agents > 2 else 'Two' if self.num_agents == 2 else 'Single'}-Agent Coordination Dashboard"
|
|
64
|
+
print(title)
|
|
65
|
+
|
|
66
|
+
if log_filename:
|
|
67
|
+
print(f"📁 Log: {log_filename}")
|
|
68
|
+
|
|
69
|
+
print("=" * self.terminal_width)
|
|
70
|
+
|
|
71
|
+
# Show column headers with backend info if available
|
|
72
|
+
headers = []
|
|
73
|
+
for agent_id in self.agent_ids:
|
|
74
|
+
# Try to get backend info from orchestrator if available
|
|
75
|
+
backend_name = "Unknown"
|
|
76
|
+
if (
|
|
77
|
+
hasattr(self, "orchestrator")
|
|
78
|
+
and self.orchestrator
|
|
79
|
+
and hasattr(self.orchestrator, "agents")
|
|
80
|
+
):
|
|
81
|
+
agent = self.orchestrator.agents.get(agent_id)
|
|
82
|
+
if (
|
|
83
|
+
agent
|
|
84
|
+
and hasattr(agent, "backend")
|
|
85
|
+
and hasattr(agent.backend, "get_provider_name")
|
|
86
|
+
):
|
|
87
|
+
try:
|
|
88
|
+
backend_name = agent.backend.get_provider_name()
|
|
89
|
+
except:
|
|
90
|
+
backend_name = "Unknown"
|
|
91
|
+
|
|
92
|
+
# Generic header format for any agent
|
|
93
|
+
header_text = f"{agent_id.upper()} ({backend_name})"
|
|
94
|
+
headers.append(f"{header_text:^{self.col_width}}")
|
|
95
|
+
|
|
96
|
+
if self.num_agents == 1:
|
|
97
|
+
print(headers[0])
|
|
98
|
+
print("─" * self.col_width)
|
|
99
|
+
else:
|
|
100
|
+
print(self.separators.join(headers))
|
|
101
|
+
print(self.separators.join(["─" * self.col_width] * self.num_agents))
|
|
102
|
+
|
|
103
|
+
print("=" * self.terminal_width)
|
|
104
|
+
print()
|
|
105
|
+
|
|
106
|
+
def update_agent_content(
|
|
107
|
+
self, agent_id: str, content: str, content_type: str = "thinking"
|
|
108
|
+
):
|
|
109
|
+
"""Update content for a specific agent."""
|
|
110
|
+
if agent_id not in self.agent_ids:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
# Clean content - remove any legacy agent prefixes but preserve line breaks
|
|
114
|
+
clean_content = content
|
|
115
|
+
if clean_content.startswith(f"[{agent_id}]"):
|
|
116
|
+
clean_content = clean_content[len(f"[{agent_id}]") :]
|
|
117
|
+
|
|
118
|
+
# Remove any legacy ** prefixes from orchestrator messages (kept for compatibility)
|
|
119
|
+
if clean_content.startswith(f"🤖 **{agent_id}**"):
|
|
120
|
+
clean_content = clean_content.replace(f"🤖 **{agent_id}**", "🤖")
|
|
121
|
+
|
|
122
|
+
# Only strip if content doesn't contain line breaks (preserve formatting)
|
|
123
|
+
if "\n" not in clean_content:
|
|
124
|
+
clean_content = clean_content.strip()
|
|
125
|
+
|
|
126
|
+
should_refresh = False
|
|
127
|
+
|
|
128
|
+
if content_type == "tool":
|
|
129
|
+
# Temporarily show "Tool result" messages for debugging
|
|
130
|
+
# if "Tool result:" in clean_content:
|
|
131
|
+
# return # Skip tool result messages as they're just noise
|
|
132
|
+
# Tool usage - add with arrow prefix
|
|
133
|
+
self.agent_outputs[agent_id].append(f"→ {clean_content}")
|
|
134
|
+
should_refresh = True # Always refresh for tool calls
|
|
135
|
+
elif content_type == "status":
|
|
136
|
+
# Status messages - add without prefix
|
|
137
|
+
self.agent_outputs[agent_id].append(clean_content)
|
|
138
|
+
should_refresh = True # Always refresh for status changes
|
|
139
|
+
elif content_type == "presentation":
|
|
140
|
+
# Presentation content - always start on a new line
|
|
141
|
+
self.agent_outputs[agent_id].append(f"🎤 {clean_content}")
|
|
142
|
+
should_refresh = True # Always refresh for presentations
|
|
143
|
+
else:
|
|
144
|
+
# Thinking content - smart formatting based on content type
|
|
145
|
+
if (
|
|
146
|
+
self.agent_outputs[agent_id]
|
|
147
|
+
and self.agent_outputs[agent_id][-1] == "⚡ Working..."
|
|
148
|
+
):
|
|
149
|
+
# Replace "Working..." with actual thinking
|
|
150
|
+
self.agent_outputs[agent_id][-1] = clean_content
|
|
151
|
+
should_refresh = True # Refresh when replacing "Working..."
|
|
152
|
+
elif self._is_action_content(clean_content):
|
|
153
|
+
# Action content (tool usage, results) - always start new line
|
|
154
|
+
self.agent_outputs[agent_id].append(clean_content)
|
|
155
|
+
should_refresh = True
|
|
156
|
+
elif (
|
|
157
|
+
self.agent_outputs[agent_id]
|
|
158
|
+
and not self.agent_outputs[agent_id][-1].startswith("→")
|
|
159
|
+
and not self.agent_outputs[agent_id][-1].startswith("🎤")
|
|
160
|
+
and not self._is_action_content(self.agent_outputs[agent_id][-1])
|
|
161
|
+
):
|
|
162
|
+
# Continue building regular thinking content
|
|
163
|
+
if "\n" in clean_content:
|
|
164
|
+
# Handle explicit line breaks
|
|
165
|
+
parts = clean_content.split("\n")
|
|
166
|
+
self.agent_outputs[agent_id][-1] += parts[0]
|
|
167
|
+
for part in parts[1:]:
|
|
168
|
+
self.agent_outputs[agent_id].append(part)
|
|
169
|
+
should_refresh = True
|
|
170
|
+
else:
|
|
171
|
+
# Simple concatenation for regular text with careful spacing
|
|
172
|
+
# Only add space if the new content looks like a separate word/token
|
|
173
|
+
# Avoid adding spaces within words that were split across tokens
|
|
174
|
+
current_content = self.agent_outputs[agent_id][-1]
|
|
175
|
+
if (
|
|
176
|
+
current_content
|
|
177
|
+
and clean_content
|
|
178
|
+
and not current_content[-1].isspace()
|
|
179
|
+
and not clean_content[0].isspace()
|
|
180
|
+
and current_content[-1] not in ".,!?;:-()[]{}\"'"
|
|
181
|
+
and clean_content[0] not in ".,!?;:-()[]{}\"'"
|
|
182
|
+
and not clean_content.startswith("\n")
|
|
183
|
+
and
|
|
184
|
+
# Only add space if content looks like a new word (starts with letter/digit)
|
|
185
|
+
(clean_content[0].isalpha() or clean_content[0].isdigit())
|
|
186
|
+
and
|
|
187
|
+
# And previous content ended with a letter/digit
|
|
188
|
+
(current_content[-1].isalpha() or current_content[-1].isdigit())
|
|
189
|
+
and
|
|
190
|
+
# And the new content is more than 2 characters (likely a word, not suffix)
|
|
191
|
+
len(clean_content.strip()) > 2
|
|
192
|
+
):
|
|
193
|
+
self.agent_outputs[agent_id][-1] += " " + clean_content
|
|
194
|
+
else:
|
|
195
|
+
self.agent_outputs[agent_id][-1] += clean_content
|
|
196
|
+
else:
|
|
197
|
+
# New line of content
|
|
198
|
+
self.agent_outputs[agent_id].append(clean_content)
|
|
199
|
+
should_refresh = True
|
|
200
|
+
|
|
201
|
+
if should_refresh:
|
|
202
|
+
self._refresh_display()
|
|
203
|
+
|
|
204
|
+
def _is_action_content(self, content: str) -> bool:
|
|
205
|
+
"""Check if content represents an action that should be on its own line."""
|
|
206
|
+
action_indicators = [
|
|
207
|
+
"💡",
|
|
208
|
+
"🗳️",
|
|
209
|
+
"✅",
|
|
210
|
+
"🔄",
|
|
211
|
+
"❌",
|
|
212
|
+
"🔧", # Tool and result emojis
|
|
213
|
+
"Providing answer:",
|
|
214
|
+
"Voting for",
|
|
215
|
+
"Answer provided",
|
|
216
|
+
"Vote recorded",
|
|
217
|
+
"Vote ignored",
|
|
218
|
+
"Vote invalid",
|
|
219
|
+
"Using",
|
|
220
|
+
]
|
|
221
|
+
return any(indicator in content for indicator in action_indicators)
|
|
222
|
+
|
|
223
|
+
def update_agent_status(self, agent_id: str, status: str):
|
|
224
|
+
"""Update status for a specific agent."""
|
|
225
|
+
if agent_id not in self.agent_ids:
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
old_status = self.agent_status.get(agent_id)
|
|
229
|
+
if old_status == status:
|
|
230
|
+
return # No change, no need to refresh
|
|
231
|
+
|
|
232
|
+
self.agent_status[agent_id] = status
|
|
233
|
+
|
|
234
|
+
# Add working indicator if transitioning to working
|
|
235
|
+
if old_status != "working" and status == "working":
|
|
236
|
+
agent_prefix = f"[{agent_id}] " if self.num_agents > 1 else ""
|
|
237
|
+
print(f"\n{agent_prefix}⚡ Working...")
|
|
238
|
+
if not self.agent_outputs[agent_id] or not self.agent_outputs[agent_id][
|
|
239
|
+
-1
|
|
240
|
+
].startswith("⚡"):
|
|
241
|
+
self.agent_outputs[agent_id].append("⚡ Working...")
|
|
242
|
+
|
|
243
|
+
# Show status update in footer
|
|
244
|
+
self._refresh_display()
|
|
245
|
+
|
|
246
|
+
def add_orchestrator_event(self, event: str):
|
|
247
|
+
"""Add an orchestrator coordination event."""
|
|
248
|
+
self.orchestrator_events.append(event)
|
|
249
|
+
self._refresh_display()
|
|
250
|
+
|
|
251
|
+
def show_final_answer(self, answer: str):
|
|
252
|
+
"""Display the final coordinated answer prominently."""
|
|
253
|
+
print(f"\n🎯 FINAL COORDINATED ANSWER:")
|
|
254
|
+
print("=" * 60)
|
|
255
|
+
print(f"📋 {answer}")
|
|
256
|
+
print("=" * 60)
|
|
257
|
+
|
|
258
|
+
def cleanup(self):
|
|
259
|
+
"""Clean up display resources."""
|
|
260
|
+
# No special cleanup needed for terminal display
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
def _refresh_display(self):
|
|
264
|
+
"""Refresh the entire display with proper columns."""
|
|
265
|
+
import time
|
|
266
|
+
|
|
267
|
+
current_time = time.time()
|
|
268
|
+
if current_time - self._last_refresh_time < 0.005: # 5ms between refreshes
|
|
269
|
+
return
|
|
270
|
+
self._last_refresh_time = current_time
|
|
271
|
+
|
|
272
|
+
# Move to after headers and clear content area
|
|
273
|
+
print("\033[7;1H\033[0J", end="") # Move to line 7 and clear down
|
|
274
|
+
|
|
275
|
+
# Show agent outputs in columns with word wrapping
|
|
276
|
+
max_lines = (
|
|
277
|
+
max(len(self.agent_outputs[agent_id]) for agent_id in self.agent_ids)
|
|
278
|
+
if self.agent_outputs
|
|
279
|
+
else 0
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# For single agent, don't wrap - show full content
|
|
283
|
+
if self.num_agents == 1:
|
|
284
|
+
for i in range(max_lines):
|
|
285
|
+
line = (
|
|
286
|
+
self.agent_outputs[self.agent_ids[0]][i]
|
|
287
|
+
if i < len(self.agent_outputs[self.agent_ids[0]])
|
|
288
|
+
else ""
|
|
289
|
+
)
|
|
290
|
+
print(line)
|
|
291
|
+
else:
|
|
292
|
+
# For multiple agents, wrap long lines to fit columns
|
|
293
|
+
wrapped_outputs = {}
|
|
294
|
+
for agent_id in self.agent_ids:
|
|
295
|
+
wrapped_outputs[agent_id] = []
|
|
296
|
+
for line in self.agent_outputs[agent_id]:
|
|
297
|
+
if len(line) > self.col_width - 2:
|
|
298
|
+
# Smart word wrap - preserve formatting and avoid breaking mid-sentence
|
|
299
|
+
words = line.split(" ")
|
|
300
|
+
current_line = ""
|
|
301
|
+
for word in words:
|
|
302
|
+
test_line = (
|
|
303
|
+
current_line + (" " if current_line else "") + word
|
|
304
|
+
)
|
|
305
|
+
if len(test_line) > self.col_width - 2:
|
|
306
|
+
if current_line:
|
|
307
|
+
wrapped_outputs[agent_id].append(current_line)
|
|
308
|
+
current_line = word
|
|
309
|
+
else:
|
|
310
|
+
# Single word longer than column width - truncate gracefully
|
|
311
|
+
wrapped_outputs[agent_id].append(
|
|
312
|
+
word[: self.col_width - 2] + "…"
|
|
313
|
+
)
|
|
314
|
+
current_line = ""
|
|
315
|
+
else:
|
|
316
|
+
current_line = test_line
|
|
317
|
+
if current_line:
|
|
318
|
+
wrapped_outputs[agent_id].append(current_line)
|
|
319
|
+
else:
|
|
320
|
+
wrapped_outputs[agent_id].append(line)
|
|
321
|
+
|
|
322
|
+
# Display wrapped content
|
|
323
|
+
max_wrapped_lines = (
|
|
324
|
+
max(len(wrapped_outputs[agent_id]) for agent_id in self.agent_ids)
|
|
325
|
+
if wrapped_outputs
|
|
326
|
+
else 0
|
|
327
|
+
)
|
|
328
|
+
for i in range(max_wrapped_lines):
|
|
329
|
+
output_lines = []
|
|
330
|
+
for agent_id in self.agent_ids:
|
|
331
|
+
line = (
|
|
332
|
+
wrapped_outputs[agent_id][i]
|
|
333
|
+
if i < len(wrapped_outputs[agent_id])
|
|
334
|
+
else ""
|
|
335
|
+
)
|
|
336
|
+
output_lines.append(f"{line:<{self.col_width}}")
|
|
337
|
+
print(self.separators.join(output_lines))
|
|
338
|
+
|
|
339
|
+
# Show status footer with proper column alignment
|
|
340
|
+
print("\n" + "=" * self.terminal_width)
|
|
341
|
+
|
|
342
|
+
# Simple status line aligned with columns, matching header format
|
|
343
|
+
status_lines = []
|
|
344
|
+
for agent_id in self.agent_ids:
|
|
345
|
+
# Get backend info same as in header
|
|
346
|
+
backend_name = "Unknown"
|
|
347
|
+
if (
|
|
348
|
+
hasattr(self, "orchestrator")
|
|
349
|
+
and self.orchestrator
|
|
350
|
+
and hasattr(self.orchestrator, "agents")
|
|
351
|
+
):
|
|
352
|
+
agent = self.orchestrator.agents.get(agent_id)
|
|
353
|
+
if (
|
|
354
|
+
agent
|
|
355
|
+
and hasattr(agent, "backend")
|
|
356
|
+
and hasattr(agent.backend, "get_provider_name")
|
|
357
|
+
):
|
|
358
|
+
try:
|
|
359
|
+
backend_name = agent.backend.get_provider_name()
|
|
360
|
+
except:
|
|
361
|
+
backend_name = "Unknown"
|
|
362
|
+
|
|
363
|
+
status_text = (
|
|
364
|
+
f"{agent_id.upper()} ({backend_name}): {self.agent_status[agent_id]}"
|
|
365
|
+
)
|
|
366
|
+
status_lines.append(f"{status_text:^{self.col_width}}")
|
|
367
|
+
|
|
368
|
+
if self.num_agents == 1:
|
|
369
|
+
print(status_lines[0])
|
|
370
|
+
else:
|
|
371
|
+
print(self.separators.join(status_lines))
|
|
372
|
+
|
|
373
|
+
print("=" * self.terminal_width)
|
|
374
|
+
|
|
375
|
+
# Show recent coordination events
|
|
376
|
+
if self.orchestrator_events:
|
|
377
|
+
print("\n📋 RECENT COORDINATION EVENTS:")
|
|
378
|
+
recent_events = self.orchestrator_events[-2:] # Show last 2 events
|
|
379
|
+
for event in recent_events:
|
|
380
|
+
print(f" • {event}")
|
|
381
|
+
print()
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Real-time Logger for MassGen Coordination
|
|
3
|
+
|
|
4
|
+
Provides comprehensive logging of coordination sessions with live updates.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Dict, List, Any, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RealtimeLogger:
|
|
14
|
+
"""Real-time logger for MassGen coordination sessions."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, filename: Optional[str] = None, update_frequency: int = 5):
|
|
17
|
+
"""Initialize the real-time logger.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
filename: Custom filename for log (default: auto-generated)
|
|
21
|
+
update_frequency: Update log file every N chunks (default: 5)
|
|
22
|
+
"""
|
|
23
|
+
self.update_frequency = update_frequency
|
|
24
|
+
self.chunk_count = 0
|
|
25
|
+
|
|
26
|
+
# Generate filename if not provided
|
|
27
|
+
if filename is None:
|
|
28
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
29
|
+
self.filename = f"mass_coordination_{timestamp}.json"
|
|
30
|
+
else:
|
|
31
|
+
self.filename = filename
|
|
32
|
+
|
|
33
|
+
# Initialize log data structure
|
|
34
|
+
self.log_data = {
|
|
35
|
+
"metadata": {
|
|
36
|
+
"start_time": datetime.now().isoformat(),
|
|
37
|
+
"status": "in_progress",
|
|
38
|
+
"logger_version": "1.0",
|
|
39
|
+
},
|
|
40
|
+
"session": {"question": "", "agent_ids": [], "final_answer": ""},
|
|
41
|
+
"agent_outputs": {},
|
|
42
|
+
"orchestrator_events": [],
|
|
43
|
+
"aggregated_responses": {},
|
|
44
|
+
"workflow_analysis": {"new_answers": 0, "votes": 0, "total_chunks": 0},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Track aggregated content by source
|
|
48
|
+
self._content_buffers = {}
|
|
49
|
+
|
|
50
|
+
def initialize_session(self, question: str, agent_ids: List[str]):
|
|
51
|
+
"""Initialize a new coordination session.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
question: The coordination question
|
|
55
|
+
agent_ids: List of participating agent IDs
|
|
56
|
+
"""
|
|
57
|
+
self.log_data["session"]["question"] = question
|
|
58
|
+
self.log_data["session"]["agent_ids"] = agent_ids
|
|
59
|
+
self.log_data["agent_outputs"] = {agent_id: [] for agent_id in agent_ids}
|
|
60
|
+
|
|
61
|
+
# Write initial log file
|
|
62
|
+
self._update_log_file()
|
|
63
|
+
|
|
64
|
+
return self.filename
|
|
65
|
+
|
|
66
|
+
def log_chunk(
|
|
67
|
+
self, source: Optional[str], content: str, chunk_type: str = "content"
|
|
68
|
+
):
|
|
69
|
+
"""Log a streaming chunk by aggregating content by source.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
source: Source of the chunk (agent ID or orchestrator)
|
|
73
|
+
content: Content of the chunk
|
|
74
|
+
chunk_type: Type of chunk ("content", "tool_calls", "done")
|
|
75
|
+
"""
|
|
76
|
+
self.chunk_count += 1
|
|
77
|
+
self.log_data["workflow_analysis"]["total_chunks"] = self.chunk_count
|
|
78
|
+
|
|
79
|
+
# Initialize buffer for this source if not exists
|
|
80
|
+
source_key = source or "orchestrator"
|
|
81
|
+
if source_key not in self._content_buffers:
|
|
82
|
+
self._content_buffers[source_key] = {
|
|
83
|
+
"thinking": "",
|
|
84
|
+
"tool": "",
|
|
85
|
+
"presentation": "",
|
|
86
|
+
"other": "",
|
|
87
|
+
"first_timestamp": datetime.now().isoformat(),
|
|
88
|
+
"last_timestamp": datetime.now().isoformat(),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Aggregate content by type
|
|
92
|
+
content_type = (
|
|
93
|
+
chunk_type
|
|
94
|
+
if chunk_type in ["thinking", "tool", "presentation"]
|
|
95
|
+
else "other"
|
|
96
|
+
)
|
|
97
|
+
self._content_buffers[source_key][content_type] += content
|
|
98
|
+
self._content_buffers[source_key]["last_timestamp"] = datetime.now().isoformat()
|
|
99
|
+
|
|
100
|
+
# Track tool usage
|
|
101
|
+
if "🔧 Using new_answer tool:" in content:
|
|
102
|
+
self.log_data["workflow_analysis"]["new_answers"] += 1
|
|
103
|
+
elif "🔧 Using vote tool:" in content:
|
|
104
|
+
self.log_data["workflow_analysis"]["votes"] += 1
|
|
105
|
+
|
|
106
|
+
# Update aggregated responses in log data
|
|
107
|
+
self.log_data["aggregated_responses"] = {}
|
|
108
|
+
for src, buffer in self._content_buffers.items():
|
|
109
|
+
self.log_data["aggregated_responses"][src] = {
|
|
110
|
+
"thinking": buffer["thinking"].strip(),
|
|
111
|
+
"tool_calls": buffer["tool"].strip(),
|
|
112
|
+
"presentation": buffer["presentation"].strip(),
|
|
113
|
+
"other_content": buffer["other"].strip(),
|
|
114
|
+
"first_timestamp": buffer["first_timestamp"],
|
|
115
|
+
"last_timestamp": buffer["last_timestamp"],
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Update log file periodically
|
|
119
|
+
if self.chunk_count % self.update_frequency == 0:
|
|
120
|
+
self._update_log_file()
|
|
121
|
+
|
|
122
|
+
def log_agent_content(
|
|
123
|
+
self, agent_id: str, content: str, content_type: str = "thinking"
|
|
124
|
+
):
|
|
125
|
+
"""Log agent-specific content.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
agent_id: The agent ID
|
|
129
|
+
content: The content to log
|
|
130
|
+
content_type: Type of content ("thinking", "tool", "status")
|
|
131
|
+
"""
|
|
132
|
+
if agent_id in self.log_data["agent_outputs"]:
|
|
133
|
+
entry = {
|
|
134
|
+
"timestamp": datetime.now().isoformat(),
|
|
135
|
+
"type": content_type,
|
|
136
|
+
"content": content,
|
|
137
|
+
}
|
|
138
|
+
self.log_data["agent_outputs"][agent_id].append(entry)
|
|
139
|
+
|
|
140
|
+
def log_orchestrator_event(self, event: str):
|
|
141
|
+
"""Log an orchestrator coordination event.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
event: The coordination event description
|
|
145
|
+
"""
|
|
146
|
+
event_entry = {"timestamp": datetime.now().isoformat(), "event": event}
|
|
147
|
+
self.log_data["orchestrator_events"].append(event_entry)
|
|
148
|
+
|
|
149
|
+
def finalize_session(self, final_answer: str = "", success: bool = True):
|
|
150
|
+
"""Finalize the coordination session.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
final_answer: The final coordinated answer
|
|
154
|
+
success: Whether coordination was successful
|
|
155
|
+
"""
|
|
156
|
+
self.log_data["metadata"]["end_time"] = datetime.now().isoformat()
|
|
157
|
+
self.log_data["metadata"]["status"] = "completed" if success else "failed"
|
|
158
|
+
self.log_data["session"]["final_answer"] = final_answer
|
|
159
|
+
|
|
160
|
+
# Calculate session duration
|
|
161
|
+
start_time = datetime.fromisoformat(self.log_data["metadata"]["start_time"])
|
|
162
|
+
end_time = datetime.fromisoformat(self.log_data["metadata"]["end_time"])
|
|
163
|
+
duration = (end_time - start_time).total_seconds()
|
|
164
|
+
self.log_data["metadata"]["duration_seconds"] = duration
|
|
165
|
+
|
|
166
|
+
# Final log file update
|
|
167
|
+
self._update_log_file()
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
"filename": self.filename,
|
|
171
|
+
"duration": duration,
|
|
172
|
+
"total_chunks": self.chunk_count,
|
|
173
|
+
"orchestrator_events": len(self.log_data["orchestrator_events"]),
|
|
174
|
+
"success": success,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
def get_log_filename(self) -> str:
|
|
178
|
+
"""Get the current log filename."""
|
|
179
|
+
return self.filename
|
|
180
|
+
|
|
181
|
+
def get_monitoring_commands(self) -> Dict[str, str]:
|
|
182
|
+
"""Get commands for monitoring the log file."""
|
|
183
|
+
return {
|
|
184
|
+
"tail": f"tail -f {self.filename}",
|
|
185
|
+
"watch": f"watch -n 1 'tail -20 {self.filename}'",
|
|
186
|
+
"code": f"code {self.filename}",
|
|
187
|
+
"less": f"less +F {self.filename}",
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
def _update_log_file(self):
|
|
191
|
+
"""Update the log file with current data."""
|
|
192
|
+
try:
|
|
193
|
+
with open(self.filename, "w") as f:
|
|
194
|
+
json.dump(self.log_data, f, indent=2, ensure_ascii=False)
|
|
195
|
+
except Exception as e:
|
|
196
|
+
# Silently fail to avoid disrupting coordination
|
|
197
|
+
pass
|