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
|
@@ -1,344 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""
|
|
3
|
-
MCP tracking utilities for the Gemini backend, handling deduplication across streaming chunks and extraction from SDK objects.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import hashlib
|
|
7
|
-
import json
|
|
8
|
-
import time
|
|
9
|
-
from typing import Any, Dict, Optional
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class MCPResponseTracker:
|
|
13
|
-
"""
|
|
14
|
-
Tracks MCP tool responses across streaming chunks to handle deduplication.
|
|
15
|
-
|
|
16
|
-
Similar to MCPCallTracker but for tracking tool responses to avoid duplicate output.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
def __init__(self):
|
|
20
|
-
"""Initialize the tracker with empty storage."""
|
|
21
|
-
self.processed_responses = set() # Store hashes of processed responses
|
|
22
|
-
self.response_history = [] # Store all unique responses with timestamps
|
|
23
|
-
|
|
24
|
-
def get_response_hash(self, tool_name: str, tool_response: Any) -> str:
|
|
25
|
-
"""
|
|
26
|
-
Generate a unique hash for a tool response based on name and response content.
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
tool_name: Name of the tool that responded
|
|
30
|
-
tool_response: Response from the tool
|
|
31
|
-
|
|
32
|
-
Returns:
|
|
33
|
-
MD5 hash string identifying this specific response
|
|
34
|
-
"""
|
|
35
|
-
# Create a deterministic string representation
|
|
36
|
-
content = f"{tool_name}:{str(tool_response)}"
|
|
37
|
-
return hashlib.md5(content.encode()).hexdigest()
|
|
38
|
-
|
|
39
|
-
def is_new_response(self, tool_name: str, tool_response: Any) -> bool:
|
|
40
|
-
"""
|
|
41
|
-
Check if this is a new tool response we haven't seen before.
|
|
42
|
-
|
|
43
|
-
Args:
|
|
44
|
-
tool_name: Name of the tool that responded
|
|
45
|
-
tool_response: Response from the tool
|
|
46
|
-
|
|
47
|
-
Returns:
|
|
48
|
-
True if this is a new response, False if already processed
|
|
49
|
-
"""
|
|
50
|
-
response_hash = self.get_response_hash(tool_name, tool_response)
|
|
51
|
-
return response_hash not in self.processed_responses
|
|
52
|
-
|
|
53
|
-
def add_response(self, tool_name: str, tool_response: Any) -> Dict[str, Any]:
|
|
54
|
-
"""
|
|
55
|
-
Add a new response to the tracker.
|
|
56
|
-
|
|
57
|
-
Args:
|
|
58
|
-
tool_name: Name of the tool that responded
|
|
59
|
-
tool_response: Response from the tool
|
|
60
|
-
|
|
61
|
-
Returns:
|
|
62
|
-
Dictionary containing response details and timestamp
|
|
63
|
-
"""
|
|
64
|
-
response_hash = self.get_response_hash(tool_name, tool_response)
|
|
65
|
-
self.processed_responses.add(response_hash)
|
|
66
|
-
|
|
67
|
-
record = {
|
|
68
|
-
"tool_name": tool_name,
|
|
69
|
-
"response": tool_response,
|
|
70
|
-
"hash": response_hash,
|
|
71
|
-
"timestamp": time.time(),
|
|
72
|
-
}
|
|
73
|
-
self.response_history.append(record)
|
|
74
|
-
return record
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
class MCPCallTracker:
|
|
78
|
-
"""
|
|
79
|
-
Tracks MCP tool calls across streaming chunks to handle deduplication.
|
|
80
|
-
|
|
81
|
-
Uses hashing to identify unique tool calls and timestamps to track when they occurred.
|
|
82
|
-
This ensures we don't double-count the same tool call appearing in multiple chunks.
|
|
83
|
-
"""
|
|
84
|
-
|
|
85
|
-
def __init__(self):
|
|
86
|
-
"""Initialize the tracker with empty storage."""
|
|
87
|
-
self.processed_calls = set() # Store hashes of processed calls
|
|
88
|
-
self.call_history = [] # Store all unique calls with timestamps
|
|
89
|
-
self.last_chunk_calls = [] # Track calls from the last chunk for deduplication
|
|
90
|
-
self.dedup_window = 0.5 # Time window in seconds for deduplication
|
|
91
|
-
|
|
92
|
-
def get_call_hash(self, tool_name: str, tool_args: Dict[str, Any]) -> str:
|
|
93
|
-
"""
|
|
94
|
-
Generate a unique hash for a tool call based on name and arguments.
|
|
95
|
-
|
|
96
|
-
Args:
|
|
97
|
-
tool_name: Name of the tool being called
|
|
98
|
-
tool_args: Arguments passed to the tool
|
|
99
|
-
|
|
100
|
-
Returns:
|
|
101
|
-
MD5 hash string identifying this specific call
|
|
102
|
-
"""
|
|
103
|
-
# Create a deterministic string representation
|
|
104
|
-
content = f"{tool_name}:{json.dumps(tool_args, sort_keys=True)}"
|
|
105
|
-
return hashlib.md5(content.encode()).hexdigest()
|
|
106
|
-
|
|
107
|
-
def is_new_call(self, tool_name: str, tool_args: Dict[str, Any]) -> bool:
|
|
108
|
-
"""
|
|
109
|
-
Check if this is a new tool call we haven't seen before.
|
|
110
|
-
|
|
111
|
-
Uses a time-window based approach: identical calls within the dedup_window
|
|
112
|
-
are considered duplicates (likely from streaming chunks), while those outside
|
|
113
|
-
the window are considered new calls (likely intentional repeated calls).
|
|
114
|
-
|
|
115
|
-
Args:
|
|
116
|
-
tool_name: Name of the tool being called
|
|
117
|
-
tool_args: Arguments passed to the tool
|
|
118
|
-
|
|
119
|
-
Returns:
|
|
120
|
-
True if this is a new call, False if we've seen it before
|
|
121
|
-
"""
|
|
122
|
-
call_hash = self.get_call_hash(tool_name, tool_args)
|
|
123
|
-
current_time = time.time()
|
|
124
|
-
|
|
125
|
-
# Check if this call exists in recent history within the dedup window
|
|
126
|
-
for call in self.call_history[-10:]: # Check last 10 calls for efficiency
|
|
127
|
-
if call.get("hash") == call_hash:
|
|
128
|
-
time_diff = current_time - call.get("timestamp", 0)
|
|
129
|
-
if time_diff < self.dedup_window:
|
|
130
|
-
# This is likely a duplicate from streaming chunks
|
|
131
|
-
return False
|
|
132
|
-
# If outside the window, treat as a new intentional call
|
|
133
|
-
|
|
134
|
-
# Mark as processed
|
|
135
|
-
self.processed_calls.add(call_hash)
|
|
136
|
-
return True
|
|
137
|
-
|
|
138
|
-
def add_call(self, tool_name: str, tool_args: Dict[str, Any]) -> Dict[str, Any]:
|
|
139
|
-
"""
|
|
140
|
-
Add a new tool call to the history.
|
|
141
|
-
|
|
142
|
-
Args:
|
|
143
|
-
tool_name: Name of the tool being called
|
|
144
|
-
tool_args: Arguments passed to the tool
|
|
145
|
-
|
|
146
|
-
Returns:
|
|
147
|
-
Dictionary containing the call details with timestamp and hash
|
|
148
|
-
"""
|
|
149
|
-
call_record = {
|
|
150
|
-
"name": tool_name,
|
|
151
|
-
"arguments": tool_args,
|
|
152
|
-
"timestamp": time.time(),
|
|
153
|
-
"hash": self.get_call_hash(tool_name, tool_args),
|
|
154
|
-
"sequence": len(self.call_history), # Add sequence number for ordering
|
|
155
|
-
}
|
|
156
|
-
self.call_history.append(call_record)
|
|
157
|
-
|
|
158
|
-
# Clean up old history to prevent memory growth
|
|
159
|
-
if len(self.call_history) > 100:
|
|
160
|
-
self.call_history = self.call_history[-50:]
|
|
161
|
-
|
|
162
|
-
return call_record
|
|
163
|
-
|
|
164
|
-
def get_summary(self) -> str:
|
|
165
|
-
"""
|
|
166
|
-
Get a summary of all tracked tool calls.
|
|
167
|
-
|
|
168
|
-
Returns:
|
|
169
|
-
Human-readable summary of tool usage
|
|
170
|
-
"""
|
|
171
|
-
if not self.call_history:
|
|
172
|
-
return "No MCP tools called"
|
|
173
|
-
|
|
174
|
-
tool_names = [call["name"] for call in self.call_history]
|
|
175
|
-
unique_tools = list(dict.fromkeys(tool_names)) # Preserve order
|
|
176
|
-
return f"Used {len(self.call_history)} MCP tool calls: {', '.join(unique_tools)}"
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
class MCPResponseExtractor:
|
|
180
|
-
"""
|
|
181
|
-
Extracts MCP tool calls and responses from Gemini SDK stream chunks.
|
|
182
|
-
|
|
183
|
-
This class parses the internal SDK chunks to capture:
|
|
184
|
-
- function_call parts (tool invocations)
|
|
185
|
-
- function_response parts (tool results)
|
|
186
|
-
- Paired call-response data for tracking complete tool executions
|
|
187
|
-
"""
|
|
188
|
-
|
|
189
|
-
def __init__(self):
|
|
190
|
-
"""Initialize the extractor with empty storage."""
|
|
191
|
-
self.mcp_calls = [] # All tool calls
|
|
192
|
-
self.mcp_responses = [] # All tool responses
|
|
193
|
-
self.call_response_pairs = [] # Matched call-response pairs
|
|
194
|
-
self._pending_call = None # Track current call awaiting response
|
|
195
|
-
|
|
196
|
-
def extract_function_call(self, function_call) -> Optional[Dict[str, Any]]:
|
|
197
|
-
"""
|
|
198
|
-
Extract tool call information from SDK function_call object.
|
|
199
|
-
|
|
200
|
-
Tries multiple methods to extract data from different SDK versions:
|
|
201
|
-
1. Direct attributes (name, args)
|
|
202
|
-
2. Dictionary-like interface (get method)
|
|
203
|
-
3. __dict__ attributes
|
|
204
|
-
4. Protobuf _pb attributes
|
|
205
|
-
"""
|
|
206
|
-
tool_name = None
|
|
207
|
-
tool_args = None
|
|
208
|
-
|
|
209
|
-
# Method 1: Direct attributes
|
|
210
|
-
tool_name = getattr(function_call, "name", None)
|
|
211
|
-
tool_args = getattr(function_call, "args", None)
|
|
212
|
-
|
|
213
|
-
# Method 2: Dictionary-like object
|
|
214
|
-
if tool_name is None:
|
|
215
|
-
try:
|
|
216
|
-
if hasattr(function_call, "get"):
|
|
217
|
-
tool_name = function_call.get("name", None)
|
|
218
|
-
tool_args = function_call.get("args", None)
|
|
219
|
-
except Exception:
|
|
220
|
-
pass
|
|
221
|
-
|
|
222
|
-
# Method 3: __dict__ inspection
|
|
223
|
-
if tool_name is None:
|
|
224
|
-
try:
|
|
225
|
-
if hasattr(function_call, "__dict__"):
|
|
226
|
-
fc_dict = function_call.__dict__
|
|
227
|
-
tool_name = fc_dict.get("name", None)
|
|
228
|
-
tool_args = fc_dict.get("args", None)
|
|
229
|
-
except Exception:
|
|
230
|
-
pass
|
|
231
|
-
|
|
232
|
-
# Method 4: Protobuf _pb attribute
|
|
233
|
-
if tool_name is None:
|
|
234
|
-
try:
|
|
235
|
-
if hasattr(function_call, "_pb"):
|
|
236
|
-
pb = function_call._pb
|
|
237
|
-
if hasattr(pb, "name"):
|
|
238
|
-
tool_name = pb.name
|
|
239
|
-
if hasattr(pb, "args"):
|
|
240
|
-
tool_args = pb.args
|
|
241
|
-
except Exception:
|
|
242
|
-
pass
|
|
243
|
-
|
|
244
|
-
if tool_name:
|
|
245
|
-
call_data = {
|
|
246
|
-
"name": tool_name,
|
|
247
|
-
"arguments": tool_args or {},
|
|
248
|
-
"timestamp": time.time(),
|
|
249
|
-
"raw": str(function_call)[:200], # Truncate for logging
|
|
250
|
-
}
|
|
251
|
-
self.mcp_calls.append(call_data)
|
|
252
|
-
self._pending_call = call_data
|
|
253
|
-
return call_data
|
|
254
|
-
|
|
255
|
-
return None
|
|
256
|
-
|
|
257
|
-
def extract_function_response(self, function_response) -> Optional[Dict[str, Any]]:
|
|
258
|
-
"""
|
|
259
|
-
Extract tool response information from SDK function_response object.
|
|
260
|
-
|
|
261
|
-
Uses same extraction methods as function_call for consistency.
|
|
262
|
-
"""
|
|
263
|
-
tool_name = None
|
|
264
|
-
tool_response = None
|
|
265
|
-
|
|
266
|
-
# Method 1: Direct attributes
|
|
267
|
-
tool_name = getattr(function_response, "name", None)
|
|
268
|
-
tool_response = getattr(function_response, "response", None)
|
|
269
|
-
|
|
270
|
-
# Method 2: Dictionary-like object
|
|
271
|
-
if tool_name is None:
|
|
272
|
-
try:
|
|
273
|
-
if hasattr(function_response, "get"):
|
|
274
|
-
tool_name = function_response.get("name", None)
|
|
275
|
-
tool_response = function_response.get("response", None)
|
|
276
|
-
except Exception:
|
|
277
|
-
pass
|
|
278
|
-
|
|
279
|
-
# Method 3: __dict__ inspection
|
|
280
|
-
if tool_name is None:
|
|
281
|
-
try:
|
|
282
|
-
if hasattr(function_response, "__dict__"):
|
|
283
|
-
fr_dict = function_response.__dict__
|
|
284
|
-
tool_name = fr_dict.get("name", None)
|
|
285
|
-
tool_response = fr_dict.get("response", None)
|
|
286
|
-
except Exception:
|
|
287
|
-
pass
|
|
288
|
-
|
|
289
|
-
# Method 4: Protobuf _pb attribute
|
|
290
|
-
if tool_name is None:
|
|
291
|
-
try:
|
|
292
|
-
if hasattr(function_response, "_pb"):
|
|
293
|
-
pb = function_response._pb
|
|
294
|
-
if hasattr(pb, "name"):
|
|
295
|
-
tool_name = pb.name
|
|
296
|
-
if hasattr(pb, "response"):
|
|
297
|
-
tool_response = pb.response
|
|
298
|
-
except Exception:
|
|
299
|
-
pass
|
|
300
|
-
|
|
301
|
-
if tool_name:
|
|
302
|
-
response_data = {
|
|
303
|
-
"name": tool_name,
|
|
304
|
-
"response": tool_response or {},
|
|
305
|
-
"timestamp": time.time(),
|
|
306
|
-
"raw": str(function_response)[:500], # Truncate for logging
|
|
307
|
-
}
|
|
308
|
-
self.mcp_responses.append(response_data)
|
|
309
|
-
|
|
310
|
-
# Pair with pending call if names match
|
|
311
|
-
if self._pending_call and self._pending_call["name"] == tool_name:
|
|
312
|
-
self.call_response_pairs.append(
|
|
313
|
-
{
|
|
314
|
-
"call": self._pending_call,
|
|
315
|
-
"response": response_data,
|
|
316
|
-
"duration": response_data["timestamp"] - self._pending_call["timestamp"],
|
|
317
|
-
"paired_at": time.time(),
|
|
318
|
-
},
|
|
319
|
-
)
|
|
320
|
-
self._pending_call = None
|
|
321
|
-
|
|
322
|
-
return response_data
|
|
323
|
-
|
|
324
|
-
return None
|
|
325
|
-
|
|
326
|
-
def get_summary(self) -> Dict[str, Any]:
|
|
327
|
-
"""
|
|
328
|
-
Get a summary of all extracted MCP tool interactions.
|
|
329
|
-
"""
|
|
330
|
-
return {
|
|
331
|
-
"total_calls": len(self.mcp_calls),
|
|
332
|
-
"total_responses": len(self.mcp_responses),
|
|
333
|
-
"paired_interactions": len(self.call_response_pairs),
|
|
334
|
-
"pending_call": self._pending_call is not None,
|
|
335
|
-
"tool_names": list(set(call["name"] for call in self.mcp_calls)),
|
|
336
|
-
"average_duration": (sum(pair["duration"] for pair in self.call_response_pairs) / len(self.call_response_pairs) if self.call_response_pairs else 0),
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
def clear(self):
|
|
340
|
-
"""Clear all stored data."""
|
|
341
|
-
self.mcp_calls.clear()
|
|
342
|
-
self.mcp_responses.clear()
|
|
343
|
-
self.call_response_pairs.clear()
|
|
344
|
-
self._pending_call = None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|