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.
- massgen/__init__.py +1 -1
- massgen/agent_config.py +17 -0
- massgen/api_params_handler/_api_params_handler_base.py +1 -0
- massgen/api_params_handler/_chat_completions_api_params_handler.py +8 -1
- massgen/api_params_handler/_claude_api_params_handler.py +8 -1
- massgen/api_params_handler/_gemini_api_params_handler.py +73 -0
- massgen/api_params_handler/_response_api_params_handler.py +8 -1
- massgen/backend/base.py +31 -0
- massgen/backend/{base_with_mcp.py → base_with_custom_tool_and_mcp.py} +282 -11
- massgen/backend/chat_completions.py +182 -92
- massgen/backend/claude.py +115 -18
- massgen/backend/claude_code.py +378 -14
- massgen/backend/docs/CLAUDE_API_RESEARCH.md +3 -3
- massgen/backend/gemini.py +1275 -1607
- massgen/backend/gemini_mcp_manager.py +545 -0
- massgen/backend/gemini_trackers.py +344 -0
- massgen/backend/gemini_utils.py +43 -0
- massgen/backend/response.py +129 -70
- massgen/cli.py +643 -132
- massgen/config_builder.py +381 -32
- massgen/configs/README.md +111 -80
- massgen/configs/basic/multi/three_agents_default.yaml +1 -1
- massgen/configs/basic/single/single_agent.yaml +1 -1
- massgen/configs/providers/openai/gpt5_nano.yaml +3 -3
- massgen/configs/tools/custom_tools/claude_code_custom_tool_example.yaml +32 -0
- massgen/configs/tools/custom_tools/claude_code_custom_tool_example_no_path.yaml +28 -0
- massgen/configs/tools/custom_tools/claude_code_custom_tool_with_mcp_example.yaml +40 -0
- massgen/configs/tools/custom_tools/claude_code_custom_tool_with_wrong_mcp_example.yaml +38 -0
- massgen/configs/tools/custom_tools/claude_code_wrong_custom_tool_with_mcp_example.yaml +38 -0
- massgen/configs/tools/custom_tools/claude_custom_tool_example.yaml +24 -0
- massgen/configs/tools/custom_tools/claude_custom_tool_example_no_path.yaml +22 -0
- massgen/configs/tools/custom_tools/claude_custom_tool_with_mcp_example.yaml +35 -0
- massgen/configs/tools/custom_tools/claude_custom_tool_with_wrong_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/claude_wrong_custom_tool_with_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/gemini_custom_tool_example.yaml +24 -0
- massgen/configs/tools/custom_tools/gemini_custom_tool_example_no_path.yaml +22 -0
- massgen/configs/tools/custom_tools/gemini_custom_tool_with_mcp_example.yaml +35 -0
- massgen/configs/tools/custom_tools/gemini_custom_tool_with_wrong_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/gemini_wrong_custom_tool_with_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/github_issue_market_analysis.yaml +94 -0
- massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example.yaml +24 -0
- massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example_no_path.yaml +22 -0
- massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_mcp_example.yaml +35 -0
- massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_wrong_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/gpt5_nano_wrong_custom_tool_with_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example.yaml +25 -0
- massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example_no_path.yaml +23 -0
- massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_mcp_example.yaml +34 -0
- massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_wrong_mcp_example.yaml +34 -0
- massgen/configs/tools/custom_tools/gpt_oss_wrong_custom_tool_with_mcp_example.yaml +34 -0
- massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example.yaml +24 -0
- massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example_no_path.yaml +22 -0
- massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_mcp_example.yaml +35 -0
- massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_wrong_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/grok3_mini_wrong_custom_tool_with_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/qwen_api_custom_tool_example.yaml +25 -0
- massgen/configs/tools/custom_tools/qwen_api_custom_tool_example_no_path.yaml +23 -0
- massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_mcp_example.yaml +36 -0
- massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_wrong_mcp_example.yaml +34 -0
- massgen/configs/tools/custom_tools/qwen_api_wrong_custom_tool_with_mcp_example.yaml +34 -0
- massgen/configs/tools/custom_tools/qwen_local_custom_tool_example.yaml +24 -0
- massgen/configs/tools/custom_tools/qwen_local_custom_tool_example_no_path.yaml +22 -0
- massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_mcp_example.yaml +35 -0
- massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_wrong_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/qwen_local_wrong_custom_tool_with_mcp_example.yaml +33 -0
- massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +1 -1
- massgen/configs/voting/gemini_gpt_voting_sensitivity.yaml +67 -0
- massgen/formatter/_chat_completions_formatter.py +104 -0
- massgen/formatter/_claude_formatter.py +120 -0
- massgen/formatter/_gemini_formatter.py +448 -0
- massgen/formatter/_response_formatter.py +88 -0
- massgen/frontend/coordination_ui.py +4 -2
- massgen/logger_config.py +35 -3
- massgen/message_templates.py +56 -6
- massgen/orchestrator.py +179 -10
- massgen/stream_chunk/base.py +3 -0
- massgen/tests/custom_tools_example.py +392 -0
- massgen/tests/mcp_test_server.py +17 -7
- massgen/tests/test_config_builder.py +423 -0
- massgen/tests/test_custom_tools.py +401 -0
- massgen/tests/test_tools.py +127 -0
- massgen/tool/README.md +935 -0
- massgen/tool/__init__.py +39 -0
- massgen/tool/_async_helpers.py +70 -0
- massgen/tool/_basic/__init__.py +8 -0
- massgen/tool/_basic/_two_num_tool.py +24 -0
- massgen/tool/_code_executors/__init__.py +10 -0
- massgen/tool/_code_executors/_python_executor.py +74 -0
- massgen/tool/_code_executors/_shell_executor.py +61 -0
- massgen/tool/_exceptions.py +39 -0
- massgen/tool/_file_handlers/__init__.py +10 -0
- massgen/tool/_file_handlers/_file_operations.py +218 -0
- massgen/tool/_manager.py +634 -0
- massgen/tool/_registered_tool.py +88 -0
- massgen/tool/_result.py +66 -0
- massgen/tool/_self_evolution/_github_issue_analyzer.py +369 -0
- massgen/tool/docs/builtin_tools.md +681 -0
- massgen/tool/docs/exceptions.md +794 -0
- massgen/tool/docs/execution_results.md +691 -0
- massgen/tool/docs/manager.md +887 -0
- massgen/tool/docs/workflow_toolkits.md +529 -0
- massgen/tool/workflow_toolkits/__init__.py +57 -0
- massgen/tool/workflow_toolkits/base.py +55 -0
- massgen/tool/workflow_toolkits/new_answer.py +126 -0
- massgen/tool/workflow_toolkits/vote.py +167 -0
- {massgen-0.1.0a2.dist-info → massgen-0.1.1.dist-info}/METADATA +89 -131
- {massgen-0.1.0a2.dist-info → massgen-0.1.1.dist-info}/RECORD +111 -36
- {massgen-0.1.0a2.dist-info → massgen-0.1.1.dist-info}/WHEEL +0 -0
- {massgen-0.1.0a2.dist-info → massgen-0.1.1.dist-info}/entry_points.txt +0 -0
- {massgen-0.1.0a2.dist-info → massgen-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {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())
|