massgen 0.1.5__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/cli.py +73 -6
- massgen/config_builder.py +20 -54
- massgen/config_validator.py +931 -0
- massgen/configs/README.md +51 -8
- 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/tests/test_ag2_lesson_planner.py +223 -0
- massgen/tests/test_config_validator.py +1156 -0
- massgen/tests/test_langgraph_lesson_planner.py +223 -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.5.dist-info → massgen-0.1.6.dist-info}/METADATA +104 -76
- {massgen-0.1.5.dist-info → massgen-0.1.6.dist-info}/RECORD +50 -39
- massgen/backend/gemini_mcp_manager.py +0 -545
- massgen/backend/gemini_trackers.py +0 -344
- massgen/configs/tools/custom_tools/multimodal_tools/playwright_with_img_understanding.yaml +0 -98
- massgen/configs/tools/custom_tools/multimodal_tools/understand_video_example.yaml +0 -54
- massgen/tools/__init__.py +0 -8
- massgen/tools/_planning_mcp_server.py +0 -520
- massgen/tools/planning_dataclasses.py +0 -434
- {massgen-0.1.5.dist-info → massgen-0.1.6.dist-info}/WHEEL +0 -0
- {massgen-0.1.5.dist-info → massgen-0.1.6.dist-info}/entry_points.txt +0 -0
- {massgen-0.1.5.dist-info → massgen-0.1.6.dist-info}/licenses/LICENSE +0 -0
- {massgen-0.1.5.dist-info → massgen-0.1.6.dist-info}/top_level.txt +0 -0
|
@@ -52,11 +52,57 @@ class GeminiFormatter(FormatterBase):
|
|
|
52
52
|
"""
|
|
53
53
|
return tools or []
|
|
54
54
|
|
|
55
|
-
def format_mcp_tools(
|
|
55
|
+
def format_mcp_tools(
|
|
56
|
+
self,
|
|
57
|
+
mcp_functions: Dict[str, Any],
|
|
58
|
+
return_sdk_objects: bool = True,
|
|
59
|
+
) -> List[Any]:
|
|
56
60
|
"""
|
|
57
|
-
MCP
|
|
61
|
+
Convert MCP Function objects to Gemini FunctionDeclaration format.
|
|
62
|
+
|
|
58
63
|
"""
|
|
59
|
-
|
|
64
|
+
if not mcp_functions:
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
# Step 1: Convert MCP Function objects to Gemini dictionary format
|
|
68
|
+
gemini_dicts = []
|
|
69
|
+
|
|
70
|
+
for mcp_function in mcp_functions.values():
|
|
71
|
+
try:
|
|
72
|
+
# Extract attributes from Function object
|
|
73
|
+
name = getattr(mcp_function, "name", "")
|
|
74
|
+
description = getattr(mcp_function, "description", "")
|
|
75
|
+
parameters = getattr(mcp_function, "parameters", {})
|
|
76
|
+
|
|
77
|
+
# Build Gemini-compatible dictionary
|
|
78
|
+
gemini_dict = {
|
|
79
|
+
"name": name,
|
|
80
|
+
"description": description,
|
|
81
|
+
"parameters": parameters,
|
|
82
|
+
}
|
|
83
|
+
gemini_dicts.append(gemini_dict)
|
|
84
|
+
|
|
85
|
+
logger.debug(f"[GeminiFormatter] Converted MCP tool '{name}' to dictionary format")
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logger.error(f"[GeminiFormatter] Failed to convert MCP tool: {e}")
|
|
89
|
+
# Continue processing remaining tools instead of failing completely
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
if not return_sdk_objects:
|
|
93
|
+
return gemini_dicts
|
|
94
|
+
|
|
95
|
+
# Step 2: Convert dictionaries to SDK FunctionDeclaration objects
|
|
96
|
+
function_declarations = self._convert_to_function_declarations(gemini_dicts)
|
|
97
|
+
|
|
98
|
+
# Log successful conversion
|
|
99
|
+
for func_decl in function_declarations:
|
|
100
|
+
if hasattr(func_decl, "name"):
|
|
101
|
+
logger.debug(
|
|
102
|
+
f"[GeminiFormatter] Converted MCP tool '{func_decl.name}' to FunctionDeclaration",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return function_declarations
|
|
60
106
|
|
|
61
107
|
# Coordination helpers
|
|
62
108
|
|
|
@@ -239,15 +285,14 @@ Make your decision and include the JSON at the very end of your response."""
|
|
|
239
285
|
vote_data = structured_response.get("vote_data", {})
|
|
240
286
|
return [
|
|
241
287
|
{
|
|
242
|
-
"
|
|
243
|
-
"
|
|
244
|
-
"
|
|
245
|
-
|
|
246
|
-
"arguments": {
|
|
288
|
+
"call_id": f"vote_{abs(hash(str(vote_data))) % 10000 + 1}",
|
|
289
|
+
"name": "vote",
|
|
290
|
+
"arguments": json.dumps(
|
|
291
|
+
{
|
|
247
292
|
"agent_id": vote_data.get("agent_id", ""),
|
|
248
293
|
"reason": vote_data.get("reason", ""),
|
|
249
294
|
},
|
|
250
|
-
|
|
295
|
+
),
|
|
251
296
|
},
|
|
252
297
|
]
|
|
253
298
|
|
|
@@ -255,12 +300,13 @@ Make your decision and include the JSON at the very end of your response."""
|
|
|
255
300
|
answer_data = structured_response.get("answer_data", {})
|
|
256
301
|
return [
|
|
257
302
|
{
|
|
258
|
-
"
|
|
259
|
-
"
|
|
260
|
-
"
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
303
|
+
"call_id": f"new_answer_{abs(hash(str(answer_data))) % 10000 + 1}",
|
|
304
|
+
"name": "new_answer",
|
|
305
|
+
"arguments": json.dumps(
|
|
306
|
+
{
|
|
307
|
+
"content": answer_data.get("content", ""),
|
|
308
|
+
},
|
|
309
|
+
),
|
|
264
310
|
},
|
|
265
311
|
]
|
|
266
312
|
|
|
@@ -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"])
|