chuk-tool-processor 0.1.0__py3-none-any.whl → 0.1.2__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.
- chuk_tool_processor/core/processor.py +8 -8
- chuk_tool_processor/execution/strategies/inprocess_strategy.py +110 -148
- chuk_tool_processor/execution/strategies/subprocess_strategy.py +1 -1
- chuk_tool_processor/execution/wrappers/retry.py +1 -1
- chuk_tool_processor/logging/__init__.py +33 -0
- chuk_tool_processor/logging/context.py +47 -0
- chuk_tool_processor/logging/formatter.py +55 -0
- chuk_tool_processor/logging/helpers.py +112 -0
- chuk_tool_processor/logging/metrics.py +59 -0
- chuk_tool_processor/models/execution_strategy.py +1 -1
- chuk_tool_processor/models/tool_export_mixin.py +29 -0
- chuk_tool_processor/models/validated_tool.py +155 -0
- chuk_tool_processor/plugins/discovery.py +105 -172
- chuk_tool_processor/plugins/parsers/__init__.py +1 -1
- chuk_tool_processor/plugins/parsers/base.py +18 -0
- chuk_tool_processor/plugins/parsers/function_call_tool_plugin.py +81 -0
- chuk_tool_processor/plugins/parsers/json_tool_plugin.py +38 -0
- chuk_tool_processor/plugins/parsers/openai_tool_plugin.py +76 -0
- chuk_tool_processor/plugins/parsers/xml_tool.py +28 -24
- chuk_tool_processor/registry/__init__.py +11 -10
- chuk_tool_processor/registry/auto_register.py +125 -0
- chuk_tool_processor/registry/provider.py +84 -29
- chuk_tool_processor/registry/providers/memory.py +77 -112
- chuk_tool_processor/registry/tool_export.py +76 -0
- chuk_tool_processor/utils/validation.py +106 -177
- {chuk_tool_processor-0.1.0.dist-info → chuk_tool_processor-0.1.2.dist-info}/METADATA +5 -2
- chuk_tool_processor-0.1.2.dist-info/RECORD +47 -0
- chuk_tool_processor/plugins/parsers/function_call_tool.py +0 -105
- chuk_tool_processor/plugins/parsers/json_tool.py +0 -17
- chuk_tool_processor/utils/logging.py +0 -260
- chuk_tool_processor-0.1.0.dist-info/RECORD +0 -37
- {chuk_tool_processor-0.1.0.dist-info → chuk_tool_processor-0.1.2.dist-info}/WHEEL +0 -0
- {chuk_tool_processor-0.1.0.dist-info → chuk_tool_processor-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# chuk_tool_processor/registry/providers/memory.py
|
|
2
|
+
# chuk_tool_processor/registry/providers/memory.py
|
|
2
3
|
"""
|
|
3
4
|
In-memory implementation of the tool registry.
|
|
4
5
|
"""
|
|
5
6
|
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
6
9
|
import inspect
|
|
7
10
|
from typing import Any, Dict, List, Optional, Tuple
|
|
8
11
|
|
|
@@ -14,152 +17,114 @@ from chuk_tool_processor.registry.metadata import ToolMetadata
|
|
|
14
17
|
class InMemoryToolRegistry(ToolRegistryInterface):
|
|
15
18
|
"""
|
|
16
19
|
In-memory implementation of ToolRegistryInterface with namespace support.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
organized by namespace. It's suitable for single-process applications
|
|
20
|
-
or for testing, but doesn't provide persistence or sharing across
|
|
21
|
-
multiple processes.
|
|
20
|
+
|
|
21
|
+
Suitable for single-process apps or tests; not persisted across processes.
|
|
22
22
|
"""
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
|
|
24
|
+
# ------------------------------------------------------------------ #
|
|
25
|
+
# construction
|
|
26
|
+
# ------------------------------------------------------------------ #
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
# {namespace: {tool_name: tool_obj}}
|
|
26
30
|
self._tools: Dict[str, Dict[str, Any]] = {}
|
|
27
|
-
#
|
|
31
|
+
# {namespace: {tool_name: ToolMetadata}}
|
|
28
32
|
self._metadata: Dict[str, Dict[str, ToolMetadata]] = {}
|
|
29
33
|
|
|
34
|
+
# ------------------------------------------------------------------ #
|
|
35
|
+
# registration
|
|
36
|
+
# ------------------------------------------------------------------ #
|
|
37
|
+
|
|
30
38
|
def register_tool(
|
|
31
|
-
self,
|
|
32
|
-
tool: Any,
|
|
39
|
+
self,
|
|
40
|
+
tool: Any,
|
|
33
41
|
name: Optional[str] = None,
|
|
34
42
|
namespace: str = "default",
|
|
35
|
-
metadata: Optional[Dict[str, Any]] = None
|
|
43
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
36
44
|
) -> None:
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
# ensure namespace buckets
|
|
46
|
+
self._tools.setdefault(namespace, {})
|
|
47
|
+
self._metadata.setdefault(namespace, {})
|
|
39
48
|
|
|
40
|
-
Args:
|
|
41
|
-
tool: The tool class or instance with an `execute` method.
|
|
42
|
-
name: Optional explicit name; if omitted, uses tool.__name__.
|
|
43
|
-
namespace: Namespace for the tool (default: "default").
|
|
44
|
-
metadata: Optional additional metadata for the tool.
|
|
45
|
-
"""
|
|
46
|
-
# Ensure the namespace exists
|
|
47
|
-
if namespace not in self._tools:
|
|
48
|
-
self._tools[namespace] = {}
|
|
49
|
-
self._metadata[namespace] = {}
|
|
50
|
-
|
|
51
|
-
# Determine tool name
|
|
52
49
|
key = name or getattr(tool, "__name__", None) or repr(tool)
|
|
53
|
-
|
|
54
|
-
# Register the tool
|
|
55
50
|
self._tools[namespace][key] = tool
|
|
56
|
-
|
|
57
|
-
#
|
|
51
|
+
|
|
52
|
+
# build metadata -------------------------------------------------
|
|
58
53
|
is_async = inspect.iscoroutinefunction(getattr(tool, "execute", None))
|
|
59
|
-
|
|
60
|
-
#
|
|
61
|
-
description =
|
|
62
|
-
|
|
63
|
-
description
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
54
|
+
|
|
55
|
+
# default description -> docstring
|
|
56
|
+
description = (
|
|
57
|
+
(inspect.getdoc(tool) or "").strip()
|
|
58
|
+
if not (metadata and "description" in metadata)
|
|
59
|
+
else None
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
meta_dict: Dict[str, Any] = {
|
|
67
63
|
"name": key,
|
|
68
64
|
"namespace": namespace,
|
|
69
|
-
"is_async": is_async
|
|
65
|
+
"is_async": is_async,
|
|
70
66
|
}
|
|
71
|
-
|
|
72
|
-
# Add description if available (but don't override metadata if provided)
|
|
73
|
-
if description and not (metadata and "description" in metadata):
|
|
67
|
+
if description:
|
|
74
68
|
meta_dict["description"] = description
|
|
75
|
-
|
|
76
|
-
# Add any additional metadata
|
|
77
69
|
if metadata:
|
|
78
70
|
meta_dict.update(metadata)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
|
|
72
|
+
self._metadata[namespace][key] = ToolMetadata(**meta_dict)
|
|
73
|
+
|
|
74
|
+
# ------------------------------------------------------------------ #
|
|
75
|
+
# retrieval
|
|
76
|
+
# ------------------------------------------------------------------ #
|
|
83
77
|
|
|
84
78
|
def get_tool(self, name: str, namespace: str = "default") -> Optional[Any]:
|
|
85
|
-
|
|
86
|
-
Retrieve a registered tool by name and namespace.
|
|
87
|
-
|
|
88
|
-
Args:
|
|
89
|
-
name: The name of the tool.
|
|
90
|
-
namespace: The namespace of the tool (default: "default").
|
|
91
|
-
|
|
92
|
-
Returns:
|
|
93
|
-
The tool implementation or None if not found.
|
|
94
|
-
"""
|
|
95
|
-
if namespace not in self._tools:
|
|
96
|
-
return None
|
|
97
|
-
return self._tools[namespace].get(name)
|
|
79
|
+
return self._tools.get(namespace, {}).get(name)
|
|
98
80
|
|
|
99
81
|
def get_tool_strict(self, name: str, namespace: str = "default") -> Any:
|
|
100
|
-
"""
|
|
101
|
-
Retrieve a registered tool by name and namespace, raising an exception if not found.
|
|
102
|
-
|
|
103
|
-
Args:
|
|
104
|
-
name: The name of the tool.
|
|
105
|
-
namespace: The namespace of the tool (default: "default").
|
|
106
|
-
|
|
107
|
-
Returns:
|
|
108
|
-
The tool implementation.
|
|
109
|
-
|
|
110
|
-
Raises:
|
|
111
|
-
ToolNotFoundError: If the tool is not found.
|
|
112
|
-
"""
|
|
113
82
|
tool = self.get_tool(name, namespace)
|
|
114
83
|
if tool is None:
|
|
115
84
|
raise ToolNotFoundError(f"{namespace}.{name}")
|
|
116
85
|
return tool
|
|
117
86
|
|
|
118
|
-
def get_metadata(
|
|
119
|
-
""
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
Returns:
|
|
127
|
-
ToolMetadata if found, None otherwise.
|
|
128
|
-
"""
|
|
129
|
-
if namespace not in self._metadata:
|
|
130
|
-
return None
|
|
131
|
-
return self._metadata[namespace].get(name)
|
|
87
|
+
def get_metadata(
|
|
88
|
+
self, name: str, namespace: str = "default"
|
|
89
|
+
) -> Optional[ToolMetadata]:
|
|
90
|
+
return self._metadata.get(namespace, {}).get(name)
|
|
91
|
+
|
|
92
|
+
# ------------------------------------------------------------------ #
|
|
93
|
+
# listing helpers
|
|
94
|
+
# ------------------------------------------------------------------ #
|
|
132
95
|
|
|
133
96
|
def list_tools(self, namespace: Optional[str] = None) -> List[Tuple[str, str]]:
|
|
134
97
|
"""
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
Args:
|
|
138
|
-
namespace: Optional namespace filter.
|
|
139
|
-
|
|
140
|
-
Returns:
|
|
141
|
-
List of (namespace, name) tuples.
|
|
98
|
+
Return a list of ``(namespace, name)`` tuples.
|
|
142
99
|
"""
|
|
143
|
-
result = []
|
|
144
|
-
|
|
145
100
|
if namespace:
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
for name in tools.keys():
|
|
154
|
-
result.append((ns, name))
|
|
155
|
-
|
|
101
|
+
return [
|
|
102
|
+
(namespace, n) for n in self._tools.get(namespace, {}).keys()
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
result: List[Tuple[str, str]] = []
|
|
106
|
+
for ns, tools in self._tools.items():
|
|
107
|
+
result.extend((ns, n) for n in tools.keys())
|
|
156
108
|
return result
|
|
157
109
|
|
|
158
110
|
def list_namespaces(self) -> List[str]:
|
|
111
|
+
return list(self._tools.keys())
|
|
112
|
+
|
|
113
|
+
def list_metadata(self, namespace: str | None = None) -> List[ToolMetadata]:
|
|
159
114
|
"""
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
115
|
+
Return *all* :class:`ToolMetadata` objects.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
namespace
|
|
120
|
+
• ``None`` *(default)* – metadata from **all** namespaces
|
|
121
|
+
• ``"some_ns"`` – only that namespace
|
|
164
122
|
"""
|
|
165
|
-
|
|
123
|
+
if namespace is not None:
|
|
124
|
+
return list(self._metadata.get(namespace, {}).values())
|
|
125
|
+
|
|
126
|
+
# flatten
|
|
127
|
+
result: List[ToolMetadata] = []
|
|
128
|
+
for ns_meta in self._metadata.values():
|
|
129
|
+
result.extend(ns_meta.values())
|
|
130
|
+
return result
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# chuk_tool_processor/registry/tool_export.py
|
|
2
|
+
"""
|
|
3
|
+
Helpers that expose all registered tools in various formats and
|
|
4
|
+
translate an OpenAI `function.name` back to the matching tool.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Dict, List
|
|
9
|
+
|
|
10
|
+
from .provider import ToolRegistryProvider
|
|
11
|
+
|
|
12
|
+
# --------------------------------------------------------------------------- #
|
|
13
|
+
# internal cache so tool-name lookup is O(1)
|
|
14
|
+
# --------------------------------------------------------------------------- #
|
|
15
|
+
_OPENAI_NAME_CACHE: dict[str, object] | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _build_openai_name_cache() -> None:
|
|
19
|
+
"""Populate the global reverse-lookup table once."""
|
|
20
|
+
global _OPENAI_NAME_CACHE
|
|
21
|
+
if _OPENAI_NAME_CACHE is not None: # already built
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
_OPENAI_NAME_CACHE = {}
|
|
25
|
+
reg = ToolRegistryProvider.get_registry()
|
|
26
|
+
|
|
27
|
+
for ns, key in reg.list_tools():
|
|
28
|
+
tool = reg.get_tool(key, ns)
|
|
29
|
+
|
|
30
|
+
# ▸ registry key -> tool
|
|
31
|
+
_OPENAI_NAME_CACHE[key] = tool
|
|
32
|
+
|
|
33
|
+
# ▸ class name -> tool (legacy)
|
|
34
|
+
_OPENAI_NAME_CACHE[tool.__class__.__name__] = tool
|
|
35
|
+
|
|
36
|
+
# ▸ OpenAI name -> tool (may differ from both above)
|
|
37
|
+
_OPENAI_NAME_CACHE[tool.to_openai()["function"]["name"]] = tool
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# --------------------------------------------------------------------------- #
|
|
41
|
+
# public helpers
|
|
42
|
+
# --------------------------------------------------------------------------- #
|
|
43
|
+
def openai_functions() -> List[Dict]:
|
|
44
|
+
"""
|
|
45
|
+
Return **all** registered tools in the exact schema the Chat-Completions
|
|
46
|
+
API expects in its ``tools=[ … ]`` parameter.
|
|
47
|
+
|
|
48
|
+
The ``function.name`` is always the *registry key* so that the round-trip
|
|
49
|
+
(export → model → parser) stays consistent even when the class name and
|
|
50
|
+
the registered key differ.
|
|
51
|
+
"""
|
|
52
|
+
reg = ToolRegistryProvider.get_registry()
|
|
53
|
+
specs: list[dict] = []
|
|
54
|
+
|
|
55
|
+
for ns, key in reg.list_tools():
|
|
56
|
+
tool = reg.get_tool(key, ns)
|
|
57
|
+
spec = tool.to_openai()
|
|
58
|
+
spec["function"]["name"] = key # ensure round-trip consistency
|
|
59
|
+
specs.append(spec)
|
|
60
|
+
|
|
61
|
+
# Ensure the cache is built the first time we export
|
|
62
|
+
_build_openai_name_cache()
|
|
63
|
+
return specs
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def tool_by_openai_name(name: str):
|
|
67
|
+
"""
|
|
68
|
+
Map an OpenAI ``function.name`` back to the registered tool.
|
|
69
|
+
|
|
70
|
+
Raises ``KeyError`` if the name is unknown.
|
|
71
|
+
"""
|
|
72
|
+
_build_openai_name_cache()
|
|
73
|
+
try:
|
|
74
|
+
return _OPENAI_NAME_CACHE[name] # type: ignore[index]
|
|
75
|
+
except (KeyError, TypeError):
|
|
76
|
+
raise KeyError(f"No tool registered for OpenAI name {name!r}") from None
|
|
@@ -1,192 +1,121 @@
|
|
|
1
1
|
# chuk_tool_processor/utils/validation.py
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
"""
|
|
3
|
+
Runtime helpers for validating tool inputs / outputs with Pydantic.
|
|
4
|
+
|
|
5
|
+
Public API
|
|
6
|
+
----------
|
|
7
|
+
validate_arguments(tool_name, fn, args) -> dict
|
|
8
|
+
validate_result(tool_name, fn, result) -> Any
|
|
9
|
+
@with_validation -> class decorator
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
4
12
|
import inspect
|
|
5
|
-
from functools import wraps
|
|
13
|
+
from functools import lru_cache, wraps
|
|
14
|
+
from typing import Any, Callable, Dict, get_type_hints
|
|
15
|
+
from pydantic import BaseModel, ValidationError, create_model, Extra
|
|
6
16
|
|
|
17
|
+
# excpetion
|
|
7
18
|
from chuk_tool_processor.core.exceptions import ToolValidationError
|
|
8
19
|
|
|
20
|
+
__all__ = [
|
|
21
|
+
"validate_arguments",
|
|
22
|
+
"validate_result",
|
|
23
|
+
"with_validation",
|
|
24
|
+
]
|
|
9
25
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
# --------------------------------------------------------------------------- #
|
|
27
|
+
# helpers – create & cache ad-hoc pydantic models
|
|
28
|
+
# --------------------------------------------------------------------------- #
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@lru_cache(maxsize=256)
|
|
32
|
+
def _arg_model(tool_name: str, fn: Callable) -> type[BaseModel]:
|
|
33
|
+
"""Return (and memoise) a pydantic model derived from *fn*'s signature."""
|
|
34
|
+
hints = get_type_hints(fn)
|
|
35
|
+
hints.pop("return", None)
|
|
36
|
+
|
|
37
|
+
sig = inspect.signature(fn)
|
|
38
|
+
fields: Dict[str, tuple[Any, Any]] = {}
|
|
39
|
+
for name, hint in hints.items():
|
|
40
|
+
param = sig.parameters[name]
|
|
41
|
+
default = param.default if param.default is not inspect.Parameter.empty else ...
|
|
42
|
+
fields[name] = (hint, default)
|
|
43
|
+
|
|
44
|
+
return create_model(
|
|
45
|
+
f"{tool_name}Args",
|
|
46
|
+
__config__=type(
|
|
47
|
+
"Cfg",
|
|
48
|
+
(),
|
|
49
|
+
{"extra": Extra.forbid}, # disallow unknown keys
|
|
50
|
+
),
|
|
51
|
+
**fields,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@lru_cache(maxsize=256)
|
|
56
|
+
def _result_model(tool_name: str, fn: Callable) -> type[BaseModel] | None:
|
|
57
|
+
"""Return a pydantic model for the annotated return type (or None)."""
|
|
58
|
+
return_hint = get_type_hints(fn).get("return")
|
|
59
|
+
if return_hint is None or return_hint is type(None): # noqa: E721
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
return create_model(
|
|
63
|
+
f"{tool_name}Result",
|
|
64
|
+
result=(return_hint, ...),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# --------------------------------------------------------------------------- #
|
|
69
|
+
# public validation helpers
|
|
70
|
+
# --------------------------------------------------------------------------- #
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def validate_arguments(tool_name: str, fn: Callable, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
25
74
|
try:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
# Add optional fields based on default values
|
|
39
|
-
sig = inspect.signature(tool_func)
|
|
40
|
-
for param_name, param in sig.parameters.items():
|
|
41
|
-
if param.default is not inspect.Parameter.empty:
|
|
42
|
-
if param_name in field_definitions:
|
|
43
|
-
field_type, _ = field_definitions[param_name]
|
|
44
|
-
field_definitions[param_name] = (field_type, param.default)
|
|
45
|
-
|
|
46
|
-
# Create model
|
|
47
|
-
model = create_model(f"{tool_name}Args", **field_definitions)
|
|
48
|
-
|
|
49
|
-
# Validate args
|
|
50
|
-
validated = model(**args)
|
|
51
|
-
return validated.dict()
|
|
52
|
-
|
|
53
|
-
except ValidationError as e:
|
|
54
|
-
raise ToolValidationError(tool_name, e.errors())
|
|
55
|
-
except Exception as e:
|
|
56
|
-
raise ToolValidationError(tool_name, {"general": str(e)})
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def validate_result(tool_name: str, tool_func: Callable, result: Any) -> Any:
|
|
60
|
-
"""
|
|
61
|
-
Validate tool result against function return type.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
tool_name: Name of the tool for error reporting.
|
|
65
|
-
tool_func: Tool function to validate against.
|
|
66
|
-
result: Result to validate.
|
|
67
|
-
|
|
68
|
-
Returns:
|
|
69
|
-
Validated result.
|
|
70
|
-
|
|
71
|
-
Raises:
|
|
72
|
-
ToolValidationError: If validation fails.
|
|
73
|
-
"""
|
|
75
|
+
model = _arg_model(tool_name, fn)
|
|
76
|
+
return model(**args).dict()
|
|
77
|
+
except ValidationError as exc:
|
|
78
|
+
raise ToolValidationError(tool_name, exc.errors()) from exc
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def validate_result(tool_name: str, fn: Callable, result: Any) -> Any:
|
|
82
|
+
model = _result_model(tool_name, fn)
|
|
83
|
+
if model is None: # no annotation ⇒ no validation
|
|
84
|
+
return result
|
|
74
85
|
try:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
# Create dynamic Pydantic model for validation
|
|
84
|
-
model = create_model(
|
|
85
|
-
f"{tool_name}Result",
|
|
86
|
-
result=(return_type, ...)
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
# Validate result
|
|
90
|
-
validated = model(result=result)
|
|
91
|
-
return validated.result
|
|
92
|
-
|
|
93
|
-
except ValidationError as e:
|
|
94
|
-
raise ToolValidationError(tool_name, e.errors())
|
|
95
|
-
except Exception as e:
|
|
96
|
-
raise ToolValidationError(tool_name, {"general": str(e)})
|
|
86
|
+
return model(result=result).result
|
|
87
|
+
except ValidationError as exc:
|
|
88
|
+
raise ToolValidationError(tool_name, exc.errors()) from exc
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# --------------------------------------------------------------------------- #
|
|
92
|
+
# decorator for classic “imperative” tools
|
|
93
|
+
# --------------------------------------------------------------------------- #
|
|
97
94
|
|
|
98
95
|
|
|
99
96
|
def with_validation(cls):
|
|
100
97
|
"""
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
98
|
+
Wrap *execute* / *_execute* so that their arguments & return values
|
|
99
|
+
are type-checked each call.
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
@with_validation
|
|
103
|
+
class MyTool:
|
|
104
|
+
def execute(self, x: int, y: int) -> int:
|
|
105
|
+
return x + y
|
|
106
|
+
```
|
|
108
107
|
"""
|
|
109
|
-
original_execute = cls.execute
|
|
110
|
-
|
|
111
|
-
@wraps(original_execute)
|
|
112
|
-
def execute_with_validation(self, **kwargs):
|
|
113
|
-
# Get tool name
|
|
114
|
-
tool_name = getattr(cls, "__name__", repr(cls))
|
|
115
|
-
|
|
116
|
-
# Validate arguments
|
|
117
|
-
validated_args = validate_arguments(tool_name, original_execute, kwargs)
|
|
118
|
-
|
|
119
|
-
# Execute the tool
|
|
120
|
-
result = original_execute(self, **validated_args)
|
|
121
|
-
|
|
122
|
-
# Validate result
|
|
123
|
-
return validate_result(tool_name, original_execute, result)
|
|
124
|
-
|
|
125
|
-
cls.execute = execute_with_validation
|
|
126
|
-
return cls
|
|
127
108
|
|
|
109
|
+
# Which method did the user provide?
|
|
110
|
+
fn_name = "_execute" if hasattr(cls, "_execute") else "execute"
|
|
111
|
+
original = getattr(cls, fn_name)
|
|
128
112
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
class Result(BaseModel):
|
|
140
|
-
sum: int
|
|
141
|
-
|
|
142
|
-
def execute(self, x: int, y: int) -> Result:
|
|
143
|
-
return self.Result(sum=x + y)
|
|
144
|
-
"""
|
|
145
|
-
class Arguments(BaseModel):
|
|
146
|
-
"""Base arguments model to be overridden by subclasses."""
|
|
147
|
-
pass
|
|
148
|
-
|
|
149
|
-
class Result(BaseModel):
|
|
150
|
-
"""Base result model to be overridden by subclasses."""
|
|
151
|
-
pass
|
|
152
|
-
|
|
153
|
-
def execute(self, **kwargs) -> Any:
|
|
154
|
-
"""
|
|
155
|
-
Execute the tool with validated arguments.
|
|
156
|
-
|
|
157
|
-
Args:
|
|
158
|
-
**kwargs: Arguments to validate against Arguments model.
|
|
159
|
-
|
|
160
|
-
Returns:
|
|
161
|
-
Validated result according to Result model.
|
|
162
|
-
|
|
163
|
-
Raises:
|
|
164
|
-
ToolValidationError: If validation fails.
|
|
165
|
-
"""
|
|
166
|
-
try:
|
|
167
|
-
# Validate arguments
|
|
168
|
-
validated_args = self.Arguments(**kwargs)
|
|
169
|
-
|
|
170
|
-
# Execute implementation
|
|
171
|
-
result = self._execute(**validated_args.dict())
|
|
172
|
-
|
|
173
|
-
# Validate result if it's not already a Result instance
|
|
174
|
-
if not isinstance(result, self.Result):
|
|
175
|
-
result = self.Result(**result if isinstance(result, dict) else {"value": result})
|
|
176
|
-
|
|
177
|
-
return result
|
|
178
|
-
|
|
179
|
-
except ValidationError as e:
|
|
180
|
-
raise ToolValidationError(self.__class__.__name__, e.errors())
|
|
181
|
-
|
|
182
|
-
def _execute(self, **kwargs) -> Any:
|
|
183
|
-
"""
|
|
184
|
-
Implementation method to be overridden by subclasses.
|
|
185
|
-
|
|
186
|
-
Args:
|
|
187
|
-
**kwargs: Validated arguments.
|
|
188
|
-
|
|
189
|
-
Returns:
|
|
190
|
-
Result that will be validated against Result model.
|
|
191
|
-
"""
|
|
192
|
-
raise NotImplementedError("Subclasses must implement _execute")
|
|
113
|
+
@wraps(original)
|
|
114
|
+
def _validated(self, **kwargs):
|
|
115
|
+
name = cls.__name__
|
|
116
|
+
kwargs = validate_arguments(name, original, kwargs)
|
|
117
|
+
res = original(self, **kwargs)
|
|
118
|
+
return validate_result(name, original, res)
|
|
119
|
+
|
|
120
|
+
setattr(cls, fn_name, _validated)
|
|
121
|
+
return cls
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chuk-tool-processor
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: dotenv>=0.9.9
|
|
8
|
+
Requires-Dist: openai>=1.76.0
|
|
7
9
|
Requires-Dist: pydantic>=2.11.3
|
|
10
|
+
Requires-Dist: uuid>=1.30
|
|
8
11
|
|
|
9
12
|
# CHUK Tool Processor
|
|
10
13
|
|
|
@@ -222,7 +225,7 @@ plugin_registry.register_plugin("parser", "BracketToolParser", BracketToolParser
|
|
|
222
225
|
### Structured Logging
|
|
223
226
|
|
|
224
227
|
```python
|
|
225
|
-
from chuk_tool_processor.
|
|
228
|
+
from chuk_tool_processor.logging import get_logger, log_context_span
|
|
226
229
|
|
|
227
230
|
logger = get_logger("my_module")
|
|
228
231
|
|