massgen 0.1.0a3__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 +577 -110
- massgen/config_builder.py +376 -27
- 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.0a3.dist-info → massgen-0.1.1.dist-info}/METADATA +89 -131
- {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/RECORD +111 -36
- {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/WHEEL +0 -0
- {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/entry_points.txt +0 -0
- {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
# Exception Handling Documentation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The MassGen Tool System provides a hierarchy of custom exceptions for handling errors in tool operations. These exceptions provide clear, specific error information to help with debugging and error recovery.
|
|
6
|
+
|
|
7
|
+
**What are Tool Exceptions?**
|
|
8
|
+
Exceptions are Python's way of signaling that something went wrong. The Tool System defines specific exception types for different error scenarios, making it easier to understand and handle problems.
|
|
9
|
+
|
|
10
|
+
**Why use custom exceptions?**
|
|
11
|
+
Instead of generic errors, custom exceptions provide:
|
|
12
|
+
- **Specific Error Types**: Know exactly what went wrong
|
|
13
|
+
- **Structured Information**: Access to error details programmatically
|
|
14
|
+
- **Better Error Messages**: Clear, actionable error descriptions
|
|
15
|
+
- **Easier Debugging**: Specific exceptions help identify issues quickly
|
|
16
|
+
|
|
17
|
+
## Exception Hierarchy
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Exception
|
|
21
|
+
└── ToolException (base)
|
|
22
|
+
├── InvalidToolArgumentsException
|
|
23
|
+
├── ToolNotFoundException
|
|
24
|
+
├── ToolExecutionException
|
|
25
|
+
└── CategoryNotFoundException
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
All tool exceptions inherit from `ToolException`, which inherits from Python's built-in `Exception` class.
|
|
29
|
+
|
|
30
|
+
## Exception Classes
|
|
31
|
+
|
|
32
|
+
### ToolException
|
|
33
|
+
|
|
34
|
+
**What it is**: Base exception class for all tool-related errors.
|
|
35
|
+
|
|
36
|
+
**Why use it**: Catch all tool system errors with a single except clause.
|
|
37
|
+
|
|
38
|
+
**Location**: `massgen.tool._exceptions`
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from massgen.tool._exceptions import ToolException
|
|
42
|
+
|
|
43
|
+
class ToolException(Exception):
|
|
44
|
+
"""Base exception for tool-related errors."""
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Usage**:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from massgen.tool._exceptions import ToolException
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
# Any tool operation
|
|
54
|
+
manager.add_tool_function(func=my_tool)
|
|
55
|
+
await manager.execute_tool(tool_request)
|
|
56
|
+
except ToolException as e:
|
|
57
|
+
# Catches all tool-specific errors
|
|
58
|
+
print(f"Tool system error: {e}")
|
|
59
|
+
except Exception as e:
|
|
60
|
+
# Catches other errors
|
|
61
|
+
print(f"General error: {e}")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**When to use**:
|
|
65
|
+
- When you want to catch any tool-related error
|
|
66
|
+
- When specific error type doesn't matter
|
|
67
|
+
- For logging all tool errors uniformly
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
### InvalidToolArgumentsException
|
|
72
|
+
|
|
73
|
+
**What it is**: Raised when a tool receives invalid or malformed arguments.
|
|
74
|
+
|
|
75
|
+
**Why it happens**:
|
|
76
|
+
- Arguments don't match tool's parameter schema
|
|
77
|
+
- Required parameters are missing
|
|
78
|
+
- Parameter types are incorrect
|
|
79
|
+
- Parameter values are invalid
|
|
80
|
+
|
|
81
|
+
**Location**: `massgen.tool._exceptions`
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
class InvalidToolArgumentsException(ToolException):
|
|
85
|
+
"""Raised when tool receives invalid arguments."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, error_msg: str):
|
|
88
|
+
self.error_msg = error_msg
|
|
89
|
+
super().__init__(self.error_msg)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Attributes**:
|
|
93
|
+
- `error_msg`: Detailed description of what's wrong with the arguments
|
|
94
|
+
|
|
95
|
+
**Examples**:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from massgen.tool._exceptions import InvalidToolArgumentsException
|
|
99
|
+
|
|
100
|
+
# Missing required parameter
|
|
101
|
+
try:
|
|
102
|
+
await my_tool() # Missing required 'name' parameter
|
|
103
|
+
except InvalidToolArgumentsException as e:
|
|
104
|
+
print(f"Invalid arguments: {e.error_msg}")
|
|
105
|
+
# Output: "Missing required parameter: name"
|
|
106
|
+
|
|
107
|
+
# Wrong type
|
|
108
|
+
try:
|
|
109
|
+
await my_tool(name=123) # Expected string, got int
|
|
110
|
+
except InvalidToolArgumentsException as e:
|
|
111
|
+
print(f"Type error: {e.error_msg}")
|
|
112
|
+
# Output: "Parameter 'name' must be string, got int"
|
|
113
|
+
|
|
114
|
+
# Invalid value
|
|
115
|
+
try:
|
|
116
|
+
await my_tool(age=-5) # Age can't be negative
|
|
117
|
+
except InvalidToolArgumentsException as e:
|
|
118
|
+
print(f"Value error: {e.error_msg}")
|
|
119
|
+
# Output: "Parameter 'age' must be positive"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**How to handle**:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
async def safe_tool_call(tool_func, **kwargs):
|
|
126
|
+
"""Safely call a tool with argument validation."""
|
|
127
|
+
try:
|
|
128
|
+
result = await tool_func(**kwargs)
|
|
129
|
+
return result
|
|
130
|
+
except InvalidToolArgumentsException as e:
|
|
131
|
+
print(f"❌ Argument validation failed: {e.error_msg}")
|
|
132
|
+
print(f"💡 Check parameter names, types, and values")
|
|
133
|
+
# Return error result instead of crashing
|
|
134
|
+
return ExecutionResult(
|
|
135
|
+
output_blocks=[TextContent(data=f"Error: {e.error_msg}")]
|
|
136
|
+
)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### ToolNotFoundException
|
|
142
|
+
|
|
143
|
+
**What it is**: Raised when attempting to access a tool that doesn't exist in the registry.
|
|
144
|
+
|
|
145
|
+
**Why it happens**:
|
|
146
|
+
- Tool was never registered
|
|
147
|
+
- Tool name is misspelled
|
|
148
|
+
- Tool was deleted
|
|
149
|
+
- Wrong category (disabled or non-existent)
|
|
150
|
+
|
|
151
|
+
**Location**: `massgen.tool._exceptions`
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
class ToolNotFoundException(ToolException):
|
|
155
|
+
"""Raised when requested tool is not found."""
|
|
156
|
+
|
|
157
|
+
def __init__(self, tool_name: str):
|
|
158
|
+
self.tool_name = tool_name
|
|
159
|
+
super().__init__(f"Tool '{tool_name}' not found in registry")
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Attributes**:
|
|
163
|
+
- `tool_name`: The name of the tool that wasn't found
|
|
164
|
+
|
|
165
|
+
**Examples**:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from massgen.tool._exceptions import ToolNotFoundException
|
|
169
|
+
|
|
170
|
+
# Tool never registered
|
|
171
|
+
try:
|
|
172
|
+
manager.delete_tool_function("nonexistent_tool")
|
|
173
|
+
# Note: delete_tool_function doesn't raise this exception
|
|
174
|
+
# It's raised by other operations
|
|
175
|
+
except ToolNotFoundException as e:
|
|
176
|
+
print(f"Tool '{e.tool_name}' doesn't exist")
|
|
177
|
+
|
|
178
|
+
# Misspelled name
|
|
179
|
+
try:
|
|
180
|
+
manager.apply_extension_model("custom_tool__my_too", MyModel)
|
|
181
|
+
# Misspelled: "my_too" instead of "my_tool"
|
|
182
|
+
except ToolNotFoundException as e:
|
|
183
|
+
print(f"Did you mean 'custom_tool__my_tool'?")
|
|
184
|
+
|
|
185
|
+
# Tool in disabled category
|
|
186
|
+
manager.setup_category("experimental", "Experimental tools", enabled=False)
|
|
187
|
+
manager.add_tool_function(func=experimental_tool, category="experimental")
|
|
188
|
+
|
|
189
|
+
# Tool exists but category is disabled
|
|
190
|
+
schemas = manager.fetch_tool_schemas()
|
|
191
|
+
# experimental_tool won't be in schemas
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**How to handle**:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
def find_tool_or_suggest(manager: ToolManager, tool_name: str):
|
|
198
|
+
"""Find tool or suggest alternatives."""
|
|
199
|
+
try:
|
|
200
|
+
# Try to find tool
|
|
201
|
+
if tool_name in manager.registered_tools:
|
|
202
|
+
return manager.registered_tools[tool_name]
|
|
203
|
+
else:
|
|
204
|
+
raise ToolNotFoundException(tool_name)
|
|
205
|
+
except ToolNotFoundException as e:
|
|
206
|
+
# Suggest similar tools
|
|
207
|
+
all_tools = list(manager.registered_tools.keys())
|
|
208
|
+
similar = [t for t in all_tools if e.tool_name.lower() in t.lower()]
|
|
209
|
+
|
|
210
|
+
if similar:
|
|
211
|
+
print(f"❌ Tool '{e.tool_name}' not found")
|
|
212
|
+
print(f"💡 Did you mean one of these?")
|
|
213
|
+
for tool in similar:
|
|
214
|
+
print(f" - {tool}")
|
|
215
|
+
else:
|
|
216
|
+
print(f"❌ No tools matching '{e.tool_name}' found")
|
|
217
|
+
print(f"📋 Available tools: {all_tools}")
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
### ToolExecutionException
|
|
223
|
+
|
|
224
|
+
**What it is**: Raised when a tool's execution fails due to runtime errors.
|
|
225
|
+
|
|
226
|
+
**Why it happens**:
|
|
227
|
+
- Tool code raises an exception
|
|
228
|
+
- External dependency fails
|
|
229
|
+
- Resource unavailable (file, network, etc.)
|
|
230
|
+
- Timeout or cancellation
|
|
231
|
+
|
|
232
|
+
**Location**: `massgen.tool._exceptions`
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
class ToolExecutionException(ToolException):
|
|
236
|
+
"""Raised when tool execution fails."""
|
|
237
|
+
|
|
238
|
+
def __init__(self, tool_name: str, error_details: str):
|
|
239
|
+
self.tool_name = tool_name
|
|
240
|
+
self.error_details = error_details
|
|
241
|
+
super().__init__(f"Tool '{tool_name}' execution failed: {error_details}")
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**Attributes**:
|
|
245
|
+
- `tool_name`: Name of the tool that failed
|
|
246
|
+
- `error_details`: Description of what went wrong
|
|
247
|
+
|
|
248
|
+
**Examples**:
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
from massgen.tool._exceptions import ToolExecutionException
|
|
252
|
+
|
|
253
|
+
# File operation failure
|
|
254
|
+
try:
|
|
255
|
+
result = await read_file_content("/nonexistent/file.txt")
|
|
256
|
+
except ToolExecutionException as e:
|
|
257
|
+
print(f"Tool: {e.tool_name}")
|
|
258
|
+
print(f"Error: {e.error_details}")
|
|
259
|
+
# Output:
|
|
260
|
+
# Tool: read_file_content
|
|
261
|
+
# Error: File not found: /nonexistent/file.txt
|
|
262
|
+
|
|
263
|
+
# Network failure
|
|
264
|
+
try:
|
|
265
|
+
result = await fetch_api_data(url="https://invalid-url-xyz.com")
|
|
266
|
+
except ToolExecutionException as e:
|
|
267
|
+
print(f"API call failed: {e.error_details}")
|
|
268
|
+
# Output: API call failed: Connection timeout
|
|
269
|
+
|
|
270
|
+
# Code execution failure
|
|
271
|
+
try:
|
|
272
|
+
result = await run_python_script("import nonexistent_module")
|
|
273
|
+
except ToolExecutionException as e:
|
|
274
|
+
print(f"Script failed: {e.error_details}")
|
|
275
|
+
# Output: Script failed: ModuleNotFoundError: No module named 'nonexistent_module'
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**How to handle**:
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
async def execute_with_retry(manager, tool_request, max_retries=3):
|
|
282
|
+
"""Execute tool with automatic retry on failure."""
|
|
283
|
+
for attempt in range(max_retries):
|
|
284
|
+
try:
|
|
285
|
+
result = await manager.execute_tool(tool_request)
|
|
286
|
+
return result
|
|
287
|
+
|
|
288
|
+
except ToolExecutionException as e:
|
|
289
|
+
if attempt < max_retries - 1:
|
|
290
|
+
print(f"⚠️ Attempt {attempt + 1} failed: {e.error_details}")
|
|
291
|
+
print(f"🔄 Retrying...")
|
|
292
|
+
await asyncio.sleep(2 ** attempt) # Exponential backoff
|
|
293
|
+
else:
|
|
294
|
+
print(f"❌ All {max_retries} attempts failed")
|
|
295
|
+
print(f"Tool: {e.tool_name}")
|
|
296
|
+
print(f"Error: {e.error_details}")
|
|
297
|
+
raise # Re-raise after final attempt
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
### CategoryNotFoundException
|
|
303
|
+
|
|
304
|
+
**What it is**: Raised when attempting to access a tool category that doesn't exist.
|
|
305
|
+
|
|
306
|
+
**Why it happens**:
|
|
307
|
+
- Category was never created
|
|
308
|
+
- Category name is misspelled
|
|
309
|
+
- Category was deleted
|
|
310
|
+
- Trying to use reserved name "default" incorrectly
|
|
311
|
+
|
|
312
|
+
**Location**: `massgen.tool._exceptions`
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
class CategoryNotFoundException(ToolException):
|
|
316
|
+
"""Raised when tool category is not found."""
|
|
317
|
+
|
|
318
|
+
def __init__(self, category_name: str):
|
|
319
|
+
self.category_name = category_name
|
|
320
|
+
super().__init__(f"Category '{category_name}' not found")
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Attributes**:
|
|
324
|
+
- `category_name`: Name of the category that wasn't found
|
|
325
|
+
|
|
326
|
+
**Examples**:
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
from massgen.tool._exceptions import CategoryNotFoundException
|
|
330
|
+
|
|
331
|
+
# Add tool to non-existent category
|
|
332
|
+
try:
|
|
333
|
+
manager.add_tool_function(func=my_tool, category="nonexistent")
|
|
334
|
+
except ValueError as e: # Note: This actually raises ValueError, not CategoryNotFoundException
|
|
335
|
+
print(f"Category error: {e}")
|
|
336
|
+
# Output: Category 'nonexistent' not found
|
|
337
|
+
|
|
338
|
+
# Note: CategoryNotFoundException is defined but not currently used
|
|
339
|
+
# The actual implementation raises ValueError instead
|
|
340
|
+
# This may change in future versions
|
|
341
|
+
|
|
342
|
+
# Proper category usage
|
|
343
|
+
manager.setup_category("my_category", "Description", enabled=True)
|
|
344
|
+
manager.add_tool_function(func=my_tool, category="my_category") # ✅ Works
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**How to handle**:
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
def ensure_category(manager: ToolManager, category_name: str):
|
|
351
|
+
"""Ensure category exists, create if not."""
|
|
352
|
+
if category_name in manager.tool_categories:
|
|
353
|
+
print(f"✅ Category '{category_name}' already exists")
|
|
354
|
+
else:
|
|
355
|
+
try:
|
|
356
|
+
manager.setup_category(
|
|
357
|
+
category_name=category_name,
|
|
358
|
+
description=f"Auto-created category: {category_name}",
|
|
359
|
+
enabled=True
|
|
360
|
+
)
|
|
361
|
+
print(f"✅ Created category '{category_name}'")
|
|
362
|
+
except Exception as e:
|
|
363
|
+
print(f"❌ Failed to create category: {e}")
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Error Handling Patterns
|
|
367
|
+
|
|
368
|
+
### Pattern 1: Specific Exception Handling
|
|
369
|
+
|
|
370
|
+
Handle each exception type differently:
|
|
371
|
+
|
|
372
|
+
```python
|
|
373
|
+
from massgen.tool._exceptions import (
|
|
374
|
+
InvalidToolArgumentsException,
|
|
375
|
+
ToolNotFoundException,
|
|
376
|
+
ToolExecutionException,
|
|
377
|
+
CategoryNotFoundException
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
manager.add_tool_function(func=my_tool, category="custom")
|
|
382
|
+
result = await manager.execute_tool(tool_request)
|
|
383
|
+
|
|
384
|
+
except InvalidToolArgumentsException as e:
|
|
385
|
+
# Fix arguments and retry
|
|
386
|
+
print(f"Invalid arguments: {e.error_msg}")
|
|
387
|
+
# Prompt user for correct arguments
|
|
388
|
+
corrected_args = get_user_input()
|
|
389
|
+
result = await manager.execute_tool({
|
|
390
|
+
"name": tool_request["name"],
|
|
391
|
+
"input": corrected_args
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
except ToolNotFoundException as e:
|
|
395
|
+
# Suggest alternatives
|
|
396
|
+
print(f"Tool '{e.tool_name}' not found")
|
|
397
|
+
similar_tools = find_similar_tools(e.tool_name)
|
|
398
|
+
print(f"Similar tools: {similar_tools}")
|
|
399
|
+
|
|
400
|
+
except ToolExecutionException as e:
|
|
401
|
+
# Log error and return safe result
|
|
402
|
+
logger.error(f"Tool {e.tool_name} failed: {e.error_details}")
|
|
403
|
+
result = ExecutionResult(
|
|
404
|
+
output_blocks=[TextContent(data=f"Error: {e.error_details}")]
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
except CategoryNotFoundException as e:
|
|
408
|
+
# Create category and retry
|
|
409
|
+
print(f"Category '{e.category_name}' not found, creating...")
|
|
410
|
+
manager.setup_category(e.category_name, "Auto-created", enabled=True)
|
|
411
|
+
manager.add_tool_function(func=my_tool, category=e.category_name)
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Pattern 2: Catch-All with Logging
|
|
415
|
+
|
|
416
|
+
Log all errors uniformly:
|
|
417
|
+
|
|
418
|
+
```python
|
|
419
|
+
import logging
|
|
420
|
+
from massgen.tool._exceptions import ToolException
|
|
421
|
+
|
|
422
|
+
logger = logging.getLogger(__name__)
|
|
423
|
+
|
|
424
|
+
try:
|
|
425
|
+
result = await manager.execute_tool(tool_request)
|
|
426
|
+
|
|
427
|
+
except ToolException as e:
|
|
428
|
+
# Log with full context
|
|
429
|
+
logger.error(
|
|
430
|
+
"Tool operation failed",
|
|
431
|
+
exc_info=True,
|
|
432
|
+
extra={
|
|
433
|
+
"tool_request": tool_request,
|
|
434
|
+
"exception_type": type(e).__name__,
|
|
435
|
+
"exception_message": str(e)
|
|
436
|
+
}
|
|
437
|
+
)
|
|
438
|
+
# Return error result
|
|
439
|
+
result = ExecutionResult(
|
|
440
|
+
output_blocks=[TextContent(data=f"Error: {str(e)}")]
|
|
441
|
+
)
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Pattern 3: Retry with Backoff
|
|
445
|
+
|
|
446
|
+
Automatically retry failed operations:
|
|
447
|
+
|
|
448
|
+
```python
|
|
449
|
+
import asyncio
|
|
450
|
+
from massgen.tool._exceptions import ToolExecutionException
|
|
451
|
+
|
|
452
|
+
async def execute_with_backoff(manager, tool_request, max_retries=3):
|
|
453
|
+
"""Execute with exponential backoff on failure."""
|
|
454
|
+
for attempt in range(max_retries):
|
|
455
|
+
try:
|
|
456
|
+
result = await manager.execute_tool(tool_request)
|
|
457
|
+
return result
|
|
458
|
+
|
|
459
|
+
except ToolExecutionException as e:
|
|
460
|
+
wait_time = 2 ** attempt # 1s, 2s, 4s...
|
|
461
|
+
|
|
462
|
+
if attempt < max_retries - 1:
|
|
463
|
+
logger.warning(
|
|
464
|
+
f"Tool execution failed (attempt {attempt + 1}/{max_retries}): {e.error_details}"
|
|
465
|
+
)
|
|
466
|
+
logger.info(f"Retrying in {wait_time} seconds...")
|
|
467
|
+
await asyncio.sleep(wait_time)
|
|
468
|
+
else:
|
|
469
|
+
logger.error(f"Tool execution failed after {max_retries} attempts")
|
|
470
|
+
raise
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Pattern 4: Graceful Degradation
|
|
474
|
+
|
|
475
|
+
Provide fallback behavior:
|
|
476
|
+
|
|
477
|
+
```python
|
|
478
|
+
from massgen.tool._exceptions import ToolNotFoundException
|
|
479
|
+
|
|
480
|
+
async def execute_with_fallback(manager, primary_tool, fallback_tool, args):
|
|
481
|
+
"""Try primary tool, fall back to alternative if not found."""
|
|
482
|
+
try:
|
|
483
|
+
# Try primary tool
|
|
484
|
+
result = await manager.execute_tool({
|
|
485
|
+
"name": primary_tool,
|
|
486
|
+
"input": args
|
|
487
|
+
})
|
|
488
|
+
return result
|
|
489
|
+
|
|
490
|
+
except ToolNotFoundException:
|
|
491
|
+
logger.warning(f"Primary tool '{primary_tool}' not found, using fallback")
|
|
492
|
+
|
|
493
|
+
# Try fallback
|
|
494
|
+
try:
|
|
495
|
+
result = await manager.execute_tool({
|
|
496
|
+
"name": fallback_tool,
|
|
497
|
+
"input": args
|
|
498
|
+
})
|
|
499
|
+
return result
|
|
500
|
+
|
|
501
|
+
except ToolNotFoundException:
|
|
502
|
+
logger.error(f"Neither '{primary_tool}' nor '{fallback_tool}' available")
|
|
503
|
+
# Return error result
|
|
504
|
+
return ExecutionResult(
|
|
505
|
+
output_blocks=[TextContent(data="No suitable tool available")]
|
|
506
|
+
)
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### Pattern 5: Validation Before Execution
|
|
510
|
+
|
|
511
|
+
Prevent errors proactively:
|
|
512
|
+
|
|
513
|
+
```python
|
|
514
|
+
from massgen.tool._exceptions import InvalidToolArgumentsException
|
|
515
|
+
|
|
516
|
+
def validate_tool_request(manager, tool_request):
|
|
517
|
+
"""Validate tool request before execution."""
|
|
518
|
+
tool_name = tool_request.get("name")
|
|
519
|
+
tool_input = tool_request.get("input", {})
|
|
520
|
+
|
|
521
|
+
# Check tool exists
|
|
522
|
+
if tool_name not in manager.registered_tools:
|
|
523
|
+
raise ToolNotFoundException(tool_name)
|
|
524
|
+
|
|
525
|
+
# Get tool schema
|
|
526
|
+
tool_entry = manager.registered_tools[tool_name]
|
|
527
|
+
schema = tool_entry.schema_def["function"]["parameters"]
|
|
528
|
+
|
|
529
|
+
# Check required parameters
|
|
530
|
+
required = schema.get("required", [])
|
|
531
|
+
missing = [p for p in required if p not in tool_input]
|
|
532
|
+
|
|
533
|
+
if missing:
|
|
534
|
+
raise InvalidToolArgumentsException(
|
|
535
|
+
f"Missing required parameters: {', '.join(missing)}"
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
# Check parameter types
|
|
539
|
+
properties = schema.get("properties", {})
|
|
540
|
+
for param_name, param_value in tool_input.items():
|
|
541
|
+
if param_name in properties:
|
|
542
|
+
expected_type = properties[param_name].get("type")
|
|
543
|
+
actual_type = type(param_value).__name__
|
|
544
|
+
|
|
545
|
+
# Simple type checking (can be more sophisticated)
|
|
546
|
+
if expected_type == "string" and not isinstance(param_value, str):
|
|
547
|
+
raise InvalidToolArgumentsException(
|
|
548
|
+
f"Parameter '{param_name}' must be string, got {actual_type}"
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
return True
|
|
552
|
+
|
|
553
|
+
# Usage
|
|
554
|
+
try:
|
|
555
|
+
validate_tool_request(manager, tool_request)
|
|
556
|
+
result = await manager.execute_tool(tool_request)
|
|
557
|
+
except ToolException as e:
|
|
558
|
+
print(f"Validation or execution failed: {e}")
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
## Best Practices
|
|
562
|
+
|
|
563
|
+
### 1. Be Specific
|
|
564
|
+
|
|
565
|
+
Catch specific exceptions when you can handle them differently:
|
|
566
|
+
|
|
567
|
+
```python
|
|
568
|
+
# ❌ Too broad
|
|
569
|
+
try:
|
|
570
|
+
result = await manager.execute_tool(tool_request)
|
|
571
|
+
except Exception as e:
|
|
572
|
+
print("Something went wrong")
|
|
573
|
+
|
|
574
|
+
# ✅ Specific handling
|
|
575
|
+
try:
|
|
576
|
+
result = await manager.execute_tool(tool_request)
|
|
577
|
+
except InvalidToolArgumentsException as e:
|
|
578
|
+
fix_arguments(e.error_msg)
|
|
579
|
+
except ToolNotFoundException as e:
|
|
580
|
+
suggest_alternatives(e.tool_name)
|
|
581
|
+
except ToolExecutionException as e:
|
|
582
|
+
log_and_retry(e.tool_name, e.error_details)
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
### 2. Preserve Stack Traces
|
|
586
|
+
|
|
587
|
+
Use `exc_info=True` for debugging:
|
|
588
|
+
|
|
589
|
+
```python
|
|
590
|
+
import logging
|
|
591
|
+
|
|
592
|
+
try:
|
|
593
|
+
result = await manager.execute_tool(tool_request)
|
|
594
|
+
except ToolException as e:
|
|
595
|
+
# Logs full stack trace
|
|
596
|
+
logger.error("Tool execution failed", exc_info=True)
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### 3. Provide Context
|
|
600
|
+
|
|
601
|
+
Add context to error messages:
|
|
602
|
+
|
|
603
|
+
```python
|
|
604
|
+
try:
|
|
605
|
+
result = await manager.execute_tool(tool_request)
|
|
606
|
+
except ToolExecutionException as e:
|
|
607
|
+
# Add context
|
|
608
|
+
enhanced_message = (
|
|
609
|
+
f"Failed to execute tool for task '{task_id}'\n"
|
|
610
|
+
f"Tool: {e.tool_name}\n"
|
|
611
|
+
f"Error: {e.error_details}\n"
|
|
612
|
+
f"Request: {tool_request}"
|
|
613
|
+
)
|
|
614
|
+
logger.error(enhanced_message)
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
### 4. Don't Swallow Exceptions
|
|
618
|
+
|
|
619
|
+
Always handle or re-raise:
|
|
620
|
+
|
|
621
|
+
```python
|
|
622
|
+
# ❌ Silently ignores errors
|
|
623
|
+
try:
|
|
624
|
+
result = await manager.execute_tool(tool_request)
|
|
625
|
+
except ToolException:
|
|
626
|
+
pass # Bad! Error is lost
|
|
627
|
+
|
|
628
|
+
# ✅ Log and handle
|
|
629
|
+
try:
|
|
630
|
+
result = await manager.execute_tool(tool_request)
|
|
631
|
+
except ToolException as e:
|
|
632
|
+
logger.error(f"Tool failed: {e}")
|
|
633
|
+
result = ExecutionResult(
|
|
634
|
+
output_blocks=[TextContent(data=f"Error: {e}")]
|
|
635
|
+
)
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### 5. Use Finally for Cleanup
|
|
639
|
+
|
|
640
|
+
Ensure resources are cleaned up:
|
|
641
|
+
|
|
642
|
+
```python
|
|
643
|
+
resource = None
|
|
644
|
+
try:
|
|
645
|
+
resource = acquire_resource()
|
|
646
|
+
result = await manager.execute_tool(tool_request)
|
|
647
|
+
except ToolException as e:
|
|
648
|
+
logger.error(f"Tool failed: {e}")
|
|
649
|
+
raise
|
|
650
|
+
finally:
|
|
651
|
+
if resource:
|
|
652
|
+
resource.cleanup()
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
## Complete Example
|
|
656
|
+
|
|
657
|
+
```python
|
|
658
|
+
import logging
|
|
659
|
+
import asyncio
|
|
660
|
+
from typing import Optional
|
|
661
|
+
from massgen.tool import ToolManager, ExecutionResult, TextContent
|
|
662
|
+
from massgen.tool._exceptions import (
|
|
663
|
+
ToolException,
|
|
664
|
+
InvalidToolArgumentsException,
|
|
665
|
+
ToolNotFoundException,
|
|
666
|
+
ToolExecutionException,
|
|
667
|
+
CategoryNotFoundException
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
logger = logging.getLogger(__name__)
|
|
671
|
+
|
|
672
|
+
class RobustToolExecutor:
|
|
673
|
+
"""Robust tool executor with comprehensive error handling."""
|
|
674
|
+
|
|
675
|
+
def __init__(self, manager: ToolManager):
|
|
676
|
+
self.manager = manager
|
|
677
|
+
self.retry_count = 3
|
|
678
|
+
self.retry_delay = 1.0
|
|
679
|
+
|
|
680
|
+
async def execute(
|
|
681
|
+
self,
|
|
682
|
+
tool_request: dict,
|
|
683
|
+
fallback_result: Optional[ExecutionResult] = None
|
|
684
|
+
) -> ExecutionResult:
|
|
685
|
+
"""Execute tool with robust error handling."""
|
|
686
|
+
|
|
687
|
+
# Validate request
|
|
688
|
+
if not self._validate_request(tool_request):
|
|
689
|
+
return ExecutionResult(
|
|
690
|
+
output_blocks=[TextContent(data="Invalid tool request format")]
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
# Try execution with retry
|
|
694
|
+
for attempt in range(self.retry_count):
|
|
695
|
+
try:
|
|
696
|
+
result = await self._execute_with_timeout(tool_request)
|
|
697
|
+
logger.info(f"Tool execution succeeded on attempt {attempt + 1}")
|
|
698
|
+
return result
|
|
699
|
+
|
|
700
|
+
except InvalidToolArgumentsException as e:
|
|
701
|
+
logger.error(f"Invalid arguments: {e.error_msg}")
|
|
702
|
+
return ExecutionResult(
|
|
703
|
+
output_blocks=[TextContent(data=f"Argument error: {e.error_msg}")]
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
except ToolNotFoundException as e:
|
|
707
|
+
logger.error(f"Tool not found: {e.tool_name}")
|
|
708
|
+
alternatives = self._find_similar_tools(e.tool_name)
|
|
709
|
+
msg = f"Tool '{e.tool_name}' not found."
|
|
710
|
+
if alternatives:
|
|
711
|
+
msg += f" Similar tools: {', '.join(alternatives)}"
|
|
712
|
+
return ExecutionResult(
|
|
713
|
+
output_blocks=[TextContent(data=msg)]
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
except ToolExecutionException as e:
|
|
717
|
+
if attempt < self.retry_count - 1:
|
|
718
|
+
logger.warning(
|
|
719
|
+
f"Execution failed (attempt {attempt + 1}): {e.error_details}"
|
|
720
|
+
)
|
|
721
|
+
await asyncio.sleep(self.retry_delay * (2 ** attempt))
|
|
722
|
+
else:
|
|
723
|
+
logger.error(f"Execution failed after {self.retry_count} attempts")
|
|
724
|
+
if fallback_result:
|
|
725
|
+
logger.info("Using fallback result")
|
|
726
|
+
return fallback_result
|
|
727
|
+
return ExecutionResult(
|
|
728
|
+
output_blocks=[TextContent(data=f"Execution error: {e.error_details}")]
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
except Exception as e:
|
|
732
|
+
logger.exception("Unexpected error during tool execution")
|
|
733
|
+
return ExecutionResult(
|
|
734
|
+
output_blocks=[TextContent(data=f"Unexpected error: {str(e)}")]
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
async def _execute_with_timeout(
|
|
738
|
+
self,
|
|
739
|
+
tool_request: dict,
|
|
740
|
+
timeout: float = 30.0
|
|
741
|
+
) -> ExecutionResult:
|
|
742
|
+
"""Execute with timeout."""
|
|
743
|
+
try:
|
|
744
|
+
result = await asyncio.wait_for(
|
|
745
|
+
self.manager.execute_tool(tool_request).__anext__(),
|
|
746
|
+
timeout=timeout
|
|
747
|
+
)
|
|
748
|
+
return result
|
|
749
|
+
except asyncio.TimeoutError:
|
|
750
|
+
raise ToolExecutionException(
|
|
751
|
+
tool_request["name"],
|
|
752
|
+
f"Execution timed out after {timeout} seconds"
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
def _validate_request(self, tool_request: dict) -> bool:
|
|
756
|
+
"""Validate tool request structure."""
|
|
757
|
+
return (
|
|
758
|
+
isinstance(tool_request, dict) and
|
|
759
|
+
"name" in tool_request and
|
|
760
|
+
isinstance(tool_request.get("input", {}), dict)
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
def _find_similar_tools(self, tool_name: str) -> list:
|
|
764
|
+
"""Find similar tool names."""
|
|
765
|
+
all_tools = list(self.manager.registered_tools.keys())
|
|
766
|
+
similar = [
|
|
767
|
+
t for t in all_tools
|
|
768
|
+
if tool_name.lower() in t.lower() or t.lower() in tool_name.lower()
|
|
769
|
+
]
|
|
770
|
+
return similar[:3] # Return top 3
|
|
771
|
+
|
|
772
|
+
# Usage
|
|
773
|
+
async def main():
|
|
774
|
+
manager = ToolManager()
|
|
775
|
+
executor = RobustToolExecutor(manager)
|
|
776
|
+
|
|
777
|
+
# Execute with full error handling
|
|
778
|
+
result = await executor.execute({
|
|
779
|
+
"name": "my_tool",
|
|
780
|
+
"input": {"arg1": "value1"}
|
|
781
|
+
})
|
|
782
|
+
|
|
783
|
+
print(result.output_blocks[0].data)
|
|
784
|
+
|
|
785
|
+
if __name__ == "__main__":
|
|
786
|
+
asyncio.run(main())
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
---
|
|
790
|
+
|
|
791
|
+
For more information, see:
|
|
792
|
+
- [ToolManager Documentation](manager.md)
|
|
793
|
+
- [ExecutionResult Documentation](execution_results.md)
|
|
794
|
+
- [Built-in Tools Guide](builtin_tools.md)
|