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
massgen/tool/_manager.py
ADDED
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Tool management system for MassGen."""
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import importlib
|
|
6
|
+
import importlib.util
|
|
7
|
+
import inspect
|
|
8
|
+
import sys
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from functools import partial
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, AsyncGenerator, Callable, Dict, Generator, List, Optional, Type
|
|
13
|
+
|
|
14
|
+
from docstring_parser import parse
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field, create_model
|
|
16
|
+
|
|
17
|
+
from ._async_helpers import (
|
|
18
|
+
wrap_as_async_generator,
|
|
19
|
+
wrap_object_async,
|
|
20
|
+
wrap_sync_gen_async,
|
|
21
|
+
)
|
|
22
|
+
from ._registered_tool import RegisteredToolEntry
|
|
23
|
+
from ._result import ExecutionResult, TextContent
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ToolCategory:
|
|
28
|
+
"""Tool category configuration."""
|
|
29
|
+
|
|
30
|
+
category_name: str
|
|
31
|
+
"""Category identifier for grouping tools."""
|
|
32
|
+
|
|
33
|
+
is_enabled: bool
|
|
34
|
+
"""Whether tools in this category are active."""
|
|
35
|
+
|
|
36
|
+
category_desc: str
|
|
37
|
+
"""Description of the tool category."""
|
|
38
|
+
|
|
39
|
+
usage_hints: Optional[str] = None
|
|
40
|
+
"""Usage guidelines for tools in this category."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ToolManager:
|
|
44
|
+
"""Manager class for tool registration and execution.
|
|
45
|
+
|
|
46
|
+
Provides methods for:
|
|
47
|
+
- Tool registration: `add_tool_function`
|
|
48
|
+
- Tool removal: `delete_tool_function`
|
|
49
|
+
- Category management: `setup_category`, `modify_categories`, `delete_categories`
|
|
50
|
+
- Schema retrieval: `fetch_tool_schemas`
|
|
51
|
+
- Tool execution: `execute_tool`
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self) -> None:
|
|
55
|
+
"""Initialize the tool manager."""
|
|
56
|
+
self.registered_tools: Dict[str, RegisteredToolEntry] = {}
|
|
57
|
+
self.tool_categories: Dict[str, ToolCategory] = {}
|
|
58
|
+
|
|
59
|
+
def setup_category(
|
|
60
|
+
self,
|
|
61
|
+
category_name: str,
|
|
62
|
+
description: str,
|
|
63
|
+
enabled: bool = False,
|
|
64
|
+
usage_hints: Optional[str] = None,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Create a new tool category.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
category_name: Name of the category
|
|
70
|
+
description: Category description
|
|
71
|
+
enabled: Whether category is initially active
|
|
72
|
+
usage_hints: Optional usage guidelines
|
|
73
|
+
"""
|
|
74
|
+
if category_name in self.tool_categories or category_name == "default":
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"Category '{category_name}' already exists or is reserved.",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
self.tool_categories[category_name] = ToolCategory(
|
|
80
|
+
category_name=category_name,
|
|
81
|
+
category_desc=description,
|
|
82
|
+
usage_hints=usage_hints,
|
|
83
|
+
is_enabled=enabled,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def modify_categories(self, category_list: List[str], enabled: bool) -> None:
|
|
87
|
+
"""Update the activation status of categories.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
category_list: List of category names
|
|
91
|
+
enabled: New activation status
|
|
92
|
+
"""
|
|
93
|
+
for cat_name in category_list:
|
|
94
|
+
if cat_name == "default":
|
|
95
|
+
continue # Default category is always active
|
|
96
|
+
|
|
97
|
+
if cat_name in self.tool_categories:
|
|
98
|
+
self.tool_categories[cat_name].is_enabled = enabled
|
|
99
|
+
|
|
100
|
+
def delete_categories(self, category_list: List[str]) -> None:
|
|
101
|
+
"""Remove categories and their associated tools.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
category_list: Categories to remove
|
|
105
|
+
"""
|
|
106
|
+
if isinstance(category_list, str):
|
|
107
|
+
category_list = [category_list]
|
|
108
|
+
|
|
109
|
+
if "default" in category_list:
|
|
110
|
+
raise ValueError("Cannot remove the default category.")
|
|
111
|
+
|
|
112
|
+
for cat_name in category_list:
|
|
113
|
+
self.tool_categories.pop(cat_name, None)
|
|
114
|
+
|
|
115
|
+
# Remove tools in deleted categories
|
|
116
|
+
tool_list = list(self.registered_tools.keys())
|
|
117
|
+
for tool_name in tool_list:
|
|
118
|
+
if self.registered_tools[tool_name].category in category_list:
|
|
119
|
+
self.registered_tools.pop(tool_name)
|
|
120
|
+
|
|
121
|
+
def add_tool_function(
|
|
122
|
+
self,
|
|
123
|
+
path: Optional[str] = None,
|
|
124
|
+
func: Optional[Callable] = None,
|
|
125
|
+
category: str = "default",
|
|
126
|
+
preset_args: Optional[Dict[str, Any]] = None,
|
|
127
|
+
description: Optional[str] = None,
|
|
128
|
+
tool_schema: Optional[dict] = None,
|
|
129
|
+
include_full_desc: bool = True,
|
|
130
|
+
allow_var_args: bool = False,
|
|
131
|
+
allow_var_kwargs: bool = False,
|
|
132
|
+
post_processor: Optional[Callable] = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Register a tool function.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
path: Optional path to Python file or module. If None, use func parameter
|
|
138
|
+
func: The tool function to register (required if path is None)
|
|
139
|
+
category: Category for the tool
|
|
140
|
+
preset_args: Arguments to preset (hidden from schema)
|
|
141
|
+
description: Optional function description
|
|
142
|
+
tool_schema: Optional manual JSON schema
|
|
143
|
+
include_full_desc: Include long description from docstring
|
|
144
|
+
allow_var_args: Include *args in schema
|
|
145
|
+
allow_var_kwargs: Include **kwargs in schema
|
|
146
|
+
post_processor: Optional post-processing function
|
|
147
|
+
"""
|
|
148
|
+
if category not in self.tool_categories and category != "default":
|
|
149
|
+
raise ValueError(f"Category '{category}' not found.")
|
|
150
|
+
|
|
151
|
+
# Handle function loading
|
|
152
|
+
if isinstance(func, str):
|
|
153
|
+
# func is a string - treat it as function name to load
|
|
154
|
+
func_name = func
|
|
155
|
+
if path is not None:
|
|
156
|
+
# Load from specified path
|
|
157
|
+
func = self._load_function_from_path(path, func_name)
|
|
158
|
+
if func is None:
|
|
159
|
+
raise ValueError(f"Could not load function '{func_name}' from path: {path}")
|
|
160
|
+
else:
|
|
161
|
+
# No path specified - try to find in tool folder
|
|
162
|
+
func = self._load_builtin_function(func_name)
|
|
163
|
+
if func is None:
|
|
164
|
+
raise ValueError(f"Could not find built-in function: {func_name}")
|
|
165
|
+
elif func is None:
|
|
166
|
+
# No func provided at all
|
|
167
|
+
if path is not None:
|
|
168
|
+
# Try to load from path (will auto-detect function)
|
|
169
|
+
func = self._load_function_from_path(path, None)
|
|
170
|
+
if func is None:
|
|
171
|
+
raise ValueError(f"Could not load function from path: {path}")
|
|
172
|
+
else:
|
|
173
|
+
raise ValueError("Either 'path' or 'func' must be provided")
|
|
174
|
+
elif not callable(func):
|
|
175
|
+
raise ValueError("'func' must be a callable or a string (function name)")
|
|
176
|
+
|
|
177
|
+
# Validate schema if provided
|
|
178
|
+
if tool_schema:
|
|
179
|
+
assert isinstance(tool_schema, dict) and "type" in tool_schema and tool_schema["type"] == "function", "Invalid tool schema format."
|
|
180
|
+
|
|
181
|
+
# Handle partial functions
|
|
182
|
+
if isinstance(func, partial):
|
|
183
|
+
func_kwargs = func.keywords.copy()
|
|
184
|
+
if func.args:
|
|
185
|
+
param_list = list(inspect.signature(func.func).parameters.keys())
|
|
186
|
+
for idx, arg_val in enumerate(func.args):
|
|
187
|
+
if idx < len(param_list):
|
|
188
|
+
func_kwargs[param_list[idx]] = arg_val
|
|
189
|
+
|
|
190
|
+
preset_args = {**func_kwargs, **(preset_args or {})}
|
|
191
|
+
tool_name = func.func.__name__
|
|
192
|
+
base_func = func.func
|
|
193
|
+
tool_schema = tool_schema or self._extract_tool_schema(
|
|
194
|
+
func.func,
|
|
195
|
+
include_full_desc,
|
|
196
|
+
allow_var_args,
|
|
197
|
+
allow_var_kwargs,
|
|
198
|
+
)
|
|
199
|
+
else:
|
|
200
|
+
tool_name = func.__name__
|
|
201
|
+
base_func = func
|
|
202
|
+
tool_schema = tool_schema or self._extract_tool_schema(
|
|
203
|
+
func,
|
|
204
|
+
include_full_desc,
|
|
205
|
+
allow_var_args,
|
|
206
|
+
allow_var_kwargs,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Add prefix for custom tools (not built-in ones)
|
|
210
|
+
if category != "builtin" and not tool_name.startswith("custom_tool__"):
|
|
211
|
+
tool_name = f"custom_tool__{tool_name}"
|
|
212
|
+
# Update the schema to reflect the new name
|
|
213
|
+
tool_schema["function"]["name"] = tool_name
|
|
214
|
+
|
|
215
|
+
# Check for duplicate names
|
|
216
|
+
if tool_name in self.registered_tools:
|
|
217
|
+
raise ValueError(f"Tool '{tool_name}' is already registered.")
|
|
218
|
+
|
|
219
|
+
# Override description if provided
|
|
220
|
+
if description:
|
|
221
|
+
tool_schema["function"]["description"] = description
|
|
222
|
+
|
|
223
|
+
# Remove preset args from schema
|
|
224
|
+
for arg in preset_args or {}:
|
|
225
|
+
if arg in tool_schema["function"]["parameters"]["properties"]:
|
|
226
|
+
tool_schema["function"]["parameters"]["properties"].pop(arg)
|
|
227
|
+
|
|
228
|
+
if "required" in tool_schema["function"]["parameters"]:
|
|
229
|
+
if arg in tool_schema["function"]["parameters"]["required"]:
|
|
230
|
+
tool_schema["function"]["parameters"]["required"].remove(arg)
|
|
231
|
+
|
|
232
|
+
if not tool_schema["function"]["parameters"]["required"]:
|
|
233
|
+
tool_schema["function"]["parameters"].pop("required", None)
|
|
234
|
+
|
|
235
|
+
tool_entry = RegisteredToolEntry(
|
|
236
|
+
tool_name=tool_name,
|
|
237
|
+
category=category,
|
|
238
|
+
origin="function",
|
|
239
|
+
base_function=base_func,
|
|
240
|
+
schema_def=tool_schema,
|
|
241
|
+
preset_params=preset_args or {},
|
|
242
|
+
extension_model=None,
|
|
243
|
+
post_processor=post_processor,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
self.registered_tools[tool_name] = tool_entry
|
|
247
|
+
|
|
248
|
+
def delete_tool_function(self, tool_name: str) -> None:
|
|
249
|
+
"""Remove a tool function by name.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
tool_name: Name of tool to remove
|
|
253
|
+
"""
|
|
254
|
+
self.registered_tools.pop(tool_name, None)
|
|
255
|
+
|
|
256
|
+
def fetch_tool_schemas(self) -> List[dict]:
|
|
257
|
+
"""Get JSON schemas for all active tools.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
List of tool JSON schemas
|
|
261
|
+
"""
|
|
262
|
+
schemas = []
|
|
263
|
+
for tool in self.registered_tools.values():
|
|
264
|
+
if tool.category == "default":
|
|
265
|
+
schemas.append(tool.get_extended_schema)
|
|
266
|
+
elif tool.category in self.tool_categories:
|
|
267
|
+
if self.tool_categories[tool.category].is_enabled:
|
|
268
|
+
schemas.append(tool.get_extended_schema)
|
|
269
|
+
return schemas
|
|
270
|
+
|
|
271
|
+
def apply_extension_model(
|
|
272
|
+
self,
|
|
273
|
+
tool_name: str,
|
|
274
|
+
model_class: Optional[Type[BaseModel]],
|
|
275
|
+
) -> None:
|
|
276
|
+
"""Apply an extension model to a tool's schema.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
tool_name: Name of the tool
|
|
280
|
+
model_class: Pydantic model to extend schema with
|
|
281
|
+
"""
|
|
282
|
+
if model_class and not issubclass(model_class, BaseModel):
|
|
283
|
+
raise TypeError("Extension model must be a Pydantic BaseModel.")
|
|
284
|
+
|
|
285
|
+
if tool_name in self.registered_tools:
|
|
286
|
+
self.registered_tools[tool_name].extension_model = model_class
|
|
287
|
+
else:
|
|
288
|
+
raise ValueError(f"Tool '{tool_name}' not found.")
|
|
289
|
+
|
|
290
|
+
async def execute_tool(
|
|
291
|
+
self,
|
|
292
|
+
tool_request: dict,
|
|
293
|
+
) -> AsyncGenerator[ExecutionResult, None]:
|
|
294
|
+
"""Execute a tool and return results as async generator.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
tool_request: Tool execution request with name and input
|
|
298
|
+
|
|
299
|
+
Yields:
|
|
300
|
+
ExecutionResult objects (accumulated)
|
|
301
|
+
"""
|
|
302
|
+
tool_name = tool_request.get("name")
|
|
303
|
+
|
|
304
|
+
if tool_name not in self.registered_tools:
|
|
305
|
+
yield ExecutionResult(
|
|
306
|
+
output_blocks=[
|
|
307
|
+
TextContent(
|
|
308
|
+
data=f"ToolNotFound: No tool named '{tool_name}' exists",
|
|
309
|
+
),
|
|
310
|
+
],
|
|
311
|
+
)
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
tool_entry = self.registered_tools[tool_name]
|
|
315
|
+
exec_kwargs = {
|
|
316
|
+
**tool_entry.preset_params,
|
|
317
|
+
**(tool_request.get("input", {}) or {}),
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
# Prepare post-processor if exists
|
|
321
|
+
if tool_entry.post_processor:
|
|
322
|
+
post_proc_partial = partial(
|
|
323
|
+
tool_entry.post_processor,
|
|
324
|
+
tool_request,
|
|
325
|
+
)
|
|
326
|
+
else:
|
|
327
|
+
post_proc_partial = None
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
# Execute based on function type
|
|
331
|
+
if inspect.iscoroutinefunction(tool_entry.base_function):
|
|
332
|
+
try:
|
|
333
|
+
result = await tool_entry.base_function(**exec_kwargs)
|
|
334
|
+
except asyncio.CancelledError:
|
|
335
|
+
result = ExecutionResult(
|
|
336
|
+
output_blocks=[
|
|
337
|
+
TextContent(
|
|
338
|
+
data="<system>Tool execution was interrupted</system>",
|
|
339
|
+
),
|
|
340
|
+
],
|
|
341
|
+
is_streaming=True,
|
|
342
|
+
is_final=True,
|
|
343
|
+
was_interrupted=True,
|
|
344
|
+
)
|
|
345
|
+
else:
|
|
346
|
+
result = tool_entry.base_function(**exec_kwargs)
|
|
347
|
+
|
|
348
|
+
except Exception as err:
|
|
349
|
+
result = ExecutionResult(
|
|
350
|
+
output_blocks=[
|
|
351
|
+
TextContent(data=f"Error: {err}"),
|
|
352
|
+
],
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
# Handle different return types
|
|
356
|
+
if isinstance(result, AsyncGenerator):
|
|
357
|
+
async for item in wrap_as_async_generator(result, post_proc_partial):
|
|
358
|
+
yield item
|
|
359
|
+
elif isinstance(result, Generator):
|
|
360
|
+
async for item in wrap_sync_gen_async(result, post_proc_partial):
|
|
361
|
+
yield item
|
|
362
|
+
elif isinstance(result, ExecutionResult):
|
|
363
|
+
async for item in wrap_object_async(result, post_proc_partial):
|
|
364
|
+
yield item
|
|
365
|
+
else:
|
|
366
|
+
raise TypeError(
|
|
367
|
+
f"Tool must return ExecutionResult or Generator, got {type(result)}",
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
def fetch_category_hints(self) -> str:
|
|
371
|
+
"""Get usage hints from active categories.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Combined usage hints string
|
|
375
|
+
"""
|
|
376
|
+
hints_list = []
|
|
377
|
+
for cat_name, category in self.tool_categories.items():
|
|
378
|
+
if category.is_enabled and category.usage_hints:
|
|
379
|
+
hints_list.append(
|
|
380
|
+
f"## {cat_name} Tools\n{category.usage_hints}",
|
|
381
|
+
)
|
|
382
|
+
return "\n".join(hints_list)
|
|
383
|
+
|
|
384
|
+
def reset_state(self) -> None:
|
|
385
|
+
"""Clear all registered tools and categories."""
|
|
386
|
+
self.registered_tools.clear()
|
|
387
|
+
self.tool_categories.clear()
|
|
388
|
+
|
|
389
|
+
def _load_builtin_function(self, func_name: str) -> Optional[Callable]:
|
|
390
|
+
"""Load a built-in function from the tool folder.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
func_name: Name of the function to load
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
The loaded function or None if not found
|
|
397
|
+
"""
|
|
398
|
+
# Try to import from tool module submodules
|
|
399
|
+
tool_modules = [
|
|
400
|
+
"_basic",
|
|
401
|
+
"_code_executors",
|
|
402
|
+
"_file_handlers",
|
|
403
|
+
"_multimedia_processors",
|
|
404
|
+
"workflow_toolkits",
|
|
405
|
+
]
|
|
406
|
+
|
|
407
|
+
for module_name in tool_modules:
|
|
408
|
+
try:
|
|
409
|
+
full_module_name = f"massgen.tool.{module_name}"
|
|
410
|
+
module = importlib.import_module(full_module_name)
|
|
411
|
+
if hasattr(module, func_name):
|
|
412
|
+
return getattr(module, func_name)
|
|
413
|
+
except ImportError:
|
|
414
|
+
continue
|
|
415
|
+
|
|
416
|
+
# Try to find in __init__.py exports
|
|
417
|
+
try:
|
|
418
|
+
tool_module = importlib.import_module("massgen.tool")
|
|
419
|
+
if hasattr(tool_module, func_name):
|
|
420
|
+
return getattr(tool_module, func_name)
|
|
421
|
+
except ImportError:
|
|
422
|
+
pass
|
|
423
|
+
|
|
424
|
+
# Search in all Python files in tool folder
|
|
425
|
+
tool_folder = Path(__file__).parent
|
|
426
|
+
for py_file in tool_folder.glob("*.py"):
|
|
427
|
+
if py_file.stem == "__init__" or py_file.stem == "_manager":
|
|
428
|
+
continue
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
spec = importlib.util.spec_from_file_location(py_file.stem, py_file)
|
|
432
|
+
if spec and spec.loader:
|
|
433
|
+
module = importlib.util.module_from_spec(spec)
|
|
434
|
+
spec.loader.exec_module(module)
|
|
435
|
+
if hasattr(module, func_name):
|
|
436
|
+
return getattr(module, func_name)
|
|
437
|
+
except Exception:
|
|
438
|
+
continue
|
|
439
|
+
|
|
440
|
+
return None
|
|
441
|
+
|
|
442
|
+
def _load_function_from_path(
|
|
443
|
+
self,
|
|
444
|
+
path: str,
|
|
445
|
+
func_name: Optional[str] = None,
|
|
446
|
+
) -> Optional[Callable]:
|
|
447
|
+
"""Load a function from a given path.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
path: Path to Python file or module name
|
|
451
|
+
func_name: Optional specific function name to load
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
The loaded function or None if not found
|
|
455
|
+
"""
|
|
456
|
+
# If path doesn't contain file extension, try to find it in tool folder
|
|
457
|
+
if not path.endswith(".py"):
|
|
458
|
+
# Try to import from tool module
|
|
459
|
+
try:
|
|
460
|
+
# First try as a submodule of tool
|
|
461
|
+
module_name = f"massgen.tool.{path}"
|
|
462
|
+
module = importlib.import_module(module_name)
|
|
463
|
+
except ImportError:
|
|
464
|
+
# Try to find in tool folder's Python files
|
|
465
|
+
tool_folder = Path(__file__).parent
|
|
466
|
+
possible_files = [
|
|
467
|
+
tool_folder / f"{path}.py",
|
|
468
|
+
tool_folder / f"_{path}.py",
|
|
469
|
+
]
|
|
470
|
+
|
|
471
|
+
for file_path in possible_files:
|
|
472
|
+
if file_path.exists():
|
|
473
|
+
path = str(file_path)
|
|
474
|
+
break
|
|
475
|
+
else:
|
|
476
|
+
# If still not found, try as a direct module import
|
|
477
|
+
try:
|
|
478
|
+
module = importlib.import_module(path)
|
|
479
|
+
except ImportError:
|
|
480
|
+
return None
|
|
481
|
+
|
|
482
|
+
# If path is a file path, load it dynamically
|
|
483
|
+
if path.endswith(".py"):
|
|
484
|
+
path_obj = Path(path)
|
|
485
|
+
|
|
486
|
+
# If path is not absolute, try multiple resolution strategies
|
|
487
|
+
if not path_obj.is_absolute():
|
|
488
|
+
# First try as relative to current working directory
|
|
489
|
+
cwd_path = Path.cwd() / path
|
|
490
|
+
if cwd_path.exists():
|
|
491
|
+
path_obj = cwd_path
|
|
492
|
+
else:
|
|
493
|
+
# Then try relative to tool folder
|
|
494
|
+
tool_folder = Path(__file__).parent
|
|
495
|
+
tool_path = tool_folder / path
|
|
496
|
+
if tool_path.exists():
|
|
497
|
+
path_obj = tool_path
|
|
498
|
+
else:
|
|
499
|
+
# Finally try resolving from tool folder's parent (massgen/)
|
|
500
|
+
# in case path starts with "massgen/"
|
|
501
|
+
if path.startswith("massgen/"):
|
|
502
|
+
# Get the massgen package root
|
|
503
|
+
massgen_root = tool_folder.parent.parent
|
|
504
|
+
full_path = massgen_root / path
|
|
505
|
+
if full_path.exists():
|
|
506
|
+
path_obj = full_path
|
|
507
|
+
|
|
508
|
+
if not path_obj.exists():
|
|
509
|
+
return None
|
|
510
|
+
|
|
511
|
+
# Load module from file
|
|
512
|
+
module_name = path_obj.stem
|
|
513
|
+
spec = importlib.util.spec_from_file_location(module_name, path_obj)
|
|
514
|
+
if spec is None or spec.loader is None:
|
|
515
|
+
return None
|
|
516
|
+
|
|
517
|
+
module = importlib.util.module_from_spec(spec)
|
|
518
|
+
sys.modules[module_name] = module
|
|
519
|
+
spec.loader.exec_module(module)
|
|
520
|
+
|
|
521
|
+
# Extract function from module
|
|
522
|
+
if func_name:
|
|
523
|
+
# If specific function name provided
|
|
524
|
+
if hasattr(module, func_name):
|
|
525
|
+
return getattr(module, func_name)
|
|
526
|
+
else:
|
|
527
|
+
# Try to find a main function or the first callable
|
|
528
|
+
# Priority order: main -> first public function -> first function
|
|
529
|
+
if hasattr(module, "main"):
|
|
530
|
+
return getattr(module, "main")
|
|
531
|
+
|
|
532
|
+
# Find all callables in the module
|
|
533
|
+
callables = []
|
|
534
|
+
for name in dir(module):
|
|
535
|
+
attr = getattr(module, name)
|
|
536
|
+
if callable(attr) and not name.startswith("_"):
|
|
537
|
+
# Skip imported functions
|
|
538
|
+
if hasattr(attr, "__module__") and attr.__module__ != module.__name__:
|
|
539
|
+
continue
|
|
540
|
+
callables.append((name, attr))
|
|
541
|
+
|
|
542
|
+
# Return the first public function found
|
|
543
|
+
if callables:
|
|
544
|
+
return callables[0][1]
|
|
545
|
+
|
|
546
|
+
return None
|
|
547
|
+
|
|
548
|
+
@staticmethod
|
|
549
|
+
def _extract_tool_schema(
|
|
550
|
+
func: Callable,
|
|
551
|
+
include_full: bool,
|
|
552
|
+
include_varargs: bool,
|
|
553
|
+
include_varkwargs: bool,
|
|
554
|
+
) -> dict:
|
|
555
|
+
"""Extract JSON schema from function signature and docstring."""
|
|
556
|
+
doc_parsed = parse(func.__doc__)
|
|
557
|
+
param_docs = {p.arg_name: p.description for p in doc_parsed.params}
|
|
558
|
+
|
|
559
|
+
# Build description
|
|
560
|
+
desc_parts = []
|
|
561
|
+
if doc_parsed.short_description:
|
|
562
|
+
desc_parts.append(doc_parsed.short_description)
|
|
563
|
+
if include_full and doc_parsed.long_description:
|
|
564
|
+
desc_parts.append(doc_parsed.long_description)
|
|
565
|
+
|
|
566
|
+
func_desc = "\n\n".join(desc_parts)
|
|
567
|
+
|
|
568
|
+
# Build parameter fields
|
|
569
|
+
param_fields = {}
|
|
570
|
+
for param_name, param_info in inspect.signature(func).parameters.items():
|
|
571
|
+
if param_name in ["self", "cls"]:
|
|
572
|
+
continue
|
|
573
|
+
|
|
574
|
+
if param_info.kind == inspect.Parameter.VAR_KEYWORD:
|
|
575
|
+
if not include_varkwargs:
|
|
576
|
+
continue
|
|
577
|
+
param_fields[param_name] = (
|
|
578
|
+
Dict[str, Any] if param_info.annotation == inspect.Parameter.empty else Dict[str, param_info.annotation],
|
|
579
|
+
Field(
|
|
580
|
+
description=param_docs.get(param_name),
|
|
581
|
+
default={} if param_info.default is param_info.empty else param_info.default,
|
|
582
|
+
),
|
|
583
|
+
)
|
|
584
|
+
elif param_info.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
585
|
+
if not include_varargs:
|
|
586
|
+
continue
|
|
587
|
+
param_fields[param_name] = (
|
|
588
|
+
list[Any] if param_info.annotation == inspect.Parameter.empty else list[param_info.annotation],
|
|
589
|
+
Field(
|
|
590
|
+
description=param_docs.get(param_name),
|
|
591
|
+
default=[] if param_info.default is param_info.empty else param_info.default,
|
|
592
|
+
),
|
|
593
|
+
)
|
|
594
|
+
else:
|
|
595
|
+
param_fields[param_name] = (
|
|
596
|
+
Any if param_info.annotation == inspect.Parameter.empty else param_info.annotation,
|
|
597
|
+
Field(
|
|
598
|
+
description=param_docs.get(param_name),
|
|
599
|
+
default=... if param_info.default is param_info.empty else param_info.default,
|
|
600
|
+
),
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
dynamic_model = create_model(
|
|
604
|
+
"_DynamicToolModel",
|
|
605
|
+
__config__=ConfigDict(arbitrary_types_allowed=True),
|
|
606
|
+
**param_fields,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
params_schema = dynamic_model.model_json_schema()
|
|
610
|
+
|
|
611
|
+
# Remove title fields
|
|
612
|
+
def remove_titles(obj):
|
|
613
|
+
if isinstance(obj, dict):
|
|
614
|
+
obj.pop("title", None)
|
|
615
|
+
for v in obj.values():
|
|
616
|
+
remove_titles(v)
|
|
617
|
+
elif isinstance(obj, list):
|
|
618
|
+
for item in obj:
|
|
619
|
+
remove_titles(item)
|
|
620
|
+
|
|
621
|
+
remove_titles(params_schema)
|
|
622
|
+
|
|
623
|
+
schema = {
|
|
624
|
+
"type": "function",
|
|
625
|
+
"function": {
|
|
626
|
+
"name": func.__name__,
|
|
627
|
+
"parameters": params_schema,
|
|
628
|
+
},
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if func_desc:
|
|
632
|
+
schema["function"]["description"] = func_desc
|
|
633
|
+
|
|
634
|
+
return schema
|