massgen 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of massgen might be problematic. Click here for more details.
- massgen/__init__.py +94 -0
- massgen/agent_config.py +507 -0
- massgen/backend/CLAUDE_API_RESEARCH.md +266 -0
- massgen/backend/Function calling openai responses.md +1161 -0
- massgen/backend/GEMINI_API_DOCUMENTATION.md +410 -0
- massgen/backend/OPENAI_RESPONSES_API_FORMAT.md +65 -0
- massgen/backend/__init__.py +25 -0
- massgen/backend/base.py +180 -0
- massgen/backend/chat_completions.py +228 -0
- massgen/backend/claude.py +661 -0
- massgen/backend/gemini.py +652 -0
- massgen/backend/grok.py +187 -0
- massgen/backend/response.py +397 -0
- massgen/chat_agent.py +440 -0
- massgen/cli.py +686 -0
- massgen/configs/README.md +293 -0
- massgen/configs/creative_team.yaml +53 -0
- massgen/configs/gemini_4o_claude.yaml +31 -0
- massgen/configs/news_analysis.yaml +51 -0
- massgen/configs/research_team.yaml +51 -0
- massgen/configs/single_agent.yaml +18 -0
- massgen/configs/single_flash2.5.yaml +44 -0
- massgen/configs/technical_analysis.yaml +51 -0
- massgen/configs/three_agents_default.yaml +31 -0
- massgen/configs/travel_planning.yaml +51 -0
- massgen/configs/two_agents.yaml +39 -0
- massgen/frontend/__init__.py +20 -0
- massgen/frontend/coordination_ui.py +945 -0
- massgen/frontend/displays/__init__.py +24 -0
- massgen/frontend/displays/base_display.py +83 -0
- massgen/frontend/displays/rich_terminal_display.py +3497 -0
- massgen/frontend/displays/simple_display.py +93 -0
- massgen/frontend/displays/terminal_display.py +381 -0
- massgen/frontend/logging/__init__.py +9 -0
- massgen/frontend/logging/realtime_logger.py +197 -0
- massgen/message_templates.py +431 -0
- massgen/orchestrator.py +1222 -0
- massgen/tests/__init__.py +10 -0
- massgen/tests/multi_turn_conversation_design.md +214 -0
- massgen/tests/multiturn_llm_input_analysis.md +189 -0
- massgen/tests/test_case_studies.md +113 -0
- massgen/tests/test_claude_backend.py +310 -0
- massgen/tests/test_grok_backend.py +160 -0
- massgen/tests/test_message_context_building.py +293 -0
- massgen/tests/test_rich_terminal_display.py +378 -0
- massgen/tests/test_v3_3agents.py +117 -0
- massgen/tests/test_v3_simple.py +216 -0
- massgen/tests/test_v3_three_agents.py +272 -0
- massgen/tests/test_v3_two_agents.py +176 -0
- massgen/utils.py +79 -0
- massgen/v1/README.md +330 -0
- massgen/v1/__init__.py +91 -0
- massgen/v1/agent.py +605 -0
- massgen/v1/agents.py +330 -0
- massgen/v1/backends/gemini.py +584 -0
- massgen/v1/backends/grok.py +410 -0
- massgen/v1/backends/oai.py +571 -0
- massgen/v1/cli.py +351 -0
- massgen/v1/config.py +169 -0
- massgen/v1/examples/fast-4o-mini-config.yaml +44 -0
- massgen/v1/examples/fast_config.yaml +44 -0
- massgen/v1/examples/production.yaml +70 -0
- massgen/v1/examples/single_agent.yaml +39 -0
- massgen/v1/logging.py +974 -0
- massgen/v1/main.py +368 -0
- massgen/v1/orchestrator.py +1138 -0
- massgen/v1/streaming_display.py +1190 -0
- massgen/v1/tools.py +160 -0
- massgen/v1/types.py +245 -0
- massgen/v1/utils.py +199 -0
- massgen-0.0.3.dist-info/METADATA +568 -0
- massgen-0.0.3.dist-info/RECORD +76 -0
- massgen-0.0.3.dist-info/WHEEL +5 -0
- massgen-0.0.3.dist-info/entry_points.txt +2 -0
- massgen-0.0.3.dist-info/licenses/LICENSE +204 -0
- massgen-0.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MassGen Streaming Display System
|
|
3
|
+
|
|
4
|
+
Provides real-time multi-region display for MassGen agents with:
|
|
5
|
+
- Individual agent columns showing streaming conversations
|
|
6
|
+
- System status panel with phase transitions and voting
|
|
7
|
+
- File logging for all conversations and events
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
import threading
|
|
13
|
+
import unicodedata
|
|
14
|
+
import sys
|
|
15
|
+
import re
|
|
16
|
+
from typing import Dict, List, Optional, Callable, Union
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MultiRegionDisplay:
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
display_enabled: bool = True,
|
|
24
|
+
max_lines: int = 10,
|
|
25
|
+
save_logs: bool = True,
|
|
26
|
+
answers_dir: Optional[str] = None,
|
|
27
|
+
):
|
|
28
|
+
self.display_enabled = display_enabled
|
|
29
|
+
self.max_lines = max_lines
|
|
30
|
+
self.save_logs = save_logs
|
|
31
|
+
self.answers_dir = answers_dir # Path to answers directory from logging system
|
|
32
|
+
self.agent_outputs: Dict[int, str] = {}
|
|
33
|
+
self.agent_models: Dict[int, str] = {}
|
|
34
|
+
self.agent_statuses: Dict[int, str] = {} # Track agent statuses
|
|
35
|
+
self.system_messages: List[str] = []
|
|
36
|
+
self.start_time = time.time()
|
|
37
|
+
self._lock = threading.RLock() # Use reentrant lock to prevent deadlock
|
|
38
|
+
|
|
39
|
+
# MassGen-specific state tracking
|
|
40
|
+
self.current_phase = "collaboration"
|
|
41
|
+
self.vote_distribution: Dict[int, int] = {}
|
|
42
|
+
self.consensus_reached = False
|
|
43
|
+
self.representative_agent_id: Optional[int] = None
|
|
44
|
+
self.debate_rounds: int = 0 # Track debate rounds
|
|
45
|
+
|
|
46
|
+
# Detailed agent state tracking for display
|
|
47
|
+
self._agent_vote_targets: Dict[int, Optional[int]] = {}
|
|
48
|
+
self._agent_chat_rounds: Dict[int, int] = {}
|
|
49
|
+
self._agent_update_counts: Dict[int, int] = {} # Track update history count
|
|
50
|
+
self._agent_votes_cast: Dict[int, int] = (
|
|
51
|
+
{}
|
|
52
|
+
) # Track number of votes cast by each agent
|
|
53
|
+
|
|
54
|
+
# Simplified, consistent border tracking
|
|
55
|
+
self._display_cache = None # Single cache object for all dimensions
|
|
56
|
+
self._last_agent_count = 0 # Track when to invalidate cache
|
|
57
|
+
|
|
58
|
+
# CRITICAL FIX: Debounced display updates to prevent race conditions
|
|
59
|
+
self._update_timer = None
|
|
60
|
+
self._update_delay = 0.1 # 100ms debounce
|
|
61
|
+
self._display_updating = False
|
|
62
|
+
self._pending_update = False
|
|
63
|
+
|
|
64
|
+
# ROBUST DISPLAY: Improved ANSI and Unicode handling
|
|
65
|
+
self._ansi_pattern = re.compile(
|
|
66
|
+
r"\x1B(?:" # ESC
|
|
67
|
+
r"[@-Z\\-_]" # Fe Escape sequences
|
|
68
|
+
r"|"
|
|
69
|
+
r"\["
|
|
70
|
+
r"[0-?]*[ -/]*[@-~]" # CSI sequences
|
|
71
|
+
r"|"
|
|
72
|
+
r"\][^\x07]*(?:\x07|\x1B\\)" # OSC sequences
|
|
73
|
+
r"|"
|
|
74
|
+
r"[PX^_][^\x1B]*\x1B\\" # Other escape sequences
|
|
75
|
+
r")"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Initialize logging directory and files
|
|
79
|
+
if self.save_logs:
|
|
80
|
+
self._setup_logging()
|
|
81
|
+
|
|
82
|
+
def _get_terminal_width(self):
|
|
83
|
+
"""Get terminal width with conservative fallback."""
|
|
84
|
+
try:
|
|
85
|
+
return os.get_terminal_size().columns
|
|
86
|
+
except:
|
|
87
|
+
return 120 # Safe default
|
|
88
|
+
|
|
89
|
+
def _calculate_layout(self, num_agents: int):
|
|
90
|
+
"""
|
|
91
|
+
Calculate all layout dimensions in one place for consistency.
|
|
92
|
+
Returns: (col_width, total_width, terminal_width)
|
|
93
|
+
"""
|
|
94
|
+
# Invalidate cache if agent count changed or no cache exists
|
|
95
|
+
if self._display_cache is None or self._last_agent_count != num_agents:
|
|
96
|
+
|
|
97
|
+
terminal_width = self._get_terminal_width()
|
|
98
|
+
|
|
99
|
+
# More conservative calculation to prevent overflow
|
|
100
|
+
# Each column needs: content + left border (│)
|
|
101
|
+
# Plus one final border (│) at the end
|
|
102
|
+
border_chars = num_agents + 1 # │col1│col2│col3│
|
|
103
|
+
safety_margin = 10 # Increased safety margin for terminal variations
|
|
104
|
+
|
|
105
|
+
available_width = terminal_width - border_chars - safety_margin
|
|
106
|
+
col_width = max(
|
|
107
|
+
25, available_width // num_agents
|
|
108
|
+
) # Minimum 25 chars per column
|
|
109
|
+
|
|
110
|
+
# Calculate actual total width used
|
|
111
|
+
total_width = (col_width * num_agents) + border_chars
|
|
112
|
+
|
|
113
|
+
# Final safety check - ensure we don't exceed terminal
|
|
114
|
+
if total_width > terminal_width - 2: # Extra 2 char safety
|
|
115
|
+
col_width = max(20, (terminal_width - border_chars - 4) // num_agents)
|
|
116
|
+
total_width = (col_width * num_agents) + border_chars
|
|
117
|
+
|
|
118
|
+
# Cache the results
|
|
119
|
+
self._display_cache = {
|
|
120
|
+
"col_width": col_width,
|
|
121
|
+
"total_width": total_width,
|
|
122
|
+
"terminal_width": terminal_width,
|
|
123
|
+
"num_agents": num_agents,
|
|
124
|
+
"border_chars": border_chars,
|
|
125
|
+
}
|
|
126
|
+
self._last_agent_count = num_agents
|
|
127
|
+
|
|
128
|
+
cache = self._display_cache
|
|
129
|
+
return cache["col_width"], cache["total_width"], cache["terminal_width"]
|
|
130
|
+
|
|
131
|
+
def _get_display_width(self, text: str) -> int:
|
|
132
|
+
"""
|
|
133
|
+
ROBUST: Calculate the actual display width of text with proper ANSI and Unicode handling.
|
|
134
|
+
"""
|
|
135
|
+
if not text:
|
|
136
|
+
return 0
|
|
137
|
+
|
|
138
|
+
# Remove ALL ANSI escape sequences using comprehensive regex
|
|
139
|
+
clean_text = self._ansi_pattern.sub("", text)
|
|
140
|
+
|
|
141
|
+
width = 0
|
|
142
|
+
i = 0
|
|
143
|
+
while i < len(clean_text):
|
|
144
|
+
char = clean_text[i]
|
|
145
|
+
char_code = ord(char)
|
|
146
|
+
|
|
147
|
+
# Handle control characters (should not contribute to width)
|
|
148
|
+
if char_code < 32 or char_code == 127: # Control characters
|
|
149
|
+
i += 1
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
# Handle Unicode combining characters (zero-width)
|
|
153
|
+
if unicodedata.combining(char):
|
|
154
|
+
i += 1
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
# Handle emoji and wide characters more comprehensively
|
|
158
|
+
char_width = self._get_char_width(char)
|
|
159
|
+
width += char_width
|
|
160
|
+
i += 1
|
|
161
|
+
|
|
162
|
+
return width
|
|
163
|
+
|
|
164
|
+
def _get_char_width(self, char: str) -> int:
|
|
165
|
+
"""
|
|
166
|
+
ROBUST: Get the display width of a single character.
|
|
167
|
+
"""
|
|
168
|
+
char_code = ord(char)
|
|
169
|
+
|
|
170
|
+
# ASCII printable characters
|
|
171
|
+
if 32 <= char_code <= 126:
|
|
172
|
+
return 1
|
|
173
|
+
|
|
174
|
+
# Common emoji ranges (display as width 2)
|
|
175
|
+
if (
|
|
176
|
+
# Basic emoji ranges
|
|
177
|
+
(0x1F600 <= char_code <= 0x1F64F) # Emoticons
|
|
178
|
+
or (0x1F300 <= char_code <= 0x1F5FF) # Misc symbols
|
|
179
|
+
or (0x1F680 <= char_code <= 0x1F6FF) # Transport
|
|
180
|
+
or (0x1F700 <= char_code <= 0x1F77F) # Alchemical symbols
|
|
181
|
+
or (0x1F780 <= char_code <= 0x1F7FF) # Geometric shapes extended
|
|
182
|
+
or (0x1F800 <= char_code <= 0x1F8FF) # Supplemental arrows-C
|
|
183
|
+
or (0x1F900 <= char_code <= 0x1F9FF) # Supplemental symbols
|
|
184
|
+
or (0x1FA00 <= char_code <= 0x1FA6F) # Chess symbols
|
|
185
|
+
or (0x1FA70 <= char_code <= 0x1FAFF) # Symbols and pictographs extended-A
|
|
186
|
+
or (0x1F1E6 <= char_code <= 0x1F1FF) # Regional indicator symbols (flags)
|
|
187
|
+
or
|
|
188
|
+
# Misc symbols and dingbats
|
|
189
|
+
(0x2600 <= char_code <= 0x26FF) # Misc symbols
|
|
190
|
+
or (0x2700 <= char_code <= 0x27BF) # Dingbats
|
|
191
|
+
or (0x1F0A0 <= char_code <= 0x1F0FF) # Playing cards
|
|
192
|
+
or
|
|
193
|
+
# Mathematical symbols
|
|
194
|
+
(0x1F100 <= char_code <= 0x1F1FF) # Enclosed alphanumeric supplement
|
|
195
|
+
):
|
|
196
|
+
return 2
|
|
197
|
+
|
|
198
|
+
# Use Unicode East Asian Width property for CJK characters
|
|
199
|
+
east_asian_width = unicodedata.east_asian_width(char)
|
|
200
|
+
if east_asian_width in ("F", "W"): # Fullwidth or Wide
|
|
201
|
+
return 2
|
|
202
|
+
elif east_asian_width in ("N", "Na", "H"): # Narrow, Not assigned, Halfwidth
|
|
203
|
+
return 1
|
|
204
|
+
elif east_asian_width == "A": # Ambiguous - default to 1 for safety
|
|
205
|
+
return 1
|
|
206
|
+
|
|
207
|
+
# Default to 1 for unknown characters
|
|
208
|
+
return 1
|
|
209
|
+
|
|
210
|
+
def _preserve_ansi_truncate(self, text: str, max_width: int) -> str:
|
|
211
|
+
"""
|
|
212
|
+
ROBUST: Truncate text while preserving ANSI color codes and handling wide characters.
|
|
213
|
+
"""
|
|
214
|
+
if max_width <= 0:
|
|
215
|
+
return ""
|
|
216
|
+
|
|
217
|
+
if max_width <= 1:
|
|
218
|
+
return "…"
|
|
219
|
+
|
|
220
|
+
# Split text into ANSI codes and regular text segments
|
|
221
|
+
segments = self._ansi_pattern.split(text)
|
|
222
|
+
ansi_codes = self._ansi_pattern.findall(text)
|
|
223
|
+
|
|
224
|
+
result = ""
|
|
225
|
+
current_width = 0
|
|
226
|
+
ansi_index = 0
|
|
227
|
+
|
|
228
|
+
for i, segment in enumerate(segments):
|
|
229
|
+
# Add ANSI code if this isn't the first segment
|
|
230
|
+
if i > 0 and ansi_index < len(ansi_codes):
|
|
231
|
+
result += ansi_codes[ansi_index]
|
|
232
|
+
ansi_index += 1
|
|
233
|
+
|
|
234
|
+
# Process regular text segment
|
|
235
|
+
for char in segment:
|
|
236
|
+
char_width = self._get_char_width(char)
|
|
237
|
+
|
|
238
|
+
# Check if we can fit this character
|
|
239
|
+
if (
|
|
240
|
+
current_width + char_width > max_width - 1
|
|
241
|
+
): # Save space for ellipsis
|
|
242
|
+
# Try to add ellipsis if possible
|
|
243
|
+
if current_width < max_width:
|
|
244
|
+
result += "…"
|
|
245
|
+
return result
|
|
246
|
+
|
|
247
|
+
result += char
|
|
248
|
+
current_width += char_width
|
|
249
|
+
|
|
250
|
+
return result
|
|
251
|
+
|
|
252
|
+
def _pad_to_width(self, text: str, target_width: int, align: str = "left") -> str:
|
|
253
|
+
"""
|
|
254
|
+
ROBUST: Pad text to exact target width with proper ANSI and Unicode handling.
|
|
255
|
+
"""
|
|
256
|
+
if target_width <= 0:
|
|
257
|
+
return ""
|
|
258
|
+
|
|
259
|
+
current_width = self._get_display_width(text)
|
|
260
|
+
|
|
261
|
+
# Truncate if too long
|
|
262
|
+
if current_width > target_width:
|
|
263
|
+
text = self._preserve_ansi_truncate(text, target_width)
|
|
264
|
+
current_width = self._get_display_width(text)
|
|
265
|
+
|
|
266
|
+
# Calculate padding needed
|
|
267
|
+
padding = target_width - current_width
|
|
268
|
+
if padding <= 0:
|
|
269
|
+
return text
|
|
270
|
+
|
|
271
|
+
# Apply padding based on alignment
|
|
272
|
+
if align == "center":
|
|
273
|
+
left_pad = padding // 2
|
|
274
|
+
right_pad = padding - left_pad
|
|
275
|
+
return " " * left_pad + text + " " * right_pad
|
|
276
|
+
elif align == "right":
|
|
277
|
+
return " " * padding + text
|
|
278
|
+
else: # left
|
|
279
|
+
return text + " " * padding
|
|
280
|
+
|
|
281
|
+
def _create_bordered_line(self, content_parts: List[str], total_width: int) -> str:
|
|
282
|
+
"""
|
|
283
|
+
ROBUST: Create a single bordered line with guaranteed correct width.
|
|
284
|
+
"""
|
|
285
|
+
# Ensure all content parts are exactly the right width
|
|
286
|
+
validated_parts = []
|
|
287
|
+
for part in content_parts:
|
|
288
|
+
if self._get_display_width(part) != self._display_cache["col_width"]:
|
|
289
|
+
# Re-pad if width is incorrect
|
|
290
|
+
part = self._pad_to_width(
|
|
291
|
+
part, self._display_cache["col_width"], "left"
|
|
292
|
+
)
|
|
293
|
+
validated_parts.append(part)
|
|
294
|
+
|
|
295
|
+
# Join content with borders: │content1│content2│content3│
|
|
296
|
+
line = "│" + "│".join(validated_parts) + "│"
|
|
297
|
+
|
|
298
|
+
# Final width validation
|
|
299
|
+
actual_width = self._get_display_width(line)
|
|
300
|
+
expected_width = total_width
|
|
301
|
+
|
|
302
|
+
if actual_width != expected_width:
|
|
303
|
+
# Emergency fix - truncate or pad the entire line
|
|
304
|
+
if actual_width > expected_width:
|
|
305
|
+
# Strip ANSI codes and truncate
|
|
306
|
+
clean_line = self._ansi_pattern.sub("", line)
|
|
307
|
+
if len(clean_line) > expected_width:
|
|
308
|
+
clean_line = clean_line[: expected_width - 1] + "│"
|
|
309
|
+
line = clean_line
|
|
310
|
+
else:
|
|
311
|
+
# Pad to reach exact width
|
|
312
|
+
line += " " * (expected_width - actual_width)
|
|
313
|
+
|
|
314
|
+
return line
|
|
315
|
+
|
|
316
|
+
def _create_system_bordered_line(self, content: str, total_width: int) -> str:
|
|
317
|
+
"""
|
|
318
|
+
ROBUST: Create a system section line with borders.
|
|
319
|
+
"""
|
|
320
|
+
content_width = total_width - 2 # Account for │ on each side
|
|
321
|
+
if content_width <= 0:
|
|
322
|
+
return "│" + " " * max(0, total_width - 2) + "│"
|
|
323
|
+
|
|
324
|
+
padded_content = self._pad_to_width(content, content_width, "left")
|
|
325
|
+
line = f"│{padded_content}│"
|
|
326
|
+
|
|
327
|
+
# Validate final width
|
|
328
|
+
actual_width = self._get_display_width(line)
|
|
329
|
+
if actual_width != total_width:
|
|
330
|
+
# Emergency padding
|
|
331
|
+
if actual_width < total_width:
|
|
332
|
+
line += " " * (total_width - actual_width)
|
|
333
|
+
elif actual_width > total_width:
|
|
334
|
+
# Strip ANSI and truncate
|
|
335
|
+
clean_line = self._ansi_pattern.sub("", line)
|
|
336
|
+
if len(clean_line) > total_width:
|
|
337
|
+
clean_line = clean_line[: total_width - 1] + "│"
|
|
338
|
+
line = clean_line
|
|
339
|
+
|
|
340
|
+
return line
|
|
341
|
+
|
|
342
|
+
def _invalidate_display_cache(self):
|
|
343
|
+
"""Reset display cache when terminal is resized."""
|
|
344
|
+
self._display_cache = None
|
|
345
|
+
|
|
346
|
+
def cleanup(self):
|
|
347
|
+
"""Clean up resources when display is no longer needed."""
|
|
348
|
+
with self._lock:
|
|
349
|
+
if self._update_timer:
|
|
350
|
+
self._update_timer.cancel()
|
|
351
|
+
self._update_timer = None
|
|
352
|
+
self._pending_update = False
|
|
353
|
+
self._display_updating = False
|
|
354
|
+
|
|
355
|
+
def _clear_terminal_atomic(self):
|
|
356
|
+
"""Atomically clear terminal using proper ANSI sequences."""
|
|
357
|
+
try:
|
|
358
|
+
# Use ANSI escape sequences for atomic terminal clearing
|
|
359
|
+
# This is more reliable than os.system('clear')
|
|
360
|
+
sys.stdout.write("\033[2J") # Clear entire screen
|
|
361
|
+
sys.stdout.write("\033[H") # Move cursor to home position
|
|
362
|
+
sys.stdout.flush() # Ensure immediate execution
|
|
363
|
+
except Exception:
|
|
364
|
+
# Fallback to os.system if ANSI sequences fail
|
|
365
|
+
try:
|
|
366
|
+
os.system("clear" if os.name == "posix" else "cls")
|
|
367
|
+
except Exception:
|
|
368
|
+
pass # Silent fallback if all clearing methods fail
|
|
369
|
+
|
|
370
|
+
def _schedule_display_update(self):
|
|
371
|
+
"""Schedule a debounced display update to prevent rapid refreshes."""
|
|
372
|
+
with self._lock:
|
|
373
|
+
if self._update_timer:
|
|
374
|
+
self._update_timer.cancel()
|
|
375
|
+
|
|
376
|
+
# Set pending update flag
|
|
377
|
+
self._pending_update = True
|
|
378
|
+
|
|
379
|
+
# Schedule update after delay
|
|
380
|
+
self._update_timer = threading.Timer(
|
|
381
|
+
self._update_delay, self._execute_display_update
|
|
382
|
+
)
|
|
383
|
+
self._update_timer.start()
|
|
384
|
+
|
|
385
|
+
def _execute_display_update(self):
|
|
386
|
+
"""Execute the actual display update."""
|
|
387
|
+
with self._lock:
|
|
388
|
+
if not self._pending_update:
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
# Prevent concurrent updates
|
|
392
|
+
if self._display_updating:
|
|
393
|
+
# Reschedule if another update is in progress
|
|
394
|
+
self._update_timer = threading.Timer(
|
|
395
|
+
self._update_delay, self._execute_display_update
|
|
396
|
+
)
|
|
397
|
+
self._update_timer.start()
|
|
398
|
+
return
|
|
399
|
+
|
|
400
|
+
self._display_updating = True
|
|
401
|
+
self._pending_update = False
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
self._update_display_immediate()
|
|
405
|
+
finally:
|
|
406
|
+
with self._lock:
|
|
407
|
+
self._display_updating = False
|
|
408
|
+
|
|
409
|
+
def set_agent_model(self, agent_id: int, model_name: str):
|
|
410
|
+
"""Set the model name for a specific agent."""
|
|
411
|
+
with self._lock:
|
|
412
|
+
self.agent_models[agent_id] = model_name
|
|
413
|
+
# Ensure agent appears in display even if no content yet
|
|
414
|
+
if agent_id not in self.agent_outputs:
|
|
415
|
+
self.agent_outputs[agent_id] = ""
|
|
416
|
+
|
|
417
|
+
def update_agent_status(self, agent_id: int, status: str):
|
|
418
|
+
"""Update agent status (working, voted, failed)."""
|
|
419
|
+
with self._lock:
|
|
420
|
+
old_status = self.agent_statuses.get(agent_id, "unknown")
|
|
421
|
+
self.agent_statuses[agent_id] = status
|
|
422
|
+
|
|
423
|
+
# Ensure agent appears in display even if no content yet
|
|
424
|
+
if agent_id not in self.agent_outputs:
|
|
425
|
+
self.agent_outputs[agent_id] = ""
|
|
426
|
+
|
|
427
|
+
# Status emoji mapping for system messages
|
|
428
|
+
status_change_emoji = {
|
|
429
|
+
"working": "🔄",
|
|
430
|
+
"voted": "✅",
|
|
431
|
+
"failed": "❌",
|
|
432
|
+
"unknown": "❓",
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
# Log status change with emoji
|
|
436
|
+
old_emoji = status_change_emoji.get(old_status, "❓")
|
|
437
|
+
new_emoji = status_change_emoji.get(status, "❓")
|
|
438
|
+
status_msg = (
|
|
439
|
+
f"{old_emoji}→{new_emoji} Agent {agent_id}: {old_status} → {status}"
|
|
440
|
+
)
|
|
441
|
+
self.add_system_message(status_msg)
|
|
442
|
+
|
|
443
|
+
def update_phase(self, old_phase: str, new_phase: str):
|
|
444
|
+
"""Update system phase."""
|
|
445
|
+
with self._lock:
|
|
446
|
+
self.current_phase = new_phase
|
|
447
|
+
phase_msg = f"Phase: {old_phase} → {new_phase}"
|
|
448
|
+
self.add_system_message(phase_msg)
|
|
449
|
+
|
|
450
|
+
def update_vote_distribution(self, vote_dist: Dict[int, int]):
|
|
451
|
+
"""Update vote distribution."""
|
|
452
|
+
with self._lock:
|
|
453
|
+
self.vote_distribution = vote_dist.copy()
|
|
454
|
+
|
|
455
|
+
def update_consensus_status(
|
|
456
|
+
self, representative_id: int, vote_dist: Dict[int, int]
|
|
457
|
+
):
|
|
458
|
+
"""Update when consensus is reached."""
|
|
459
|
+
with self._lock:
|
|
460
|
+
self.consensus_reached = True
|
|
461
|
+
self.representative_agent_id = representative_id
|
|
462
|
+
self.vote_distribution = vote_dist.copy()
|
|
463
|
+
|
|
464
|
+
consensus_msg = f"🎉 CONSENSUS REACHED! Agent {representative_id} selected as representative"
|
|
465
|
+
self.add_system_message(consensus_msg)
|
|
466
|
+
|
|
467
|
+
def reset_consensus(self):
|
|
468
|
+
"""Reset consensus state for new debate round."""
|
|
469
|
+
with self._lock:
|
|
470
|
+
self.consensus_reached = False
|
|
471
|
+
self.representative_agent_id = None
|
|
472
|
+
self.vote_distribution.clear()
|
|
473
|
+
|
|
474
|
+
def update_agent_vote_target(self, agent_id: int, target_id: Optional[int]):
|
|
475
|
+
"""Update which agent this agent voted for."""
|
|
476
|
+
with self._lock:
|
|
477
|
+
self._agent_vote_targets[agent_id] = target_id
|
|
478
|
+
|
|
479
|
+
def update_agent_chat_round(self, agent_id: int, round_num: int):
|
|
480
|
+
"""Update the chat round for an agent."""
|
|
481
|
+
with self._lock:
|
|
482
|
+
self._agent_chat_rounds[agent_id] = round_num
|
|
483
|
+
|
|
484
|
+
def update_agent_update_count(self, agent_id: int, count: int):
|
|
485
|
+
"""Update the update count for an agent."""
|
|
486
|
+
with self._lock:
|
|
487
|
+
self._agent_update_counts[agent_id] = count
|
|
488
|
+
|
|
489
|
+
def update_agent_votes_cast(self, agent_id: int, votes_cast: int):
|
|
490
|
+
"""Update the number of votes cast by an agent."""
|
|
491
|
+
with self._lock:
|
|
492
|
+
self._agent_votes_cast[agent_id] = votes_cast
|
|
493
|
+
|
|
494
|
+
def update_debate_rounds(self, rounds: int):
|
|
495
|
+
"""Update the debate rounds count."""
|
|
496
|
+
with self._lock:
|
|
497
|
+
self.debate_rounds = rounds
|
|
498
|
+
|
|
499
|
+
def _setup_logging(self):
|
|
500
|
+
"""Set up the logging directory and initialize log files."""
|
|
501
|
+
# Create logs directory if it doesn't exist
|
|
502
|
+
base_logs_dir = "logs"
|
|
503
|
+
os.makedirs(base_logs_dir, exist_ok=True)
|
|
504
|
+
|
|
505
|
+
# Create timestamped subdirectory for this session
|
|
506
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
507
|
+
self.session_logs_dir = os.path.join(base_logs_dir, timestamp, "display")
|
|
508
|
+
os.makedirs(self.session_logs_dir, exist_ok=True)
|
|
509
|
+
|
|
510
|
+
# Initialize log file paths with simple names
|
|
511
|
+
self.agent_log_files = {}
|
|
512
|
+
self.system_log_file = os.path.join(self.session_logs_dir, "system.txt")
|
|
513
|
+
|
|
514
|
+
# Initialize system log file
|
|
515
|
+
with open(self.system_log_file, "w", encoding="utf-8") as f:
|
|
516
|
+
f.write(f"MassGen System Messages Log\n")
|
|
517
|
+
f.write(
|
|
518
|
+
f"Session started: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
|
519
|
+
)
|
|
520
|
+
f.write("=" * 80 + "\n\n")
|
|
521
|
+
|
|
522
|
+
def _get_agent_log_file(self, agent_id: int) -> str:
|
|
523
|
+
"""Get or create the log file path for a specific agent."""
|
|
524
|
+
if agent_id not in self.agent_log_files:
|
|
525
|
+
# Use simple filename: agent_0.txt, agent_1.txt, etc.
|
|
526
|
+
self.agent_log_files[agent_id] = os.path.join(
|
|
527
|
+
self.session_logs_dir, f"agent_{agent_id}.txt"
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
# Initialize agent log file
|
|
531
|
+
with open(self.agent_log_files[agent_id], "w", encoding="utf-8") as f:
|
|
532
|
+
f.write(f"MassGen Agent {agent_id} Output Log\n")
|
|
533
|
+
f.write(
|
|
534
|
+
f"Session started: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
|
535
|
+
)
|
|
536
|
+
f.write("=" * 80 + "\n\n")
|
|
537
|
+
|
|
538
|
+
return self.agent_log_files[agent_id]
|
|
539
|
+
|
|
540
|
+
def get_agent_log_path_for_display(self, agent_id: int) -> str:
|
|
541
|
+
"""Get the log file path for display purposes (clickable link)."""
|
|
542
|
+
if not self.save_logs:
|
|
543
|
+
return ""
|
|
544
|
+
|
|
545
|
+
# Ensure the log file exists by calling _get_agent_log_file
|
|
546
|
+
log_path = self._get_agent_log_file(agent_id)
|
|
547
|
+
|
|
548
|
+
# Return relative path for better display
|
|
549
|
+
return log_path
|
|
550
|
+
|
|
551
|
+
def get_agent_answer_path_for_display(self, agent_id: int) -> str:
|
|
552
|
+
"""Get the answer file path for display purposes (clickable link)."""
|
|
553
|
+
if not self.save_logs or not self.answers_dir:
|
|
554
|
+
return ""
|
|
555
|
+
|
|
556
|
+
# Construct answer file path using the answers directory
|
|
557
|
+
answer_file_path = os.path.join(self.answers_dir, f"agent_{agent_id}.txt")
|
|
558
|
+
|
|
559
|
+
# Return relative path for better display
|
|
560
|
+
return answer_file_path
|
|
561
|
+
|
|
562
|
+
def get_system_log_path_for_display(self) -> str:
|
|
563
|
+
"""Get the system log file path for display purposes (clickable link)."""
|
|
564
|
+
if not self.save_logs:
|
|
565
|
+
return ""
|
|
566
|
+
|
|
567
|
+
return self.system_log_file
|
|
568
|
+
|
|
569
|
+
def _write_agent_log(self, agent_id: int, content: str):
|
|
570
|
+
"""Write content to the agent's log file."""
|
|
571
|
+
if not self.save_logs:
|
|
572
|
+
return
|
|
573
|
+
|
|
574
|
+
try:
|
|
575
|
+
log_file = self._get_agent_log_file(agent_id)
|
|
576
|
+
with open(log_file, "a", encoding="utf-8") as f:
|
|
577
|
+
f.write(content)
|
|
578
|
+
f.flush() # Ensure immediate write
|
|
579
|
+
except Exception as e:
|
|
580
|
+
print(f"Error writing to agent {agent_id} log: {e}")
|
|
581
|
+
|
|
582
|
+
def _write_system_log(self, message: str):
|
|
583
|
+
"""Write a system message to the system log file."""
|
|
584
|
+
if not self.save_logs:
|
|
585
|
+
return
|
|
586
|
+
|
|
587
|
+
try:
|
|
588
|
+
with open(self.system_log_file, "a", encoding="utf-8") as f:
|
|
589
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
590
|
+
f.write(f"[{timestamp}] {message}\n")
|
|
591
|
+
f.flush() # Ensure immediate write
|
|
592
|
+
except Exception as e:
|
|
593
|
+
print(f"Error writing to system log: {e}")
|
|
594
|
+
|
|
595
|
+
def stream_output_sync(self, agent_id: int, content: str):
|
|
596
|
+
"""FIXED: Buffered streaming with debounced display updates."""
|
|
597
|
+
if not self.display_enabled:
|
|
598
|
+
return
|
|
599
|
+
|
|
600
|
+
with self._lock:
|
|
601
|
+
if agent_id not in self.agent_outputs:
|
|
602
|
+
self.agent_outputs[agent_id] = ""
|
|
603
|
+
|
|
604
|
+
# Handle special content markers for display vs logging
|
|
605
|
+
display_content = content
|
|
606
|
+
log_content = content
|
|
607
|
+
|
|
608
|
+
# Check for special markers (keep text markers for backward compatibility)
|
|
609
|
+
if content.startswith("[CODE_DISPLAY_ONLY]"):
|
|
610
|
+
# This content should only be shown in display, not logged
|
|
611
|
+
display_content = content[len("[CODE_DISPLAY_ONLY]") :]
|
|
612
|
+
log_content = "" # Don't log this content
|
|
613
|
+
elif content.startswith("[CODE_LOG_ONLY]"):
|
|
614
|
+
# This content should only be logged, not displayed
|
|
615
|
+
display_content = "" # Don't display this content
|
|
616
|
+
log_content = content[len("[CODE_LOG_ONLY]") :]
|
|
617
|
+
|
|
618
|
+
# Add to display output only if there's display content
|
|
619
|
+
if display_content:
|
|
620
|
+
self.agent_outputs[agent_id] += display_content
|
|
621
|
+
|
|
622
|
+
# Write to log file only if there's log content
|
|
623
|
+
if log_content:
|
|
624
|
+
self._write_agent_log(agent_id, log_content)
|
|
625
|
+
|
|
626
|
+
# CRITICAL FIX: Use debounced updates instead of immediate updates
|
|
627
|
+
if display_content:
|
|
628
|
+
self._schedule_display_update()
|
|
629
|
+
|
|
630
|
+
def _handle_terminal_resize(self):
|
|
631
|
+
"""Handle terminal resize by resetting cached dimensions."""
|
|
632
|
+
try:
|
|
633
|
+
current_width = os.get_terminal_size().columns
|
|
634
|
+
if (
|
|
635
|
+
self._display_cache
|
|
636
|
+
and abs(current_width - self._display_cache["terminal_width"]) > 2
|
|
637
|
+
):
|
|
638
|
+
# Even small changes should invalidate cache for border alignment
|
|
639
|
+
self._invalidate_display_cache()
|
|
640
|
+
return True
|
|
641
|
+
except:
|
|
642
|
+
# If we can't detect terminal size, invalidate cache to be safe
|
|
643
|
+
self._invalidate_display_cache()
|
|
644
|
+
return True
|
|
645
|
+
return False
|
|
646
|
+
|
|
647
|
+
def add_system_message(self, message: str):
|
|
648
|
+
"""Add a system message with timestamp."""
|
|
649
|
+
with self._lock:
|
|
650
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
651
|
+
formatted_message = f"[{timestamp}] {message}"
|
|
652
|
+
self.system_messages.append(formatted_message)
|
|
653
|
+
|
|
654
|
+
# Keep only recent messages
|
|
655
|
+
if len(self.system_messages) > 20:
|
|
656
|
+
self.system_messages = self.system_messages[-20:]
|
|
657
|
+
|
|
658
|
+
# Write to system log
|
|
659
|
+
self._write_system_log(formatted_message + "\n")
|
|
660
|
+
|
|
661
|
+
def format_agent_notification(
|
|
662
|
+
self, agent_id: int, notification_type: str, content: str
|
|
663
|
+
):
|
|
664
|
+
"""Format agent notifications for display."""
|
|
665
|
+
notification_emoji = {
|
|
666
|
+
"update": "📢",
|
|
667
|
+
"debate": "🗣️",
|
|
668
|
+
"presentation": "🎯",
|
|
669
|
+
"prompt": "💡",
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
emoji = notification_emoji.get(notification_type, "📨")
|
|
673
|
+
notification_msg = (
|
|
674
|
+
f"{emoji} Agent {agent_id} received {notification_type} notification"
|
|
675
|
+
)
|
|
676
|
+
self.add_system_message(notification_msg)
|
|
677
|
+
|
|
678
|
+
def _update_display_immediate(self):
|
|
679
|
+
"""Immediate display update - called by the debounced scheduler."""
|
|
680
|
+
if not self.display_enabled:
|
|
681
|
+
return
|
|
682
|
+
|
|
683
|
+
try:
|
|
684
|
+
# Handle potential terminal resize
|
|
685
|
+
self._handle_terminal_resize()
|
|
686
|
+
|
|
687
|
+
# Use atomic terminal clearing
|
|
688
|
+
self._clear_terminal_atomic()
|
|
689
|
+
|
|
690
|
+
# Get sorted agent IDs for consistent ordering
|
|
691
|
+
agent_ids = sorted(self.agent_outputs.keys())
|
|
692
|
+
if not agent_ids:
|
|
693
|
+
return
|
|
694
|
+
|
|
695
|
+
# Get terminal dimensions and calculate display dimensions
|
|
696
|
+
num_agents = len(agent_ids)
|
|
697
|
+
col_width, total_width, terminal_width = self._calculate_layout(num_agents)
|
|
698
|
+
except Exception as e:
|
|
699
|
+
# Fallback to simple text output if display fails
|
|
700
|
+
print(f"Display error: {e}")
|
|
701
|
+
for agent_id in sorted(self.agent_outputs.keys()):
|
|
702
|
+
print(
|
|
703
|
+
f"Agent {agent_id}: {self.agent_outputs[agent_id][-100:]}"
|
|
704
|
+
) # Last 100 chars
|
|
705
|
+
return
|
|
706
|
+
|
|
707
|
+
# Split content into lines for each agent and limit to max_lines
|
|
708
|
+
agent_lines = {}
|
|
709
|
+
max_lines = 0
|
|
710
|
+
for agent_id in agent_ids:
|
|
711
|
+
lines = self.agent_outputs[agent_id].split("\n")
|
|
712
|
+
# Keep only the last max_lines lines (tail behavior)
|
|
713
|
+
if len(lines) > self.max_lines:
|
|
714
|
+
lines = lines[-self.max_lines :]
|
|
715
|
+
agent_lines[agent_id] = lines
|
|
716
|
+
max_lines = max(max_lines, len(lines))
|
|
717
|
+
|
|
718
|
+
# Create horizontal border line - use the locked width
|
|
719
|
+
border_line = "─" * total_width
|
|
720
|
+
|
|
721
|
+
# Enhanced MassGen system header with fixed width
|
|
722
|
+
print("")
|
|
723
|
+
|
|
724
|
+
# ANSI color codes
|
|
725
|
+
BRIGHT_CYAN = "\033[96m"
|
|
726
|
+
BRIGHT_BLUE = "\033[94m"
|
|
727
|
+
BRIGHT_GREEN = "\033[92m"
|
|
728
|
+
BRIGHT_YELLOW = "\033[93m"
|
|
729
|
+
BRIGHT_MAGENTA = "\033[95m"
|
|
730
|
+
BRIGHT_RED = "\033[91m"
|
|
731
|
+
BRIGHT_WHITE = "\033[97m"
|
|
732
|
+
BOLD = "\033[1m"
|
|
733
|
+
RESET = "\033[0m"
|
|
734
|
+
|
|
735
|
+
# Header with exact width
|
|
736
|
+
header_top = f"{BRIGHT_CYAN}{BOLD}╔{'═' * (total_width - 2)}╗{RESET}"
|
|
737
|
+
print(header_top)
|
|
738
|
+
|
|
739
|
+
# Empty line
|
|
740
|
+
header_empty = f"{BRIGHT_CYAN}║{' ' * (total_width - 2)}║{RESET}"
|
|
741
|
+
print(header_empty)
|
|
742
|
+
|
|
743
|
+
# Title line with exact centering
|
|
744
|
+
title_text = "🚀 MassGen - Multi-Agent Scaling System 🚀"
|
|
745
|
+
title_line_content = self._pad_to_width(title_text, total_width - 2, "center")
|
|
746
|
+
title_line = f"{BRIGHT_CYAN}║{BRIGHT_YELLOW}{BOLD}{title_line_content}{RESET}{BRIGHT_CYAN}║{RESET}"
|
|
747
|
+
print(title_line)
|
|
748
|
+
|
|
749
|
+
# Subtitle line
|
|
750
|
+
subtitle_text = "🔬 Advanced Agent Collaboration Framework"
|
|
751
|
+
subtitle_line_content = self._pad_to_width(
|
|
752
|
+
subtitle_text, total_width - 2, "center"
|
|
753
|
+
)
|
|
754
|
+
subtitle_line = f"{BRIGHT_CYAN}║{BRIGHT_GREEN}{subtitle_line_content}{RESET}{BRIGHT_CYAN}║{RESET}"
|
|
755
|
+
print(subtitle_line)
|
|
756
|
+
|
|
757
|
+
# Empty line and bottom border
|
|
758
|
+
print(header_empty)
|
|
759
|
+
header_bottom = f"{BRIGHT_CYAN}{BOLD}╚{'═' * (total_width - 2)}╝{RESET}"
|
|
760
|
+
print(header_bottom)
|
|
761
|
+
|
|
762
|
+
# Agent section with perfect alignment
|
|
763
|
+
print(f"\n{border_line}")
|
|
764
|
+
|
|
765
|
+
# Agent headers with exact column widths
|
|
766
|
+
header_parts = []
|
|
767
|
+
for agent_id in agent_ids:
|
|
768
|
+
model_name = self.agent_models.get(agent_id, "")
|
|
769
|
+
status = self.agent_statuses.get(agent_id, "unknown")
|
|
770
|
+
|
|
771
|
+
# Status configuration
|
|
772
|
+
status_config = {
|
|
773
|
+
"working": {"emoji": "🔄", "color": BRIGHT_YELLOW},
|
|
774
|
+
"voted": {"emoji": "✅", "color": BRIGHT_GREEN},
|
|
775
|
+
"failed": {"emoji": "❌", "color": BRIGHT_RED},
|
|
776
|
+
"unknown": {"emoji": "❓", "color": BRIGHT_WHITE},
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
config = status_config.get(status, status_config["unknown"])
|
|
780
|
+
emoji = config["emoji"]
|
|
781
|
+
status_color = config["color"]
|
|
782
|
+
|
|
783
|
+
# Create agent header with exact width
|
|
784
|
+
if model_name:
|
|
785
|
+
agent_header = f"{emoji} {BRIGHT_CYAN}Agent {agent_id}{RESET} {BRIGHT_MAGENTA}({model_name}){RESET} {status_color}[{status}]{RESET}"
|
|
786
|
+
else:
|
|
787
|
+
agent_header = f"{emoji} {BRIGHT_CYAN}Agent {agent_id}{RESET} {status_color}[{status}]{RESET}"
|
|
788
|
+
|
|
789
|
+
header_content = self._pad_to_width(agent_header, col_width, "center")
|
|
790
|
+
# Validate width immediately
|
|
791
|
+
if self._get_display_width(header_content) != col_width:
|
|
792
|
+
# Fallback to simple text if formatting issues
|
|
793
|
+
simple_header = f"Agent {agent_id} [{status}]"
|
|
794
|
+
header_content = self._pad_to_width(simple_header, col_width, "center")
|
|
795
|
+
header_parts.append(header_content)
|
|
796
|
+
|
|
797
|
+
# Print agent header line with exact borders
|
|
798
|
+
try:
|
|
799
|
+
header_line = self._create_bordered_line(header_parts, total_width)
|
|
800
|
+
print(header_line)
|
|
801
|
+
except Exception as e:
|
|
802
|
+
# Fallback to simple border if formatting fails
|
|
803
|
+
print("─" * total_width)
|
|
804
|
+
|
|
805
|
+
# Agent state information line
|
|
806
|
+
state_parts = []
|
|
807
|
+
for agent_id in agent_ids:
|
|
808
|
+
chat_round = getattr(self, "_agent_chat_rounds", {}).get(agent_id, 0)
|
|
809
|
+
vote_target = getattr(self, "_agent_vote_targets", {}).get(agent_id)
|
|
810
|
+
update_count = getattr(self, "_agent_update_counts", {}).get(agent_id, 0)
|
|
811
|
+
votes_cast = getattr(self, "_agent_votes_cast", {}).get(agent_id, 0)
|
|
812
|
+
|
|
813
|
+
# Format state info with better handling of color codes (removed redundant status)
|
|
814
|
+
state_info = []
|
|
815
|
+
state_info.append(
|
|
816
|
+
f"{BRIGHT_WHITE}Round:{RESET} {BRIGHT_GREEN}{chat_round}{RESET}"
|
|
817
|
+
)
|
|
818
|
+
state_info.append(
|
|
819
|
+
f"{BRIGHT_WHITE}#Updates:{RESET} {BRIGHT_MAGENTA}{update_count}{RESET}"
|
|
820
|
+
)
|
|
821
|
+
state_info.append(
|
|
822
|
+
f"{BRIGHT_WHITE}#Votes:{RESET} {BRIGHT_CYAN}{votes_cast}{RESET}"
|
|
823
|
+
)
|
|
824
|
+
if vote_target:
|
|
825
|
+
state_info.append(
|
|
826
|
+
f"{BRIGHT_WHITE}Vote →{RESET} {BRIGHT_GREEN}{vote_target}{RESET}"
|
|
827
|
+
)
|
|
828
|
+
else:
|
|
829
|
+
state_info.append(f"{BRIGHT_WHITE}Vote →{RESET} None")
|
|
830
|
+
|
|
831
|
+
state_text = f"📊 {' | '.join(state_info)}"
|
|
832
|
+
# Ensure exact column width with improved padding
|
|
833
|
+
state_content = self._pad_to_width(state_text, col_width, "center")
|
|
834
|
+
state_parts.append(state_content)
|
|
835
|
+
|
|
836
|
+
# Validate state line consistency before printing
|
|
837
|
+
try:
|
|
838
|
+
state_line = self._create_bordered_line(state_parts, total_width)
|
|
839
|
+
print(state_line)
|
|
840
|
+
except Exception as e:
|
|
841
|
+
# Fallback to simple border if formatting fails
|
|
842
|
+
print("─" * total_width)
|
|
843
|
+
|
|
844
|
+
# Answer file information
|
|
845
|
+
if self.save_logs and (hasattr(self, "session_logs_dir") or self.answers_dir):
|
|
846
|
+
UNDERLINE = "\033[4m"
|
|
847
|
+
link_parts = []
|
|
848
|
+
for agent_id in agent_ids:
|
|
849
|
+
# Try to get answer file path first, fallback to log file path
|
|
850
|
+
answer_path = self.get_agent_answer_path_for_display(agent_id)
|
|
851
|
+
if answer_path:
|
|
852
|
+
# Shortened display path
|
|
853
|
+
display_path = (
|
|
854
|
+
answer_path.replace(os.getcwd() + "/", "")
|
|
855
|
+
if answer_path.startswith(os.getcwd())
|
|
856
|
+
else answer_path
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
# Safe path truncation with better width handling
|
|
860
|
+
prefix = "📄 Answers: "
|
|
861
|
+
# More conservative calculation
|
|
862
|
+
max_path_len = max(
|
|
863
|
+
10, col_width - self._get_display_width(prefix) - 8
|
|
864
|
+
)
|
|
865
|
+
if len(display_path) > max_path_len:
|
|
866
|
+
display_path = "..." + display_path[-(max_path_len - 3) :]
|
|
867
|
+
|
|
868
|
+
link_text = f"{prefix}{UNDERLINE}{display_path}{RESET}"
|
|
869
|
+
link_content = self._pad_to_width(link_text, col_width, "center")
|
|
870
|
+
else:
|
|
871
|
+
# Fallback to log file path if answer path not available
|
|
872
|
+
log_path = self.get_agent_log_path_for_display(agent_id)
|
|
873
|
+
if log_path:
|
|
874
|
+
display_path = (
|
|
875
|
+
log_path.replace(os.getcwd() + "/", "")
|
|
876
|
+
if log_path.startswith(os.getcwd())
|
|
877
|
+
else log_path
|
|
878
|
+
)
|
|
879
|
+
prefix = "📁 Log: "
|
|
880
|
+
max_path_len = max(
|
|
881
|
+
10, col_width - self._get_display_width(prefix) - 8
|
|
882
|
+
)
|
|
883
|
+
if len(display_path) > max_path_len:
|
|
884
|
+
display_path = "..." + display_path[-(max_path_len - 3) :]
|
|
885
|
+
link_text = f"{prefix}{UNDERLINE}{display_path}{RESET}"
|
|
886
|
+
link_content = self._pad_to_width(
|
|
887
|
+
link_text, col_width, "center"
|
|
888
|
+
)
|
|
889
|
+
else:
|
|
890
|
+
link_content = self._pad_to_width("", col_width, "center")
|
|
891
|
+
link_parts.append(link_content)
|
|
892
|
+
|
|
893
|
+
# Validate log line consistency
|
|
894
|
+
try:
|
|
895
|
+
log_line = self._create_bordered_line(link_parts, total_width)
|
|
896
|
+
print(log_line)
|
|
897
|
+
except Exception as e:
|
|
898
|
+
# Fallback to simple border if formatting fails
|
|
899
|
+
print("─" * total_width)
|
|
900
|
+
|
|
901
|
+
print(border_line)
|
|
902
|
+
|
|
903
|
+
# Content area with perfect column alignment - Apply validation to every content line
|
|
904
|
+
for line_idx in range(max_lines):
|
|
905
|
+
content_parts = []
|
|
906
|
+
for agent_id in agent_ids:
|
|
907
|
+
lines = agent_lines[agent_id]
|
|
908
|
+
content = lines[line_idx] if line_idx < len(lines) else ""
|
|
909
|
+
|
|
910
|
+
# Ensure exact column width for each content piece
|
|
911
|
+
padded_content = self._pad_to_width(content, col_width, "left")
|
|
912
|
+
content_parts.append(padded_content)
|
|
913
|
+
|
|
914
|
+
# Apply border validation to every content line for consistency
|
|
915
|
+
try:
|
|
916
|
+
content_line = self._create_bordered_line(content_parts, total_width)
|
|
917
|
+
print(content_line)
|
|
918
|
+
except Exception as e:
|
|
919
|
+
# Fallback: print content without borders to maintain functionality
|
|
920
|
+
simple_line = " | ".join(content_parts)[: total_width - 4] + " " * max(
|
|
921
|
+
0, total_width - 4 - len(simple_line)
|
|
922
|
+
)
|
|
923
|
+
print(f"│ {simple_line} │")
|
|
924
|
+
|
|
925
|
+
# System status section with exact width
|
|
926
|
+
if self.system_messages or self.current_phase or self.vote_distribution:
|
|
927
|
+
print(f"\n{border_line}")
|
|
928
|
+
|
|
929
|
+
# System state header
|
|
930
|
+
phase_color = (
|
|
931
|
+
BRIGHT_YELLOW if self.current_phase == "collaboration" else BRIGHT_GREEN
|
|
932
|
+
)
|
|
933
|
+
consensus_color = BRIGHT_GREEN if self.consensus_reached else BRIGHT_RED
|
|
934
|
+
consensus_text = "✅ YES" if self.consensus_reached else "❌ NO"
|
|
935
|
+
|
|
936
|
+
system_state_info = []
|
|
937
|
+
system_state_info.append(
|
|
938
|
+
f"{BRIGHT_WHITE}Phase:{RESET} {phase_color}{self.current_phase.upper()}{RESET}"
|
|
939
|
+
)
|
|
940
|
+
system_state_info.append(
|
|
941
|
+
f"{BRIGHT_WHITE}Consensus:{RESET} {consensus_color}{consensus_text}{RESET}"
|
|
942
|
+
)
|
|
943
|
+
system_state_info.append(
|
|
944
|
+
f"{BRIGHT_WHITE}Debate Rounds:{RESET} {BRIGHT_CYAN}{self.debate_rounds}{RESET}"
|
|
945
|
+
)
|
|
946
|
+
if self.representative_agent_id:
|
|
947
|
+
system_state_info.append(
|
|
948
|
+
f"{BRIGHT_WHITE}Representative Agent:{RESET} {BRIGHT_GREEN}{self.representative_agent_id}{RESET}"
|
|
949
|
+
)
|
|
950
|
+
else:
|
|
951
|
+
system_state_info.append(
|
|
952
|
+
f"{BRIGHT_WHITE}Representative Agent:{RESET} None"
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
system_header_text = (
|
|
956
|
+
f"{BRIGHT_CYAN}📋 SYSTEM STATE{RESET} - {' | '.join(system_state_info)}"
|
|
957
|
+
)
|
|
958
|
+
system_header_line = self._create_system_bordered_line(
|
|
959
|
+
system_header_text, total_width
|
|
960
|
+
)
|
|
961
|
+
print(system_header_line)
|
|
962
|
+
|
|
963
|
+
# System log file link
|
|
964
|
+
if self.save_logs and hasattr(self, "system_log_file"):
|
|
965
|
+
system_log_path = self.get_system_log_path_for_display()
|
|
966
|
+
if system_log_path:
|
|
967
|
+
UNDERLINE = "\033[4m"
|
|
968
|
+
display_path = (
|
|
969
|
+
system_log_path.replace(os.getcwd() + "/", "")
|
|
970
|
+
if system_log_path.startswith(os.getcwd())
|
|
971
|
+
else system_log_path
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
# Safe path truncation with consistent width handling
|
|
975
|
+
prefix = "📁 Log: "
|
|
976
|
+
max_path_len = max(
|
|
977
|
+
10, total_width - self._get_display_width(prefix) - 15
|
|
978
|
+
)
|
|
979
|
+
if len(display_path) > max_path_len:
|
|
980
|
+
display_path = "..." + display_path[-(max_path_len - 3) :]
|
|
981
|
+
|
|
982
|
+
system_link_text = f"{prefix}{UNDERLINE}{display_path}{RESET}"
|
|
983
|
+
system_link_line = self._create_system_bordered_line(
|
|
984
|
+
system_link_text, total_width
|
|
985
|
+
)
|
|
986
|
+
print(system_link_line)
|
|
987
|
+
|
|
988
|
+
print(border_line)
|
|
989
|
+
|
|
990
|
+
# System messages with exact width and validation
|
|
991
|
+
if self.consensus_reached and self.representative_agent_id is not None:
|
|
992
|
+
consensus_msg = f"🎉 CONSENSUS REACHED! Representative: Agent {self.representative_agent_id}"
|
|
993
|
+
consensus_line = self._create_system_bordered_line(
|
|
994
|
+
consensus_msg, total_width
|
|
995
|
+
)
|
|
996
|
+
print(consensus_line)
|
|
997
|
+
|
|
998
|
+
# Vote distribution with validation
|
|
999
|
+
if self.vote_distribution:
|
|
1000
|
+
vote_msg = "📊 Vote Distribution: " + ", ".join(
|
|
1001
|
+
[f"Agent {k}→{v} votes" for k, v in self.vote_distribution.items()]
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
# Use the new safe wrapping method
|
|
1005
|
+
max_content_width = total_width - 2
|
|
1006
|
+
if self._get_display_width(vote_msg) <= max_content_width:
|
|
1007
|
+
vote_line = self._create_system_bordered_line(vote_msg, total_width)
|
|
1008
|
+
print(vote_line)
|
|
1009
|
+
else:
|
|
1010
|
+
# Wrap vote distribution using safe method
|
|
1011
|
+
vote_header = "📊 Vote Distribution:"
|
|
1012
|
+
header_line = self._create_system_bordered_line(
|
|
1013
|
+
vote_header, total_width
|
|
1014
|
+
)
|
|
1015
|
+
print(header_line)
|
|
1016
|
+
|
|
1017
|
+
for agent_id, votes in self.vote_distribution.items():
|
|
1018
|
+
vote_detail = f" Agent {agent_id}: {votes} votes"
|
|
1019
|
+
detail_line = self._create_system_bordered_line(
|
|
1020
|
+
vote_detail, total_width
|
|
1021
|
+
)
|
|
1022
|
+
print(detail_line)
|
|
1023
|
+
|
|
1024
|
+
# Regular system messages with validation
|
|
1025
|
+
for message in self.system_messages:
|
|
1026
|
+
# Use consistent width calculation throughout
|
|
1027
|
+
max_content_width = total_width - 2
|
|
1028
|
+
if self._get_display_width(message) <= max_content_width:
|
|
1029
|
+
line = self._create_system_bordered_line(message, total_width)
|
|
1030
|
+
print(line)
|
|
1031
|
+
else:
|
|
1032
|
+
# Simple word wrapping
|
|
1033
|
+
words = message.split()
|
|
1034
|
+
current_line = ""
|
|
1035
|
+
|
|
1036
|
+
for word in words:
|
|
1037
|
+
test_line = f"{current_line} {word}".strip()
|
|
1038
|
+
if self._get_display_width(test_line) > max_content_width:
|
|
1039
|
+
# Print current line if it has content
|
|
1040
|
+
if current_line.strip():
|
|
1041
|
+
line = self._create_system_bordered_line(
|
|
1042
|
+
current_line.strip(), total_width
|
|
1043
|
+
)
|
|
1044
|
+
print(line)
|
|
1045
|
+
current_line = word
|
|
1046
|
+
else:
|
|
1047
|
+
current_line = test_line
|
|
1048
|
+
|
|
1049
|
+
# Print final line if it has content
|
|
1050
|
+
if current_line.strip():
|
|
1051
|
+
line = self._create_system_bordered_line(
|
|
1052
|
+
current_line.strip(), total_width
|
|
1053
|
+
)
|
|
1054
|
+
print(line)
|
|
1055
|
+
|
|
1056
|
+
# Final border
|
|
1057
|
+
print(border_line)
|
|
1058
|
+
|
|
1059
|
+
# Force output to be written immediately
|
|
1060
|
+
sys.stdout.flush()
|
|
1061
|
+
|
|
1062
|
+
def force_update_display(self):
|
|
1063
|
+
"""Force an immediate display update (for status changes)."""
|
|
1064
|
+
with self._lock:
|
|
1065
|
+
if self._update_timer:
|
|
1066
|
+
self._update_timer.cancel()
|
|
1067
|
+
self._pending_update = True
|
|
1068
|
+
self._execute_display_update()
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
class StreamingOrchestrator:
|
|
1072
|
+
def __init__(
|
|
1073
|
+
self,
|
|
1074
|
+
display_enabled: bool = True,
|
|
1075
|
+
stream_callback: Optional[Callable] = None,
|
|
1076
|
+
max_lines: int = 10,
|
|
1077
|
+
save_logs: bool = True,
|
|
1078
|
+
answers_dir: Optional[str] = None,
|
|
1079
|
+
):
|
|
1080
|
+
self.display = MultiRegionDisplay(
|
|
1081
|
+
display_enabled, max_lines, save_logs, answers_dir
|
|
1082
|
+
)
|
|
1083
|
+
self.stream_callback = stream_callback
|
|
1084
|
+
|
|
1085
|
+
def stream_output(self, agent_id: int, content: str):
|
|
1086
|
+
"""Streaming content - uses debounced updates."""
|
|
1087
|
+
self.display.stream_output_sync(agent_id, content)
|
|
1088
|
+
if self.stream_callback:
|
|
1089
|
+
try:
|
|
1090
|
+
self.stream_callback(agent_id, content)
|
|
1091
|
+
except Exception:
|
|
1092
|
+
pass
|
|
1093
|
+
|
|
1094
|
+
def set_agent_model(self, agent_id: int, model_name: str):
|
|
1095
|
+
"""Set agent model - immediate update."""
|
|
1096
|
+
self.display.set_agent_model(agent_id, model_name)
|
|
1097
|
+
self.display.force_update_display()
|
|
1098
|
+
|
|
1099
|
+
def update_agent_status(self, agent_id: int, status: str):
|
|
1100
|
+
"""Update agent status - immediate update for critical state changes."""
|
|
1101
|
+
self.display.update_agent_status(agent_id, status)
|
|
1102
|
+
self.display.force_update_display()
|
|
1103
|
+
|
|
1104
|
+
def update_phase(self, old_phase: str, new_phase: str):
|
|
1105
|
+
"""Update phase - immediate update for critical state changes."""
|
|
1106
|
+
self.display.update_phase(old_phase, new_phase)
|
|
1107
|
+
self.display.force_update_display()
|
|
1108
|
+
|
|
1109
|
+
def update_vote_distribution(self, vote_dist: Dict[int, int]):
|
|
1110
|
+
"""Update vote distribution - immediate update for critical state changes."""
|
|
1111
|
+
self.display.update_vote_distribution(vote_dist)
|
|
1112
|
+
self.display.force_update_display()
|
|
1113
|
+
|
|
1114
|
+
def update_consensus_status(
|
|
1115
|
+
self, representative_id: int, vote_dist: Dict[int, int]
|
|
1116
|
+
):
|
|
1117
|
+
"""Update consensus status - immediate update for critical state changes."""
|
|
1118
|
+
self.display.update_consensus_status(representative_id, vote_dist)
|
|
1119
|
+
self.display.force_update_display()
|
|
1120
|
+
|
|
1121
|
+
def reset_consensus(self):
|
|
1122
|
+
"""Reset consensus - immediate update for critical state changes."""
|
|
1123
|
+
self.display.reset_consensus()
|
|
1124
|
+
self.display.force_update_display()
|
|
1125
|
+
|
|
1126
|
+
def add_system_message(self, message: str):
|
|
1127
|
+
"""Add system message - immediate update for important messages."""
|
|
1128
|
+
self.display.add_system_message(message)
|
|
1129
|
+
self.display.force_update_display()
|
|
1130
|
+
|
|
1131
|
+
def update_agent_vote_target(self, agent_id: int, target_id: Optional[int]):
|
|
1132
|
+
"""Update agent vote target - immediate update for critical state changes."""
|
|
1133
|
+
self.display.update_agent_vote_target(agent_id, target_id)
|
|
1134
|
+
self.display.force_update_display()
|
|
1135
|
+
|
|
1136
|
+
def update_agent_chat_round(self, agent_id: int, round_num: int):
|
|
1137
|
+
"""Update agent chat round - debounced update."""
|
|
1138
|
+
self.display.update_agent_chat_round(agent_id, round_num)
|
|
1139
|
+
# Don't force immediate update for chat rounds
|
|
1140
|
+
|
|
1141
|
+
def update_agent_update_count(self, agent_id: int, count: int):
|
|
1142
|
+
"""Update agent update count - debounced update."""
|
|
1143
|
+
self.display.update_agent_update_count(agent_id, count)
|
|
1144
|
+
# Don't force immediate update for update counts
|
|
1145
|
+
|
|
1146
|
+
def update_agent_votes_cast(self, agent_id: int, votes_cast: int):
|
|
1147
|
+
"""Update agent votes cast - immediate update for vote-related changes."""
|
|
1148
|
+
self.display.update_agent_votes_cast(agent_id, votes_cast)
|
|
1149
|
+
self.display.force_update_display()
|
|
1150
|
+
|
|
1151
|
+
def update_debate_rounds(self, rounds: int):
|
|
1152
|
+
"""Update debate rounds - immediate update for critical state changes."""
|
|
1153
|
+
self.display.update_debate_rounds(rounds)
|
|
1154
|
+
self.display.force_update_display()
|
|
1155
|
+
|
|
1156
|
+
def format_agent_notification(
|
|
1157
|
+
self, agent_id: int, notification_type: str, content: str
|
|
1158
|
+
):
|
|
1159
|
+
"""Format agent notifications - immediate update for notifications."""
|
|
1160
|
+
self.display.format_agent_notification(agent_id, notification_type, content)
|
|
1161
|
+
self.display.force_update_display()
|
|
1162
|
+
|
|
1163
|
+
def get_agent_log_path(self, agent_id: int) -> str:
|
|
1164
|
+
"""Get the log file path for a specific agent."""
|
|
1165
|
+
return self.display.get_agent_log_path_for_display(agent_id)
|
|
1166
|
+
|
|
1167
|
+
def get_agent_answer_path(self, agent_id: int) -> str:
|
|
1168
|
+
"""Get the answer file path for a specific agent."""
|
|
1169
|
+
return self.display.get_agent_answer_path_for_display(agent_id)
|
|
1170
|
+
|
|
1171
|
+
def get_system_log_path(self) -> str:
|
|
1172
|
+
"""Get the system log file path."""
|
|
1173
|
+
return self.display.get_system_log_path_for_display()
|
|
1174
|
+
|
|
1175
|
+
def cleanup(self):
|
|
1176
|
+
"""Clean up resources when orchestrator is no longer needed."""
|
|
1177
|
+
self.display.cleanup()
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
def create_streaming_display(
|
|
1181
|
+
display_enabled: bool = True,
|
|
1182
|
+
stream_callback: Optional[Callable] = None,
|
|
1183
|
+
max_lines: int = 10,
|
|
1184
|
+
save_logs: bool = True,
|
|
1185
|
+
answers_dir: Optional[str] = None,
|
|
1186
|
+
) -> StreamingOrchestrator:
|
|
1187
|
+
"""Create a streaming orchestrator with display capabilities."""
|
|
1188
|
+
return StreamingOrchestrator(
|
|
1189
|
+
display_enabled, stream_callback, max_lines, save_logs, answers_dir
|
|
1190
|
+
)
|