massgen 0.1.4__py3-none-any.whl → 0.1.6__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 (84) hide show
  1. massgen/__init__.py +1 -1
  2. massgen/backend/base_with_custom_tool_and_mcp.py +453 -23
  3. massgen/backend/capabilities.py +39 -0
  4. massgen/backend/chat_completions.py +111 -197
  5. massgen/backend/claude.py +210 -181
  6. massgen/backend/gemini.py +1015 -1559
  7. massgen/backend/grok.py +3 -2
  8. massgen/backend/response.py +160 -220
  9. massgen/chat_agent.py +340 -20
  10. massgen/cli.py +399 -25
  11. massgen/config_builder.py +20 -54
  12. massgen/config_validator.py +931 -0
  13. massgen/configs/README.md +95 -10
  14. massgen/configs/memory/gpt5mini_gemini_baseline_research_to_implementation.yaml +94 -0
  15. massgen/configs/memory/gpt5mini_gemini_context_window_management.yaml +187 -0
  16. massgen/configs/memory/gpt5mini_gemini_research_to_implementation.yaml +127 -0
  17. massgen/configs/memory/gpt5mini_high_reasoning_gemini.yaml +107 -0
  18. massgen/configs/memory/single_agent_compression_test.yaml +64 -0
  19. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_mcp_example.yaml +1 -0
  20. massgen/configs/tools/custom_tools/claude_custom_tool_example_no_path.yaml +1 -1
  21. massgen/configs/tools/custom_tools/claude_custom_tool_with_mcp_example.yaml +1 -0
  22. massgen/configs/tools/custom_tools/computer_use_browser_example.yaml +1 -1
  23. massgen/configs/tools/custom_tools/computer_use_docker_example.yaml +1 -1
  24. massgen/configs/tools/custom_tools/gemini_custom_tool_with_mcp_example.yaml +1 -0
  25. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_mcp_example.yaml +1 -0
  26. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_mcp_example.yaml +1 -0
  27. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_mcp_example.yaml +1 -0
  28. massgen/configs/tools/custom_tools/interop/ag2_and_langgraph_lesson_planner.yaml +65 -0
  29. massgen/configs/tools/custom_tools/interop/ag2_and_openai_assistant_lesson_planner.yaml +65 -0
  30. massgen/configs/tools/custom_tools/interop/ag2_lesson_planner_example.yaml +48 -0
  31. massgen/configs/tools/custom_tools/interop/agentscope_lesson_planner_example.yaml +48 -0
  32. massgen/configs/tools/custom_tools/interop/langgraph_lesson_planner_example.yaml +49 -0
  33. massgen/configs/tools/custom_tools/interop/openai_assistant_lesson_planner_example.yaml +50 -0
  34. massgen/configs/tools/custom_tools/interop/smolagent_lesson_planner_example.yaml +49 -0
  35. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_mcp_example.yaml +1 -0
  36. massgen/configs/tools/custom_tools/two_models_with_tools_example.yaml +44 -0
  37. massgen/formatter/_gemini_formatter.py +61 -15
  38. massgen/memory/README.md +277 -0
  39. massgen/memory/__init__.py +26 -0
  40. massgen/memory/_base.py +193 -0
  41. massgen/memory/_compression.py +237 -0
  42. massgen/memory/_context_monitor.py +211 -0
  43. massgen/memory/_conversation.py +255 -0
  44. massgen/memory/_fact_extraction_prompts.py +333 -0
  45. massgen/memory/_mem0_adapters.py +257 -0
  46. massgen/memory/_persistent.py +687 -0
  47. massgen/memory/docker-compose.qdrant.yml +36 -0
  48. massgen/memory/docs/DESIGN.md +388 -0
  49. massgen/memory/docs/QUICKSTART.md +409 -0
  50. massgen/memory/docs/SUMMARY.md +319 -0
  51. massgen/memory/docs/agent_use_memory.md +408 -0
  52. massgen/memory/docs/orchestrator_use_memory.md +586 -0
  53. massgen/memory/examples.py +237 -0
  54. massgen/orchestrator.py +207 -7
  55. massgen/tests/memory/test_agent_compression.py +174 -0
  56. massgen/tests/memory/test_context_window_management.py +286 -0
  57. massgen/tests/memory/test_force_compression.py +154 -0
  58. massgen/tests/memory/test_simple_compression.py +147 -0
  59. massgen/tests/test_ag2_lesson_planner.py +223 -0
  60. massgen/tests/test_agent_memory.py +534 -0
  61. massgen/tests/test_config_validator.py +1156 -0
  62. massgen/tests/test_conversation_memory.py +382 -0
  63. massgen/tests/test_langgraph_lesson_planner.py +223 -0
  64. massgen/tests/test_orchestrator_memory.py +620 -0
  65. massgen/tests/test_persistent_memory.py +435 -0
  66. massgen/token_manager/token_manager.py +6 -0
  67. massgen/tool/__init__.py +2 -9
  68. massgen/tool/_decorators.py +52 -0
  69. massgen/tool/_extraframework_agents/ag2_lesson_planner_tool.py +251 -0
  70. massgen/tool/_extraframework_agents/agentscope_lesson_planner_tool.py +303 -0
  71. massgen/tool/_extraframework_agents/langgraph_lesson_planner_tool.py +275 -0
  72. massgen/tool/_extraframework_agents/openai_assistant_lesson_planner_tool.py +247 -0
  73. massgen/tool/_extraframework_agents/smolagent_lesson_planner_tool.py +180 -0
  74. massgen/tool/_manager.py +102 -16
  75. massgen/tool/_registered_tool.py +3 -0
  76. massgen/tool/_result.py +3 -0
  77. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/METADATA +138 -77
  78. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/RECORD +82 -37
  79. massgen/backend/gemini_mcp_manager.py +0 -545
  80. massgen/backend/gemini_trackers.py +0 -344
  81. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/WHEEL +0 -0
  82. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/entry_points.txt +0 -0
  83. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/licenses/LICENSE +0 -0
  84. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/top_level.txt +0 -0
@@ -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())
@@ -0,0 +1,286 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Test script for Context Window Management with Memory.
5
+
6
+ This script demonstrates how to configure and test the context window
7
+ management feature with persistent memory integration.
8
+
9
+ Usage:
10
+ python massgen/configs/tools/memory/test_context_window_management.py
11
+
12
+ # Or specify a custom config:
13
+ python massgen/configs/tools/memory/test_context_window_management.py --config path/to/config.yaml
14
+ """
15
+
16
+ import asyncio
17
+ import os
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ # Add parent directory to path for imports
22
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
23
+
24
+ import yaml # noqa: E402
25
+ from dotenv import load_dotenv # noqa: E402
26
+
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
+
31
+ # Load environment variables from .env file
32
+ load_dotenv()
33
+
34
+
35
+ def load_config(config_path: str = None) -> dict:
36
+ """Load configuration from YAML file."""
37
+ if config_path is None:
38
+ # Default to the config in same directory
39
+ config_path = Path(__file__).parent / "gpt5mini_gemini_context_window_management.yaml"
40
+
41
+ with open(config_path, "r") as f:
42
+ return yaml.safe_load(f)
43
+
44
+
45
+ async def test_with_persistent_memory(config: dict):
46
+ """Test context compression with persistent memory enabled."""
47
+ # Check if memory is enabled in config
48
+ memory_config = config.get("memory", {})
49
+ if not memory_config.get("enabled", True):
50
+ print("\n⚠️ Skipping: memory.enabled is false in config")
51
+ return
52
+
53
+ persistent_enabled = memory_config.get("persistent_memory", {}).get("enabled", True)
54
+ if not persistent_enabled:
55
+ print("\n⚠️ Skipping: memory.persistent_memory.enabled is false in config")
56
+ return
57
+
58
+ print("\n" + "=" * 70)
59
+ print("TEST 1: Context Window Management WITH Persistent Memory")
60
+ print("=" * 70 + "\n")
61
+
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)
67
+
68
+ # Create LLM backend for both agent and memory
69
+ llm_backend = ChatCompletionsBackend(
70
+ type="openai",
71
+ model="gpt-4o-mini", # Use smaller model for faster testing
72
+ api_key=os.getenv("OPENAI_API_KEY"),
73
+ )
74
+
75
+ # Create embedding backend for persistent memory
76
+ embedding_backend = ChatCompletionsBackend(
77
+ type="openai",
78
+ model="text-embedding-3-small",
79
+ api_key=os.getenv("OPENAI_API_KEY"),
80
+ )
81
+
82
+ # Initialize memory systems
83
+ conversation_memory = ConversationMemory()
84
+ persistent_memory = PersistentMemory(
85
+ agent_name=agent_name,
86
+ session_name=session_name,
87
+ llm_backend=llm_backend,
88
+ embedding_backend=embedding_backend,
89
+ on_disk=on_disk,
90
+ )
91
+
92
+ # Create agent with memory
93
+ agent = SingleAgent(
94
+ backend=llm_backend,
95
+ agent_id="storyteller",
96
+ system_message="You are a creative storyteller. Create detailed, " "immersive narratives with rich descriptions.",
97
+ conversation_memory=conversation_memory,
98
+ persistent_memory=persistent_memory,
99
+ )
100
+
101
+ print("✅ Agent initialized with memory")
102
+ print(" - ConversationMemory: Active")
103
+ print(f" - PersistentMemory: Active (agent={agent_name}, session={session_name}, on_disk={on_disk})")
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")
107
+
108
+ # Simulate a conversation that will fill context
109
+ # Each turn will add significant tokens
110
+ story_prompts = [
111
+ "Tell me the beginning of a space exploration story. Include details about the ship, crew, and their mission. (Make it 400+ words)",
112
+ "What happens when they encounter their first alien planet? Describe it in vivid detail.",
113
+ "Describe a tense first contact situation with aliens. What do they look like? How do they communicate?",
114
+ "The mission takes an unexpected turn. What crisis occurs and how does the crew respond?",
115
+ "Show me a dramatic action sequence involving the ship's technology and the alien environment.",
116
+ "Reveal a plot twist about one of the crew members or the mission itself.",
117
+ "Continue the story with escalating tension and more discoveries.",
118
+ "How do cultural differences between humans and aliens create conflicts?",
119
+ "Describe a major decision point for the crew captain. What are the stakes?",
120
+ "Bring the story to a climactic moment with high drama.",
121
+ ]
122
+
123
+ turn = 0
124
+ for prompt in story_prompts:
125
+ turn += 1
126
+ print(f"\n--- Turn {turn} ---")
127
+ print(f"User: {prompt}\n")
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"Agent: {response_text[:200]}...")
135
+ print(f" [{len(response_text)} chars in response]")
136
+
137
+ # Check if compression occurred by examining conversation size
138
+ if conversation_memory:
139
+ size = await conversation_memory.size()
140
+ print(f" [Conversation memory: {size} messages]\n")
141
+
142
+ print("\n✅ Test completed!")
143
+ print(" Check the output above for compression logs:")
144
+ print(" - Look for: '📊 Context usage: ...'")
145
+ print(" - Look for: '📦 Compressed N messages into long-term memory'")
146
+
147
+
148
+ async def test_without_persistent_memory(config: dict):
149
+ """Test context compression without persistent memory (warning case)."""
150
+ # Check if we should run this test
151
+ memory_config = config.get("memory", {})
152
+ persistent_enabled = memory_config.get("persistent_memory", {}).get("enabled", True)
153
+
154
+ if persistent_enabled:
155
+ # Skip if persistent memory is enabled - we already tested that scenario
156
+ print("\n⚠️ Skipping Test 2: persistent memory is enabled in config")
157
+ print(" To test without persistent memory, set memory.persistent_memory.enabled: false")
158
+ return
159
+
160
+ print("\n" + "=" * 70)
161
+ print("TEST 2: Context Window Management WITHOUT Persistent Memory")
162
+ print("=" * 70 + "\n")
163
+
164
+ # Create LLM backend
165
+ llm_backend = ChatCompletionsBackend(
166
+ type="openai",
167
+ model="gpt-4o-mini",
168
+ api_key=os.getenv("OPENAI_API_KEY"),
169
+ )
170
+
171
+ # Only conversation memory, NO persistent memory
172
+ conversation_memory = ConversationMemory()
173
+
174
+ # Create agent without persistent memory
175
+ agent = SingleAgent(
176
+ backend=llm_backend,
177
+ agent_id="storyteller_no_persist",
178
+ system_message="You are a creative storyteller.",
179
+ conversation_memory=conversation_memory,
180
+ persistent_memory=None, # No persistent memory!
181
+ )
182
+
183
+ print("⚠️ Agent initialized WITHOUT persistent memory")
184
+ print(" - ConversationMemory: Active")
185
+ print(" - PersistentMemory: NONE")
186
+ print(" - This will trigger warning messages when context fills\n")
187
+
188
+ # Shorter test - just trigger compression
189
+ story_prompts = [
190
+ "Tell me a 500-word science fiction story about time travel.",
191
+ "Continue the story with 500 more words about paradoxes.",
192
+ "Add another 500 words with a plot twist.",
193
+ "Continue with 500 words about the resolution.",
194
+ "Write a 500-word epilogue.",
195
+ ]
196
+
197
+ turn = 0
198
+ for prompt in story_prompts:
199
+ turn += 1
200
+ print(f"\n--- Turn {turn} ---")
201
+ print(f"User: {prompt}\n")
202
+
203
+ response_text = ""
204
+ async for chunk in agent.chat([{"role": "user", "content": prompt}]):
205
+ if chunk.type == "content" and chunk.content:
206
+ response_text += chunk.content
207
+
208
+ print(f"Agent: {response_text[:150]}...")
209
+
210
+ print("\n✅ Test completed!")
211
+ print(" Check the output above for warning messages:")
212
+ print(" - Look for: '⚠️ Warning: Dropping N messages'")
213
+ print(" - Look for: 'No persistent memory configured'")
214
+
215
+
216
+ async def main(config_path: str = None):
217
+ """Run all tests."""
218
+ print("\n" + "=" * 70)
219
+ print("Context Window Management Test Suite")
220
+ print("=" * 70)
221
+
222
+ # Load configuration
223
+ config = load_config(config_path)
224
+
225
+ # Show memory configuration
226
+ memory_config = config.get("memory", {})
227
+ print("\n📋 Memory Configuration (from YAML):")
228
+ print(f" - Enabled: {memory_config.get('enabled', True)}")
229
+ print(f" - Conversation Memory: {memory_config.get('conversation_memory', {}).get('enabled', True)}")
230
+ print(f" - Persistent Memory: {memory_config.get('persistent_memory', {}).get('enabled', True)}")
231
+
232
+ if memory_config.get("persistent_memory", {}).get("enabled", True):
233
+ pm_config = memory_config.get("persistent_memory", {})
234
+ print(f" - Agent Name: {pm_config.get('agent_name', 'N/A')}")
235
+ print(f" - Session Name: {pm_config.get('session_name', 'N/A')}")
236
+ print(f" - On Disk: {pm_config.get('on_disk', True)}")
237
+
238
+ compression_config = memory_config.get("compression", {})
239
+ print(f" - Compression Trigger: {compression_config.get('trigger_threshold', 0.75)*100}%")
240
+ print(f" - Target After Compression: {compression_config.get('target_ratio', 0.40)*100}%\n")
241
+
242
+ # Check for API key
243
+ if not os.getenv("OPENAI_API_KEY"):
244
+ print("\n❌ Error: OPENAI_API_KEY environment variable not set")
245
+ print(" Please set your OpenAI API key:")
246
+ print(" export OPENAI_API_KEY='your-key-here'")
247
+ return
248
+
249
+ try:
250
+ # Test 1: With persistent memory (if enabled)
251
+ await test_with_persistent_memory(config)
252
+
253
+ # Wait between tests
254
+ print("\n" + "-" * 70)
255
+ print("Waiting 5 seconds before next test...")
256
+ print("-" * 70)
257
+ await asyncio.sleep(5)
258
+
259
+ # Test 2: Without persistent memory (if disabled in config)
260
+ await test_without_persistent_memory(config)
261
+
262
+ except KeyboardInterrupt:
263
+ print("\n\n⚠️ Test interrupted by user")
264
+ except Exception as e:
265
+ print(f"\n\n❌ Test failed with error: {e}")
266
+ import traceback
267
+
268
+ traceback.print_exc()
269
+
270
+ print("\n" + "=" * 70)
271
+ print("All tests completed!")
272
+ print("=" * 70 + "\n")
273
+
274
+
275
+ if __name__ == "__main__":
276
+ import argparse
277
+
278
+ parser = argparse.ArgumentParser(description="Test context window management with memory")
279
+ parser.add_argument(
280
+ "--config",
281
+ type=str,
282
+ help="Path to YAML config file (default: gpt5mini_gemini_context_window_management.yaml)",
283
+ )
284
+ args = parser.parse_args()
285
+
286
+ asyncio.run(main(args.config))
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Force Compression Test
5
+
6
+ Directly tests compression by manually adding many messages to trigger it.
7
+ Bypasses LLM calls for faster testing.
8
+
9
+ Usage:
10
+ uv run python massgen/configs/memory/test_force_compression.py
11
+ """
12
+
13
+ import asyncio
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
18
+
19
+ from massgen.memory import ContextCompressor, ConversationMemory # noqa: E402
20
+ from massgen.memory._context_monitor import ContextWindowMonitor # noqa: E402
21
+ from massgen.token_manager.token_manager import TokenCostCalculator # noqa: E402
22
+
23
+
24
+ async def main():
25
+ """Test compression by manually creating a large conversation."""
26
+ print("=" * 80)
27
+ print("Force Compression Test")
28
+ print("=" * 80 + "\n")
29
+
30
+ # Create components
31
+ calculator = TokenCostCalculator()
32
+ conversation_memory = ConversationMemory()
33
+
34
+ # Create monitor with low threshold
35
+ monitor = ContextWindowMonitor(
36
+ model_name="gpt-4o-mini",
37
+ provider="openai",
38
+ trigger_threshold=0.10, # 10% = 12,800 tokens
39
+ target_ratio=0.05, # 5% = 6,400 tokens
40
+ enabled=True,
41
+ )
42
+
43
+ # Create compressor (no persistent memory for this test)
44
+ compressor = ContextCompressor(
45
+ token_calculator=calculator,
46
+ conversation_memory=conversation_memory,
47
+ persistent_memory=None, # Test without it first
48
+ )
49
+
50
+ print("Configuration:")
51
+ print(f" Context window: {monitor.context_window:,} tokens")
52
+ print(f" Trigger at: {int(monitor.context_window * monitor.trigger_threshold):,} tokens ({monitor.trigger_threshold*100:.0f}%)")
53
+ print(f" Target after: {int(monitor.context_window * monitor.target_ratio):,} tokens ({monitor.target_ratio*100:.0f}%)\n")
54
+
55
+ # Manually create a large conversation
56
+ print("Creating large conversation...")
57
+
58
+ messages = [
59
+ {"role": "system", "content": "You are a helpful assistant."},
60
+ ]
61
+
62
+ # Add many long messages to exceed threshold
63
+ long_content = "This is a detailed explanation about Python programming. " * 200 # ~2000 tokens per message
64
+
65
+ for i in range(10):
66
+ messages.append({"role": "user", "content": f"Question {i}: {long_content}"})
67
+ messages.append({"role": "assistant", "content": f"Answer {i}: {long_content}"})
68
+
69
+ # Add to conversation memory
70
+ await conversation_memory.add(messages)
71
+
72
+ message_count = len(messages)
73
+ total_tokens = calculator.estimate_tokens(messages)
74
+
75
+ print("✅ Created conversation:")
76
+ print(f" Messages: {message_count}")
77
+ print(f" Estimated tokens: {total_tokens:,}\n")
78
+
79
+ # Check if we should compress
80
+ usage_info = monitor.log_context_usage(messages, turn_number=1)
81
+
82
+ print("\n📊 Context Analysis:")
83
+ print(f" Current: {usage_info['current_tokens']:,} / {usage_info['max_tokens']:,} tokens")
84
+ print(f" Usage: {usage_info['usage_percent']*100:.1f}%")
85
+ print(f" Should compress: {usage_info['should_compress']}\n")
86
+
87
+ if not usage_info["should_compress"]:
88
+ print("⚠️ Not over threshold yet, adding more messages...\n")
89
+ # Add more messages
90
+ for i in range(10, 20):
91
+ messages.append({"role": "user", "content": f"Question {i}: {long_content}"})
92
+ messages.append({"role": "assistant", "content": f"Answer {i}: {long_content}"})
93
+
94
+ await conversation_memory.add(messages[21:]) # Add new messages
95
+ total_tokens = calculator.estimate_tokens(messages)
96
+ usage_info = monitor.log_context_usage(messages, turn_number=2)
97
+
98
+ print("\n📊 After adding more:")
99
+ print(f" Messages: {len(messages)}")
100
+ print(f" Current: {usage_info['current_tokens']:,} tokens")
101
+ print(f" Usage: {usage_info['usage_percent']*100:.1f}%")
102
+ print(f" Should compress: {usage_info['should_compress']}\n")
103
+
104
+ # Trigger compression
105
+ print("=" * 80)
106
+ print("Triggering Compression...")
107
+ print("=" * 80 + "\n")
108
+
109
+ compression_stats = await compressor.compress_if_needed(
110
+ messages=messages,
111
+ current_tokens=usage_info["current_tokens"],
112
+ target_tokens=usage_info["target_tokens"],
113
+ should_compress=True, # Force it
114
+ )
115
+
116
+ # Show results
117
+ print("\n" + "=" * 80)
118
+ print("Compression Results")
119
+ print("=" * 80 + "\n")
120
+
121
+ if compression_stats:
122
+ print("✅ COMPRESSION OCCURRED!")
123
+ print("\n📦 Stats:")
124
+ print(f" Messages removed: {compression_stats.messages_removed}")
125
+ print(f" Tokens removed: {compression_stats.tokens_removed:,}")
126
+ print(f" Messages kept: {compression_stats.messages_kept}")
127
+ print(f" Tokens kept: {compression_stats.tokens_kept:,}")
128
+
129
+ # Verify conversation memory was updated
130
+ final_messages = await conversation_memory.get_messages()
131
+ print("\n💾 Conversation Memory After Compression:")
132
+ print(f" Messages remaining: {len(final_messages)}")
133
+ print(f" Expected: {compression_stats.messages_kept}")
134
+
135
+ if len(final_messages) == compression_stats.messages_kept:
136
+ print("\n✅ SUCCESS: Conversation memory correctly updated!")
137
+ else:
138
+ print("\n❌ ERROR: Message count mismatch!")
139
+
140
+ # Show compressor overall stats
141
+ comp_stats = compressor.get_stats()
142
+ print("\n📊 Compressor Total Stats:")
143
+ print(f" Total compressions: {comp_stats['total_compressions']}")
144
+ print(f" Total messages removed: {comp_stats['total_messages_removed']}")
145
+ print(f" Total tokens removed: {comp_stats['total_tokens_removed']:,}")
146
+
147
+ else:
148
+ print("❌ No compression occurred")
149
+
150
+ print("\n" + "=" * 80 + "\n")
151
+
152
+
153
+ if __name__ == "__main__":
154
+ asyncio.run(main())