chuk-tool-processor 0.6.4__py3-none-any.whl → 0.9.7__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/__init__.py +32 -1
- chuk_tool_processor/core/exceptions.py +225 -13
- chuk_tool_processor/core/processor.py +135 -104
- chuk_tool_processor/execution/strategies/__init__.py +6 -0
- chuk_tool_processor/execution/strategies/inprocess_strategy.py +142 -150
- chuk_tool_processor/execution/strategies/subprocess_strategy.py +202 -206
- chuk_tool_processor/execution/tool_executor.py +82 -84
- chuk_tool_processor/execution/wrappers/__init__.py +42 -0
- chuk_tool_processor/execution/wrappers/caching.py +150 -116
- chuk_tool_processor/execution/wrappers/circuit_breaker.py +370 -0
- chuk_tool_processor/execution/wrappers/rate_limiting.py +76 -43
- chuk_tool_processor/execution/wrappers/retry.py +116 -78
- chuk_tool_processor/logging/__init__.py +23 -17
- chuk_tool_processor/logging/context.py +40 -45
- chuk_tool_processor/logging/formatter.py +22 -21
- chuk_tool_processor/logging/helpers.py +28 -42
- chuk_tool_processor/logging/metrics.py +13 -15
- chuk_tool_processor/mcp/__init__.py +8 -12
- chuk_tool_processor/mcp/mcp_tool.py +158 -114
- chuk_tool_processor/mcp/register_mcp_tools.py +22 -22
- chuk_tool_processor/mcp/setup_mcp_http_streamable.py +57 -17
- chuk_tool_processor/mcp/setup_mcp_sse.py +57 -17
- chuk_tool_processor/mcp/setup_mcp_stdio.py +11 -11
- chuk_tool_processor/mcp/stream_manager.py +333 -276
- chuk_tool_processor/mcp/transport/__init__.py +22 -29
- chuk_tool_processor/mcp/transport/base_transport.py +180 -44
- chuk_tool_processor/mcp/transport/http_streamable_transport.py +505 -325
- chuk_tool_processor/mcp/transport/models.py +100 -0
- chuk_tool_processor/mcp/transport/sse_transport.py +607 -276
- chuk_tool_processor/mcp/transport/stdio_transport.py +597 -116
- chuk_tool_processor/models/__init__.py +21 -1
- chuk_tool_processor/models/execution_strategy.py +16 -21
- chuk_tool_processor/models/streaming_tool.py +28 -25
- chuk_tool_processor/models/tool_call.py +49 -31
- chuk_tool_processor/models/tool_export_mixin.py +22 -8
- chuk_tool_processor/models/tool_result.py +40 -77
- chuk_tool_processor/models/tool_spec.py +350 -0
- chuk_tool_processor/models/validated_tool.py +36 -18
- chuk_tool_processor/observability/__init__.py +30 -0
- chuk_tool_processor/observability/metrics.py +312 -0
- chuk_tool_processor/observability/setup.py +105 -0
- chuk_tool_processor/observability/tracing.py +345 -0
- chuk_tool_processor/plugins/__init__.py +1 -1
- chuk_tool_processor/plugins/discovery.py +11 -11
- chuk_tool_processor/plugins/parsers/__init__.py +1 -1
- chuk_tool_processor/plugins/parsers/base.py +1 -2
- chuk_tool_processor/plugins/parsers/function_call_tool.py +13 -8
- chuk_tool_processor/plugins/parsers/json_tool.py +4 -3
- chuk_tool_processor/plugins/parsers/openai_tool.py +12 -7
- chuk_tool_processor/plugins/parsers/xml_tool.py +4 -4
- chuk_tool_processor/registry/__init__.py +12 -12
- chuk_tool_processor/registry/auto_register.py +22 -30
- chuk_tool_processor/registry/decorators.py +127 -129
- chuk_tool_processor/registry/interface.py +26 -23
- chuk_tool_processor/registry/metadata.py +27 -22
- chuk_tool_processor/registry/provider.py +17 -18
- chuk_tool_processor/registry/providers/__init__.py +16 -19
- chuk_tool_processor/registry/providers/memory.py +18 -25
- chuk_tool_processor/registry/tool_export.py +42 -51
- chuk_tool_processor/utils/validation.py +15 -16
- chuk_tool_processor-0.9.7.dist-info/METADATA +1813 -0
- chuk_tool_processor-0.9.7.dist-info/RECORD +67 -0
- chuk_tool_processor-0.6.4.dist-info/METADATA +0 -697
- chuk_tool_processor-0.6.4.dist-info/RECORD +0 -60
- {chuk_tool_processor-0.6.4.dist-info → chuk_tool_processor-0.9.7.dist-info}/WHEEL +0 -0
- {chuk_tool_processor-0.6.4.dist-info → chuk_tool_processor-0.9.7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# chuk_tool_processor/models/tool_spec.py
|
|
2
|
+
"""
|
|
3
|
+
Formal tool specification with JSON Schema export, versioning, and capability discovery.
|
|
4
|
+
|
|
5
|
+
This module provides a unified way to describe tools with their:
|
|
6
|
+
- Input/output schemas (JSON Schema)
|
|
7
|
+
- Versioning information
|
|
8
|
+
- Capabilities (streaming, cancellable, idempotent, etc.)
|
|
9
|
+
- Export to various formats (OpenAI, MCP, Anthropic)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import inspect
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ToolCapability(str, Enum):
|
|
23
|
+
"""Capabilities that a tool can support."""
|
|
24
|
+
|
|
25
|
+
STREAMING = "streaming" # Tool supports streaming responses
|
|
26
|
+
CANCELLABLE = "cancellable" # Tool supports cancellation
|
|
27
|
+
IDEMPOTENT = "idempotent" # Tool is safe to retry (same result)
|
|
28
|
+
CACHEABLE = "cacheable" # Results can be cached
|
|
29
|
+
RATE_LIMITED = "rate_limited" # Tool has rate limits
|
|
30
|
+
REQUIRES_AUTH = "requires_auth" # Tool requires authentication
|
|
31
|
+
LONG_RUNNING = "long_running" # Tool may take >30s
|
|
32
|
+
STATEFUL = "stateful" # Tool maintains state across calls
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ToolSpec(BaseModel):
|
|
36
|
+
"""
|
|
37
|
+
Formal tool specification with JSON Schema export and versioning.
|
|
38
|
+
|
|
39
|
+
This provides a complete description of a tool's interface, capabilities,
|
|
40
|
+
and metadata for discovery and validation.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
# Core metadata
|
|
44
|
+
name: str = Field(..., description="Tool name (must be unique within namespace)")
|
|
45
|
+
version: str = Field(default="1.0.0", description="Semantic version (e.g., '1.2.3')")
|
|
46
|
+
description: str = Field(..., description="Human-readable description of what the tool does")
|
|
47
|
+
namespace: str = Field(default="default", description="Namespace for organizing tools")
|
|
48
|
+
|
|
49
|
+
# Schema definitions
|
|
50
|
+
parameters: dict[str, Any] = Field(
|
|
51
|
+
...,
|
|
52
|
+
description="JSON Schema for tool parameters (input)",
|
|
53
|
+
)
|
|
54
|
+
returns: dict[str, Any] | None = Field(
|
|
55
|
+
None,
|
|
56
|
+
description="JSON Schema for return value (output). None if unstructured.",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Capabilities and metadata
|
|
60
|
+
capabilities: list[ToolCapability] = Field(
|
|
61
|
+
default_factory=list,
|
|
62
|
+
description="List of capabilities this tool supports",
|
|
63
|
+
)
|
|
64
|
+
tags: list[str] = Field(
|
|
65
|
+
default_factory=list,
|
|
66
|
+
description="Tags for categorization (e.g., ['search', 'web'])",
|
|
67
|
+
)
|
|
68
|
+
examples: list[dict[str, Any]] = Field(
|
|
69
|
+
default_factory=list,
|
|
70
|
+
description="Example input/output pairs for documentation",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Optional metadata
|
|
74
|
+
author: str | None = Field(None, description="Tool author/maintainer")
|
|
75
|
+
license: str | None = Field(None, description="License (e.g., 'MIT', 'Apache-2.0')")
|
|
76
|
+
documentation_url: str | None = Field(None, description="Link to full documentation")
|
|
77
|
+
source_url: str | None = Field(None, description="Link to source code")
|
|
78
|
+
|
|
79
|
+
# Execution hints
|
|
80
|
+
estimated_duration_seconds: float | None = Field(
|
|
81
|
+
None,
|
|
82
|
+
description="Typical execution time in seconds (for timeout planning)",
|
|
83
|
+
)
|
|
84
|
+
max_retries: int | None = Field(
|
|
85
|
+
None,
|
|
86
|
+
description="Maximum recommended retries (None = use default)",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# ------------------------------------------------------------------ #
|
|
90
|
+
# Capability checks
|
|
91
|
+
# ------------------------------------------------------------------ #
|
|
92
|
+
def has_capability(self, capability: ToolCapability) -> bool:
|
|
93
|
+
"""Check if tool has a specific capability."""
|
|
94
|
+
return capability in self.capabilities
|
|
95
|
+
|
|
96
|
+
def is_streaming(self) -> bool:
|
|
97
|
+
"""Check if tool supports streaming."""
|
|
98
|
+
return self.has_capability(ToolCapability.STREAMING)
|
|
99
|
+
|
|
100
|
+
def is_idempotent(self) -> bool:
|
|
101
|
+
"""Check if tool is safe to retry."""
|
|
102
|
+
return self.has_capability(ToolCapability.IDEMPOTENT)
|
|
103
|
+
|
|
104
|
+
def is_cacheable(self) -> bool:
|
|
105
|
+
"""Check if results can be cached."""
|
|
106
|
+
return self.has_capability(ToolCapability.CACHEABLE)
|
|
107
|
+
|
|
108
|
+
# ------------------------------------------------------------------ #
|
|
109
|
+
# Export formats
|
|
110
|
+
# ------------------------------------------------------------------ #
|
|
111
|
+
def to_openai(self) -> dict[str, Any]:
|
|
112
|
+
"""
|
|
113
|
+
Export as OpenAI function calling format.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Dict compatible with OpenAI's tools=[...] parameter
|
|
117
|
+
"""
|
|
118
|
+
return {
|
|
119
|
+
"type": "function",
|
|
120
|
+
"function": {
|
|
121
|
+
"name": self.name,
|
|
122
|
+
"description": self.description,
|
|
123
|
+
"parameters": self.parameters,
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
def to_anthropic(self) -> dict[str, Any]:
|
|
128
|
+
"""
|
|
129
|
+
Export as Anthropic tool format.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Dict compatible with Anthropic's tools parameter
|
|
133
|
+
"""
|
|
134
|
+
return {
|
|
135
|
+
"name": self.name,
|
|
136
|
+
"description": self.description,
|
|
137
|
+
"input_schema": self.parameters,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
def to_mcp(self) -> dict[str, Any]:
|
|
141
|
+
"""
|
|
142
|
+
Export as MCP tool format.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Dict compatible with MCP tool schema
|
|
146
|
+
"""
|
|
147
|
+
result = {
|
|
148
|
+
"name": self.name,
|
|
149
|
+
"description": self.description,
|
|
150
|
+
"inputSchema": self.parameters,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# Add optional fields if present
|
|
154
|
+
if self.returns:
|
|
155
|
+
result["outputSchema"] = self.returns
|
|
156
|
+
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
def to_json_schema(self) -> dict[str, Any]:
|
|
160
|
+
"""
|
|
161
|
+
Export as pure JSON Schema (parameters only).
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
JSON Schema dict for tool parameters
|
|
165
|
+
"""
|
|
166
|
+
return self.parameters
|
|
167
|
+
|
|
168
|
+
def to_dict(self) -> dict[str, Any]:
|
|
169
|
+
"""
|
|
170
|
+
Export complete spec as dict.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Full tool specification as dictionary
|
|
174
|
+
"""
|
|
175
|
+
return self.model_dump(exclude_none=True)
|
|
176
|
+
|
|
177
|
+
# ------------------------------------------------------------------ #
|
|
178
|
+
# Factory methods
|
|
179
|
+
# ------------------------------------------------------------------ #
|
|
180
|
+
@classmethod
|
|
181
|
+
def from_validated_tool(
|
|
182
|
+
cls,
|
|
183
|
+
tool_cls: type,
|
|
184
|
+
name: str | None = None,
|
|
185
|
+
namespace: str = "default",
|
|
186
|
+
) -> ToolSpec:
|
|
187
|
+
"""
|
|
188
|
+
Create ToolSpec from a ValidatedTool class.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
tool_cls: ValidatedTool subclass
|
|
192
|
+
name: Override tool name (default: class name)
|
|
193
|
+
namespace: Tool namespace
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
ToolSpec instance
|
|
197
|
+
"""
|
|
198
|
+
from chuk_tool_processor.models.validated_tool import ValidatedTool
|
|
199
|
+
|
|
200
|
+
if not issubclass(tool_cls, ValidatedTool):
|
|
201
|
+
raise TypeError(f"{tool_cls.__name__} must be a ValidatedTool subclass")
|
|
202
|
+
|
|
203
|
+
# Extract metadata
|
|
204
|
+
tool_name = name or tool_cls.__name__
|
|
205
|
+
description = (tool_cls.__doc__ or f"{tool_name} tool").strip()
|
|
206
|
+
|
|
207
|
+
# Extract schemas
|
|
208
|
+
parameters = tool_cls.Arguments.model_json_schema()
|
|
209
|
+
returns = tool_cls.Result.model_json_schema() if hasattr(tool_cls, "Result") else None
|
|
210
|
+
|
|
211
|
+
# Detect capabilities
|
|
212
|
+
capabilities = []
|
|
213
|
+
|
|
214
|
+
# Check if tool is marked cacheable
|
|
215
|
+
if hasattr(tool_cls, "_cacheable") and tool_cls._cacheable:
|
|
216
|
+
capabilities.append(ToolCapability.CACHEABLE)
|
|
217
|
+
|
|
218
|
+
# Check if idempotent (common pattern: GET-like operations)
|
|
219
|
+
if "get" in tool_name.lower() or "read" in tool_name.lower():
|
|
220
|
+
capabilities.append(ToolCapability.IDEMPOTENT)
|
|
221
|
+
|
|
222
|
+
# Check if streaming
|
|
223
|
+
from chuk_tool_processor.models.streaming_tool import StreamingTool
|
|
224
|
+
|
|
225
|
+
if issubclass(tool_cls, StreamingTool):
|
|
226
|
+
capabilities.append(ToolCapability.STREAMING)
|
|
227
|
+
|
|
228
|
+
return cls( # type: ignore[call-arg]
|
|
229
|
+
name=tool_name,
|
|
230
|
+
description=description,
|
|
231
|
+
namespace=namespace,
|
|
232
|
+
parameters=parameters,
|
|
233
|
+
returns=returns,
|
|
234
|
+
capabilities=capabilities,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
@classmethod
|
|
238
|
+
def from_function(
|
|
239
|
+
cls,
|
|
240
|
+
func: Callable,
|
|
241
|
+
name: str | None = None,
|
|
242
|
+
description: str | None = None,
|
|
243
|
+
namespace: str = "default",
|
|
244
|
+
) -> ToolSpec:
|
|
245
|
+
"""
|
|
246
|
+
Create ToolSpec from a plain function.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
func: Function to wrap
|
|
250
|
+
name: Tool name (default: function name)
|
|
251
|
+
description: Tool description (default: function docstring)
|
|
252
|
+
namespace: Tool namespace
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
ToolSpec instance
|
|
256
|
+
"""
|
|
257
|
+
# Extract metadata
|
|
258
|
+
tool_name = name or func.__name__
|
|
259
|
+
tool_description = description or (func.__doc__ or f"{tool_name} function").strip()
|
|
260
|
+
|
|
261
|
+
# Build parameter schema from function signature
|
|
262
|
+
sig = inspect.signature(func)
|
|
263
|
+
parameters: dict[str, Any] = {
|
|
264
|
+
"type": "object",
|
|
265
|
+
"properties": {},
|
|
266
|
+
"required": [],
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for param_name, param in sig.parameters.items():
|
|
270
|
+
if param_name == "self":
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
# Build property schema
|
|
274
|
+
prop: dict[str, Any] = {}
|
|
275
|
+
|
|
276
|
+
# Try to infer type from annotation
|
|
277
|
+
if param.annotation != inspect.Parameter.empty:
|
|
278
|
+
annotation = param.annotation
|
|
279
|
+
# Handle basic types
|
|
280
|
+
if annotation is str:
|
|
281
|
+
prop["type"] = "string"
|
|
282
|
+
elif annotation is int:
|
|
283
|
+
prop["type"] = "integer"
|
|
284
|
+
elif annotation is float:
|
|
285
|
+
prop["type"] = "number"
|
|
286
|
+
elif annotation is bool:
|
|
287
|
+
prop["type"] = "boolean"
|
|
288
|
+
elif annotation is list:
|
|
289
|
+
prop["type"] = "array"
|
|
290
|
+
elif annotation is dict:
|
|
291
|
+
prop["type"] = "object"
|
|
292
|
+
|
|
293
|
+
# Add to schema
|
|
294
|
+
parameters["properties"][param_name] = prop
|
|
295
|
+
|
|
296
|
+
# Mark as required if no default
|
|
297
|
+
if param.default == inspect.Parameter.empty:
|
|
298
|
+
parameters["required"].append(param_name)
|
|
299
|
+
|
|
300
|
+
return cls( # type: ignore[call-arg]
|
|
301
|
+
name=tool_name,
|
|
302
|
+
description=tool_description,
|
|
303
|
+
namespace=namespace,
|
|
304
|
+
parameters=parameters,
|
|
305
|
+
returns=None, # Can't infer return type from plain function
|
|
306
|
+
capabilities=[],
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# ------------------------------------------------------------------ #
|
|
311
|
+
# Convenience decorators
|
|
312
|
+
# ------------------------------------------------------------------ #
|
|
313
|
+
def tool_spec(
|
|
314
|
+
*,
|
|
315
|
+
version: str = "1.0.0",
|
|
316
|
+
capabilities: list[ToolCapability] | None = None,
|
|
317
|
+
tags: list[str] | None = None,
|
|
318
|
+
estimated_duration_seconds: float | None = None,
|
|
319
|
+
) -> Callable:
|
|
320
|
+
"""
|
|
321
|
+
Decorator to attach tool specification metadata to a tool class.
|
|
322
|
+
|
|
323
|
+
Example:
|
|
324
|
+
@tool_spec(
|
|
325
|
+
version="2.1.0",
|
|
326
|
+
capabilities=[ToolCapability.CACHEABLE, ToolCapability.IDEMPOTENT],
|
|
327
|
+
tags=["search", "web"],
|
|
328
|
+
estimated_duration_seconds=2.0,
|
|
329
|
+
)
|
|
330
|
+
class SearchTool(ValidatedTool):
|
|
331
|
+
...
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
version: Semantic version
|
|
335
|
+
capabilities: List of capabilities
|
|
336
|
+
tags: List of tags
|
|
337
|
+
estimated_duration_seconds: Estimated execution time
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Decorator function
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
def decorator(cls):
|
|
344
|
+
cls._tool_spec_version = version
|
|
345
|
+
cls._tool_spec_capabilities = capabilities or []
|
|
346
|
+
cls._tool_spec_tags = tags or []
|
|
347
|
+
cls._tool_spec_estimated_duration = estimated_duration_seconds
|
|
348
|
+
return cls
|
|
349
|
+
|
|
350
|
+
return decorator
|
|
@@ -15,14 +15,15 @@ Subclass it like so:
|
|
|
15
15
|
async def _execute(self, *, x: int, y: int) -> Result:
|
|
16
16
|
return self.Result(sum=x + y)
|
|
17
17
|
"""
|
|
18
|
+
|
|
18
19
|
from __future__ import annotations
|
|
19
20
|
|
|
20
21
|
import html
|
|
21
22
|
import inspect
|
|
22
23
|
import json
|
|
23
|
-
from typing import Any,
|
|
24
|
+
from typing import Any, TypeVar
|
|
24
25
|
|
|
25
|
-
from pydantic import BaseModel, ValidationError
|
|
26
|
+
from pydantic import BaseModel, ConfigDict, ValidationError
|
|
26
27
|
|
|
27
28
|
from chuk_tool_processor.core.exceptions import ToolValidationError
|
|
28
29
|
|
|
@@ -48,7 +49,7 @@ class _ExportMixin:
|
|
|
48
49
|
cls: type[T_Validated],
|
|
49
50
|
*,
|
|
50
51
|
registry_name: str | None = None,
|
|
51
|
-
) ->
|
|
52
|
+
) -> dict[str, Any]:
|
|
52
53
|
"""
|
|
53
54
|
Build the structure expected by `tools=[…]`.
|
|
54
55
|
|
|
@@ -67,7 +68,7 @@ class _ExportMixin:
|
|
|
67
68
|
"function": {
|
|
68
69
|
"name": fn_name,
|
|
69
70
|
"description": description,
|
|
70
|
-
"parameters": cls.Arguments.model_json_schema(),
|
|
71
|
+
"parameters": cls.Arguments.model_json_schema(),
|
|
71
72
|
},
|
|
72
73
|
}
|
|
73
74
|
|
|
@@ -75,18 +76,15 @@ class _ExportMixin:
|
|
|
75
76
|
# Plain JSON schema (arguments only)
|
|
76
77
|
# ------------------------------------------------------------------ #
|
|
77
78
|
@classmethod
|
|
78
|
-
def to_json_schema(cls: type[T_Validated]) ->
|
|
79
|
-
return cls.Arguments.model_json_schema()
|
|
79
|
+
def to_json_schema(cls: type[T_Validated]) -> dict[str, Any]:
|
|
80
|
+
return cls.Arguments.model_json_schema()
|
|
80
81
|
|
|
81
82
|
# ------------------------------------------------------------------ #
|
|
82
83
|
# Tiny XML tag - handy for unit-tests / demos
|
|
83
84
|
# ------------------------------------------------------------------ #
|
|
84
85
|
@classmethod
|
|
85
|
-
def to_xml_tag(cls: type[T_Validated], **arguments: Any) -> str:
|
|
86
|
-
return (
|
|
87
|
-
f'<tool name="{html.escape(cls.__name__)}" '
|
|
88
|
-
f"args='{html.escape(json.dumps(arguments))}'/>"
|
|
89
|
-
)
|
|
86
|
+
def to_xml_tag(cls: type[T_Validated], **arguments: Any) -> str: # type: ignore[misc]
|
|
87
|
+
return f"<tool name=\"{html.escape(cls.__name__)}\" args='{html.escape(json.dumps(arguments))}'/>"
|
|
90
88
|
|
|
91
89
|
|
|
92
90
|
# --------------------------------------------------------------------------- #
|
|
@@ -99,19 +97,39 @@ class ValidatedTool(_ExportMixin, BaseModel):
|
|
|
99
97
|
# Inner models - override in subclasses
|
|
100
98
|
# ------------------------------------------------------------------ #
|
|
101
99
|
class Arguments(BaseModel): # noqa: D401 - acts as a namespace
|
|
102
|
-
"""Input model"""
|
|
100
|
+
"""Input model with LLM-friendly coercion defaults."""
|
|
101
|
+
|
|
102
|
+
model_config = ConfigDict(
|
|
103
|
+
# Coerce string numbers to actual numbers
|
|
104
|
+
coerce_numbers_to_str=False,
|
|
105
|
+
# Strip whitespace from strings
|
|
106
|
+
str_strip_whitespace=True,
|
|
107
|
+
# Validate default values
|
|
108
|
+
validate_default=True,
|
|
109
|
+
# Be lenient with extra fields (ignore them)
|
|
110
|
+
extra="ignore",
|
|
111
|
+
# Use enum values instead of enum objects
|
|
112
|
+
use_enum_values=True,
|
|
113
|
+
)
|
|
103
114
|
|
|
104
115
|
class Result(BaseModel): # noqa: D401
|
|
105
116
|
"""Output model"""
|
|
106
117
|
|
|
118
|
+
model_config = ConfigDict(
|
|
119
|
+
# Validate default values in results too
|
|
120
|
+
validate_default=True,
|
|
121
|
+
# Use enum values in outputs
|
|
122
|
+
use_enum_values=True,
|
|
123
|
+
)
|
|
124
|
+
|
|
107
125
|
# ------------------------------------------------------------------ #
|
|
108
126
|
# Public entry-point called by the processor
|
|
109
127
|
# ------------------------------------------------------------------ #
|
|
110
128
|
async def execute(self: T_Validated, **kwargs: Any) -> BaseModel:
|
|
111
129
|
"""Validate *kwargs*, run `_execute`, validate the result."""
|
|
112
130
|
try:
|
|
113
|
-
args = self.Arguments(**kwargs)
|
|
114
|
-
res = await self._execute(**args.model_dump())
|
|
131
|
+
args = self.Arguments(**kwargs)
|
|
132
|
+
res = await self._execute(**args.model_dump())
|
|
115
133
|
|
|
116
134
|
return (
|
|
117
135
|
res
|
|
@@ -119,7 +137,7 @@ class ValidatedTool(_ExportMixin, BaseModel):
|
|
|
119
137
|
else self.Result(**(res if isinstance(res, dict) else {"value": res}))
|
|
120
138
|
)
|
|
121
139
|
except ValidationError as exc:
|
|
122
|
-
raise ToolValidationError(self.__class__.__name__, exc.errors()) from exc
|
|
140
|
+
raise ToolValidationError(self.__class__.__name__, {"errors": exc.errors()}) from exc
|
|
123
141
|
|
|
124
142
|
# ------------------------------------------------------------------ #
|
|
125
143
|
# Sub-classes must implement this
|
|
@@ -143,15 +161,15 @@ def with_validation(cls): # noqa: D401 - factory
|
|
|
143
161
|
validate_result,
|
|
144
162
|
)
|
|
145
163
|
|
|
146
|
-
original = cls.execute
|
|
164
|
+
original = cls.execute
|
|
147
165
|
if not inspect.iscoroutinefunction(original):
|
|
148
166
|
raise TypeError(f"Tool {cls.__name__} must have an async execute method")
|
|
149
167
|
|
|
150
|
-
async def _async_wrapper(self, **kwargs):
|
|
168
|
+
async def _async_wrapper(self, **kwargs):
|
|
151
169
|
tool_name = cls.__name__
|
|
152
170
|
validated = validate_arguments(tool_name, original, kwargs)
|
|
153
171
|
result = await original(self, **validated)
|
|
154
172
|
return validate_result(tool_name, original, result)
|
|
155
173
|
|
|
156
|
-
cls.execute = _async_wrapper
|
|
174
|
+
cls.execute = _async_wrapper
|
|
157
175
|
return cls
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenTelemetry observability integration for chuk-tool-processor.
|
|
3
|
+
|
|
4
|
+
This module provides drop-in OpenTelemetry tracing and Prometheus metrics
|
|
5
|
+
for tool execution, making it trivial to instrument your tool pipeline.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
from chuk_tool_processor.observability import setup_observability
|
|
9
|
+
|
|
10
|
+
# Enable both tracing and metrics
|
|
11
|
+
setup_observability(
|
|
12
|
+
service_name="my-tool-service",
|
|
13
|
+
enable_tracing=True,
|
|
14
|
+
enable_metrics=True,
|
|
15
|
+
metrics_port=9090
|
|
16
|
+
)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from .metrics import PrometheusMetrics, get_metrics
|
|
22
|
+
from .setup import setup_observability
|
|
23
|
+
from .tracing import get_tracer
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"setup_observability",
|
|
27
|
+
"get_tracer",
|
|
28
|
+
"get_metrics",
|
|
29
|
+
"PrometheusMetrics",
|
|
30
|
+
]
|