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
@@ -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