chuk-tool-processor 0.6.11__py3-none-any.whl → 0.6.13__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 (56) hide show
  1. chuk_tool_processor/core/__init__.py +1 -1
  2. chuk_tool_processor/core/exceptions.py +10 -4
  3. chuk_tool_processor/core/processor.py +97 -97
  4. chuk_tool_processor/execution/strategies/inprocess_strategy.py +142 -150
  5. chuk_tool_processor/execution/strategies/subprocess_strategy.py +200 -205
  6. chuk_tool_processor/execution/tool_executor.py +82 -84
  7. chuk_tool_processor/execution/wrappers/caching.py +102 -103
  8. chuk_tool_processor/execution/wrappers/rate_limiting.py +45 -42
  9. chuk_tool_processor/execution/wrappers/retry.py +23 -25
  10. chuk_tool_processor/logging/__init__.py +23 -17
  11. chuk_tool_processor/logging/context.py +40 -45
  12. chuk_tool_processor/logging/formatter.py +22 -21
  13. chuk_tool_processor/logging/helpers.py +24 -38
  14. chuk_tool_processor/logging/metrics.py +11 -13
  15. chuk_tool_processor/mcp/__init__.py +8 -12
  16. chuk_tool_processor/mcp/mcp_tool.py +153 -109
  17. chuk_tool_processor/mcp/register_mcp_tools.py +17 -17
  18. chuk_tool_processor/mcp/setup_mcp_http_streamable.py +11 -13
  19. chuk_tool_processor/mcp/setup_mcp_sse.py +11 -13
  20. chuk_tool_processor/mcp/setup_mcp_stdio.py +7 -9
  21. chuk_tool_processor/mcp/stream_manager.py +168 -204
  22. chuk_tool_processor/mcp/transport/__init__.py +4 -4
  23. chuk_tool_processor/mcp/transport/base_transport.py +43 -58
  24. chuk_tool_processor/mcp/transport/http_streamable_transport.py +145 -163
  25. chuk_tool_processor/mcp/transport/sse_transport.py +266 -252
  26. chuk_tool_processor/mcp/transport/stdio_transport.py +171 -189
  27. chuk_tool_processor/models/__init__.py +1 -1
  28. chuk_tool_processor/models/execution_strategy.py +16 -21
  29. chuk_tool_processor/models/streaming_tool.py +28 -25
  30. chuk_tool_processor/models/tool_call.py +19 -34
  31. chuk_tool_processor/models/tool_export_mixin.py +22 -8
  32. chuk_tool_processor/models/tool_result.py +40 -77
  33. chuk_tool_processor/models/validated_tool.py +14 -16
  34. chuk_tool_processor/plugins/__init__.py +1 -1
  35. chuk_tool_processor/plugins/discovery.py +10 -10
  36. chuk_tool_processor/plugins/parsers/__init__.py +1 -1
  37. chuk_tool_processor/plugins/parsers/base.py +1 -2
  38. chuk_tool_processor/plugins/parsers/function_call_tool.py +13 -8
  39. chuk_tool_processor/plugins/parsers/json_tool.py +4 -3
  40. chuk_tool_processor/plugins/parsers/openai_tool.py +12 -7
  41. chuk_tool_processor/plugins/parsers/xml_tool.py +4 -4
  42. chuk_tool_processor/registry/__init__.py +12 -12
  43. chuk_tool_processor/registry/auto_register.py +22 -30
  44. chuk_tool_processor/registry/decorators.py +127 -129
  45. chuk_tool_processor/registry/interface.py +26 -23
  46. chuk_tool_processor/registry/metadata.py +27 -22
  47. chuk_tool_processor/registry/provider.py +17 -18
  48. chuk_tool_processor/registry/providers/__init__.py +16 -19
  49. chuk_tool_processor/registry/providers/memory.py +18 -25
  50. chuk_tool_processor/registry/tool_export.py +42 -51
  51. chuk_tool_processor/utils/validation.py +15 -16
  52. {chuk_tool_processor-0.6.11.dist-info → chuk_tool_processor-0.6.13.dist-info}/METADATA +1 -1
  53. chuk_tool_processor-0.6.13.dist-info/RECORD +60 -0
  54. chuk_tool_processor-0.6.11.dist-info/RECORD +0 -60
  55. {chuk_tool_processor-0.6.11.dist-info → chuk_tool_processor-0.6.13.dist-info}/WHEEL +0 -0
  56. {chuk_tool_processor-0.6.11.dist-info → chuk_tool_processor-0.6.13.dist-info}/top_level.txt +0 -0
@@ -7,31 +7,33 @@ This strategy executes tools in separate Python processes using a process pool,
7
7
  providing isolation and potentially better parallelism on multi-core systems.
8
8
 
9
9
  Enhanced tool name resolution that properly handles:
10
- - Simple names: "get_current_time"
10
+ - Simple names: "get_current_time"
11
11
  - Namespaced names: "diagnostic_test.get_current_time"
12
12
  - Cross-namespace fallback searching
13
13
 
14
14
  Properly handles tool serialization and ensures tool_name is preserved.
15
15
  """
16
+
16
17
  from __future__ import annotations
17
18
 
18
19
  import asyncio
19
20
  import concurrent.futures
21
+ import contextlib
20
22
  import functools
21
23
  import inspect
22
24
  import os
23
25
  import pickle
26
+ import platform
24
27
  import signal
25
- import sys
26
- import traceback
27
- from datetime import datetime, timezone
28
- from typing import Any, AsyncIterator, Dict, List, Optional, Tuple, Set
28
+ from collections.abc import AsyncIterator
29
+ from datetime import UTC, datetime
30
+ from typing import Any
29
31
 
32
+ from chuk_tool_processor.logging import get_logger, log_context_span
30
33
  from chuk_tool_processor.models.execution_strategy import ExecutionStrategy
31
34
  from chuk_tool_processor.models.tool_call import ToolCall
32
35
  from chuk_tool_processor.models.tool_result import ToolResult
33
36
  from chuk_tool_processor.registry.interface import ToolRegistryInterface
34
- from chuk_tool_processor.logging import get_logger, log_context_span
35
37
 
36
38
  logger = get_logger("chuk_tool_processor.execution.subprocess_strategy")
37
39
 
@@ -44,7 +46,7 @@ def _init_worker():
44
46
  """Initialize worker process with signal handlers."""
45
47
  # Ignore keyboard interrupt in workers
46
48
  signal.signal(signal.SIGINT, signal.SIG_IGN)
47
-
49
+
48
50
 
49
51
  def _pool_test_func():
50
52
  """Simple function to test if the process pool is working."""
@@ -52,38 +54,34 @@ def _pool_test_func():
52
54
 
53
55
 
54
56
  def _serialized_tool_worker(
55
- tool_name: str,
56
- namespace: str,
57
- arguments: Dict[str, Any],
58
- timeout: Optional[float],
59
- serialized_tool_data: bytes
60
- ) -> Dict[str, Any]:
57
+ tool_name: str, namespace: str, arguments: dict[str, Any], timeout: float | None, serialized_tool_data: bytes
58
+ ) -> dict[str, Any]:
61
59
  """
62
60
  Worker function that uses serialized tools and ensures tool_name is available.
63
-
61
+
64
62
  This worker deserializes the complete tool and executes it, with multiple
65
63
  fallbacks to ensure tool_name is properly set.
66
-
64
+
67
65
  Args:
68
66
  tool_name: Name of the tool
69
67
  namespace: Namespace of the tool
70
68
  arguments: Arguments to pass to the tool
71
69
  timeout: Optional timeout in seconds
72
70
  serialized_tool_data: Pickled tool instance
73
-
71
+
74
72
  Returns:
75
73
  Serialized result data
76
74
  """
77
75
  import asyncio
78
- import pickle
79
- import os
80
76
  import inspect
81
- from datetime import datetime, timezone
82
-
83
- start_time = datetime.now(timezone.utc)
77
+ import os
78
+ import pickle
79
+ from datetime import datetime
80
+
81
+ start_time = datetime.now(UTC)
84
82
  pid = os.getpid()
85
- hostname = os.uname().nodename
86
-
83
+ hostname = platform.node()
84
+
87
85
  result_data = {
88
86
  "tool": tool_name,
89
87
  "namespace": namespace,
@@ -94,17 +92,17 @@ def _serialized_tool_worker(
94
92
  "result": None,
95
93
  "error": None,
96
94
  }
97
-
95
+
98
96
  try:
99
97
  # Deserialize the complete tool
100
98
  tool = pickle.loads(serialized_tool_data)
101
-
99
+
102
100
  # Multiple fallbacks to ensure tool_name is available
103
-
101
+
104
102
  # Fallback 1: If tool doesn't have tool_name, set it directly
105
- if not hasattr(tool, 'tool_name') or not tool.tool_name:
103
+ if not hasattr(tool, "tool_name") or not tool.tool_name:
106
104
  tool.tool_name = tool_name
107
-
105
+
108
106
  # Fallback 2: If it's a class instead of instance, instantiate it
109
107
  if inspect.isclass(tool):
110
108
  try:
@@ -112,46 +110,44 @@ def _serialized_tool_worker(
112
110
  tool.tool_name = tool_name
113
111
  except Exception as e:
114
112
  result_data["error"] = f"Failed to instantiate tool class: {str(e)}"
115
- result_data["end_time"] = datetime.now(timezone.utc).isoformat()
113
+ result_data["end_time"] = datetime.now(UTC).isoformat()
116
114
  return result_data
117
-
115
+
118
116
  # Fallback 3: Ensure tool_name exists using setattr
119
- if not getattr(tool, 'tool_name', None):
120
- setattr(tool, 'tool_name', tool_name)
121
-
117
+ if not getattr(tool, "tool_name", None):
118
+ tool.tool_name = tool_name
119
+
122
120
  # Fallback 4: Verify execute method exists
123
- if not hasattr(tool, 'execute'):
124
- result_data["error"] = f"Tool missing execute method"
125
- result_data["end_time"] = datetime.now(timezone.utc).isoformat()
121
+ if not hasattr(tool, "execute"):
122
+ result_data["error"] = "Tool missing execute method"
123
+ result_data["end_time"] = datetime.now(UTC).isoformat()
126
124
  return result_data
127
-
125
+
128
126
  # Create event loop for execution
129
127
  loop = asyncio.new_event_loop()
130
128
  asyncio.set_event_loop(loop)
131
-
129
+
132
130
  try:
133
131
  # Execute the tool with timeout
134
132
  if timeout is not None and timeout > 0:
135
- result_value = loop.run_until_complete(
136
- asyncio.wait_for(tool.execute(**arguments), timeout)
137
- )
133
+ result_value = loop.run_until_complete(asyncio.wait_for(tool.execute(**arguments), timeout))
138
134
  else:
139
135
  result_value = loop.run_until_complete(tool.execute(**arguments))
140
-
136
+
141
137
  result_data["result"] = result_value
142
-
143
- except asyncio.TimeoutError:
138
+
139
+ except TimeoutError:
144
140
  result_data["error"] = f"Tool execution timed out after {timeout}s"
145
141
  except Exception as e:
146
142
  result_data["error"] = f"Tool execution failed: {str(e)}"
147
-
143
+
148
144
  finally:
149
145
  loop.close()
150
-
146
+
151
147
  except Exception as e:
152
148
  result_data["error"] = f"Worker error: {str(e)}"
153
-
154
- result_data["end_time"] = datetime.now(timezone.utc).isoformat()
149
+
150
+ result_data["end_time"] = datetime.now(UTC).isoformat()
155
151
  return result_data
156
152
 
157
153
 
@@ -161,11 +157,11 @@ def _serialized_tool_worker(
161
157
  class SubprocessStrategy(ExecutionStrategy):
162
158
  """
163
159
  Execute tools in separate processes for isolation and parallelism.
164
-
160
+
165
161
  This strategy creates a pool of worker processes and distributes tool calls
166
162
  among them. Each tool executes in its own process, providing isolation and
167
163
  parallelism.
168
-
164
+
169
165
  Enhanced tool name resolution and proper tool serialization.
170
166
  """
171
167
 
@@ -174,12 +170,12 @@ class SubprocessStrategy(ExecutionStrategy):
174
170
  registry: ToolRegistryInterface,
175
171
  *,
176
172
  max_workers: int = 4,
177
- default_timeout: Optional[float] = None,
173
+ default_timeout: float | None = None,
178
174
  worker_init_timeout: float = 5.0,
179
175
  ) -> None:
180
176
  """
181
177
  Initialize the subprocess execution strategy.
182
-
178
+
183
179
  Args:
184
180
  registry: Tool registry for tool lookups
185
181
  max_workers: Maximum number of worker processes
@@ -190,26 +186,25 @@ class SubprocessStrategy(ExecutionStrategy):
190
186
  self.max_workers = max_workers
191
187
  self.default_timeout = default_timeout or 30.0 # Always have a default
192
188
  self.worker_init_timeout = worker_init_timeout
193
-
189
+
194
190
  # Process pool (initialized lazily)
195
- self._process_pool: Optional[concurrent.futures.ProcessPoolExecutor] = None
191
+ self._process_pool: concurrent.futures.ProcessPoolExecutor | None = None
196
192
  self._pool_lock = asyncio.Lock()
197
-
193
+
198
194
  # Task tracking for cleanup
199
- self._active_tasks: Set[asyncio.Task] = set()
195
+ self._active_tasks: set[asyncio.Task] = set()
200
196
  self._shutdown_event = asyncio.Event()
201
197
  self._shutting_down = False
202
-
203
- logger.debug("SubprocessStrategy initialized with timeout: %ss, max_workers: %d",
204
- self.default_timeout, max_workers)
205
-
198
+
199
+ logger.debug(
200
+ "SubprocessStrategy initialized with timeout: %ss, max_workers: %d", self.default_timeout, max_workers
201
+ )
202
+
206
203
  # Register shutdown handler if in main thread
207
204
  try:
208
205
  loop = asyncio.get_running_loop()
209
206
  for sig in (signal.SIGTERM, signal.SIGINT):
210
- loop.add_signal_handler(
211
- sig, lambda s=sig: asyncio.create_task(self._signal_handler(s))
212
- )
207
+ loop.add_signal_handler(sig, lambda s=sig: asyncio.create_task(self._signal_handler(s)))
213
208
  except (RuntimeError, NotImplementedError):
214
209
  # Not in the main thread or not on Unix
215
210
  pass
@@ -218,24 +213,23 @@ class SubprocessStrategy(ExecutionStrategy):
218
213
  """Initialize the process pool if not already initialized."""
219
214
  if self._process_pool is not None:
220
215
  return
221
-
216
+
222
217
  async with self._pool_lock:
223
218
  if self._process_pool is not None:
224
219
  return
225
-
220
+
226
221
  # Create process pool
227
222
  self._process_pool = concurrent.futures.ProcessPoolExecutor(
228
223
  max_workers=self.max_workers,
229
224
  initializer=_init_worker,
230
225
  )
231
-
226
+
232
227
  # Test the pool with a simple task
233
228
  loop = asyncio.get_running_loop()
234
229
  try:
235
230
  # Use a module-level function instead of a lambda
236
231
  await asyncio.wait_for(
237
- loop.run_in_executor(self._process_pool, _pool_test_func),
238
- timeout=self.worker_init_timeout
232
+ loop.run_in_executor(self._process_pool, _pool_test_func), timeout=self.worker_init_timeout
239
233
  )
240
234
  logger.info("Process pool initialized with %d workers", self.max_workers)
241
235
  except Exception as e:
@@ -244,16 +238,16 @@ class SubprocessStrategy(ExecutionStrategy):
244
238
  self._process_pool = None
245
239
  logger.error("Failed to initialize process pool: %s", e)
246
240
  raise RuntimeError(f"Failed to initialize process pool: {e}") from e
247
-
241
+
248
242
  # ------------------------------------------------------------------ #
249
243
  # 🔌 legacy façade for older wrappers #
250
244
  # ------------------------------------------------------------------ #
251
245
  async def execute(
252
246
  self,
253
- calls: List[ToolCall],
247
+ calls: list[ToolCall],
254
248
  *,
255
- timeout: Optional[float] = None,
256
- ) -> List[ToolResult]:
249
+ timeout: float | None = None,
250
+ ) -> list[ToolResult]:
257
251
  """
258
252
  Back-compat shim.
259
253
 
@@ -262,25 +256,25 @@ class SubprocessStrategy(ExecutionStrategy):
262
256
  The real implementation lives in :meth:`run`, so we just forward.
263
257
  """
264
258
  return await self.run(calls, timeout)
265
-
259
+
266
260
  async def run(
267
261
  self,
268
- calls: List[ToolCall],
269
- timeout: Optional[float] = None,
270
- ) -> List[ToolResult]:
262
+ calls: list[ToolCall],
263
+ timeout: float | None = None,
264
+ ) -> list[ToolResult]:
271
265
  """
272
266
  Execute tool calls in separate processes.
273
-
267
+
274
268
  Args:
275
269
  calls: List of tool calls to execute
276
270
  timeout: Optional timeout for each execution (overrides default)
277
-
271
+
278
272
  Returns:
279
273
  List of tool results in the same order as calls
280
274
  """
281
275
  if not calls:
282
276
  return []
283
-
277
+
284
278
  if self._shutting_down:
285
279
  # Return early with error results if shutting down
286
280
  return [
@@ -288,50 +282,53 @@ class SubprocessStrategy(ExecutionStrategy):
288
282
  tool=call.tool,
289
283
  result=None,
290
284
  error="System is shutting down",
291
- start_time=datetime.now(timezone.utc),
292
- end_time=datetime.now(timezone.utc),
293
- machine=os.uname().nodename,
285
+ start_time=datetime.now(UTC),
286
+ end_time=datetime.now(UTC),
287
+ machine=platform.node(),
294
288
  pid=os.getpid(),
295
289
  )
296
290
  for call in calls
297
291
  ]
298
-
292
+
299
293
  # Use default_timeout if no timeout specified
300
294
  effective_timeout = timeout if timeout is not None else self.default_timeout
301
295
  logger.debug("Executing %d calls in subprocesses with %ss timeout each", len(calls), effective_timeout)
302
-
296
+
303
297
  # Create tasks for each call
304
298
  tasks = []
305
299
  for call in calls:
306
- task = asyncio.create_task(self._execute_single_call(
307
- call, effective_timeout # Always pass concrete timeout
308
- ))
300
+ task = asyncio.create_task(
301
+ self._execute_single_call(
302
+ call,
303
+ effective_timeout, # Always pass concrete timeout
304
+ )
305
+ )
309
306
  self._active_tasks.add(task)
310
307
  task.add_done_callback(self._active_tasks.discard)
311
308
  tasks.append(task)
312
-
309
+
313
310
  # Execute all tasks concurrently
314
311
  async with log_context_span("subprocess_execution", {"num_calls": len(calls)}):
315
312
  return await asyncio.gather(*tasks)
316
313
 
317
314
  async def stream_run(
318
315
  self,
319
- calls: List[ToolCall],
320
- timeout: Optional[float] = None,
316
+ calls: list[ToolCall],
317
+ timeout: float | None = None,
321
318
  ) -> AsyncIterator[ToolResult]:
322
319
  """
323
320
  Execute tool calls and yield results as they become available.
324
-
321
+
325
322
  Args:
326
323
  calls: List of tool calls to execute
327
324
  timeout: Optional timeout for each execution
328
-
325
+
329
326
  Yields:
330
327
  Tool results as they complete (not necessarily in order)
331
328
  """
332
329
  if not calls:
333
330
  return
334
-
331
+
335
332
  if self._shutting_down:
336
333
  # Yield error results if shutting down
337
334
  for call in calls:
@@ -339,40 +336,42 @@ class SubprocessStrategy(ExecutionStrategy):
339
336
  tool=call.tool,
340
337
  result=None,
341
338
  error="System is shutting down",
342
- start_time=datetime.now(timezone.utc),
343
- end_time=datetime.now(timezone.utc),
344
- machine=os.uname().nodename,
339
+ start_time=datetime.now(UTC),
340
+ end_time=datetime.now(UTC),
341
+ machine=platform.node(),
345
342
  pid=os.getpid(),
346
343
  )
347
344
  return
348
-
345
+
349
346
  # Use default_timeout if no timeout specified
350
347
  effective_timeout = timeout if timeout is not None else self.default_timeout
351
-
348
+
352
349
  # Create a queue for results
353
350
  queue = asyncio.Queue()
354
-
351
+
355
352
  # Start all executions and have them put results in the queue
356
353
  pending = set()
357
354
  for call in calls:
358
- task = asyncio.create_task(self._execute_to_queue(
359
- call, queue, effective_timeout # Always pass concrete timeout
360
- ))
355
+ task = asyncio.create_task(
356
+ self._execute_to_queue(
357
+ call,
358
+ queue,
359
+ effective_timeout, # Always pass concrete timeout
360
+ )
361
+ )
361
362
  self._active_tasks.add(task)
362
363
  task.add_done_callback(self._active_tasks.discard)
363
364
  pending.add(task)
364
-
365
+
365
366
  # Yield results as they become available
366
367
  while pending:
367
368
  # Get next result from queue
368
369
  result = await queue.get()
369
370
  yield result
370
-
371
+
371
372
  # Check for completed tasks
372
- done, pending = await asyncio.wait(
373
- pending, timeout=0, return_when=asyncio.FIRST_COMPLETED
374
- )
375
-
373
+ done, pending = await asyncio.wait(pending, timeout=0, return_when=asyncio.FIRST_COMPLETED)
374
+
376
375
  # Handle any exceptions
377
376
  for task in done:
378
377
  try:
@@ -397,22 +396,22 @@ class SubprocessStrategy(ExecutionStrategy):
397
396
  ) -> ToolResult:
398
397
  """
399
398
  Execute a single tool call with enhanced tool resolution and serialization.
400
-
399
+
401
400
  Args:
402
401
  call: Tool call to execute
403
402
  timeout: Timeout in seconds (required)
404
-
403
+
405
404
  Returns:
406
405
  Tool execution result
407
406
  """
408
- start_time = datetime.now(timezone.utc)
409
-
407
+ start_time = datetime.now(UTC)
408
+
410
409
  logger.debug("Executing %s in subprocess with %ss timeout", call.tool, timeout)
411
-
410
+
412
411
  try:
413
412
  # Ensure pool is initialized
414
413
  await self._ensure_pool()
415
-
414
+
416
415
  # Use enhanced tool resolution instead of direct lookup
417
416
  tool_impl, resolved_namespace = await self._resolve_tool_info(call.tool, call.namespace)
418
417
  if tool_impl is None:
@@ -421,29 +420,24 @@ class SubprocessStrategy(ExecutionStrategy):
421
420
  result=None,
422
421
  error=f"Tool '{call.tool}' not found in any namespace",
423
422
  start_time=start_time,
424
- end_time=datetime.now(timezone.utc),
425
- machine=os.uname().nodename,
423
+ end_time=datetime.now(UTC),
424
+ machine=platform.node(),
426
425
  pid=os.getpid(),
427
426
  )
428
-
427
+
429
428
  logger.debug(f"Resolved subprocess tool '{call.tool}' to namespace '{resolved_namespace}'")
430
-
429
+
431
430
  # Ensure tool is properly prepared before serialization
432
- if inspect.isclass(tool_impl):
433
- tool = tool_impl()
434
- else:
435
- tool = tool_impl
436
-
431
+ tool = tool_impl() if inspect.isclass(tool_impl) else tool_impl
432
+
437
433
  # Ensure tool_name attribute exists
438
- if not hasattr(tool, 'tool_name'):
434
+ if not hasattr(tool, "tool_name") or not tool.tool_name:
439
435
  tool.tool_name = call.tool
440
- elif not tool.tool_name:
441
- tool.tool_name = call.tool
442
-
436
+
443
437
  # Also set _tool_name class attribute for consistency
444
- if not hasattr(tool.__class__, '_tool_name'):
438
+ if not hasattr(tool.__class__, "_tool_name"):
445
439
  tool.__class__._tool_name = call.tool
446
-
440
+
447
441
  # Serialize the properly prepared tool
448
442
  try:
449
443
  serialized_tool_data = pickle.dumps(tool)
@@ -455,15 +449,15 @@ class SubprocessStrategy(ExecutionStrategy):
455
449
  result=None,
456
450
  error=f"Tool serialization failed: {str(e)}",
457
451
  start_time=start_time,
458
- end_time=datetime.now(timezone.utc),
459
- machine=os.uname().nodename,
452
+ end_time=datetime.now(UTC),
453
+ machine=platform.node(),
460
454
  pid=os.getpid(),
461
455
  )
462
-
456
+
463
457
  # Execute in subprocess using the FIXED worker
464
458
  loop = asyncio.get_running_loop()
465
459
  safety_timeout = timeout + 5.0
466
-
460
+
467
461
  try:
468
462
  result_data = await asyncio.wait_for(
469
463
  loop.run_in_executor(
@@ -474,29 +468,29 @@ class SubprocessStrategy(ExecutionStrategy):
474
468
  resolved_namespace, # Use resolved namespace
475
469
  call.arguments,
476
470
  timeout,
477
- serialized_tool_data # Pass serialized tool data
478
- )
471
+ serialized_tool_data, # Pass serialized tool data
472
+ ),
479
473
  ),
480
- timeout=safety_timeout
474
+ timeout=safety_timeout,
481
475
  )
482
-
476
+
483
477
  # Parse timestamps
484
478
  if isinstance(result_data["start_time"], str):
485
479
  result_data["start_time"] = datetime.fromisoformat(result_data["start_time"])
486
-
480
+
487
481
  if isinstance(result_data["end_time"], str):
488
482
  result_data["end_time"] = datetime.fromisoformat(result_data["end_time"])
489
-
490
- end_time = datetime.now(timezone.utc)
483
+
484
+ end_time = datetime.now(UTC)
491
485
  actual_duration = (end_time - start_time).total_seconds()
492
-
486
+
493
487
  if result_data.get("error"):
494
- logger.debug("%s subprocess failed after %.3fs: %s",
495
- call.tool, actual_duration, result_data["error"])
488
+ logger.debug(
489
+ "%s subprocess failed after %.3fs: %s", call.tool, actual_duration, result_data["error"]
490
+ )
496
491
  else:
497
- logger.debug("%s subprocess completed in %.3fs (limit: %ss)",
498
- call.tool, actual_duration, timeout)
499
-
492
+ logger.debug("%s subprocess completed in %.3fs (limit: %ss)", call.tool, actual_duration, timeout)
493
+
500
494
  # Create ToolResult from worker data
501
495
  return ToolResult(
502
496
  tool=result_data.get("tool", call.tool),
@@ -504,42 +498,46 @@ class SubprocessStrategy(ExecutionStrategy):
504
498
  error=result_data.get("error"),
505
499
  start_time=result_data.get("start_time", start_time),
506
500
  end_time=result_data.get("end_time", end_time),
507
- machine=result_data.get("machine", os.uname().nodename),
501
+ machine=result_data.get("machine", platform.node()),
508
502
  pid=result_data.get("pid", os.getpid()),
509
503
  )
510
-
511
- except asyncio.TimeoutError:
512
- end_time = datetime.now(timezone.utc)
504
+
505
+ except TimeoutError:
506
+ end_time = datetime.now(UTC)
513
507
  actual_duration = (end_time - start_time).total_seconds()
514
- logger.debug("%s subprocess timed out after %.3fs (safety limit: %ss)",
515
- call.tool, actual_duration, safety_timeout)
516
-
508
+ logger.debug(
509
+ "%s subprocess timed out after %.3fs (safety limit: %ss)",
510
+ call.tool,
511
+ actual_duration,
512
+ safety_timeout,
513
+ )
514
+
517
515
  return ToolResult(
518
516
  tool=call.tool,
519
517
  result=None,
520
518
  error=f"Worker process timed out after {safety_timeout}s",
521
519
  start_time=start_time,
522
520
  end_time=end_time,
523
- machine=os.uname().nodename,
521
+ machine=platform.node(),
524
522
  pid=os.getpid(),
525
523
  )
526
-
524
+
527
525
  except concurrent.futures.process.BrokenProcessPool:
528
526
  logger.error("Process pool broke during execution - recreating")
529
527
  if self._process_pool:
530
528
  self._process_pool.shutdown(wait=False)
531
529
  self._process_pool = None
532
-
530
+
533
531
  return ToolResult(
534
532
  tool=call.tool,
535
533
  result=None,
536
534
  error="Worker process crashed",
537
535
  start_time=start_time,
538
- end_time=datetime.now(timezone.utc),
539
- machine=os.uname().nodename,
536
+ end_time=datetime.now(UTC),
537
+ machine=platform.node(),
540
538
  pid=os.getpid(),
541
539
  )
542
-
540
+
543
541
  except asyncio.CancelledError:
544
542
  logger.debug("%s subprocess was cancelled", call.tool)
545
543
  return ToolResult(
@@ -547,54 +545,55 @@ class SubprocessStrategy(ExecutionStrategy):
547
545
  result=None,
548
546
  error="Execution was cancelled",
549
547
  start_time=start_time,
550
- end_time=datetime.now(timezone.utc),
551
- machine=os.uname().nodename,
548
+ end_time=datetime.now(UTC),
549
+ machine=platform.node(),
552
550
  pid=os.getpid(),
553
551
  )
554
-
552
+
555
553
  except Exception as e:
556
554
  logger.exception("Error executing %s in subprocess: %s", call.tool, e)
557
- end_time = datetime.now(timezone.utc)
555
+ end_time = datetime.now(UTC)
558
556
  actual_duration = (end_time - start_time).total_seconds()
559
- logger.debug("%s subprocess setup failed after %.3fs: %s",
560
- call.tool, actual_duration, e)
561
-
557
+ logger.debug("%s subprocess setup failed after %.3fs: %s", call.tool, actual_duration, e)
558
+
562
559
  return ToolResult(
563
560
  tool=call.tool,
564
561
  result=None,
565
562
  error=f"Error: {str(e)}",
566
563
  start_time=start_time,
567
564
  end_time=end_time,
568
- machine=os.uname().nodename,
565
+ machine=platform.node(),
569
566
  pid=os.getpid(),
570
567
  )
571
568
 
572
- async def _resolve_tool_info(self, tool_name: str, preferred_namespace: str = "default") -> Tuple[Optional[Any], Optional[str]]:
569
+ async def _resolve_tool_info(
570
+ self, tool_name: str, preferred_namespace: str = "default"
571
+ ) -> tuple[Any | None, str | None]:
573
572
  """
574
573
  Enhanced tool name resolution with comprehensive fallback logic.
575
-
574
+
576
575
  This method handles:
577
576
  1. Simple names: "get_current_time" -> search in specified namespace first, then all namespaces
578
577
  2. Namespaced names: "diagnostic_test.get_current_time" -> extract namespace and tool name
579
578
  3. Fallback searching across all namespaces when not found in default
580
-
579
+
581
580
  Args:
582
581
  tool_name: Name of the tool to resolve
583
582
  preferred_namespace: Preferred namespace to search first
584
-
583
+
585
584
  Returns:
586
585
  Tuple of (tool_object, resolved_namespace) or (None, None) if not found
587
586
  """
588
587
  logger.debug(f"Resolving tool: '{tool_name}' (preferred namespace: '{preferred_namespace}')")
589
-
588
+
590
589
  # Strategy 1: Handle namespaced tool names (namespace.tool_name format)
591
- if '.' in tool_name:
592
- parts = tool_name.split('.', 1) # Split on first dot only
590
+ if "." in tool_name:
591
+ parts = tool_name.split(".", 1) # Split on first dot only
593
592
  namespace = parts[0]
594
593
  actual_tool_name = parts[1]
595
-
594
+
596
595
  logger.debug(f"Namespaced lookup: namespace='{namespace}', tool='{actual_tool_name}'")
597
-
596
+
598
597
  tool = await self.registry.get_tool(actual_tool_name, namespace)
599
598
  if tool is not None:
600
599
  logger.debug(f"Found tool '{actual_tool_name}' in namespace '{namespace}'")
@@ -602,7 +601,7 @@ class SubprocessStrategy(ExecutionStrategy):
602
601
  else:
603
602
  logger.debug(f"Tool '{actual_tool_name}' not found in namespace '{namespace}'")
604
603
  return None, None
605
-
604
+
606
605
  # Strategy 2: Simple tool name - try preferred namespace first
607
606
  if preferred_namespace:
608
607
  logger.debug(f"Simple tool lookup: trying preferred namespace '{preferred_namespace}' for '{tool_name}'")
@@ -610,7 +609,7 @@ class SubprocessStrategy(ExecutionStrategy):
610
609
  if tool is not None:
611
610
  logger.debug(f"Found tool '{tool_name}' in preferred namespace '{preferred_namespace}'")
612
611
  return tool, preferred_namespace
613
-
612
+
614
613
  # Strategy 3: Try default namespace if different from preferred
615
614
  if preferred_namespace != "default":
616
615
  logger.debug(f"Simple tool lookup: trying default namespace for '{tool_name}'")
@@ -618,30 +617,30 @@ class SubprocessStrategy(ExecutionStrategy):
618
617
  if tool is not None:
619
618
  logger.debug(f"Found tool '{tool_name}' in default namespace")
620
619
  return tool, "default"
621
-
620
+
622
621
  # Strategy 4: Search all namespaces as fallback
623
622
  logger.debug(f"Tool '{tool_name}' not in preferred/default namespace, searching all namespaces...")
624
-
623
+
625
624
  try:
626
625
  # Get all available namespaces
627
626
  namespaces = await self.registry.list_namespaces()
628
627
  logger.debug(f"Available namespaces: {namespaces}")
629
-
628
+
630
629
  # Search each namespace
631
630
  for namespace in namespaces:
632
631
  if namespace in [preferred_namespace, "default"]:
633
632
  continue # Already tried these
634
-
633
+
635
634
  logger.debug(f"Searching namespace '{namespace}' for tool '{tool_name}'")
636
635
  tool = await self.registry.get_tool(tool_name, namespace)
637
636
  if tool is not None:
638
637
  logger.debug(f"Found tool '{tool_name}' in namespace '{namespace}'")
639
638
  return tool, namespace
640
-
639
+
641
640
  # Strategy 5: Final fallback - list all tools and do fuzzy matching
642
641
  logger.debug(f"Tool '{tool_name}' not found in any namespace, trying fuzzy matching...")
643
642
  all_tools = await self.registry.list_tools()
644
-
643
+
645
644
  # Look for exact matches in tool name (ignoring namespace)
646
645
  for namespace, registered_name in all_tools:
647
646
  if registered_name == tool_name:
@@ -649,13 +648,13 @@ class SubprocessStrategy(ExecutionStrategy):
649
648
  tool = await self.registry.get_tool(registered_name, namespace)
650
649
  if tool is not None:
651
650
  return tool, namespace
652
-
651
+
653
652
  # Log all available tools for debugging
654
653
  logger.debug(f"Available tools: {all_tools}")
655
-
654
+
656
655
  except Exception as e:
657
656
  logger.error(f"Error during namespace search: {e}")
658
-
657
+
659
658
  logger.warning(f"Tool '{tool_name}' not found in any namespace")
660
659
  return None, None
661
660
 
@@ -663,26 +662,26 @@ class SubprocessStrategy(ExecutionStrategy):
663
662
  def supports_streaming(self) -> bool:
664
663
  """Check if this strategy supports streaming execution."""
665
664
  return True
666
-
665
+
667
666
  async def _signal_handler(self, sig: int) -> None:
668
667
  """Handle termination signals."""
669
668
  signame = signal.Signals(sig).name
670
669
  logger.info("Received %s, shutting down process pool", signame)
671
670
  await self.shutdown()
672
-
671
+
673
672
  async def shutdown(self) -> None:
674
673
  """Enhanced shutdown with graceful task handling and proper null checks."""
675
674
  if self._shutting_down:
676
675
  return
677
-
676
+
678
677
  self._shutting_down = True
679
678
  self._shutdown_event.set()
680
-
679
+
681
680
  # Handle active tasks gracefully
682
681
  active_tasks = list(self._active_tasks)
683
682
  if active_tasks:
684
683
  logger.debug(f"Completing {len(active_tasks)} active operations")
685
-
684
+
686
685
  # Cancel tasks with brief intervals for clean handling
687
686
  for task in active_tasks:
688
687
  try:
@@ -691,22 +690,18 @@ class SubprocessStrategy(ExecutionStrategy):
691
690
  except Exception:
692
691
  pass
693
692
  # Small delay to prevent overwhelming the event loop
694
- try:
693
+ with contextlib.suppress(Exception):
695
694
  await asyncio.sleep(0.001)
696
- except Exception:
697
- pass
698
-
695
+
699
696
  # Allow reasonable time for completion
700
697
  try:
701
- completion_task = asyncio.create_task(
702
- asyncio.gather(*active_tasks, return_exceptions=True)
703
- )
698
+ completion_task = asyncio.create_task(asyncio.gather(*active_tasks, return_exceptions=True))
704
699
  await asyncio.wait_for(completion_task, timeout=2.0)
705
- except asyncio.TimeoutError:
700
+ except TimeoutError:
706
701
  logger.debug("Active operations completed within timeout constraints")
707
702
  except Exception:
708
703
  logger.debug("Active operations completed successfully")
709
-
704
+
710
705
  # Handle process pool shutdown with proper null checks
711
706
  if self._process_pool is not None:
712
707
  logger.debug("Finalizing process pool")
@@ -714,18 +709,18 @@ class SubprocessStrategy(ExecutionStrategy):
714
709
  # Store reference and null check before async operation
715
710
  pool_to_shutdown = self._process_pool
716
711
  self._process_pool = None # Clear immediately to prevent race conditions
717
-
712
+
718
713
  # Create shutdown task with the stored reference
719
714
  shutdown_task = asyncio.create_task(
720
715
  asyncio.get_event_loop().run_in_executor(
721
716
  None, lambda: pool_to_shutdown.shutdown(wait=False) if pool_to_shutdown else None
722
717
  )
723
718
  )
724
-
719
+
725
720
  try:
726
721
  await asyncio.wait_for(shutdown_task, timeout=1.0)
727
722
  logger.debug("Process pool shutdown completed")
728
- except asyncio.TimeoutError:
723
+ except TimeoutError:
729
724
  logger.debug("Process pool shutdown timed out, forcing cleanup")
730
725
  if not shutdown_task.done():
731
726
  shutdown_task.cancel()
@@ -734,4 +729,4 @@ class SubprocessStrategy(ExecutionStrategy):
734
729
  except Exception as e:
735
730
  logger.debug(f"Process pool finalization completed: {e}")
736
731
  else:
737
- logger.debug("Process pool already cleaned up")
732
+ logger.debug("Process pool already cleaned up")