massgen 0.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of massgen might be problematic. Click here for more details.

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