arcade-core 2.3.0__py3-none-any.whl → 2.5.0rc1__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.
- arcade_core/catalog.py +97 -38
- arcade_core/context.py +128 -0
- arcade_core/converters/openai.py +220 -0
- arcade_core/discovery.py +253 -0
- arcade_core/errors.py +310 -35
- arcade_core/executor.py +10 -17
- arcade_core/output.py +45 -9
- arcade_core/parse.py +12 -0
- arcade_core/schema.py +82 -20
- arcade_core/toolkit.py +74 -3
- arcade_core/utils.py +4 -1
- {arcade_core-2.3.0.dist-info → arcade_core-2.5.0rc1.dist-info}/METADATA +1 -4
- arcade_core-2.5.0rc1.dist-info/RECORD +21 -0
- arcade_core/telemetry.py +0 -130
- arcade_core-2.3.0.dist-info/RECORD +0 -19
- {arcade_core-2.3.0.dist-info → arcade_core-2.5.0rc1.dist-info}/WHEEL +0 -0
arcade_core/discovery.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Discovery utilities for Arcade Tools.
|
|
3
|
+
|
|
4
|
+
Provides modular, testable functions to discover toolkits and local tool files,
|
|
5
|
+
load modules, collect tools, and build a ToolCatalog.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib.util
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from types import ModuleType
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from loguru import logger
|
|
16
|
+
|
|
17
|
+
from arcade_core.catalog import ToolCatalog
|
|
18
|
+
from arcade_core.parse import get_tools_from_file
|
|
19
|
+
from arcade_core.toolkit import Toolkit, ToolkitLoadError
|
|
20
|
+
|
|
21
|
+
DISCOVERY_PATTERNS = ["*.py", "tools/*.py", "arcade_tools/*.py", "tools/**/*.py"]
|
|
22
|
+
FILTER_PATTERNS = ["_test.py", "test_*.py", "__pycache__", "*.lock", "*.egg-info", "*.pyc"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def normalize_package_name(package_name: str) -> str:
|
|
26
|
+
"""Normalize a package name for import resolution."""
|
|
27
|
+
return package_name.lower().replace("-", "_")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_toolkit_from_package(package_name: str, show_packages: bool = False) -> Toolkit:
|
|
31
|
+
"""Attempt to load a Toolkit from an installed package name."""
|
|
32
|
+
toolkit = Toolkit.from_package(package_name)
|
|
33
|
+
if show_packages:
|
|
34
|
+
logger.info(f"Loading package: {toolkit.name}")
|
|
35
|
+
return toolkit
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_package(package_name: str, show_packages: bool = False) -> Toolkit:
|
|
39
|
+
"""Load a toolkit for a specific package name.
|
|
40
|
+
|
|
41
|
+
Raises ToolkitLoadError if the package is not found.
|
|
42
|
+
"""
|
|
43
|
+
normalized = normalize_package_name(package_name)
|
|
44
|
+
try:
|
|
45
|
+
return load_toolkit_from_package(normalized, show_packages)
|
|
46
|
+
except ToolkitLoadError:
|
|
47
|
+
return load_toolkit_from_package(f"arcade_{normalized}", show_packages)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def find_candidate_tool_files(root: Path | None = None) -> list[Path]:
|
|
51
|
+
"""Find candidate Python files for auto-discovery in common locations."""
|
|
52
|
+
cwd = root or Path.cwd()
|
|
53
|
+
|
|
54
|
+
candidates: list[Path] = []
|
|
55
|
+
for pattern in DISCOVERY_PATTERNS:
|
|
56
|
+
candidates.extend(cwd.glob(pattern))
|
|
57
|
+
# Deduplicate candidates (same file might match multiple patterns)
|
|
58
|
+
unique_candidates = list(set(candidates))
|
|
59
|
+
# Filter out private, cache, and tests
|
|
60
|
+
return [
|
|
61
|
+
p for p in unique_candidates if not any(p.match(pattern) for pattern in FILTER_PATTERNS)
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def analyze_files_for_tools(files: list[Path]) -> list[tuple[Path, list[str]]]:
|
|
66
|
+
"""Parse files with a fast AST pass to find declared @tool function names."""
|
|
67
|
+
results: list[tuple[Path, list[str]]] = []
|
|
68
|
+
for file_path in files:
|
|
69
|
+
try:
|
|
70
|
+
names = get_tools_from_file(file_path)
|
|
71
|
+
if names:
|
|
72
|
+
logger.info(f"Found {len(names)} tool(s) in {file_path.name}: {', '.join(names)}")
|
|
73
|
+
results.append((file_path, names))
|
|
74
|
+
except Exception:
|
|
75
|
+
logger.exception(f"Could not parse {file_path}")
|
|
76
|
+
return results
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def load_module_from_path(file_path: Path) -> ModuleType:
|
|
80
|
+
"""Dynamically import a Python module from a file path."""
|
|
81
|
+
import sys
|
|
82
|
+
|
|
83
|
+
# Add the directory containing the file to sys.path temporarily
|
|
84
|
+
# This allows local imports to work
|
|
85
|
+
file_dir = str(file_path.parent)
|
|
86
|
+
path_added = False
|
|
87
|
+
if file_dir not in sys.path:
|
|
88
|
+
sys.path.insert(0, file_dir)
|
|
89
|
+
path_added = True
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
spec = importlib.util.spec_from_file_location(
|
|
93
|
+
f"_tools_{file_path.stem}",
|
|
94
|
+
file_path,
|
|
95
|
+
)
|
|
96
|
+
if not spec or not spec.loader:
|
|
97
|
+
raise ToolkitLoadError(f"Unable to create import spec for {file_path}")
|
|
98
|
+
|
|
99
|
+
module = importlib.util.module_from_spec(spec)
|
|
100
|
+
try:
|
|
101
|
+
spec.loader.exec_module(module)
|
|
102
|
+
except Exception:
|
|
103
|
+
logger.exception(f"Failed to load {file_path}")
|
|
104
|
+
raise ToolkitLoadError(f"Failed to load {file_path}")
|
|
105
|
+
|
|
106
|
+
return module
|
|
107
|
+
finally:
|
|
108
|
+
# Remove the path we added
|
|
109
|
+
if path_added and file_dir in sys.path:
|
|
110
|
+
sys.path.remove(file_dir)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def collect_tools_from_modules(
|
|
114
|
+
files_with_tools: list[tuple[Path, list[str]]],
|
|
115
|
+
) -> list[tuple[Any, ModuleType]]:
|
|
116
|
+
"""Load modules and collect the expected tool callables.
|
|
117
|
+
|
|
118
|
+
Returns a list of (callable, module) pairs.
|
|
119
|
+
"""
|
|
120
|
+
discovered: list[tuple[Any, ModuleType]] = []
|
|
121
|
+
|
|
122
|
+
for file_path, expected_names in files_with_tools:
|
|
123
|
+
logger.debug(f"Loading tools from {file_path}...")
|
|
124
|
+
try:
|
|
125
|
+
module = load_module_from_path(file_path)
|
|
126
|
+
except ToolkitLoadError:
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
for name in expected_names:
|
|
130
|
+
if hasattr(module, name):
|
|
131
|
+
attr = getattr(module, name)
|
|
132
|
+
if callable(attr) and hasattr(attr, "__tool_name__"):
|
|
133
|
+
discovered.append((attr, module))
|
|
134
|
+
else:
|
|
135
|
+
logger.warning(
|
|
136
|
+
f"Expected {name} to be a tool but it wasn't (missing __tool_name__)\n\n"
|
|
137
|
+
)
|
|
138
|
+
return discovered
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def build_minimal_toolkit(
|
|
142
|
+
server_name: str | None,
|
|
143
|
+
server_version: str | None,
|
|
144
|
+
description: str | None = None,
|
|
145
|
+
) -> Toolkit:
|
|
146
|
+
"""Create a minimal Toolkit to host locally discovered tools."""
|
|
147
|
+
name = server_name or "ArcadeMCP"
|
|
148
|
+
version = server_version or "0.1.0dev"
|
|
149
|
+
pkg = f"{name}.{Path.cwd().name}"
|
|
150
|
+
desc = description or f"MCP Server for {name} version {version}"
|
|
151
|
+
return Toolkit(name=name, package_name=pkg, version=version, description=desc)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def build_catalog_from_toolkits(toolkits: list[Toolkit]) -> ToolCatalog:
|
|
155
|
+
"""Create a ToolCatalog and add the provided toolkits."""
|
|
156
|
+
catalog = ToolCatalog()
|
|
157
|
+
for tk in toolkits:
|
|
158
|
+
catalog.add_toolkit(tk)
|
|
159
|
+
return catalog
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def add_discovered_tools(
|
|
163
|
+
catalog: ToolCatalog,
|
|
164
|
+
toolkit: Toolkit,
|
|
165
|
+
tools: list[tuple[Any, ModuleType]],
|
|
166
|
+
) -> None:
|
|
167
|
+
"""Add discovered local tools to the catalog, preserving module context."""
|
|
168
|
+
for tool_func, module in tools:
|
|
169
|
+
if module.__name__ not in __import__("sys").modules:
|
|
170
|
+
__import__("sys").modules[module.__name__] = module
|
|
171
|
+
catalog.add_tool(tool_func, toolkit, module)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def load_toolkits_for_option(tool_package: str, show_packages: bool = False) -> list[Toolkit]:
|
|
175
|
+
"""
|
|
176
|
+
Load toolkits for a given package option.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
tool_package: Package name or comma-separated list of package names
|
|
180
|
+
show_packages: Whether to log loaded packages
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
List of loaded toolkits
|
|
184
|
+
"""
|
|
185
|
+
toolkits = []
|
|
186
|
+
packages = [p.strip() for p in tool_package.split(",")]
|
|
187
|
+
|
|
188
|
+
for package in packages:
|
|
189
|
+
try:
|
|
190
|
+
toolkit = load_package(package, show_packages)
|
|
191
|
+
toolkits.append(toolkit)
|
|
192
|
+
except ToolkitLoadError as e:
|
|
193
|
+
logger.warning(f"Failed to load package '{package}': {e}")
|
|
194
|
+
|
|
195
|
+
return toolkits
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def load_all_installed_toolkits(show_packages: bool = False) -> list[Toolkit]:
|
|
199
|
+
"""
|
|
200
|
+
Discover and load all installed arcade toolkits.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
show_packages: Whether to log loaded packages
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
List of all installed toolkits
|
|
207
|
+
"""
|
|
208
|
+
toolkits = Toolkit.find_all_arcade_toolkits()
|
|
209
|
+
|
|
210
|
+
if show_packages:
|
|
211
|
+
for toolkit in toolkits:
|
|
212
|
+
logger.info(f"Loading package: {toolkit.name}")
|
|
213
|
+
|
|
214
|
+
return toolkits
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def discover_tools(
|
|
218
|
+
tool_package: str | None = None,
|
|
219
|
+
show_packages: bool = False,
|
|
220
|
+
discover_installed: bool = False,
|
|
221
|
+
server_name: str | None = None,
|
|
222
|
+
server_version: str | None = None,
|
|
223
|
+
) -> ToolCatalog:
|
|
224
|
+
"""High-level discovery that returns a ToolCatalog.
|
|
225
|
+
|
|
226
|
+
This function is pure (does not sys.exit); callers should handle errors.
|
|
227
|
+
"""
|
|
228
|
+
# 1) Package-based discovery
|
|
229
|
+
if tool_package:
|
|
230
|
+
toolkits = load_toolkits_for_option(tool_package, show_packages)
|
|
231
|
+
return build_catalog_from_toolkits(toolkits)
|
|
232
|
+
|
|
233
|
+
# 2) Discover all installed packages
|
|
234
|
+
if discover_installed:
|
|
235
|
+
toolkits = load_all_installed_toolkits(show_packages)
|
|
236
|
+
return build_catalog_from_toolkits(toolkits)
|
|
237
|
+
|
|
238
|
+
# 3) Local file discovery
|
|
239
|
+
logger.info("Auto-discovering tools from current directory")
|
|
240
|
+
files = find_candidate_tool_files()
|
|
241
|
+
if not files:
|
|
242
|
+
# Return empty catalog; caller can decide how to handle
|
|
243
|
+
return ToolCatalog()
|
|
244
|
+
|
|
245
|
+
files_with_tools = analyze_files_for_tools(files)
|
|
246
|
+
if not files_with_tools:
|
|
247
|
+
return ToolCatalog()
|
|
248
|
+
|
|
249
|
+
discovered = collect_tools_from_modules(files_with_tools)
|
|
250
|
+
catalog = ToolCatalog()
|
|
251
|
+
toolkit = build_minimal_toolkit(server_name, server_version)
|
|
252
|
+
add_discovered_tools(catalog, toolkit, discovered)
|
|
253
|
+
return catalog
|
arcade_core/errors.py
CHANGED
|
@@ -1,103 +1,378 @@
|
|
|
1
1
|
import traceback
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import warnings
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ErrorKind(str, Enum):
|
|
9
|
+
"""Error kind that is comprised of
|
|
10
|
+
- the who (toolkit, tool, upstream)
|
|
11
|
+
- the when (load time, definition parsing time, runtime)
|
|
12
|
+
- the what (bad_definition, bad_input, bad_output, retry, context_required, fatal, etc.)"""
|
|
13
|
+
|
|
14
|
+
TOOLKIT_LOAD_FAILED = "TOOLKIT_LOAD_FAILED"
|
|
15
|
+
TOOL_DEFINITION_BAD_DEFINITION = "TOOL_DEFINITION_BAD_DEFINITION"
|
|
16
|
+
TOOL_DEFINITION_BAD_INPUT_SCHEMA = "TOOL_DEFINITION_BAD_INPUT_SCHEMA"
|
|
17
|
+
TOOL_DEFINITION_BAD_OUTPUT_SCHEMA = "TOOL_DEFINITION_BAD_OUTPUT_SCHEMA"
|
|
18
|
+
TOOL_RUNTIME_BAD_INPUT_VALUE = "TOOL_RUNTIME_BAD_INPUT_VALUE"
|
|
19
|
+
TOOL_RUNTIME_BAD_OUTPUT_VALUE = "TOOL_RUNTIME_BAD_OUTPUT_VALUE"
|
|
20
|
+
TOOL_RUNTIME_RETRY = "TOOL_RUNTIME_RETRY"
|
|
21
|
+
TOOL_RUNTIME_CONTEXT_REQUIRED = "TOOL_RUNTIME_CONTEXT_REQUIRED"
|
|
22
|
+
TOOL_RUNTIME_FATAL = "TOOL_RUNTIME_FATAL"
|
|
23
|
+
UPSTREAM_RUNTIME_BAD_REQUEST = "UPSTREAM_RUNTIME_BAD_REQUEST"
|
|
24
|
+
UPSTREAM_RUNTIME_AUTH_ERROR = "UPSTREAM_RUNTIME_AUTH_ERROR"
|
|
25
|
+
UPSTREAM_RUNTIME_NOT_FOUND = "UPSTREAM_RUNTIME_NOT_FOUND"
|
|
26
|
+
UPSTREAM_RUNTIME_VALIDATION_ERROR = "UPSTREAM_RUNTIME_VALIDATION_ERROR"
|
|
27
|
+
UPSTREAM_RUNTIME_RATE_LIMIT = "UPSTREAM_RUNTIME_RATE_LIMIT"
|
|
28
|
+
UPSTREAM_RUNTIME_SERVER_ERROR = "UPSTREAM_RUNTIME_SERVER_ERROR"
|
|
29
|
+
UPSTREAM_RUNTIME_UNMAPPED = "UPSTREAM_RUNTIME_UNMAPPED"
|
|
30
|
+
UNKNOWN = "UNKNOWN"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ToolkitError(Exception, ABC):
|
|
6
34
|
"""
|
|
7
|
-
Base class for all errors
|
|
35
|
+
Base class for all Arcade errors.
|
|
36
|
+
|
|
37
|
+
Note: This class is an abstract class and cannot be instantiated directly.
|
|
38
|
+
|
|
39
|
+
These errors are ultimately converted to the ToolCallError schema.
|
|
40
|
+
Attributes expected from subclasses:
|
|
41
|
+
message : str # user-facing error message
|
|
42
|
+
kind : ErrorKind # the error kind
|
|
43
|
+
can_retry : bool # whether the operation can be retried
|
|
44
|
+
developer_message : str | None # developer-facing error details
|
|
45
|
+
status_code : int | None # HTTP status code when relevant
|
|
46
|
+
additional_prompt_content : str | None # content for retry prompts
|
|
47
|
+
retry_after_ms : int | None # milliseconds to wait before retry
|
|
48
|
+
stacktrace : str | None # stacktrace information
|
|
49
|
+
extra : dict[str, Any] | None # arbitrary structured metadata
|
|
8
50
|
"""
|
|
9
51
|
|
|
10
|
-
|
|
52
|
+
def __new__(cls, *args: Any, **kwargs: Any) -> "ToolkitError":
|
|
53
|
+
abs_methods = getattr(cls, "__abstractmethods__", None)
|
|
54
|
+
if abs_methods:
|
|
55
|
+
raise TypeError(f"Can't instantiate abstract class {cls.__name__}")
|
|
56
|
+
return super().__new__(cls)
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def create_message_prefix(self, name: str) -> str:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
def with_context(self, name: str) -> "ToolkitError":
|
|
63
|
+
"""
|
|
64
|
+
Add context to the error message.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
name: The name of the tool or toolkit that caused the error.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The error with the context added to the message.
|
|
71
|
+
"""
|
|
72
|
+
prefix = self.create_message_prefix(name)
|
|
73
|
+
self.message = f"{prefix}{self.message}" # type: ignore[has-type]
|
|
74
|
+
if hasattr(self, "developer_message") and self.developer_message: # type: ignore[has-type]
|
|
75
|
+
self.developer_message = f"{prefix}{self.developer_message}" # type: ignore[has-type]
|
|
76
|
+
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def is_toolkit_error(self) -> bool:
|
|
81
|
+
"""Check if this error originated from loading a toolkit."""
|
|
82
|
+
return hasattr(self, "kind") and self.kind.name.startswith("TOOLKIT_")
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def is_tool_error(self) -> bool:
|
|
86
|
+
"""Check if this error originated from a tool."""
|
|
87
|
+
return hasattr(self, "kind") and self.kind.name.startswith("TOOL_")
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def is_upstream_error(self) -> bool:
|
|
91
|
+
"""Check if this error originated from an upstream service."""
|
|
92
|
+
return hasattr(self, "kind") and self.kind.name.startswith("UPSTREAM_")
|
|
93
|
+
|
|
94
|
+
def __str__(self) -> str:
|
|
95
|
+
return self.message
|
|
11
96
|
|
|
12
97
|
|
|
13
98
|
class ToolkitLoadError(ToolkitError):
|
|
14
99
|
"""
|
|
15
|
-
Raised
|
|
100
|
+
Raised while importing / loading a toolkit package
|
|
101
|
+
(e.g. missing dependency, SyntaxError in module top-level code).
|
|
16
102
|
"""
|
|
17
103
|
|
|
18
|
-
|
|
104
|
+
kind: ErrorKind = ErrorKind.TOOLKIT_LOAD_FAILED
|
|
105
|
+
can_retry: bool = False
|
|
19
106
|
|
|
107
|
+
def __init__(self, message: str) -> None:
|
|
108
|
+
super().__init__(message)
|
|
109
|
+
self.message = message
|
|
20
110
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
111
|
+
def create_message_prefix(self, toolkit_name: str) -> str:
|
|
112
|
+
return f"[{self.kind.value}] {type(self).__name__} when loading toolkit '{toolkit_name}': "
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ToolError(ToolkitError):
|
|
24
116
|
"""
|
|
117
|
+
Any error related to an Arcade tool.
|
|
25
118
|
|
|
26
|
-
|
|
119
|
+
Note: This class is an abstract class and cannot be instantiated directly.
|
|
120
|
+
"""
|
|
27
121
|
|
|
28
122
|
|
|
123
|
+
# ------ definition-time errors (tool developer's responsibility) ------
|
|
29
124
|
class ToolDefinitionError(ToolError):
|
|
30
125
|
"""
|
|
31
|
-
Raised when there is an error in the definition of a tool.
|
|
126
|
+
Raised when there is an error in the definition/signature of a tool.
|
|
32
127
|
"""
|
|
33
128
|
|
|
34
|
-
|
|
129
|
+
kind: ErrorKind = ErrorKind.TOOL_DEFINITION_BAD_DEFINITION
|
|
130
|
+
|
|
131
|
+
def __init__(self, message: str) -> None:
|
|
132
|
+
super().__init__(message)
|
|
133
|
+
self.message = message
|
|
134
|
+
|
|
135
|
+
def create_message_prefix(self, tool_name: str) -> str:
|
|
136
|
+
return f"[{self.kind.value}] {type(self).__name__} in definition of tool '{tool_name}': "
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class ToolInputSchemaError(ToolDefinitionError):
|
|
140
|
+
"""Raised when there is an error in the schema of a tool's input parameter."""
|
|
141
|
+
|
|
142
|
+
kind: ErrorKind = ErrorKind.TOOL_DEFINITION_BAD_INPUT_SCHEMA
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class ToolOutputSchemaError(ToolDefinitionError):
|
|
146
|
+
"""Raised when there is an error in the schema of a tool's output parameter."""
|
|
147
|
+
|
|
148
|
+
kind: ErrorKind = ErrorKind.TOOL_DEFINITION_BAD_OUTPUT_SCHEMA
|
|
35
149
|
|
|
36
150
|
|
|
37
151
|
# ------ runtime errors ------
|
|
152
|
+
class ToolRuntimeError(ToolError, RuntimeError):
|
|
153
|
+
"""
|
|
154
|
+
Any failure starting from when the tool call begins until the tool call returns.
|
|
38
155
|
|
|
156
|
+
Note: This class should typically not be instantiated directly, but rather subclassed.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
kind: ErrorKind = ErrorKind.TOOL_RUNTIME_FATAL
|
|
160
|
+
can_retry: bool = False
|
|
161
|
+
status_code: int | None = None
|
|
162
|
+
extra: dict[str, Any] | None = None
|
|
39
163
|
|
|
40
|
-
class ToolRuntimeError(RuntimeError):
|
|
41
164
|
def __init__(
|
|
42
165
|
self,
|
|
43
166
|
message: str,
|
|
44
|
-
developer_message:
|
|
167
|
+
developer_message: str | None = None,
|
|
168
|
+
*,
|
|
169
|
+
extra: dict[str, Any] | None = None,
|
|
45
170
|
):
|
|
46
171
|
super().__init__(message)
|
|
47
172
|
self.message = message
|
|
48
|
-
self.developer_message = developer_message
|
|
173
|
+
self.developer_message = developer_message # type: ignore[assignment]
|
|
174
|
+
self.extra = extra
|
|
49
175
|
|
|
50
|
-
def
|
|
51
|
-
|
|
176
|
+
def create_message_prefix(self, tool_name: str) -> str:
|
|
177
|
+
return f"[{self.kind.value}] {type(self).__name__} during execution of tool '{tool_name}': "
|
|
178
|
+
|
|
179
|
+
def stacktrace(self) -> str | None:
|
|
52
180
|
if self.__cause__:
|
|
53
181
|
return "\n".join(traceback.format_exception(self.__cause__))
|
|
54
182
|
return None
|
|
55
183
|
|
|
184
|
+
def traceback_info(self) -> str | None:
|
|
185
|
+
"""DEPRECATED: Use stacktrace() instead.
|
|
186
|
+
|
|
187
|
+
This method is deprecated and will be removed in a future major version.
|
|
188
|
+
"""
|
|
189
|
+
return self.stacktrace()
|
|
56
190
|
|
|
191
|
+
# wire-format helper
|
|
192
|
+
def to_payload(self) -> dict[str, Any]:
|
|
193
|
+
return {
|
|
194
|
+
"message": self.message,
|
|
195
|
+
"developer_message": self.developer_message,
|
|
196
|
+
"kind": self.kind,
|
|
197
|
+
"can_retry": self.can_retry,
|
|
198
|
+
"status_code": self.status_code,
|
|
199
|
+
**(self.extra or {}),
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# 1. ------ serialization errors ------
|
|
204
|
+
class ToolSerializationError(ToolRuntimeError):
|
|
205
|
+
"""
|
|
206
|
+
Raised when there is an error serializing/marshalling the tool call arguments or return value.
|
|
207
|
+
|
|
208
|
+
Note: This class is not intended to be instantiated directly, but rather subclassed.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class ToolInputError(ToolSerializationError):
|
|
213
|
+
"""
|
|
214
|
+
Raised when there is an error parsing a tool call argument.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
kind: ErrorKind = ErrorKind.TOOL_RUNTIME_BAD_INPUT_VALUE
|
|
218
|
+
status_code: int = 400
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class ToolOutputError(ToolSerializationError):
|
|
222
|
+
"""
|
|
223
|
+
Raised when there is an error serializing a tool call return value.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
kind: ErrorKind = ErrorKind.TOOL_RUNTIME_BAD_OUTPUT_VALUE
|
|
227
|
+
status_code: int = 500
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# 2. ------ tool-body errors ------
|
|
57
231
|
class ToolExecutionError(ToolRuntimeError):
|
|
58
232
|
"""
|
|
59
|
-
Raised when there is an error executing a tool.
|
|
233
|
+
DEPRECATED: Raised when there is an error executing a tool.
|
|
234
|
+
|
|
235
|
+
ToolExecutionError is deprecated and will be removed in a future major version.
|
|
236
|
+
Use more specific error types instead:
|
|
237
|
+
- RetryableToolError for retryable errors
|
|
238
|
+
- ContextRequiredToolError for errors requiring user context
|
|
239
|
+
- FatalToolError for fatal/unexpected errors
|
|
240
|
+
- UpstreamError for upstream service errors
|
|
241
|
+
- UpstreamRateLimitError for upstream rate limiting errors
|
|
60
242
|
"""
|
|
61
243
|
|
|
62
|
-
|
|
244
|
+
def __init__(
|
|
245
|
+
self,
|
|
246
|
+
message: str,
|
|
247
|
+
developer_message: str | None = None,
|
|
248
|
+
*,
|
|
249
|
+
extra: dict[str, Any] | None = None,
|
|
250
|
+
):
|
|
251
|
+
if type(self) is ToolExecutionError:
|
|
252
|
+
warnings.warn(
|
|
253
|
+
"ToolExecutionError is deprecated and will be removed in a future major version. "
|
|
254
|
+
"Use more specific error types instead: RetryableToolError, ContextRequiredToolError, "
|
|
255
|
+
"FatalToolError, UpstreamError, or UpstreamRateLimitError.",
|
|
256
|
+
DeprecationWarning,
|
|
257
|
+
stacklevel=2,
|
|
258
|
+
)
|
|
259
|
+
super().__init__(message, developer_message=developer_message, extra=extra)
|
|
63
260
|
|
|
64
261
|
|
|
65
262
|
class RetryableToolError(ToolExecutionError):
|
|
66
263
|
"""
|
|
67
|
-
Raised when a tool error is retryable.
|
|
264
|
+
Raised when a tool execution error is retryable.
|
|
68
265
|
"""
|
|
69
266
|
|
|
267
|
+
kind: ErrorKind = ErrorKind.TOOL_RUNTIME_RETRY
|
|
268
|
+
can_retry: bool = True
|
|
269
|
+
|
|
70
270
|
def __init__(
|
|
71
271
|
self,
|
|
72
272
|
message: str,
|
|
73
|
-
developer_message:
|
|
74
|
-
additional_prompt_content:
|
|
75
|
-
retry_after_ms:
|
|
273
|
+
developer_message: str | None = None,
|
|
274
|
+
additional_prompt_content: str | None = None, # TODO: Make required in next major version
|
|
275
|
+
retry_after_ms: int | None = None,
|
|
276
|
+
extra: dict[str, Any] | None = None,
|
|
76
277
|
):
|
|
77
|
-
super().__init__(message, developer_message)
|
|
278
|
+
super().__init__(message, developer_message=developer_message, extra=extra)
|
|
78
279
|
self.additional_prompt_content = additional_prompt_content
|
|
79
280
|
self.retry_after_ms = retry_after_ms
|
|
80
281
|
|
|
81
282
|
|
|
82
|
-
class
|
|
283
|
+
class ContextRequiredToolError(ToolExecutionError):
|
|
83
284
|
"""
|
|
84
|
-
Raised when
|
|
285
|
+
Raised when the combination of additional content from the tool AND
|
|
286
|
+
additional context from the end-user/orchestrator is required before retrying the tool.
|
|
287
|
+
|
|
288
|
+
This is typically used when an argument provided to the tool is invalid in some way,
|
|
289
|
+
and immediately prompting an LLM to retry the tool call is not desired.
|
|
85
290
|
"""
|
|
86
291
|
|
|
87
|
-
|
|
292
|
+
kind: ErrorKind = ErrorKind.TOOL_RUNTIME_CONTEXT_REQUIRED
|
|
88
293
|
|
|
294
|
+
def __init__(
|
|
295
|
+
self,
|
|
296
|
+
message: str,
|
|
297
|
+
additional_prompt_content: str,
|
|
298
|
+
developer_message: str | None = None,
|
|
299
|
+
*,
|
|
300
|
+
extra: dict[str, Any] | None = None,
|
|
301
|
+
):
|
|
302
|
+
super().__init__(message, developer_message=developer_message, extra=extra)
|
|
303
|
+
self.additional_prompt_content = additional_prompt_content
|
|
89
304
|
|
|
90
|
-
|
|
305
|
+
|
|
306
|
+
class FatalToolError(ToolExecutionError):
|
|
91
307
|
"""
|
|
92
|
-
Raised when there is an
|
|
308
|
+
Raised when there is an unexpected or unknown error executing a tool.
|
|
93
309
|
"""
|
|
94
310
|
|
|
95
|
-
|
|
311
|
+
status_code: int = 500
|
|
96
312
|
|
|
313
|
+
def __init__(
|
|
314
|
+
self,
|
|
315
|
+
message: str,
|
|
316
|
+
developer_message: str | None = None,
|
|
317
|
+
*,
|
|
318
|
+
extra: dict[str, Any] | None = None,
|
|
319
|
+
):
|
|
320
|
+
super().__init__(message, developer_message=developer_message, extra=extra)
|
|
97
321
|
|
|
98
|
-
|
|
322
|
+
|
|
323
|
+
# 3. ------ upstream errors in tool body------
|
|
324
|
+
class UpstreamError(ToolExecutionError):
|
|
325
|
+
"""
|
|
326
|
+
Error from an upstream service/API during tool execution.
|
|
327
|
+
|
|
328
|
+
This class handles all upstream failures except rate limiting.
|
|
329
|
+
The status_code and extra dict provide details about the specific error type.
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
def __init__(
|
|
333
|
+
self,
|
|
334
|
+
message: str,
|
|
335
|
+
developer_message: str | None = None,
|
|
336
|
+
*,
|
|
337
|
+
status_code: int,
|
|
338
|
+
extra: dict[str, Any] | None = None,
|
|
339
|
+
):
|
|
340
|
+
super().__init__(message, developer_message=developer_message, extra=extra)
|
|
341
|
+
self.status_code = status_code
|
|
342
|
+
# Determine retryability based on status code
|
|
343
|
+
self.can_retry = status_code >= 500 or status_code == 429
|
|
344
|
+
# Set appropriate error kind based on status
|
|
345
|
+
if status_code in (401, 403):
|
|
346
|
+
self.kind = ErrorKind.UPSTREAM_RUNTIME_AUTH_ERROR
|
|
347
|
+
elif status_code == 404:
|
|
348
|
+
self.kind = ErrorKind.UPSTREAM_RUNTIME_NOT_FOUND
|
|
349
|
+
elif status_code == 429:
|
|
350
|
+
self.kind = ErrorKind.UPSTREAM_RUNTIME_RATE_LIMIT
|
|
351
|
+
elif status_code >= 500:
|
|
352
|
+
self.kind = ErrorKind.UPSTREAM_RUNTIME_SERVER_ERROR
|
|
353
|
+
elif 400 <= status_code < 500:
|
|
354
|
+
self.kind = ErrorKind.UPSTREAM_RUNTIME_BAD_REQUEST
|
|
355
|
+
else:
|
|
356
|
+
self.kind = ErrorKind.UPSTREAM_RUNTIME_UNMAPPED
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class UpstreamRateLimitError(UpstreamError):
|
|
99
360
|
"""
|
|
100
|
-
|
|
361
|
+
Rate limit error from an upstream service.
|
|
362
|
+
|
|
363
|
+
Special case of UpstreamError that includes retry_after_ms information.
|
|
101
364
|
"""
|
|
102
365
|
|
|
103
|
-
|
|
366
|
+
kind: ErrorKind = ErrorKind.UPSTREAM_RUNTIME_RATE_LIMIT
|
|
367
|
+
can_retry: bool = True
|
|
368
|
+
|
|
369
|
+
def __init__(
|
|
370
|
+
self,
|
|
371
|
+
message: str,
|
|
372
|
+
retry_after_ms: int,
|
|
373
|
+
developer_message: str | None = None,
|
|
374
|
+
*,
|
|
375
|
+
extra: dict[str, Any] | None = None,
|
|
376
|
+
):
|
|
377
|
+
super().__init__(message, status_code=429, developer_message=developer_message, extra=extra)
|
|
378
|
+
self.retry_after_ms = retry_after_ms
|