chuk-tool-processor 0.6.13__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.

Files changed (35) hide show
  1. chuk_tool_processor/core/__init__.py +31 -0
  2. chuk_tool_processor/core/exceptions.py +218 -12
  3. chuk_tool_processor/core/processor.py +38 -7
  4. chuk_tool_processor/execution/strategies/__init__.py +6 -0
  5. chuk_tool_processor/execution/strategies/subprocess_strategy.py +2 -1
  6. chuk_tool_processor/execution/wrappers/__init__.py +42 -0
  7. chuk_tool_processor/execution/wrappers/caching.py +48 -13
  8. chuk_tool_processor/execution/wrappers/circuit_breaker.py +370 -0
  9. chuk_tool_processor/execution/wrappers/rate_limiting.py +31 -1
  10. chuk_tool_processor/execution/wrappers/retry.py +93 -53
  11. chuk_tool_processor/logging/metrics.py +2 -2
  12. chuk_tool_processor/mcp/mcp_tool.py +5 -5
  13. chuk_tool_processor/mcp/setup_mcp_http_streamable.py +44 -2
  14. chuk_tool_processor/mcp/setup_mcp_sse.py +44 -2
  15. chuk_tool_processor/mcp/setup_mcp_stdio.py +2 -0
  16. chuk_tool_processor/mcp/stream_manager.py +130 -75
  17. chuk_tool_processor/mcp/transport/__init__.py +10 -0
  18. chuk_tool_processor/mcp/transport/http_streamable_transport.py +193 -108
  19. chuk_tool_processor/mcp/transport/models.py +100 -0
  20. chuk_tool_processor/mcp/transport/sse_transport.py +155 -59
  21. chuk_tool_processor/mcp/transport/stdio_transport.py +58 -10
  22. chuk_tool_processor/models/__init__.py +20 -0
  23. chuk_tool_processor/models/tool_call.py +34 -1
  24. chuk_tool_processor/models/tool_spec.py +350 -0
  25. chuk_tool_processor/models/validated_tool.py +22 -2
  26. chuk_tool_processor/observability/__init__.py +30 -0
  27. chuk_tool_processor/observability/metrics.py +312 -0
  28. chuk_tool_processor/observability/setup.py +105 -0
  29. chuk_tool_processor/observability/tracing.py +345 -0
  30. chuk_tool_processor/plugins/discovery.py +1 -1
  31. chuk_tool_processor-0.9.7.dist-info/METADATA +1813 -0
  32. {chuk_tool_processor-0.6.13.dist-info → chuk_tool_processor-0.9.7.dist-info}/RECORD +34 -27
  33. chuk_tool_processor-0.6.13.dist-info/METADATA +0 -698
  34. {chuk_tool_processor-0.6.13.dist-info → chuk_tool_processor-0.9.7.dist-info}/WHEEL +0 -0
  35. {chuk_tool_processor-0.6.13.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
@@ -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
+ ]