chuk-tool-processor 0.7.0__py3-none-any.whl → 0.10__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/__init__.py +114 -0
- chuk_tool_processor/core/__init__.py +31 -0
- chuk_tool_processor/core/exceptions.py +218 -12
- chuk_tool_processor/core/processor.py +391 -43
- chuk_tool_processor/execution/wrappers/__init__.py +42 -0
- chuk_tool_processor/execution/wrappers/caching.py +43 -10
- chuk_tool_processor/execution/wrappers/circuit_breaker.py +370 -0
- chuk_tool_processor/execution/wrappers/rate_limiting.py +31 -1
- chuk_tool_processor/execution/wrappers/retry.py +93 -53
- chuk_tool_processor/logging/__init__.py +5 -8
- chuk_tool_processor/logging/context.py +2 -5
- chuk_tool_processor/mcp/__init__.py +3 -0
- chuk_tool_processor/mcp/mcp_tool.py +8 -3
- chuk_tool_processor/mcp/models.py +87 -0
- chuk_tool_processor/mcp/setup_mcp_http_streamable.py +38 -2
- chuk_tool_processor/mcp/setup_mcp_sse.py +38 -2
- chuk_tool_processor/mcp/setup_mcp_stdio.py +92 -12
- chuk_tool_processor/mcp/stream_manager.py +109 -6
- chuk_tool_processor/mcp/transport/http_streamable_transport.py +18 -5
- chuk_tool_processor/mcp/transport/sse_transport.py +16 -3
- chuk_tool_processor/models/__init__.py +20 -0
- chuk_tool_processor/models/tool_call.py +34 -1
- chuk_tool_processor/models/tool_export_mixin.py +4 -4
- chuk_tool_processor/models/tool_spec.py +350 -0
- chuk_tool_processor/models/validated_tool.py +22 -2
- 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 +346 -0
- chuk_tool_processor/py.typed +0 -0
- chuk_tool_processor/registry/interface.py +7 -7
- chuk_tool_processor/registry/providers/__init__.py +2 -1
- chuk_tool_processor/registry/tool_export.py +1 -6
- chuk_tool_processor-0.10.dist-info/METADATA +2326 -0
- chuk_tool_processor-0.10.dist-info/RECORD +69 -0
- chuk_tool_processor-0.7.0.dist-info/METADATA +0 -1230
- chuk_tool_processor-0.7.0.dist-info/RECORD +0 -61
- {chuk_tool_processor-0.7.0.dist-info → chuk_tool_processor-0.10.dist-info}/WHEEL +0 -0
- {chuk_tool_processor-0.7.0.dist-info → chuk_tool_processor-0.10.dist-info}/top_level.txt +0 -0
|
@@ -596,16 +596,29 @@ class SSETransport(MCPBaseTransport):
|
|
|
596
596
|
self._metrics.update_call_metrics(response_time, success)
|
|
597
597
|
|
|
598
598
|
def _is_oauth_error(self, error_msg: str) -> bool:
|
|
599
|
-
"""
|
|
599
|
+
"""
|
|
600
|
+
Detect if error is OAuth-related per RFC 6750 and MCP OAuth spec.
|
|
601
|
+
|
|
602
|
+
Checks for:
|
|
603
|
+
- RFC 6750 Section 3.1 Bearer token errors (invalid_token, insufficient_scope)
|
|
604
|
+
- OAuth 2.1 token refresh errors (invalid_grant)
|
|
605
|
+
- MCP spec OAuth validation failures (401/403 responses)
|
|
606
|
+
"""
|
|
600
607
|
if not error_msg:
|
|
601
608
|
return False
|
|
602
609
|
|
|
603
610
|
error_lower = error_msg.lower()
|
|
604
611
|
oauth_indicators = [
|
|
605
|
-
|
|
606
|
-
"expired
|
|
612
|
+
# RFC 6750 Section 3.1 - Standard Bearer token errors
|
|
613
|
+
"invalid_token", # Token expired, revoked, malformed, or invalid
|
|
614
|
+
"insufficient_scope", # Request requires higher privileges (403 Forbidden)
|
|
615
|
+
# OAuth 2.1 token refresh errors
|
|
616
|
+
"invalid_grant", # Refresh token errors
|
|
617
|
+
# MCP spec - OAuth validation failures (401 Unauthorized)
|
|
607
618
|
"oauth validation",
|
|
608
619
|
"unauthorized",
|
|
620
|
+
# Common OAuth error descriptions
|
|
621
|
+
"expired token",
|
|
609
622
|
"token expired",
|
|
610
623
|
"authentication failed",
|
|
611
624
|
"invalid access token",
|
|
@@ -1 +1,21 @@
|
|
|
1
1
|
# chuk_tool_processor/models/__init__.py
|
|
2
|
+
"""Data models for the tool processor."""
|
|
3
|
+
|
|
4
|
+
from chuk_tool_processor.models.execution_strategy import ExecutionStrategy
|
|
5
|
+
from chuk_tool_processor.models.streaming_tool import StreamingTool
|
|
6
|
+
from chuk_tool_processor.models.tool_call import ToolCall
|
|
7
|
+
from chuk_tool_processor.models.tool_result import ToolResult
|
|
8
|
+
from chuk_tool_processor.models.tool_spec import ToolCapability, ToolSpec, tool_spec
|
|
9
|
+
from chuk_tool_processor.models.validated_tool import ValidatedTool, with_validation
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ExecutionStrategy",
|
|
13
|
+
"StreamingTool",
|
|
14
|
+
"ToolCall",
|
|
15
|
+
"ToolResult",
|
|
16
|
+
"ToolSpec",
|
|
17
|
+
"ToolCapability",
|
|
18
|
+
"tool_spec",
|
|
19
|
+
"ValidatedTool",
|
|
20
|
+
"with_validation",
|
|
21
|
+
]
|
|
@@ -5,10 +5,12 @@ Model representing a tool call with arguments.
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
8
10
|
import uuid
|
|
9
11
|
from typing import Any
|
|
10
12
|
|
|
11
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class ToolCall(BaseModel):
|
|
@@ -20,6 +22,7 @@ class ToolCall(BaseModel):
|
|
|
20
22
|
tool: Name of the tool to call
|
|
21
23
|
namespace: Namespace the tool belongs to
|
|
22
24
|
arguments: Arguments to pass to the tool
|
|
25
|
+
idempotency_key: Optional key for deduplicating duplicate calls (auto-generated)
|
|
23
26
|
"""
|
|
24
27
|
|
|
25
28
|
model_config = ConfigDict(extra="ignore")
|
|
@@ -28,6 +31,36 @@ class ToolCall(BaseModel):
|
|
|
28
31
|
tool: str = Field(..., min_length=1, description="Name of the tool to call; must be non-empty")
|
|
29
32
|
namespace: str = Field(default="default", description="Namespace the tool belongs to")
|
|
30
33
|
arguments: dict[str, Any] = Field(default_factory=dict, description="Arguments to pass to the tool")
|
|
34
|
+
idempotency_key: str | None = Field(
|
|
35
|
+
None,
|
|
36
|
+
description="Idempotency key for deduplication. Auto-generated if not provided.",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
@model_validator(mode="after")
|
|
40
|
+
def generate_idempotency_key(self) -> ToolCall:
|
|
41
|
+
"""Generate idempotency key if not provided."""
|
|
42
|
+
if self.idempotency_key is None:
|
|
43
|
+
self.idempotency_key = self._compute_idempotency_key()
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def _compute_idempotency_key(self) -> str:
|
|
47
|
+
"""
|
|
48
|
+
Compute a stable idempotency key from tool name, namespace, and arguments.
|
|
49
|
+
|
|
50
|
+
Uses SHA256 hash of the sorted JSON representation.
|
|
51
|
+
Returns first 16 characters of the hex digest for brevity.
|
|
52
|
+
"""
|
|
53
|
+
# Create a stable representation
|
|
54
|
+
payload = {
|
|
55
|
+
"tool": self.tool,
|
|
56
|
+
"namespace": self.namespace,
|
|
57
|
+
"arguments": self.arguments,
|
|
58
|
+
}
|
|
59
|
+
# Sort keys for stability
|
|
60
|
+
json_str = json.dumps(payload, sort_keys=True, default=str)
|
|
61
|
+
# Hash it
|
|
62
|
+
hash_obj = hashlib.sha256(json_str.encode(), usedforsecurity=False)
|
|
63
|
+
return hash_obj.hexdigest()[:16] # Use first 16 chars for brevity
|
|
31
64
|
|
|
32
65
|
async def to_dict(self) -> dict[str, Any]:
|
|
33
66
|
"""Convert to a dictionary for serialization."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# chuk_tool_processor/models/tool_export_mix_in.py
|
|
2
2
|
|
|
3
|
-
from typing import Any, Protocol, runtime_checkable
|
|
3
|
+
from typing import Any, Protocol, cast, runtime_checkable
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel
|
|
6
6
|
|
|
@@ -18,7 +18,7 @@ class ToolExportMixin:
|
|
|
18
18
|
@classmethod
|
|
19
19
|
def to_openai(cls) -> dict[str, Any]:
|
|
20
20
|
assert hasattr(cls, "Arguments"), f"{cls.__name__} must have an Arguments attribute"
|
|
21
|
-
schema = cls.Arguments.model_json_schema() #
|
|
21
|
+
schema = cls.Arguments.model_json_schema() # noqa: ANN401
|
|
22
22
|
return {
|
|
23
23
|
"type": "function",
|
|
24
24
|
"function": {
|
|
@@ -31,13 +31,13 @@ class ToolExportMixin:
|
|
|
31
31
|
@classmethod
|
|
32
32
|
def to_json_schema(cls) -> dict[str, Any]:
|
|
33
33
|
assert hasattr(cls, "Arguments"), f"{cls.__name__} must have an Arguments attribute"
|
|
34
|
-
return cls.Arguments.model_json_schema()
|
|
34
|
+
return cast(dict[str, Any], cls.Arguments.model_json_schema())
|
|
35
35
|
|
|
36
36
|
@classmethod
|
|
37
37
|
def to_xml(cls) -> str:
|
|
38
38
|
"""Very small helper so existing XML-based parsers still work."""
|
|
39
39
|
assert hasattr(cls, "Arguments"), f"{cls.__name__} must have an Arguments attribute"
|
|
40
40
|
name = cls.__name__.removesuffix("Tool").lower()
|
|
41
|
-
params = cls.Arguments.model_json_schema()["properties"] #
|
|
41
|
+
params = cls.Arguments.model_json_schema()["properties"] # noqa: ANN401
|
|
42
42
|
args = ", ".join(params)
|
|
43
43
|
return f'<tool name="{name}" args="{{{args}}}"/>'
|
|
@@ -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
|
|
@@ -23,7 +23,7 @@ import inspect
|
|
|
23
23
|
import json
|
|
24
24
|
from typing import Any, TypeVar
|
|
25
25
|
|
|
26
|
-
from pydantic import BaseModel, ValidationError
|
|
26
|
+
from pydantic import BaseModel, ConfigDict, ValidationError
|
|
27
27
|
|
|
28
28
|
from chuk_tool_processor.core.exceptions import ToolValidationError
|
|
29
29
|
|
|
@@ -97,11 +97,31 @@ class ValidatedTool(_ExportMixin, BaseModel):
|
|
|
97
97
|
# Inner models - override in subclasses
|
|
98
98
|
# ------------------------------------------------------------------ #
|
|
99
99
|
class Arguments(BaseModel): # noqa: D401 - acts as a namespace
|
|
100
|
-
"""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
|
+
)
|
|
101
114
|
|
|
102
115
|
class Result(BaseModel): # noqa: D401
|
|
103
116
|
"""Output model"""
|
|
104
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
|
+
|
|
105
125
|
# ------------------------------------------------------------------ #
|
|
106
126
|
# Public entry-point called by the processor
|
|
107
127
|
# ------------------------------------------------------------------ #
|
|
@@ -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
|
+
]
|