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
massgen/v1/logging.py ADDED
@@ -0,0 +1,974 @@
1
+ """
2
+ MassGen Logging System
3
+
4
+ This module provides comprehensive logging capabilities for the MassGen system,
5
+ recording all agent state changes, orchestration events, and system activities
6
+ to local files for detailed analysis.
7
+ """
8
+
9
+ import os
10
+ import json
11
+ import time
12
+ import logging
13
+ import threading
14
+ from datetime import datetime
15
+ from typing import Dict, Any, List, Optional, Union
16
+ from pathlib import Path
17
+ from dataclasses import dataclass, field, asdict
18
+ from collections import Counter
19
+ import textwrap
20
+
21
+ from .types import LogEntry, AnswerRecord, VoteRecord
22
+
23
+
24
+ class MassLogManager:
25
+ """
26
+ Comprehensive logging system for the MassGen framework.
27
+
28
+ Records all significant events including:
29
+ - Agent state changes (working, voted, failed)
30
+ - Answer updates and notifications
31
+ - Voting events and consensus decisions
32
+ - Phase transitions (collaboration, debate, consensus)
33
+ - System metrics and performance data
34
+
35
+ New organized structure:
36
+ logs/
37
+ └── YYYYMMDD_HHMMSS/
38
+ ├── display/
39
+ │ ├── agent_0.txt, agent_1.txt, ... # Real-time display logs
40
+ │ └── system.txt # System messages
41
+ ├── answers/
42
+ │ ├── agent_0.txt, agent_1.txt, ... # Agent answer histories
43
+ ├── votes/
44
+ │ ├── agent_0.txt, agent_1.txt, ... # Agent voting records
45
+ ├── events.jsonl # Structured event log
46
+ └── console.log # Python logging output
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ log_dir: str = "logs",
52
+ session_id: Optional[str] = None,
53
+ non_blocking: bool = False,
54
+ ):
55
+ """
56
+ Initialize the logging system.
57
+
58
+ Args:
59
+ log_dir: Directory to save log files
60
+ session_id: Unique identifier for this session
61
+ non_blocking: If True, disable file logging to prevent hanging issues
62
+ """
63
+ self.base_log_dir = Path(log_dir)
64
+ self.session_id = session_id or self._generate_session_id()
65
+ self.non_blocking = non_blocking
66
+
67
+ if self.non_blocking:
68
+ print(f"⚠️ LOGGING: Non-blocking mode enabled - file logging disabled")
69
+
70
+ # Create main session directory
71
+ self.session_dir = self.base_log_dir / self.session_id
72
+ if not self.non_blocking:
73
+ try:
74
+ self.session_dir.mkdir(parents=True, exist_ok=True)
75
+ except Exception as e:
76
+ print(
77
+ f"Warning: Failed to create session directory, enabling non-blocking mode: {e}"
78
+ )
79
+ self.non_blocking = True
80
+
81
+ # Create subdirectories
82
+ self.display_dir = self.session_dir / "display"
83
+ self.answers_dir = self.session_dir / "answers"
84
+ self.votes_dir = self.session_dir / "votes"
85
+
86
+ if not self.non_blocking:
87
+ try:
88
+ self.display_dir.mkdir(exist_ok=True)
89
+ self.answers_dir.mkdir(exist_ok=True)
90
+ self.votes_dir.mkdir(exist_ok=True)
91
+ except Exception as e:
92
+ print(
93
+ f"Warning: Failed to create subdirectories, enabling non-blocking mode: {e}"
94
+ )
95
+ self.non_blocking = True
96
+
97
+ # File paths
98
+ self.events_log_file = self.session_dir / "events.jsonl"
99
+ self.console_log_file = self.session_dir / "console.log"
100
+ self.system_log_file = self.display_dir / "system.txt"
101
+
102
+ # In-memory log storage for real-time access
103
+ self.log_entries: List[LogEntry] = []
104
+ self.agent_logs: Dict[int, List[LogEntry]] = {}
105
+
106
+ # MassGen-specific event counters
107
+ self.event_counters = {
108
+ "answer_updates": 0,
109
+ "votes_cast": 0,
110
+ "consensus_reached": 0,
111
+ "debates_started": 0,
112
+ "agent_restarts": 0,
113
+ "notifications_sent": 0,
114
+ }
115
+
116
+ # Thread lock for concurrent access
117
+ self._lock = threading.Lock()
118
+
119
+ # Initialize logging
120
+ self._setup_logging()
121
+
122
+ # Initialize system log file
123
+ if not self.non_blocking:
124
+ self._initialize_system_log()
125
+
126
+ # Log session start
127
+ self.log_event(
128
+ "session_started",
129
+ data={
130
+ "session_id": self.session_id,
131
+ "timestamp": time.time(),
132
+ "session_dir": str(self.session_dir),
133
+ "non_blocking_mode": self.non_blocking,
134
+ },
135
+ )
136
+
137
+ def _generate_session_id(self) -> str:
138
+ """Generate a unique session ID."""
139
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
140
+ return f"{timestamp}"
141
+
142
+ def _initialize_system_log(self):
143
+ """Initialize the system log file with header."""
144
+ if self.non_blocking:
145
+ return
146
+
147
+ try:
148
+ with open(self.system_log_file, "w", encoding="utf-8") as f:
149
+ f.write(f"MassGen System Messages Log\n")
150
+ f.write(f"Session ID: {self.session_id}\n")
151
+ f.write(
152
+ f"Session started: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
153
+ )
154
+ f.write("=" * 80 + "\n\n")
155
+ except Exception as e:
156
+ print(f"Warning: Failed to initialize system log: {e}")
157
+
158
+ def _setup_logging(self):
159
+ """Set up file logging configuration."""
160
+ # Skip file logging setup in non-blocking mode
161
+ if self.non_blocking:
162
+ return
163
+
164
+ log_formatter = logging.Formatter(
165
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
166
+ )
167
+
168
+ # Ensure log directory exists before creating file handler
169
+ try:
170
+ self.session_dir.mkdir(parents=True, exist_ok=True)
171
+ except Exception as e:
172
+ print(
173
+ f"Warning: Failed to create session directory {self.session_dir}, skipping file logging: {e}"
174
+ )
175
+ return
176
+
177
+ # Create console log file handler
178
+ console_log_handler = logging.FileHandler(self.console_log_file)
179
+ console_log_handler.setFormatter(log_formatter)
180
+ console_log_handler.setLevel(logging.DEBUG)
181
+
182
+ # Add handler to the mass logger
183
+ mass_logger = logging.getLogger("massgen")
184
+ mass_logger.addHandler(console_log_handler)
185
+ mass_logger.setLevel(logging.DEBUG)
186
+
187
+ # Prevent duplicate console logs
188
+ mass_logger.propagate = False
189
+
190
+ # Add console handler if not already present
191
+ if not any(isinstance(h, logging.StreamHandler) for h in mass_logger.handlers):
192
+ console_handler = logging.StreamHandler()
193
+ console_handler.setFormatter(log_formatter)
194
+ console_handler.setLevel(logging.INFO)
195
+ mass_logger.addHandler(console_handler)
196
+
197
+ def _format_timestamp(self, timestamp: float) -> str:
198
+ """Format timestamp to human-readable format."""
199
+ return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
200
+
201
+ def _format_answer_record(self, record: AnswerRecord, agent_id: int) -> str:
202
+ """Format an AnswerRecord into human-readable text."""
203
+ timestamp_str = self._format_timestamp(record.timestamp)
204
+
205
+ # Status emoji mapping
206
+ status_emoji = {"working": "🔄", "voted": "✅", "failed": "❌", "unknown": "❓"}
207
+ emoji = status_emoji.get(record.status, "��")
208
+
209
+ return f"""
210
+ {emoji} UPDATE DETAILS
211
+ 🕒 Time: {timestamp_str}
212
+ 📊 Status: {record.status.upper()}
213
+ 📏 Length: {len(record.answer)} characters
214
+
215
+ 📄 Content:
216
+ {record.answer}
217
+
218
+ {'=' * 80}
219
+ """
220
+
221
+ def _format_vote_record(self, record: VoteRecord, agent_id: int) -> str:
222
+ """Format a VoteRecord into human-readable text."""
223
+ timestamp_str = self._format_timestamp(record.timestamp)
224
+
225
+ reason_text = record.reason if record.reason else "No reason provided"
226
+
227
+ return f"""
228
+ 🗳️ VOTE CAST
229
+ 🕒 Time: {timestamp_str}
230
+ 👤 Voter: Agent {record.voter_id}
231
+ 🎯 Target: Agent {record.target_id}
232
+
233
+ 📝 Reasoning:
234
+ {reason_text}
235
+
236
+ {'=' * 80}
237
+ """
238
+
239
+ def _write_agent_answers(self, agent_id: int, answer_records: List[AnswerRecord]):
240
+ """Write agent's answer history to the answers folder."""
241
+ if self.non_blocking:
242
+ return
243
+
244
+ try:
245
+ answers_file = self.answers_dir / f"agent_{agent_id}.txt"
246
+
247
+ with open(answers_file, "w", encoding="utf-8") as f:
248
+ # Clean header with useful information
249
+ f.write("=" * 80 + "\n")
250
+ f.write(f"📝 MASSGEN AGENT {agent_id} - ANSWER HISTORY\n")
251
+ f.write("=" * 80 + "\n")
252
+ f.write(f"🆔 Session: {self.session_id}\n")
253
+ f.write(
254
+ f"📅 Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
255
+ )
256
+
257
+ if answer_records:
258
+ # Calculate some summary statistics
259
+ total_chars = sum(len(record.answer) for record in answer_records)
260
+ avg_chars = (
261
+ total_chars / len(answer_records) if answer_records else 0
262
+ )
263
+ first_update = answer_records[0].timestamp if answer_records else 0
264
+ last_update = answer_records[-1].timestamp if answer_records else 0
265
+ duration = (
266
+ last_update - first_update if len(answer_records) > 1 else 0
267
+ )
268
+
269
+ f.write(f"📊 Total Updates: {len(answer_records)}\n")
270
+ f.write(f"📏 Total Characters: {total_chars:,}\n")
271
+ f.write(f"📈 Average Length: {avg_chars:.0f} chars\n")
272
+ if duration > 0:
273
+ duration_str = (
274
+ f"{duration/60:.1f} minutes"
275
+ if duration > 60
276
+ else f"{duration:.1f} seconds"
277
+ )
278
+ f.write(f"⏱️ Time Span: {duration_str}\n")
279
+ else:
280
+ f.write("❌ No answer records found for this agent.\n")
281
+
282
+ f.write("=" * 80 + "\n\n")
283
+
284
+ if answer_records:
285
+ for i, record in enumerate(answer_records, 1):
286
+ # Calculate time elapsed since session start
287
+ elapsed = record.timestamp - (
288
+ answer_records[0].timestamp
289
+ if answer_records
290
+ else record.timestamp
291
+ )
292
+ elapsed_str = (
293
+ f"[+{elapsed/60:.1f}m]"
294
+ if elapsed > 60
295
+ else f"[+{elapsed:.1f}s]"
296
+ )
297
+
298
+ f.write(f"🔢 UPDATE #{i} {elapsed_str}\n")
299
+ f.write(self._format_answer_record(record, agent_id))
300
+ f.write("\n")
301
+
302
+ except Exception as e:
303
+ print(f"Warning: Failed to write answers for agent {agent_id}: {e}")
304
+
305
+ def _write_agent_votes(self, agent_id: int, vote_records: List[VoteRecord]):
306
+ """Write agent's vote history to the votes folder."""
307
+ if self.non_blocking:
308
+ return
309
+
310
+ try:
311
+ votes_file = self.votes_dir / f"agent_{agent_id}.txt"
312
+
313
+ with open(votes_file, "w", encoding="utf-8") as f:
314
+ # Clean header with useful information
315
+ f.write("=" * 80 + "\n")
316
+ f.write(f"🗳️ MASSGEN AGENT {agent_id} - VOTE HISTORY\n")
317
+ f.write("=" * 80 + "\n")
318
+ f.write(f"🆔 Session: {self.session_id}\n")
319
+ f.write(
320
+ f"📅 Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
321
+ )
322
+
323
+ if vote_records:
324
+ # Calculate voting statistics
325
+ vote_targets = {}
326
+ total_reason_chars = 0
327
+ for vote in vote_records:
328
+ vote_targets[vote.target_id] = (
329
+ vote_targets.get(vote.target_id, 0) + 1
330
+ )
331
+ total_reason_chars += len(vote.reason) if vote.reason else 0
332
+
333
+ most_voted_target = (
334
+ max(vote_targets.items(), key=lambda x: x[1])
335
+ if vote_targets
336
+ else None
337
+ )
338
+ avg_reason_length = (
339
+ total_reason_chars / len(vote_records) if vote_records else 0
340
+ )
341
+
342
+ first_vote = vote_records[0].timestamp if vote_records else 0
343
+ last_vote = vote_records[-1].timestamp if vote_records else 0
344
+ voting_duration = (
345
+ last_vote - first_vote if len(vote_records) > 1 else 0
346
+ )
347
+
348
+ f.write(f"📊 Total Votes Cast: {len(vote_records)}\n")
349
+ f.write(f"🎯 Unique Targets: {len(vote_targets)}\n")
350
+ if most_voted_target:
351
+ f.write(
352
+ f"👑 Most Voted For: Agent {most_voted_target[0]} ({most_voted_target[1]} votes)\n"
353
+ )
354
+ f.write(f"📝 Avg Reason Length: {avg_reason_length:.0f} chars\n")
355
+ if voting_duration > 0:
356
+ duration_str = (
357
+ f"{voting_duration/60:.1f} minutes"
358
+ if voting_duration > 60
359
+ else f"{voting_duration:.1f} seconds"
360
+ )
361
+ f.write(f"⏱️ Voting Duration: {duration_str}\n")
362
+ else:
363
+ f.write("❌ No vote records found for this agent.\n")
364
+
365
+ f.write("=" * 80 + "\n\n")
366
+
367
+ if vote_records:
368
+ for i, record in enumerate(vote_records, 1):
369
+ # Calculate time elapsed since first vote
370
+ elapsed = record.timestamp - (
371
+ vote_records[0].timestamp
372
+ if vote_records
373
+ else record.timestamp
374
+ )
375
+ elapsed_str = (
376
+ f"[+{elapsed/60:.1f}m]"
377
+ if elapsed > 60
378
+ else f"[+{elapsed:.1f}s]"
379
+ )
380
+
381
+ f.write(f"🗳️ VOTE #{i} {elapsed_str}\n")
382
+ f.write(self._format_vote_record(record, agent_id))
383
+ f.write("\n")
384
+
385
+ except Exception as e:
386
+ print(f"Warning: Failed to write votes for agent {agent_id}: {e}")
387
+
388
+ def log_event(
389
+ self,
390
+ event_type: str,
391
+ agent_id: Optional[int] = None,
392
+ phase: str = "unknown",
393
+ data: Optional[Dict[str, Any]] = None,
394
+ ):
395
+ """
396
+ Log a general system event.
397
+
398
+ Args:
399
+ event_type: Type of event (e.g., "session_started", "phase_change")
400
+ agent_id: Agent ID if event is agent-specific
401
+ phase: Current system phase
402
+ data: Additional event data
403
+ """
404
+ with self._lock:
405
+ entry = LogEntry(
406
+ timestamp=time.time(),
407
+ event_type=event_type,
408
+ agent_id=agent_id,
409
+ phase=phase,
410
+ data=data or {},
411
+ session_id=self.session_id,
412
+ )
413
+
414
+ self.log_entries.append(entry)
415
+
416
+ # Also store in agent-specific logs
417
+ if agent_id is not None:
418
+ if agent_id not in self.agent_logs:
419
+ self.agent_logs[agent_id] = []
420
+ self.agent_logs[agent_id].append(entry)
421
+
422
+ # Write to file immediately
423
+ self._write_log_entry(entry)
424
+
425
+ def log_agent_answer_update(
426
+ self, agent_id: int, answer: str, phase: str = "unknown", orchestrator=None
427
+ ):
428
+ """
429
+ Log agent answer update with detailed information and immediately save to file.
430
+
431
+ Args:
432
+ agent_id: Agent ID
433
+ answer: Updated answer content
434
+ phase: Current workflow phase
435
+ orchestrator: MassOrchestrator instance to get agent state data
436
+ """
437
+ data = {
438
+ "answer": answer,
439
+ "answer_length": len(answer),
440
+ }
441
+
442
+ self.log_event("agent_answer_update", agent_id, phase, data)
443
+
444
+ # Immediately write agent answer history to file
445
+ if orchestrator and agent_id in orchestrator.agent_states:
446
+ agent_state = orchestrator.agent_states[agent_id]
447
+ self._write_agent_answers(agent_id, agent_state.updated_answers)
448
+
449
+ def log_agent_status_change(
450
+ self, agent_id: int, old_status: str, new_status: str, phase: str = "unknown"
451
+ ):
452
+ """
453
+ Log agent status change.
454
+
455
+ Args:
456
+ agent_id: Agent ID
457
+ old_status: Previous status
458
+ new_status: New status
459
+ phase: Current workflow phase
460
+ """
461
+ data = {
462
+ "old_status": old_status,
463
+ "new_status": new_status,
464
+ "status_change": f"{old_status} {new_status}",
465
+ }
466
+
467
+ self.log_event("agent_status_change", agent_id, phase, data)
468
+
469
+ # Status changes are captured in system state snapshots
470
+
471
+ def log_system_state_snapshot(self, orchestrator, phase: str = "unknown"):
472
+ """
473
+ Log a complete system state snapshot including all agent answers and voting status.
474
+
475
+ Args:
476
+ orchestrator: The MassOrchestrator instance
477
+ phase: Current workflow phase
478
+ """
479
+
480
+ # Collect all agent states
481
+ agent_states = {}
482
+ all_agent_answers = {}
483
+ vote_records = []
484
+
485
+ for agent_id, agent_state in orchestrator.agent_states.items():
486
+ # Full agent state information
487
+ agent_states[agent_id] = {
488
+ "status": agent_state.status,
489
+ "curr_answer": agent_state.curr_answer,
490
+ "vote_target": (
491
+ agent_state.curr_vote.target_id if agent_state.curr_vote else None
492
+ ),
493
+ "execution_time": agent_state.execution_time,
494
+ "update_count": len(agent_state.updated_answers),
495
+ "seen_updates_timestamps": agent_state.seen_updates_timestamps,
496
+ }
497
+
498
+ # Answer history for each agent
499
+ all_agent_answers[agent_id] = {
500
+ "current_answer": agent_state.curr_answer,
501
+ "answer_history": [
502
+ {
503
+ "timestamp": update.timestamp,
504
+ "answer": update.answer,
505
+ "status": update.status,
506
+ }
507
+ for update in agent_state.updated_answers
508
+ ],
509
+ }
510
+
511
+ # Collect voting information
512
+ for vote in orchestrator.votes:
513
+ vote_records.append(
514
+ {
515
+ "voter_id": vote.voter_id,
516
+ "target_id": vote.target_id,
517
+ "timestamp": vote.timestamp,
518
+ }
519
+ )
520
+
521
+ # Calculate voting status
522
+ vote_counts = Counter(vote.target_id for vote in orchestrator.votes)
523
+ voting_status = {
524
+ "vote_distribution": dict(vote_counts),
525
+ "total_votes_cast": len(orchestrator.votes),
526
+ "total_agents": len(orchestrator.agents),
527
+ "consensus_reached": orchestrator.system_state.consensus_reached,
528
+ "winning_agent_id": orchestrator.system_state.representative_agent_id,
529
+ "votes_needed_for_consensus": max(
530
+ 1, int(len(orchestrator.agents) * orchestrator.consensus_threshold)
531
+ ),
532
+ }
533
+
534
+ # Complete system state snapshot
535
+ system_snapshot = {
536
+ "agent_states": agent_states,
537
+ "agent_answers": all_agent_answers,
538
+ "voting_records": vote_records,
539
+ "voting_status": voting_status,
540
+ "system_phase": phase,
541
+ "system_runtime": (
542
+ (time.time() - orchestrator.system_state.start_time)
543
+ if orchestrator.system_state.start_time
544
+ else 0
545
+ ),
546
+ }
547
+
548
+ # Log the system snapshot
549
+ self.log_event("system_state_snapshot", phase=phase, data=system_snapshot)
550
+
551
+ # Write system state to each agent's log file for complete context
552
+ system_state_entry = {
553
+ "timestamp": time.time(),
554
+ "event": "system_state_snapshot",
555
+ "phase": phase,
556
+ "system_state": system_snapshot,
557
+ }
558
+
559
+ # Save individual agent states to answers and votes folders
560
+ for agent_id, agent_state in orchestrator.agent_states.items():
561
+ # Save answer history
562
+ self._write_agent_answers(agent_id, agent_state.updated_answers)
563
+
564
+ # Save vote history
565
+ self._write_agent_votes(agent_id, agent_state.cast_votes)
566
+
567
+ # Write system state to each agent's display log file for complete context
568
+ for agent_id in orchestrator.agents.keys():
569
+ self._write_agent_display_log(agent_id, system_state_entry)
570
+
571
+ return system_snapshot
572
+
573
+ def log_voting_event(
574
+ self,
575
+ voter_id: int,
576
+ target_id: int,
577
+ phase: str = "unknown",
578
+ reason: str = "",
579
+ orchestrator=None,
580
+ ):
581
+ """
582
+ Log a voting event with detailed information and immediately save to file.
583
+
584
+ Args:
585
+ voter_id: ID of the agent casting the vote
586
+ target_id: ID of the agent being voted for
587
+ phase: Current workflow phase
588
+ reason: Reason for the vote
589
+ orchestrator: MassOrchestrator instance to get agent state data
590
+ """
591
+ with self._lock:
592
+ self.event_counters["votes_cast"] += 1
593
+
594
+ data = {
595
+ "voter_id": voter_id,
596
+ "target_id": target_id,
597
+ "reason": reason,
598
+ "total_votes_cast": self.event_counters["votes_cast"],
599
+ }
600
+
601
+ self.log_event("voting_event", voter_id, phase, data)
602
+
603
+ # Immediately write agent vote history to file
604
+ if orchestrator and voter_id in orchestrator.agent_states:
605
+ agent_state = orchestrator.agent_states[voter_id]
606
+ self._write_agent_votes(voter_id, agent_state.cast_votes)
607
+
608
+ def log_consensus_reached(
609
+ self,
610
+ winning_agent_id: int,
611
+ vote_distribution: Dict[int, int],
612
+ is_fallback: bool = False,
613
+ phase: str = "unknown",
614
+ ):
615
+ """
616
+ Log when consensus is reached.
617
+
618
+ Args:
619
+ winning_agent_id: ID of the winning agent
620
+ vote_distribution: Dictionary of agent_id -> vote_count
621
+ is_fallback: Whether this was a fallback consensus (timeout)
622
+ phase: Current workflow phase
623
+ """
624
+ with self._lock:
625
+ self.event_counters["consensus_reached"] += 1
626
+
627
+ data = {
628
+ "winning_agent_id": winning_agent_id,
629
+ "vote_distribution": vote_distribution,
630
+ "is_fallback": is_fallback,
631
+ "total_consensus_events": self.event_counters["consensus_reached"],
632
+ }
633
+
634
+ self.log_event("consensus_reached", winning_agent_id, phase, data)
635
+
636
+ # Log to all agent display files
637
+ consensus_entry = {
638
+ "timestamp": time.time(),
639
+ "event": "consensus_reached",
640
+ "phase": phase,
641
+ "winning_agent_id": winning_agent_id,
642
+ "vote_distribution": vote_distribution,
643
+ "is_fallback": is_fallback,
644
+ }
645
+ for agent_id in vote_distribution.keys():
646
+ self._write_agent_display_log(agent_id, consensus_entry)
647
+
648
+ def log_phase_transition(
649
+ self, old_phase: str, new_phase: str, additional_data: Dict[str, Any] = None
650
+ ):
651
+ """
652
+ Log system phase transitions.
653
+
654
+ Args:
655
+ old_phase: Previous phase
656
+ new_phase: New phase
657
+ additional_data: Additional context data
658
+ """
659
+ data = {
660
+ "old_phase": old_phase,
661
+ "new_phase": new_phase,
662
+ "phase_transition": f"{old_phase} -> {new_phase}",
663
+ **(additional_data or {}),
664
+ }
665
+
666
+ self.log_event("phase_transition", phase=new_phase, data=data)
667
+
668
+ def log_notification_sent(
669
+ self,
670
+ agent_id: int,
671
+ notification_type: str,
672
+ content_preview: str,
673
+ phase: str = "unknown",
674
+ ):
675
+ """
676
+ Log when a notification is sent to an agent.
677
+
678
+ Args:
679
+ agent_id: Target agent ID
680
+ notification_type: Type of notification (update, debate, presentation, prompt)
681
+ content_preview: Preview of notification content
682
+ phase: Current workflow phase
683
+ """
684
+ with self._lock:
685
+ self.event_counters["notifications_sent"] += 1
686
+
687
+ data = {
688
+ "notification_type": notification_type,
689
+ "content_preview": (
690
+ content_preview[:200] + "..."
691
+ if len(content_preview) > 200
692
+ else content_preview
693
+ ),
694
+ "content_length": len(content_preview),
695
+ "total_notifications_sent": self.event_counters["notifications_sent"],
696
+ }
697
+
698
+ self.log_event("notification_sent", agent_id, phase, data)
699
+
700
+ # Log to agent display file
701
+ notification_entry = {
702
+ "timestamp": time.time(),
703
+ "event": "notification_received",
704
+ "phase": phase,
705
+ "notification_type": notification_type,
706
+ "content": content_preview,
707
+ }
708
+ self._write_agent_display_log(agent_id, notification_entry)
709
+
710
+ def log_agent_restart(self, agent_id: int, reason: str, phase: str = "unknown"):
711
+ """
712
+ Log when an agent is restarted.
713
+
714
+ Args:
715
+ agent_id: ID of the restarted agent
716
+ reason: Reason for restart
717
+ phase: Current workflow phase
718
+ """
719
+ with self._lock:
720
+ self.event_counters["agent_restarts"] += 1
721
+
722
+ data = {
723
+ "restart_reason": reason,
724
+ "total_restarts": self.event_counters["agent_restarts"],
725
+ }
726
+
727
+ self.log_event("agent_restart", agent_id, phase, data)
728
+
729
+ # Log to agent display file
730
+ restart_entry = {
731
+ "timestamp": time.time(),
732
+ "event": "agent_restarted",
733
+ "phase": phase,
734
+ "reason": reason,
735
+ }
736
+ self._write_agent_display_log(agent_id, restart_entry)
737
+
738
+ def log_debate_started(self, phase: str = "unknown"):
739
+ """
740
+ Log when a debate phase starts.
741
+
742
+ Args:
743
+ phase: Current workflow phase
744
+ """
745
+ with self._lock:
746
+ self.event_counters["debates_started"] += 1
747
+
748
+ data = {"total_debates": self.event_counters["debates_started"]}
749
+
750
+ self.log_event("debate_started", phase=phase, data=data)
751
+
752
+ def log_task_completion(self, final_solution: Dict[str, Any]):
753
+ """
754
+ Log task completion with final results.
755
+
756
+ Args:
757
+ final_solution: Complete final solution data
758
+ """
759
+ data = {"final_solution": final_solution, "completion_timestamp": time.time()}
760
+
761
+ self.log_event("task_completed", phase="completed", data=data)
762
+
763
+ def _write_log_entry(self, entry: LogEntry):
764
+ """Write a single log entry to the session JSONL file."""
765
+ # Skip file operations in non-blocking mode
766
+ if self.non_blocking:
767
+ return
768
+
769
+ try:
770
+ # Create directory if it doesn't exist
771
+ self.events_log_file.parent.mkdir(parents=True, exist_ok=True)
772
+
773
+ with open(self.events_log_file, "a", buffering=1) as f: # Line buffering
774
+ json_line = json.dumps(entry.to_dict(), default=str, ensure_ascii=False)
775
+ f.write(json_line + "\n")
776
+ f.flush()
777
+ except Exception as e:
778
+ print(f"Warning: Failed to write log entry: {e}")
779
+
780
+ def _write_agent_display_log(self, agent_id: int, data: Dict[str, Any]):
781
+ """Write agent-specific display log entry."""
782
+ # Skip file operations in non-blocking mode
783
+ if self.non_blocking:
784
+ return
785
+
786
+ try:
787
+ agent_log_file = self.display_dir / f"agent_{agent_id}.txt"
788
+
789
+ # Create directory if it doesn't exist
790
+ agent_log_file.parent.mkdir(parents=True, exist_ok=True)
791
+
792
+ # Initialize file if it doesn't exist
793
+ if not agent_log_file.exists():
794
+ with open(agent_log_file, "w", encoding="utf-8") as f:
795
+ f.write(f"MassGen Agent {agent_id} Display Log\n")
796
+ f.write(f"Session: {self.session_id}\n")
797
+ f.write(
798
+ f"Started: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
799
+ )
800
+ f.write("=" * 80 + "\n\n")
801
+
802
+ # Write event entry
803
+ with open(agent_log_file, "a", encoding="utf-8") as f:
804
+ timestamp_str = self._format_timestamp(
805
+ data.get("timestamp", time.time())
806
+ )
807
+ f.write(f"[{timestamp_str}] {data.get('event', 'unknown_event')}\n")
808
+
809
+ # Write event details
810
+ for key, value in data.items():
811
+ if key not in ["timestamp", "event"]:
812
+ f.write(f" {key}: {value}\n")
813
+ f.write("\n")
814
+ f.flush()
815
+ except Exception as e:
816
+ print(f"Warning: Failed to write agent display log: {e}")
817
+
818
+ def _write_system_log(self, message: str):
819
+ """Write a system message to the system log file."""
820
+ if self.non_blocking:
821
+ return
822
+
823
+ try:
824
+ with open(self.system_log_file, "a", encoding="utf-8") as f:
825
+ timestamp = datetime.now().strftime("%H:%M:%S")
826
+ f.write(f"[{timestamp}] {message}\n")
827
+ f.flush() # Ensure immediate write
828
+ except Exception as e:
829
+ print(f"Error writing to system log: {e}")
830
+
831
+ def get_agent_history(self, agent_id: int) -> List[LogEntry]:
832
+ """Get complete history for a specific agent."""
833
+ with self._lock:
834
+ return self.agent_logs.get(agent_id, []).copy()
835
+
836
+ def get_session_summary(self) -> Dict[str, Any]:
837
+ """Get comprehensive session summary."""
838
+ with self._lock:
839
+ # Count events by type
840
+ event_counts = {}
841
+ agent_activities = {}
842
+
843
+ for entry in self.log_entries:
844
+ # Count events
845
+ event_counts[entry.event_type] = (
846
+ event_counts.get(entry.event_type, 0) + 1
847
+ )
848
+
849
+ # Count agent activities
850
+ if entry.agent_id is not None:
851
+ agent_id = entry.agent_id
852
+ if agent_id not in agent_activities:
853
+ agent_activities[agent_id] = []
854
+ agent_activities[agent_id].append(
855
+ {
856
+ "timestamp": entry.timestamp,
857
+ "event_type": entry.event_type,
858
+ "phase": entry.phase,
859
+ }
860
+ )
861
+
862
+ return {
863
+ "session_id": self.session_id,
864
+ "total_events": len(self.log_entries),
865
+ "event_counts": event_counts,
866
+ "agents_involved": list(agent_activities.keys()),
867
+ "agent_activities": agent_activities,
868
+ "session_duration": self._calculate_session_duration(),
869
+ "log_files": {
870
+ "session_dir": str(self.session_dir),
871
+ "events_log": str(self.events_log_file),
872
+ "console_log": str(self.console_log_file),
873
+ "display_dir": str(self.display_dir),
874
+ "answers_dir": str(self.answers_dir),
875
+ "votes_dir": str(self.votes_dir),
876
+ },
877
+ }
878
+
879
+ def _calculate_session_duration(self) -> float:
880
+ """Calculate total session duration."""
881
+ if not self.log_entries:
882
+ return 0.0
883
+
884
+ start_time = min(entry.timestamp for entry in self.log_entries)
885
+ end_time = max(entry.timestamp for entry in self.log_entries)
886
+ return end_time - start_time
887
+
888
+ def save_agent_states(self, orchestrator):
889
+ """Save current agent states to answers and votes folders."""
890
+ if self.non_blocking:
891
+ return
892
+
893
+ try:
894
+ for agent_id, agent_state in orchestrator.agent_states.items():
895
+ # Save answer history
896
+ self._write_agent_answers(agent_id, agent_state.updated_answers)
897
+
898
+ # Save vote history
899
+ self._write_agent_votes(agent_id, agent_state.cast_votes)
900
+ except Exception as e:
901
+ print(f"Warning: Failed to save agent states: {e}")
902
+
903
+ def cleanup(self):
904
+ """Clean up and finalize the logging session."""
905
+ self.log_event(
906
+ "session_ended",
907
+ data={
908
+ "end_timestamp": time.time(),
909
+ "total_events_logged": len(self.log_entries),
910
+ },
911
+ )
912
+
913
+ def get_session_statistics(self) -> Dict[str, Any]:
914
+ """
915
+ Get comprehensive session statistics.
916
+
917
+ Returns:
918
+ Dictionary containing session metrics and statistics
919
+ """
920
+ with self._lock:
921
+ total_events = len(self.log_entries)
922
+ agent_event_counts = {}
923
+
924
+ for agent_id, logs in self.agent_logs.items():
925
+ agent_event_counts[agent_id] = len(logs)
926
+
927
+ return {
928
+ "session_id": self.session_id,
929
+ "total_events": total_events,
930
+ "event_counters": self.event_counters.copy(),
931
+ "agent_event_counts": agent_event_counts,
932
+ "total_agents": len(self.agent_logs),
933
+ "session_duration": time.time()
934
+ - (self.log_entries[0].timestamp if self.log_entries else time.time()),
935
+ }
936
+
937
+
938
+ # Global log manager instance
939
+ _log_manager: Optional[MassLogManager] = None
940
+
941
+
942
+ def initialize_logging(
943
+ log_dir: str = "logs", session_id: Optional[str] = None, non_blocking: bool = False
944
+ ) -> MassLogManager:
945
+ """Initialize the global logging system."""
946
+ global _log_manager
947
+
948
+ # Check environment variable for non-blocking mode
949
+ env_non_blocking = os.getenv("MassGen_NON_BLOCKING_LOGGING", "").lower() in (
950
+ "true",
951
+ "1",
952
+ "yes",
953
+ )
954
+ if env_non_blocking:
955
+ print(
956
+ "🔧 MassGen_NON_BLOCKING_LOGGING environment variable detected - enabling non-blocking mode"
957
+ )
958
+ non_blocking = True
959
+
960
+ _log_manager = MassLogManager(log_dir, session_id, non_blocking)
961
+ return _log_manager
962
+
963
+
964
+ def get_log_manager() -> Optional[MassLogManager]:
965
+ """Get the current log manager instance."""
966
+ return _log_manager
967
+
968
+
969
+ def cleanup_logging():
970
+ """Cleanup the global logging system."""
971
+ global _log_manager
972
+ if _log_manager:
973
+ _log_manager.cleanup()
974
+ _log_manager = None