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/catalog.py
CHANGED
|
@@ -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,13 @@ 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(
|
|
408
|
+
raise ToolDefinitionError(
|
|
409
|
+
f"Tool '{raw_tool_name}' is missing a description. Tool descriptions are specified as docstrings for the tool function."
|
|
410
|
+
)
|
|
396
411
|
|
|
397
412
|
# If the function returns a value, it must have a type annotation
|
|
398
413
|
if does_function_return_value(tool) and tool.__annotations__.get("return") is None:
|
|
399
|
-
raise
|
|
414
|
+
raise ToolOutputSchemaError(f"Tool '{raw_tool_name}' must have a return type")
|
|
400
415
|
|
|
401
416
|
auth_requirement = create_auth_requirement(tool)
|
|
402
417
|
secrets_requirement = create_secrets_requirement(tool)
|
|
@@ -436,9 +451,11 @@ def create_input_definition(func: Callable) -> ToolInput:
|
|
|
436
451
|
tool_context_param_name: str | None = None
|
|
437
452
|
|
|
438
453
|
for _, param in inspect.signature(func, follow_wrapped=True).parameters.items():
|
|
439
|
-
|
|
454
|
+
ann = param.annotation
|
|
455
|
+
if isinstance(ann, type) and issubclass(ann, ToolContext):
|
|
456
|
+
# Soft guidance for developers using legacy ToolContext
|
|
440
457
|
if tool_context_param_name is not None:
|
|
441
|
-
raise
|
|
458
|
+
raise ToolInputSchemaError(
|
|
442
459
|
f"Only one ToolContext parameter is supported, but tool {func.__name__} has multiple."
|
|
443
460
|
)
|
|
444
461
|
|
|
@@ -483,7 +500,11 @@ def create_output_definition(func: Callable) -> ToolOutput:
|
|
|
483
500
|
)
|
|
484
501
|
|
|
485
502
|
if hasattr(return_type, "__metadata__"):
|
|
486
|
-
description =
|
|
503
|
+
description = (
|
|
504
|
+
return_type.__metadata__[0]
|
|
505
|
+
if return_type.__metadata__
|
|
506
|
+
else "No description provided for return type."
|
|
507
|
+
)
|
|
487
508
|
return_type = return_type.__origin__
|
|
488
509
|
|
|
489
510
|
# Unwrap Optional types
|
|
@@ -631,7 +652,7 @@ def extract_field_info(param: inspect.Parameter) -> ToolParamInfo:
|
|
|
631
652
|
"""
|
|
632
653
|
annotation = param.annotation
|
|
633
654
|
if annotation == inspect.Parameter.empty:
|
|
634
|
-
raise
|
|
655
|
+
raise ToolInputSchemaError(f"Parameter {param} has no type annotation.")
|
|
635
656
|
|
|
636
657
|
# Get the majority of the param info from either the Pydantic Field() or regular inspection
|
|
637
658
|
if isinstance(param.default, FieldInfo):
|
|
@@ -650,7 +671,7 @@ def extract_field_info(param: inspect.Parameter) -> ToolParamInfo:
|
|
|
650
671
|
elif len(str_annotations) == 2:
|
|
651
672
|
new_name = str_annotations[0]
|
|
652
673
|
if not new_name.isidentifier():
|
|
653
|
-
raise
|
|
674
|
+
raise ToolInputSchemaError(
|
|
654
675
|
f"Invalid parameter name: '{new_name}' is not a valid identifier. "
|
|
655
676
|
"Identifiers must start with a letter or underscore, "
|
|
656
677
|
"and can only contain letters, digits, or underscores."
|
|
@@ -658,7 +679,7 @@ def extract_field_info(param: inspect.Parameter) -> ToolParamInfo:
|
|
|
658
679
|
param_info.name = new_name
|
|
659
680
|
param_info.description = str_annotations[1]
|
|
660
681
|
else:
|
|
661
|
-
raise
|
|
682
|
+
raise ToolInputSchemaError(
|
|
662
683
|
f"Parameter {param} has too many string annotations. Expected 0, 1, or 2, got {len(str_annotations)}."
|
|
663
684
|
)
|
|
664
685
|
|
|
@@ -673,10 +694,12 @@ def extract_field_info(param: inspect.Parameter) -> ToolParamInfo:
|
|
|
673
694
|
|
|
674
695
|
# Final reality check
|
|
675
696
|
if param_info.description is None:
|
|
676
|
-
raise
|
|
697
|
+
raise ToolInputSchemaError(
|
|
698
|
+
f"Parameter '{param_info.name}' is missing a description. Parameter descriptions are specified as string annotations using the typing.Annotated class."
|
|
699
|
+
)
|
|
677
700
|
|
|
678
701
|
if wire_type_info.wire_type is None:
|
|
679
|
-
raise
|
|
702
|
+
raise ToolInputSchemaError(f"Unknown parameter type: {param_info.field_type}")
|
|
680
703
|
|
|
681
704
|
return ToolParamInfo.from_param_info(param_info, wire_type_info, is_inferrable)
|
|
682
705
|
|
|
@@ -792,6 +815,7 @@ def extract_properties(type_to_check: type) -> dict[str, WireTypeInfo] | None:
|
|
|
792
815
|
field_type = next(arg for arg in get_args(field_type) if arg is not type(None))
|
|
793
816
|
|
|
794
817
|
# Get wire type info recursively
|
|
818
|
+
# field_type cannot be None here due to the check above
|
|
795
819
|
wire_info = get_wire_type_info(field_type)
|
|
796
820
|
properties[field_name] = wire_info
|
|
797
821
|
|
|
@@ -870,7 +894,7 @@ def extract_python_param_info(param: inspect.Parameter) -> ParamInfo:
|
|
|
870
894
|
# Union types are not currently supported
|
|
871
895
|
# (other than optional, which is handled above)
|
|
872
896
|
if is_union(field_type):
|
|
873
|
-
raise
|
|
897
|
+
raise ToolInputSchemaError(
|
|
874
898
|
f"Parameter {param.name} is a union type. Only optional types are supported."
|
|
875
899
|
)
|
|
876
900
|
|
|
@@ -890,7 +914,7 @@ def extract_pydantic_param_info(param: inspect.Parameter) -> ParamInfo:
|
|
|
890
914
|
if callable(param.default.default_factory):
|
|
891
915
|
default_value = param.default.default_factory()
|
|
892
916
|
else:
|
|
893
|
-
raise
|
|
917
|
+
raise ToolInputSchemaError(f"Default factory for parameter {param} is not callable.")
|
|
894
918
|
|
|
895
919
|
# If the param is Annotated[], unwrap the annotation to get the "real" type
|
|
896
920
|
# Otherwise, use the literal type
|
|
@@ -965,15 +989,18 @@ def create_func_models(func: Callable) -> tuple[type[BaseModel], type[BaseModel]
|
|
|
965
989
|
if asyncio.iscoroutinefunction(func) and hasattr(func, "__wrapped__"):
|
|
966
990
|
func = func.__wrapped__
|
|
967
991
|
for name, param in inspect.signature(func, follow_wrapped=True).parameters.items():
|
|
968
|
-
# Skip ToolContext parameters
|
|
969
|
-
|
|
992
|
+
# Skip ToolContext parameters (including subclasses like arcade_mcp_server.Context)
|
|
993
|
+
ann = param.annotation
|
|
994
|
+
if isinstance(ann, type) and issubclass(ann, ToolContext):
|
|
970
995
|
continue
|
|
971
996
|
|
|
972
997
|
# TODO make this cleaner
|
|
973
998
|
tool_field_info = extract_field_info(param)
|
|
974
999
|
param_fields = {
|
|
975
1000
|
"default": tool_field_info.default,
|
|
976
|
-
"description": tool_field_info.description
|
|
1001
|
+
"description": tool_field_info.description
|
|
1002
|
+
if tool_field_info.description
|
|
1003
|
+
else "No description provided.",
|
|
977
1004
|
# TODO more here?
|
|
978
1005
|
}
|
|
979
1006
|
input_fields[name] = (tool_field_info.field_type, Field(**param_fields))
|
|
@@ -981,18 +1008,23 @@ def create_func_models(func: Callable) -> tuple[type[BaseModel], type[BaseModel]
|
|
|
981
1008
|
input_model = create_model(f"{snake_to_pascal_case(func.__name__)}Input", **input_fields) # type: ignore[call-overload]
|
|
982
1009
|
|
|
983
1010
|
output_model = determine_output_model(func)
|
|
984
|
-
|
|
985
1011
|
return input_model, output_model
|
|
986
1012
|
|
|
987
1013
|
|
|
988
|
-
def determine_output_model(func: Callable) -> type[BaseModel]:
|
|
1014
|
+
def determine_output_model(func: Callable) -> type[BaseModel]:
|
|
989
1015
|
"""
|
|
990
1016
|
Determine the output model for a function based on its return annotation.
|
|
991
1017
|
"""
|
|
992
1018
|
return_annotation = inspect.signature(func).return_annotation
|
|
993
1019
|
output_model_name = f"{snake_to_pascal_case(func.__name__)}Output"
|
|
1020
|
+
|
|
1021
|
+
# If the return annotation is empty, create a model with no fields
|
|
994
1022
|
if return_annotation is inspect.Signature.empty:
|
|
995
1023
|
return create_model(output_model_name)
|
|
1024
|
+
|
|
1025
|
+
# If the return annotation has an __origin__ attribute
|
|
1026
|
+
# and does not have a __metadata__ attribute.
|
|
1027
|
+
# This is the case for TypedDicts.
|
|
996
1028
|
elif hasattr(return_annotation, "__origin__"):
|
|
997
1029
|
if hasattr(return_annotation, "__metadata__"):
|
|
998
1030
|
field_type = return_annotation.__args__[0]
|
|
@@ -1008,15 +1040,30 @@ def determine_output_model(func: Callable) -> type[BaseModel]: # noqa: C901
|
|
|
1008
1040
|
)
|
|
1009
1041
|
return create_model(
|
|
1010
1042
|
output_model_name,
|
|
1011
|
-
result=(
|
|
1043
|
+
result=(
|
|
1044
|
+
typeddict_model,
|
|
1045
|
+
Field(
|
|
1046
|
+
description=str(description)
|
|
1047
|
+
if description
|
|
1048
|
+
else "No description provided."
|
|
1049
|
+
),
|
|
1050
|
+
),
|
|
1012
1051
|
)
|
|
1013
1052
|
|
|
1053
|
+
# If the return annotation has a description, use it
|
|
1014
1054
|
if description:
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1055
|
+
try:
|
|
1056
|
+
return create_model(
|
|
1057
|
+
output_model_name,
|
|
1058
|
+
result=(field_type, Field(description=str(description))),
|
|
1059
|
+
)
|
|
1060
|
+
except Exception:
|
|
1061
|
+
raise ToolOutputSchemaError(
|
|
1062
|
+
f"Unsupported output type '{field_type}'. Only built-in Python types, TypedDicts, "
|
|
1063
|
+
"Pydantic models, and standard collections are supported as tool output types."
|
|
1064
|
+
)
|
|
1065
|
+
|
|
1066
|
+
# If the return annotation is a Union type
|
|
1020
1067
|
origin = return_annotation.__origin__
|
|
1021
1068
|
if origin is typing.Union:
|
|
1022
1069
|
# For union types, create a model with the first non-None argument
|
|
@@ -1037,10 +1084,15 @@ def determine_output_model(func: Callable) -> type[BaseModel]: # noqa: C901
|
|
|
1037
1084
|
)
|
|
1038
1085
|
return create_model(
|
|
1039
1086
|
output_model_name,
|
|
1040
|
-
result=(
|
|
1087
|
+
result=(
|
|
1088
|
+
arg,
|
|
1089
|
+
Field(description="No description provided."),
|
|
1090
|
+
),
|
|
1041
1091
|
)
|
|
1042
|
-
|
|
1092
|
+
|
|
1093
|
+
# If the return annotation has an __origin__ attribute
|
|
1043
1094
|
# and does not have a __metadata__ attribute.
|
|
1095
|
+
# This is the case for TypedDicts.
|
|
1044
1096
|
return create_model(
|
|
1045
1097
|
output_model_name,
|
|
1046
1098
|
result=(
|
|
@@ -1049,7 +1101,7 @@ def determine_output_model(func: Callable) -> type[BaseModel]: # noqa: C901
|
|
|
1049
1101
|
),
|
|
1050
1102
|
)
|
|
1051
1103
|
else:
|
|
1052
|
-
#
|
|
1104
|
+
# If the return annotation is a TypedDict
|
|
1053
1105
|
if is_typeddict(return_annotation):
|
|
1054
1106
|
typeddict_model = create_model_from_typeddict(return_annotation, output_model_name)
|
|
1055
1107
|
return create_model(
|
|
@@ -1060,10 +1112,13 @@ def determine_output_model(func: Callable) -> type[BaseModel]: # noqa: C901
|
|
|
1060
1112
|
),
|
|
1061
1113
|
)
|
|
1062
1114
|
|
|
1063
|
-
#
|
|
1115
|
+
# If the return annotation is a simple type (like str)
|
|
1064
1116
|
return create_model(
|
|
1065
1117
|
output_model_name,
|
|
1066
|
-
result=(
|
|
1118
|
+
result=(
|
|
1119
|
+
return_annotation,
|
|
1120
|
+
Field(description="No description provided."),
|
|
1121
|
+
),
|
|
1067
1122
|
)
|
|
1068
1123
|
|
|
1069
1124
|
|
|
@@ -1101,9 +1156,13 @@ def create_model_from_typeddict(typeddict_class: type, model_name: str) -> type[
|
|
|
1101
1156
|
def to_tool_secret_requirements(
|
|
1102
1157
|
secrets_requirement: list[str],
|
|
1103
1158
|
) -> list[ToolSecretRequirement]:
|
|
1104
|
-
#
|
|
1105
|
-
|
|
1106
|
-
|
|
1159
|
+
# De-dupe case-insensitively but preserve the original casing for env var lookup
|
|
1160
|
+
unique_map: dict[str, str] = {}
|
|
1161
|
+
for name in secrets_requirement:
|
|
1162
|
+
lowered = str(name).lower()
|
|
1163
|
+
if lowered not in unique_map:
|
|
1164
|
+
unique_map[lowered] = str(name)
|
|
1165
|
+
return [ToolSecretRequirement(key=orig_name) for orig_name in unique_map.values()]
|
|
1107
1166
|
|
|
1108
1167
|
|
|
1109
1168
|
def to_tool_metadata_requirements(
|
arcade_core/context.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Arcade Core Runtime Context Protocols
|
|
3
|
+
|
|
4
|
+
Defines the developer-facing, transport-agnostic runtime context interfaces
|
|
5
|
+
(namespaced APIs: logs, progress, resources, tools, prompts, sampling, UI,
|
|
6
|
+
notifications) and the top-level ModelContext Protocol that aggregates them.
|
|
7
|
+
|
|
8
|
+
Implementations live in runtime packages (e.g., arcade_mcp_server); tool authors should
|
|
9
|
+
use `arcade_mcp_server.Context` for concrete usage.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any, Protocol, runtime_checkable
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LogsContext(Protocol):
|
|
20
|
+
async def debug(self, message: str, **kwargs: dict[str, Any]) -> None: ...
|
|
21
|
+
|
|
22
|
+
async def info(self, message: str, **kwargs: dict[str, Any]) -> None: ...
|
|
23
|
+
|
|
24
|
+
async def warning(self, message: str, **kwargs: dict[str, Any]) -> None: ...
|
|
25
|
+
|
|
26
|
+
async def error(self, message: str, **kwargs: dict[str, Any]) -> None: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ProgressContext(Protocol):
|
|
30
|
+
async def report(
|
|
31
|
+
self, progress: float, total: float | None = None, message: str | None = None
|
|
32
|
+
) -> None: ...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ResourcesContext(Protocol):
|
|
36
|
+
async def list_(self) -> list[Any]: ...
|
|
37
|
+
|
|
38
|
+
async def get(self, uri: str) -> Any: ...
|
|
39
|
+
|
|
40
|
+
async def read(self, uri: str) -> list[Any]: ...
|
|
41
|
+
|
|
42
|
+
async def list_roots(self) -> list[Any]: ...
|
|
43
|
+
|
|
44
|
+
async def list_templates(self) -> list[Any]: ...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ToolsContext(Protocol):
|
|
48
|
+
async def list_(self) -> list[Any]: ...
|
|
49
|
+
|
|
50
|
+
async def call_raw(self, name: str, params: dict[str, Any]) -> BaseModel: ...
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class PromptsContext(Protocol):
|
|
54
|
+
async def list_(self) -> list[Any]: ...
|
|
55
|
+
|
|
56
|
+
async def get(self, name: str, arguments: dict[str, str] | None = None) -> Any: ...
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class SamplingContext(Protocol):
|
|
60
|
+
async def create_message(
|
|
61
|
+
self,
|
|
62
|
+
messages: str | list[str | Any],
|
|
63
|
+
system_prompt: str | None = None,
|
|
64
|
+
include_context: str | None = None,
|
|
65
|
+
temperature: float | None = None,
|
|
66
|
+
max_tokens: int | None = None,
|
|
67
|
+
model_preferences: Any | None = None,
|
|
68
|
+
) -> Any: ...
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class UIContext(Protocol):
|
|
72
|
+
async def elicit(self, message: str, schema: dict[str, Any] | None = None) -> Any: ...
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class NotificationsToolsContext(Protocol):
|
|
76
|
+
async def list_changed(self) -> None: ...
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class NotificationsResourcesContext(Protocol):
|
|
80
|
+
async def list_changed(self) -> None: ...
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class NotificationsPromptsContext(Protocol):
|
|
84
|
+
async def list_changed(self) -> None: ...
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class NotificationsContext(Protocol):
|
|
88
|
+
@property
|
|
89
|
+
def tools(self) -> NotificationsToolsContext: ...
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def resources(self) -> NotificationsResourcesContext: ...
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def prompts(self) -> NotificationsPromptsContext: ...
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@runtime_checkable
|
|
99
|
+
class ModelContext(Protocol):
|
|
100
|
+
@property
|
|
101
|
+
def log(self) -> LogsContext: ...
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def progress(self) -> ProgressContext: ...
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def resources(self) -> ResourcesContext: ...
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def tools(self) -> ToolsContext: ...
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def prompts(self) -> PromptsContext: ...
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def sampling(self) -> SamplingContext: ...
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def ui(self) -> UIContext: ...
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def notifications(self) -> NotificationsContext: ...
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def request_id(self) -> str | None: ...
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def session_id(self) -> str | None: ...
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Converter for converting Arcade ToolDefinition to OpenAI tool schema."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal, TypedDict
|
|
4
|
+
|
|
5
|
+
from arcade_core.catalog import MaterializedTool
|
|
6
|
+
from arcade_core.schema import InputParameter, ValueSchema
|
|
7
|
+
|
|
8
|
+
# ----------------------------------------------------------------------------
|
|
9
|
+
# Type definitions for JSON tool schemas used by OpenAI APIs.
|
|
10
|
+
# Defines the proper types for tool schemas to ensure
|
|
11
|
+
# compatibility with OpenAI's Responses and Chat Completions APIs.
|
|
12
|
+
# ----------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OpenAIFunctionParameterProperty(TypedDict, total=False):
|
|
16
|
+
"""Type definition for a property within OpenAI function parameters schema."""
|
|
17
|
+
|
|
18
|
+
type: str | list[str]
|
|
19
|
+
"""The JSON Schema type(s) for this property. Can be a single type or list for unions (e.g., ["string", "null"])."""
|
|
20
|
+
|
|
21
|
+
description: str
|
|
22
|
+
"""Description of the property."""
|
|
23
|
+
|
|
24
|
+
enum: list[Any]
|
|
25
|
+
"""Allowed values for enum properties."""
|
|
26
|
+
|
|
27
|
+
items: dict[str, Any]
|
|
28
|
+
"""Schema for array items when type is 'array'."""
|
|
29
|
+
|
|
30
|
+
properties: dict[str, "OpenAIFunctionParameterProperty"]
|
|
31
|
+
"""Nested properties when type is 'object'."""
|
|
32
|
+
|
|
33
|
+
required: list[str]
|
|
34
|
+
"""Required fields for nested objects."""
|
|
35
|
+
|
|
36
|
+
additionalProperties: Literal[False]
|
|
37
|
+
"""Must be False for strict mode compliance."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OpenAIFunctionParameters(TypedDict, total=False):
|
|
41
|
+
"""Type definition for OpenAI function parameters schema."""
|
|
42
|
+
|
|
43
|
+
type: Literal["object"]
|
|
44
|
+
"""Must be 'object' for function parameters."""
|
|
45
|
+
|
|
46
|
+
properties: dict[str, OpenAIFunctionParameterProperty]
|
|
47
|
+
"""The properties of the function parameters."""
|
|
48
|
+
|
|
49
|
+
required: list[str]
|
|
50
|
+
"""List of required parameter names. In strict mode, all properties should be listed here."""
|
|
51
|
+
|
|
52
|
+
additionalProperties: Literal[False]
|
|
53
|
+
"""Must be False for strict mode compliance."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class OpenAIFunctionSchema(TypedDict, total=False):
|
|
57
|
+
"""Type definition for a function tool parameter matching OpenAI's API."""
|
|
58
|
+
|
|
59
|
+
name: str
|
|
60
|
+
"""The name of the function to call."""
|
|
61
|
+
|
|
62
|
+
parameters: OpenAIFunctionParameters | None
|
|
63
|
+
"""A JSON schema object describing the parameters of the function."""
|
|
64
|
+
|
|
65
|
+
strict: Literal[True]
|
|
66
|
+
"""Always enforce strict parameter validation. Default `true`."""
|
|
67
|
+
|
|
68
|
+
description: str | None
|
|
69
|
+
"""A description of the function.
|
|
70
|
+
Used by the model to determine whether or not to call the function.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class OpenAIToolSchema(TypedDict):
|
|
75
|
+
"""
|
|
76
|
+
Schema for a tool definition passed to OpenAI's `tools` parameter.
|
|
77
|
+
A tool wraps a callable function for function-calling. Each tool
|
|
78
|
+
includes a type (always 'function') and a `function` payload that
|
|
79
|
+
specifies the callable via `OpenAIFunctionSchema`.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
type: Literal["function"]
|
|
83
|
+
"""The type field, always 'function'."""
|
|
84
|
+
|
|
85
|
+
function: OpenAIFunctionSchema
|
|
86
|
+
"""The function definition."""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# Type alias for a list of openai tool schemas
|
|
90
|
+
OpenAIToolList = list[OpenAIToolSchema]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ----------------------------------------------------------------------------
|
|
94
|
+
# Converters
|
|
95
|
+
# ----------------------------------------------------------------------------
|
|
96
|
+
def to_openai(tool: MaterializedTool) -> OpenAIToolSchema:
|
|
97
|
+
"""Convert a MaterializedTool to OpenAI JsonToolSchema format.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
tool: The MaterializedTool to convert
|
|
101
|
+
Returns:
|
|
102
|
+
The OpenAI JsonToolSchema format (what is passed to the OpenAI API)
|
|
103
|
+
"""
|
|
104
|
+
name = tool.definition.fully_qualified_name.replace(".", "_")
|
|
105
|
+
description = tool.description
|
|
106
|
+
parameters_schema = _convert_input_parameters_to_json_schema(tool.definition.input.parameters)
|
|
107
|
+
return _create_tool_schema(name, description, parameters_schema)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _create_tool_schema(
|
|
111
|
+
name: str, description: str, parameters: OpenAIFunctionParameters
|
|
112
|
+
) -> OpenAIToolSchema:
|
|
113
|
+
"""Create a properly typed tool schema.
|
|
114
|
+
Args:
|
|
115
|
+
name: The name of the function
|
|
116
|
+
description: Description of what the function does
|
|
117
|
+
parameters: JSON schema for the function parameters
|
|
118
|
+
strict: Whether to enforce strict validation (default: True for reliable function calls)
|
|
119
|
+
Returns:
|
|
120
|
+
A properly typed OpenAIToolSchema
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
function: OpenAIFunctionSchema = {
|
|
124
|
+
"name": name,
|
|
125
|
+
"description": description,
|
|
126
|
+
"parameters": parameters,
|
|
127
|
+
"strict": True,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
tool: OpenAIToolSchema = {
|
|
131
|
+
"type": "function",
|
|
132
|
+
"function": function,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return tool
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _convert_value_schema_to_json_schema(
|
|
139
|
+
value_schema: ValueSchema,
|
|
140
|
+
) -> OpenAIFunctionParameterProperty:
|
|
141
|
+
"""Convert Arcade ValueSchema to JSON Schema format."""
|
|
142
|
+
type_mapping = {
|
|
143
|
+
"string": "string",
|
|
144
|
+
"integer": "integer",
|
|
145
|
+
"number": "number",
|
|
146
|
+
"boolean": "boolean",
|
|
147
|
+
"json": "object",
|
|
148
|
+
"array": "array",
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
schema: OpenAIFunctionParameterProperty = {"type": type_mapping[value_schema.val_type]}
|
|
152
|
+
|
|
153
|
+
if value_schema.val_type == "array" and value_schema.inner_val_type:
|
|
154
|
+
items_schema: dict[str, Any] = {"type": type_mapping[value_schema.inner_val_type]}
|
|
155
|
+
|
|
156
|
+
# For arrays, enum should be applied to the items, not the array itself
|
|
157
|
+
if value_schema.enum:
|
|
158
|
+
items_schema["enum"] = value_schema.enum
|
|
159
|
+
|
|
160
|
+
schema["items"] = items_schema
|
|
161
|
+
else:
|
|
162
|
+
# Handle enum for non-array types
|
|
163
|
+
if value_schema.enum:
|
|
164
|
+
schema["enum"] = value_schema.enum
|
|
165
|
+
|
|
166
|
+
# Handle object properties
|
|
167
|
+
if value_schema.val_type == "json" and value_schema.properties:
|
|
168
|
+
schema["properties"] = {
|
|
169
|
+
name: _convert_value_schema_to_json_schema(nested_schema)
|
|
170
|
+
for name, nested_schema in value_schema.properties.items()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return schema
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _convert_input_parameters_to_json_schema(
|
|
177
|
+
parameters: list[InputParameter],
|
|
178
|
+
) -> OpenAIFunctionParameters:
|
|
179
|
+
"""Convert list of InputParameter to JSON schema parameters object."""
|
|
180
|
+
if not parameters:
|
|
181
|
+
# Minimal JSON schema for a tool with no input parameters
|
|
182
|
+
return {
|
|
183
|
+
"type": "object",
|
|
184
|
+
"properties": {},
|
|
185
|
+
"additionalProperties": False,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
properties = {}
|
|
189
|
+
required = []
|
|
190
|
+
|
|
191
|
+
for parameter in parameters:
|
|
192
|
+
param_schema = _convert_value_schema_to_json_schema(parameter.value_schema)
|
|
193
|
+
|
|
194
|
+
# For optional parameters in strict mode, we need to add "null" as a type option
|
|
195
|
+
if not parameter.required:
|
|
196
|
+
param_type = param_schema.get("type")
|
|
197
|
+
if isinstance(param_type, str):
|
|
198
|
+
# Convert single type to union with null
|
|
199
|
+
param_schema["type"] = [param_type, "null"]
|
|
200
|
+
elif isinstance(param_type, list) and "null" not in param_type:
|
|
201
|
+
param_schema["type"] = [*param_type, "null"]
|
|
202
|
+
|
|
203
|
+
if parameter.description:
|
|
204
|
+
param_schema["description"] = parameter.description
|
|
205
|
+
properties[parameter.name] = param_schema
|
|
206
|
+
|
|
207
|
+
# In strict mode, all parameters (including optional ones) go in required array
|
|
208
|
+
# Optional parameters are handled by adding "null" to their type
|
|
209
|
+
required.append(parameter.name)
|
|
210
|
+
|
|
211
|
+
json_schema: OpenAIFunctionParameters = {
|
|
212
|
+
"type": "object",
|
|
213
|
+
"properties": properties,
|
|
214
|
+
"required": required,
|
|
215
|
+
"additionalProperties": False,
|
|
216
|
+
}
|
|
217
|
+
if not required:
|
|
218
|
+
del json_schema["required"]
|
|
219
|
+
|
|
220
|
+
return json_schema
|