arcade-core 2.2.0__tar.gz → 2.2.2__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.0 → arcade_core-2.2.2}/PKG-INFO +1 -1
- {arcade_core-2.2.0 → arcade_core-2.2.2}/arcade_core/catalog.py +249 -29
- {arcade_core-2.2.0 → arcade_core-2.2.2}/arcade_core/executor.py +13 -4
- {arcade_core-2.2.0 → arcade_core-2.2.2}/arcade_core/output.py +25 -2
- {arcade_core-2.2.0 → arcade_core-2.2.2}/arcade_core/parse.py +2 -3
- {arcade_core-2.2.0 → arcade_core-2.2.2}/arcade_core/schema.py +35 -22
- {arcade_core-2.2.0 → arcade_core-2.2.2}/arcade_core/toolkit.py +86 -17
- {arcade_core-2.2.0 → arcade_core-2.2.2}/arcade_core/utils.py +2 -2
- {arcade_core-2.2.0 → arcade_core-2.2.2}/pyproject.toml +1 -1
- {arcade_core-2.2.0 → arcade_core-2.2.2}/.gitignore +0 -0
- {arcade_core-2.2.0 → arcade_core-2.2.2}/README.md +0 -0
- {arcade_core-2.2.0 → arcade_core-2.2.2}/arcade_core/__init__.py +0 -0
- {arcade_core-2.2.0 → arcade_core-2.2.2}/arcade_core/annotations.py +0 -0
- {arcade_core-2.2.0 → arcade_core-2.2.2}/arcade_core/auth.py +0 -0
- {arcade_core-2.2.0 → arcade_core-2.2.2}/arcade_core/config.py +0 -0
- {arcade_core-2.2.0 → arcade_core-2.2.2}/arcade_core/config_model.py +0 -0
- {arcade_core-2.2.0 → arcade_core-2.2.2}/arcade_core/errors.py +0 -0
- {arcade_core-2.2.0 → arcade_core-2.2.2}/arcade_core/py.typed +0 -0
- {arcade_core-2.2.0 → arcade_core-2.2.2}/arcade_core/telemetry.py +0 -0
- {arcade_core-2.2.0 → arcade_core-2.2.2}/arcade_core/version.py +0 -0
|
@@ -4,7 +4,7 @@ import logging
|
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
6
6
|
import typing
|
|
7
|
-
from collections.abc import Iterator
|
|
7
|
+
from collections.abc import Callable, Iterator
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
from datetime import datetime
|
|
10
10
|
from enum import Enum
|
|
@@ -13,13 +13,12 @@ from types import ModuleType
|
|
|
13
13
|
from typing import (
|
|
14
14
|
Annotated,
|
|
15
15
|
Any,
|
|
16
|
-
Callable,
|
|
17
16
|
Literal,
|
|
18
|
-
Optional,
|
|
19
17
|
Union,
|
|
20
18
|
cast,
|
|
21
19
|
get_args,
|
|
22
20
|
get_origin,
|
|
21
|
+
get_type_hints,
|
|
23
22
|
)
|
|
24
23
|
|
|
25
24
|
from pydantic import BaseModel, Field, create_model
|
|
@@ -62,6 +61,28 @@ InnerWireType = Literal["string", "integer", "number", "boolean", "json"]
|
|
|
62
61
|
WireType = Union[InnerWireType, Literal["array"]]
|
|
63
62
|
|
|
64
63
|
|
|
64
|
+
def is_typeddict(tp: type) -> bool:
|
|
65
|
+
"""
|
|
66
|
+
Check if a type is a TypedDict.
|
|
67
|
+
Works with both typing.TypedDict and typing_extensions.TypedDict.
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
# TypedDict creates classes that inherit from dict
|
|
71
|
+
if not isinstance(tp, type) or not issubclass(tp, dict):
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
# Check for TypedDict-specific attributes
|
|
75
|
+
return (
|
|
76
|
+
hasattr(tp, "__annotations__")
|
|
77
|
+
and hasattr(tp, "__total__")
|
|
78
|
+
and hasattr(tp, "__required_keys__")
|
|
79
|
+
and hasattr(tp, "__optional_keys__")
|
|
80
|
+
)
|
|
81
|
+
except TypeError:
|
|
82
|
+
# Some special forms raise TypeError when checking issubclass
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
|
|
65
86
|
@dataclass
|
|
66
87
|
class WireTypeInfo:
|
|
67
88
|
"""
|
|
@@ -71,6 +92,9 @@ class WireTypeInfo:
|
|
|
71
92
|
wire_type: WireType
|
|
72
93
|
inner_wire_type: InnerWireType | None = None
|
|
73
94
|
enum_values: list[str] | None = None
|
|
95
|
+
properties: dict[str, "WireTypeInfo"] | None = None
|
|
96
|
+
inner_properties: dict[str, "WireTypeInfo"] | None = None
|
|
97
|
+
description: str | None = None
|
|
74
98
|
|
|
75
99
|
|
|
76
100
|
class ToolMeta(BaseModel):
|
|
@@ -79,9 +103,9 @@ class ToolMeta(BaseModel):
|
|
|
79
103
|
"""
|
|
80
104
|
|
|
81
105
|
module: str
|
|
82
|
-
toolkit:
|
|
83
|
-
package:
|
|
84
|
-
path:
|
|
106
|
+
toolkit: str | None = None
|
|
107
|
+
package: str | None = None
|
|
108
|
+
path: str | None = None
|
|
85
109
|
date_added: datetime = Field(default_factory=datetime.now)
|
|
86
110
|
date_updated: datetime = Field(default_factory=datetime.now)
|
|
87
111
|
|
|
@@ -171,7 +195,7 @@ class ToolCatalog(BaseModel):
|
|
|
171
195
|
def add_tool(
|
|
172
196
|
self,
|
|
173
197
|
tool_func: Callable,
|
|
174
|
-
toolkit_or_name:
|
|
198
|
+
toolkit_or_name: str | Toolkit,
|
|
175
199
|
module: ModuleType | None = None,
|
|
176
200
|
) -> None:
|
|
177
201
|
"""
|
|
@@ -289,7 +313,10 @@ class ToolCatalog(BaseModel):
|
|
|
289
313
|
raise ValueError(f"Tool {func} not found in the catalog.")
|
|
290
314
|
|
|
291
315
|
def get_tool_by_name(
|
|
292
|
-
self,
|
|
316
|
+
self,
|
|
317
|
+
name: str,
|
|
318
|
+
version: str | None = None,
|
|
319
|
+
separator: str = TOOL_NAME_SEPARATOR,
|
|
293
320
|
) -> MaterializedTool:
|
|
294
321
|
"""Get a tool from the catalog by name.
|
|
295
322
|
|
|
@@ -353,8 +380,8 @@ class ToolCatalog(BaseModel):
|
|
|
353
380
|
def create_tool_definition(
|
|
354
381
|
tool: Callable,
|
|
355
382
|
toolkit_name: str,
|
|
356
|
-
toolkit_version:
|
|
357
|
-
toolkit_desc:
|
|
383
|
+
toolkit_version: str | None = None,
|
|
384
|
+
toolkit_desc: str | None = None,
|
|
358
385
|
) -> ToolDefinition:
|
|
359
386
|
"""
|
|
360
387
|
Given a tool function, create a ToolDefinition
|
|
@@ -431,16 +458,13 @@ def create_input_definition(func: Callable) -> ToolInput:
|
|
|
431
458
|
description=tool_field_info.description,
|
|
432
459
|
required=is_required,
|
|
433
460
|
inferrable=tool_field_info.is_inferrable,
|
|
434
|
-
value_schema=
|
|
435
|
-
val_type=tool_field_info.wire_type_info.wire_type,
|
|
436
|
-
inner_val_type=tool_field_info.wire_type_info.inner_wire_type,
|
|
437
|
-
enum=tool_field_info.wire_type_info.enum_values,
|
|
438
|
-
),
|
|
461
|
+
value_schema=wire_type_info_to_value_schema(tool_field_info.wire_type_info),
|
|
439
462
|
)
|
|
440
463
|
)
|
|
441
464
|
|
|
442
465
|
return ToolInput(
|
|
443
|
-
parameters=input_parameters,
|
|
466
|
+
parameters=input_parameters,
|
|
467
|
+
tool_context_parameter_name=tool_context_param_name,
|
|
444
468
|
)
|
|
445
469
|
|
|
446
470
|
|
|
@@ -478,11 +502,7 @@ def create_output_definition(func: Callable) -> ToolOutput:
|
|
|
478
502
|
return ToolOutput(
|
|
479
503
|
description=description,
|
|
480
504
|
available_modes=available_modes,
|
|
481
|
-
value_schema=
|
|
482
|
-
val_type=wire_type_info.wire_type,
|
|
483
|
-
inner_val_type=wire_type_info.inner_wire_type,
|
|
484
|
-
enum=wire_type_info.enum_values,
|
|
485
|
-
),
|
|
505
|
+
value_schema=wire_type_info_to_value_schema(wire_type_info),
|
|
486
506
|
)
|
|
487
507
|
|
|
488
508
|
|
|
@@ -669,12 +689,21 @@ def get_wire_type_info(_type: type) -> WireTypeInfo:
|
|
|
669
689
|
# Is this a list type?
|
|
670
690
|
# If so, get the inner (enclosed) type
|
|
671
691
|
is_list = get_origin(_type) is list
|
|
692
|
+
inner_properties = None
|
|
693
|
+
|
|
672
694
|
if is_list:
|
|
673
695
|
inner_type = get_args(_type)[0]
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
)
|
|
696
|
+
|
|
697
|
+
# Recursively get wire type info for inner type
|
|
698
|
+
inner_info = get_wire_type_info(inner_type)
|
|
699
|
+
inner_wire_type = cast(InnerWireType, inner_info.wire_type)
|
|
700
|
+
|
|
701
|
+
# If inner type has properties (it's a complex object), propagate them
|
|
702
|
+
if inner_info.properties:
|
|
703
|
+
inner_properties = inner_info.properties
|
|
704
|
+
# If inner type is array (nested arrays), propagate inner_properties
|
|
705
|
+
elif inner_info.inner_properties:
|
|
706
|
+
inner_properties = inner_info.inner_properties
|
|
678
707
|
else:
|
|
679
708
|
inner_wire_type = None
|
|
680
709
|
|
|
@@ -696,11 +725,133 @@ def get_wire_type_info(_type: type) -> WireTypeInfo:
|
|
|
696
725
|
enum_values = [str(e) for e in get_args(type_to_check)]
|
|
697
726
|
|
|
698
727
|
# Special case: Enum can be enumerated on the wire
|
|
699
|
-
elif issubclass(actual_type, Enum):
|
|
728
|
+
elif isinstance(actual_type, type) and issubclass(actual_type, Enum):
|
|
700
729
|
is_enum = True
|
|
701
|
-
enum_values = [e.value for e in actual_type]
|
|
730
|
+
enum_values = [e.value for e in actual_type]
|
|
731
|
+
|
|
732
|
+
# Extract properties for complex types
|
|
733
|
+
properties = None
|
|
734
|
+
if wire_type == "json" and not is_list:
|
|
735
|
+
properties = extract_properties(type_to_check)
|
|
736
|
+
|
|
737
|
+
return WireTypeInfo(
|
|
738
|
+
wire_type,
|
|
739
|
+
inner_wire_type,
|
|
740
|
+
enum_values if is_enum else None,
|
|
741
|
+
properties,
|
|
742
|
+
inner_properties,
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _extract_typeddict_field_descriptions(typeddict_class: type) -> dict[str, str]:
|
|
747
|
+
"""
|
|
748
|
+
Extract field descriptions from TypedDict docstrings.
|
|
749
|
+
|
|
750
|
+
TypedDict classes typically have field descriptions as docstrings after each field.
|
|
751
|
+
This function attempts to parse the source code to extract these descriptions.
|
|
752
|
+
"""
|
|
753
|
+
descriptions = {}
|
|
754
|
+
|
|
755
|
+
try:
|
|
756
|
+
source = inspect.getsource(typeddict_class)
|
|
757
|
+
# Simple regex to match field: type pattern followed by a docstring
|
|
758
|
+
# This is a simplified approach - a full AST parser would be more robust
|
|
759
|
+
import re
|
|
760
|
+
|
|
761
|
+
# Pattern to match field definition followed by docstring
|
|
762
|
+
pattern = r'(\w+):\s*[^"\n]+\n\s*"""([^"]+)"""'
|
|
763
|
+
matches = re.findall(pattern, source)
|
|
764
|
+
|
|
765
|
+
for field_name, description in matches:
|
|
766
|
+
descriptions[field_name] = description.strip()
|
|
767
|
+
|
|
768
|
+
except (OSError, TypeError):
|
|
769
|
+
# If we can't get the source, return empty descriptions
|
|
770
|
+
pass
|
|
771
|
+
|
|
772
|
+
return descriptions
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def extract_properties(type_to_check: type) -> dict[str, WireTypeInfo] | None:
|
|
776
|
+
"""
|
|
777
|
+
Extract properties from TypedDict, Pydantic models, or other structured types.
|
|
778
|
+
"""
|
|
779
|
+
properties = {}
|
|
780
|
+
|
|
781
|
+
# Handle Pydantic BaseModel
|
|
782
|
+
if isinstance(type_to_check, type) and issubclass(type_to_check, BaseModel):
|
|
783
|
+
for field_name, field_info in type_to_check.model_fields.items():
|
|
784
|
+
# Get the field type
|
|
785
|
+
field_type = field_info.annotation
|
|
786
|
+
if field_type is None:
|
|
787
|
+
continue
|
|
788
|
+
|
|
789
|
+
# Handle Optional types (Union[T, None])
|
|
790
|
+
if is_strict_optional(field_type):
|
|
791
|
+
# Extract the non-None type from Optional
|
|
792
|
+
field_type = next(arg for arg in get_args(field_type) if arg is not type(None))
|
|
793
|
+
|
|
794
|
+
# Get wire type info recursively
|
|
795
|
+
wire_info = get_wire_type_info(field_type)
|
|
796
|
+
properties[field_name] = wire_info
|
|
797
|
+
|
|
798
|
+
# Handle TypedDict
|
|
799
|
+
elif is_typeddict(type_to_check):
|
|
800
|
+
# Get type hints for the TypedDict
|
|
801
|
+
type_hints = get_type_hints(type_to_check, include_extras=True)
|
|
702
802
|
|
|
703
|
-
|
|
803
|
+
# Try to extract field descriptions from the class source
|
|
804
|
+
field_descriptions = _extract_typeddict_field_descriptions(type_to_check)
|
|
805
|
+
|
|
806
|
+
for field_name, field_type in type_hints.items():
|
|
807
|
+
# Handle Optional types (Union[T, None])
|
|
808
|
+
if is_strict_optional(field_type):
|
|
809
|
+
# Extract the non-None type from Optional
|
|
810
|
+
field_type = next(arg for arg in get_args(field_type) if arg is not type(None))
|
|
811
|
+
wire_info = get_wire_type_info(field_type)
|
|
812
|
+
|
|
813
|
+
# Add description if available
|
|
814
|
+
if field_name in field_descriptions:
|
|
815
|
+
wire_info.description = field_descriptions[field_name]
|
|
816
|
+
|
|
817
|
+
properties[field_name] = wire_info
|
|
818
|
+
|
|
819
|
+
# Handle regular dict with type annotations (e.g., dict[str, Any])
|
|
820
|
+
elif get_origin(type_to_check) is dict:
|
|
821
|
+
# For generic dicts, we can't extract specific properties
|
|
822
|
+
return None
|
|
823
|
+
|
|
824
|
+
return properties if properties else None
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def wire_type_info_to_value_schema(wire_info: WireTypeInfo) -> ValueSchema:
|
|
828
|
+
"""
|
|
829
|
+
Convert WireTypeInfo to ValueSchema, including nested properties.
|
|
830
|
+
"""
|
|
831
|
+
# Convert nested properties if they exist
|
|
832
|
+
properties = None
|
|
833
|
+
if wire_info.properties:
|
|
834
|
+
properties = {
|
|
835
|
+
name: wire_type_info_to_value_schema(nested_info)
|
|
836
|
+
for name, nested_info in wire_info.properties.items()
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
# Convert inner properties for array items
|
|
840
|
+
inner_properties = None
|
|
841
|
+
if wire_info.inner_properties:
|
|
842
|
+
inner_properties = {
|
|
843
|
+
name: wire_type_info_to_value_schema(nested_info)
|
|
844
|
+
for name, nested_info in wire_info.inner_properties.items()
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return ValueSchema(
|
|
848
|
+
val_type=wire_info.wire_type,
|
|
849
|
+
inner_val_type=wire_info.inner_wire_type,
|
|
850
|
+
enum=wire_info.enum_values,
|
|
851
|
+
properties=properties,
|
|
852
|
+
inner_properties=inner_properties,
|
|
853
|
+
description=wire_info.description,
|
|
854
|
+
)
|
|
704
855
|
|
|
705
856
|
|
|
706
857
|
def extract_python_param_info(param: inspect.Parameter) -> ParamInfo:
|
|
@@ -799,6 +950,9 @@ def get_wire_type(
|
|
|
799
950
|
if isinstance(_type, type) and issubclass(_type, BaseModel):
|
|
800
951
|
return "json"
|
|
801
952
|
|
|
953
|
+
if is_typeddict(_type):
|
|
954
|
+
return "json"
|
|
955
|
+
|
|
802
956
|
raise ToolDefinitionError(f"Unsupported parameter type: {_type}")
|
|
803
957
|
|
|
804
958
|
|
|
@@ -831,7 +985,7 @@ def create_func_models(func: Callable) -> tuple[type[BaseModel], type[BaseModel]
|
|
|
831
985
|
return input_model, output_model
|
|
832
986
|
|
|
833
987
|
|
|
834
|
-
def determine_output_model(func: Callable) -> type[BaseModel]:
|
|
988
|
+
def determine_output_model(func: Callable) -> type[BaseModel]: # noqa: C901
|
|
835
989
|
"""
|
|
836
990
|
Determine the output model for a function based on its return annotation.
|
|
837
991
|
"""
|
|
@@ -845,6 +999,18 @@ def determine_output_model(func: Callable) -> type[BaseModel]:
|
|
|
845
999
|
description = (
|
|
846
1000
|
return_annotation.__metadata__[0] if return_annotation.__metadata__ else ""
|
|
847
1001
|
)
|
|
1002
|
+
|
|
1003
|
+
# Check if the field type is a TypedDict
|
|
1004
|
+
if is_typeddict(field_type):
|
|
1005
|
+
# Create a Pydantic model from TypedDict
|
|
1006
|
+
typeddict_model = create_model_from_typeddict(
|
|
1007
|
+
field_type, f"{output_model_name}TypedDict"
|
|
1008
|
+
)
|
|
1009
|
+
return create_model(
|
|
1010
|
+
output_model_name,
|
|
1011
|
+
result=(typeddict_model, Field(description=str(description))),
|
|
1012
|
+
)
|
|
1013
|
+
|
|
848
1014
|
if description:
|
|
849
1015
|
return create_model(
|
|
850
1016
|
output_model_name,
|
|
@@ -857,6 +1023,18 @@ def determine_output_model(func: Callable) -> type[BaseModel]:
|
|
|
857
1023
|
# TODO handle multiple non-None arguments. Raise error?
|
|
858
1024
|
for arg in get_args(return_annotation):
|
|
859
1025
|
if arg is not type(None):
|
|
1026
|
+
# Check if the arg is a TypedDict
|
|
1027
|
+
if is_typeddict(arg):
|
|
1028
|
+
typeddict_model = create_model_from_typeddict(
|
|
1029
|
+
arg, f"{output_model_name}TypedDict"
|
|
1030
|
+
)
|
|
1031
|
+
return create_model(
|
|
1032
|
+
output_model_name,
|
|
1033
|
+
result=(
|
|
1034
|
+
typeddict_model,
|
|
1035
|
+
Field(description="No description provided."),
|
|
1036
|
+
),
|
|
1037
|
+
)
|
|
860
1038
|
return create_model(
|
|
861
1039
|
output_model_name,
|
|
862
1040
|
result=(arg, Field(description="No description provided.")),
|
|
@@ -871,6 +1049,17 @@ def determine_output_model(func: Callable) -> type[BaseModel]:
|
|
|
871
1049
|
),
|
|
872
1050
|
)
|
|
873
1051
|
else:
|
|
1052
|
+
# Check if return type is TypedDict
|
|
1053
|
+
if is_typeddict(return_annotation):
|
|
1054
|
+
typeddict_model = create_model_from_typeddict(return_annotation, output_model_name)
|
|
1055
|
+
return create_model(
|
|
1056
|
+
output_model_name,
|
|
1057
|
+
result=(
|
|
1058
|
+
typeddict_model,
|
|
1059
|
+
Field(description="No description provided."),
|
|
1060
|
+
),
|
|
1061
|
+
)
|
|
1062
|
+
|
|
874
1063
|
# Handle simple return types (like str)
|
|
875
1064
|
return create_model(
|
|
876
1065
|
output_model_name,
|
|
@@ -878,6 +1067,37 @@ def determine_output_model(func: Callable) -> type[BaseModel]:
|
|
|
878
1067
|
)
|
|
879
1068
|
|
|
880
1069
|
|
|
1070
|
+
def create_model_from_typeddict(typeddict_class: type, model_name: str) -> type[BaseModel]:
|
|
1071
|
+
"""
|
|
1072
|
+
Create a Pydantic model from a TypedDict class.
|
|
1073
|
+
This enables runtime validation of TypedDict structures.
|
|
1074
|
+
"""
|
|
1075
|
+
# Get type hints for the TypedDict
|
|
1076
|
+
type_hints = get_type_hints(typeddict_class, include_extras=True)
|
|
1077
|
+
|
|
1078
|
+
# Build field definitions for the Pydantic model
|
|
1079
|
+
field_definitions: dict[str, Any] = {}
|
|
1080
|
+
for field_name, field_type in type_hints.items():
|
|
1081
|
+
# Check if field is required
|
|
1082
|
+
is_required = field_name in getattr(typeddict_class, "__required_keys__", set())
|
|
1083
|
+
|
|
1084
|
+
# Handle nested TypedDict
|
|
1085
|
+
if is_typeddict(field_type):
|
|
1086
|
+
nested_model = create_model_from_typeddict(field_type, f"{model_name}_{field_name}")
|
|
1087
|
+
if is_required:
|
|
1088
|
+
field_definitions[field_name] = (nested_model, Field())
|
|
1089
|
+
else:
|
|
1090
|
+
field_definitions[field_name] = (nested_model, Field(default=None))
|
|
1091
|
+
else:
|
|
1092
|
+
if is_required:
|
|
1093
|
+
field_definitions[field_name] = (field_type, Field())
|
|
1094
|
+
else:
|
|
1095
|
+
field_definitions[field_name] = (field_type, Field(default=None))
|
|
1096
|
+
|
|
1097
|
+
# Create and return the Pydantic model
|
|
1098
|
+
return create_model(model_name, **field_definitions)
|
|
1099
|
+
|
|
1100
|
+
|
|
881
1101
|
def to_tool_secret_requirements(
|
|
882
1102
|
secrets_requirement: list[str],
|
|
883
1103
|
) -> list[ToolSecretRequirement]:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import traceback
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
from pydantic import BaseModel, ValidationError
|
|
6
7
|
|
|
@@ -12,7 +13,12 @@ from arcade_core.errors import (
|
|
|
12
13
|
ToolSerializationError,
|
|
13
14
|
)
|
|
14
15
|
from arcade_core.output import output_factory
|
|
15
|
-
from arcade_core.schema import
|
|
16
|
+
from arcade_core.schema import (
|
|
17
|
+
ToolCallLog,
|
|
18
|
+
ToolCallOutput,
|
|
19
|
+
ToolContext,
|
|
20
|
+
ToolDefinition,
|
|
21
|
+
)
|
|
16
22
|
|
|
17
23
|
|
|
18
24
|
class ToolExecutor:
|
|
@@ -34,7 +40,9 @@ class ToolExecutor:
|
|
|
34
40
|
if definition.deprecation_message is not None:
|
|
35
41
|
tool_call_logs.append(
|
|
36
42
|
ToolCallLog(
|
|
37
|
-
message=definition.deprecation_message,
|
|
43
|
+
message=definition.deprecation_message,
|
|
44
|
+
level="warning",
|
|
45
|
+
subtype="deprecation",
|
|
38
46
|
)
|
|
39
47
|
)
|
|
40
48
|
|
|
@@ -101,7 +109,8 @@ class ToolExecutor:
|
|
|
101
109
|
|
|
102
110
|
except ValidationError as e:
|
|
103
111
|
raise ToolInputError(
|
|
104
|
-
message="Error in tool input deserialization",
|
|
112
|
+
message="Error in tool input deserialization",
|
|
113
|
+
developer_message=str(e),
|
|
105
114
|
) from e
|
|
106
115
|
|
|
107
116
|
return inputs
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from typing import TypeVar
|
|
2
2
|
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
3
5
|
from arcade_core.schema import ToolCallError, ToolCallLog, ToolCallOutput
|
|
4
6
|
from arcade_core.utils import coerce_empty_list_to_none
|
|
5
7
|
|
|
@@ -17,9 +19,29 @@ class ToolOutputFactory:
|
|
|
17
19
|
data: T | None = None,
|
|
18
20
|
logs: list[ToolCallLog] | None = None,
|
|
19
21
|
) -> ToolCallOutput:
|
|
20
|
-
|
|
22
|
+
# Extract the result value
|
|
23
|
+
"""
|
|
24
|
+
Extracts the result value for the tool output.
|
|
25
|
+
|
|
26
|
+
The executor guarantees that `data` is either a string, a dict, or None.
|
|
27
|
+
"""
|
|
28
|
+
value: str | int | float | bool | dict | list[str] | None
|
|
29
|
+
if data is None:
|
|
30
|
+
value = ""
|
|
31
|
+
elif hasattr(data, "result"):
|
|
32
|
+
value = getattr(data, "result", "")
|
|
33
|
+
elif isinstance(data, BaseModel):
|
|
34
|
+
value = data.model_dump()
|
|
35
|
+
elif isinstance(data, (str, int, float, bool, list)):
|
|
36
|
+
value = data
|
|
37
|
+
else:
|
|
38
|
+
raise ValueError(f"Unsupported data output type: {type(data)}")
|
|
39
|
+
|
|
21
40
|
logs = coerce_empty_list_to_none(logs)
|
|
22
|
-
return ToolCallOutput(
|
|
41
|
+
return ToolCallOutput(
|
|
42
|
+
value=value,
|
|
43
|
+
logs=logs,
|
|
44
|
+
)
|
|
23
45
|
|
|
24
46
|
def fail(
|
|
25
47
|
self,
|
|
@@ -56,6 +78,7 @@ class ToolOutputFactory:
|
|
|
56
78
|
can_retry=True,
|
|
57
79
|
additional_prompt_content=additional_prompt_content,
|
|
58
80
|
retry_after_ms=retry_after_ms,
|
|
81
|
+
traceback_info=traceback_info,
|
|
59
82
|
),
|
|
60
83
|
logs=coerce_empty_list_to_none(logs),
|
|
61
84
|
)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import ast
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Optional, Union
|
|
4
3
|
|
|
5
4
|
|
|
6
5
|
def load_ast_tree(filepath: str | Path) -> ast.AST:
|
|
@@ -16,8 +15,8 @@ def load_ast_tree(filepath: str | Path) -> ast.AST:
|
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
def get_function_name_if_decorated(
|
|
19
|
-
node:
|
|
20
|
-
) ->
|
|
18
|
+
node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
19
|
+
) -> str | None:
|
|
21
20
|
"""
|
|
22
21
|
Check if a function has a decorator.
|
|
23
22
|
"""
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from enum import Enum
|
|
4
|
-
from typing import Any, Literal
|
|
4
|
+
from typing import Any, Literal
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
@@ -15,12 +15,21 @@ class ValueSchema(BaseModel):
|
|
|
15
15
|
val_type: Literal["string", "integer", "number", "boolean", "json", "array"]
|
|
16
16
|
"""The type of the value."""
|
|
17
17
|
|
|
18
|
-
inner_val_type:
|
|
18
|
+
inner_val_type: Literal["string", "integer", "number", "boolean", "json"] | None = None
|
|
19
19
|
"""The type of the inner value, if the value is a list."""
|
|
20
20
|
|
|
21
|
-
enum:
|
|
21
|
+
enum: list[str] | None = None
|
|
22
22
|
"""The list of possible values for the value, if it is a closed list."""
|
|
23
23
|
|
|
24
|
+
properties: dict[str, "ValueSchema"] | None = None
|
|
25
|
+
"""For object types (json), the schema of nested properties."""
|
|
26
|
+
|
|
27
|
+
inner_properties: dict[str, "ValueSchema"] | None = None
|
|
28
|
+
"""For array types with json items, the schema of properties for each array item."""
|
|
29
|
+
|
|
30
|
+
description: str | None = None
|
|
31
|
+
"""Optional description of the value."""
|
|
32
|
+
|
|
24
33
|
|
|
25
34
|
class InputParameter(BaseModel):
|
|
26
35
|
"""A parameter that can be passed to a tool."""
|
|
@@ -30,8 +39,9 @@ class InputParameter(BaseModel):
|
|
|
30
39
|
...,
|
|
31
40
|
description="Whether this parameter is required (true) or optional (false).",
|
|
32
41
|
)
|
|
33
|
-
description:
|
|
34
|
-
None,
|
|
42
|
+
description: str | None = Field(
|
|
43
|
+
None,
|
|
44
|
+
description="A descriptive, human-readable explanation of the parameter.",
|
|
35
45
|
)
|
|
36
46
|
value_schema: ValueSchema = Field(
|
|
37
47
|
...,
|
|
@@ -59,14 +69,14 @@ class ToolInput(BaseModel):
|
|
|
59
69
|
class ToolOutput(BaseModel):
|
|
60
70
|
"""The output of a tool."""
|
|
61
71
|
|
|
62
|
-
description:
|
|
72
|
+
description: str | None = Field(
|
|
63
73
|
None, description="A descriptive, human-readable explanation of the output."
|
|
64
74
|
)
|
|
65
75
|
available_modes: list[str] = Field(
|
|
66
76
|
default_factory=lambda: ["value", "error", "null"],
|
|
67
77
|
description="The available modes for the output.",
|
|
68
78
|
)
|
|
69
|
-
value_schema:
|
|
79
|
+
value_schema: ValueSchema | None = Field(
|
|
70
80
|
None, description="The schema of the value of the output."
|
|
71
81
|
)
|
|
72
82
|
|
|
@@ -74,7 +84,7 @@ class ToolOutput(BaseModel):
|
|
|
74
84
|
class OAuth2Requirement(BaseModel):
|
|
75
85
|
"""Indicates that the tool requires OAuth 2.0 authorization."""
|
|
76
86
|
|
|
77
|
-
scopes:
|
|
87
|
+
scopes: list[str] | None = None
|
|
78
88
|
"""The scope(s) needed for the authorized action."""
|
|
79
89
|
|
|
80
90
|
|
|
@@ -90,16 +100,16 @@ class ToolAuthRequirement(BaseModel):
|
|
|
90
100
|
#
|
|
91
101
|
# The Arcade SDK translates these into the appropriate provider ID (Google) and type (OAuth2).
|
|
92
102
|
# The only time the developer will set these is if they are using a custom auth provider.
|
|
93
|
-
provider_id:
|
|
103
|
+
provider_id: str | None = None
|
|
94
104
|
"""The provider ID configured in Arcade that acts as an alias to well-known configuration."""
|
|
95
105
|
|
|
96
106
|
provider_type: str
|
|
97
107
|
"""The type of the authorization provider."""
|
|
98
108
|
|
|
99
|
-
id:
|
|
109
|
+
id: str | None = None
|
|
100
110
|
"""A provider's unique identifier, allowing the tool to specify a specific authorization provider. Recommended for private tools only."""
|
|
101
111
|
|
|
102
|
-
oauth2:
|
|
112
|
+
oauth2: OAuth2Requirement | None = None
|
|
103
113
|
"""The OAuth 2.0 requirement, if any."""
|
|
104
114
|
|
|
105
115
|
|
|
@@ -133,13 +143,13 @@ class ToolMetadataRequirement(BaseModel):
|
|
|
133
143
|
class ToolRequirements(BaseModel):
|
|
134
144
|
"""The requirements for a tool to run."""
|
|
135
145
|
|
|
136
|
-
authorization:
|
|
146
|
+
authorization: ToolAuthRequirement | None = None
|
|
137
147
|
"""The authorization requirements for the tool, if any."""
|
|
138
148
|
|
|
139
|
-
secrets:
|
|
149
|
+
secrets: list[ToolSecretRequirement] | None = None
|
|
140
150
|
"""The secret requirements for the tool, if any."""
|
|
141
151
|
|
|
142
|
-
metadata:
|
|
152
|
+
metadata: list[ToolMetadataRequirement] | None = None
|
|
143
153
|
"""The metadata requirements for the tool, if any."""
|
|
144
154
|
|
|
145
155
|
|
|
@@ -149,10 +159,10 @@ class ToolkitDefinition(BaseModel):
|
|
|
149
159
|
name: str
|
|
150
160
|
"""The name of the toolkit."""
|
|
151
161
|
|
|
152
|
-
description:
|
|
162
|
+
description: str | None = None
|
|
153
163
|
"""The description of the toolkit."""
|
|
154
164
|
|
|
155
|
-
version:
|
|
165
|
+
version: str | None = None
|
|
156
166
|
"""The version identifier of the toolkit."""
|
|
157
167
|
|
|
158
168
|
|
|
@@ -166,7 +176,7 @@ class FullyQualifiedName:
|
|
|
166
176
|
toolkit_name: str
|
|
167
177
|
"""The name of the toolkit containing the tool."""
|
|
168
178
|
|
|
169
|
-
toolkit_version:
|
|
179
|
+
toolkit_version: str | None = None
|
|
170
180
|
"""The version of the toolkit containing the tool."""
|
|
171
181
|
|
|
172
182
|
def __str__(self) -> str:
|
|
@@ -225,7 +235,7 @@ class ToolDefinition(BaseModel):
|
|
|
225
235
|
requirements: ToolRequirements
|
|
226
236
|
"""The requirements (e.g. authorization) for the tool to run."""
|
|
227
237
|
|
|
228
|
-
deprecation_message:
|
|
238
|
+
deprecation_message: str | None = None
|
|
229
239
|
"""The message to display when the tool is deprecated."""
|
|
230
240
|
|
|
231
241
|
def get_fully_qualified_name(self) -> FullyQualifiedName:
|
|
@@ -241,7 +251,7 @@ class ToolReference(BaseModel):
|
|
|
241
251
|
toolkit: str
|
|
242
252
|
"""The name of the toolkit containing the tool."""
|
|
243
253
|
|
|
244
|
-
version:
|
|
254
|
+
version: str | None = None
|
|
245
255
|
"""The version of the toolkit containing the tool."""
|
|
246
256
|
|
|
247
257
|
def get_fully_qualified_name(self) -> FullyQualifiedName:
|
|
@@ -313,7 +323,10 @@ class ToolContext(BaseModel):
|
|
|
313
323
|
return self._get_item(key, self.metadata, "metadata")
|
|
314
324
|
|
|
315
325
|
def _get_item(
|
|
316
|
-
self,
|
|
326
|
+
self,
|
|
327
|
+
key: str,
|
|
328
|
+
items: list[ToolMetadataItem] | list[ToolSecretItem] | None,
|
|
329
|
+
item_name: str,
|
|
317
330
|
) -> str:
|
|
318
331
|
if not key or not key.strip():
|
|
319
332
|
raise ValueError(
|
|
@@ -368,7 +381,7 @@ class ToolCallLog(BaseModel):
|
|
|
368
381
|
]
|
|
369
382
|
"""The level of severity for the log."""
|
|
370
383
|
|
|
371
|
-
subtype:
|
|
384
|
+
subtype: Literal["deprecation"] | None = None
|
|
372
385
|
"""Optional field for further categorization of the log."""
|
|
373
386
|
|
|
374
387
|
|
|
@@ -405,7 +418,7 @@ class ToolCallRequiresAuthorization(BaseModel):
|
|
|
405
418
|
class ToolCallOutput(BaseModel):
|
|
406
419
|
"""The output of a tool invocation."""
|
|
407
420
|
|
|
408
|
-
value:
|
|
421
|
+
value: str | int | float | bool | dict | list[str] | None = None
|
|
409
422
|
"""The value returned by the tool."""
|
|
410
423
|
logs: list[ToolCallLog] | None = None
|
|
411
424
|
"""The logs that occurred during the tool invocation."""
|
|
@@ -4,7 +4,7 @@ import logging
|
|
|
4
4
|
import os
|
|
5
5
|
import types
|
|
6
6
|
from collections import defaultdict
|
|
7
|
-
from pathlib import Path
|
|
7
|
+
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
8
8
|
|
|
9
9
|
from pydantic import BaseModel, ConfigDict, field_validator
|
|
10
10
|
|
|
@@ -87,14 +87,6 @@ class Toolkit(BaseModel):
|
|
|
87
87
|
except (ImportError, AttributeError) as e:
|
|
88
88
|
raise ToolkitLoadError(f"Failed to locate package directory for '{package}'.") from e
|
|
89
89
|
|
|
90
|
-
# Get all python files in the package directory
|
|
91
|
-
try:
|
|
92
|
-
modules = [f for f in package_dir.glob("**/*.py") if f.is_file()]
|
|
93
|
-
except OSError as e:
|
|
94
|
-
raise ToolkitLoadError(
|
|
95
|
-
f"Failed to locate Python files in package directory for '{package}'."
|
|
96
|
-
) from e
|
|
97
|
-
|
|
98
90
|
toolkit = cls(
|
|
99
91
|
name=name,
|
|
100
92
|
package_name=package_name,
|
|
@@ -105,14 +97,7 @@ class Toolkit(BaseModel):
|
|
|
105
97
|
repository=repo,
|
|
106
98
|
)
|
|
107
99
|
|
|
108
|
-
|
|
109
|
-
relative_path = module_path.relative_to(package_dir)
|
|
110
|
-
import_path = ".".join(relative_path.with_suffix("").parts)
|
|
111
|
-
import_path = f"{package_name}.{import_path}"
|
|
112
|
-
toolkit.tools[import_path] = get_tools_from_file(str(module_path))
|
|
113
|
-
|
|
114
|
-
if not toolkit.tools:
|
|
115
|
-
raise ToolkitLoadError(f"No tools found in package {package}")
|
|
100
|
+
toolkit.tools = cls.tools_from_directory(package_dir, package_name)
|
|
116
101
|
|
|
117
102
|
return toolkit
|
|
118
103
|
|
|
@@ -229,6 +214,61 @@ class Toolkit(BaseModel):
|
|
|
229
214
|
|
|
230
215
|
return all_toolkits
|
|
231
216
|
|
|
217
|
+
@classmethod
|
|
218
|
+
def tools_from_directory(cls, package_dir: Path, package_name: str) -> dict[str, list[str]]:
|
|
219
|
+
"""
|
|
220
|
+
Load a Toolkit from a directory.
|
|
221
|
+
"""
|
|
222
|
+
# Get all python files in the package directory
|
|
223
|
+
try:
|
|
224
|
+
modules = [f for f in package_dir.glob("**/*.py") if f.is_file() and Validate.path(f)]
|
|
225
|
+
except OSError as e:
|
|
226
|
+
raise ToolkitLoadError(
|
|
227
|
+
f"Failed to locate Python files in package directory for '{package_name}'."
|
|
228
|
+
) from e
|
|
229
|
+
|
|
230
|
+
tools: dict[str, list[str]] = {}
|
|
231
|
+
|
|
232
|
+
for module_path in modules:
|
|
233
|
+
relative_path = module_path.relative_to(package_dir)
|
|
234
|
+
cls.validate_file(module_path)
|
|
235
|
+
import_path = ".".join(relative_path.with_suffix("").parts)
|
|
236
|
+
import_path = f"{package_name}.{import_path}"
|
|
237
|
+
tools[import_path] = get_tools_from_file(str(module_path))
|
|
238
|
+
|
|
239
|
+
if not tools:
|
|
240
|
+
raise ToolkitLoadError(f"No tools found in package {package_name}")
|
|
241
|
+
|
|
242
|
+
return tools
|
|
243
|
+
|
|
244
|
+
@classmethod
|
|
245
|
+
def validate_file(cls, file_path: str | Path) -> None:
|
|
246
|
+
"""
|
|
247
|
+
Validate that the Python code in the given file is syntactically correct.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
file_path: Path to the Python file to validate
|
|
251
|
+
"""
|
|
252
|
+
# Convert string path to Path object if needed
|
|
253
|
+
path = Path(file_path) if isinstance(file_path, str) else file_path
|
|
254
|
+
|
|
255
|
+
# Check if file exists
|
|
256
|
+
if not path.exists():
|
|
257
|
+
raise ValueError(f"❌ File not found: {path}")
|
|
258
|
+
|
|
259
|
+
# Check if it's a Python file
|
|
260
|
+
if not path.suffix == ".py":
|
|
261
|
+
raise ValueError(f"❌ Not a Python file: {path}")
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
# Try to compile the code to check for syntax errors
|
|
265
|
+
with open(path, encoding="utf-8") as f:
|
|
266
|
+
source = f.read()
|
|
267
|
+
|
|
268
|
+
compile(source, str(path), "exec")
|
|
269
|
+
except Exception as e:
|
|
270
|
+
raise SyntaxError(f"{path}: {e}")
|
|
271
|
+
|
|
232
272
|
|
|
233
273
|
def get_package_directory(package_name: str) -> str:
|
|
234
274
|
"""
|
|
@@ -247,3 +287,32 @@ def get_package_directory(package_name: str) -> str:
|
|
|
247
287
|
return spec.submodule_search_locations[0]
|
|
248
288
|
else:
|
|
249
289
|
raise ImportError(f"Package {package_name} does not have a file path associated with it")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class Validate:
|
|
293
|
+
warn = True
|
|
294
|
+
|
|
295
|
+
@classmethod
|
|
296
|
+
def path(cls, path: str | Path) -> bool:
|
|
297
|
+
"""
|
|
298
|
+
Validate if a path is valid to be served or deployed.
|
|
299
|
+
"""
|
|
300
|
+
# Check both POSIX and Windows interpretations
|
|
301
|
+
posix_path = PurePosixPath(path)
|
|
302
|
+
windows_path = PureWindowsPath(path)
|
|
303
|
+
|
|
304
|
+
# Get all possible parts from both interpretations
|
|
305
|
+
all_parts = set(posix_path.parts) | set(windows_path.parts)
|
|
306
|
+
|
|
307
|
+
for part in all_parts:
|
|
308
|
+
if (part == "venv" or part.startswith(".")) and cls.warn:
|
|
309
|
+
print(
|
|
310
|
+
f"⚠️ Your package may contain a venv directory or hidden files. We suggest moving these out of the toolkit directory to avoid deployment issues: {path}"
|
|
311
|
+
)
|
|
312
|
+
cls.warn = False
|
|
313
|
+
if part in {"dist", "build", "__pycache__", "coverage.xml"}:
|
|
314
|
+
return False
|
|
315
|
+
if part.endswith(".lock"):
|
|
316
|
+
return False
|
|
317
|
+
|
|
318
|
+
return True
|
|
@@ -3,9 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
import ast
|
|
4
4
|
import inspect
|
|
5
5
|
import re
|
|
6
|
-
from collections.abc import Iterable
|
|
6
|
+
from collections.abc import Callable, Iterable
|
|
7
7
|
from types import UnionType
|
|
8
|
-
from typing import Any,
|
|
8
|
+
from typing import Any, Literal, TypeVar, Union, get_args, get_origin
|
|
9
9
|
|
|
10
10
|
T = TypeVar("T")
|
|
11
11
|
|
|
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
|