arcade-core 2.2.2__tar.gz → 2.4.0__tar.gz
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-2.2.2 → arcade_core-2.4.0}/PKG-INFO +1 -1
- {arcade_core-2.2.2 → arcade_core-2.4.0}/arcade_core/auth.py +9 -0
- {arcade_core-2.2.2 → arcade_core-2.4.0}/arcade_core/catalog.py +79 -31
- arcade_core-2.4.0/arcade_core/errors.py +378 -0
- {arcade_core-2.2.2 → arcade_core-2.4.0}/arcade_core/executor.py +10 -17
- {arcade_core-2.2.2 → arcade_core-2.4.0}/arcade_core/output.py +45 -9
- {arcade_core-2.2.2 → arcade_core-2.4.0}/arcade_core/schema.py +26 -3
- {arcade_core-2.2.2 → arcade_core-2.4.0}/pyproject.toml +1 -1
- arcade_core-2.2.2/arcade_core/errors.py +0 -103
- {arcade_core-2.2.2 → arcade_core-2.4.0}/.gitignore +0 -0
- {arcade_core-2.2.2 → arcade_core-2.4.0}/README.md +0 -0
- {arcade_core-2.2.2 → arcade_core-2.4.0}/arcade_core/__init__.py +0 -0
- {arcade_core-2.2.2 → arcade_core-2.4.0}/arcade_core/annotations.py +0 -0
- {arcade_core-2.2.2 → arcade_core-2.4.0}/arcade_core/config.py +0 -0
- {arcade_core-2.2.2 → arcade_core-2.4.0}/arcade_core/config_model.py +0 -0
- {arcade_core-2.2.2 → arcade_core-2.4.0}/arcade_core/parse.py +0 -0
- {arcade_core-2.2.2 → arcade_core-2.4.0}/arcade_core/py.typed +0 -0
- {arcade_core-2.2.2 → arcade_core-2.4.0}/arcade_core/telemetry.py +0 -0
- {arcade_core-2.2.2 → arcade_core-2.4.0}/arcade_core/toolkit.py +0 -0
- {arcade_core-2.2.2 → arcade_core-2.4.0}/arcade_core/utils.py +0 -0
- {arcade_core-2.2.2 → arcade_core-2.4.0}/arcade_core/version.py +0 -0
|
@@ -51,6 +51,15 @@ class Atlassian(OAuth2):
|
|
|
51
51
|
super().__init__(id=id, scopes=scopes)
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
class ClickUp(OAuth2):
|
|
55
|
+
"""Marks a tool as requiring ClickUp authorization."""
|
|
56
|
+
|
|
57
|
+
provider_id: str = "clickup"
|
|
58
|
+
|
|
59
|
+
def __init__(self, *, id: Optional[str] = None, scopes: Optional[list[str]] = None): # noqa: A002
|
|
60
|
+
super().__init__(id=id, scopes=scopes)
|
|
61
|
+
|
|
62
|
+
|
|
54
63
|
class Discord(OAuth2):
|
|
55
64
|
"""Marks a tool as requiring Discord authorization."""
|
|
56
65
|
|
|
@@ -27,7 +27,12 @@ from pydantic_core import PydanticUndefined
|
|
|
27
27
|
|
|
28
28
|
from arcade_core.annotations import Inferrable
|
|
29
29
|
from arcade_core.auth import OAuth2, ToolAuthorization
|
|
30
|
-
from arcade_core.errors import
|
|
30
|
+
from arcade_core.errors import (
|
|
31
|
+
ToolDefinitionError,
|
|
32
|
+
ToolInputSchemaError,
|
|
33
|
+
ToolkitLoadError,
|
|
34
|
+
ToolOutputSchemaError,
|
|
35
|
+
)
|
|
31
36
|
from arcade_core.schema import (
|
|
32
37
|
TOOL_NAME_SEPARATOR,
|
|
33
38
|
FullyQualifiedName,
|
|
@@ -224,7 +229,9 @@ class ToolCatalog(BaseModel):
|
|
|
224
229
|
fully_qualified_name = definition.get_fully_qualified_name()
|
|
225
230
|
|
|
226
231
|
if fully_qualified_name in self._tools:
|
|
227
|
-
raise
|
|
232
|
+
raise ToolkitLoadError(
|
|
233
|
+
f"Tool '{definition.name}' in toolkit '{toolkit_name}' already exists in the catalog."
|
|
234
|
+
)
|
|
228
235
|
|
|
229
236
|
if str(fully_qualified_name).lower() in self._disabled_tools:
|
|
230
237
|
logger.info(f"Tool '{fully_qualified_name!s}' is disabled and will not be cataloged.")
|
|
@@ -270,20 +277,26 @@ class ToolCatalog(BaseModel):
|
|
|
270
277
|
tool_func = getattr(module, tool_name)
|
|
271
278
|
self.add_tool(tool_func, toolkit, module)
|
|
272
279
|
|
|
280
|
+
except ToolDefinitionError as e:
|
|
281
|
+
raise e.with_context(tool_name) from e
|
|
282
|
+
except ToolkitLoadError as e:
|
|
283
|
+
raise e.with_context(toolkit.name) from e
|
|
284
|
+
except ImportError as e:
|
|
285
|
+
raise ToolkitLoadError(
|
|
286
|
+
f"Could not import module {module_name}. Reason: {e}"
|
|
287
|
+
).with_context(tool_name)
|
|
273
288
|
except AttributeError as e:
|
|
274
289
|
raise ToolDefinitionError(
|
|
275
290
|
f"Could not import tool {tool_name} in module {module_name}. Reason: {e}"
|
|
276
|
-
)
|
|
277
|
-
except ImportError as e:
|
|
278
|
-
raise ToolDefinitionError(f"Could not import module {module_name}. Reason: {e}")
|
|
291
|
+
).with_context(tool_name)
|
|
279
292
|
except TypeError as e:
|
|
280
293
|
raise ToolDefinitionError(
|
|
281
294
|
f"Type error encountered while adding tool {tool_name} from {module_name}. Reason: {e}"
|
|
282
|
-
)
|
|
295
|
+
).with_context(tool_name)
|
|
283
296
|
except Exception as e:
|
|
284
297
|
raise ToolDefinitionError(
|
|
285
298
|
f"Error encountered while adding tool {tool_name} from {module_name}. Reason: {e}"
|
|
286
|
-
)
|
|
299
|
+
).with_context(tool_name)
|
|
287
300
|
|
|
288
301
|
def __getitem__(self, name: FullyQualifiedName) -> MaterializedTool:
|
|
289
302
|
return self.get_tool(name)
|
|
@@ -392,11 +405,11 @@ class ToolCatalog(BaseModel):
|
|
|
392
405
|
# Hard requirement: tools must have descriptions
|
|
393
406
|
tool_description = getattr(tool, "__tool_description__", None)
|
|
394
407
|
if not tool_description:
|
|
395
|
-
raise ToolDefinitionError(f"Tool {raw_tool_name} is missing a description")
|
|
408
|
+
raise ToolDefinitionError(f"Tool '{raw_tool_name}' is missing a description")
|
|
396
409
|
|
|
397
410
|
# If the function returns a value, it must have a type annotation
|
|
398
411
|
if does_function_return_value(tool) and tool.__annotations__.get("return") is None:
|
|
399
|
-
raise
|
|
412
|
+
raise ToolOutputSchemaError(f"Tool '{raw_tool_name}' must have a return type")
|
|
400
413
|
|
|
401
414
|
auth_requirement = create_auth_requirement(tool)
|
|
402
415
|
secrets_requirement = create_secrets_requirement(tool)
|
|
@@ -438,7 +451,7 @@ def create_input_definition(func: Callable) -> ToolInput:
|
|
|
438
451
|
for _, param in inspect.signature(func, follow_wrapped=True).parameters.items():
|
|
439
452
|
if param.annotation is ToolContext:
|
|
440
453
|
if tool_context_param_name is not None:
|
|
441
|
-
raise
|
|
454
|
+
raise ToolInputSchemaError(
|
|
442
455
|
f"Only one ToolContext parameter is supported, but tool {func.__name__} has multiple."
|
|
443
456
|
)
|
|
444
457
|
|
|
@@ -483,7 +496,11 @@ def create_output_definition(func: Callable) -> ToolOutput:
|
|
|
483
496
|
)
|
|
484
497
|
|
|
485
498
|
if hasattr(return_type, "__metadata__"):
|
|
486
|
-
description =
|
|
499
|
+
description = (
|
|
500
|
+
return_type.__metadata__[0]
|
|
501
|
+
if return_type.__metadata__
|
|
502
|
+
else "No description provided for return type."
|
|
503
|
+
)
|
|
487
504
|
return_type = return_type.__origin__
|
|
488
505
|
|
|
489
506
|
# Unwrap Optional types
|
|
@@ -631,7 +648,7 @@ def extract_field_info(param: inspect.Parameter) -> ToolParamInfo:
|
|
|
631
648
|
"""
|
|
632
649
|
annotation = param.annotation
|
|
633
650
|
if annotation == inspect.Parameter.empty:
|
|
634
|
-
raise
|
|
651
|
+
raise ToolInputSchemaError(f"Parameter {param} has no type annotation.")
|
|
635
652
|
|
|
636
653
|
# Get the majority of the param info from either the Pydantic Field() or regular inspection
|
|
637
654
|
if isinstance(param.default, FieldInfo):
|
|
@@ -650,7 +667,7 @@ def extract_field_info(param: inspect.Parameter) -> ToolParamInfo:
|
|
|
650
667
|
elif len(str_annotations) == 2:
|
|
651
668
|
new_name = str_annotations[0]
|
|
652
669
|
if not new_name.isidentifier():
|
|
653
|
-
raise
|
|
670
|
+
raise ToolInputSchemaError(
|
|
654
671
|
f"Invalid parameter name: '{new_name}' is not a valid identifier. "
|
|
655
672
|
"Identifiers must start with a letter or underscore, "
|
|
656
673
|
"and can only contain letters, digits, or underscores."
|
|
@@ -658,7 +675,7 @@ def extract_field_info(param: inspect.Parameter) -> ToolParamInfo:
|
|
|
658
675
|
param_info.name = new_name
|
|
659
676
|
param_info.description = str_annotations[1]
|
|
660
677
|
else:
|
|
661
|
-
raise
|
|
678
|
+
raise ToolInputSchemaError(
|
|
662
679
|
f"Parameter {param} has too many string annotations. Expected 0, 1, or 2, got {len(str_annotations)}."
|
|
663
680
|
)
|
|
664
681
|
|
|
@@ -673,10 +690,10 @@ def extract_field_info(param: inspect.Parameter) -> ToolParamInfo:
|
|
|
673
690
|
|
|
674
691
|
# Final reality check
|
|
675
692
|
if param_info.description is None:
|
|
676
|
-
raise
|
|
693
|
+
raise ToolInputSchemaError(f"Parameter '{param_info.name}' is missing a description")
|
|
677
694
|
|
|
678
695
|
if wire_type_info.wire_type is None:
|
|
679
|
-
raise
|
|
696
|
+
raise ToolInputSchemaError(f"Unknown parameter type: {param_info.field_type}")
|
|
680
697
|
|
|
681
698
|
return ToolParamInfo.from_param_info(param_info, wire_type_info, is_inferrable)
|
|
682
699
|
|
|
@@ -792,6 +809,7 @@ def extract_properties(type_to_check: type) -> dict[str, WireTypeInfo] | None:
|
|
|
792
809
|
field_type = next(arg for arg in get_args(field_type) if arg is not type(None))
|
|
793
810
|
|
|
794
811
|
# Get wire type info recursively
|
|
812
|
+
# field_type cannot be None here due to the check above
|
|
795
813
|
wire_info = get_wire_type_info(field_type)
|
|
796
814
|
properties[field_name] = wire_info
|
|
797
815
|
|
|
@@ -870,7 +888,7 @@ def extract_python_param_info(param: inspect.Parameter) -> ParamInfo:
|
|
|
870
888
|
# Union types are not currently supported
|
|
871
889
|
# (other than optional, which is handled above)
|
|
872
890
|
if is_union(field_type):
|
|
873
|
-
raise
|
|
891
|
+
raise ToolInputSchemaError(
|
|
874
892
|
f"Parameter {param.name} is a union type. Only optional types are supported."
|
|
875
893
|
)
|
|
876
894
|
|
|
@@ -890,7 +908,7 @@ def extract_pydantic_param_info(param: inspect.Parameter) -> ParamInfo:
|
|
|
890
908
|
if callable(param.default.default_factory):
|
|
891
909
|
default_value = param.default.default_factory()
|
|
892
910
|
else:
|
|
893
|
-
raise
|
|
911
|
+
raise ToolInputSchemaError(f"Default factory for parameter {param} is not callable.")
|
|
894
912
|
|
|
895
913
|
# If the param is Annotated[], unwrap the annotation to get the "real" type
|
|
896
914
|
# Otherwise, use the literal type
|
|
@@ -973,7 +991,9 @@ def create_func_models(func: Callable) -> tuple[type[BaseModel], type[BaseModel]
|
|
|
973
991
|
tool_field_info = extract_field_info(param)
|
|
974
992
|
param_fields = {
|
|
975
993
|
"default": tool_field_info.default,
|
|
976
|
-
"description": tool_field_info.description
|
|
994
|
+
"description": tool_field_info.description
|
|
995
|
+
if tool_field_info.description
|
|
996
|
+
else "No description provided.",
|
|
977
997
|
# TODO more here?
|
|
978
998
|
}
|
|
979
999
|
input_fields[name] = (tool_field_info.field_type, Field(**param_fields))
|
|
@@ -981,7 +1001,6 @@ def create_func_models(func: Callable) -> tuple[type[BaseModel], type[BaseModel]
|
|
|
981
1001
|
input_model = create_model(f"{snake_to_pascal_case(func.__name__)}Input", **input_fields) # type: ignore[call-overload]
|
|
982
1002
|
|
|
983
1003
|
output_model = determine_output_model(func)
|
|
984
|
-
|
|
985
1004
|
return input_model, output_model
|
|
986
1005
|
|
|
987
1006
|
|
|
@@ -991,8 +1010,14 @@ def determine_output_model(func: Callable) -> type[BaseModel]: # noqa: C901
|
|
|
991
1010
|
"""
|
|
992
1011
|
return_annotation = inspect.signature(func).return_annotation
|
|
993
1012
|
output_model_name = f"{snake_to_pascal_case(func.__name__)}Output"
|
|
1013
|
+
|
|
1014
|
+
# If the return annotation is empty, create a model with no fields
|
|
994
1015
|
if return_annotation is inspect.Signature.empty:
|
|
995
1016
|
return create_model(output_model_name)
|
|
1017
|
+
|
|
1018
|
+
# If the return annotation has an __origin__ attribute
|
|
1019
|
+
# and does not have a __metadata__ attribute.
|
|
1020
|
+
# This is the case for TypedDicts.
|
|
996
1021
|
elif hasattr(return_annotation, "__origin__"):
|
|
997
1022
|
if hasattr(return_annotation, "__metadata__"):
|
|
998
1023
|
field_type = return_annotation.__args__[0]
|
|
@@ -1008,15 +1033,30 @@ def determine_output_model(func: Callable) -> type[BaseModel]: # noqa: C901
|
|
|
1008
1033
|
)
|
|
1009
1034
|
return create_model(
|
|
1010
1035
|
output_model_name,
|
|
1011
|
-
result=(
|
|
1036
|
+
result=(
|
|
1037
|
+
typeddict_model,
|
|
1038
|
+
Field(
|
|
1039
|
+
description=str(description)
|
|
1040
|
+
if description
|
|
1041
|
+
else "No description provided."
|
|
1042
|
+
),
|
|
1043
|
+
),
|
|
1012
1044
|
)
|
|
1013
1045
|
|
|
1046
|
+
# If the return annotation has a description, use it
|
|
1014
1047
|
if description:
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1048
|
+
try:
|
|
1049
|
+
return create_model(
|
|
1050
|
+
output_model_name,
|
|
1051
|
+
result=(field_type, Field(description=str(description))),
|
|
1052
|
+
)
|
|
1053
|
+
except Exception:
|
|
1054
|
+
raise ToolOutputSchemaError(
|
|
1055
|
+
f"Unsupported output type '{field_type}'. Only built-in Python types, TypedDicts, "
|
|
1056
|
+
"Pydantic models, and standard collections are supported as tool output types."
|
|
1057
|
+
)
|
|
1058
|
+
|
|
1059
|
+
# If the return annotation is a Union type
|
|
1020
1060
|
origin = return_annotation.__origin__
|
|
1021
1061
|
if origin is typing.Union:
|
|
1022
1062
|
# For union types, create a model with the first non-None argument
|
|
@@ -1037,10 +1077,15 @@ def determine_output_model(func: Callable) -> type[BaseModel]: # noqa: C901
|
|
|
1037
1077
|
)
|
|
1038
1078
|
return create_model(
|
|
1039
1079
|
output_model_name,
|
|
1040
|
-
result=(
|
|
1080
|
+
result=(
|
|
1081
|
+
arg,
|
|
1082
|
+
Field(description="No description provided."),
|
|
1083
|
+
),
|
|
1041
1084
|
)
|
|
1042
|
-
|
|
1085
|
+
|
|
1086
|
+
# If the return annotation has an __origin__ attribute
|
|
1043
1087
|
# and does not have a __metadata__ attribute.
|
|
1088
|
+
# This is the case for TypedDicts.
|
|
1044
1089
|
return create_model(
|
|
1045
1090
|
output_model_name,
|
|
1046
1091
|
result=(
|
|
@@ -1049,7 +1094,7 @@ def determine_output_model(func: Callable) -> type[BaseModel]: # noqa: C901
|
|
|
1049
1094
|
),
|
|
1050
1095
|
)
|
|
1051
1096
|
else:
|
|
1052
|
-
#
|
|
1097
|
+
# If the return annotation is a TypedDict
|
|
1053
1098
|
if is_typeddict(return_annotation):
|
|
1054
1099
|
typeddict_model = create_model_from_typeddict(return_annotation, output_model_name)
|
|
1055
1100
|
return create_model(
|
|
@@ -1060,10 +1105,13 @@ def determine_output_model(func: Callable) -> type[BaseModel]: # noqa: C901
|
|
|
1060
1105
|
),
|
|
1061
1106
|
)
|
|
1062
1107
|
|
|
1063
|
-
#
|
|
1108
|
+
# If the return annotation is a simple type (like str)
|
|
1064
1109
|
return create_model(
|
|
1065
1110
|
output_model_name,
|
|
1066
|
-
result=(
|
|
1111
|
+
result=(
|
|
1112
|
+
return_annotation,
|
|
1113
|
+
Field(description="No description provided."),
|
|
1114
|
+
),
|
|
1067
1115
|
)
|
|
1068
1116
|
|
|
1069
1117
|
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import traceback
|
|
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):
|
|
34
|
+
"""
|
|
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
|
|
50
|
+
"""
|
|
51
|
+
|
|
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
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ToolkitLoadError(ToolkitError):
|
|
99
|
+
"""
|
|
100
|
+
Raised while importing / loading a toolkit package
|
|
101
|
+
(e.g. missing dependency, SyntaxError in module top-level code).
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
kind: ErrorKind = ErrorKind.TOOLKIT_LOAD_FAILED
|
|
105
|
+
can_retry: bool = False
|
|
106
|
+
|
|
107
|
+
def __init__(self, message: str) -> None:
|
|
108
|
+
super().__init__(message)
|
|
109
|
+
self.message = message
|
|
110
|
+
|
|
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):
|
|
116
|
+
"""
|
|
117
|
+
Any error related to an Arcade tool.
|
|
118
|
+
|
|
119
|
+
Note: This class is an abstract class and cannot be instantiated directly.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ------ definition-time errors (tool developer's responsibility) ------
|
|
124
|
+
class ToolDefinitionError(ToolError):
|
|
125
|
+
"""
|
|
126
|
+
Raised when there is an error in the definition/signature of a tool.
|
|
127
|
+
"""
|
|
128
|
+
|
|
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
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ------ runtime errors ------
|
|
152
|
+
class ToolRuntimeError(ToolError, RuntimeError):
|
|
153
|
+
"""
|
|
154
|
+
Any failure starting from when the tool call begins until the tool call returns.
|
|
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
|
|
163
|
+
|
|
164
|
+
def __init__(
|
|
165
|
+
self,
|
|
166
|
+
message: str,
|
|
167
|
+
developer_message: str | None = None,
|
|
168
|
+
*,
|
|
169
|
+
extra: dict[str, Any] | None = None,
|
|
170
|
+
):
|
|
171
|
+
super().__init__(message)
|
|
172
|
+
self.message = message
|
|
173
|
+
self.developer_message = developer_message # type: ignore[assignment]
|
|
174
|
+
self.extra = extra
|
|
175
|
+
|
|
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:
|
|
180
|
+
if self.__cause__:
|
|
181
|
+
return "\n".join(traceback.format_exception(self.__cause__))
|
|
182
|
+
return None
|
|
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()
|
|
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 ------
|
|
231
|
+
class ToolExecutionError(ToolRuntimeError):
|
|
232
|
+
"""
|
|
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
|
|
242
|
+
"""
|
|
243
|
+
|
|
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)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class RetryableToolError(ToolExecutionError):
|
|
263
|
+
"""
|
|
264
|
+
Raised when a tool execution error is retryable.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
kind: ErrorKind = ErrorKind.TOOL_RUNTIME_RETRY
|
|
268
|
+
can_retry: bool = True
|
|
269
|
+
|
|
270
|
+
def __init__(
|
|
271
|
+
self,
|
|
272
|
+
message: str,
|
|
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,
|
|
277
|
+
):
|
|
278
|
+
super().__init__(message, developer_message=developer_message, extra=extra)
|
|
279
|
+
self.additional_prompt_content = additional_prompt_content
|
|
280
|
+
self.retry_after_ms = retry_after_ms
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class ContextRequiredToolError(ToolExecutionError):
|
|
284
|
+
"""
|
|
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.
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
kind: ErrorKind = ErrorKind.TOOL_RUNTIME_CONTEXT_REQUIRED
|
|
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
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class FatalToolError(ToolExecutionError):
|
|
307
|
+
"""
|
|
308
|
+
Raised when there is an unexpected or unknown error executing a tool.
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
status_code: int = 500
|
|
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)
|
|
321
|
+
|
|
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):
|
|
360
|
+
"""
|
|
361
|
+
Rate limit error from an upstream service.
|
|
362
|
+
|
|
363
|
+
Special case of UpstreamError that includes retry_after_ms information.
|
|
364
|
+
"""
|
|
365
|
+
|
|
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
|
|
@@ -6,11 +6,9 @@ from typing import Any
|
|
|
6
6
|
from pydantic import BaseModel, ValidationError
|
|
7
7
|
|
|
8
8
|
from arcade_core.errors import (
|
|
9
|
-
RetryableToolError,
|
|
10
9
|
ToolInputError,
|
|
11
10
|
ToolOutputError,
|
|
12
11
|
ToolRuntimeError,
|
|
13
|
-
ToolSerializationError,
|
|
14
12
|
)
|
|
15
13
|
from arcade_core.output import output_factory
|
|
16
14
|
from arcade_core.schema import (
|
|
@@ -69,31 +67,26 @@ class ToolExecutor:
|
|
|
69
67
|
# return the output
|
|
70
68
|
return output_factory.success(data=output, logs=tool_call_logs)
|
|
71
69
|
|
|
72
|
-
except RetryableToolError as e:
|
|
73
|
-
return output_factory.fail_retry(
|
|
74
|
-
message=e.message,
|
|
75
|
-
developer_message=e.developer_message,
|
|
76
|
-
additional_prompt_content=e.additional_prompt_content,
|
|
77
|
-
retry_after_ms=e.retry_after_ms,
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
except ToolSerializationError as e:
|
|
81
|
-
return output_factory.fail(message=e.message, developer_message=e.developer_message)
|
|
82
|
-
|
|
83
|
-
# should catch all tool exceptions due to the try/except in the tool decorator
|
|
84
70
|
except ToolRuntimeError as e:
|
|
71
|
+
e.with_context(func.__name__)
|
|
85
72
|
return output_factory.fail(
|
|
86
73
|
message=e.message,
|
|
87
74
|
developer_message=e.developer_message,
|
|
88
|
-
|
|
75
|
+
stacktrace=e.stacktrace(),
|
|
76
|
+
additional_prompt_content=getattr(e, "additional_prompt_content", None),
|
|
77
|
+
retry_after_ms=getattr(e, "retry_after_ms", None),
|
|
78
|
+
kind=e.kind,
|
|
79
|
+
can_retry=e.can_retry,
|
|
80
|
+
status_code=e.status_code,
|
|
81
|
+
extra=e.extra,
|
|
89
82
|
)
|
|
90
83
|
|
|
91
84
|
# if we get here we're in trouble
|
|
92
85
|
except Exception as e:
|
|
93
86
|
return output_factory.fail(
|
|
94
|
-
message="Error in execution",
|
|
87
|
+
message=f"Error in execution of '{func.__name__}'",
|
|
95
88
|
developer_message=str(e),
|
|
96
|
-
|
|
89
|
+
stacktrace=traceback.format_exc(),
|
|
97
90
|
)
|
|
98
91
|
|
|
99
92
|
@staticmethod
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
from typing import TypeVar
|
|
1
|
+
from typing import Any, TypeVar
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel
|
|
4
4
|
|
|
5
|
+
from arcade_core.errors import ErrorKind
|
|
5
6
|
from arcade_core.schema import ToolCallError, ToolCallLog, ToolCallOutput
|
|
6
7
|
from arcade_core.utils import coerce_empty_list_to_none
|
|
7
8
|
|
|
@@ -25,14 +26,27 @@ class ToolOutputFactory:
|
|
|
25
26
|
|
|
26
27
|
The executor guarantees that `data` is either a string, a dict, or None.
|
|
27
28
|
"""
|
|
28
|
-
value: str | int | float | bool | dict | list
|
|
29
|
+
value: str | int | float | bool | dict | list | None
|
|
29
30
|
if data is None:
|
|
30
31
|
value = ""
|
|
31
32
|
elif hasattr(data, "result"):
|
|
32
|
-
|
|
33
|
+
result = getattr(data, "result", "")
|
|
34
|
+
# Handle None result the same way as None data
|
|
35
|
+
if result is None:
|
|
36
|
+
value = ""
|
|
37
|
+
# If the result is a BaseModel (e.g., from TypedDict conversion), convert to dict
|
|
38
|
+
elif isinstance(result, BaseModel):
|
|
39
|
+
value = result.model_dump()
|
|
40
|
+
# If the result is a list, check if it contains BaseModel objects
|
|
41
|
+
elif isinstance(result, list):
|
|
42
|
+
value = [
|
|
43
|
+
item.model_dump() if isinstance(item, BaseModel) else item for item in result
|
|
44
|
+
]
|
|
45
|
+
else:
|
|
46
|
+
value = result
|
|
33
47
|
elif isinstance(data, BaseModel):
|
|
34
48
|
value = data.model_dump()
|
|
35
|
-
elif isinstance(data, (str, int, float, bool, list)):
|
|
49
|
+
elif isinstance(data, (str, int, float, bool, list, dict)):
|
|
36
50
|
value = data
|
|
37
51
|
else:
|
|
38
52
|
raise ValueError(f"Unsupported data output type: {type(data)}")
|
|
@@ -48,15 +62,26 @@ class ToolOutputFactory:
|
|
|
48
62
|
*,
|
|
49
63
|
message: str,
|
|
50
64
|
developer_message: str | None = None,
|
|
51
|
-
|
|
65
|
+
stacktrace: str | None = None,
|
|
52
66
|
logs: list[ToolCallLog] | None = None,
|
|
67
|
+
additional_prompt_content: str | None = None,
|
|
68
|
+
retry_after_ms: int | None = None,
|
|
69
|
+
kind: ErrorKind = ErrorKind.UNKNOWN,
|
|
70
|
+
can_retry: bool = False,
|
|
71
|
+
status_code: int | None = None,
|
|
72
|
+
extra: dict[str, Any] | None = None,
|
|
53
73
|
) -> ToolCallOutput:
|
|
54
74
|
return ToolCallOutput(
|
|
55
75
|
error=ToolCallError(
|
|
56
76
|
message=message,
|
|
57
77
|
developer_message=developer_message,
|
|
58
|
-
can_retry=
|
|
59
|
-
|
|
78
|
+
can_retry=can_retry,
|
|
79
|
+
additional_prompt_content=additional_prompt_content,
|
|
80
|
+
retry_after_ms=retry_after_ms,
|
|
81
|
+
stacktrace=stacktrace,
|
|
82
|
+
kind=kind,
|
|
83
|
+
status_code=status_code,
|
|
84
|
+
extra=extra,
|
|
60
85
|
),
|
|
61
86
|
logs=coerce_empty_list_to_none(logs),
|
|
62
87
|
)
|
|
@@ -68,9 +93,17 @@ class ToolOutputFactory:
|
|
|
68
93
|
developer_message: str | None = None,
|
|
69
94
|
additional_prompt_content: str | None = None,
|
|
70
95
|
retry_after_ms: int | None = None,
|
|
71
|
-
|
|
96
|
+
stacktrace: str | None = None,
|
|
72
97
|
logs: list[ToolCallLog] | None = None,
|
|
98
|
+
kind: ErrorKind = ErrorKind.TOOL_RUNTIME_RETRY,
|
|
99
|
+
status_code: int = 500,
|
|
100
|
+
extra: dict[str, Any] | None = None,
|
|
73
101
|
) -> ToolCallOutput:
|
|
102
|
+
"""
|
|
103
|
+
DEPRECATED: Use ToolOutputFactory.fail instead.
|
|
104
|
+
This method will be removed in version 3.0.0
|
|
105
|
+
"""
|
|
106
|
+
|
|
74
107
|
return ToolCallOutput(
|
|
75
108
|
error=ToolCallError(
|
|
76
109
|
message=message,
|
|
@@ -78,7 +111,10 @@ class ToolOutputFactory:
|
|
|
78
111
|
can_retry=True,
|
|
79
112
|
additional_prompt_content=additional_prompt_content,
|
|
80
113
|
retry_after_ms=retry_after_ms,
|
|
81
|
-
|
|
114
|
+
stacktrace=stacktrace,
|
|
115
|
+
kind=kind,
|
|
116
|
+
status_code=status_code,
|
|
117
|
+
extra=extra,
|
|
82
118
|
),
|
|
83
119
|
logs=coerce_empty_list_to_none(logs),
|
|
84
120
|
)
|
|
@@ -5,6 +5,8 @@ from typing import Any, Literal
|
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
|
+
from arcade_core.errors import ErrorKind
|
|
9
|
+
|
|
8
10
|
# allow for custom tool name separator
|
|
9
11
|
TOOL_NAME_SEPARATOR = os.getenv("ARCADE_TOOL_NAME_SEPARATOR", ".")
|
|
10
12
|
|
|
@@ -390,6 +392,8 @@ class ToolCallError(BaseModel):
|
|
|
390
392
|
|
|
391
393
|
message: str
|
|
392
394
|
"""The user-facing error message."""
|
|
395
|
+
kind: ErrorKind
|
|
396
|
+
"""The error kind that uniquely identifies the kind of error."""
|
|
393
397
|
developer_message: str | None = None
|
|
394
398
|
"""The developer-facing error details."""
|
|
395
399
|
can_retry: bool = False
|
|
@@ -398,8 +402,27 @@ class ToolCallError(BaseModel):
|
|
|
398
402
|
"""Additional content to be included in the retry prompt."""
|
|
399
403
|
retry_after_ms: int | None = None
|
|
400
404
|
"""The number of milliseconds (if any) to wait before retrying the tool call."""
|
|
401
|
-
|
|
402
|
-
"""The
|
|
405
|
+
stacktrace: str | None = None
|
|
406
|
+
"""The stacktrace information for the tool call."""
|
|
407
|
+
status_code: int | None = None
|
|
408
|
+
"""The HTTP status code of the error."""
|
|
409
|
+
extra: dict[str, Any] | None = None
|
|
410
|
+
"""Additional information about the error."""
|
|
411
|
+
|
|
412
|
+
@property
|
|
413
|
+
def is_toolkit_error(self) -> bool:
|
|
414
|
+
"""Check if this error originated from loading a toolkit."""
|
|
415
|
+
return self.kind.name.startswith("TOOLKIT_")
|
|
416
|
+
|
|
417
|
+
@property
|
|
418
|
+
def is_tool_error(self) -> bool:
|
|
419
|
+
"""Check if this error originated from a tool."""
|
|
420
|
+
return self.kind.name.startswith("TOOL_")
|
|
421
|
+
|
|
422
|
+
@property
|
|
423
|
+
def is_upstream_error(self) -> bool:
|
|
424
|
+
"""Check if this error originated from an upstream service."""
|
|
425
|
+
return self.kind.name.startswith("UPSTREAM_")
|
|
403
426
|
|
|
404
427
|
|
|
405
428
|
class ToolCallRequiresAuthorization(BaseModel):
|
|
@@ -418,7 +441,7 @@ class ToolCallRequiresAuthorization(BaseModel):
|
|
|
418
441
|
class ToolCallOutput(BaseModel):
|
|
419
442
|
"""The output of a tool invocation."""
|
|
420
443
|
|
|
421
|
-
value: str | int | float | bool | dict | list
|
|
444
|
+
value: str | int | float | bool | dict | list | None = None
|
|
422
445
|
"""The value returned by the tool."""
|
|
423
446
|
logs: list[ToolCallLog] | None = None
|
|
424
447
|
"""The logs that occurred during the tool invocation."""
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import traceback
|
|
2
|
-
from typing import Optional
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class ToolkitError(Exception):
|
|
6
|
-
"""
|
|
7
|
-
Base class for all errors related to toolkits.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
pass
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class ToolkitLoadError(ToolkitError):
|
|
14
|
-
"""
|
|
15
|
-
Raised when there is an error loading a toolkit.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
pass
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class ToolError(Exception):
|
|
22
|
-
"""
|
|
23
|
-
Base class for all errors related to tools.
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
pass
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class ToolDefinitionError(ToolError):
|
|
30
|
-
"""
|
|
31
|
-
Raised when there is an error in the definition of a tool.
|
|
32
|
-
"""
|
|
33
|
-
|
|
34
|
-
pass
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# ------ runtime errors ------
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class ToolRuntimeError(RuntimeError):
|
|
41
|
-
def __init__(
|
|
42
|
-
self,
|
|
43
|
-
message: str,
|
|
44
|
-
developer_message: Optional[str] = None,
|
|
45
|
-
):
|
|
46
|
-
super().__init__(message)
|
|
47
|
-
self.message = message
|
|
48
|
-
self.developer_message = developer_message
|
|
49
|
-
|
|
50
|
-
def traceback_info(self) -> str | None:
|
|
51
|
-
# return the traceback information of the parent exception
|
|
52
|
-
if self.__cause__:
|
|
53
|
-
return "\n".join(traceback.format_exception(self.__cause__))
|
|
54
|
-
return None
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class ToolExecutionError(ToolRuntimeError):
|
|
58
|
-
"""
|
|
59
|
-
Raised when there is an error executing a tool.
|
|
60
|
-
"""
|
|
61
|
-
|
|
62
|
-
pass
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class RetryableToolError(ToolExecutionError):
|
|
66
|
-
"""
|
|
67
|
-
Raised when a tool error is retryable.
|
|
68
|
-
"""
|
|
69
|
-
|
|
70
|
-
def __init__(
|
|
71
|
-
self,
|
|
72
|
-
message: str,
|
|
73
|
-
developer_message: Optional[str] = None,
|
|
74
|
-
additional_prompt_content: Optional[str] = None,
|
|
75
|
-
retry_after_ms: Optional[int] = None,
|
|
76
|
-
):
|
|
77
|
-
super().__init__(message, developer_message)
|
|
78
|
-
self.additional_prompt_content = additional_prompt_content
|
|
79
|
-
self.retry_after_ms = retry_after_ms
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
class ToolSerializationError(ToolRuntimeError):
|
|
83
|
-
"""
|
|
84
|
-
Raised when there is an error executing a tool.
|
|
85
|
-
"""
|
|
86
|
-
|
|
87
|
-
pass
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
class ToolInputError(ToolSerializationError):
|
|
91
|
-
"""
|
|
92
|
-
Raised when there is an error in the input to a tool.
|
|
93
|
-
"""
|
|
94
|
-
|
|
95
|
-
pass
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
class ToolOutputError(ToolSerializationError):
|
|
99
|
-
"""
|
|
100
|
-
Raised when there is an error in the output of a tool.
|
|
101
|
-
"""
|
|
102
|
-
|
|
103
|
-
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|