chuk-tool-processor 0.4__py3-none-any.whl → 0.4.1__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 +1 -1
- chuk_tool_processor/execution/strategies/inprocess_strategy.py +1 -1
- chuk_tool_processor/execution/wrappers/caching.py +3 -3
- chuk_tool_processor/execution/wrappers/retry.py +163 -174
- chuk_tool_processor/logging/context.py +6 -6
- chuk_tool_processor/mcp/mcp_tool.py +48 -36
- chuk_tool_processor/mcp/register_mcp_tools.py +3 -3
- chuk_tool_processor/mcp/setup_mcp_sse.py +4 -4
- chuk_tool_processor/mcp/setup_mcp_stdio.py +2 -2
- chuk_tool_processor/mcp/stream_manager.py +6 -6
- chuk_tool_processor/mcp/transport/base_transport.py +2 -2
- chuk_tool_processor/mcp/transport/sse_transport.py +1 -1
- chuk_tool_processor/mcp/transport/stdio_transport.py +2 -2
- chuk_tool_processor/models/validated_tool.py +6 -6
- chuk_tool_processor/plugins/discovery.py +3 -3
- chuk_tool_processor/plugins/parsers/base.py +1 -1
- chuk_tool_processor/plugins/parsers/xml_tool.py +2 -2
- chuk_tool_processor/registry/auto_register.py +5 -5
- chuk_tool_processor/registry/interface.py +2 -2
- chuk_tool_processor/registry/providers/memory.py +2 -2
- chuk_tool_processor/utils/validation.py +1 -1
- {chuk_tool_processor-0.4.dist-info → chuk_tool_processor-0.4.1.dist-info}/METADATA +1 -1
- {chuk_tool_processor-0.4.dist-info → chuk_tool_processor-0.4.1.dist-info}/RECORD +25 -25
- {chuk_tool_processor-0.4.dist-info → chuk_tool_processor-0.4.1.dist-info}/WHEEL +0 -0
- {chuk_tool_processor-0.4.dist-info → chuk_tool_processor-0.4.1.dist-info}/top_level.txt +0 -0
|
@@ -367,7 +367,7 @@ class ToolProcessor:
|
|
|
367
367
|
all_calls.extend(result)
|
|
368
368
|
|
|
369
369
|
# ------------------------------------------------------------------ #
|
|
370
|
-
# Remove duplicates
|
|
370
|
+
# Remove duplicates - use a stable digest instead of hashing a
|
|
371
371
|
# frozenset of argument items (which breaks on unhashable types).
|
|
372
372
|
# ------------------------------------------------------------------ #
|
|
373
373
|
def _args_digest(args: Dict[str, Any]) -> str:
|
|
@@ -393,7 +393,7 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
393
393
|
"""
|
|
394
394
|
Execute a single tool call with guaranteed timeout.
|
|
395
395
|
|
|
396
|
-
The entire invocation
|
|
396
|
+
The entire invocation - including argument validation - is wrapped
|
|
397
397
|
by the semaphore to honour *max_concurrency*.
|
|
398
398
|
|
|
399
399
|
Args:
|
|
@@ -4,9 +4,9 @@ Async-native caching wrapper for tool execution.
|
|
|
4
4
|
|
|
5
5
|
This module provides:
|
|
6
6
|
|
|
7
|
-
* **CacheInterface**
|
|
8
|
-
* **InMemoryCache**
|
|
9
|
-
* **CachingToolExecutor**
|
|
7
|
+
* **CacheInterface** - abstract async cache contract for custom implementations
|
|
8
|
+
* **InMemoryCache** - simple, thread-safe in-memory cache with TTL support
|
|
9
|
+
* **CachingToolExecutor** - executor wrapper that transparently caches results
|
|
10
10
|
|
|
11
11
|
Results retrieved from cache are marked with `cached=True` and `machine="cache"`
|
|
12
12
|
for easy detection.
|
|
@@ -2,36 +2,31 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Async-native retry wrapper for tool execution.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Adds exponential–back-off retry logic and *deadline-aware* timeout handling so a
|
|
6
|
+
`timeout=` passed by callers is treated as the **total wall-clock budget** for
|
|
7
|
+
all attempts of a single tool call.
|
|
7
8
|
"""
|
|
8
9
|
from __future__ import annotations
|
|
9
10
|
|
|
10
11
|
import asyncio
|
|
11
|
-
import logging
|
|
12
12
|
import random
|
|
13
|
+
import time
|
|
13
14
|
from datetime import datetime, timezone
|
|
14
|
-
from typing import Any, Dict, List, Optional, Type
|
|
15
|
+
from typing import Any, Dict, List, Optional, Type
|
|
15
16
|
|
|
17
|
+
from chuk_tool_processor.logging import get_logger
|
|
16
18
|
from chuk_tool_processor.models.tool_call import ToolCall
|
|
17
19
|
from chuk_tool_processor.models.tool_result import ToolResult
|
|
18
|
-
from chuk_tool_processor.logging import get_logger
|
|
19
20
|
|
|
20
21
|
logger = get_logger("chuk_tool_processor.execution.wrappers.retry")
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
# --------------------------------------------------------------------------- #
|
|
25
|
+
# Retry configuration
|
|
26
|
+
# --------------------------------------------------------------------------- #
|
|
23
27
|
class RetryConfig:
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
Attributes:
|
|
28
|
-
max_retries: Maximum number of retry attempts
|
|
29
|
-
base_delay: Base delay between retries in seconds
|
|
30
|
-
max_delay: Maximum delay between retries in seconds
|
|
31
|
-
jitter: Whether to add random jitter to delays
|
|
32
|
-
retry_on_exceptions: List of exception types to retry on
|
|
33
|
-
retry_on_error_substrings: List of error message substrings to retry on
|
|
34
|
-
"""
|
|
28
|
+
"""Configuration object that decides *whether* and *when* to retry."""
|
|
29
|
+
|
|
35
30
|
def __init__(
|
|
36
31
|
self,
|
|
37
32
|
max_retries: int = 3,
|
|
@@ -39,248 +34,242 @@ class RetryConfig:
|
|
|
39
34
|
max_delay: float = 60.0,
|
|
40
35
|
jitter: bool = True,
|
|
41
36
|
retry_on_exceptions: Optional[List[Type[Exception]]] = None,
|
|
42
|
-
retry_on_error_substrings: Optional[List[str]] = None
|
|
37
|
+
retry_on_error_substrings: Optional[List[str]] = None,
|
|
43
38
|
):
|
|
39
|
+
if max_retries < 0:
|
|
40
|
+
raise ValueError("max_retries cannot be negative")
|
|
44
41
|
self.max_retries = max_retries
|
|
45
42
|
self.base_delay = base_delay
|
|
46
43
|
self.max_delay = max_delay
|
|
47
44
|
self.jitter = jitter
|
|
48
45
|
self.retry_on_exceptions = retry_on_exceptions or []
|
|
49
46
|
self.retry_on_error_substrings = retry_on_error_substrings or []
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"""
|
|
47
|
+
|
|
48
|
+
# --------------------------------------------------------------------- #
|
|
49
|
+
# Decision helpers
|
|
50
|
+
# --------------------------------------------------------------------- #
|
|
51
|
+
def should_retry( # noqa: D401 (imperative mood is fine)
|
|
52
|
+
self,
|
|
53
|
+
attempt: int,
|
|
54
|
+
*,
|
|
55
|
+
error: Optional[Exception] = None,
|
|
56
|
+
error_str: Optional[str] = None,
|
|
57
|
+
) -> bool:
|
|
58
|
+
"""Return *True* iff another retry is allowed for this attempt."""
|
|
63
59
|
if attempt >= self.max_retries:
|
|
64
60
|
return False
|
|
61
|
+
|
|
62
|
+
# Nothing specified → always retry until max_retries reached
|
|
65
63
|
if not self.retry_on_exceptions and not self.retry_on_error_substrings:
|
|
66
64
|
return True
|
|
65
|
+
|
|
67
66
|
if error is not None and any(isinstance(error, exc) for exc in self.retry_on_exceptions):
|
|
68
67
|
return True
|
|
68
|
+
|
|
69
69
|
if error_str and any(substr in error_str for substr in self.retry_on_error_substrings):
|
|
70
70
|
return True
|
|
71
|
+
|
|
71
72
|
return False
|
|
72
|
-
|
|
73
|
+
|
|
74
|
+
# --------------------------------------------------------------------- #
|
|
75
|
+
# Back-off
|
|
76
|
+
# --------------------------------------------------------------------- #
|
|
73
77
|
def get_delay(self, attempt: int) -> float:
|
|
74
|
-
"""
|
|
75
|
-
Calculate the delay for the current attempt with exponential backoff.
|
|
76
|
-
|
|
77
|
-
Args:
|
|
78
|
-
attempt: Current attempt number (0-based)
|
|
79
|
-
|
|
80
|
-
Returns:
|
|
81
|
-
Delay in seconds
|
|
82
|
-
"""
|
|
78
|
+
"""Exponential back-off delay for *attempt* (0-based)."""
|
|
83
79
|
delay = min(self.base_delay * (2 ** attempt), self.max_delay)
|
|
84
80
|
if self.jitter:
|
|
85
|
-
delay *=
|
|
81
|
+
delay *= 0.5 + random.random() # jitter in [0.5, 1.5)
|
|
86
82
|
return delay
|
|
87
83
|
|
|
88
84
|
|
|
85
|
+
# --------------------------------------------------------------------------- #
|
|
86
|
+
# Retryable executor
|
|
87
|
+
# --------------------------------------------------------------------------- #
|
|
89
88
|
class RetryableToolExecutor:
|
|
90
89
|
"""
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
This executor wraps another executor and automatically retries failed
|
|
94
|
-
tool calls based on configured retry policies.
|
|
90
|
+
Wraps another executor and re-invokes it according to a :class:`RetryConfig`.
|
|
95
91
|
"""
|
|
92
|
+
|
|
96
93
|
def __init__(
|
|
97
94
|
self,
|
|
98
95
|
executor: Any,
|
|
96
|
+
*,
|
|
99
97
|
default_config: Optional[RetryConfig] = None,
|
|
100
|
-
tool_configs: Optional[Dict[str, RetryConfig]] = None
|
|
98
|
+
tool_configs: Optional[Dict[str, RetryConfig]] = None,
|
|
101
99
|
):
|
|
102
|
-
"""
|
|
103
|
-
Initialize the retryable executor.
|
|
104
|
-
|
|
105
|
-
Args:
|
|
106
|
-
executor: The underlying executor to wrap
|
|
107
|
-
default_config: Default retry configuration for all tools
|
|
108
|
-
tool_configs: Tool-specific retry configurations
|
|
109
|
-
"""
|
|
110
100
|
self.executor = executor
|
|
111
101
|
self.default_config = default_config or RetryConfig()
|
|
112
102
|
self.tool_configs = tool_configs or {}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
103
|
+
|
|
104
|
+
# --------------------------------------------------------------------- #
|
|
105
|
+
# Public helpers
|
|
106
|
+
# --------------------------------------------------------------------- #
|
|
107
|
+
def _config_for(self, tool: str) -> RetryConfig:
|
|
116
108
|
return self.tool_configs.get(tool, self.default_config)
|
|
117
|
-
|
|
109
|
+
|
|
118
110
|
async def execute(
|
|
119
111
|
self,
|
|
120
112
|
calls: List[ToolCall],
|
|
113
|
+
*,
|
|
121
114
|
timeout: Optional[float] = None,
|
|
122
|
-
use_cache: bool = True
|
|
115
|
+
use_cache: bool = True,
|
|
123
116
|
) -> List[ToolResult]:
|
|
124
|
-
"""
|
|
125
|
-
Execute tool calls with retry logic.
|
|
126
|
-
|
|
127
|
-
Args:
|
|
128
|
-
calls: List of tool calls to execute
|
|
129
|
-
timeout: Optional timeout for each execution
|
|
130
|
-
use_cache: Whether to use cached results (passed to underlying executor)
|
|
131
|
-
|
|
132
|
-
Returns:
|
|
133
|
-
List of tool results
|
|
134
|
-
"""
|
|
135
|
-
# Handle empty calls list
|
|
136
117
|
if not calls:
|
|
137
118
|
return []
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
results: List[ToolResult] = []
|
|
119
|
+
|
|
120
|
+
out: List[ToolResult] = []
|
|
141
121
|
for call in calls:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
122
|
+
cfg = self._config_for(call.tool)
|
|
123
|
+
out.append(await self._execute_single(call, cfg, timeout, use_cache))
|
|
124
|
+
return out
|
|
125
|
+
|
|
126
|
+
# --------------------------------------------------------------------- #
|
|
127
|
+
# Core retry loop (per call)
|
|
128
|
+
# --------------------------------------------------------------------- #
|
|
129
|
+
async def _execute_single(
|
|
148
130
|
self,
|
|
149
131
|
call: ToolCall,
|
|
150
|
-
|
|
132
|
+
cfg: RetryConfig,
|
|
151
133
|
timeout: Optional[float],
|
|
152
|
-
use_cache: bool
|
|
134
|
+
use_cache: bool,
|
|
153
135
|
) -> ToolResult:
|
|
154
|
-
"""
|
|
155
|
-
Execute a single tool call with retries.
|
|
156
|
-
|
|
157
|
-
Args:
|
|
158
|
-
call: Tool call to execute
|
|
159
|
-
config: Retry configuration to use
|
|
160
|
-
timeout: Optional timeout for execution
|
|
161
|
-
use_cache: Whether to use cached results
|
|
162
|
-
|
|
163
|
-
Returns:
|
|
164
|
-
Tool result after retries
|
|
165
|
-
"""
|
|
166
136
|
attempt = 0
|
|
167
137
|
last_error: Optional[str] = None
|
|
168
138
|
pid = 0
|
|
169
139
|
machine = "unknown"
|
|
170
|
-
|
|
140
|
+
|
|
141
|
+
# ---------------------------------------------------------------- #
|
|
142
|
+
# Deadline budget (wall-clock)
|
|
143
|
+
# ---------------------------------------------------------------- #
|
|
144
|
+
deadline = None
|
|
145
|
+
if timeout is not None:
|
|
146
|
+
deadline = time.monotonic() + timeout
|
|
147
|
+
|
|
171
148
|
while True:
|
|
149
|
+
# ---------------------------------------------------------------- #
|
|
150
|
+
# Check whether we have any time left *before* trying the call
|
|
151
|
+
# ---------------------------------------------------------------- #
|
|
152
|
+
if deadline is not None:
|
|
153
|
+
remaining = deadline - time.monotonic()
|
|
154
|
+
if remaining <= 0:
|
|
155
|
+
return ToolResult(
|
|
156
|
+
tool=call.tool,
|
|
157
|
+
result=None,
|
|
158
|
+
error=f"Timeout after {timeout}s",
|
|
159
|
+
start_time=datetime.now(timezone.utc),
|
|
160
|
+
end_time=datetime.now(timezone.utc),
|
|
161
|
+
machine=machine,
|
|
162
|
+
pid=pid,
|
|
163
|
+
attempts=attempt,
|
|
164
|
+
)
|
|
165
|
+
else:
|
|
166
|
+
remaining = None # unlimited
|
|
167
|
+
|
|
168
|
+
# ---------------------------------------------------------------- #
|
|
169
|
+
# Execute one attempt
|
|
170
|
+
# ---------------------------------------------------------------- #
|
|
172
171
|
start_time = datetime.now(timezone.utc)
|
|
173
|
-
|
|
174
172
|
try:
|
|
175
|
-
|
|
176
|
-
executor_kwargs = {"timeout": timeout}
|
|
173
|
+
kwargs = {"timeout": remaining} if remaining is not None else {}
|
|
177
174
|
if hasattr(self.executor, "use_cache"):
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
tool_results = await self.executor.execute([call], **executor_kwargs)
|
|
182
|
-
result = tool_results[0]
|
|
175
|
+
kwargs["use_cache"] = use_cache
|
|
176
|
+
|
|
177
|
+
result = (await self.executor.execute([call], **kwargs))[0]
|
|
183
178
|
pid = result.pid
|
|
184
179
|
machine = result.machine
|
|
185
|
-
|
|
186
|
-
#
|
|
187
|
-
if result.error:
|
|
188
|
-
|
|
189
|
-
if config.should_retry(attempt, error_str=result.error):
|
|
190
|
-
logger.debug(
|
|
191
|
-
f"Retrying tool {call.tool} after error: {result.error} (attempt {attempt + 1}/{config.max_retries})"
|
|
192
|
-
)
|
|
193
|
-
await asyncio.sleep(config.get_delay(attempt))
|
|
194
|
-
attempt += 1
|
|
195
|
-
continue
|
|
196
|
-
|
|
197
|
-
# No retry: if any retries happened, wrap final error
|
|
198
|
-
if attempt > 0:
|
|
199
|
-
end_time = datetime.now(timezone.utc)
|
|
200
|
-
final = ToolResult(
|
|
201
|
-
tool=call.tool,
|
|
202
|
-
result=None,
|
|
203
|
-
error=f"Max retries reached ({config.max_retries}): {last_error}",
|
|
204
|
-
start_time=start_time,
|
|
205
|
-
end_time=end_time,
|
|
206
|
-
machine=machine,
|
|
207
|
-
pid=pid
|
|
208
|
-
)
|
|
209
|
-
# Attach attempts
|
|
210
|
-
final.attempts = attempt + 1 # Include the original attempt
|
|
211
|
-
return final
|
|
212
|
-
|
|
213
|
-
# No retries occurred, return the original failure
|
|
214
|
-
result.attempts = 1
|
|
180
|
+
|
|
181
|
+
# Success?
|
|
182
|
+
if not result.error:
|
|
183
|
+
result.attempts = attempt + 1
|
|
215
184
|
return result
|
|
216
|
-
|
|
217
|
-
#
|
|
218
|
-
|
|
185
|
+
|
|
186
|
+
# Error: decide on retry
|
|
187
|
+
last_error = result.error
|
|
188
|
+
if cfg.should_retry(attempt, error_str=result.error):
|
|
189
|
+
delay = cfg.get_delay(attempt)
|
|
190
|
+
# never overshoot the deadline
|
|
191
|
+
if deadline is not None:
|
|
192
|
+
delay = min(delay, max(deadline - time.monotonic(), 0))
|
|
193
|
+
if delay:
|
|
194
|
+
await asyncio.sleep(delay)
|
|
195
|
+
attempt += 1
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
# No more retries wanted
|
|
199
|
+
result.error = self._wrap_error(last_error, attempt, cfg)
|
|
200
|
+
result.attempts = attempt + 1
|
|
219
201
|
return result
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
202
|
+
|
|
203
|
+
# ---------------------------------------------------------------- #
|
|
204
|
+
# Exception path
|
|
205
|
+
# ---------------------------------------------------------------- #
|
|
206
|
+
except Exception as exc: # noqa: BLE001
|
|
207
|
+
err_str = str(exc)
|
|
223
208
|
last_error = err_str
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
209
|
+
if cfg.should_retry(attempt, error=exc):
|
|
210
|
+
delay = cfg.get_delay(attempt)
|
|
211
|
+
if deadline is not None:
|
|
212
|
+
delay = min(delay, max(deadline - time.monotonic(), 0))
|
|
213
|
+
if delay:
|
|
214
|
+
await asyncio.sleep(delay)
|
|
230
215
|
attempt += 1
|
|
231
216
|
continue
|
|
232
|
-
|
|
233
|
-
# No more retries: return error result
|
|
217
|
+
|
|
234
218
|
end_time = datetime.now(timezone.utc)
|
|
235
|
-
|
|
219
|
+
return ToolResult(
|
|
236
220
|
tool=call.tool,
|
|
237
221
|
result=None,
|
|
238
|
-
error=err_str,
|
|
222
|
+
error=self._wrap_error(err_str, attempt, cfg),
|
|
239
223
|
start_time=start_time,
|
|
240
224
|
end_time=end_time,
|
|
241
225
|
machine=machine,
|
|
242
|
-
pid=pid
|
|
226
|
+
pid=pid,
|
|
227
|
+
attempts=attempt + 1,
|
|
243
228
|
)
|
|
244
|
-
final_exc.attempts = attempt + 1 # Include the original attempt
|
|
245
|
-
return final_exc
|
|
246
229
|
|
|
230
|
+
# --------------------------------------------------------------------- #
|
|
231
|
+
# Helpers
|
|
232
|
+
# --------------------------------------------------------------------- #
|
|
233
|
+
@staticmethod
|
|
234
|
+
def _wrap_error(err: str, attempt: int, cfg: RetryConfig) -> str:
|
|
235
|
+
if attempt >= cfg.max_retries and attempt > 0:
|
|
236
|
+
return f"Max retries reached ({cfg.max_retries}): {err}"
|
|
237
|
+
return err
|
|
247
238
|
|
|
239
|
+
|
|
240
|
+
# --------------------------------------------------------------------------- #
|
|
241
|
+
# Decorator helper
|
|
242
|
+
# --------------------------------------------------------------------------- #
|
|
248
243
|
def retryable(
|
|
244
|
+
*,
|
|
249
245
|
max_retries: int = 3,
|
|
250
246
|
base_delay: float = 1.0,
|
|
251
247
|
max_delay: float = 60.0,
|
|
252
248
|
jitter: bool = True,
|
|
253
249
|
retry_on_exceptions: Optional[List[Type[Exception]]] = None,
|
|
254
|
-
retry_on_error_substrings: Optional[List[str]] = None
|
|
250
|
+
retry_on_error_substrings: Optional[List[str]] = None,
|
|
255
251
|
):
|
|
256
252
|
"""
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
Example
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
max_retries: Maximum number of retry attempts
|
|
267
|
-
base_delay: Base delay between retries in seconds
|
|
268
|
-
max_delay: Maximum delay between retries in seconds
|
|
269
|
-
jitter: Whether to add random jitter to delays
|
|
270
|
-
retry_on_exceptions: List of exception types to retry on
|
|
271
|
-
retry_on_error_substrings: List of error message substrings to retry on
|
|
272
|
-
|
|
273
|
-
Returns:
|
|
274
|
-
Decorated class with retry configuration
|
|
253
|
+
Class decorator that attaches a :class:`RetryConfig` to a *tool* class.
|
|
254
|
+
|
|
255
|
+
Example
|
|
256
|
+
-------
|
|
257
|
+
```python
|
|
258
|
+
@retryable(max_retries=5, base_delay=0.5)
|
|
259
|
+
class MyTool:
|
|
260
|
+
...
|
|
261
|
+
```
|
|
275
262
|
"""
|
|
276
|
-
|
|
263
|
+
|
|
264
|
+
def _decorator(cls):
|
|
277
265
|
cls._retry_config = RetryConfig(
|
|
278
266
|
max_retries=max_retries,
|
|
279
267
|
base_delay=base_delay,
|
|
280
268
|
max_delay=max_delay,
|
|
281
269
|
jitter=jitter,
|
|
282
270
|
retry_on_exceptions=retry_on_exceptions,
|
|
283
|
-
retry_on_error_substrings=retry_on_error_substrings
|
|
271
|
+
retry_on_error_substrings=retry_on_error_substrings,
|
|
284
272
|
)
|
|
285
273
|
return cls
|
|
286
|
-
|
|
274
|
+
|
|
275
|
+
return _decorator
|
|
@@ -4,12 +4,12 @@ Async-safe context management for structured logging.
|
|
|
4
4
|
|
|
5
5
|
This module provides:
|
|
6
6
|
|
|
7
|
-
* **LogContext**
|
|
7
|
+
* **LogContext** - an `asyncio`-aware container that keeps a per-task dict of
|
|
8
8
|
contextual data (request IDs, span IDs, arbitrary metadata, …).
|
|
9
|
-
* **log_context**
|
|
10
|
-
* **StructuredAdapter**
|
|
9
|
+
* **log_context** - a global instance of `LogContext` for convenience.
|
|
10
|
+
* **StructuredAdapter** - a `logging.LoggerAdapter` that injects the current
|
|
11
11
|
`log_context.context` into every log record.
|
|
12
|
-
* **get_logger**
|
|
12
|
+
* **get_logger** - helper that returns a configured `StructuredAdapter`.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
@@ -75,7 +75,7 @@ class LogContext:
|
|
|
75
75
|
Async-safe context container.
|
|
76
76
|
|
|
77
77
|
Holds a mutable dict that is *local* to the current asyncio task, so
|
|
78
|
-
concurrent coroutines don
|
|
78
|
+
concurrent coroutines don't interfere with each other.
|
|
79
79
|
"""
|
|
80
80
|
|
|
81
81
|
# ------------------------------------------------------------------ #
|
|
@@ -196,7 +196,7 @@ class StructuredAdapter(logging.LoggerAdapter):
|
|
|
196
196
|
"""
|
|
197
197
|
|
|
198
198
|
# --------------------------- core hook -------------------------------- #
|
|
199
|
-
def process(self, msg, kwargs): # noqa: D401
|
|
199
|
+
def process(self, msg, kwargs): # noqa: D401 - keep signature from base
|
|
200
200
|
kwargs = kwargs or {}
|
|
201
201
|
extra = kwargs.get("extra", {}).copy()
|
|
202
202
|
ctx = log_context.context
|
|
@@ -36,11 +36,11 @@ class MCPTool:
|
|
|
36
36
|
servers: Optional[List[str]] = None,
|
|
37
37
|
server_names: Optional[Dict[int, str]] = None,
|
|
38
38
|
namespace: str = "stdio",
|
|
39
|
-
default_timeout: Optional[float] = None
|
|
39
|
+
default_timeout: Optional[float] = None
|
|
40
40
|
) -> None:
|
|
41
41
|
self.tool_name = tool_name
|
|
42
42
|
self._sm: Optional[StreamManager] = stream_manager
|
|
43
|
-
self.default_timeout = default_timeout
|
|
43
|
+
self.default_timeout = default_timeout
|
|
44
44
|
|
|
45
45
|
# Boot-strap parameters (only needed if _sm is None)
|
|
46
46
|
self._cfg_file = cfg_file
|
|
@@ -79,55 +79,67 @@ class MCPTool:
|
|
|
79
79
|
|
|
80
80
|
return self._sm # type: ignore[return-value]
|
|
81
81
|
|
|
82
|
-
# ------------------------------------------------------------------ #
|
|
83
82
|
async def execute(self, timeout: Optional[float] = None, **kwargs: Any) -> Any:
|
|
84
83
|
"""
|
|
85
|
-
|
|
84
|
+
Invoke the remote MCP tool, guaranteeing that *one* timeout is enforced.
|
|
86
85
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
timeout : float | None
|
|
89
|
+
If provided, forward this to StreamManager. Otherwise fall back
|
|
90
|
+
to ``self.default_timeout``.
|
|
91
|
+
**kwargs
|
|
92
|
+
Arguments forwarded to the tool.
|
|
91
93
|
|
|
92
|
-
Returns
|
|
93
|
-
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
Any
|
|
97
|
+
The ``content`` of the remote tool response.
|
|
94
98
|
|
|
95
99
|
Raises
|
|
96
100
|
------
|
|
97
101
|
RuntimeError
|
|
98
|
-
|
|
102
|
+
The remote tool returned an error payload.
|
|
99
103
|
asyncio.TimeoutError
|
|
100
|
-
|
|
104
|
+
The call exceeded the chosen timeout.
|
|
101
105
|
"""
|
|
102
106
|
sm = await self._ensure_stream_manager()
|
|
103
|
-
|
|
104
|
-
#
|
|
105
|
-
effective_timeout =
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
|
|
108
|
+
# Pick the timeout we will enforce (may be None = no limit).
|
|
109
|
+
effective_timeout: Optional[float] = (
|
|
110
|
+
timeout if timeout is not None else self.default_timeout
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
call_kwargs: dict[str, Any] = {
|
|
114
|
+
"tool_name": self.tool_name,
|
|
115
|
+
"arguments": kwargs,
|
|
116
|
+
}
|
|
117
|
+
if effective_timeout is not None:
|
|
118
|
+
call_kwargs["timeout"] = effective_timeout
|
|
119
|
+
logger.debug(
|
|
120
|
+
"Forwarding timeout=%ss to StreamManager for tool '%s'",
|
|
121
|
+
effective_timeout,
|
|
122
|
+
self.tool_name,
|
|
115
123
|
)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
logger.error("Remote MCP error from '%s': %s", self.tool_name, err)
|
|
120
|
-
raise RuntimeError(err)
|
|
121
|
-
|
|
122
|
-
return result.get("content")
|
|
123
|
-
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
result = await sm.call_tool(**call_kwargs)
|
|
124
127
|
except asyncio.TimeoutError:
|
|
125
|
-
logger.warning(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
logger.warning(
|
|
129
|
+
"MCP tool '%s' timed out after %ss",
|
|
130
|
+
self.tool_name,
|
|
131
|
+
effective_timeout,
|
|
132
|
+
)
|
|
129
133
|
raise
|
|
130
134
|
|
|
135
|
+
if result.get("isError"):
|
|
136
|
+
err = result.get("error", "Unknown error")
|
|
137
|
+
logger.error("Remote MCP error from '%s': %s", self.tool_name, err)
|
|
138
|
+
raise RuntimeError(err)
|
|
139
|
+
|
|
140
|
+
return result.get("content")
|
|
141
|
+
|
|
142
|
+
|
|
131
143
|
# ------------------------------------------------------------------ #
|
|
132
144
|
# Legacy method name support
|
|
133
145
|
async def _aexecute(self, timeout: Optional[float] = None, **kwargs: Any) -> Any:
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
Discover the remote MCP tools exposed by a :class:`~chuk_tool_processor.mcp.stream_manager.StreamManager`
|
|
5
5
|
instance and register them in the local CHUK registry.
|
|
6
6
|
|
|
7
|
-
The helper is now **async-native**
|
|
7
|
+
The helper is now **async-native** - call it with ``await``.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
@@ -55,7 +55,7 @@ async def register_mcp_tools(
|
|
|
55
55
|
for tool_def in mcp_tools:
|
|
56
56
|
tool_name = tool_def.get("name")
|
|
57
57
|
if not tool_name:
|
|
58
|
-
logger.warning("Remote tool definition without a 'name' field
|
|
58
|
+
logger.warning("Remote tool definition without a 'name' field - skipped")
|
|
59
59
|
continue
|
|
60
60
|
|
|
61
61
|
description = tool_def.get("description") or f"MCP tool • {tool_name}"
|
|
@@ -96,5 +96,5 @@ async def register_mcp_tools(
|
|
|
96
96
|
except Exception as exc: # noqa: BLE001
|
|
97
97
|
logger.error("Failed to register MCP tool '%s': %s", tool_name, exc)
|
|
98
98
|
|
|
99
|
-
logger.info("MCP registration complete
|
|
99
|
+
logger.info("MCP registration complete - %d tool(s) available", len(registered))
|
|
100
100
|
return registered
|
|
@@ -8,7 +8,7 @@ Utility that wires up:
|
|
|
8
8
|
2. The remote MCP tools exposed by that manager (via
|
|
9
9
|
:pyfunc:`~chuk_tool_processor.mcp.register_mcp_tools.register_mcp_tools`).
|
|
10
10
|
3. A fully-featured :class:`~chuk_tool_processor.core.processor.ToolProcessor`
|
|
11
|
-
instance that can execute those tools
|
|
11
|
+
instance that can execute those tools - with optional caching,
|
|
12
12
|
rate-limiting, retries, etc.
|
|
13
13
|
"""
|
|
14
14
|
|
|
@@ -28,7 +28,7 @@ logger = get_logger("chuk_tool_processor.mcp.setup_sse")
|
|
|
28
28
|
# --------------------------------------------------------------------------- #
|
|
29
29
|
# public helper
|
|
30
30
|
# --------------------------------------------------------------------------- #
|
|
31
|
-
async def setup_mcp_sse( # noqa: C901
|
|
31
|
+
async def setup_mcp_sse( # noqa: C901 - long, but just a config wrapper
|
|
32
32
|
*,
|
|
33
33
|
servers: List[Dict[str, str]],
|
|
34
34
|
server_names: Optional[Dict[int, str]] = None,
|
|
@@ -47,7 +47,7 @@ async def setup_mcp_sse( # noqa: C901 – long, but just a config wrapper
|
|
|
47
47
|
Spin up an SSE-backed *StreamManager*, register all its remote tools,
|
|
48
48
|
and return a ready-to-go :class:`ToolProcessor`.
|
|
49
49
|
|
|
50
|
-
Everything is **async-native**
|
|
50
|
+
Everything is **async-native** - call with ``await``.
|
|
51
51
|
|
|
52
52
|
NEW: Automatically detects and adds bearer token from MCP_BEARER_TOKEN
|
|
53
53
|
environment variable if not explicitly provided in server config.
|
|
@@ -91,7 +91,7 @@ async def setup_mcp_sse( # noqa: C901 – long, but just a config wrapper
|
|
|
91
91
|
)
|
|
92
92
|
|
|
93
93
|
logger.info(
|
|
94
|
-
"MCP (SSE) initialised
|
|
94
|
+
"MCP (SSE) initialised - %s tool%s registered into namespace '%s'",
|
|
95
95
|
len(registered),
|
|
96
96
|
"" if len(registered) == 1 else "s",
|
|
97
97
|
namespace,
|
|
@@ -26,7 +26,7 @@ logger = get_logger("chuk_tool_processor.mcp.setup_stdio")
|
|
|
26
26
|
# --------------------------------------------------------------------------- #
|
|
27
27
|
# public helper
|
|
28
28
|
# --------------------------------------------------------------------------- #
|
|
29
|
-
async def setup_mcp_stdio( # noqa: C901
|
|
29
|
+
async def setup_mcp_stdio( # noqa: C901 - long but just a config facade
|
|
30
30
|
*,
|
|
31
31
|
config_file: str,
|
|
32
32
|
servers: List[str],
|
|
@@ -72,7 +72,7 @@ async def setup_mcp_stdio( # noqa: C901 – long but just a config facade
|
|
|
72
72
|
)
|
|
73
73
|
|
|
74
74
|
logger.info(
|
|
75
|
-
"MCP (stdio) initialised
|
|
75
|
+
"MCP (stdio) initialised - %s tool%s registered into namespace '%s'",
|
|
76
76
|
len(registered),
|
|
77
77
|
"" if len(registered) == 1 else "s",
|
|
78
78
|
namespace,
|
|
@@ -77,7 +77,7 @@ class StreamManager:
|
|
|
77
77
|
return inst
|
|
78
78
|
|
|
79
79
|
# ------------------------------------------------------------------ #
|
|
80
|
-
# initialisation
|
|
80
|
+
# initialisation - stdio / sse #
|
|
81
81
|
# ------------------------------------------------------------------ #
|
|
82
82
|
async def initialize(
|
|
83
83
|
self,
|
|
@@ -143,12 +143,12 @@ class StreamManager:
|
|
|
143
143
|
"status": status,
|
|
144
144
|
}
|
|
145
145
|
)
|
|
146
|
-
logger.info("Initialised %s
|
|
146
|
+
logger.info("Initialised %s - %d tool(s)", server_name, len(tools))
|
|
147
147
|
except Exception as exc: # noqa: BLE001
|
|
148
148
|
logger.error("Error initialising %s: %s", server_name, exc)
|
|
149
149
|
|
|
150
150
|
logger.info(
|
|
151
|
-
"StreamManager ready
|
|
151
|
+
"StreamManager ready - %d server(s), %d tool(s)",
|
|
152
152
|
len(self.transports),
|
|
153
153
|
len(self.all_tools),
|
|
154
154
|
)
|
|
@@ -194,12 +194,12 @@ class StreamManager:
|
|
|
194
194
|
self.server_info.append(
|
|
195
195
|
{"id": idx, "name": name, "tools": len(tools), "status": status}
|
|
196
196
|
)
|
|
197
|
-
logger.info("Initialised SSE %s
|
|
197
|
+
logger.info("Initialised SSE %s - %d tool(s)", name, len(tools))
|
|
198
198
|
except Exception as exc: # noqa: BLE001
|
|
199
199
|
logger.error("Error initialising SSE %s: %s", name, exc)
|
|
200
200
|
|
|
201
201
|
logger.info(
|
|
202
|
-
"StreamManager ready
|
|
202
|
+
"StreamManager ready - %d SSE server(s), %d tool(s)",
|
|
203
203
|
len(self.transports),
|
|
204
204
|
len(self.all_tools),
|
|
205
205
|
)
|
|
@@ -245,7 +245,7 @@ class StreamManager:
|
|
|
245
245
|
return []
|
|
246
246
|
|
|
247
247
|
# ------------------------------------------------------------------ #
|
|
248
|
-
# EXTRA HELPERS
|
|
248
|
+
# EXTRA HELPERS - ping / resources / prompts #
|
|
249
249
|
# ------------------------------------------------------------------ #
|
|
250
250
|
async def ping_servers(self) -> List[Dict[str, Any]]:
|
|
251
251
|
async def _ping_one(name: str, tr: MCPBaseTransport):
|
|
@@ -73,7 +73,7 @@ class MCPBaseTransport(ABC):
|
|
|
73
73
|
@abstractmethod
|
|
74
74
|
async def list_resources(self) -> Dict[str, Any]:
|
|
75
75
|
"""
|
|
76
|
-
Retrieve the server
|
|
76
|
+
Retrieve the server's resources catalogue.
|
|
77
77
|
|
|
78
78
|
Expected shape::
|
|
79
79
|
{ "resources": [ {...}, ... ], "nextCursor": "…", … }
|
|
@@ -83,7 +83,7 @@ class MCPBaseTransport(ABC):
|
|
|
83
83
|
@abstractmethod
|
|
84
84
|
async def list_prompts(self) -> Dict[str, Any]:
|
|
85
85
|
"""
|
|
86
|
-
Retrieve the server
|
|
86
|
+
Retrieve the server's prompt catalogue.
|
|
87
87
|
|
|
88
88
|
Expected shape::
|
|
89
89
|
{ "prompts": [ {...}, ... ], "nextCursor": "…", … }
|
|
@@ -351,7 +351,7 @@ class SSETransport(MCPBaseTransport):
|
|
|
351
351
|
"""
|
|
352
352
|
# NEW: Ensure initialization before tool calls
|
|
353
353
|
if not self._initialized.is_set():
|
|
354
|
-
return {"isError": True, "error": "
|
|
354
|
+
return {"isError": True, "error": "SSE transport not implemented"}
|
|
355
355
|
|
|
356
356
|
if not self._message_url:
|
|
357
357
|
return {"isError": True, "error": "No message endpoint available"}
|
|
@@ -131,7 +131,7 @@ class StdioTransport(MCPBaseTransport):
|
|
|
131
131
|
def get_streams(self):
|
|
132
132
|
"""
|
|
133
133
|
Expose the low-level streams so legacy callers can access them
|
|
134
|
-
directly. The base-class
|
|
134
|
+
directly. The base-class' default returns an empty list; here we
|
|
135
135
|
return a single-element list when the transport is active.
|
|
136
136
|
"""
|
|
137
137
|
if self.read_stream and self.write_stream:
|
|
@@ -145,7 +145,7 @@ class StdioTransport(MCPBaseTransport):
|
|
|
145
145
|
self, tool_name: str, arguments: Dict[str, Any]
|
|
146
146
|
) -> Dict[str, Any]:
|
|
147
147
|
"""
|
|
148
|
-
Execute *tool_name* with *arguments* and normalise the server
|
|
148
|
+
Execute *tool_name* with *arguments* and normalise the server's reply.
|
|
149
149
|
|
|
150
150
|
The echo-server often returns:
|
|
151
151
|
{
|
|
@@ -35,7 +35,7 @@ T_Validated = TypeVar("T_Validated", bound="ValidatedTool")
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
# --------------------------------------------------------------------------- #
|
|
38
|
-
# Helper mix-in
|
|
38
|
+
# Helper mix-in - serialise a *class* into assorted formats
|
|
39
39
|
# --------------------------------------------------------------------------- #
|
|
40
40
|
class _ExportMixin:
|
|
41
41
|
"""Static helpers that expose a tool class in other specs."""
|
|
@@ -79,7 +79,7 @@ class _ExportMixin:
|
|
|
79
79
|
return cls.Arguments.model_json_schema() # type: ignore[attr-defined]
|
|
80
80
|
|
|
81
81
|
# ------------------------------------------------------------------ #
|
|
82
|
-
# Tiny XML tag
|
|
82
|
+
# Tiny XML tag - handy for unit-tests / demos
|
|
83
83
|
# ------------------------------------------------------------------ #
|
|
84
84
|
@classmethod
|
|
85
85
|
def to_xml_tag(cls: type[T_Validated], **arguments: Any) -> str:
|
|
@@ -96,9 +96,9 @@ class ValidatedTool(_ExportMixin, BaseModel):
|
|
|
96
96
|
"""Pydantic-validated base for new async-native tools."""
|
|
97
97
|
|
|
98
98
|
# ------------------------------------------------------------------ #
|
|
99
|
-
# Inner models
|
|
99
|
+
# Inner models - override in subclasses
|
|
100
100
|
# ------------------------------------------------------------------ #
|
|
101
|
-
class Arguments(BaseModel): # noqa: D401
|
|
101
|
+
class Arguments(BaseModel): # noqa: D401 - acts as a namespace
|
|
102
102
|
"""Input model"""
|
|
103
103
|
|
|
104
104
|
class Result(BaseModel): # noqa: D401
|
|
@@ -124,14 +124,14 @@ class ValidatedTool(_ExportMixin, BaseModel):
|
|
|
124
124
|
# ------------------------------------------------------------------ #
|
|
125
125
|
# Sub-classes must implement this
|
|
126
126
|
# ------------------------------------------------------------------ #
|
|
127
|
-
async def _execute(self, **_kwargs: Any): # noqa: D401
|
|
127
|
+
async def _execute(self, **_kwargs: Any): # noqa: D401 - expected override
|
|
128
128
|
raise NotImplementedError("Tool must implement async _execute()")
|
|
129
129
|
|
|
130
130
|
|
|
131
131
|
# --------------------------------------------------------------------------- #
|
|
132
132
|
# Decorator to retrofit validation onto classic "imperative" tools
|
|
133
133
|
# --------------------------------------------------------------------------- #
|
|
134
|
-
def with_validation(cls): # noqa: D401
|
|
134
|
+
def with_validation(cls): # noqa: D401 - factory
|
|
135
135
|
"""
|
|
136
136
|
Decorator that wraps an existing async ``execute`` method with:
|
|
137
137
|
|
|
@@ -58,13 +58,13 @@ class PluginDiscovery:
|
|
|
58
58
|
"""
|
|
59
59
|
Recursively scans *package_paths* for plugin classes and registers them.
|
|
60
60
|
|
|
61
|
-
* Parser plugins
|
|
61
|
+
* Parser plugins - concrete subclasses of :class:`ParserPlugin`
|
|
62
62
|
with an **async** ``try_parse`` coroutine.
|
|
63
63
|
|
|
64
|
-
* Execution strategies
|
|
64
|
+
* Execution strategies - concrete subclasses of
|
|
65
65
|
:class:`ExecutionStrategy`.
|
|
66
66
|
|
|
67
|
-
* Explicitly-decorated plugins
|
|
67
|
+
* Explicitly-decorated plugins - classes tagged with ``@plugin(...)``.
|
|
68
68
|
"""
|
|
69
69
|
|
|
70
70
|
# ------------------------------------------------------------------ #
|
|
@@ -16,7 +16,7 @@ class ParserPlugin(ABC):
|
|
|
16
16
|
Every parser plugin **must** implement the async ``try_parse`` coroutine.
|
|
17
17
|
|
|
18
18
|
The processor awaits it and expects *a list* of :class:`ToolCall`
|
|
19
|
-
objects. If the plugin doesn
|
|
19
|
+
objects. If the plugin doesn't recognise the input it should return an
|
|
20
20
|
empty list.
|
|
21
21
|
"""
|
|
22
22
|
|
|
@@ -68,7 +68,7 @@ class XmlToolPlugin(ParserPlugin):
|
|
|
68
68
|
return calls
|
|
69
69
|
|
|
70
70
|
# ------------------------------------------------------------------ #
|
|
71
|
-
# Helper
|
|
71
|
+
# Helper - robust JSON decode for the args attribute
|
|
72
72
|
# ------------------------------------------------------------------ #
|
|
73
73
|
@staticmethod
|
|
74
74
|
def _decode_args(raw_args: str) -> dict:
|
|
@@ -89,7 +89,7 @@ class XmlToolPlugin(ParserPlugin):
|
|
|
89
89
|
except json.JSONDecodeError:
|
|
90
90
|
parsed = None
|
|
91
91
|
|
|
92
|
-
# 3️⃣ Last resort
|
|
92
|
+
# 3️⃣ Last resort - naive unescaping of \" → "
|
|
93
93
|
if parsed is None:
|
|
94
94
|
try:
|
|
95
95
|
parsed = json.loads(raw_args.replace(r"\"", "\""))
|
|
@@ -22,7 +22,7 @@ from pydantic import BaseModel, create_model
|
|
|
22
22
|
try: # optional dependency
|
|
23
23
|
from langchain.tools.base import BaseTool # type: ignore
|
|
24
24
|
except ModuleNotFoundError: # pragma: no cover
|
|
25
|
-
BaseTool = None # noqa: N816
|
|
25
|
+
BaseTool = None # noqa: N816 - keep the name for isinstance() checks
|
|
26
26
|
|
|
27
27
|
# registry
|
|
28
28
|
from .decorators import register_tool
|
|
@@ -30,7 +30,7 @@ from .provider import ToolRegistryProvider
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
# ────────────────────────────────────────────────────────────────────────────
|
|
33
|
-
# internals
|
|
33
|
+
# internals - build a Pydantic schema from an arbitrary callable
|
|
34
34
|
# ────────────────────────────────────────────────────────────────────────────
|
|
35
35
|
|
|
36
36
|
|
|
@@ -39,7 +39,7 @@ def _auto_schema(func: Callable) -> Type[BaseModel]:
|
|
|
39
39
|
Turn a function signature into a `pydantic.BaseModel` subclass.
|
|
40
40
|
|
|
41
41
|
*Unknown* or *un-imported* annotations (common with third-party libs that
|
|
42
|
-
use forward-refs without importing the target
|
|
42
|
+
use forward-refs without importing the target - e.g. ``uuid.UUID`` in
|
|
43
43
|
LangChain's `CallbackManagerForToolRun`) default to ``str`` instead of
|
|
44
44
|
crashing `get_type_hints()`.
|
|
45
45
|
"""
|
|
@@ -90,7 +90,7 @@ async def register_fn_tool(
|
|
|
90
90
|
tool_description = (description or func.__doc__ or "").strip()
|
|
91
91
|
|
|
92
92
|
# Create the tool wrapper class
|
|
93
|
-
class _Tool: # noqa: D401, N801
|
|
93
|
+
class _Tool: # noqa: D401, N801 - internal auto-wrapper
|
|
94
94
|
"""Auto-generated tool wrapper for function."""
|
|
95
95
|
|
|
96
96
|
async def execute(self, **kwargs: Any) -> Any:
|
|
@@ -154,7 +154,7 @@ async def register_langchain_tool(
|
|
|
154
154
|
|
|
155
155
|
if not isinstance(tool, BaseTool): # pragma: no cover
|
|
156
156
|
raise TypeError(
|
|
157
|
-
"Expected a langchain.tools.base.BaseTool instance
|
|
157
|
+
"Expected a langchain.tools.base.BaseTool instance - got "
|
|
158
158
|
f"{type(tool).__name__}"
|
|
159
159
|
)
|
|
160
160
|
|
|
@@ -104,8 +104,8 @@ class ToolRegistryInterface(Protocol):
|
|
|
104
104
|
|
|
105
105
|
Args:
|
|
106
106
|
namespace: Optional filter by namespace.
|
|
107
|
-
• None (default)
|
|
108
|
-
• "some_ns"
|
|
107
|
+
• None (default) - metadata from all namespaces
|
|
108
|
+
• "some_ns" - only that namespace
|
|
109
109
|
|
|
110
110
|
Returns:
|
|
111
111
|
List of ToolMetadata objects.
|
|
@@ -125,8 +125,8 @@ class InMemoryToolRegistry(ToolRegistryInterface):
|
|
|
125
125
|
|
|
126
126
|
Args:
|
|
127
127
|
namespace: Optional filter by namespace.
|
|
128
|
-
• None (default)
|
|
129
|
-
• "some_ns"
|
|
128
|
+
• None (default) - metadata from all namespaces
|
|
129
|
+
• "some_ns" - only that namespace
|
|
130
130
|
|
|
131
131
|
Returns:
|
|
132
132
|
List of ToolMetadata objects.
|
|
@@ -25,7 +25,7 @@ __all__ = [
|
|
|
25
25
|
]
|
|
26
26
|
|
|
27
27
|
# --------------------------------------------------------------------------- #
|
|
28
|
-
# helpers
|
|
28
|
+
# helpers - create & cache ad-hoc pydantic models
|
|
29
29
|
# --------------------------------------------------------------------------- #
|
|
30
30
|
|
|
31
31
|
|
|
@@ -1,58 +1,58 @@
|
|
|
1
1
|
chuk_tool_processor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
chuk_tool_processor/core/__init__.py,sha256=slM7pZna88tyZrF3KtN22ApYyCqGNt5Yscv-knsLOOA,38
|
|
3
3
|
chuk_tool_processor/core/exceptions.py,sha256=h4zL1jpCY1Ud1wT8xDeMxZ8GR8ttmkObcv36peUHJEA,1571
|
|
4
|
-
chuk_tool_processor/core/processor.py,sha256=
|
|
4
|
+
chuk_tool_processor/core/processor.py,sha256=diquzXCQax6wxK-MLfezIJIjdCm9rkRYSFsWMHXU2A4,18367
|
|
5
5
|
chuk_tool_processor/execution/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
chuk_tool_processor/execution/tool_executor.py,sha256=SGnOrsQJ8b9dPD_2rYlRyp1WLcwn7pLfbrm5APOsQvo,14387
|
|
7
7
|
chuk_tool_processor/execution/strategies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
chuk_tool_processor/execution/strategies/inprocess_strategy.py,sha256=
|
|
8
|
+
chuk_tool_processor/execution/strategies/inprocess_strategy.py,sha256=Mn4gPC9ocxUNHYUMhtALLyR2-DOZphWmBoFSx6_Aelo,22578
|
|
9
9
|
chuk_tool_processor/execution/strategies/subprocess_strategy.py,sha256=Rb5GTffl-4dkAQG_zz8wjggqyWznVOr9gReLGHmE2io,22469
|
|
10
10
|
chuk_tool_processor/execution/wrappers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
chuk_tool_processor/execution/wrappers/caching.py,sha256=
|
|
11
|
+
chuk_tool_processor/execution/wrappers/caching.py,sha256=3KDb84ARhDKjUQXrpJT1FaO2pM8ciXFuEdr5zmeLiBM,20410
|
|
12
12
|
chuk_tool_processor/execution/wrappers/rate_limiting.py,sha256=CBBsI1VLosjo8dZXLeJ3IaclGvy9VdjGyqgunY089KQ,9231
|
|
13
|
-
chuk_tool_processor/execution/wrappers/retry.py,sha256=
|
|
13
|
+
chuk_tool_processor/execution/wrappers/retry.py,sha256=UgJ1UzOGx-HOXfMYmqVfRcMQKT6Q4l2eb4nJJ6nSSps,10144
|
|
14
14
|
chuk_tool_processor/logging/__init__.py,sha256=Uux0QnL6Xcd4vNjtt54rNei3yvxUZ-sOuAahlcPAez0,3382
|
|
15
|
-
chuk_tool_processor/logging/context.py,sha256=
|
|
15
|
+
chuk_tool_processor/logging/context.py,sha256=vPK7Z4TgLfsSl90exIE6mCqwdrjax_aDa9P6JqnbCsA,8508
|
|
16
16
|
chuk_tool_processor/logging/formatter.py,sha256=RhlV6NqBYRBOtytDY49c9Y1J4l02ZjNXIgVRn03tfSQ,3061
|
|
17
17
|
chuk_tool_processor/logging/helpers.py,sha256=c1mS1sb_rh4bKG0hisyvT7l7cirQfXPSyWeBqmqALRw,5941
|
|
18
18
|
chuk_tool_processor/logging/metrics.py,sha256=s59Au8q0eqGGtJMDqmJBZhbJHh4BWGE1CzT0iI8lRS8,3624
|
|
19
19
|
chuk_tool_processor/mcp/__init__.py,sha256=vR9HHxLpXlKTIIwJJRr3QTmZegcdedR1YKyb46j6FIM,689
|
|
20
|
-
chuk_tool_processor/mcp/mcp_tool.py,sha256=
|
|
21
|
-
chuk_tool_processor/mcp/register_mcp_tools.py,sha256=
|
|
22
|
-
chuk_tool_processor/mcp/setup_mcp_sse.py,sha256=
|
|
23
|
-
chuk_tool_processor/mcp/setup_mcp_stdio.py,sha256=
|
|
24
|
-
chuk_tool_processor/mcp/stream_manager.py,sha256=
|
|
20
|
+
chuk_tool_processor/mcp/mcp_tool.py,sha256=VOyz8uvUGlHOtfypuuOy8qkJx8cc7IIMIjZovQHSuCw,4870
|
|
21
|
+
chuk_tool_processor/mcp/register_mcp_tools.py,sha256=ZqR9kikHCbD7InL5cYl9ttUkhA5e4q2S76lbPLWe98I,3636
|
|
22
|
+
chuk_tool_processor/mcp/setup_mcp_sse.py,sha256=Sja3y1uBkqKfpGS69Y90KBb9XNDxKDu3GgQsFqgFiTU,3761
|
|
23
|
+
chuk_tool_processor/mcp/setup_mcp_stdio.py,sha256=emSL1IabdHoFdNUpEJNdzlc9-0qA51ZMuNJHpsYtw5o,2749
|
|
24
|
+
chuk_tool_processor/mcp/stream_manager.py,sha256=oho45ZdxJqYhDk3V9gTDQUvdxFaiPhyj2QRr4rLw4Mw,16902
|
|
25
25
|
chuk_tool_processor/mcp/transport/__init__.py,sha256=7QQqeSKVKv0N9GcyJuYF0R4FDZeooii5RjggvFFg5GY,296
|
|
26
|
-
chuk_tool_processor/mcp/transport/base_transport.py,sha256=
|
|
27
|
-
chuk_tool_processor/mcp/transport/sse_transport.py,sha256=
|
|
28
|
-
chuk_tool_processor/mcp/transport/stdio_transport.py,sha256=
|
|
26
|
+
chuk_tool_processor/mcp/transport/base_transport.py,sha256=bqId34OMQMxzMXtrKq_86sot0_x0NS_ecaIllsCyy6I,3423
|
|
27
|
+
chuk_tool_processor/mcp/transport/sse_transport.py,sha256=ZvPJzTJHLEOYCLs1fugyHIofhPzCvlh_3lGH7HxxM4I,19691
|
|
28
|
+
chuk_tool_processor/mcp/transport/stdio_transport.py,sha256=X9NgK_rU1Rw4DpHJ54Et8mICnRbutTZItHdP3PX3dU4,7759
|
|
29
29
|
chuk_tool_processor/models/__init__.py,sha256=TC__rdVa0lQsmJHM_hbLDPRgToa_pQT_UxRcPZk6iVw,40
|
|
30
30
|
chuk_tool_processor/models/execution_strategy.py,sha256=UVW35YIeMY2B3mpIKZD2rAkyOPayI6ckOOUALyf0YiQ,2115
|
|
31
31
|
chuk_tool_processor/models/streaming_tool.py,sha256=0v2PSPTgZ5TS_PpVdohvVhh99fPwPQM_R_z4RU0mlLM,3541
|
|
32
32
|
chuk_tool_processor/models/tool_call.py,sha256=t3QOQVzUaNRBea_n5fsrCgK4_ILiZlT8LvF7UwaZU84,1748
|
|
33
33
|
chuk_tool_processor/models/tool_export_mixin.py,sha256=U9NJvn9fqH3pW50ozdDAHlA0fQSnjUt-lYhEoW_leZU,998
|
|
34
34
|
chuk_tool_processor/models/tool_result.py,sha256=-8LdlaE9Ajfb1t-UyN9lW0XB2abQWALD7j8PZc-txhQ,5029
|
|
35
|
-
chuk_tool_processor/models/validated_tool.py,sha256=
|
|
35
|
+
chuk_tool_processor/models/validated_tool.py,sha256=m12jmaOsKek5pib1uXEOuDMILf9qoDzb-aqlp9wedMc,5896
|
|
36
36
|
chuk_tool_processor/plugins/__init__.py,sha256=QO_ipvlsWG-rbaqGzj6-YtD7zi7Lx26hw-Cqha4MuWc,48
|
|
37
|
-
chuk_tool_processor/plugins/discovery.py,sha256=
|
|
37
|
+
chuk_tool_processor/plugins/discovery.py,sha256=ZKWTJgrKzKnIuCPHg1CPu0j7e1L4R8y10Tq9IKXkj5M,7106
|
|
38
38
|
chuk_tool_processor/plugins/parsers/__init__.py,sha256=07waNfAGuytn-Dntx2IxjhgSkM9F40TBYNUXC8G4VVo,49
|
|
39
|
-
chuk_tool_processor/plugins/parsers/base.py,sha256=
|
|
39
|
+
chuk_tool_processor/plugins/parsers/base.py,sha256=sqBngdyCoUwUsTTJlT6722jN0QxxYz-ijtD4hmEK6mU,750
|
|
40
40
|
chuk_tool_processor/plugins/parsers/function_call_tool.py,sha256=kaUTMhIpDP4jZa_CJZiGchIoszK3AboZM2BJOXzPJmg,3198
|
|
41
41
|
chuk_tool_processor/plugins/parsers/json_tool.py,sha256=jljG7jR9070oHSTFqcrXbtFjYorw1cb4ZpwfrsLgFSE,1556
|
|
42
42
|
chuk_tool_processor/plugins/parsers/openai_tool.py,sha256=O33DyXN0Llqv7AHygE9sVQkbSDVNIOcNqqa1CaZF6vo,2849
|
|
43
|
-
chuk_tool_processor/plugins/parsers/xml_tool.py,sha256=
|
|
43
|
+
chuk_tool_processor/plugins/parsers/xml_tool.py,sha256=oIDSWOk38IUKBAMoGuN2hnrAhrhDuO_9L0-p77rEHJw,3182
|
|
44
44
|
chuk_tool_processor/registry/__init__.py,sha256=PBv5QyzHmUJEjgFQW1zgcUmppa5xUTjXTLVsYoVnDQI,2036
|
|
45
|
-
chuk_tool_processor/registry/auto_register.py,sha256=
|
|
45
|
+
chuk_tool_processor/registry/auto_register.py,sha256=cPod3t1Gng9Dz2-ujqpZBsrpOOcLd7UBJ7L6NqXzXqU,7242
|
|
46
46
|
chuk_tool_processor/registry/decorators.py,sha256=qFK-z7wd53BY7rEbqqWMrWQXbP4UaTLVTCQRI2iUhHI,5035
|
|
47
|
-
chuk_tool_processor/registry/interface.py,sha256=
|
|
47
|
+
chuk_tool_processor/registry/interface.py,sha256=sFacGnhcZXmeK7iF5fSm21ktecYTVzRiyGjxGWSkt-k,3463
|
|
48
48
|
chuk_tool_processor/registry/metadata.py,sha256=lR9FO__lhPr-ax7PhFk8Se1bJdN_48rabiN7qegx4Ns,4582
|
|
49
49
|
chuk_tool_processor/registry/provider.py,sha256=YUCGCFARNxnzV2Dm72Ur4DOrDs_ZYDtCzd6OBBzlis8,5162
|
|
50
50
|
chuk_tool_processor/registry/tool_export.py,sha256=FEdMsfZ3uhiOjoDHqcnSwNehAN7Z5mgi4nI1UKu2hgs,7569
|
|
51
51
|
chuk_tool_processor/registry/providers/__init__.py,sha256=eigwG_So11j7WbDGSWaKd3KwVen3Rap-aNQCSWA6T4k,2705
|
|
52
|
-
chuk_tool_processor/registry/providers/memory.py,sha256=
|
|
52
|
+
chuk_tool_processor/registry/providers/memory.py,sha256=6cMtUwLO6zrk3pguQRgxJ2CReHAzewgZsizWZhsoStk,5184
|
|
53
53
|
chuk_tool_processor/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
54
|
-
chuk_tool_processor/utils/validation.py,sha256=
|
|
55
|
-
chuk_tool_processor-0.4.dist-info/METADATA,sha256=
|
|
56
|
-
chuk_tool_processor-0.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
57
|
-
chuk_tool_processor-0.4.dist-info/top_level.txt,sha256=7lTsnuRx4cOW4U2sNJWNxl4ZTt_J1ndkjTbj3pHPY5M,20
|
|
58
|
-
chuk_tool_processor-0.4.dist-info/RECORD,,
|
|
54
|
+
chuk_tool_processor/utils/validation.py,sha256=V5N1dH9sJlHepFIbiI2k2MU82o7nvnh0hKyIt2jdgww,4136
|
|
55
|
+
chuk_tool_processor-0.4.1.dist-info/METADATA,sha256=fk4FL20xDVBGSZEa96BLL2R0ulRbGbUGnKgCruGzWIg,24314
|
|
56
|
+
chuk_tool_processor-0.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
57
|
+
chuk_tool_processor-0.4.1.dist-info/top_level.txt,sha256=7lTsnuRx4cOW4U2sNJWNxl4ZTt_J1ndkjTbj3pHPY5M,20
|
|
58
|
+
chuk_tool_processor-0.4.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|