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.

Files changed (57) 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/cli.py +73 -6
  10. massgen/config_builder.py +20 -54
  11. massgen/config_validator.py +931 -0
  12. massgen/configs/README.md +51 -8
  13. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_mcp_example.yaml +1 -0
  14. massgen/configs/tools/custom_tools/claude_custom_tool_example_no_path.yaml +1 -1
  15. massgen/configs/tools/custom_tools/claude_custom_tool_with_mcp_example.yaml +1 -0
  16. massgen/configs/tools/custom_tools/computer_use_browser_example.yaml +1 -1
  17. massgen/configs/tools/custom_tools/computer_use_docker_example.yaml +1 -1
  18. massgen/configs/tools/custom_tools/gemini_custom_tool_with_mcp_example.yaml +1 -0
  19. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_mcp_example.yaml +1 -0
  20. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_mcp_example.yaml +1 -0
  21. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_mcp_example.yaml +1 -0
  22. massgen/configs/tools/custom_tools/interop/ag2_and_langgraph_lesson_planner.yaml +65 -0
  23. massgen/configs/tools/custom_tools/interop/ag2_and_openai_assistant_lesson_planner.yaml +65 -0
  24. massgen/configs/tools/custom_tools/interop/ag2_lesson_planner_example.yaml +48 -0
  25. massgen/configs/tools/custom_tools/interop/agentscope_lesson_planner_example.yaml +48 -0
  26. massgen/configs/tools/custom_tools/interop/langgraph_lesson_planner_example.yaml +49 -0
  27. massgen/configs/tools/custom_tools/interop/openai_assistant_lesson_planner_example.yaml +50 -0
  28. massgen/configs/tools/custom_tools/interop/smolagent_lesson_planner_example.yaml +49 -0
  29. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_mcp_example.yaml +1 -0
  30. massgen/configs/tools/custom_tools/two_models_with_tools_example.yaml +44 -0
  31. massgen/formatter/_gemini_formatter.py +61 -15
  32. massgen/tests/test_ag2_lesson_planner.py +223 -0
  33. massgen/tests/test_config_validator.py +1156 -0
  34. massgen/tests/test_langgraph_lesson_planner.py +223 -0
  35. massgen/tool/__init__.py +2 -9
  36. massgen/tool/_decorators.py +52 -0
  37. massgen/tool/_extraframework_agents/ag2_lesson_planner_tool.py +251 -0
  38. massgen/tool/_extraframework_agents/agentscope_lesson_planner_tool.py +303 -0
  39. massgen/tool/_extraframework_agents/langgraph_lesson_planner_tool.py +275 -0
  40. massgen/tool/_extraframework_agents/openai_assistant_lesson_planner_tool.py +247 -0
  41. massgen/tool/_extraframework_agents/smolagent_lesson_planner_tool.py +180 -0
  42. massgen/tool/_manager.py +102 -16
  43. massgen/tool/_registered_tool.py +3 -0
  44. massgen/tool/_result.py +3 -0
  45. {massgen-0.1.5.dist-info → massgen-0.1.6.dist-info}/METADATA +104 -76
  46. {massgen-0.1.5.dist-info → massgen-0.1.6.dist-info}/RECORD +50 -39
  47. massgen/backend/gemini_mcp_manager.py +0 -545
  48. massgen/backend/gemini_trackers.py +0 -344
  49. massgen/configs/tools/custom_tools/multimodal_tools/playwright_with_img_understanding.yaml +0 -98
  50. massgen/configs/tools/custom_tools/multimodal_tools/understand_video_example.yaml +0 -54
  51. massgen/tools/__init__.py +0 -8
  52. massgen/tools/_planning_mcp_server.py +0 -520
  53. massgen/tools/planning_dataclasses.py +0 -434
  54. {massgen-0.1.5.dist-info → massgen-0.1.6.dist-info}/WHEEL +0 -0
  55. {massgen-0.1.5.dist-info → massgen-0.1.6.dist-info}/entry_points.txt +0 -0
  56. {massgen-0.1.5.dist-info → massgen-0.1.6.dist-info}/licenses/LICENSE +0 -0
  57. {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(self, mcp_functions: Dict[str, Any]) -> List[Dict[str, Any]]:
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 tools are passed via SDK sessions in stream_with_tools; not function declarations.
61
+ Convert MCP Function objects to Gemini FunctionDeclaration format.
62
+
58
63
  """
59
- return []
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
- "id": f"vote_{abs(hash(str(vote_data))) % 10000 + 1}",
243
- "type": "function",
244
- "function": {
245
- "name": "vote",
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
- "id": f"new_answer_{abs(hash(str(answer_data))) % 10000 + 1}",
259
- "type": "function",
260
- "function": {
261
- "name": "new_answer",
262
- "arguments": {"content": answer_data.get("content", "")},
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"])