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.
- massgen/__init__.py +1 -1
- massgen/backend/base_with_custom_tool_and_mcp.py +453 -23
- massgen/backend/capabilities.py +39 -0
- massgen/backend/chat_completions.py +111 -197
- massgen/backend/claude.py +210 -181
- massgen/backend/gemini.py +1015 -1559
- massgen/backend/grok.py +3 -2
- massgen/backend/response.py +160 -220
- massgen/chat_agent.py +340 -20
- massgen/cli.py +399 -25
- massgen/config_builder.py +20 -54
- massgen/config_validator.py +931 -0
- massgen/configs/README.md +95 -10
- 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/claude_code_custom_tool_with_mcp_example.yaml +1 -0
- massgen/configs/tools/custom_tools/claude_custom_tool_example_no_path.yaml +1 -1
- massgen/configs/tools/custom_tools/claude_custom_tool_with_mcp_example.yaml +1 -0
- massgen/configs/tools/custom_tools/computer_use_browser_example.yaml +1 -1
- massgen/configs/tools/custom_tools/computer_use_docker_example.yaml +1 -1
- massgen/configs/tools/custom_tools/gemini_custom_tool_with_mcp_example.yaml +1 -0
- massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_mcp_example.yaml +1 -0
- massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_mcp_example.yaml +1 -0
- massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_mcp_example.yaml +1 -0
- massgen/configs/tools/custom_tools/interop/ag2_and_langgraph_lesson_planner.yaml +65 -0
- massgen/configs/tools/custom_tools/interop/ag2_and_openai_assistant_lesson_planner.yaml +65 -0
- massgen/configs/tools/custom_tools/interop/ag2_lesson_planner_example.yaml +48 -0
- massgen/configs/tools/custom_tools/interop/agentscope_lesson_planner_example.yaml +48 -0
- massgen/configs/tools/custom_tools/interop/langgraph_lesson_planner_example.yaml +49 -0
- massgen/configs/tools/custom_tools/interop/openai_assistant_lesson_planner_example.yaml +50 -0
- massgen/configs/tools/custom_tools/interop/smolagent_lesson_planner_example.yaml +49 -0
- massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_mcp_example.yaml +1 -0
- massgen/configs/tools/custom_tools/two_models_with_tools_example.yaml +44 -0
- massgen/formatter/_gemini_formatter.py +61 -15
- 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/orchestrator.py +207 -7
- massgen/tests/memory/test_agent_compression.py +174 -0
- massgen/tests/memory/test_context_window_management.py +286 -0
- massgen/tests/memory/test_force_compression.py +154 -0
- massgen/tests/memory/test_simple_compression.py +147 -0
- massgen/tests/test_ag2_lesson_planner.py +223 -0
- massgen/tests/test_agent_memory.py +534 -0
- massgen/tests/test_config_validator.py +1156 -0
- massgen/tests/test_conversation_memory.py +382 -0
- massgen/tests/test_langgraph_lesson_planner.py +223 -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/__init__.py +2 -9
- massgen/tool/_decorators.py +52 -0
- massgen/tool/_extraframework_agents/ag2_lesson_planner_tool.py +251 -0
- massgen/tool/_extraframework_agents/agentscope_lesson_planner_tool.py +303 -0
- massgen/tool/_extraframework_agents/langgraph_lesson_planner_tool.py +275 -0
- massgen/tool/_extraframework_agents/openai_assistant_lesson_planner_tool.py +247 -0
- massgen/tool/_extraframework_agents/smolagent_lesson_planner_tool.py +180 -0
- massgen/tool/_manager.py +102 -16
- massgen/tool/_registered_tool.py +3 -0
- massgen/tool/_result.py +3 -0
- {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/METADATA +138 -77
- {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/RECORD +82 -37
- massgen/backend/gemini_mcp_manager.py +0 -545
- massgen/backend/gemini_trackers.py +0 -344
- {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/WHEEL +0 -0
- {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/entry_points.txt +0 -0
- {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/licenses/LICENSE +0 -0
- {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Tests for PersistentMemory implementation.
|
|
5
|
+
|
|
6
|
+
This module tests the long-term persistent memory functionality using mem0,
|
|
7
|
+
including recording, retrieving, and managing memories across sessions.
|
|
8
|
+
|
|
9
|
+
Note: Some tests require mem0ai to be installed and may be skipped if unavailable.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
# Try to import memory components
|
|
17
|
+
try:
|
|
18
|
+
from massgen.memory import PersistentMemory
|
|
19
|
+
|
|
20
|
+
MEMORY_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
MEMORY_AVAILABLE = False
|
|
23
|
+
PersistentMemory = None
|
|
24
|
+
|
|
25
|
+
# Check if mem0 is available
|
|
26
|
+
try:
|
|
27
|
+
MEM0_AVAILABLE = True
|
|
28
|
+
except ImportError:
|
|
29
|
+
MEM0_AVAILABLE = False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Helper function to create mock backend
|
|
33
|
+
def create_mock_backend():
|
|
34
|
+
"""Create a mock backend for testing."""
|
|
35
|
+
backend = MagicMock()
|
|
36
|
+
backend.chat_completion = AsyncMock(
|
|
37
|
+
return_value={
|
|
38
|
+
"choices": [{"message": {"content": "Test response"}}],
|
|
39
|
+
},
|
|
40
|
+
)
|
|
41
|
+
return backend
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.mark.skipif(not MEMORY_AVAILABLE, reason="Memory module not available")
|
|
45
|
+
class TestPersistentMemoryInitialization:
|
|
46
|
+
"""Tests for PersistentMemory initialization."""
|
|
47
|
+
|
|
48
|
+
@pytest.mark.skipif(not MEM0_AVAILABLE, reason="mem0 not installed")
|
|
49
|
+
def test_initialization_without_identifiers_fails(self):
|
|
50
|
+
"""Test that initialization fails without agent/user/session identifiers."""
|
|
51
|
+
with pytest.raises(ValueError, match="At least one of"):
|
|
52
|
+
PersistentMemory(
|
|
53
|
+
llm_backend=create_mock_backend(),
|
|
54
|
+
embedding_backend=create_mock_backend(),
|
|
55
|
+
)
|
|
56
|
+
print("✅ Initialization validation works")
|
|
57
|
+
|
|
58
|
+
@pytest.mark.skipif(not MEM0_AVAILABLE, reason="mem0 not installed")
|
|
59
|
+
def test_initialization_without_backends_fails(self):
|
|
60
|
+
"""Test that initialization fails without required backends."""
|
|
61
|
+
with pytest.raises(ValueError, match="Both llm_backend and embedding_backend"):
|
|
62
|
+
PersistentMemory(agent_name="test_agent")
|
|
63
|
+
print("✅ Backend validation works")
|
|
64
|
+
|
|
65
|
+
@pytest.mark.skipif(not MEM0_AVAILABLE, reason="mem0 not installed")
|
|
66
|
+
def test_initialization_with_agent_name(self):
|
|
67
|
+
"""Test successful initialization with agent name."""
|
|
68
|
+
memory = PersistentMemory(
|
|
69
|
+
agent_name="test_agent",
|
|
70
|
+
llm_backend=create_mock_backend(),
|
|
71
|
+
embedding_backend=create_mock_backend(),
|
|
72
|
+
)
|
|
73
|
+
assert memory.agent_id == "test_agent"
|
|
74
|
+
assert memory.user_id is None
|
|
75
|
+
assert memory.session_id is None
|
|
76
|
+
print("✅ Initialization with agent_name works")
|
|
77
|
+
|
|
78
|
+
@pytest.mark.skipif(not MEM0_AVAILABLE, reason="mem0 not installed")
|
|
79
|
+
def test_initialization_with_all_identifiers(self):
|
|
80
|
+
"""Test initialization with all identifiers."""
|
|
81
|
+
memory = PersistentMemory(
|
|
82
|
+
agent_name="test_agent",
|
|
83
|
+
user_name="test_user",
|
|
84
|
+
session_name="test_session",
|
|
85
|
+
llm_backend=create_mock_backend(),
|
|
86
|
+
embedding_backend=create_mock_backend(),
|
|
87
|
+
)
|
|
88
|
+
assert memory.agent_id == "test_agent"
|
|
89
|
+
assert memory.user_id == "test_user"
|
|
90
|
+
assert memory.session_id == "test_session"
|
|
91
|
+
print("✅ Initialization with all identifiers works")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@pytest.mark.skipif(
|
|
95
|
+
not MEMORY_AVAILABLE or not MEM0_AVAILABLE,
|
|
96
|
+
reason="Memory module or mem0 not available",
|
|
97
|
+
)
|
|
98
|
+
class TestPersistentMemoryMocked:
|
|
99
|
+
"""Tests for PersistentMemory with mocked mem0 backend."""
|
|
100
|
+
|
|
101
|
+
@pytest.fixture
|
|
102
|
+
def mock_memory(self):
|
|
103
|
+
"""Create a PersistentMemory instance with mocked mem0."""
|
|
104
|
+
with patch("mem0.AsyncMemory") as mock_mem0:
|
|
105
|
+
# Configure mock
|
|
106
|
+
mock_mem0_instance = AsyncMock()
|
|
107
|
+
mock_mem0.return_value = mock_mem0_instance
|
|
108
|
+
|
|
109
|
+
memory = PersistentMemory(
|
|
110
|
+
agent_name="test_agent",
|
|
111
|
+
llm_backend=create_mock_backend(),
|
|
112
|
+
embedding_backend=create_mock_backend(),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Replace with our mock
|
|
116
|
+
memory.mem0_memory = mock_mem0_instance
|
|
117
|
+
|
|
118
|
+
yield memory, mock_mem0_instance
|
|
119
|
+
|
|
120
|
+
@pytest.mark.asyncio
|
|
121
|
+
async def test_record_messages(self, mock_memory):
|
|
122
|
+
"""Test recording messages to persistent memory."""
|
|
123
|
+
memory, mock_mem0 = mock_memory
|
|
124
|
+
|
|
125
|
+
# Mock the add method
|
|
126
|
+
mock_mem0.add = AsyncMock(return_value={"results": ["mem_1", "mem_2"]})
|
|
127
|
+
|
|
128
|
+
messages = [
|
|
129
|
+
{"role": "user", "content": "What is quantum computing?"},
|
|
130
|
+
{"role": "assistant", "content": "Quantum computing uses qubits..."},
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
await memory.record(messages)
|
|
134
|
+
|
|
135
|
+
# Verify mem0.add was called
|
|
136
|
+
assert mock_mem0.add.called
|
|
137
|
+
call_kwargs = mock_mem0.add.call_args.kwargs
|
|
138
|
+
assert call_kwargs["agent_id"] == "test_agent"
|
|
139
|
+
print("✅ Recording messages works")
|
|
140
|
+
|
|
141
|
+
@pytest.mark.asyncio
|
|
142
|
+
async def test_retrieve_memories(self, mock_memory):
|
|
143
|
+
"""Test retrieving memories based on query."""
|
|
144
|
+
memory, mock_mem0 = mock_memory
|
|
145
|
+
|
|
146
|
+
# Mock search results
|
|
147
|
+
mock_mem0.search = AsyncMock(
|
|
148
|
+
return_value={
|
|
149
|
+
"results": [
|
|
150
|
+
{"memory": "Quantum computing uses qubits"},
|
|
151
|
+
{"memory": "Qubits can be in superposition"},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
result = await memory.retrieve("quantum computing")
|
|
157
|
+
|
|
158
|
+
assert "Quantum computing uses qubits" in result
|
|
159
|
+
assert "Qubits can be in superposition" in result
|
|
160
|
+
assert mock_mem0.search.called
|
|
161
|
+
print("✅ Retrieving memories works")
|
|
162
|
+
|
|
163
|
+
@pytest.mark.asyncio
|
|
164
|
+
async def test_retrieve_with_message_dict(self, mock_memory):
|
|
165
|
+
"""Test retrieving with message dictionary."""
|
|
166
|
+
memory, mock_mem0 = mock_memory
|
|
167
|
+
|
|
168
|
+
mock_mem0.search = AsyncMock(
|
|
169
|
+
return_value={
|
|
170
|
+
"results": [{"memory": "Relevant information"}],
|
|
171
|
+
},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
query = {"role": "user", "content": "Tell me about AI"}
|
|
175
|
+
result = await memory.retrieve(query)
|
|
176
|
+
|
|
177
|
+
assert "Relevant information" in result
|
|
178
|
+
print("✅ Retrieving with message dict works")
|
|
179
|
+
|
|
180
|
+
@pytest.mark.asyncio
|
|
181
|
+
async def test_retrieve_with_message_list(self, mock_memory):
|
|
182
|
+
"""Test retrieving with list of messages."""
|
|
183
|
+
memory, mock_mem0 = mock_memory
|
|
184
|
+
|
|
185
|
+
mock_mem0.search = AsyncMock(
|
|
186
|
+
return_value={
|
|
187
|
+
"results": [{"memory": "AI information"}],
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
queries = [
|
|
192
|
+
{"role": "user", "content": "What is AI?"},
|
|
193
|
+
{"role": "user", "content": "How does it work?"},
|
|
194
|
+
]
|
|
195
|
+
await memory.retrieve(queries)
|
|
196
|
+
|
|
197
|
+
assert mock_mem0.search.call_count == 2
|
|
198
|
+
print("✅ Retrieving with message list works")
|
|
199
|
+
|
|
200
|
+
@pytest.mark.asyncio
|
|
201
|
+
async def test_save_to_memory_tool(self, mock_memory):
|
|
202
|
+
"""Test the save_to_memory agent tool."""
|
|
203
|
+
memory, mock_mem0 = mock_memory
|
|
204
|
+
|
|
205
|
+
mock_mem0.add = AsyncMock(return_value={"results": ["mem_123"]})
|
|
206
|
+
|
|
207
|
+
result = await memory.save_to_memory(
|
|
208
|
+
thinking="User mentioned their birthday",
|
|
209
|
+
content=["User's birthday is March 15"],
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
assert result["success"] is True
|
|
213
|
+
assert "Successfully saved" in result["message"]
|
|
214
|
+
assert "mem_123" in result["memory_ids"]
|
|
215
|
+
print("✅ save_to_memory tool works")
|
|
216
|
+
|
|
217
|
+
@pytest.mark.asyncio
|
|
218
|
+
async def test_save_to_memory_error_handling(self, mock_memory):
|
|
219
|
+
"""Test save_to_memory error handling."""
|
|
220
|
+
memory, mock_mem0 = mock_memory
|
|
221
|
+
|
|
222
|
+
# Mock an error
|
|
223
|
+
mock_mem0.add = AsyncMock(side_effect=Exception("Database error"))
|
|
224
|
+
|
|
225
|
+
result = await memory.save_to_memory(
|
|
226
|
+
thinking="Test thinking",
|
|
227
|
+
content=["Test content"],
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
assert result["success"] is False
|
|
231
|
+
assert "Error saving to memory" in result["message"]
|
|
232
|
+
print("✅ save_to_memory error handling works")
|
|
233
|
+
|
|
234
|
+
@pytest.mark.asyncio
|
|
235
|
+
async def test_recall_from_memory_tool(self, mock_memory):
|
|
236
|
+
"""Test the recall_from_memory agent tool."""
|
|
237
|
+
memory, mock_mem0 = mock_memory
|
|
238
|
+
|
|
239
|
+
mock_mem0.search = AsyncMock(
|
|
240
|
+
return_value={
|
|
241
|
+
"results": [
|
|
242
|
+
{"memory": "User likes Python programming"},
|
|
243
|
+
{"memory": "User's favorite framework is Django"},
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
result = await memory.recall_from_memory(
|
|
249
|
+
keywords=["programming", "preferences"],
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
assert result["success"] is True
|
|
253
|
+
assert len(result["memories"]) == 4 # 2 results per keyword
|
|
254
|
+
assert result["count"] == 4
|
|
255
|
+
print("✅ recall_from_memory tool works")
|
|
256
|
+
|
|
257
|
+
@pytest.mark.asyncio
|
|
258
|
+
async def test_recall_from_memory_with_limit(self, mock_memory):
|
|
259
|
+
"""Test recall with custom limit."""
|
|
260
|
+
memory, mock_mem0 = mock_memory
|
|
261
|
+
|
|
262
|
+
mock_mem0.search = AsyncMock(
|
|
263
|
+
return_value={
|
|
264
|
+
"results": [{"memory": f"Memory {i}"} for i in range(3)],
|
|
265
|
+
},
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
await memory.recall_from_memory(
|
|
269
|
+
keywords=["test"],
|
|
270
|
+
limit=3,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
call_kwargs = mock_mem0.search.call_args.kwargs
|
|
274
|
+
assert call_kwargs["limit"] == 3
|
|
275
|
+
print("✅ recall_from_memory with limit works")
|
|
276
|
+
|
|
277
|
+
@pytest.mark.asyncio
|
|
278
|
+
async def test_recall_from_memory_error_handling(self, mock_memory):
|
|
279
|
+
"""Test recall_from_memory error handling."""
|
|
280
|
+
memory, mock_mem0 = mock_memory
|
|
281
|
+
|
|
282
|
+
mock_mem0.search = AsyncMock(side_effect=Exception("Search error"))
|
|
283
|
+
|
|
284
|
+
result = await memory.recall_from_memory(keywords=["test"])
|
|
285
|
+
|
|
286
|
+
assert result["success"] is False
|
|
287
|
+
assert "Error retrieving memories" in result["message"]
|
|
288
|
+
print("✅ recall_from_memory error handling works")
|
|
289
|
+
|
|
290
|
+
@pytest.mark.asyncio
|
|
291
|
+
async def test_record_empty_messages(self, mock_memory):
|
|
292
|
+
"""Test recording empty or None messages."""
|
|
293
|
+
memory, mock_mem0 = mock_memory
|
|
294
|
+
|
|
295
|
+
# Should not call mem0.add
|
|
296
|
+
await memory.record([])
|
|
297
|
+
await memory.record(None)
|
|
298
|
+
await memory.record([None, None])
|
|
299
|
+
|
|
300
|
+
assert not mock_mem0.add.called
|
|
301
|
+
print("✅ Recording empty messages handled gracefully")
|
|
302
|
+
|
|
303
|
+
@pytest.mark.asyncio
|
|
304
|
+
async def test_retrieve_empty_query(self, mock_memory):
|
|
305
|
+
"""Test retrieving with empty query."""
|
|
306
|
+
memory, mock_mem0 = mock_memory
|
|
307
|
+
|
|
308
|
+
result = await memory.retrieve("")
|
|
309
|
+
assert result == ""
|
|
310
|
+
|
|
311
|
+
result = await memory.retrieve([])
|
|
312
|
+
assert result == ""
|
|
313
|
+
|
|
314
|
+
result = await memory.retrieve({"role": "user", "content": ""})
|
|
315
|
+
assert result == ""
|
|
316
|
+
|
|
317
|
+
print("✅ Retrieving with empty query handled gracefully")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@pytest.mark.skipif(
|
|
321
|
+
not MEMORY_AVAILABLE or not MEM0_AVAILABLE,
|
|
322
|
+
reason="Memory module or mem0 not available",
|
|
323
|
+
)
|
|
324
|
+
class TestPersistentMemoryIntegration:
|
|
325
|
+
"""Integration tests with actual mem0 (if available)."""
|
|
326
|
+
|
|
327
|
+
@pytest.mark.asyncio
|
|
328
|
+
async def test_full_memory_workflow(self):
|
|
329
|
+
"""Test a complete memory workflow: record and retrieve."""
|
|
330
|
+
try:
|
|
331
|
+
# Create memory instance
|
|
332
|
+
memory = PersistentMemory(
|
|
333
|
+
agent_name="test_integration_agent",
|
|
334
|
+
llm_backend=create_mock_backend(),
|
|
335
|
+
embedding_backend=create_mock_backend(),
|
|
336
|
+
on_disk=False, # Use in-memory for tests
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# Record some information
|
|
340
|
+
messages = [
|
|
341
|
+
{"role": "user", "content": "I love Python programming"},
|
|
342
|
+
{"role": "assistant", "content": "That's great! Python is versatile."},
|
|
343
|
+
]
|
|
344
|
+
await memory.record(messages)
|
|
345
|
+
|
|
346
|
+
# Try to retrieve
|
|
347
|
+
result = await memory.retrieve("Python")
|
|
348
|
+
|
|
349
|
+
# Should return something (exact match depends on embeddings)
|
|
350
|
+
assert isinstance(result, str)
|
|
351
|
+
print("✅ Full memory workflow works")
|
|
352
|
+
|
|
353
|
+
except Exception as e:
|
|
354
|
+
pytest.skip(f"Integration test skipped: {e}")
|
|
355
|
+
|
|
356
|
+
@pytest.mark.asyncio
|
|
357
|
+
async def test_memory_with_multiple_identifiers(self):
|
|
358
|
+
"""Test memory filtering with multiple identifiers."""
|
|
359
|
+
try:
|
|
360
|
+
memory = PersistentMemory(
|
|
361
|
+
agent_name="agent_1",
|
|
362
|
+
user_name="user_1",
|
|
363
|
+
session_name="session_1",
|
|
364
|
+
llm_backend=create_mock_backend(),
|
|
365
|
+
embedding_backend=create_mock_backend(),
|
|
366
|
+
on_disk=False,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Record and verify identifiers are used
|
|
370
|
+
await memory.record(
|
|
371
|
+
[
|
|
372
|
+
{"role": "user", "content": "Test message"},
|
|
373
|
+
],
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
assert memory.agent_id == "agent_1"
|
|
377
|
+
assert memory.user_id == "user_1"
|
|
378
|
+
assert memory.session_id == "session_1"
|
|
379
|
+
print("✅ Memory with multiple identifiers works")
|
|
380
|
+
|
|
381
|
+
except Exception as e:
|
|
382
|
+
pytest.skip(f"Integration test skipped: {e}")
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@pytest.mark.skipif(not MEMORY_AVAILABLE, reason="Memory module not available")
|
|
386
|
+
class TestPersistentMemoryBase:
|
|
387
|
+
"""Tests for PersistentMemoryBase abstract methods."""
|
|
388
|
+
|
|
389
|
+
def test_base_class_methods(self):
|
|
390
|
+
"""Test that base class has expected abstract methods."""
|
|
391
|
+
from massgen.memory import PersistentMemoryBase
|
|
392
|
+
|
|
393
|
+
# Check that methods exist
|
|
394
|
+
assert hasattr(PersistentMemoryBase, "record")
|
|
395
|
+
assert hasattr(PersistentMemoryBase, "retrieve")
|
|
396
|
+
assert hasattr(PersistentMemoryBase, "save_to_memory")
|
|
397
|
+
assert hasattr(PersistentMemoryBase, "recall_from_memory")
|
|
398
|
+
print("✅ PersistentMemoryBase has expected methods")
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
if __name__ == "__main__":
|
|
402
|
+
import asyncio
|
|
403
|
+
|
|
404
|
+
async def run_all_tests():
|
|
405
|
+
"""Run all tests manually."""
|
|
406
|
+
print("\n=== Running PersistentMemory Tests ===\n")
|
|
407
|
+
|
|
408
|
+
if not MEMORY_AVAILABLE:
|
|
409
|
+
print("❌ Memory module not available, skipping tests")
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
if not MEM0_AVAILABLE:
|
|
413
|
+
print("⚠️ mem0 not installed, some tests will be skipped")
|
|
414
|
+
|
|
415
|
+
# Run initialization tests
|
|
416
|
+
print("\n--- Initialization Tests ---")
|
|
417
|
+
test_init = TestPersistentMemoryInitialization()
|
|
418
|
+
test_init.test_initialization_without_identifiers_fails()
|
|
419
|
+
|
|
420
|
+
if MEM0_AVAILABLE:
|
|
421
|
+
test_init.test_initialization_without_backends_fails()
|
|
422
|
+
test_init.test_initialization_with_agent_name()
|
|
423
|
+
test_init.test_initialization_with_all_identifiers()
|
|
424
|
+
|
|
425
|
+
# Run base class tests
|
|
426
|
+
print("\n--- Base Class Tests ---")
|
|
427
|
+
test_base = TestPersistentMemoryBase()
|
|
428
|
+
test_base.test_base_class_methods()
|
|
429
|
+
|
|
430
|
+
print("\n⚠️ For complete testing, run with pytest to execute mocked and integration tests")
|
|
431
|
+
print(" Command: pytest massgen/tests/test_persistent_memory.py -v")
|
|
432
|
+
|
|
433
|
+
print("\n=== PersistentMemory Basic Tests Passed! ===\n")
|
|
434
|
+
|
|
435
|
+
asyncio.run(run_all_tests())
|
|
@@ -49,11 +49,17 @@ class TokenCostCalculator:
|
|
|
49
49
|
# Default pricing data for various providers and models
|
|
50
50
|
PROVIDER_PRICING: Dict[str, Dict[str, ModelPricing]] = {
|
|
51
51
|
"OpenAI": {
|
|
52
|
+
# GPT-5 models (400K context window)
|
|
53
|
+
"gpt-5": ModelPricing(0.00125, 0.01, 400000, 128000),
|
|
54
|
+
"gpt-5-mini": ModelPricing(0.00025, 0.002, 400000, 128000),
|
|
55
|
+
"gpt-5-nano": ModelPricing(0.00005, 0.0004, 400000, 128000),
|
|
56
|
+
# GPT-4 series
|
|
52
57
|
"gpt-4o": ModelPricing(0.0025, 0.01, 128000, 16384),
|
|
53
58
|
"gpt-4o-mini": ModelPricing(0.00015, 0.0006, 128000, 16384),
|
|
54
59
|
"gpt-4-turbo": ModelPricing(0.01, 0.03, 128000, 4096),
|
|
55
60
|
"gpt-4": ModelPricing(0.03, 0.06, 8192, 8192),
|
|
56
61
|
"gpt-3.5-turbo": ModelPricing(0.0005, 0.0015, 16385, 4096),
|
|
62
|
+
# O-series models
|
|
57
63
|
"o1-preview": ModelPricing(0.015, 0.06, 128000, 32768),
|
|
58
64
|
"o1-mini": ModelPricing(0.003, 0.012, 128000, 65536),
|
|
59
65
|
"o3-mini": ModelPricing(0.0011, 0.0044, 200000, 100000),
|
massgen/tool/__init__.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"""Tool module for MassGen framework."""
|
|
3
3
|
|
|
4
4
|
from ._code_executors import run_python_script, run_shell_script
|
|
5
|
+
from ._decorators import context_params
|
|
5
6
|
from ._file_handlers import append_file_content, read_file_content, save_file_content
|
|
6
7
|
from ._manager import ToolManager
|
|
7
8
|
from ._result import ExecutionResult
|
|
@@ -18,21 +19,13 @@ from .workflow_toolkits import (
|
|
|
18
19
|
__all__ = [
|
|
19
20
|
"ToolManager",
|
|
20
21
|
"ExecutionResult",
|
|
22
|
+
"context_params",
|
|
21
23
|
"two_num_tool",
|
|
22
24
|
"run_python_script",
|
|
23
25
|
"run_shell_script",
|
|
24
26
|
"read_file_content",
|
|
25
27
|
"save_file_content",
|
|
26
28
|
"append_file_content",
|
|
27
|
-
"dashscope_generate_image",
|
|
28
|
-
"dashscope_generate_audio",
|
|
29
|
-
"dashscope_analyze_image",
|
|
30
|
-
"openai_generate_image",
|
|
31
|
-
"openai_generate_audio",
|
|
32
|
-
"openai_modify_image",
|
|
33
|
-
"openai_create_variation",
|
|
34
|
-
"openai_analyze_image",
|
|
35
|
-
"openai_transcribe_audio",
|
|
36
29
|
"BaseToolkit",
|
|
37
30
|
"ToolType",
|
|
38
31
|
"NewAnswerToolkit",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Decorators for custom tool functions."""
|
|
3
|
+
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def context_params(*param_names: str) -> Callable[[Callable], Callable]:
|
|
8
|
+
"""Mark parameters for auto-injection from ExecutionContext.
|
|
9
|
+
|
|
10
|
+
Parameters marked with this decorator will be:
|
|
11
|
+
1. Excluded from the LLM schema (not visible to the model)
|
|
12
|
+
2. Automatically injected from execution_context at runtime
|
|
13
|
+
|
|
14
|
+
This is useful for parameters that should come from the backend runtime
|
|
15
|
+
context rather than from the LLM, such as:
|
|
16
|
+
- messages: Conversation history
|
|
17
|
+
- agent_id: Current agent identifier
|
|
18
|
+
- backend_name: Backend provider name
|
|
19
|
+
- current_stage: Current coordination stage
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
*param_names: Names of parameters to mark as context parameters
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Decorator function that marks the parameters
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
>>> from massgen.tool import context_params, ExecutionResult
|
|
29
|
+
>>> from typing import List, Dict, Any
|
|
30
|
+
>>>
|
|
31
|
+
>>> @context_params("messages", "agent_id")
|
|
32
|
+
>>> async def analyze_conversation(
|
|
33
|
+
... query: str, # LLM provides this
|
|
34
|
+
... messages: List[Dict[str, Any]], # Auto-injected from context
|
|
35
|
+
... agent_id: str, # Auto-injected from context
|
|
36
|
+
... ) -> ExecutionResult:
|
|
37
|
+
... '''Analyze conversation with full context.'''
|
|
38
|
+
... # messages and agent_id are automatically filled from execution_context
|
|
39
|
+
... system_msg = next((m for m in messages if m.get("role") == "system"), None)
|
|
40
|
+
... return ExecutionResult(...)
|
|
41
|
+
|
|
42
|
+
Note:
|
|
43
|
+
The execution_context is provided by the backend when executing tools.
|
|
44
|
+
Only parameters marked by this decorator will be injected from the context.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def decorator(func: Callable) -> Callable:
|
|
48
|
+
"""Store context parameter names in function metadata."""
|
|
49
|
+
func.__context_params__ = set(param_names)
|
|
50
|
+
return func
|
|
51
|
+
|
|
52
|
+
return decorator
|