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
|
@@ -8,28 +8,31 @@ It has special support for streaming tools, accessing their stream_execute metho
|
|
|
8
8
|
directly to enable true item-by-item streaming.
|
|
9
9
|
|
|
10
10
|
Enhanced tool name resolution that properly handles:
|
|
11
|
-
- Simple names: "get_current_time"
|
|
11
|
+
- Simple names: "get_current_time"
|
|
12
12
|
- Namespaced names: "diagnostic_test.get_current_time"
|
|
13
13
|
- Cross-namespace fallback searching
|
|
14
14
|
|
|
15
15
|
Ensures consistent timeout handling across all execution paths.
|
|
16
16
|
ENHANCED: Clean shutdown handling to prevent anyio cancel scope errors.
|
|
17
17
|
"""
|
|
18
|
+
|
|
18
19
|
from __future__ import annotations
|
|
19
20
|
|
|
20
21
|
import asyncio
|
|
22
|
+
import builtins
|
|
21
23
|
import inspect
|
|
22
24
|
import os
|
|
23
|
-
|
|
24
|
-
from
|
|
25
|
-
from
|
|
25
|
+
import platform
|
|
26
|
+
from collections.abc import AsyncIterator
|
|
27
|
+
from contextlib import asynccontextmanager, suppress
|
|
28
|
+
from datetime import UTC, datetime
|
|
29
|
+
from typing import Any
|
|
26
30
|
|
|
27
|
-
from chuk_tool_processor.
|
|
31
|
+
from chuk_tool_processor.logging import get_logger, log_context_span
|
|
28
32
|
from chuk_tool_processor.models.execution_strategy import ExecutionStrategy
|
|
29
33
|
from chuk_tool_processor.models.tool_call import ToolCall
|
|
30
34
|
from chuk_tool_processor.models.tool_result import ToolResult
|
|
31
35
|
from chuk_tool_processor.registry.interface import ToolRegistryInterface
|
|
32
|
-
from chuk_tool_processor.logging import get_logger, log_context_span
|
|
33
36
|
|
|
34
37
|
logger = get_logger("chuk_tool_processor.execution.inprocess_strategy")
|
|
35
38
|
|
|
@@ -49,12 +52,12 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
49
52
|
def __init__(
|
|
50
53
|
self,
|
|
51
54
|
registry: ToolRegistryInterface,
|
|
52
|
-
default_timeout:
|
|
53
|
-
max_concurrency:
|
|
55
|
+
default_timeout: float | None = None,
|
|
56
|
+
max_concurrency: int | None = None,
|
|
54
57
|
) -> None:
|
|
55
58
|
"""
|
|
56
59
|
Initialize the in-process execution strategy.
|
|
57
|
-
|
|
60
|
+
|
|
58
61
|
Args:
|
|
59
62
|
registry: Tool registry to use for tool lookups
|
|
60
63
|
default_timeout: Default timeout for tool execution
|
|
@@ -63,43 +66,46 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
63
66
|
self.registry = registry
|
|
64
67
|
self.default_timeout = default_timeout or 30.0 # Always have a default
|
|
65
68
|
self._sem = asyncio.Semaphore(max_concurrency) if max_concurrency else None
|
|
66
|
-
|
|
69
|
+
|
|
67
70
|
# Task tracking for cleanup
|
|
68
71
|
self._active_tasks = set()
|
|
69
72
|
self._shutting_down = False
|
|
70
73
|
self._shutdown_event = asyncio.Event()
|
|
71
|
-
|
|
74
|
+
|
|
72
75
|
# Tracking for which calls are being handled directly by the executor
|
|
73
76
|
# to prevent duplicate streaming results
|
|
74
77
|
self._direct_streaming_calls = set()
|
|
75
|
-
|
|
76
|
-
logger.debug(
|
|
77
|
-
|
|
78
|
+
|
|
79
|
+
logger.debug(
|
|
80
|
+
"InProcessStrategy initialized with timeout: %ss, max_concurrency: %s",
|
|
81
|
+
self.default_timeout,
|
|
82
|
+
max_concurrency,
|
|
83
|
+
)
|
|
78
84
|
|
|
79
85
|
# ------------------------------------------------------------------ #
|
|
80
|
-
def mark_direct_streaming(self, call_ids:
|
|
86
|
+
def mark_direct_streaming(self, call_ids: set[str]) -> None:
|
|
81
87
|
"""
|
|
82
88
|
Mark tool calls that are being handled directly by the executor.
|
|
83
|
-
|
|
89
|
+
|
|
84
90
|
Args:
|
|
85
91
|
call_ids: Set of call IDs that should be skipped during streaming
|
|
86
92
|
because they're handled directly
|
|
87
93
|
"""
|
|
88
94
|
self._direct_streaming_calls.update(call_ids)
|
|
89
|
-
|
|
95
|
+
|
|
90
96
|
def clear_direct_streaming(self) -> None:
|
|
91
97
|
"""Clear the list of direct streaming calls."""
|
|
92
98
|
self._direct_streaming_calls.clear()
|
|
93
|
-
|
|
99
|
+
|
|
94
100
|
# ------------------------------------------------------------------ #
|
|
95
101
|
# 🔌 legacy façade for older wrappers #
|
|
96
102
|
# ------------------------------------------------------------------ #
|
|
97
103
|
async def execute(
|
|
98
104
|
self,
|
|
99
|
-
calls:
|
|
105
|
+
calls: list[ToolCall],
|
|
100
106
|
*,
|
|
101
|
-
timeout:
|
|
102
|
-
) ->
|
|
107
|
+
timeout: float | None = None,
|
|
108
|
+
) -> list[ToolResult]:
|
|
103
109
|
"""
|
|
104
110
|
Back-compat shim.
|
|
105
111
|
|
|
@@ -112,26 +118,26 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
112
118
|
# ------------------------------------------------------------------ #
|
|
113
119
|
async def run(
|
|
114
120
|
self,
|
|
115
|
-
calls:
|
|
116
|
-
timeout:
|
|
117
|
-
) ->
|
|
121
|
+
calls: list[ToolCall],
|
|
122
|
+
timeout: float | None = None,
|
|
123
|
+
) -> list[ToolResult]:
|
|
118
124
|
"""
|
|
119
125
|
Execute tool calls concurrently and preserve order.
|
|
120
|
-
|
|
126
|
+
|
|
121
127
|
Args:
|
|
122
128
|
calls: List of tool calls to execute
|
|
123
129
|
timeout: Optional timeout for execution
|
|
124
|
-
|
|
130
|
+
|
|
125
131
|
Returns:
|
|
126
132
|
List of tool results in the same order as calls
|
|
127
133
|
"""
|
|
128
134
|
if not calls:
|
|
129
135
|
return []
|
|
130
|
-
|
|
136
|
+
|
|
131
137
|
# Use default_timeout if no timeout specified
|
|
132
138
|
effective_timeout = timeout if timeout is not None else self.default_timeout
|
|
133
139
|
logger.debug("Executing %d calls with %ss timeout each", len(calls), effective_timeout)
|
|
134
|
-
|
|
140
|
+
|
|
135
141
|
tasks = []
|
|
136
142
|
for call in calls:
|
|
137
143
|
task = asyncio.create_task(
|
|
@@ -140,15 +146,15 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
140
146
|
self._active_tasks.add(task)
|
|
141
147
|
task.add_done_callback(self._active_tasks.discard)
|
|
142
148
|
tasks.append(task)
|
|
143
|
-
|
|
149
|
+
|
|
144
150
|
async with log_context_span("inprocess_execution", {"num_calls": len(calls)}):
|
|
145
151
|
return await asyncio.gather(*tasks)
|
|
146
152
|
|
|
147
153
|
# ------------------------------------------------------------------ #
|
|
148
154
|
async def stream_run(
|
|
149
155
|
self,
|
|
150
|
-
calls:
|
|
151
|
-
timeout:
|
|
156
|
+
calls: list[ToolCall],
|
|
157
|
+
timeout: float | None = None,
|
|
152
158
|
) -> AsyncIterator[ToolResult]:
|
|
153
159
|
"""
|
|
154
160
|
Execute tool calls concurrently and *yield* results as soon as they are
|
|
@@ -183,7 +189,6 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
183
189
|
for t in done:
|
|
184
190
|
t.result() # re-raise if a task crashed
|
|
185
191
|
|
|
186
|
-
|
|
187
192
|
async def _stream_tool_call(
|
|
188
193
|
self,
|
|
189
194
|
call: ToolCall,
|
|
@@ -192,10 +197,10 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
192
197
|
) -> None:
|
|
193
198
|
"""
|
|
194
199
|
Execute a tool call with streaming support.
|
|
195
|
-
|
|
200
|
+
|
|
196
201
|
This looks up the tool and if it's a streaming tool, it accesses
|
|
197
202
|
stream_execute directly to get item-by-item streaming.
|
|
198
|
-
|
|
203
|
+
|
|
199
204
|
Args:
|
|
200
205
|
call: The tool call to execute
|
|
201
206
|
queue: Queue to put results into
|
|
@@ -204,48 +209,48 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
204
209
|
# Skip if call is being handled directly by the executor
|
|
205
210
|
if call.id in self._direct_streaming_calls:
|
|
206
211
|
return
|
|
207
|
-
|
|
212
|
+
|
|
208
213
|
if self._shutting_down:
|
|
209
214
|
# Early exit if shutting down
|
|
210
|
-
now = datetime.now(
|
|
215
|
+
now = datetime.now(UTC)
|
|
211
216
|
result = ToolResult(
|
|
212
217
|
tool=call.tool,
|
|
213
218
|
result=None,
|
|
214
219
|
error="System is shutting down",
|
|
215
220
|
start_time=now,
|
|
216
221
|
end_time=now,
|
|
217
|
-
machine=
|
|
222
|
+
machine=platform.node(),
|
|
218
223
|
pid=os.getpid(),
|
|
219
224
|
)
|
|
220
225
|
await queue.put(result)
|
|
221
226
|
return
|
|
222
|
-
|
|
227
|
+
|
|
223
228
|
try:
|
|
224
229
|
# Use enhanced tool resolution instead of direct lookup
|
|
225
230
|
tool_impl, resolved_namespace = await self._resolve_tool_info(call.tool, call.namespace)
|
|
226
231
|
if tool_impl is None:
|
|
227
232
|
# Tool not found
|
|
228
|
-
now = datetime.now(
|
|
233
|
+
now = datetime.now(UTC)
|
|
229
234
|
result = ToolResult(
|
|
230
235
|
tool=call.tool,
|
|
231
236
|
result=None,
|
|
232
237
|
error=f"Tool '{call.tool}' not found in any namespace",
|
|
233
238
|
start_time=now,
|
|
234
239
|
end_time=now,
|
|
235
|
-
machine=
|
|
240
|
+
machine=platform.node(),
|
|
236
241
|
pid=os.getpid(),
|
|
237
242
|
)
|
|
238
243
|
await queue.put(result)
|
|
239
244
|
return
|
|
240
|
-
|
|
245
|
+
|
|
241
246
|
logger.debug(f"Resolved streaming tool '{call.tool}' to namespace '{resolved_namespace}'")
|
|
242
|
-
|
|
247
|
+
|
|
243
248
|
# Instantiate if class
|
|
244
249
|
tool = tool_impl() if inspect.isclass(tool_impl) else tool_impl
|
|
245
|
-
|
|
250
|
+
|
|
246
251
|
# Use semaphore if available
|
|
247
252
|
guard = self._sem if self._sem is not None else _noop_cm()
|
|
248
|
-
|
|
253
|
+
|
|
249
254
|
async with guard:
|
|
250
255
|
# Check if this is a streaming tool
|
|
251
256
|
if hasattr(tool, "supports_streaming") and tool.supports_streaming and hasattr(tool, "stream_execute"):
|
|
@@ -255,66 +260,66 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
255
260
|
# Use regular execution for non-streaming tools
|
|
256
261
|
result = await self._execute_single_call(call, timeout)
|
|
257
262
|
await queue.put(result)
|
|
258
|
-
|
|
263
|
+
|
|
259
264
|
except asyncio.CancelledError:
|
|
260
265
|
# Handle cancellation gracefully
|
|
261
|
-
now = datetime.now(
|
|
266
|
+
now = datetime.now(UTC)
|
|
262
267
|
result = ToolResult(
|
|
263
268
|
tool=call.tool,
|
|
264
269
|
result=None,
|
|
265
270
|
error="Execution was cancelled",
|
|
266
271
|
start_time=now,
|
|
267
272
|
end_time=now,
|
|
268
|
-
machine=
|
|
273
|
+
machine=platform.node(),
|
|
269
274
|
pid=os.getpid(),
|
|
270
275
|
)
|
|
271
276
|
await queue.put(result)
|
|
272
|
-
|
|
277
|
+
|
|
273
278
|
except Exception as e:
|
|
274
279
|
# Handle other errors
|
|
275
|
-
now = datetime.now(
|
|
280
|
+
now = datetime.now(UTC)
|
|
276
281
|
result = ToolResult(
|
|
277
282
|
tool=call.tool,
|
|
278
283
|
result=None,
|
|
279
284
|
error=f"Error setting up execution: {e}",
|
|
280
285
|
start_time=now,
|
|
281
286
|
end_time=now,
|
|
282
|
-
machine=
|
|
287
|
+
machine=platform.node(),
|
|
283
288
|
pid=os.getpid(),
|
|
284
289
|
)
|
|
285
290
|
await queue.put(result)
|
|
286
|
-
|
|
291
|
+
|
|
287
292
|
async def _stream_with_timeout(
|
|
288
|
-
self,
|
|
289
|
-
tool: Any,
|
|
290
|
-
call: ToolCall,
|
|
291
|
-
queue: asyncio.Queue,
|
|
293
|
+
self,
|
|
294
|
+
tool: Any,
|
|
295
|
+
call: ToolCall,
|
|
296
|
+
queue: asyncio.Queue,
|
|
292
297
|
timeout: float, # Make timeout required
|
|
293
298
|
) -> None:
|
|
294
299
|
"""
|
|
295
300
|
Stream results from a streaming tool with timeout support.
|
|
296
|
-
|
|
301
|
+
|
|
297
302
|
This method accesses the tool's stream_execute method directly
|
|
298
303
|
and puts each yielded result into the queue.
|
|
299
|
-
|
|
304
|
+
|
|
300
305
|
Args:
|
|
301
306
|
tool: The tool instance
|
|
302
307
|
call: Tool call data
|
|
303
308
|
queue: Queue to put results into
|
|
304
309
|
timeout: Timeout in seconds (required)
|
|
305
310
|
"""
|
|
306
|
-
start_time = datetime.now(
|
|
307
|
-
machine =
|
|
311
|
+
start_time = datetime.now(UTC)
|
|
312
|
+
machine = platform.node()
|
|
308
313
|
pid = os.getpid()
|
|
309
|
-
|
|
314
|
+
|
|
310
315
|
logger.debug("Streaming %s with %ss timeout", call.tool, timeout)
|
|
311
|
-
|
|
316
|
+
|
|
312
317
|
# Define the streaming task
|
|
313
318
|
async def streamer():
|
|
314
319
|
try:
|
|
315
320
|
async for result in tool.stream_execute(**call.arguments):
|
|
316
321
|
# Create a ToolResult for each streamed item
|
|
317
|
-
now = datetime.now(
|
|
322
|
+
now = datetime.now(UTC)
|
|
318
323
|
tool_result = ToolResult(
|
|
319
324
|
tool=call.tool,
|
|
320
325
|
result=result,
|
|
@@ -327,7 +332,7 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
327
332
|
await queue.put(tool_result)
|
|
328
333
|
except Exception as e:
|
|
329
334
|
# Handle errors during streaming
|
|
330
|
-
now = datetime.now(
|
|
335
|
+
now = datetime.now(UTC)
|
|
331
336
|
error_result = ToolResult(
|
|
332
337
|
tool=call.tool,
|
|
333
338
|
result=None,
|
|
@@ -338,19 +343,18 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
338
343
|
pid=pid,
|
|
339
344
|
)
|
|
340
345
|
await queue.put(error_result)
|
|
341
|
-
|
|
346
|
+
|
|
342
347
|
try:
|
|
343
348
|
# Always execute with timeout
|
|
344
349
|
await asyncio.wait_for(streamer(), timeout)
|
|
345
350
|
logger.debug("%s streaming completed within %ss", call.tool, timeout)
|
|
346
|
-
|
|
347
|
-
except
|
|
351
|
+
|
|
352
|
+
except TimeoutError:
|
|
348
353
|
# Handle timeout
|
|
349
|
-
now = datetime.now(
|
|
354
|
+
now = datetime.now(UTC)
|
|
350
355
|
actual_duration = (now - start_time).total_seconds()
|
|
351
|
-
logger.debug("%s streaming timed out after %.3fs (limit: %ss)",
|
|
352
|
-
|
|
353
|
-
|
|
356
|
+
logger.debug("%s streaming timed out after %.3fs (limit: %ss)", call.tool, actual_duration, timeout)
|
|
357
|
+
|
|
354
358
|
timeout_result = ToolResult(
|
|
355
359
|
tool=call.tool,
|
|
356
360
|
result=None,
|
|
@@ -361,12 +365,12 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
361
365
|
pid=pid,
|
|
362
366
|
)
|
|
363
367
|
await queue.put(timeout_result)
|
|
364
|
-
|
|
368
|
+
|
|
365
369
|
except Exception as e:
|
|
366
370
|
# Handle other errors
|
|
367
|
-
now = datetime.now(
|
|
371
|
+
now = datetime.now(UTC)
|
|
368
372
|
logger.debug("%s streaming failed: %s", call.tool, e)
|
|
369
|
-
|
|
373
|
+
|
|
370
374
|
error_result = ToolResult(
|
|
371
375
|
tool=call.tool,
|
|
372
376
|
result=None,
|
|
@@ -388,7 +392,7 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
388
392
|
# Skip if call is being handled directly by the executor
|
|
389
393
|
if call.id in self._direct_streaming_calls:
|
|
390
394
|
return
|
|
391
|
-
|
|
395
|
+
|
|
392
396
|
result = await self._execute_single_call(call, timeout)
|
|
393
397
|
await queue.put(result)
|
|
394
398
|
|
|
@@ -403,20 +407,20 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
403
407
|
|
|
404
408
|
The entire invocation - including argument validation - is wrapped
|
|
405
409
|
by the semaphore to honour *max_concurrency*.
|
|
406
|
-
|
|
410
|
+
|
|
407
411
|
Args:
|
|
408
412
|
call: Tool call to execute
|
|
409
413
|
timeout: Timeout in seconds (required)
|
|
410
|
-
|
|
414
|
+
|
|
411
415
|
Returns:
|
|
412
416
|
Tool execution result
|
|
413
417
|
"""
|
|
414
418
|
pid = os.getpid()
|
|
415
|
-
machine =
|
|
416
|
-
start = datetime.now(
|
|
417
|
-
|
|
419
|
+
machine = platform.node()
|
|
420
|
+
start = datetime.now(UTC)
|
|
421
|
+
|
|
418
422
|
logger.debug("Executing %s with %ss timeout", call.tool, timeout)
|
|
419
|
-
|
|
423
|
+
|
|
420
424
|
# Early exit if shutting down
|
|
421
425
|
if self._shutting_down:
|
|
422
426
|
return ToolResult(
|
|
@@ -424,7 +428,7 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
424
428
|
result=None,
|
|
425
429
|
error="System is shutting down",
|
|
426
430
|
start_time=start,
|
|
427
|
-
end_time=datetime.now(
|
|
431
|
+
end_time=datetime.now(UTC),
|
|
428
432
|
machine=machine,
|
|
429
433
|
pid=pid,
|
|
430
434
|
)
|
|
@@ -438,7 +442,7 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
438
442
|
result=None,
|
|
439
443
|
error=f"Tool '{call.tool}' not found in any namespace",
|
|
440
444
|
start_time=start,
|
|
441
|
-
end_time=datetime.now(
|
|
445
|
+
end_time=datetime.now(UTC),
|
|
442
446
|
machine=machine,
|
|
443
447
|
pid=pid,
|
|
444
448
|
)
|
|
@@ -447,15 +451,13 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
447
451
|
|
|
448
452
|
# Instantiate if class
|
|
449
453
|
tool = impl() if inspect.isclass(impl) else impl
|
|
450
|
-
|
|
454
|
+
|
|
451
455
|
# Use semaphore if available
|
|
452
456
|
guard = self._sem if self._sem is not None else _noop_cm()
|
|
453
457
|
|
|
454
458
|
try:
|
|
455
459
|
async with guard:
|
|
456
|
-
return await self._run_with_timeout(
|
|
457
|
-
tool, call, timeout, start, machine, pid
|
|
458
|
-
)
|
|
460
|
+
return await self._run_with_timeout(tool, call, timeout, start, machine, pid)
|
|
459
461
|
except Exception as exc:
|
|
460
462
|
logger.exception("Unexpected error while executing %s", call.tool)
|
|
461
463
|
return ToolResult(
|
|
@@ -463,7 +465,7 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
463
465
|
result=None,
|
|
464
466
|
error=f"Unexpected error: {exc}",
|
|
465
467
|
start_time=start,
|
|
466
|
-
end_time=datetime.now(
|
|
468
|
+
end_time=datetime.now(UTC),
|
|
467
469
|
machine=machine,
|
|
468
470
|
pid=pid,
|
|
469
471
|
)
|
|
@@ -474,7 +476,7 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
474
476
|
result=None,
|
|
475
477
|
error="Execution was cancelled",
|
|
476
478
|
start_time=start,
|
|
477
|
-
end_time=datetime.now(
|
|
479
|
+
end_time=datetime.now(UTC),
|
|
478
480
|
machine=machine,
|
|
479
481
|
pid=pid,
|
|
480
482
|
)
|
|
@@ -485,7 +487,7 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
485
487
|
result=None,
|
|
486
488
|
error=f"Setup error: {exc}",
|
|
487
489
|
start_time=start,
|
|
488
|
-
end_time=datetime.now(
|
|
490
|
+
end_time=datetime.now(UTC),
|
|
489
491
|
machine=machine,
|
|
490
492
|
pid=pid,
|
|
491
493
|
)
|
|
@@ -501,7 +503,7 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
501
503
|
) -> ToolResult:
|
|
502
504
|
"""
|
|
503
505
|
Resolve the correct async entry-point and invoke it with a guaranteed timeout.
|
|
504
|
-
|
|
506
|
+
|
|
505
507
|
Args:
|
|
506
508
|
tool: Tool instance
|
|
507
509
|
call: Tool call data
|
|
@@ -509,28 +511,23 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
509
511
|
start: Start time for the execution
|
|
510
512
|
machine: Machine name
|
|
511
513
|
pid: Process ID
|
|
512
|
-
|
|
514
|
+
|
|
513
515
|
Returns:
|
|
514
516
|
Tool execution result
|
|
515
517
|
"""
|
|
516
|
-
if hasattr(tool, "_aexecute") and inspect.iscoroutinefunction(
|
|
517
|
-
getattr(type(tool), "_aexecute", None)
|
|
518
|
-
):
|
|
518
|
+
if hasattr(tool, "_aexecute") and inspect.iscoroutinefunction(getattr(type(tool), "_aexecute", None)):
|
|
519
519
|
fn = tool._aexecute
|
|
520
|
-
elif hasattr(tool, "execute") and inspect.iscoroutinefunction(
|
|
521
|
-
getattr(tool, "execute", None)
|
|
522
|
-
):
|
|
520
|
+
elif hasattr(tool, "execute") and inspect.iscoroutinefunction(getattr(tool, "execute", None)):
|
|
523
521
|
fn = tool.execute
|
|
524
522
|
else:
|
|
525
523
|
return ToolResult(
|
|
526
524
|
tool=call.tool,
|
|
527
525
|
result=None,
|
|
528
526
|
error=(
|
|
529
|
-
"Tool must implement *async* '_aexecute' or 'execute'. "
|
|
530
|
-
"Synchronous entry-points are not supported."
|
|
527
|
+
"Tool must implement *async* '_aexecute' or 'execute'. Synchronous entry-points are not supported."
|
|
531
528
|
),
|
|
532
529
|
start_time=start,
|
|
533
|
-
end_time=datetime.now(
|
|
530
|
+
end_time=datetime.now(UTC),
|
|
534
531
|
machine=machine,
|
|
535
532
|
pid=pid,
|
|
536
533
|
)
|
|
@@ -538,15 +535,14 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
538
535
|
try:
|
|
539
536
|
# Always apply timeout
|
|
540
537
|
logger.debug("Applying %ss timeout to %s", timeout, call.tool)
|
|
541
|
-
|
|
538
|
+
|
|
542
539
|
try:
|
|
543
540
|
result_val = await asyncio.wait_for(fn(**call.arguments), timeout=timeout)
|
|
544
|
-
|
|
545
|
-
end_time = datetime.now(
|
|
541
|
+
|
|
542
|
+
end_time = datetime.now(UTC)
|
|
546
543
|
actual_duration = (end_time - start).total_seconds()
|
|
547
|
-
logger.debug("%s completed in %.3fs (limit: %ss)",
|
|
548
|
-
|
|
549
|
-
|
|
544
|
+
logger.debug("%s completed in %.3fs (limit: %ss)", call.tool, actual_duration, timeout)
|
|
545
|
+
|
|
550
546
|
return ToolResult(
|
|
551
547
|
tool=call.tool,
|
|
552
548
|
result=result_val,
|
|
@@ -556,13 +552,12 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
556
552
|
machine=machine,
|
|
557
553
|
pid=pid,
|
|
558
554
|
)
|
|
559
|
-
except
|
|
555
|
+
except TimeoutError:
|
|
560
556
|
# Handle timeout
|
|
561
|
-
end_time = datetime.now(
|
|
557
|
+
end_time = datetime.now(UTC)
|
|
562
558
|
actual_duration = (end_time - start).total_seconds()
|
|
563
|
-
logger.debug("%s timed out after %.3fs (limit: %ss)",
|
|
564
|
-
|
|
565
|
-
|
|
559
|
+
logger.debug("%s timed out after %.3fs (limit: %ss)", call.tool, actual_duration, timeout)
|
|
560
|
+
|
|
566
561
|
return ToolResult(
|
|
567
562
|
tool=call.tool,
|
|
568
563
|
result=None,
|
|
@@ -572,7 +567,7 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
572
567
|
machine=machine,
|
|
573
568
|
pid=pid,
|
|
574
569
|
)
|
|
575
|
-
|
|
570
|
+
|
|
576
571
|
except asyncio.CancelledError:
|
|
577
572
|
# Handle cancellation explicitly
|
|
578
573
|
logger.debug("%s was cancelled", call.tool)
|
|
@@ -581,16 +576,16 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
581
576
|
result=None,
|
|
582
577
|
error="Execution was cancelled",
|
|
583
578
|
start_time=start,
|
|
584
|
-
end_time=datetime.now(
|
|
579
|
+
end_time=datetime.now(UTC),
|
|
585
580
|
machine=machine,
|
|
586
581
|
pid=pid,
|
|
587
582
|
)
|
|
588
583
|
except Exception as exc:
|
|
589
584
|
logger.exception("Error executing %s: %s", call.tool, exc)
|
|
590
|
-
end_time = datetime.now(
|
|
585
|
+
end_time = datetime.now(UTC)
|
|
591
586
|
actual_duration = (end_time - start).total_seconds()
|
|
592
587
|
logger.debug("%s failed after %.3fs: %s", call.tool, actual_duration, exc)
|
|
593
|
-
|
|
588
|
+
|
|
594
589
|
return ToolResult(
|
|
595
590
|
tool=call.tool,
|
|
596
591
|
result=None,
|
|
@@ -601,32 +596,34 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
601
596
|
pid=pid,
|
|
602
597
|
)
|
|
603
598
|
|
|
604
|
-
async def _resolve_tool_info(
|
|
599
|
+
async def _resolve_tool_info(
|
|
600
|
+
self, tool_name: str, preferred_namespace: str = "default"
|
|
601
|
+
) -> tuple[Any | None, str | None]:
|
|
605
602
|
"""
|
|
606
603
|
Enhanced tool name resolution with comprehensive fallback logic.
|
|
607
|
-
|
|
604
|
+
|
|
608
605
|
This method handles:
|
|
609
606
|
1. Simple names: "get_current_time" -> search in specified namespace first, then all namespaces
|
|
610
607
|
2. Namespaced names: "diagnostic_test.get_current_time" -> extract namespace and tool name
|
|
611
608
|
3. Fallback searching across all namespaces when not found in default
|
|
612
|
-
|
|
609
|
+
|
|
613
610
|
Args:
|
|
614
611
|
tool_name: Name of the tool to resolve
|
|
615
612
|
preferred_namespace: Preferred namespace to search first
|
|
616
|
-
|
|
613
|
+
|
|
617
614
|
Returns:
|
|
618
615
|
Tuple of (tool_object, resolved_namespace) or (None, None) if not found
|
|
619
616
|
"""
|
|
620
617
|
logger.debug(f"Resolving tool: '{tool_name}' (preferred namespace: '{preferred_namespace}')")
|
|
621
|
-
|
|
618
|
+
|
|
622
619
|
# Strategy 1: Handle namespaced tool names (namespace.tool_name format)
|
|
623
|
-
if
|
|
624
|
-
parts = tool_name.split(
|
|
620
|
+
if "." in tool_name:
|
|
621
|
+
parts = tool_name.split(".", 1) # Split on first dot only
|
|
625
622
|
namespace = parts[0]
|
|
626
623
|
actual_tool_name = parts[1]
|
|
627
|
-
|
|
624
|
+
|
|
628
625
|
logger.debug(f"Namespaced lookup: namespace='{namespace}', tool='{actual_tool_name}'")
|
|
629
|
-
|
|
626
|
+
|
|
630
627
|
tool = await self.registry.get_tool(actual_tool_name, namespace)
|
|
631
628
|
if tool is not None:
|
|
632
629
|
logger.debug(f"Found tool '{actual_tool_name}' in namespace '{namespace}'")
|
|
@@ -634,7 +631,7 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
634
631
|
else:
|
|
635
632
|
logger.debug(f"Tool '{actual_tool_name}' not found in namespace '{namespace}'")
|
|
636
633
|
return None, None
|
|
637
|
-
|
|
634
|
+
|
|
638
635
|
# Strategy 2: Simple tool name - try preferred namespace first
|
|
639
636
|
if preferred_namespace:
|
|
640
637
|
logger.debug(f"Simple tool lookup: trying preferred namespace '{preferred_namespace}' for '{tool_name}'")
|
|
@@ -642,7 +639,7 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
642
639
|
if tool is not None:
|
|
643
640
|
logger.debug(f"Found tool '{tool_name}' in preferred namespace '{preferred_namespace}'")
|
|
644
641
|
return tool, preferred_namespace
|
|
645
|
-
|
|
642
|
+
|
|
646
643
|
# Strategy 3: Try default namespace if different from preferred
|
|
647
644
|
if preferred_namespace != "default":
|
|
648
645
|
logger.debug(f"Simple tool lookup: trying default namespace for '{tool_name}'")
|
|
@@ -650,30 +647,30 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
650
647
|
if tool is not None:
|
|
651
648
|
logger.debug(f"Found tool '{tool_name}' in default namespace")
|
|
652
649
|
return tool, "default"
|
|
653
|
-
|
|
650
|
+
|
|
654
651
|
# Strategy 4: Search all namespaces as fallback
|
|
655
652
|
logger.debug(f"Tool '{tool_name}' not in preferred/default namespace, searching all namespaces...")
|
|
656
|
-
|
|
653
|
+
|
|
657
654
|
try:
|
|
658
655
|
# Get all available namespaces
|
|
659
656
|
namespaces = await self.registry.list_namespaces()
|
|
660
657
|
logger.debug(f"Available namespaces: {namespaces}")
|
|
661
|
-
|
|
658
|
+
|
|
662
659
|
# Search each namespace
|
|
663
660
|
for namespace in namespaces:
|
|
664
661
|
if namespace in [preferred_namespace, "default"]:
|
|
665
662
|
continue # Already tried these
|
|
666
|
-
|
|
663
|
+
|
|
667
664
|
logger.debug(f"Searching namespace '{namespace}' for tool '{tool_name}'")
|
|
668
665
|
tool = await self.registry.get_tool(tool_name, namespace)
|
|
669
666
|
if tool is not None:
|
|
670
667
|
logger.debug(f"Found tool '{tool_name}' in namespace '{namespace}'")
|
|
671
668
|
return tool, namespace
|
|
672
|
-
|
|
669
|
+
|
|
673
670
|
# Strategy 5: Final fallback - list all tools and do fuzzy matching
|
|
674
671
|
logger.debug(f"Tool '{tool_name}' not found in any namespace, trying fuzzy matching...")
|
|
675
672
|
all_tools = await self.registry.list_tools()
|
|
676
|
-
|
|
673
|
+
|
|
677
674
|
# Look for exact matches in tool name (ignoring namespace)
|
|
678
675
|
for namespace, registered_name in all_tools:
|
|
679
676
|
if registered_name == tool_name:
|
|
@@ -681,40 +678,40 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
681
678
|
tool = await self.registry.get_tool(registered_name, namespace)
|
|
682
679
|
if tool is not None:
|
|
683
680
|
return tool, namespace
|
|
684
|
-
|
|
681
|
+
|
|
685
682
|
# Log all available tools for debugging
|
|
686
683
|
logger.debug(f"Available tools: {all_tools}")
|
|
687
|
-
|
|
684
|
+
|
|
688
685
|
except Exception as e:
|
|
689
686
|
logger.error(f"Error during namespace search: {e}")
|
|
690
|
-
|
|
687
|
+
|
|
691
688
|
logger.warning(f"Tool '{tool_name}' not found in any namespace")
|
|
692
689
|
return None, None
|
|
693
|
-
|
|
690
|
+
|
|
694
691
|
@property
|
|
695
692
|
def supports_streaming(self) -> bool:
|
|
696
693
|
"""Check if this strategy supports streaming execution."""
|
|
697
694
|
return True
|
|
698
|
-
|
|
695
|
+
|
|
699
696
|
async def shutdown(self) -> None:
|
|
700
697
|
"""
|
|
701
698
|
Enhanced shutdown with clean task management.
|
|
702
|
-
|
|
699
|
+
|
|
703
700
|
This version prevents anyio cancel scope errors by handling
|
|
704
701
|
task cancellation more gracefully with individual error handling
|
|
705
702
|
and reasonable timeouts.
|
|
706
703
|
"""
|
|
707
704
|
if self._shutting_down:
|
|
708
705
|
return
|
|
709
|
-
|
|
706
|
+
|
|
710
707
|
self._shutting_down = True
|
|
711
708
|
self._shutdown_event.set()
|
|
712
|
-
|
|
709
|
+
|
|
713
710
|
# Manage active tasks cleanly
|
|
714
711
|
active_tasks = list(self._active_tasks)
|
|
715
712
|
if active_tasks:
|
|
716
713
|
logger.debug(f"Completing {len(active_tasks)} in-process operations")
|
|
717
|
-
|
|
714
|
+
|
|
718
715
|
# Handle each task individually with brief delays
|
|
719
716
|
for task in active_tasks:
|
|
720
717
|
try:
|
|
@@ -723,17 +720,12 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
723
720
|
except Exception:
|
|
724
721
|
pass
|
|
725
722
|
# Small delay between cancellations to avoid overwhelming the event loop
|
|
726
|
-
|
|
723
|
+
with suppress(builtins.BaseException):
|
|
727
724
|
await asyncio.sleep(0.001)
|
|
728
|
-
|
|
729
|
-
pass
|
|
730
|
-
|
|
725
|
+
|
|
731
726
|
# Allow reasonable time for completion with timeout
|
|
732
727
|
try:
|
|
733
|
-
await asyncio.wait_for(
|
|
734
|
-
asyncio.gather(*active_tasks, return_exceptions=True),
|
|
735
|
-
timeout=2.0
|
|
736
|
-
)
|
|
728
|
+
await asyncio.wait_for(asyncio.gather(*active_tasks, return_exceptions=True), timeout=2.0)
|
|
737
729
|
except Exception:
|
|
738
730
|
# Suppress all errors during shutdown to prevent cancel scope issues
|
|
739
|
-
logger.debug("In-process operations completed within expected parameters")
|
|
731
|
+
logger.debug("In-process operations completed within expected parameters")
|