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.

Files changed (90) hide show
  1. massgen/__init__.py +1 -1
  2. massgen/api_params_handler/_chat_completions_api_params_handler.py +4 -0
  3. massgen/api_params_handler/_claude_api_params_handler.py +4 -0
  4. massgen/api_params_handler/_gemini_api_params_handler.py +4 -0
  5. massgen/api_params_handler/_response_api_params_handler.py +4 -0
  6. massgen/backend/base_with_custom_tool_and_mcp.py +25 -5
  7. massgen/backend/docs/permissions_and_context_files.md +2 -2
  8. massgen/backend/response.py +2 -0
  9. massgen/chat_agent.py +340 -20
  10. massgen/cli.py +326 -19
  11. massgen/configs/README.md +92 -41
  12. massgen/configs/memory/gpt5mini_gemini_baseline_research_to_implementation.yaml +94 -0
  13. massgen/configs/memory/gpt5mini_gemini_context_window_management.yaml +187 -0
  14. massgen/configs/memory/gpt5mini_gemini_research_to_implementation.yaml +127 -0
  15. massgen/configs/memory/gpt5mini_high_reasoning_gemini.yaml +107 -0
  16. massgen/configs/memory/single_agent_compression_test.yaml +64 -0
  17. massgen/configs/tools/custom_tools/crawl4ai_example.yaml +55 -0
  18. massgen/configs/tools/custom_tools/multimodal_tools/text_to_file_generation_multi.yaml +61 -0
  19. massgen/configs/tools/custom_tools/multimodal_tools/text_to_file_generation_single.yaml +29 -0
  20. massgen/configs/tools/custom_tools/multimodal_tools/text_to_image_generation_multi.yaml +51 -0
  21. massgen/configs/tools/custom_tools/multimodal_tools/text_to_image_generation_single.yaml +33 -0
  22. massgen/configs/tools/custom_tools/multimodal_tools/text_to_speech_generation_multi.yaml +55 -0
  23. massgen/configs/tools/custom_tools/multimodal_tools/text_to_speech_generation_single.yaml +33 -0
  24. massgen/configs/tools/custom_tools/multimodal_tools/text_to_video_generation_multi.yaml +47 -0
  25. massgen/configs/tools/custom_tools/multimodal_tools/text_to_video_generation_single.yaml +29 -0
  26. massgen/configs/tools/custom_tools/multimodal_tools/understand_audio.yaml +1 -1
  27. massgen/configs/tools/custom_tools/multimodal_tools/understand_file.yaml +1 -1
  28. massgen/configs/tools/custom_tools/multimodal_tools/understand_image.yaml +1 -1
  29. massgen/configs/tools/custom_tools/multimodal_tools/understand_video.yaml +1 -1
  30. massgen/configs/tools/custom_tools/multimodal_tools/youtube_video_analysis.yaml +1 -1
  31. massgen/filesystem_manager/_filesystem_manager.py +1 -0
  32. massgen/filesystem_manager/_path_permission_manager.py +148 -0
  33. massgen/memory/README.md +277 -0
  34. massgen/memory/__init__.py +26 -0
  35. massgen/memory/_base.py +193 -0
  36. massgen/memory/_compression.py +237 -0
  37. massgen/memory/_context_monitor.py +211 -0
  38. massgen/memory/_conversation.py +255 -0
  39. massgen/memory/_fact_extraction_prompts.py +333 -0
  40. massgen/memory/_mem0_adapters.py +257 -0
  41. massgen/memory/_persistent.py +687 -0
  42. massgen/memory/docker-compose.qdrant.yml +36 -0
  43. massgen/memory/docs/DESIGN.md +388 -0
  44. massgen/memory/docs/QUICKSTART.md +409 -0
  45. massgen/memory/docs/SUMMARY.md +319 -0
  46. massgen/memory/docs/agent_use_memory.md +408 -0
  47. massgen/memory/docs/orchestrator_use_memory.md +586 -0
  48. massgen/memory/examples.py +237 -0
  49. massgen/message_templates.py +160 -12
  50. massgen/orchestrator.py +223 -7
  51. massgen/tests/memory/test_agent_compression.py +174 -0
  52. massgen/{configs/tools → tests}/memory/test_context_window_management.py +30 -30
  53. massgen/tests/memory/test_force_compression.py +154 -0
  54. massgen/tests/memory/test_simple_compression.py +147 -0
  55. massgen/tests/test_agent_memory.py +534 -0
  56. massgen/tests/test_binary_file_blocking.py +274 -0
  57. massgen/tests/test_case_studies.md +12 -12
  58. massgen/tests/test_conversation_memory.py +382 -0
  59. massgen/tests/test_multimodal_size_limits.py +407 -0
  60. massgen/tests/test_orchestrator_memory.py +620 -0
  61. massgen/tests/test_persistent_memory.py +435 -0
  62. massgen/token_manager/token_manager.py +6 -0
  63. massgen/tool/_manager.py +7 -2
  64. massgen/tool/_multimodal_tools/image_to_image_generation.py +293 -0
  65. massgen/tool/_multimodal_tools/text_to_file_generation.py +455 -0
  66. massgen/tool/_multimodal_tools/text_to_image_generation.py +222 -0
  67. massgen/tool/_multimodal_tools/text_to_speech_continue_generation.py +226 -0
  68. massgen/tool/_multimodal_tools/text_to_speech_transcription_generation.py +217 -0
  69. massgen/tool/_multimodal_tools/text_to_video_generation.py +223 -0
  70. massgen/tool/_multimodal_tools/understand_audio.py +19 -1
  71. massgen/tool/_multimodal_tools/understand_file.py +6 -1
  72. massgen/tool/_multimodal_tools/understand_image.py +112 -8
  73. massgen/tool/_multimodal_tools/understand_video.py +32 -5
  74. massgen/tool/_web_tools/crawl4ai_tool.py +718 -0
  75. massgen/tool/docs/multimodal_tools.md +589 -0
  76. massgen/tools/__init__.py +8 -0
  77. massgen/tools/_planning_mcp_server.py +520 -0
  78. massgen/tools/planning_dataclasses.py +434 -0
  79. {massgen-0.1.3.dist-info → massgen-0.1.5.dist-info}/METADATA +142 -82
  80. {massgen-0.1.3.dist-info → massgen-0.1.5.dist-info}/RECORD +84 -41
  81. massgen/configs/tools/custom_tools/crawl4ai_mcp_example.yaml +0 -67
  82. massgen/configs/tools/custom_tools/crawl4ai_multi_agent_example.yaml +0 -68
  83. massgen/configs/tools/memory/README.md +0 -199
  84. massgen/configs/tools/memory/gpt5mini_gemini_context_window_management.yaml +0 -131
  85. massgen/configs/tools/memory/gpt5mini_gemini_no_persistent_memory.yaml +0 -133
  86. massgen/configs/tools/multimodal/gpt5mini_gpt5nano_documentation_evolution.yaml +0 -97
  87. {massgen-0.1.3.dist-info → massgen-0.1.5.dist-info}/WHEEL +0 -0
  88. {massgen-0.1.3.dist-info → massgen-0.1.5.dist-info}/entry_points.txt +0 -0
  89. {massgen-0.1.3.dist-info → massgen-0.1.5.dist-info}/licenses/LICENSE +0 -0
  90. {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
- chat_stream = agent.chat(conversation_messages, self.workflow_tools, reset_chat=True, current_stage=CoordinationStage.INITIAL_ANSWER)
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(enforcement_msg, self.workflow_tools, reset_chat=False, current_stage=CoordinationStage.ENFORCEMENT)
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([enforcement_message], self.workflow_tools, reset_chat=False, current_stage=CoordinationStage.ENFORCEMENT)
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(presentation_messages, reset_chat=True, current_stage=CoordinationStage.PRESENTATION):
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(messages=evaluation_messages, tools=post_eval_tools, reset_chat=True, current_stage=CoordinationStage.POST_EVALUATION):
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, and workspace_path, or None if not available
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, 'r') as f:
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('memory', {})
49
- if not memory_config.get('enabled', True):
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('persistent_memory', {}).get('enabled', True)
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('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)
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(f" - ConversationMemory: Active")
102
+ print(" - ConversationMemory: Active")
104
103
  print(f" - PersistentMemory: Active (agent={agent_name}, session={session_name}, on_disk={on_disk})")
105
- print(f" - Model context window: 128,000 tokens")
106
- print(f" - Compression triggers at: 96,000 tokens (75%)")
107
- print(f" - Target after compression: 51,200 tokens (40%)\n")
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('memory', {})
153
- persistent_enabled = memory_config.get('persistent_memory', {}).get('enabled', True)
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(f" - ConversationMemory: Active")
186
- print(f" - PersistentMemory: NONE")
187
- print(f" - This will trigger warning messages when context fills\n")
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('memory', {})
228
- print(f"\n📋 Memory Configuration (from YAML):")
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('persistent_memory', {}).get('enabled', True):
234
- pm_config = memory_config.get('persistent_memory', {})
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('compression', {})
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