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
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
|