massgen 0.1.3__py3-none-any.whl → 0.1.5__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 +1 -1
- massgen/api_params_handler/_chat_completions_api_params_handler.py +4 -0
- massgen/api_params_handler/_claude_api_params_handler.py +4 -0
- massgen/api_params_handler/_gemini_api_params_handler.py +4 -0
- massgen/api_params_handler/_response_api_params_handler.py +4 -0
- massgen/backend/base_with_custom_tool_and_mcp.py +25 -5
- massgen/backend/docs/permissions_and_context_files.md +2 -2
- massgen/backend/response.py +2 -0
- massgen/chat_agent.py +340 -20
- massgen/cli.py +326 -19
- massgen/configs/README.md +92 -41
- massgen/configs/memory/gpt5mini_gemini_baseline_research_to_implementation.yaml +94 -0
- massgen/configs/memory/gpt5mini_gemini_context_window_management.yaml +187 -0
- massgen/configs/memory/gpt5mini_gemini_research_to_implementation.yaml +127 -0
- massgen/configs/memory/gpt5mini_high_reasoning_gemini.yaml +107 -0
- massgen/configs/memory/single_agent_compression_test.yaml +64 -0
- massgen/configs/tools/custom_tools/crawl4ai_example.yaml +55 -0
- massgen/configs/tools/custom_tools/multimodal_tools/text_to_file_generation_multi.yaml +61 -0
- massgen/configs/tools/custom_tools/multimodal_tools/text_to_file_generation_single.yaml +29 -0
- massgen/configs/tools/custom_tools/multimodal_tools/text_to_image_generation_multi.yaml +51 -0
- massgen/configs/tools/custom_tools/multimodal_tools/text_to_image_generation_single.yaml +33 -0
- massgen/configs/tools/custom_tools/multimodal_tools/text_to_speech_generation_multi.yaml +55 -0
- massgen/configs/tools/custom_tools/multimodal_tools/text_to_speech_generation_single.yaml +33 -0
- massgen/configs/tools/custom_tools/multimodal_tools/text_to_video_generation_multi.yaml +47 -0
- massgen/configs/tools/custom_tools/multimodal_tools/text_to_video_generation_single.yaml +29 -0
- massgen/configs/tools/custom_tools/multimodal_tools/understand_audio.yaml +1 -1
- massgen/configs/tools/custom_tools/multimodal_tools/understand_file.yaml +1 -1
- massgen/configs/tools/custom_tools/multimodal_tools/understand_image.yaml +1 -1
- massgen/configs/tools/custom_tools/multimodal_tools/understand_video.yaml +1 -1
- massgen/configs/tools/custom_tools/multimodal_tools/youtube_video_analysis.yaml +1 -1
- massgen/filesystem_manager/_filesystem_manager.py +1 -0
- massgen/filesystem_manager/_path_permission_manager.py +148 -0
- massgen/memory/README.md +277 -0
- massgen/memory/__init__.py +26 -0
- massgen/memory/_base.py +193 -0
- massgen/memory/_compression.py +237 -0
- massgen/memory/_context_monitor.py +211 -0
- massgen/memory/_conversation.py +255 -0
- massgen/memory/_fact_extraction_prompts.py +333 -0
- massgen/memory/_mem0_adapters.py +257 -0
- massgen/memory/_persistent.py +687 -0
- massgen/memory/docker-compose.qdrant.yml +36 -0
- massgen/memory/docs/DESIGN.md +388 -0
- massgen/memory/docs/QUICKSTART.md +409 -0
- massgen/memory/docs/SUMMARY.md +319 -0
- massgen/memory/docs/agent_use_memory.md +408 -0
- massgen/memory/docs/orchestrator_use_memory.md +586 -0
- massgen/memory/examples.py +237 -0
- massgen/message_templates.py +160 -12
- massgen/orchestrator.py +223 -7
- massgen/tests/memory/test_agent_compression.py +174 -0
- massgen/{configs/tools → tests}/memory/test_context_window_management.py +30 -30
- massgen/tests/memory/test_force_compression.py +154 -0
- massgen/tests/memory/test_simple_compression.py +147 -0
- massgen/tests/test_agent_memory.py +534 -0
- massgen/tests/test_binary_file_blocking.py +274 -0
- massgen/tests/test_case_studies.md +12 -12
- massgen/tests/test_conversation_memory.py +382 -0
- massgen/tests/test_multimodal_size_limits.py +407 -0
- massgen/tests/test_orchestrator_memory.py +620 -0
- massgen/tests/test_persistent_memory.py +435 -0
- massgen/token_manager/token_manager.py +6 -0
- massgen/tool/_manager.py +7 -2
- massgen/tool/_multimodal_tools/image_to_image_generation.py +293 -0
- massgen/tool/_multimodal_tools/text_to_file_generation.py +455 -0
- massgen/tool/_multimodal_tools/text_to_image_generation.py +222 -0
- massgen/tool/_multimodal_tools/text_to_speech_continue_generation.py +226 -0
- massgen/tool/_multimodal_tools/text_to_speech_transcription_generation.py +217 -0
- massgen/tool/_multimodal_tools/text_to_video_generation.py +223 -0
- massgen/tool/_multimodal_tools/understand_audio.py +19 -1
- massgen/tool/_multimodal_tools/understand_file.py +6 -1
- massgen/tool/_multimodal_tools/understand_image.py +112 -8
- massgen/tool/_multimodal_tools/understand_video.py +32 -5
- massgen/tool/_web_tools/crawl4ai_tool.py +718 -0
- massgen/tool/docs/multimodal_tools.md +589 -0
- massgen/tools/__init__.py +8 -0
- massgen/tools/_planning_mcp_server.py +520 -0
- massgen/tools/planning_dataclasses.py +434 -0
- {massgen-0.1.3.dist-info → massgen-0.1.5.dist-info}/METADATA +142 -82
- {massgen-0.1.3.dist-info → massgen-0.1.5.dist-info}/RECORD +84 -41
- massgen/configs/tools/custom_tools/crawl4ai_mcp_example.yaml +0 -67
- massgen/configs/tools/custom_tools/crawl4ai_multi_agent_example.yaml +0 -68
- massgen/configs/tools/memory/README.md +0 -199
- massgen/configs/tools/memory/gpt5mini_gemini_context_window_management.yaml +0 -131
- massgen/configs/tools/memory/gpt5mini_gemini_no_persistent_memory.yaml +0 -133
- massgen/configs/tools/multimodal/gpt5mini_gpt5nano_documentation_evolution.yaml +0 -97
- {massgen-0.1.3.dist-info → massgen-0.1.5.dist-info}/WHEEL +0 -0
- {massgen-0.1.3.dist-info → massgen-0.1.5.dist-info}/entry_points.txt +0 -0
- {massgen-0.1.3.dist-info → massgen-0.1.5.dist-info}/licenses/LICENSE +0 -0
- {massgen-0.1.3.dist-info → massgen-0.1.5.dist-info}/top_level.txt +0 -0
massgen/orchestrator.py
CHANGED
|
@@ -42,6 +42,7 @@ from .logger_config import (
|
|
|
42
42
|
log_stream_chunk,
|
|
43
43
|
log_tool_call,
|
|
44
44
|
)
|
|
45
|
+
from .memory import ConversationMemory, PersistentMemoryBase
|
|
45
46
|
from .message_templates import MessageTemplates
|
|
46
47
|
from .stream_chunk import ChunkType
|
|
47
48
|
from .tool import get_post_evaluation_tools, get_workflow_tools
|
|
@@ -118,6 +119,9 @@ class Orchestrator(ChatAgent):
|
|
|
118
119
|
snapshot_storage: Optional[str] = None,
|
|
119
120
|
agent_temporary_workspace: Optional[str] = None,
|
|
120
121
|
previous_turns: Optional[List[Dict[str, Any]]] = None,
|
|
122
|
+
winning_agents_history: Optional[List[Dict[str, Any]]] = None,
|
|
123
|
+
shared_conversation_memory: Optional[ConversationMemory] = None,
|
|
124
|
+
shared_persistent_memory: Optional[PersistentMemoryBase] = None,
|
|
121
125
|
):
|
|
122
126
|
"""
|
|
123
127
|
Initialize MassGen orchestrator.
|
|
@@ -130,13 +134,22 @@ class Orchestrator(ChatAgent):
|
|
|
130
134
|
snapshot_storage: Optional path to store agent workspace snapshots
|
|
131
135
|
agent_temporary_workspace: Optional path for agent temporary workspaces
|
|
132
136
|
previous_turns: List of previous turn metadata for multi-turn conversations (loaded by CLI)
|
|
137
|
+
winning_agents_history: List of previous winning agents for memory sharing
|
|
138
|
+
Format: [{"agent_id": "agent_b", "turn": 1}, ...]
|
|
139
|
+
Loaded from session storage to persist across orchestrator recreations
|
|
140
|
+
shared_conversation_memory: Optional shared conversation memory for all agents
|
|
141
|
+
shared_persistent_memory: Optional shared persistent memory for all agents
|
|
133
142
|
"""
|
|
134
|
-
super().__init__(session_id)
|
|
143
|
+
super().__init__(session_id, shared_conversation_memory, shared_persistent_memory)
|
|
135
144
|
self.orchestrator_id = orchestrator_id
|
|
136
145
|
self.agents = agents
|
|
137
146
|
self.agent_states = {aid: AgentState() for aid in agents.keys()}
|
|
138
147
|
self.config = config or AgentConfig.create_openai_config()
|
|
139
148
|
|
|
149
|
+
# Shared memory for all agents
|
|
150
|
+
self.shared_conversation_memory = shared_conversation_memory
|
|
151
|
+
self.shared_persistent_memory = shared_persistent_memory
|
|
152
|
+
|
|
140
153
|
# Get message templates from config
|
|
141
154
|
self.message_templates = self.config.message_templates or MessageTemplates(
|
|
142
155
|
voting_sensitivity=self.config.voting_sensitivity,
|
|
@@ -158,6 +171,14 @@ class Orchestrator(ChatAgent):
|
|
|
158
171
|
self._selected_agent: Optional[str] = None
|
|
159
172
|
self._final_presentation_content: Optional[str] = None
|
|
160
173
|
|
|
174
|
+
# Track winning agents by turn for memory sharing
|
|
175
|
+
# Format: [{"agent_id": "agent_b", "turn": 1}, {"agent_id": "agent_a", "turn": 2}]
|
|
176
|
+
# Restore from session storage if provided (for multi-turn persistence)
|
|
177
|
+
self._winning_agents_history: List[Dict[str, Any]] = winning_agents_history or []
|
|
178
|
+
if self._winning_agents_history:
|
|
179
|
+
logger.info(f"📚 Restored {len(self._winning_agents_history)} winning agent(s) from session: {self._winning_agents_history}")
|
|
180
|
+
self._current_turn: int = 0
|
|
181
|
+
|
|
161
182
|
# Timeout and resource tracking
|
|
162
183
|
self.total_tokens: int = 0
|
|
163
184
|
self.coordination_start_time: float = 0
|
|
@@ -365,6 +386,113 @@ class Orchestrator(ChatAgent):
|
|
|
365
386
|
"full_messages": messages,
|
|
366
387
|
}
|
|
367
388
|
|
|
389
|
+
async def _inject_shared_memory_context(
|
|
390
|
+
self,
|
|
391
|
+
messages: List[Dict[str, Any]],
|
|
392
|
+
agent_id: str,
|
|
393
|
+
) -> List[Dict[str, Any]]:
|
|
394
|
+
"""
|
|
395
|
+
Inject shared memory context into agent messages.
|
|
396
|
+
|
|
397
|
+
This allows all agents to see shared memories including what other agents
|
|
398
|
+
have stored in the shared memory.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
messages: Original messages to send to agent
|
|
402
|
+
agent_id: ID of the agent receiving the messages
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
Messages with shared memory context injected
|
|
406
|
+
"""
|
|
407
|
+
if not self.shared_conversation_memory and not self.shared_persistent_memory:
|
|
408
|
+
# No shared memory configured, return original messages
|
|
409
|
+
return messages
|
|
410
|
+
|
|
411
|
+
memory_context_parts = []
|
|
412
|
+
|
|
413
|
+
# Get conversation memory content
|
|
414
|
+
if self.shared_conversation_memory:
|
|
415
|
+
try:
|
|
416
|
+
conv_messages = await self.shared_conversation_memory.get_messages()
|
|
417
|
+
if conv_messages:
|
|
418
|
+
memory_context_parts.append("=== SHARED CONVERSATION MEMORY ===")
|
|
419
|
+
for msg in conv_messages[-10:]: # Last 10 messages
|
|
420
|
+
role = msg.get("role", "unknown")
|
|
421
|
+
content = msg.get("content", "")
|
|
422
|
+
agent_source = msg.get("agent_id", "unknown")
|
|
423
|
+
memory_context_parts.append(f"[{agent_source}] {role}: {content}")
|
|
424
|
+
except Exception as e:
|
|
425
|
+
logger.warning(f"Failed to retrieve shared conversation memory: {e}")
|
|
426
|
+
|
|
427
|
+
# Get persistent memory content
|
|
428
|
+
if self.shared_persistent_memory:
|
|
429
|
+
try:
|
|
430
|
+
# Extract user message for retrieval
|
|
431
|
+
user_messages = [msg for msg in messages if msg.get("role") == "user"]
|
|
432
|
+
if user_messages:
|
|
433
|
+
retrieved = await self.shared_persistent_memory.retrieve(user_messages)
|
|
434
|
+
if retrieved:
|
|
435
|
+
memory_context_parts.append("\n=== SHARED PERSISTENT MEMORY ===")
|
|
436
|
+
memory_context_parts.append(retrieved)
|
|
437
|
+
except NotImplementedError:
|
|
438
|
+
# Memory backend doesn't support retrieve
|
|
439
|
+
pass
|
|
440
|
+
except Exception as e:
|
|
441
|
+
logger.warning(f"Failed to retrieve shared persistent memory: {e}")
|
|
442
|
+
|
|
443
|
+
# Inject memory context if we have any
|
|
444
|
+
if memory_context_parts:
|
|
445
|
+
memory_message = {
|
|
446
|
+
"role": "system",
|
|
447
|
+
"content": ("You have access to shared memory that all agents can see and contribute to.\n" + "\n".join(memory_context_parts)),
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
# Insert after existing system messages but before user messages
|
|
451
|
+
system_count = sum(1 for msg in messages if msg.get("role") == "system")
|
|
452
|
+
modified_messages = messages.copy()
|
|
453
|
+
modified_messages.insert(system_count, memory_message)
|
|
454
|
+
return modified_messages
|
|
455
|
+
|
|
456
|
+
return messages
|
|
457
|
+
|
|
458
|
+
async def _record_to_shared_memory(
|
|
459
|
+
self,
|
|
460
|
+
agent_id: str,
|
|
461
|
+
content: str,
|
|
462
|
+
role: str = "assistant",
|
|
463
|
+
) -> None:
|
|
464
|
+
"""
|
|
465
|
+
Record agent's contribution to shared memory.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
agent_id: ID of the agent contributing
|
|
469
|
+
content: Content to record
|
|
470
|
+
role: Role of the message (default: "assistant")
|
|
471
|
+
"""
|
|
472
|
+
message = {
|
|
473
|
+
"role": role,
|
|
474
|
+
"content": content,
|
|
475
|
+
"agent_id": agent_id,
|
|
476
|
+
"timestamp": time.time(),
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
# Add to conversation memory
|
|
480
|
+
if self.shared_conversation_memory:
|
|
481
|
+
try:
|
|
482
|
+
await self.shared_conversation_memory.add(message)
|
|
483
|
+
except Exception as e:
|
|
484
|
+
logger.warning(f"Failed to add to shared conversation memory: {e}")
|
|
485
|
+
|
|
486
|
+
# Record to persistent memory
|
|
487
|
+
if self.shared_persistent_memory:
|
|
488
|
+
try:
|
|
489
|
+
await self.shared_persistent_memory.record([message])
|
|
490
|
+
except NotImplementedError:
|
|
491
|
+
# Memory backend doesn't support record
|
|
492
|
+
pass
|
|
493
|
+
except Exception as e:
|
|
494
|
+
logger.warning(f"Failed to record to shared persistent memory: {e}")
|
|
495
|
+
|
|
368
496
|
def save_coordination_logs(self):
|
|
369
497
|
"""Public method to save coordination logs after final presentation is complete."""
|
|
370
498
|
# End the coordination session
|
|
@@ -798,6 +926,18 @@ Your answer:"""
|
|
|
798
926
|
current_answers = {aid: state.answer for aid, state in self.agent_states.items() if state.answer}
|
|
799
927
|
self._selected_agent = self._determine_final_agent_from_votes(votes, current_answers)
|
|
800
928
|
|
|
929
|
+
# Track winning agent for memory sharing in future turns
|
|
930
|
+
self._current_turn += 1
|
|
931
|
+
if self._selected_agent:
|
|
932
|
+
winner_entry = {
|
|
933
|
+
"agent_id": self._selected_agent,
|
|
934
|
+
"turn": self._current_turn,
|
|
935
|
+
}
|
|
936
|
+
self._winning_agents_history.append(winner_entry)
|
|
937
|
+
logger.info(
|
|
938
|
+
f"🏆 Turn {self._current_turn} winner: {self._selected_agent} " f"(tracked for memory sharing)",
|
|
939
|
+
)
|
|
940
|
+
|
|
801
941
|
log_coordination_step(
|
|
802
942
|
"Final agent selected",
|
|
803
943
|
{"selected_agent": self._selected_agent, "votes": votes},
|
|
@@ -1806,6 +1946,13 @@ Your answer:"""
|
|
|
1806
1946
|
{"role": "system", "content": conversation["system_message"]},
|
|
1807
1947
|
{"role": "user", "content": conversation["user_message"]},
|
|
1808
1948
|
]
|
|
1949
|
+
|
|
1950
|
+
# Inject shared memory context
|
|
1951
|
+
conversation_messages = await self._inject_shared_memory_context(
|
|
1952
|
+
conversation_messages,
|
|
1953
|
+
agent_id,
|
|
1954
|
+
)
|
|
1955
|
+
|
|
1809
1956
|
enforcement_msg = self.message_templates.enforcement_message()
|
|
1810
1957
|
|
|
1811
1958
|
# Update agent status to STREAMING
|
|
@@ -1832,20 +1979,42 @@ Your answer:"""
|
|
|
1832
1979
|
# First attempt: orchestrator provides initial conversation
|
|
1833
1980
|
# But we need the agent to have this in its history for subsequent calls
|
|
1834
1981
|
# First attempt: provide complete conversation and reset agent's history
|
|
1835
|
-
|
|
1982
|
+
# Pass current turn and previous winners for memory sharing
|
|
1983
|
+
chat_stream = agent.chat(
|
|
1984
|
+
conversation_messages,
|
|
1985
|
+
self.workflow_tools,
|
|
1986
|
+
reset_chat=True,
|
|
1987
|
+
current_stage=CoordinationStage.INITIAL_ANSWER,
|
|
1988
|
+
orchestrator_turn=self._current_turn + 1, # Next turn number
|
|
1989
|
+
previous_winners=self._winning_agents_history.copy(),
|
|
1990
|
+
)
|
|
1836
1991
|
else:
|
|
1837
1992
|
# Subsequent attempts: send enforcement message (set by error handling)
|
|
1838
1993
|
|
|
1839
1994
|
if isinstance(enforcement_msg, list):
|
|
1840
1995
|
# Tool message array
|
|
1841
|
-
chat_stream = agent.chat(
|
|
1996
|
+
chat_stream = agent.chat(
|
|
1997
|
+
enforcement_msg,
|
|
1998
|
+
self.workflow_tools,
|
|
1999
|
+
reset_chat=False,
|
|
2000
|
+
current_stage=CoordinationStage.ENFORCEMENT,
|
|
2001
|
+
orchestrator_turn=self._current_turn + 1,
|
|
2002
|
+
previous_winners=self._winning_agents_history.copy(),
|
|
2003
|
+
)
|
|
1842
2004
|
else:
|
|
1843
2005
|
# Single user message
|
|
1844
2006
|
enforcement_message = {
|
|
1845
2007
|
"role": "user",
|
|
1846
2008
|
"content": enforcement_msg,
|
|
1847
2009
|
}
|
|
1848
|
-
chat_stream = agent.chat(
|
|
2010
|
+
chat_stream = agent.chat(
|
|
2011
|
+
[enforcement_message],
|
|
2012
|
+
self.workflow_tools,
|
|
2013
|
+
reset_chat=False,
|
|
2014
|
+
current_stage=CoordinationStage.ENFORCEMENT,
|
|
2015
|
+
orchestrator_turn=self._current_turn + 1,
|
|
2016
|
+
previous_winners=self._winning_agents_history.copy(),
|
|
2017
|
+
)
|
|
1849
2018
|
response_text = ""
|
|
1850
2019
|
tool_calls = []
|
|
1851
2020
|
workflow_tool_found = False
|
|
@@ -2101,6 +2270,14 @@ Your answer:"""
|
|
|
2101
2270
|
"reason": reason,
|
|
2102
2271
|
}
|
|
2103
2272
|
|
|
2273
|
+
# Record vote to shared memory
|
|
2274
|
+
vote_message = f"Voted for {voted_agent}. Reason: {reason}"
|
|
2275
|
+
await self._record_to_shared_memory(
|
|
2276
|
+
agent_id=agent_id,
|
|
2277
|
+
content=vote_message,
|
|
2278
|
+
role="assistant",
|
|
2279
|
+
)
|
|
2280
|
+
|
|
2104
2281
|
# Send tool result - orchestrator will decide if vote is accepted
|
|
2105
2282
|
# Vote submitted (result will be shown by orchestrator)
|
|
2106
2283
|
yield (
|
|
@@ -2193,6 +2370,14 @@ Your answer:"""
|
|
|
2193
2370
|
return
|
|
2194
2371
|
# Send successful tool result back to agent
|
|
2195
2372
|
# Answer recorded (result will be shown by orchestrator)
|
|
2373
|
+
|
|
2374
|
+
# Record to shared memory
|
|
2375
|
+
await self._record_to_shared_memory(
|
|
2376
|
+
agent_id=agent_id,
|
|
2377
|
+
content=content,
|
|
2378
|
+
role="assistant",
|
|
2379
|
+
)
|
|
2380
|
+
|
|
2196
2381
|
yield ("result", ("answer", content))
|
|
2197
2382
|
yield ("done", None)
|
|
2198
2383
|
return
|
|
@@ -2513,6 +2698,20 @@ INSTRUCTIONS FOR NEXT ATTEMPT:
|
|
|
2513
2698
|
elif hasattr(agent, "backend") and hasattr(agent.backend, "backend_params"):
|
|
2514
2699
|
enable_audio_generation = agent.backend.backend_params.get("enable_audio_generation", False)
|
|
2515
2700
|
|
|
2701
|
+
# Check if file generation is enabled for this agent
|
|
2702
|
+
enable_file_generation = False
|
|
2703
|
+
if hasattr(agent, "config") and agent.config:
|
|
2704
|
+
enable_file_generation = agent.config.backend_params.get("enable_file_generation", False)
|
|
2705
|
+
elif hasattr(agent, "backend") and hasattr(agent.backend, "backend_params"):
|
|
2706
|
+
enable_file_generation = agent.backend.backend_params.get("enable_file_generation", False)
|
|
2707
|
+
|
|
2708
|
+
# Check if video generation is enabled for this agent
|
|
2709
|
+
enable_video_generation = False
|
|
2710
|
+
if hasattr(agent, "config") and agent.config:
|
|
2711
|
+
enable_video_generation = agent.config.backend_params.get("enable_video_generation", False)
|
|
2712
|
+
elif hasattr(agent, "backend") and hasattr(agent.backend, "backend_params"):
|
|
2713
|
+
enable_video_generation = agent.backend.backend_params.get("enable_video_generation", False)
|
|
2714
|
+
|
|
2516
2715
|
# Check if agent has write access to context paths (requires file delivery)
|
|
2517
2716
|
has_irreversible_actions = False
|
|
2518
2717
|
if agent.backend.filesystem_manager:
|
|
@@ -2525,6 +2724,8 @@ INSTRUCTIONS FOR NEXT ATTEMPT:
|
|
|
2525
2724
|
agent_system_message,
|
|
2526
2725
|
enable_image_generation,
|
|
2527
2726
|
enable_audio_generation,
|
|
2727
|
+
enable_file_generation,
|
|
2728
|
+
enable_video_generation,
|
|
2528
2729
|
has_irreversible_actions,
|
|
2529
2730
|
enable_command_execution,
|
|
2530
2731
|
)
|
|
@@ -2607,7 +2808,13 @@ INSTRUCTIONS FOR NEXT ATTEMPT:
|
|
|
2607
2808
|
|
|
2608
2809
|
try:
|
|
2609
2810
|
# Track final round iterations (each chunk is like an iteration)
|
|
2610
|
-
async for chunk in agent.chat(
|
|
2811
|
+
async for chunk in agent.chat(
|
|
2812
|
+
presentation_messages,
|
|
2813
|
+
reset_chat=True,
|
|
2814
|
+
current_stage=CoordinationStage.PRESENTATION,
|
|
2815
|
+
orchestrator_turn=self._current_turn,
|
|
2816
|
+
previous_winners=self._winning_agents_history.copy(),
|
|
2817
|
+
):
|
|
2611
2818
|
chunk_type = self._get_chunk_type_value(chunk)
|
|
2612
2819
|
# Start new iteration for this chunk
|
|
2613
2820
|
self.coordination_tracker.start_new_iteration()
|
|
@@ -2856,7 +3063,14 @@ Then call either submit(confirmed=True) if the answer is satisfactory, or restar
|
|
|
2856
3063
|
try:
|
|
2857
3064
|
timeout_seconds = self.config.timeout_config.orchestrator_timeout_seconds
|
|
2858
3065
|
async with asyncio.timeout(timeout_seconds):
|
|
2859
|
-
async for chunk in agent.chat(
|
|
3066
|
+
async for chunk in agent.chat(
|
|
3067
|
+
messages=evaluation_messages,
|
|
3068
|
+
tools=post_eval_tools,
|
|
3069
|
+
reset_chat=True,
|
|
3070
|
+
current_stage=CoordinationStage.POST_EVALUATION,
|
|
3071
|
+
orchestrator_turn=self._current_turn,
|
|
3072
|
+
previous_winners=self._winning_agents_history.copy(),
|
|
3073
|
+
):
|
|
2860
3074
|
chunk_type = self._get_chunk_type_value(chunk)
|
|
2861
3075
|
|
|
2862
3076
|
if chunk_type == "content" and chunk.content:
|
|
@@ -3087,7 +3301,8 @@ Then call either submit(confirmed=True) if the answer is satisfactory, or restar
|
|
|
3087
3301
|
Get final result for session persistence.
|
|
3088
3302
|
|
|
3089
3303
|
Returns:
|
|
3090
|
-
Dict with final_answer, winning_agent_id,
|
|
3304
|
+
Dict with final_answer, winning_agent_id, workspace_path, and winning_agents_history,
|
|
3305
|
+
or None if not available
|
|
3091
3306
|
"""
|
|
3092
3307
|
if not self._selected_agent or not self._final_presentation_content:
|
|
3093
3308
|
return None
|
|
@@ -3101,6 +3316,7 @@ Then call either submit(confirmed=True) if the answer is satisfactory, or restar
|
|
|
3101
3316
|
"final_answer": self._final_presentation_content,
|
|
3102
3317
|
"winning_agent_id": self._selected_agent,
|
|
3103
3318
|
"workspace_path": workspace_path,
|
|
3319
|
+
"winning_agents_history": self._winning_agents_history.copy(), # For cross-turn memory sharing
|
|
3104
3320
|
}
|
|
3105
3321
|
|
|
3106
3322
|
def get_status(self) -> Dict[str, Any]:
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Test Context Compression at Agent Level
|
|
5
|
+
|
|
6
|
+
This script tests compression with a SingleAgent directly (not through orchestrator).
|
|
7
|
+
It creates many messages to trigger compression and verifies it works.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
uv run python massgen/configs/memory/test_agent_compression.py
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
|
19
|
+
|
|
20
|
+
from dotenv import load_dotenv # noqa: E402
|
|
21
|
+
|
|
22
|
+
from massgen.backend.chat_completions import ChatCompletionsBackend # noqa: E402
|
|
23
|
+
from massgen.chat_agent import SingleAgent # noqa: E402
|
|
24
|
+
from massgen.memory import ConversationMemory, PersistentMemory # noqa: E402
|
|
25
|
+
from massgen.memory._context_monitor import ContextWindowMonitor # noqa: E402
|
|
26
|
+
|
|
27
|
+
load_dotenv()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def main():
|
|
31
|
+
"""Test compression with a single agent."""
|
|
32
|
+
print("=" * 80)
|
|
33
|
+
print("Testing Context Compression at Agent Level")
|
|
34
|
+
print("=" * 80 + "\n")
|
|
35
|
+
|
|
36
|
+
# Check API key
|
|
37
|
+
if not os.getenv("OPENAI_API_KEY"):
|
|
38
|
+
print("❌ Error: OPENAI_API_KEY not set")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
# Configuration
|
|
42
|
+
model_name = "gpt-4o-mini"
|
|
43
|
+
provider = "openai"
|
|
44
|
+
trigger_threshold = 0.05 # Trigger at 5% for quick testing
|
|
45
|
+
target_ratio = 0.02 # Keep only 2% after compression
|
|
46
|
+
|
|
47
|
+
print("Configuration:")
|
|
48
|
+
print(f" Model: {model_name}")
|
|
49
|
+
print(f" Trigger: {trigger_threshold*100:.0f}%")
|
|
50
|
+
print(f" Target: {target_ratio*100:.0f}%\n")
|
|
51
|
+
|
|
52
|
+
# 1. Create backend
|
|
53
|
+
backend = ChatCompletionsBackend(
|
|
54
|
+
type=provider,
|
|
55
|
+
model=model_name,
|
|
56
|
+
api_key=os.getenv("OPENAI_API_KEY"),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# 2. Create memories
|
|
60
|
+
conversation_memory = ConversationMemory()
|
|
61
|
+
|
|
62
|
+
embedding_backend = ChatCompletionsBackend(
|
|
63
|
+
type="openai",
|
|
64
|
+
model="text-embedding-3-small",
|
|
65
|
+
api_key=os.getenv("OPENAI_API_KEY"),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
persistent_memory = PersistentMemory(
|
|
69
|
+
agent_name="test_compression_agent",
|
|
70
|
+
session_name="test_session",
|
|
71
|
+
llm_backend=backend,
|
|
72
|
+
embedding_backend=embedding_backend,
|
|
73
|
+
on_disk=False, # In-memory for testing
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
print("✅ Memories created")
|
|
77
|
+
|
|
78
|
+
# 3. Create context monitor
|
|
79
|
+
monitor = ContextWindowMonitor(
|
|
80
|
+
model_name=model_name,
|
|
81
|
+
provider=provider,
|
|
82
|
+
trigger_threshold=trigger_threshold,
|
|
83
|
+
target_ratio=target_ratio,
|
|
84
|
+
enabled=True,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
print(f"✅ Monitor created (window: {monitor.context_window:,} tokens)")
|
|
88
|
+
print(f" Will warn at: {int(monitor.context_window * trigger_threshold):,} tokens\n")
|
|
89
|
+
|
|
90
|
+
# 4. Create agent with monitor
|
|
91
|
+
agent = SingleAgent(
|
|
92
|
+
backend=backend,
|
|
93
|
+
agent_id="test_agent",
|
|
94
|
+
system_message="You are a helpful assistant. Provide detailed, thorough responses.",
|
|
95
|
+
conversation_memory=conversation_memory,
|
|
96
|
+
persistent_memory=persistent_memory,
|
|
97
|
+
context_monitor=monitor,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Verify compressor was created
|
|
101
|
+
if agent.context_compressor:
|
|
102
|
+
print("✅ Context compressor created!\n")
|
|
103
|
+
else:
|
|
104
|
+
print("❌ Context compressor NOT created!\n")
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
# 5. Simulate multiple turns to fill context
|
|
108
|
+
print("=" * 80)
|
|
109
|
+
print("Simulating conversation to trigger compression...")
|
|
110
|
+
print("=" * 80 + "\n")
|
|
111
|
+
|
|
112
|
+
# Create several turns with verbose responses
|
|
113
|
+
prompts = [
|
|
114
|
+
"Explain how Python's garbage collection works in detail.",
|
|
115
|
+
"Now explain Python's Global Interpreter Lock (GIL) in detail.",
|
|
116
|
+
"Explain Python's asyncio event loop architecture in detail.",
|
|
117
|
+
"Explain Python's descriptor protocol in detail.",
|
|
118
|
+
"Explain Python's metaclasses and how they work in detail.",
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
for i, prompt in enumerate(prompts, 1):
|
|
122
|
+
print(f"\n--- Turn {i} ---")
|
|
123
|
+
print(f"User: {prompt[:60]}...")
|
|
124
|
+
|
|
125
|
+
# Check context before turn
|
|
126
|
+
current_messages = await conversation_memory.get_messages()
|
|
127
|
+
print(f"Messages before turn: {len(current_messages)}")
|
|
128
|
+
|
|
129
|
+
response_text = ""
|
|
130
|
+
async for chunk in agent.chat([{"role": "user", "content": prompt}]):
|
|
131
|
+
if chunk.type == "content" and chunk.content:
|
|
132
|
+
response_text += chunk.content
|
|
133
|
+
|
|
134
|
+
print(f"Response: {len(response_text)} chars")
|
|
135
|
+
|
|
136
|
+
# Check context after turn
|
|
137
|
+
current_messages = await conversation_memory.get_messages()
|
|
138
|
+
print(f"Messages after turn: {len(current_messages)}")
|
|
139
|
+
|
|
140
|
+
# Small delay between turns
|
|
141
|
+
await asyncio.sleep(0.5)
|
|
142
|
+
|
|
143
|
+
# 6. Show final statistics
|
|
144
|
+
print("\n" + "=" * 80)
|
|
145
|
+
print("Final Statistics")
|
|
146
|
+
print("=" * 80)
|
|
147
|
+
|
|
148
|
+
stats = monitor.get_stats()
|
|
149
|
+
print("\n📊 Monitor Stats:")
|
|
150
|
+
print(f" Total turns: {stats['turn_count']}")
|
|
151
|
+
print(f" Total tokens: {stats['total_tokens']:,}")
|
|
152
|
+
print(f" Peak usage: {stats['peak_usage_percent']*100:.1f}%")
|
|
153
|
+
|
|
154
|
+
if agent.context_compressor:
|
|
155
|
+
comp_stats = agent.context_compressor.get_stats()
|
|
156
|
+
print("\n📦 Compression Stats:")
|
|
157
|
+
print(f" Total compressions: {comp_stats['total_compressions']}")
|
|
158
|
+
print(f" Messages removed: {comp_stats['total_messages_removed']}")
|
|
159
|
+
print(f" Tokens removed: {comp_stats['total_tokens_removed']:,}")
|
|
160
|
+
|
|
161
|
+
final_messages = await conversation_memory.get_messages()
|
|
162
|
+
print("\n💾 Final Memory State:")
|
|
163
|
+
print(f" Messages in conversation_memory: {len(final_messages)}")
|
|
164
|
+
|
|
165
|
+
print("\n" + "=" * 80)
|
|
166
|
+
if agent.context_compressor and comp_stats["total_compressions"] > 0:
|
|
167
|
+
print("✅ SUCCESS: Compression worked!")
|
|
168
|
+
else:
|
|
169
|
+
print("⚠️ No compression occurred (context may not have reached threshold)")
|
|
170
|
+
print("=" * 80 + "\n")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == "__main__":
|
|
174
|
+
asyncio.run(main())
|
|
@@ -21,12 +21,12 @@ from pathlib import Path
|
|
|
21
21
|
# Add parent directory to path for imports
|
|
22
22
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
|
23
23
|
|
|
24
|
-
import yaml
|
|
25
|
-
from dotenv import load_dotenv
|
|
24
|
+
import yaml # noqa: E402
|
|
25
|
+
from dotenv import load_dotenv # noqa: E402
|
|
26
26
|
|
|
27
|
-
from massgen.backend.chat_completions import ChatCompletionsBackend
|
|
28
|
-
from massgen.chat_agent import SingleAgent
|
|
29
|
-
from massgen.memory import ConversationMemory, PersistentMemory
|
|
27
|
+
from massgen.backend.chat_completions import ChatCompletionsBackend # noqa: E402
|
|
28
|
+
from massgen.chat_agent import SingleAgent # noqa: E402
|
|
29
|
+
from massgen.memory import ConversationMemory, PersistentMemory # noqa: E402
|
|
30
30
|
|
|
31
31
|
# Load environment variables from .env file
|
|
32
32
|
load_dotenv()
|
|
@@ -38,19 +38,19 @@ def load_config(config_path: str = None) -> dict:
|
|
|
38
38
|
# Default to the config in same directory
|
|
39
39
|
config_path = Path(__file__).parent / "gpt5mini_gemini_context_window_management.yaml"
|
|
40
40
|
|
|
41
|
-
with open(config_path,
|
|
41
|
+
with open(config_path, "r") as f:
|
|
42
42
|
return yaml.safe_load(f)
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
async def test_with_persistent_memory(config: dict):
|
|
46
46
|
"""Test context compression with persistent memory enabled."""
|
|
47
47
|
# Check if memory is enabled in config
|
|
48
|
-
memory_config = config.get(
|
|
49
|
-
if not memory_config.get(
|
|
48
|
+
memory_config = config.get("memory", {})
|
|
49
|
+
if not memory_config.get("enabled", True):
|
|
50
50
|
print("\n⚠️ Skipping: memory.enabled is false in config")
|
|
51
51
|
return
|
|
52
52
|
|
|
53
|
-
persistent_enabled = memory_config.get(
|
|
53
|
+
persistent_enabled = memory_config.get("persistent_memory", {}).get("enabled", True)
|
|
54
54
|
if not persistent_enabled:
|
|
55
55
|
print("\n⚠️ Skipping: memory.persistent_memory.enabled is false in config")
|
|
56
56
|
return
|
|
@@ -60,10 +60,10 @@ async def test_with_persistent_memory(config: dict):
|
|
|
60
60
|
print("=" * 70 + "\n")
|
|
61
61
|
|
|
62
62
|
# Get memory settings from config
|
|
63
|
-
persistent_config = memory_config.get(
|
|
64
|
-
agent_name = persistent_config.get(
|
|
65
|
-
session_name = persistent_config.get(
|
|
66
|
-
on_disk = persistent_config.get(
|
|
63
|
+
persistent_config = memory_config.get("persistent_memory", {})
|
|
64
|
+
agent_name = persistent_config.get("agent_name", "storyteller_agent")
|
|
65
|
+
session_name = persistent_config.get("session_name", "test_session")
|
|
66
|
+
on_disk = persistent_config.get("on_disk", True)
|
|
67
67
|
|
|
68
68
|
# Create LLM backend for both agent and memory
|
|
69
69
|
llm_backend = ChatCompletionsBackend(
|
|
@@ -93,18 +93,17 @@ async def test_with_persistent_memory(config: dict):
|
|
|
93
93
|
agent = SingleAgent(
|
|
94
94
|
backend=llm_backend,
|
|
95
95
|
agent_id="storyteller",
|
|
96
|
-
system_message="You are a creative storyteller. Create detailed, "
|
|
97
|
-
"immersive narratives with rich descriptions.",
|
|
96
|
+
system_message="You are a creative storyteller. Create detailed, " "immersive narratives with rich descriptions.",
|
|
98
97
|
conversation_memory=conversation_memory,
|
|
99
98
|
persistent_memory=persistent_memory,
|
|
100
99
|
)
|
|
101
100
|
|
|
102
101
|
print("✅ Agent initialized with memory")
|
|
103
|
-
print(
|
|
102
|
+
print(" - ConversationMemory: Active")
|
|
104
103
|
print(f" - PersistentMemory: Active (agent={agent_name}, session={session_name}, on_disk={on_disk})")
|
|
105
|
-
print(
|
|
106
|
-
print(
|
|
107
|
-
print(
|
|
104
|
+
print(" - Model context window: 128,000 tokens")
|
|
105
|
+
print(" - Compression triggers at: 96,000 tokens (75%)")
|
|
106
|
+
print(" - Target after compression: 51,200 tokens (40%)\n")
|
|
108
107
|
|
|
109
108
|
# Simulate a conversation that will fill context
|
|
110
109
|
# Each turn will add significant tokens
|
|
@@ -149,8 +148,8 @@ async def test_with_persistent_memory(config: dict):
|
|
|
149
148
|
async def test_without_persistent_memory(config: dict):
|
|
150
149
|
"""Test context compression without persistent memory (warning case)."""
|
|
151
150
|
# Check if we should run this test
|
|
152
|
-
memory_config = config.get(
|
|
153
|
-
persistent_enabled = memory_config.get(
|
|
151
|
+
memory_config = config.get("memory", {})
|
|
152
|
+
persistent_enabled = memory_config.get("persistent_memory", {}).get("enabled", True)
|
|
154
153
|
|
|
155
154
|
if persistent_enabled:
|
|
156
155
|
# Skip if persistent memory is enabled - we already tested that scenario
|
|
@@ -182,9 +181,9 @@ async def test_without_persistent_memory(config: dict):
|
|
|
182
181
|
)
|
|
183
182
|
|
|
184
183
|
print("⚠️ Agent initialized WITHOUT persistent memory")
|
|
185
|
-
print(
|
|
186
|
-
print(
|
|
187
|
-
print(
|
|
184
|
+
print(" - ConversationMemory: Active")
|
|
185
|
+
print(" - PersistentMemory: NONE")
|
|
186
|
+
print(" - This will trigger warning messages when context fills\n")
|
|
188
187
|
|
|
189
188
|
# Shorter test - just trigger compression
|
|
190
189
|
story_prompts = [
|
|
@@ -224,19 +223,19 @@ async def main(config_path: str = None):
|
|
|
224
223
|
config = load_config(config_path)
|
|
225
224
|
|
|
226
225
|
# Show memory configuration
|
|
227
|
-
memory_config = config.get(
|
|
228
|
-
print(
|
|
226
|
+
memory_config = config.get("memory", {})
|
|
227
|
+
print("\n📋 Memory Configuration (from YAML):")
|
|
229
228
|
print(f" - Enabled: {memory_config.get('enabled', True)}")
|
|
230
229
|
print(f" - Conversation Memory: {memory_config.get('conversation_memory', {}).get('enabled', True)}")
|
|
231
230
|
print(f" - Persistent Memory: {memory_config.get('persistent_memory', {}).get('enabled', True)}")
|
|
232
231
|
|
|
233
|
-
if memory_config.get(
|
|
234
|
-
pm_config = memory_config.get(
|
|
232
|
+
if memory_config.get("persistent_memory", {}).get("enabled", True):
|
|
233
|
+
pm_config = memory_config.get("persistent_memory", {})
|
|
235
234
|
print(f" - Agent Name: {pm_config.get('agent_name', 'N/A')}")
|
|
236
235
|
print(f" - Session Name: {pm_config.get('session_name', 'N/A')}")
|
|
237
236
|
print(f" - On Disk: {pm_config.get('on_disk', True)}")
|
|
238
237
|
|
|
239
|
-
compression_config = memory_config.get(
|
|
238
|
+
compression_config = memory_config.get("compression", {})
|
|
240
239
|
print(f" - Compression Trigger: {compression_config.get('trigger_threshold', 0.75)*100}%")
|
|
241
240
|
print(f" - Target After Compression: {compression_config.get('target_ratio', 0.40)*100}%\n")
|
|
242
241
|
|
|
@@ -265,6 +264,7 @@ async def main(config_path: str = None):
|
|
|
265
264
|
except Exception as e:
|
|
266
265
|
print(f"\n\n❌ Test failed with error: {e}")
|
|
267
266
|
import traceback
|
|
267
|
+
|
|
268
268
|
traceback.print_exc()
|
|
269
269
|
|
|
270
270
|
print("\n" + "=" * 70)
|
|
@@ -279,7 +279,7 @@ if __name__ == "__main__":
|
|
|
279
279
|
parser.add_argument(
|
|
280
280
|
"--config",
|
|
281
281
|
type=str,
|
|
282
|
-
help="Path to YAML config file (default: gpt5mini_gemini_context_window_management.yaml)"
|
|
282
|
+
help="Path to YAML config file (default: gpt5mini_gemini_context_window_management.yaml)",
|
|
283
283
|
)
|
|
284
284
|
args = parser.parse_args()
|
|
285
285
|
|