massgen 0.1.0a2__py3-none-any.whl → 0.1.1__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 (111) hide show
  1. massgen/__init__.py +1 -1
  2. massgen/agent_config.py +17 -0
  3. massgen/api_params_handler/_api_params_handler_base.py +1 -0
  4. massgen/api_params_handler/_chat_completions_api_params_handler.py +8 -1
  5. massgen/api_params_handler/_claude_api_params_handler.py +8 -1
  6. massgen/api_params_handler/_gemini_api_params_handler.py +73 -0
  7. massgen/api_params_handler/_response_api_params_handler.py +8 -1
  8. massgen/backend/base.py +31 -0
  9. massgen/backend/{base_with_mcp.py → base_with_custom_tool_and_mcp.py} +282 -11
  10. massgen/backend/chat_completions.py +182 -92
  11. massgen/backend/claude.py +115 -18
  12. massgen/backend/claude_code.py +378 -14
  13. massgen/backend/docs/CLAUDE_API_RESEARCH.md +3 -3
  14. massgen/backend/gemini.py +1275 -1607
  15. massgen/backend/gemini_mcp_manager.py +545 -0
  16. massgen/backend/gemini_trackers.py +344 -0
  17. massgen/backend/gemini_utils.py +43 -0
  18. massgen/backend/response.py +129 -70
  19. massgen/cli.py +643 -132
  20. massgen/config_builder.py +381 -32
  21. massgen/configs/README.md +111 -80
  22. massgen/configs/basic/multi/three_agents_default.yaml +1 -1
  23. massgen/configs/basic/single/single_agent.yaml +1 -1
  24. massgen/configs/providers/openai/gpt5_nano.yaml +3 -3
  25. massgen/configs/tools/custom_tools/claude_code_custom_tool_example.yaml +32 -0
  26. massgen/configs/tools/custom_tools/claude_code_custom_tool_example_no_path.yaml +28 -0
  27. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_mcp_example.yaml +40 -0
  28. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_wrong_mcp_example.yaml +38 -0
  29. massgen/configs/tools/custom_tools/claude_code_wrong_custom_tool_with_mcp_example.yaml +38 -0
  30. massgen/configs/tools/custom_tools/claude_custom_tool_example.yaml +24 -0
  31. massgen/configs/tools/custom_tools/claude_custom_tool_example_no_path.yaml +22 -0
  32. massgen/configs/tools/custom_tools/claude_custom_tool_with_mcp_example.yaml +35 -0
  33. massgen/configs/tools/custom_tools/claude_custom_tool_with_wrong_mcp_example.yaml +33 -0
  34. massgen/configs/tools/custom_tools/claude_wrong_custom_tool_with_mcp_example.yaml +33 -0
  35. massgen/configs/tools/custom_tools/gemini_custom_tool_example.yaml +24 -0
  36. massgen/configs/tools/custom_tools/gemini_custom_tool_example_no_path.yaml +22 -0
  37. massgen/configs/tools/custom_tools/gemini_custom_tool_with_mcp_example.yaml +35 -0
  38. massgen/configs/tools/custom_tools/gemini_custom_tool_with_wrong_mcp_example.yaml +33 -0
  39. massgen/configs/tools/custom_tools/gemini_wrong_custom_tool_with_mcp_example.yaml +33 -0
  40. massgen/configs/tools/custom_tools/github_issue_market_analysis.yaml +94 -0
  41. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example.yaml +24 -0
  42. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example_no_path.yaml +22 -0
  43. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_mcp_example.yaml +35 -0
  44. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_wrong_mcp_example.yaml +33 -0
  45. massgen/configs/tools/custom_tools/gpt5_nano_wrong_custom_tool_with_mcp_example.yaml +33 -0
  46. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example.yaml +25 -0
  47. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example_no_path.yaml +23 -0
  48. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_mcp_example.yaml +34 -0
  49. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_wrong_mcp_example.yaml +34 -0
  50. massgen/configs/tools/custom_tools/gpt_oss_wrong_custom_tool_with_mcp_example.yaml +34 -0
  51. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example.yaml +24 -0
  52. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example_no_path.yaml +22 -0
  53. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_mcp_example.yaml +35 -0
  54. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_wrong_mcp_example.yaml +33 -0
  55. massgen/configs/tools/custom_tools/grok3_mini_wrong_custom_tool_with_mcp_example.yaml +33 -0
  56. massgen/configs/tools/custom_tools/qwen_api_custom_tool_example.yaml +25 -0
  57. massgen/configs/tools/custom_tools/qwen_api_custom_tool_example_no_path.yaml +23 -0
  58. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_mcp_example.yaml +36 -0
  59. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_wrong_mcp_example.yaml +34 -0
  60. massgen/configs/tools/custom_tools/qwen_api_wrong_custom_tool_with_mcp_example.yaml +34 -0
  61. massgen/configs/tools/custom_tools/qwen_local_custom_tool_example.yaml +24 -0
  62. massgen/configs/tools/custom_tools/qwen_local_custom_tool_example_no_path.yaml +22 -0
  63. massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_mcp_example.yaml +35 -0
  64. massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_wrong_mcp_example.yaml +33 -0
  65. massgen/configs/tools/custom_tools/qwen_local_wrong_custom_tool_with_mcp_example.yaml +33 -0
  66. massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +1 -1
  67. massgen/configs/voting/gemini_gpt_voting_sensitivity.yaml +67 -0
  68. massgen/formatter/_chat_completions_formatter.py +104 -0
  69. massgen/formatter/_claude_formatter.py +120 -0
  70. massgen/formatter/_gemini_formatter.py +448 -0
  71. massgen/formatter/_response_formatter.py +88 -0
  72. massgen/frontend/coordination_ui.py +4 -2
  73. massgen/logger_config.py +35 -3
  74. massgen/message_templates.py +56 -6
  75. massgen/orchestrator.py +179 -10
  76. massgen/stream_chunk/base.py +3 -0
  77. massgen/tests/custom_tools_example.py +392 -0
  78. massgen/tests/mcp_test_server.py +17 -7
  79. massgen/tests/test_config_builder.py +423 -0
  80. massgen/tests/test_custom_tools.py +401 -0
  81. massgen/tests/test_tools.py +127 -0
  82. massgen/tool/README.md +935 -0
  83. massgen/tool/__init__.py +39 -0
  84. massgen/tool/_async_helpers.py +70 -0
  85. massgen/tool/_basic/__init__.py +8 -0
  86. massgen/tool/_basic/_two_num_tool.py +24 -0
  87. massgen/tool/_code_executors/__init__.py +10 -0
  88. massgen/tool/_code_executors/_python_executor.py +74 -0
  89. massgen/tool/_code_executors/_shell_executor.py +61 -0
  90. massgen/tool/_exceptions.py +39 -0
  91. massgen/tool/_file_handlers/__init__.py +10 -0
  92. massgen/tool/_file_handlers/_file_operations.py +218 -0
  93. massgen/tool/_manager.py +634 -0
  94. massgen/tool/_registered_tool.py +88 -0
  95. massgen/tool/_result.py +66 -0
  96. massgen/tool/_self_evolution/_github_issue_analyzer.py +369 -0
  97. massgen/tool/docs/builtin_tools.md +681 -0
  98. massgen/tool/docs/exceptions.md +794 -0
  99. massgen/tool/docs/execution_results.md +691 -0
  100. massgen/tool/docs/manager.md +887 -0
  101. massgen/tool/docs/workflow_toolkits.md +529 -0
  102. massgen/tool/workflow_toolkits/__init__.py +57 -0
  103. massgen/tool/workflow_toolkits/base.py +55 -0
  104. massgen/tool/workflow_toolkits/new_answer.py +126 -0
  105. massgen/tool/workflow_toolkits/vote.py +167 -0
  106. {massgen-0.1.0a2.dist-info → massgen-0.1.1.dist-info}/METADATA +89 -131
  107. {massgen-0.1.0a2.dist-info → massgen-0.1.1.dist-info}/RECORD +111 -36
  108. {massgen-0.1.0a2.dist-info → massgen-0.1.1.dist-info}/WHEEL +0 -0
  109. {massgen-0.1.0a2.dist-info → massgen-0.1.1.dist-info}/entry_points.txt +0 -0
  110. {massgen-0.1.0a2.dist-info → massgen-0.1.1.dist-info}/licenses/LICENSE +0 -0
  111. {massgen-0.1.0a2.dist-info → massgen-0.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,401 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Test custom tools functionality in ResponseBackend.
4
+ """
5
+
6
+ import asyncio
7
+ import json
8
+ import os
9
+
10
+ # Add parent directory to path for imports
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ import pytest
15
+
16
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
17
+
18
+ from massgen.backend.response import ResponseBackend # noqa: E402
19
+ from massgen.tool import ExecutionResult, ToolManager # noqa: E402
20
+ from massgen.tool._result import TextContent # noqa: E402
21
+
22
+ # ============================================================================
23
+ # Sample custom tool functions for testing
24
+ # ============================================================================
25
+
26
+
27
+ def calculate_sum(a: int, b: int) -> ExecutionResult:
28
+ """Calculate sum of two numbers.
29
+
30
+ Args:
31
+ a: First number
32
+ b: Second number
33
+
34
+ Returns:
35
+ Sum of a and b
36
+ """
37
+ result = a + b
38
+ return ExecutionResult(
39
+ output_blocks=[TextContent(data=f"The sum of {a} and {b} is {result}")],
40
+ )
41
+
42
+
43
+ def string_manipulator(text: str, operation: str = "upper") -> ExecutionResult:
44
+ """Manipulate string based on operation.
45
+
46
+ Args:
47
+ text: Input string
48
+ operation: Operation to perform (upper, lower, reverse)
49
+
50
+ Returns:
51
+ Manipulated string
52
+ """
53
+ if operation == "upper":
54
+ result = text.upper()
55
+ elif operation == "lower":
56
+ result = text.lower()
57
+ elif operation == "reverse":
58
+ result = text[::-1]
59
+ else:
60
+ result = text
61
+
62
+ return ExecutionResult(
63
+ output_blocks=[TextContent(data=f"Result: {result}")],
64
+ )
65
+
66
+
67
+ async def async_weather_fetcher(city: str) -> ExecutionResult:
68
+ """Mock async function to fetch weather.
69
+
70
+ Args:
71
+ city: City name
72
+
73
+ Returns:
74
+ Mock weather data
75
+ """
76
+ # Simulate async operation
77
+ await asyncio.sleep(0.1)
78
+
79
+ weather_data = {
80
+ "New York": "Sunny, 25°C",
81
+ "London": "Cloudy, 18°C",
82
+ "Tokyo": "Rainy, 22°C",
83
+ }
84
+
85
+ weather = weather_data.get(city, "Unknown location")
86
+ return ExecutionResult(
87
+ output_blocks=[TextContent(data=f"Weather in {city}: {weather}")],
88
+ )
89
+
90
+
91
+ # ============================================================================
92
+ # Test ToolManager functionality
93
+ # ============================================================================
94
+
95
+
96
+ class TestToolManager:
97
+ """Test ToolManager class."""
98
+
99
+ def setup_method(self):
100
+ """Setup for each test."""
101
+ self.tool_manager = ToolManager()
102
+
103
+ def test_add_tool_function_direct(self):
104
+ """Test adding a tool function directly."""
105
+ self.tool_manager.add_tool_function(func=calculate_sum)
106
+
107
+ assert "calculate_sum" in self.tool_manager.registered_tools
108
+ tool_entry = self.tool_manager.registered_tools["calculate_sum"]
109
+ assert tool_entry.tool_name == "calculate_sum"
110
+ assert tool_entry.base_function == calculate_sum
111
+
112
+ def test_add_tool_with_string_name(self):
113
+ """Test adding a built-in tool by name."""
114
+ # This should find built-in functions from the tool module
115
+ try:
116
+ self.tool_manager.add_tool_function(func="read_file_content")
117
+ assert "read_file_content" in self.tool_manager.registered_tools
118
+ except ValueError:
119
+ # If built-in function not found, that's ok for this test
120
+ pass
121
+
122
+ def test_add_tool_with_path(self):
123
+ """Test adding a tool from a Python file."""
124
+ # Create a temporary Python file with a function
125
+ test_file = Path(__file__).parent / "temp_tool.py"
126
+ test_file.write_text(
127
+ """
128
+ def custom_function(x: int) -> str:
129
+ return f"Value: {x}"
130
+ """,
131
+ )
132
+
133
+ try:
134
+ self.tool_manager.add_tool_function(path=str(test_file))
135
+ assert "custom_function" in self.tool_manager.registered_tools
136
+ finally:
137
+ # Cleanup
138
+ if test_file.exists():
139
+ test_file.unlink()
140
+
141
+ def test_fetch_tool_schemas(self):
142
+ """Test fetching tool schemas."""
143
+ self.tool_manager.add_tool_function(func=calculate_sum)
144
+ self.tool_manager.add_tool_function(func=string_manipulator)
145
+
146
+ schemas = self.tool_manager.fetch_tool_schemas()
147
+ assert len(schemas) == 2
148
+
149
+ # Check schema format
150
+ for schema in schemas:
151
+ assert schema["type"] == "function"
152
+ assert "function" in schema
153
+ assert "name" in schema["function"]
154
+ assert "parameters" in schema["function"]
155
+
156
+ @pytest.mark.asyncio
157
+ async def test_execute_tool(self):
158
+ """Test executing a tool."""
159
+ self.tool_manager.add_tool_function(func=calculate_sum)
160
+
161
+ tool_request = {
162
+ "name": "calculate_sum",
163
+ "input": {"a": 5, "b": 3},
164
+ }
165
+
166
+ results = []
167
+ async for result in self.tool_manager.execute_tool(tool_request):
168
+ results.append(result)
169
+
170
+ assert len(results) > 0
171
+ result = results[0]
172
+ assert hasattr(result, "output_blocks")
173
+ assert "The sum of 5 and 3 is 8" in result.output_blocks[0].data
174
+
175
+ @pytest.mark.asyncio
176
+ async def test_execute_async_tool(self):
177
+ """Test executing an async tool."""
178
+ self.tool_manager.add_tool_function(func=async_weather_fetcher)
179
+
180
+ tool_request = {
181
+ "name": "async_weather_fetcher",
182
+ "input": {"city": "Tokyo"},
183
+ }
184
+
185
+ results = []
186
+ async for result in self.tool_manager.execute_tool(tool_request):
187
+ results.append(result)
188
+
189
+ assert len(results) > 0
190
+ result = results[0]
191
+ assert "Weather in Tokyo: Rainy, 22°C" in result.output_blocks[0].data
192
+
193
+
194
+ # ============================================================================
195
+ # Test ResponseBackend with custom tools
196
+ # ============================================================================
197
+
198
+
199
+ class TestResponseBackendCustomTools:
200
+ """Test ResponseBackend with custom tools integration."""
201
+
202
+ def setup_method(self):
203
+ """Setup for each test."""
204
+ self.api_key = os.getenv("OPENAI_API_KEY", "test-key")
205
+
206
+ def test_backend_initialization_with_custom_tools(self):
207
+ """Test initializing ResponseBackend with custom tools."""
208
+ custom_tools = [
209
+ {
210
+ "func": calculate_sum,
211
+ "description": "Calculate sum of two numbers",
212
+ },
213
+ {
214
+ "func": string_manipulator,
215
+ "category": "text",
216
+ "preset_args": {"operation": "upper"},
217
+ },
218
+ ]
219
+
220
+ backend = ResponseBackend(
221
+ api_key=self.api_key,
222
+ custom_tools=custom_tools,
223
+ )
224
+
225
+ # Check that tools were registered
226
+ assert len(backend._custom_tool_names) == 2
227
+ assert "calculate_sum" in backend._custom_tool_names
228
+ assert "string_manipulator" in backend._custom_tool_names
229
+
230
+ def test_get_custom_tools_schemas(self):
231
+ """Test getting custom tools schemas."""
232
+ custom_tools = [
233
+ {"func": calculate_sum},
234
+ {"func": string_manipulator},
235
+ ]
236
+
237
+ backend = ResponseBackend(
238
+ api_key=self.api_key,
239
+ custom_tools=custom_tools,
240
+ )
241
+
242
+ schemas = backend._get_custom_tools_schemas()
243
+ assert len(schemas) == 2
244
+
245
+ # Verify schema structure
246
+ for schema in schemas:
247
+ assert schema["type"] == "function"
248
+ function = schema["function"]
249
+ assert "name" in function
250
+ assert "parameters" in function
251
+ assert function["name"] in ["calculate_sum", "string_manipulator"]
252
+
253
+ @pytest.mark.asyncio
254
+ async def test_execute_custom_tool(self):
255
+ """Test executing a custom tool through the backend."""
256
+ backend = ResponseBackend(
257
+ api_key=self.api_key,
258
+ custom_tools=[{"func": calculate_sum}],
259
+ )
260
+
261
+ call = {
262
+ "name": "calculate_sum",
263
+ "call_id": "test_call_1",
264
+ "arguments": json.dumps({"a": 10, "b": 20}),
265
+ }
266
+
267
+ result = await backend._execute_custom_tool(call)
268
+ assert "The sum of 10 and 20 is 30" in result
269
+
270
+ @pytest.mark.asyncio
271
+ async def test_custom_tool_categorization(self):
272
+ """Test that custom tools are properly categorized in _stream_with_mcp_tools."""
273
+ backend = ResponseBackend(
274
+ api_key=self.api_key,
275
+ custom_tools=[
276
+ {"func": calculate_sum},
277
+ {"func": string_manipulator},
278
+ ],
279
+ )
280
+
281
+ # Simulate captured function calls
282
+ captured_calls = [
283
+ {"name": "calculate_sum", "call_id": "1", "arguments": '{"a": 1, "b": 2}'},
284
+ {"name": "web_search", "call_id": "2", "arguments": '{"query": "test"}'},
285
+ {"name": "unknown_mcp_tool", "call_id": "3", "arguments": "{}"},
286
+ ]
287
+
288
+ # Categorize calls (simulate the logic in _stream_with_mcp_tools)
289
+ mcp_calls = []
290
+ custom_calls = []
291
+ provider_calls = []
292
+
293
+ for call in captured_calls:
294
+ if call["name"] in backend._mcp_functions:
295
+ mcp_calls.append(call)
296
+ elif call["name"] in backend._custom_tool_names:
297
+ custom_calls.append(call)
298
+ else:
299
+ provider_calls.append(call)
300
+
301
+ # Verify categorization
302
+ assert len(custom_calls) == 1
303
+ assert custom_calls[0]["name"] == "calculate_sum"
304
+
305
+ assert len(provider_calls) == 2
306
+ assert "web_search" in [c["name"] for c in provider_calls]
307
+ assert "unknown_mcp_tool" in [c["name"] for c in provider_calls]
308
+
309
+ assert len(mcp_calls) == 0 # No MCP tools in this test
310
+
311
+
312
+ # ============================================================================
313
+ # Integration test with mock streaming
314
+ # ============================================================================
315
+
316
+
317
+ class TestCustomToolsIntegration:
318
+ """Integration tests for custom tools with streaming."""
319
+
320
+ @pytest.mark.asyncio
321
+ async def test_custom_tool_execution_flow(self):
322
+ """Test the complete flow of custom tool execution."""
323
+ # Create backend with custom tools
324
+ backend = ResponseBackend(
325
+ api_key=os.getenv("OPENAI_API_KEY", "test-key"),
326
+ custom_tools=[
327
+ {"func": calculate_sum, "description": "Add two numbers"},
328
+ {"func": async_weather_fetcher, "description": "Get weather info"},
329
+ ],
330
+ )
331
+
332
+ # Verify tools are registered
333
+ assert "calculate_sum" in backend._custom_tool_names
334
+ assert "async_weather_fetcher" in backend._custom_tool_names
335
+
336
+ # Test tool execution
337
+ call = {
338
+ "name": "async_weather_fetcher",
339
+ "call_id": "test_weather",
340
+ "arguments": json.dumps({"city": "London"}),
341
+ }
342
+
343
+ result = await backend._execute_custom_tool(call)
344
+ assert "Weather in London: Cloudy, 18°C" in result
345
+
346
+ def test_custom_tool_error_handling(self):
347
+ """Test error handling in custom tools."""
348
+
349
+ def faulty_tool(x: int) -> ExecutionResult:
350
+ raise ValueError("Intentional error")
351
+
352
+ backend = ResponseBackend(
353
+ api_key="test-key",
354
+ custom_tools=[{"func": faulty_tool}],
355
+ )
356
+
357
+ assert "faulty_tool" in backend._custom_tool_names
358
+
359
+ @pytest.mark.asyncio
360
+ async def test_mixed_tools_categorization(self):
361
+ """Test categorization with mixed tool types."""
362
+ backend = ResponseBackend(
363
+ api_key="test-key",
364
+ custom_tools=[{"func": calculate_sum}],
365
+ )
366
+
367
+ # Mock some MCP functions
368
+ backend._mcp_functions = {"mcp_tool": None}
369
+ backend._mcp_function_names = {"mcp_tool"}
370
+
371
+ # Test categorization logic
372
+ test_calls = [
373
+ {"name": "calculate_sum", "call_id": "1", "arguments": "{}"}, # Custom
374
+ {"name": "mcp_tool", "call_id": "2", "arguments": "{}"}, # MCP
375
+ {"name": "web_search", "call_id": "3", "arguments": "{}"}, # Provider
376
+ ]
377
+
378
+ custom = []
379
+ mcp = []
380
+ provider = []
381
+
382
+ for call in test_calls:
383
+ if call["name"] in backend._mcp_functions:
384
+ mcp.append(call)
385
+ elif call["name"] in backend._custom_tool_names:
386
+ custom.append(call)
387
+ else:
388
+ provider.append(call)
389
+
390
+ assert len(custom) == 1 and custom[0]["name"] == "calculate_sum"
391
+ assert len(mcp) == 1 and mcp[0]["name"] == "mcp_tool"
392
+ assert len(provider) == 1 and provider[0]["name"] == "web_search"
393
+
394
+
395
+ # ============================================================================
396
+ # Run tests
397
+ # ============================================================================
398
+
399
+ if __name__ == "__main__":
400
+ # Run pytest
401
+ pytest.main([__file__, "-v"])
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Test script for MassGen tool implementation."""
4
+
5
+ import asyncio
6
+
7
+ from massgen.tool._code_executors import run_python_script
8
+ from massgen.tool._file_handlers import read_file_content
9
+ from massgen.tool._manager import ToolManager
10
+ from massgen.tool._result import ExecutionResult, TextContent
11
+
12
+
13
+ async def sample_math_tool(x: int, y: int) -> ExecutionResult:
14
+ """Add two numbers together.
15
+
16
+ Args:
17
+ x: First number
18
+ y: Second number
19
+
20
+ Returns:
21
+ Sum of the two numbers
22
+ """
23
+ result = x + y
24
+ return ExecutionResult(
25
+ output_blocks=[
26
+ TextContent(data=f"The sum of {x} and {y} is {result}"),
27
+ ],
28
+ )
29
+
30
+
31
+ async def test_tool_manager():
32
+ """Test the tool manager functionality."""
33
+ print("Testing MassGen Tool Manager\n" + "=" * 40)
34
+
35
+ # Create manager
36
+ manager = ToolManager()
37
+ print("✓ Tool manager created")
38
+
39
+ # Create categories
40
+ manager.setup_category(
41
+ category_name="math",
42
+ description="Mathematical operations",
43
+ enabled=True,
44
+ usage_hints="Use these tools for calculations",
45
+ )
46
+ print("✓ Created 'math' category")
47
+
48
+ manager.setup_category(
49
+ category_name="file_ops",
50
+ description="File operations",
51
+ enabled=False,
52
+ )
53
+ print("✓ Created 'file_ops' category")
54
+
55
+ # Register tools
56
+ manager.add_tool_function(
57
+ func=sample_math_tool,
58
+ category="math",
59
+ description="Adds two numbers",
60
+ )
61
+ print("✓ Registered sample_math_tool")
62
+
63
+ manager.add_tool_function(
64
+ func=run_python_script,
65
+ category="default",
66
+ description="Execute Python code",
67
+ )
68
+ print("✓ Registered run_python_script")
69
+
70
+ manager.add_tool_function(
71
+ func=read_file_content,
72
+ category="file_ops",
73
+ description="Read file contents",
74
+ )
75
+ print("✓ Registered read_file_content")
76
+
77
+ # Get schemas
78
+ schemas = manager.fetch_tool_schemas()
79
+ print(f"\n✓ Active tool schemas: {len(schemas)} tools")
80
+ for schema in schemas:
81
+ print(f" - {schema['function']['name']}: {schema['function'].get('description', 'No description')}")
82
+
83
+ # Test tool execution
84
+ print("\nTesting tool execution:")
85
+
86
+ # Test math tool
87
+ tool_request = {
88
+ "name": "sample_math_tool",
89
+ "input": {"x": 5, "y": 3},
90
+ }
91
+
92
+ async for result in manager.execute_tool(tool_request):
93
+ print(f" Math result: {result.output_blocks[0].data}")
94
+
95
+ # Test Python execution
96
+ python_request = {
97
+ "name": "run_python_script",
98
+ "input": {
99
+ "source_code": "print('Hello from MassGen!')\nprint(2 + 2)",
100
+ },
101
+ }
102
+
103
+ async for result in manager.execute_tool(python_request):
104
+ output = result.output_blocks[0].data
105
+ if "<stdout>" in output:
106
+ stdout_start = output.find("<stdout>") + 8
107
+ stdout_end = output.find("</stdout>")
108
+ stdout = output[stdout_start:stdout_end]
109
+ print(f" Python output: {stdout.strip()}")
110
+
111
+ # Test enabling file_ops category
112
+ print("\nEnabling file_ops category...")
113
+ manager.modify_categories(["file_ops"], enabled=True)
114
+ schemas_after = manager.fetch_tool_schemas()
115
+ print(f"✓ Active tools after enabling: {len(schemas_after)}")
116
+
117
+ # Get category hints
118
+ hints = manager.fetch_category_hints()
119
+ if hints:
120
+ print(f"\nCategory hints:\n{hints}")
121
+
122
+ print("\n" + "=" * 40)
123
+ print("All tests completed successfully! ✓")
124
+
125
+
126
+ if __name__ == "__main__":
127
+ asyncio.run(test_tool_manager())