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,147 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Simple Compression Test (No Persistent Memory)
5
+
6
+ Tests core compression logic by:
7
+ 1. Creating an agent with only conversation_memory (no persistent_memory)
8
+ 2. Adding many long messages to trigger compression
9
+ 3. Verifying old messages are removed
10
+
11
+ Usage:
12
+ uv run python massgen/configs/memory/test_simple_compression.py
13
+ """
14
+
15
+ import asyncio
16
+ import os
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
21
+
22
+ from dotenv import load_dotenv # noqa: E402
23
+
24
+ from massgen.backend.chat_completions import ChatCompletionsBackend # noqa: E402
25
+ from massgen.chat_agent import SingleAgent # noqa: E402
26
+ from massgen.memory import ConversationMemory # noqa: E402
27
+ from massgen.memory._context_monitor import ContextWindowMonitor # noqa: E402
28
+
29
+ load_dotenv()
30
+
31
+
32
+ async def main():
33
+ """Test compression without persistent memory."""
34
+ print("=" * 80)
35
+ print("Simple Compression Test (Conversation Memory Only)")
36
+ print("=" * 80 + "\n")
37
+
38
+ # Check API key
39
+ if not os.getenv("OPENAI_API_KEY"):
40
+ print("❌ Error: OPENAI_API_KEY not set")
41
+ return
42
+
43
+ # Configuration - set very low thresholds to trigger quickly
44
+ model_name = "gpt-4o-mini"
45
+ provider = "openai"
46
+ trigger_threshold = 0.03 # Trigger at 3% (very low for testing)
47
+ target_ratio = 0.01 # Keep only 1% after compression
48
+
49
+ print("Configuration:")
50
+ print(f" Model: {model_name}")
51
+ print(" Context window: 128,000 tokens")
52
+ print(f" Trigger at: {int(128000 * trigger_threshold):,} tokens (3%)")
53
+ print(f" Target after: {int(128000 * target_ratio):,} tokens (1%)\n")
54
+
55
+ # 1. Create backend
56
+ backend = ChatCompletionsBackend(
57
+ type=provider,
58
+ model=model_name,
59
+ api_key=os.getenv("OPENAI_API_KEY"),
60
+ )
61
+
62
+ # 2. Create conversation memory ONLY (no persistent memory)
63
+ conversation_memory = ConversationMemory()
64
+ print("✅ Conversation memory created (NO persistent memory)")
65
+
66
+ # 3. Create context monitor
67
+ monitor = ContextWindowMonitor(
68
+ model_name=model_name,
69
+ provider=provider,
70
+ trigger_threshold=trigger_threshold,
71
+ target_ratio=target_ratio,
72
+ enabled=True,
73
+ )
74
+ print("✅ Monitor created\n")
75
+
76
+ # 4. Create agent (no persistent_memory!)
77
+ agent = SingleAgent(
78
+ backend=backend,
79
+ agent_id="test_agent",
80
+ system_message="You are a helpful assistant.",
81
+ conversation_memory=conversation_memory,
82
+ persistent_memory=None, # Explicitly None
83
+ context_monitor=monitor,
84
+ )
85
+
86
+ # Verify compressor was created
87
+ if agent.context_compressor:
88
+ print("✅ Context compressor created!")
89
+ print(f" Persistent memory: {agent.context_compressor.persistent_memory is not None}\n")
90
+ else:
91
+ print("❌ Context compressor NOT created (need both monitor + conversation_memory)\n")
92
+ return
93
+
94
+ # 5. Run one turn with a complex question
95
+ print("=" * 80)
96
+ print("Running conversation to trigger compression...")
97
+ print("=" * 80 + "\n")
98
+
99
+ prompt = """Explain in extreme detail:
100
+ 1. How Python's garbage collection works
101
+ 2. The Global Interpreter Lock (GIL)
102
+ 3. Python's asyncio event loop
103
+ 4. The descriptor protocol
104
+ 5. Metaclasses and the type system
105
+
106
+ Provide comprehensive explanations with code examples for each topic."""
107
+
108
+ print("Sending complex prompt to generate long response...\n")
109
+
110
+ response_text = ""
111
+ async for chunk in agent.chat([{"role": "user", "content": prompt}]):
112
+ if chunk.type == "content" and chunk.content:
113
+ response_text += chunk.content
114
+ print(".", end="", flush=True)
115
+
116
+ print(f"\n\nResponse generated: {len(response_text):,} characters\n")
117
+
118
+ # 6. Check results
119
+ print("=" * 80)
120
+ print("Results")
121
+ print("=" * 80 + "\n")
122
+
123
+ final_messages = await conversation_memory.get_messages()
124
+ print(f"Messages in conversation_memory: {len(final_messages)}")
125
+
126
+ stats = monitor.get_stats()
127
+ print("\n📊 Monitor Stats:")
128
+ print(f" Turns: {stats['turn_count']}")
129
+ print(f" Total tokens: {stats['total_tokens']:,}")
130
+
131
+ comp_stats = agent.context_compressor.get_stats()
132
+ print("\n📦 Compression Stats:")
133
+ print(f" Compressions: {comp_stats['total_compressions']}")
134
+ print(f" Messages removed: {comp_stats['total_messages_removed']}")
135
+ print(f" Tokens removed: {comp_stats['total_tokens_removed']:,}")
136
+
137
+ print("\n" + "=" * 80)
138
+ if comp_stats["total_compressions"] > 0:
139
+ print("✅ SUCCESS: Compression triggered and removed messages!")
140
+ else:
141
+ print("⚠️ No compression - response may not have been long enough")
142
+ print(f" (Needed {int(128000 * trigger_threshold):,} tokens to trigger)")
143
+ print("=" * 80 + "\n")
144
+
145
+
146
+ if __name__ == "__main__":
147
+ asyncio.run(main())
@@ -0,0 +1,223 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Test AG2 (AutoGen) Lesson Planner Tool
4
+ Tests the interoperability feature where AutoGen nested chat is wrapped as a MassGen custom tool.
5
+ """
6
+
7
+ import asyncio
8
+ import os
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ import pytest
13
+
14
+ # Add parent directory to path for imports
15
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
16
+
17
+ from massgen.tool._extraframework_agents.ag2_lesson_planner_tool import ( # noqa: E402
18
+ ag2_lesson_planner,
19
+ )
20
+ from massgen.tool._result import ExecutionResult # noqa: E402
21
+
22
+
23
+ class TestAG2LessonPlannerTool:
24
+ """Test AG2 Lesson Planner Tool functionality."""
25
+
26
+ @pytest.mark.asyncio
27
+ async def test_basic_lesson_plan_creation(self):
28
+ """Test basic lesson plan creation with a simple topic."""
29
+ # Skip if no API key
30
+ api_key = os.getenv("OPENAI_API_KEY")
31
+ if not api_key:
32
+ pytest.skip("OPENAI_API_KEY not set")
33
+
34
+ # Test with a simple topic
35
+ result = await ag2_lesson_planner(topic="photosynthesis", api_key=api_key)
36
+
37
+ # Verify result structure
38
+ assert isinstance(result, ExecutionResult)
39
+ assert len(result.output_blocks) > 0
40
+ # Check that the result doesn't contain an error
41
+ assert not result.output_blocks[0].data.startswith("Error:")
42
+
43
+ # Verify lesson plan contains expected elements
44
+ lesson_plan = result.output_blocks[0].data
45
+ assert "photosynthesis" in lesson_plan.lower()
46
+
47
+ @pytest.mark.asyncio
48
+ async def test_lesson_plan_with_env_api_key(self):
49
+ """Test lesson plan creation using environment variable for API key."""
50
+ # Skip if no API key
51
+ if not os.getenv("OPENAI_API_KEY"):
52
+ pytest.skip("OPENAI_API_KEY not set")
53
+
54
+ # Test without passing api_key parameter (should use env var)
55
+ result = await ag2_lesson_planner(topic="fractions")
56
+
57
+ assert isinstance(result, ExecutionResult)
58
+ assert len(result.output_blocks) > 0
59
+ # Check that the result doesn't contain an error
60
+ assert not result.output_blocks[0].data.startswith("Error:")
61
+
62
+ @pytest.mark.asyncio
63
+ async def test_missing_api_key_error(self):
64
+ """Test error handling when API key is missing."""
65
+ # Temporarily save and remove env var
66
+ original_key = os.environ.get("OPENAI_API_KEY")
67
+ if "OPENAI_API_KEY" in os.environ:
68
+ del os.environ["OPENAI_API_KEY"]
69
+
70
+ try:
71
+ result = await ag2_lesson_planner(topic="test topic")
72
+
73
+ # Should return error result
74
+ assert isinstance(result, ExecutionResult)
75
+ assert result.output_blocks[0].data.startswith("Error:")
76
+ assert "OPENAI_API_KEY not found" in result.output_blocks[0].data
77
+ finally:
78
+ # Restore env var
79
+ if original_key:
80
+ os.environ["OPENAI_API_KEY"] = original_key
81
+
82
+ @pytest.mark.asyncio
83
+ async def test_different_topics(self):
84
+ """Test lesson plan creation with different topics."""
85
+ # Skip if no API key
86
+ api_key = os.getenv("OPENAI_API_KEY")
87
+ if not api_key:
88
+ pytest.skip("OPENAI_API_KEY not set")
89
+
90
+ topics = ["addition", "animals", "water cycle"]
91
+
92
+ for topic in topics:
93
+ result = await ag2_lesson_planner(topic=topic, api_key=api_key)
94
+
95
+ assert isinstance(result, ExecutionResult)
96
+ assert len(result.output_blocks) > 0
97
+ # Check that the result doesn't contain an error
98
+ assert not result.output_blocks[0].data.startswith("Error:")
99
+ assert topic.lower() in result.output_blocks[0].data.lower()
100
+
101
+ @pytest.mark.asyncio
102
+ async def test_concurrent_lesson_plan_creation(self):
103
+ """Test creating multiple lesson plans concurrently."""
104
+ # Skip if no API key
105
+ api_key = os.getenv("OPENAI_API_KEY")
106
+ if not api_key:
107
+ pytest.skip("OPENAI_API_KEY not set")
108
+
109
+ topics = ["math", "science", "reading"]
110
+
111
+ # Create tasks for concurrent execution
112
+ tasks = [ag2_lesson_planner(topic=topic, api_key=api_key) for topic in topics]
113
+
114
+ # Execute concurrently
115
+ results = await asyncio.gather(*tasks)
116
+
117
+ # Verify all results
118
+ assert len(results) == len(topics)
119
+ for i, result in enumerate(results):
120
+ assert isinstance(result, ExecutionResult)
121
+ assert len(result.output_blocks) > 0
122
+ # Check that the result doesn't contain an error
123
+ assert not result.output_blocks[0].data.startswith("Error:")
124
+ assert topics[i].lower() in result.output_blocks[0].data.lower()
125
+
126
+
127
+ class TestAG2ToolIntegration:
128
+ """Test AG2 tool integration with MassGen tool system."""
129
+
130
+ def test_tool_function_signature(self):
131
+ """Test that the tool has the correct async signature."""
132
+ import inspect
133
+
134
+ assert inspect.iscoroutinefunction(ag2_lesson_planner)
135
+
136
+ # Get function signature
137
+ sig = inspect.signature(ag2_lesson_planner)
138
+ params = sig.parameters
139
+
140
+ # Verify parameters
141
+ assert "topic" in params
142
+ assert "api_key" in params
143
+
144
+ # Verify return annotation
145
+ assert sig.return_annotation == ExecutionResult
146
+
147
+ @pytest.mark.asyncio
148
+ async def test_execution_result_structure(self):
149
+ """Test that the returned ExecutionResult has the correct structure."""
150
+ # Skip if no API key
151
+ api_key = os.getenv("OPENAI_API_KEY")
152
+ if not api_key:
153
+ pytest.skip("OPENAI_API_KEY not set")
154
+
155
+ result = await ag2_lesson_planner(topic="test", api_key=api_key)
156
+
157
+ # Verify ExecutionResult structure
158
+ assert hasattr(result, "output_blocks")
159
+ assert isinstance(result.output_blocks, list)
160
+ assert len(result.output_blocks) > 0
161
+ # Check that the result doesn't contain an error
162
+ assert not result.output_blocks[0].data.startswith("Error:")
163
+
164
+ # Verify TextContent structure
165
+ from massgen.tool._result import TextContent
166
+
167
+ assert isinstance(result.output_blocks[0], TextContent)
168
+ assert hasattr(result.output_blocks[0], "data")
169
+ assert isinstance(result.output_blocks[0].data, str)
170
+
171
+
172
+ class TestAG2ToolWithBackend:
173
+ """Test AG2 tool with ResponseBackend."""
174
+
175
+ @pytest.mark.asyncio
176
+ async def test_backend_registration(self):
177
+ """Test registering AG2 tool with ResponseBackend."""
178
+ from massgen.backend.response import ResponseBackend
179
+
180
+ api_key = os.getenv("OPENAI_API_KEY", "test-key")
181
+
182
+ # Import the tool
183
+ from massgen.tool._extraframework_agents.ag2_lesson_planner_tool import (
184
+ ag2_lesson_planner,
185
+ )
186
+
187
+ # Register with backend
188
+ backend = ResponseBackend(
189
+ api_key=api_key,
190
+ custom_tools=[
191
+ {
192
+ "func": ag2_lesson_planner,
193
+ "description": "Create a comprehensive lesson plan using AG2 nested chat",
194
+ },
195
+ ],
196
+ )
197
+
198
+ # Verify tool is registered
199
+ assert "ag2_lesson_planner" in backend._custom_tool_names
200
+
201
+ # Verify schema generation
202
+ schemas = backend._get_custom_tools_schemas()
203
+ assert len(schemas) >= 1
204
+
205
+ # Find our tool's schema
206
+ ag2_schema = None
207
+ for schema in schemas:
208
+ if schema["function"]["name"] == "ag2_lesson_planner":
209
+ ag2_schema = schema
210
+ break
211
+
212
+ assert ag2_schema is not None
213
+ assert ag2_schema["type"] == "function"
214
+ assert "parameters" in ag2_schema["function"]
215
+
216
+
217
+ # ============================================================================
218
+ # Run tests
219
+ # ============================================================================
220
+
221
+ if __name__ == "__main__":
222
+ # Run pytest
223
+ pytest.main([__file__, "-v"])