chuk-tool-processor 0.1.6__py3-none-any.whl → 0.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.
Potentially problematic release.
This version of chuk-tool-processor might be problematic. Click here for more details.
- chuk_tool_processor/core/processor.py +345 -132
- chuk_tool_processor/execution/strategies/inprocess_strategy.py +522 -71
- chuk_tool_processor/execution/strategies/subprocess_strategy.py +559 -64
- chuk_tool_processor/execution/tool_executor.py +282 -24
- chuk_tool_processor/execution/wrappers/caching.py +465 -123
- chuk_tool_processor/execution/wrappers/rate_limiting.py +199 -86
- chuk_tool_processor/execution/wrappers/retry.py +133 -23
- chuk_tool_processor/logging/__init__.py +83 -10
- chuk_tool_processor/logging/context.py +218 -22
- chuk_tool_processor/logging/formatter.py +56 -13
- chuk_tool_processor/logging/helpers.py +91 -16
- chuk_tool_processor/logging/metrics.py +75 -6
- chuk_tool_processor/mcp/mcp_tool.py +80 -35
- chuk_tool_processor/mcp/register_mcp_tools.py +74 -56
- chuk_tool_processor/mcp/setup_mcp_sse.py +41 -36
- chuk_tool_processor/mcp/setup_mcp_stdio.py +39 -37
- chuk_tool_processor/mcp/transport/sse_transport.py +351 -105
- chuk_tool_processor/models/execution_strategy.py +52 -3
- chuk_tool_processor/models/streaming_tool.py +110 -0
- chuk_tool_processor/models/tool_call.py +56 -4
- chuk_tool_processor/models/tool_result.py +115 -9
- chuk_tool_processor/models/validated_tool.py +15 -13
- chuk_tool_processor/plugins/discovery.py +115 -70
- chuk_tool_processor/plugins/parsers/base.py +13 -5
- chuk_tool_processor/plugins/parsers/{function_call_tool_plugin.py → function_call_tool.py} +39 -20
- chuk_tool_processor/plugins/parsers/json_tool.py +50 -0
- chuk_tool_processor/plugins/parsers/openai_tool.py +88 -0
- chuk_tool_processor/plugins/parsers/xml_tool.py +74 -20
- chuk_tool_processor/registry/__init__.py +46 -7
- chuk_tool_processor/registry/auto_register.py +92 -28
- chuk_tool_processor/registry/decorators.py +134 -11
- chuk_tool_processor/registry/interface.py +48 -14
- chuk_tool_processor/registry/metadata.py +52 -6
- chuk_tool_processor/registry/provider.py +75 -36
- chuk_tool_processor/registry/providers/__init__.py +49 -10
- chuk_tool_processor/registry/providers/memory.py +59 -48
- chuk_tool_processor/registry/tool_export.py +208 -39
- chuk_tool_processor/utils/validation.py +18 -13
- chuk_tool_processor-0.2.dist-info/METADATA +401 -0
- chuk_tool_processor-0.2.dist-info/RECORD +58 -0
- {chuk_tool_processor-0.1.6.dist-info → chuk_tool_processor-0.2.dist-info}/WHEEL +1 -1
- chuk_tool_processor/plugins/parsers/json_tool_plugin.py +0 -38
- chuk_tool_processor/plugins/parsers/openai_tool_plugin.py +0 -76
- chuk_tool_processor-0.1.6.dist-info/METADATA +0 -462
- chuk_tool_processor-0.1.6.dist-info/RECORD +0 -57
- {chuk_tool_processor-0.1.6.dist-info → chuk_tool_processor-0.2.dist-info}/top_level.txt +0 -0
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# chuk_tool_processor/registry/providers/memory.py
|
|
2
|
-
# chuk_tool_processor/registry/providers/memory.py
|
|
3
2
|
"""
|
|
4
|
-
In-memory implementation of the tool registry.
|
|
3
|
+
In-memory implementation of the asynchronous tool registry.
|
|
5
4
|
"""
|
|
6
|
-
|
|
7
5
|
from __future__ import annotations
|
|
8
6
|
|
|
7
|
+
import asyncio
|
|
9
8
|
import inspect
|
|
10
9
|
from typing import Any, Dict, List, Optional, Tuple
|
|
11
10
|
|
|
@@ -16,9 +15,10 @@ from chuk_tool_processor.registry.metadata import ToolMetadata
|
|
|
16
15
|
|
|
17
16
|
class InMemoryToolRegistry(ToolRegistryInterface):
|
|
18
17
|
"""
|
|
19
|
-
In-memory implementation of ToolRegistryInterface with namespace support.
|
|
18
|
+
In-memory implementation of the async ToolRegistryInterface with namespace support.
|
|
20
19
|
|
|
21
20
|
Suitable for single-process apps or tests; not persisted across processes.
|
|
21
|
+
Thread-safe with asyncio locking.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
24
|
# ------------------------------------------------------------------ #
|
|
@@ -30,72 +30,80 @@ class InMemoryToolRegistry(ToolRegistryInterface):
|
|
|
30
30
|
self._tools: Dict[str, Dict[str, Any]] = {}
|
|
31
31
|
# {namespace: {tool_name: ToolMetadata}}
|
|
32
32
|
self._metadata: Dict[str, Dict[str, ToolMetadata]] = {}
|
|
33
|
+
# Lock for thread safety
|
|
34
|
+
self._lock = asyncio.Lock()
|
|
33
35
|
|
|
34
36
|
# ------------------------------------------------------------------ #
|
|
35
37
|
# registration
|
|
36
38
|
# ------------------------------------------------------------------ #
|
|
37
39
|
|
|
38
|
-
def register_tool(
|
|
40
|
+
async def register_tool(
|
|
39
41
|
self,
|
|
40
42
|
tool: Any,
|
|
41
43
|
name: Optional[str] = None,
|
|
42
44
|
namespace: str = "default",
|
|
43
45
|
metadata: Optional[Dict[str, Any]] = None,
|
|
44
46
|
) -> None:
|
|
45
|
-
|
|
46
|
-
self.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
47
|
+
"""Register a tool in the registry asynchronously."""
|
|
48
|
+
async with self._lock:
|
|
49
|
+
# ensure namespace buckets
|
|
50
|
+
self._tools.setdefault(namespace, {})
|
|
51
|
+
self._metadata.setdefault(namespace, {})
|
|
52
|
+
|
|
53
|
+
key = name or getattr(tool, "__name__", None) or repr(tool)
|
|
54
|
+
self._tools[namespace][key] = tool
|
|
55
|
+
|
|
56
|
+
# build metadata -------------------------------------------------
|
|
57
|
+
is_async = inspect.iscoroutinefunction(getattr(tool, "execute", None))
|
|
58
|
+
|
|
59
|
+
# default description -> docstring
|
|
60
|
+
description = (
|
|
61
|
+
(inspect.getdoc(tool) or "").strip()
|
|
62
|
+
if not (metadata and "description" in metadata)
|
|
63
|
+
else None
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
meta_dict: Dict[str, Any] = {
|
|
67
|
+
"name": key,
|
|
68
|
+
"namespace": namespace,
|
|
69
|
+
"is_async": is_async,
|
|
70
|
+
}
|
|
71
|
+
if description:
|
|
72
|
+
meta_dict["description"] = description
|
|
73
|
+
if metadata:
|
|
74
|
+
meta_dict.update(metadata)
|
|
75
|
+
|
|
76
|
+
self._metadata[namespace][key] = ToolMetadata(**meta_dict)
|
|
73
77
|
|
|
74
78
|
# ------------------------------------------------------------------ #
|
|
75
79
|
# retrieval
|
|
76
80
|
# ------------------------------------------------------------------ #
|
|
77
81
|
|
|
78
|
-
def get_tool(self, name: str, namespace: str = "default") -> Optional[Any]:
|
|
82
|
+
async def get_tool(self, name: str, namespace: str = "default") -> Optional[Any]:
|
|
83
|
+
"""Retrieve a tool by name and namespace asynchronously."""
|
|
84
|
+
# Read operations don't need locking for better concurrency
|
|
79
85
|
return self._tools.get(namespace, {}).get(name)
|
|
80
86
|
|
|
81
|
-
def get_tool_strict(self, name: str, namespace: str = "default") -> Any:
|
|
82
|
-
tool
|
|
87
|
+
async def get_tool_strict(self, name: str, namespace: str = "default") -> Any:
|
|
88
|
+
"""Get a tool with strict validation, raising if not found."""
|
|
89
|
+
tool = await self.get_tool(name, namespace)
|
|
83
90
|
if tool is None:
|
|
84
91
|
raise ToolNotFoundError(f"{namespace}.{name}")
|
|
85
92
|
return tool
|
|
86
93
|
|
|
87
|
-
def get_metadata(
|
|
94
|
+
async def get_metadata(
|
|
88
95
|
self, name: str, namespace: str = "default"
|
|
89
96
|
) -> Optional[ToolMetadata]:
|
|
97
|
+
"""Get metadata for a tool asynchronously."""
|
|
90
98
|
return self._metadata.get(namespace, {}).get(name)
|
|
91
99
|
|
|
92
100
|
# ------------------------------------------------------------------ #
|
|
93
101
|
# listing helpers
|
|
94
102
|
# ------------------------------------------------------------------ #
|
|
95
103
|
|
|
96
|
-
def list_tools(self, namespace: Optional[str] = None) -> List[Tuple[str, str]]:
|
|
104
|
+
async def list_tools(self, namespace: Optional[str] = None) -> List[Tuple[str, str]]:
|
|
97
105
|
"""
|
|
98
|
-
Return a list of ``(namespace, name)`` tuples.
|
|
106
|
+
Return a list of ``(namespace, name)`` tuples asynchronously.
|
|
99
107
|
"""
|
|
100
108
|
if namespace:
|
|
101
109
|
return [
|
|
@@ -107,18 +115,21 @@ class InMemoryToolRegistry(ToolRegistryInterface):
|
|
|
107
115
|
result.extend((ns, n) for n in tools.keys())
|
|
108
116
|
return result
|
|
109
117
|
|
|
110
|
-
def list_namespaces(self) -> List[str]:
|
|
118
|
+
async def list_namespaces(self) -> List[str]:
|
|
119
|
+
"""List all namespaces asynchronously."""
|
|
111
120
|
return list(self._tools.keys())
|
|
112
121
|
|
|
113
|
-
def list_metadata(self, namespace: str
|
|
122
|
+
async def list_metadata(self, namespace: Optional[str] = None) -> List[ToolMetadata]:
|
|
114
123
|
"""
|
|
115
|
-
Return
|
|
124
|
+
Return all ToolMetadata objects asynchronously.
|
|
116
125
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
Args:
|
|
127
|
+
namespace: Optional filter by namespace.
|
|
128
|
+
• None (default) – metadata from all namespaces
|
|
129
|
+
• "some_ns" – only that namespace
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
List of ToolMetadata objects.
|
|
122
133
|
"""
|
|
123
134
|
if namespace is not None:
|
|
124
135
|
return list(self._metadata.get(namespace, {}).values())
|
|
@@ -127,4 +138,4 @@ class InMemoryToolRegistry(ToolRegistryInterface):
|
|
|
127
138
|
result: List[ToolMetadata] = []
|
|
128
139
|
for ns_meta in self._metadata.values():
|
|
129
140
|
result.extend(ns_meta.values())
|
|
130
|
-
return result
|
|
141
|
+
return result
|
|
@@ -1,46 +1,77 @@
|
|
|
1
1
|
# chuk_tool_processor/registry/tool_export.py
|
|
2
2
|
"""
|
|
3
|
-
|
|
3
|
+
Async helpers that expose all registered tools in various formats and
|
|
4
4
|
translate an OpenAI `function.name` back to the matching tool.
|
|
5
5
|
"""
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import asyncio
|
|
9
|
+
from typing import Dict, List, Any, Optional, Mapping
|
|
9
10
|
|
|
11
|
+
# registry
|
|
10
12
|
from .provider import ToolRegistryProvider
|
|
11
13
|
|
|
12
14
|
# --------------------------------------------------------------------------- #
|
|
13
|
-
# internal cache so tool-name lookup is O(1)
|
|
15
|
+
# internal cache so tool-name lookup is O(1) with async protection
|
|
14
16
|
# --------------------------------------------------------------------------- #
|
|
15
|
-
_OPENAI_NAME_CACHE:
|
|
17
|
+
_OPENAI_NAME_CACHE: Optional[Dict[str, Any]] = None
|
|
18
|
+
_CACHE_LOCK = asyncio.Lock()
|
|
16
19
|
|
|
17
20
|
|
|
18
|
-
def _build_openai_name_cache() -> None:
|
|
19
|
-
"""
|
|
21
|
+
async def _build_openai_name_cache() -> None:
|
|
22
|
+
"""
|
|
23
|
+
Populate the global reverse-lookup table once asynchronously.
|
|
24
|
+
|
|
25
|
+
This function is thread-safe and will only build the cache once,
|
|
26
|
+
even with concurrent calls.
|
|
27
|
+
"""
|
|
20
28
|
global _OPENAI_NAME_CACHE
|
|
21
|
-
|
|
29
|
+
|
|
30
|
+
# Fast path - cache already exists
|
|
31
|
+
if _OPENAI_NAME_CACHE is not None:
|
|
22
32
|
return
|
|
23
33
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
#
|
|
31
|
-
_OPENAI_NAME_CACHE
|
|
32
|
-
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
#
|
|
37
|
-
|
|
34
|
+
# Slow path - build the cache with proper locking
|
|
35
|
+
async with _CACHE_LOCK:
|
|
36
|
+
# Double-check pattern: check again after acquiring the lock
|
|
37
|
+
if _OPENAI_NAME_CACHE is not None:
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
# Initialize an empty cache
|
|
41
|
+
_OPENAI_NAME_CACHE = {}
|
|
42
|
+
|
|
43
|
+
# Get the registry
|
|
44
|
+
reg = await ToolRegistryProvider.get_registry()
|
|
45
|
+
|
|
46
|
+
# Get all tools and their names
|
|
47
|
+
tools_list = await reg.list_tools()
|
|
48
|
+
|
|
49
|
+
for ns, key in tools_list:
|
|
50
|
+
# Get the tool
|
|
51
|
+
tool = await reg.get_tool(key, ns)
|
|
52
|
+
if tool is None:
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
# ▸ registry key -> tool
|
|
56
|
+
_OPENAI_NAME_CACHE[key] = tool
|
|
57
|
+
|
|
58
|
+
# ▸ class name -> tool (legacy)
|
|
59
|
+
_OPENAI_NAME_CACHE[tool.__class__.__name__] = tool
|
|
60
|
+
|
|
61
|
+
# ▸ OpenAI name -> tool (may differ from both above)
|
|
62
|
+
try:
|
|
63
|
+
openai_spec = tool.to_openai()
|
|
64
|
+
openai_name = openai_spec["function"]["name"]
|
|
65
|
+
_OPENAI_NAME_CACHE[openai_name] = tool
|
|
66
|
+
except (AttributeError, KeyError, TypeError):
|
|
67
|
+
# Skip tools that don't have proper OpenAI specs
|
|
68
|
+
pass
|
|
38
69
|
|
|
39
70
|
|
|
40
71
|
# --------------------------------------------------------------------------- #
|
|
41
72
|
# public helpers
|
|
42
73
|
# --------------------------------------------------------------------------- #
|
|
43
|
-
def openai_functions() -> List[Dict]:
|
|
74
|
+
async def openai_functions() -> List[Dict]:
|
|
44
75
|
"""
|
|
45
76
|
Return **all** registered tools in the exact schema the Chat-Completions
|
|
46
77
|
API expects in its ``tools=[ … ]`` parameter.
|
|
@@ -48,29 +79,167 @@ def openai_functions() -> List[Dict]:
|
|
|
48
79
|
The ``function.name`` is always the *registry key* so that the round-trip
|
|
49
80
|
(export → model → parser) stays consistent even when the class name and
|
|
50
81
|
the registered key differ.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
List of OpenAI function specifications
|
|
51
85
|
"""
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
86
|
+
# Get the registry
|
|
87
|
+
reg = await ToolRegistryProvider.get_registry()
|
|
88
|
+
specs: List[Dict[str, Any]] = []
|
|
89
|
+
|
|
90
|
+
# List all tools
|
|
91
|
+
tools_list = await reg.list_tools()
|
|
92
|
+
|
|
93
|
+
for ns, key in tools_list:
|
|
94
|
+
# Get each tool
|
|
95
|
+
tool = await reg.get_tool(key, ns)
|
|
96
|
+
if tool is None:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
# Get the OpenAI spec
|
|
101
|
+
spec = tool.to_openai()
|
|
102
|
+
# Override the name to ensure round-trip consistency
|
|
103
|
+
spec["function"]["name"] = key
|
|
104
|
+
specs.append(spec)
|
|
105
|
+
except (AttributeError, TypeError):
|
|
106
|
+
# Skip tools that don't support OpenAI format
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
# Ensure the cache is built
|
|
110
|
+
await _build_openai_name_cache()
|
|
63
111
|
return specs
|
|
64
112
|
|
|
65
113
|
|
|
66
|
-
def tool_by_openai_name(name: str):
|
|
114
|
+
async def tool_by_openai_name(name: str) -> Any:
|
|
67
115
|
"""
|
|
68
|
-
Map an OpenAI ``function.name`` back to the registered tool.
|
|
69
|
-
|
|
70
|
-
|
|
116
|
+
Map an OpenAI ``function.name`` back to the registered tool asynchronously.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
name: The OpenAI function name
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
The tool implementation
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
KeyError: If the name is unknown
|
|
71
126
|
"""
|
|
72
|
-
|
|
127
|
+
# Ensure the cache is built
|
|
128
|
+
await _build_openai_name_cache()
|
|
129
|
+
|
|
130
|
+
# Look up the tool
|
|
73
131
|
try:
|
|
74
|
-
|
|
132
|
+
if _OPENAI_NAME_CACHE is None:
|
|
133
|
+
raise KeyError(f"Tool cache not initialized")
|
|
134
|
+
|
|
135
|
+
return _OPENAI_NAME_CACHE[name]
|
|
75
136
|
except (KeyError, TypeError):
|
|
76
137
|
raise KeyError(f"No tool registered for OpenAI name {name!r}") from None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
async def clear_name_cache() -> None:
|
|
141
|
+
"""
|
|
142
|
+
Clear the OpenAI name cache.
|
|
143
|
+
|
|
144
|
+
This is useful in tests or when the registry changes significantly.
|
|
145
|
+
"""
|
|
146
|
+
global _OPENAI_NAME_CACHE
|
|
147
|
+
async with _CACHE_LOCK:
|
|
148
|
+
_OPENAI_NAME_CACHE = None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
async def export_tools_as_openapi(
|
|
152
|
+
title: str = "Tool API",
|
|
153
|
+
version: str = "1.0.0",
|
|
154
|
+
description: str = "API for registered tools",
|
|
155
|
+
) -> Dict[str, Any]:
|
|
156
|
+
"""
|
|
157
|
+
Export all registered tools as an OpenAPI specification.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
title: API title
|
|
161
|
+
version: API version
|
|
162
|
+
description: API description
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
OpenAPI specification as a dictionary
|
|
166
|
+
"""
|
|
167
|
+
# Get the registry
|
|
168
|
+
reg = await ToolRegistryProvider.get_registry()
|
|
169
|
+
|
|
170
|
+
# Build paths and components
|
|
171
|
+
paths: Dict[str, Any] = {}
|
|
172
|
+
schemas: Dict[str, Any] = {}
|
|
173
|
+
|
|
174
|
+
# List all tools
|
|
175
|
+
tools_list = await reg.list_tools()
|
|
176
|
+
|
|
177
|
+
for ns, key in tools_list:
|
|
178
|
+
# Get tool and metadata
|
|
179
|
+
tool = await reg.get_tool(key, ns)
|
|
180
|
+
metadata = await reg.get_metadata(key, ns)
|
|
181
|
+
|
|
182
|
+
if tool is None or metadata is None:
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
# Create path
|
|
186
|
+
path = f"/{ns}/{key}"
|
|
187
|
+
|
|
188
|
+
# Get schemas from tool if available
|
|
189
|
+
arg_schema = None
|
|
190
|
+
result_schema = None
|
|
191
|
+
|
|
192
|
+
if hasattr(tool, "Arguments") and hasattr(tool.Arguments, "model_json_schema"):
|
|
193
|
+
arg_schema = tool.Arguments.model_json_schema()
|
|
194
|
+
schemas[f"{key}Args"] = arg_schema
|
|
195
|
+
|
|
196
|
+
if hasattr(tool, "Result") and hasattr(tool.Result, "model_json_schema"):
|
|
197
|
+
result_schema = tool.Result.model_json_schema()
|
|
198
|
+
schemas[f"{key}Result"] = result_schema
|
|
199
|
+
|
|
200
|
+
# Add path
|
|
201
|
+
paths[path] = {
|
|
202
|
+
"post": {
|
|
203
|
+
"summary": metadata.description or f"Execute {key}",
|
|
204
|
+
"operationId": f"execute_{ns}_{key}",
|
|
205
|
+
"tags": [ns],
|
|
206
|
+
"requestBody": {
|
|
207
|
+
"required": True,
|
|
208
|
+
"content": {
|
|
209
|
+
"application/json": {
|
|
210
|
+
"schema": {"$ref": f"#/components/schemas/{key}Args"} if arg_schema else {}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
"responses": {
|
|
215
|
+
"200": {
|
|
216
|
+
"description": "Successful operation",
|
|
217
|
+
"content": {
|
|
218
|
+
"application/json": {
|
|
219
|
+
"schema": {"$ref": f"#/components/schemas/{key}Result"} if result_schema else {}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
"400": {
|
|
224
|
+
"description": "Bad request"
|
|
225
|
+
},
|
|
226
|
+
"500": {
|
|
227
|
+
"description": "Internal server error"
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
# Build the OpenAPI spec
|
|
234
|
+
return {
|
|
235
|
+
"openapi": "3.0.0",
|
|
236
|
+
"info": {
|
|
237
|
+
"title": title,
|
|
238
|
+
"version": version,
|
|
239
|
+
"description": description
|
|
240
|
+
},
|
|
241
|
+
"paths": paths,
|
|
242
|
+
"components": {
|
|
243
|
+
"schemas": schemas
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# chuk_tool_processor/utils/validation.py
|
|
2
2
|
"""
|
|
3
|
-
|
|
3
|
+
Async runtime helpers for validating tool inputs / outputs with Pydantic.
|
|
4
4
|
|
|
5
5
|
Public API
|
|
6
6
|
----------
|
|
@@ -10,11 +10,12 @@ validate_result(tool_name, fn, result) -> Any
|
|
|
10
10
|
"""
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
import inspect
|
|
13
|
+
import asyncio
|
|
13
14
|
from functools import lru_cache, wraps
|
|
14
|
-
from typing import Any, Callable, Dict, get_type_hints
|
|
15
|
+
from typing import Any, Callable, Dict, get_type_hints, Awaitable
|
|
15
16
|
from pydantic import BaseModel, ValidationError, create_model, Extra
|
|
16
17
|
|
|
17
|
-
#
|
|
18
|
+
# exception
|
|
18
19
|
from chuk_tool_processor.core.exceptions import ToolValidationError
|
|
19
20
|
|
|
20
21
|
__all__ = [
|
|
@@ -66,19 +67,21 @@ def _result_model(tool_name: str, fn: Callable) -> type[BaseModel] | None:
|
|
|
66
67
|
|
|
67
68
|
|
|
68
69
|
# --------------------------------------------------------------------------- #
|
|
69
|
-
# public validation helpers
|
|
70
|
+
# public validation helpers - synced with async patterns
|
|
70
71
|
# --------------------------------------------------------------------------- #
|
|
71
72
|
|
|
72
73
|
|
|
73
74
|
def validate_arguments(tool_name: str, fn: Callable, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
75
|
+
"""Validate function arguments against type hints."""
|
|
74
76
|
try:
|
|
75
77
|
model = _arg_model(tool_name, fn)
|
|
76
|
-
return model(**args).
|
|
78
|
+
return model(**args).model_dump()
|
|
77
79
|
except ValidationError as exc:
|
|
78
80
|
raise ToolValidationError(tool_name, exc.errors()) from exc
|
|
79
81
|
|
|
80
82
|
|
|
81
83
|
def validate_result(tool_name: str, fn: Callable, result: Any) -> Any:
|
|
84
|
+
"""Validate function return value against return type hint."""
|
|
82
85
|
model = _result_model(tool_name, fn)
|
|
83
86
|
if model is None: # no annotation ⇒ no validation
|
|
84
87
|
return result
|
|
@@ -89,33 +92,35 @@ def validate_result(tool_name: str, fn: Callable, result: Any) -> Any:
|
|
|
89
92
|
|
|
90
93
|
|
|
91
94
|
# --------------------------------------------------------------------------- #
|
|
92
|
-
# decorator for classic
|
|
95
|
+
# decorator for classic "imperative" tools - now requires async
|
|
93
96
|
# --------------------------------------------------------------------------- #
|
|
94
97
|
|
|
95
98
|
|
|
96
99
|
def with_validation(cls):
|
|
97
100
|
"""
|
|
98
|
-
Wrap
|
|
99
|
-
are type-checked each call.
|
|
101
|
+
Wrap an async *execute* method with argument & result validation.
|
|
100
102
|
|
|
101
103
|
```
|
|
102
104
|
@with_validation
|
|
103
105
|
class MyTool:
|
|
104
|
-
def execute(self, x: int, y: int) -> int:
|
|
106
|
+
async def execute(self, x: int, y: int) -> int:
|
|
105
107
|
return x + y
|
|
106
108
|
```
|
|
107
109
|
"""
|
|
108
|
-
|
|
109
110
|
# Which method did the user provide?
|
|
110
111
|
fn_name = "_execute" if hasattr(cls, "_execute") else "execute"
|
|
111
112
|
original = getattr(cls, fn_name)
|
|
113
|
+
|
|
114
|
+
# Ensure the method is async
|
|
115
|
+
if not inspect.iscoroutinefunction(original):
|
|
116
|
+
raise TypeError(f"Tool {cls.__name__} must have an async {fn_name} method")
|
|
112
117
|
|
|
113
118
|
@wraps(original)
|
|
114
|
-
def _validated(self, **kwargs):
|
|
119
|
+
async def _validated(self, **kwargs):
|
|
115
120
|
name = cls.__name__
|
|
116
121
|
kwargs = validate_arguments(name, original, kwargs)
|
|
117
|
-
res = original(self, **kwargs)
|
|
122
|
+
res = await original(self, **kwargs)
|
|
118
123
|
return validate_result(name, original, res)
|
|
119
124
|
|
|
120
125
|
setattr(cls, fn_name, _validated)
|
|
121
|
-
return cls
|
|
126
|
+
return cls
|