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.

Files changed (66) hide show
  1. chuk_tool_processor/core/__init__.py +32 -1
  2. chuk_tool_processor/core/exceptions.py +225 -13
  3. chuk_tool_processor/core/processor.py +135 -104
  4. chuk_tool_processor/execution/strategies/__init__.py +6 -0
  5. chuk_tool_processor/execution/strategies/inprocess_strategy.py +142 -150
  6. chuk_tool_processor/execution/strategies/subprocess_strategy.py +202 -206
  7. chuk_tool_processor/execution/tool_executor.py +82 -84
  8. chuk_tool_processor/execution/wrappers/__init__.py +42 -0
  9. chuk_tool_processor/execution/wrappers/caching.py +150 -116
  10. chuk_tool_processor/execution/wrappers/circuit_breaker.py +370 -0
  11. chuk_tool_processor/execution/wrappers/rate_limiting.py +76 -43
  12. chuk_tool_processor/execution/wrappers/retry.py +116 -78
  13. chuk_tool_processor/logging/__init__.py +23 -17
  14. chuk_tool_processor/logging/context.py +40 -45
  15. chuk_tool_processor/logging/formatter.py +22 -21
  16. chuk_tool_processor/logging/helpers.py +28 -42
  17. chuk_tool_processor/logging/metrics.py +13 -15
  18. chuk_tool_processor/mcp/__init__.py +8 -12
  19. chuk_tool_processor/mcp/mcp_tool.py +158 -114
  20. chuk_tool_processor/mcp/register_mcp_tools.py +22 -22
  21. chuk_tool_processor/mcp/setup_mcp_http_streamable.py +57 -17
  22. chuk_tool_processor/mcp/setup_mcp_sse.py +57 -17
  23. chuk_tool_processor/mcp/setup_mcp_stdio.py +11 -11
  24. chuk_tool_processor/mcp/stream_manager.py +333 -276
  25. chuk_tool_processor/mcp/transport/__init__.py +22 -29
  26. chuk_tool_processor/mcp/transport/base_transport.py +180 -44
  27. chuk_tool_processor/mcp/transport/http_streamable_transport.py +505 -325
  28. chuk_tool_processor/mcp/transport/models.py +100 -0
  29. chuk_tool_processor/mcp/transport/sse_transport.py +607 -276
  30. chuk_tool_processor/mcp/transport/stdio_transport.py +597 -116
  31. chuk_tool_processor/models/__init__.py +21 -1
  32. chuk_tool_processor/models/execution_strategy.py +16 -21
  33. chuk_tool_processor/models/streaming_tool.py +28 -25
  34. chuk_tool_processor/models/tool_call.py +49 -31
  35. chuk_tool_processor/models/tool_export_mixin.py +22 -8
  36. chuk_tool_processor/models/tool_result.py +40 -77
  37. chuk_tool_processor/models/tool_spec.py +350 -0
  38. chuk_tool_processor/models/validated_tool.py +36 -18
  39. chuk_tool_processor/observability/__init__.py +30 -0
  40. chuk_tool_processor/observability/metrics.py +312 -0
  41. chuk_tool_processor/observability/setup.py +105 -0
  42. chuk_tool_processor/observability/tracing.py +345 -0
  43. chuk_tool_processor/plugins/__init__.py +1 -1
  44. chuk_tool_processor/plugins/discovery.py +11 -11
  45. chuk_tool_processor/plugins/parsers/__init__.py +1 -1
  46. chuk_tool_processor/plugins/parsers/base.py +1 -2
  47. chuk_tool_processor/plugins/parsers/function_call_tool.py +13 -8
  48. chuk_tool_processor/plugins/parsers/json_tool.py +4 -3
  49. chuk_tool_processor/plugins/parsers/openai_tool.py +12 -7
  50. chuk_tool_processor/plugins/parsers/xml_tool.py +4 -4
  51. chuk_tool_processor/registry/__init__.py +12 -12
  52. chuk_tool_processor/registry/auto_register.py +22 -30
  53. chuk_tool_processor/registry/decorators.py +127 -129
  54. chuk_tool_processor/registry/interface.py +26 -23
  55. chuk_tool_processor/registry/metadata.py +27 -22
  56. chuk_tool_processor/registry/provider.py +17 -18
  57. chuk_tool_processor/registry/providers/__init__.py +16 -19
  58. chuk_tool_processor/registry/providers/memory.py +18 -25
  59. chuk_tool_processor/registry/tool_export.py +42 -51
  60. chuk_tool_processor/utils/validation.py +15 -16
  61. chuk_tool_processor-0.9.7.dist-info/METADATA +1813 -0
  62. chuk_tool_processor-0.9.7.dist-info/RECORD +67 -0
  63. chuk_tool_processor-0.6.4.dist-info/METADATA +0 -697
  64. chuk_tool_processor-0.6.4.dist-info/RECORD +0 -60
  65. {chuk_tool_processor-0.6.4.dist-info → chuk_tool_processor-0.9.7.dist-info}/WHEEL +0 -0
  66. {chuk_tool_processor-0.6.4.dist-info → chuk_tool_processor-0.9.7.dist-info}/top_level.txt +0 -0
@@ -8,15 +8,17 @@ to enable true item-by-item streaming behavior, while preventing duplicates.
8
8
 
9
9
  Proper timeout precedence - respects strategy's default_timeout when available.
10
10
  """
11
+
11
12
  import asyncio
12
- from datetime import datetime, timezone
13
- from typing import Any, Dict, List, Optional, AsyncIterator, Set
13
+ from collections.abc import AsyncIterator
14
+ from datetime import UTC, datetime
15
+ from typing import Any
14
16
 
17
+ from chuk_tool_processor.logging import get_logger
15
18
  from chuk_tool_processor.models.execution_strategy import ExecutionStrategy
16
19
  from chuk_tool_processor.models.tool_call import ToolCall
17
20
  from chuk_tool_processor.models.tool_result import ToolResult
18
21
  from chuk_tool_processor.registry.interface import ToolRegistryInterface
19
- from chuk_tool_processor.logging import get_logger
20
22
 
21
23
  logger = get_logger("chuk_tool_processor.execution.tool_executor")
22
24
 
@@ -24,23 +26,23 @@ logger = get_logger("chuk_tool_processor.execution.tool_executor")
24
26
  class ToolExecutor:
25
27
  """
26
28
  Async-native executor that selects and uses a strategy for tool execution.
27
-
29
+
28
30
  This class provides a unified interface for executing tools using different
29
31
  execution strategies, with special support for streaming tools.
30
-
32
+
31
33
  FIXED: Proper timeout handling that respects strategy's default_timeout.
32
34
  """
33
35
 
34
36
  def __init__(
35
37
  self,
36
- registry: Optional[ToolRegistryInterface] = None,
37
- default_timeout: Optional[float] = None, # Made optional to allow strategy precedence
38
- strategy: Optional[ExecutionStrategy] = None,
39
- strategy_kwargs: Optional[Dict[str, Any]] = None,
38
+ registry: ToolRegistryInterface | None = None,
39
+ default_timeout: float | None = None, # Made optional to allow strategy precedence
40
+ strategy: ExecutionStrategy | None = None,
41
+ strategy_kwargs: dict[str, Any] | None = None,
40
42
  ) -> None:
41
43
  """
42
44
  Initialize the tool executor.
43
-
45
+
44
46
  Args:
45
47
  registry: Tool registry to use for tool lookups
46
48
  default_timeout: Default timeout for tool execution (optional)
@@ -49,28 +51,28 @@ class ToolExecutor:
49
51
  strategy_kwargs: Additional arguments for the strategy constructor
50
52
  """
51
53
  self.registry = registry
52
-
54
+
53
55
  # Create strategy if not provided
54
56
  if strategy is None:
55
57
  # Lazy import to allow for circular imports
56
58
  import chuk_tool_processor.execution.strategies.inprocess_strategy as _inprocess_mod
57
-
59
+
58
60
  if registry is None:
59
61
  raise ValueError("Registry must be provided if strategy is not")
60
-
62
+
61
63
  strategy_kwargs = strategy_kwargs or {}
62
-
64
+
63
65
  # If no default_timeout specified, use a reasonable default for the strategy
64
66
  strategy_timeout = default_timeout if default_timeout is not None else 30.0
65
-
67
+
66
68
  strategy = _inprocess_mod.InProcessStrategy(
67
69
  registry,
68
70
  default_timeout=strategy_timeout,
69
71
  **strategy_kwargs,
70
72
  )
71
-
73
+
72
74
  self.strategy = strategy
73
-
75
+
74
76
  # Set default timeout with proper precedence:
75
77
  # 1. Explicit default_timeout parameter
76
78
  # 2. Strategy's default_timeout (if available and not None)
@@ -78,7 +80,7 @@ class ToolExecutor:
78
80
  if default_timeout is not None:
79
81
  self.default_timeout = default_timeout
80
82
  logger.debug(f"Using explicit default_timeout: {self.default_timeout}s")
81
- elif hasattr(strategy, 'default_timeout') and strategy.default_timeout is not None:
83
+ elif hasattr(strategy, "default_timeout") and strategy.default_timeout is not None:
82
84
  self.default_timeout = strategy.default_timeout
83
85
  logger.debug(f"Using strategy's default_timeout: {self.default_timeout}s")
84
86
  else:
@@ -92,74 +94,77 @@ class ToolExecutor:
92
94
 
93
95
  async def execute(
94
96
  self,
95
- calls: List[ToolCall],
96
- timeout: Optional[float] = None,
97
- use_cache: bool = True,
98
- ) -> List[ToolResult]:
97
+ calls: list[ToolCall],
98
+ timeout: float | None = None,
99
+ use_cache: bool = True, # noqa: ARG002
100
+ ) -> list[ToolResult]:
99
101
  """
100
102
  Execute tool calls using the configured strategy.
101
-
103
+
102
104
  Args:
103
105
  calls: List of tool calls to execute
104
106
  timeout: Optional timeout for execution (overrides all defaults)
105
107
  use_cache: Whether to use cached results (for caching wrappers)
106
-
108
+
107
109
  Returns:
108
110
  List of tool results in the same order as calls
109
111
  """
110
112
  if not calls:
111
113
  return []
112
-
114
+
113
115
  # Timeout precedence:
114
116
  # 1. Explicit timeout parameter (highest priority)
115
117
  # 2. Executor's default_timeout (which already considers strategy's timeout)
116
118
  effective_timeout = timeout if timeout is not None else self.default_timeout
117
-
118
- logger.debug(f"Executing {len(calls)} tool calls with timeout {effective_timeout}s "
119
- f"(explicit: {timeout is not None})")
120
-
119
+
120
+ logger.debug(
121
+ f"Executing {len(calls)} tool calls with timeout {effective_timeout}s (explicit: {timeout is not None})"
122
+ )
123
+
121
124
  # Delegate to the strategy
122
125
  return await self.strategy.run(calls, timeout=effective_timeout)
123
-
126
+
124
127
  async def stream_execute(
125
128
  self,
126
- calls: List[ToolCall],
127
- timeout: Optional[float] = None,
129
+ calls: list[ToolCall],
130
+ timeout: float | None = None,
128
131
  ) -> AsyncIterator[ToolResult]:
129
132
  """
130
133
  Execute tool calls and yield results as they become available.
131
-
134
+
132
135
  For streaming tools, this directly accesses their stream_execute method
133
136
  to yield individual results as they are produced, rather than collecting
134
137
  them into lists.
135
-
138
+
136
139
  Args:
137
140
  calls: List of tool calls to execute
138
141
  timeout: Optional timeout for execution
139
-
142
+
140
143
  Yields:
141
144
  Tool results as they become available
142
145
  """
143
146
  if not calls:
144
147
  return
145
-
148
+
146
149
  # Use the same timeout precedence as execute()
147
150
  effective_timeout = timeout if timeout is not None else self.default_timeout
148
-
149
- logger.debug(f"Stream executing {len(calls)} tool calls with timeout {effective_timeout}s "
150
- f"(explicit: {timeout is not None})")
151
-
151
+
152
+ logger.debug(
153
+ f"Stream executing {len(calls)} tool calls with timeout {effective_timeout}s "
154
+ f"(explicit: {timeout is not None})"
155
+ )
156
+
152
157
  # There are two possible ways to handle streaming:
153
158
  # 1. Use the strategy's stream_run if available
154
159
  # 2. Use direct streaming for streaming tools
155
160
  # We'll choose one approach based on the tool types to avoid duplicates
156
-
161
+
157
162
  # Check if strategy supports streaming
158
163
  if hasattr(self.strategy, "stream_run") and self.strategy.supports_streaming:
159
164
  # Check for streaming tools
160
165
  streaming_tools = []
161
166
  non_streaming_tools = []
162
-
167
+
163
168
  for call in calls:
164
169
  # Check if the tool is a streaming tool
165
170
  tool_impl = await self.registry.get_tool(call.tool, call.namespace)
@@ -167,63 +172,62 @@ class ToolExecutor:
167
172
  # Tool not found - treat as non-streaming
168
173
  non_streaming_tools.append(call)
169
174
  continue
170
-
175
+
171
176
  # Instantiate if class
172
177
  tool = tool_impl() if callable(tool_impl) else tool_impl
173
-
178
+
174
179
  # Check for streaming support
175
180
  if hasattr(tool, "supports_streaming") and tool.supports_streaming and hasattr(tool, "stream_execute"):
176
181
  streaming_tools.append((call, tool))
177
182
  else:
178
183
  non_streaming_tools.append(call)
179
-
184
+
180
185
  # If we have streaming tools, handle them directly
181
186
  if streaming_tools:
182
187
  # Create a tracking queue for all results
183
188
  queue = asyncio.Queue()
184
-
189
+
185
190
  # Track processing to avoid duplicates
186
191
  processed_calls = set()
187
-
192
+
188
193
  # For streaming tools, create direct streaming tasks
189
194
  pending_tasks = set()
190
195
  for call, tool in streaming_tools:
191
196
  # Add to processed list to avoid duplication
192
197
  processed_calls.add(call.id)
193
-
198
+
194
199
  # Create task for direct streaming
195
- task = asyncio.create_task(self._direct_stream_tool(
196
- call, tool, queue, effective_timeout
197
- ))
200
+ task = asyncio.create_task(self._direct_stream_tool(call, tool, queue, effective_timeout))
198
201
  pending_tasks.add(task)
199
202
  task.add_done_callback(pending_tasks.discard)
200
-
203
+
201
204
  # For non-streaming tools, use the strategy's stream_run
202
205
  if non_streaming_tools:
206
+
203
207
  async def strategy_streamer():
204
208
  async for result in self.strategy.stream_run(non_streaming_tools, timeout=effective_timeout):
205
209
  await queue.put(result)
206
-
210
+
207
211
  strategy_task = asyncio.create_task(strategy_streamer())
208
212
  pending_tasks.add(strategy_task)
209
213
  strategy_task.add_done_callback(pending_tasks.discard)
210
-
214
+
211
215
  # Yield results as they arrive in the queue
212
216
  while pending_tasks:
213
217
  try:
214
218
  # Wait a short time for a result, then check task status
215
219
  result = await asyncio.wait_for(queue.get(), 0.1)
216
220
  yield result
217
- except asyncio.TimeoutError:
221
+ except TimeoutError:
218
222
  # Check if tasks have completed
219
223
  if not pending_tasks:
220
224
  break
221
-
225
+
222
226
  # Check for completed tasks
223
227
  done, pending_tasks = await asyncio.wait(
224
228
  pending_tasks, timeout=0, return_when=asyncio.FIRST_COMPLETED
225
229
  )
226
-
230
+
227
231
  # Handle any exceptions
228
232
  for task in done:
229
233
  try:
@@ -239,35 +243,29 @@ class ToolExecutor:
239
243
  results = await self.execute(calls, timeout=effective_timeout)
240
244
  for result in results:
241
245
  yield result
242
-
243
- async def _direct_stream_tool(
244
- self,
245
- call: ToolCall,
246
- tool: Any,
247
- queue: asyncio.Queue,
248
- timeout: Optional[float]
249
- ) -> None:
246
+
247
+ async def _direct_stream_tool(self, call: ToolCall, tool: Any, queue: asyncio.Queue, timeout: float | None) -> None:
250
248
  """
251
249
  Stream results directly from a streaming tool.
252
-
250
+
253
251
  Args:
254
252
  call: Tool call to execute
255
253
  tool: Tool instance
256
254
  queue: Queue to put results into
257
255
  timeout: Optional timeout in seconds
258
256
  """
259
- start_time = datetime.now(timezone.utc)
257
+ start_time = datetime.now(UTC)
260
258
  machine = "direct-stream"
261
259
  pid = 0
262
-
260
+
263
261
  logger.debug(f"Direct streaming {call.tool} with timeout {timeout}s")
264
-
262
+
265
263
  # Create streaming task with timeout
266
264
  async def stream_with_timeout():
267
265
  try:
268
266
  async for result in tool.stream_execute(**call.arguments):
269
267
  # Create a ToolResult for each result
270
- end_time = datetime.now(timezone.utc)
268
+ end_time = datetime.now(UTC)
271
269
  tool_result = ToolResult(
272
270
  tool=call.tool,
273
271
  result=result,
@@ -275,12 +273,12 @@ class ToolExecutor:
275
273
  start_time=start_time,
276
274
  end_time=end_time,
277
275
  machine=machine,
278
- pid=pid
276
+ pid=pid,
279
277
  )
280
278
  await queue.put(tool_result)
281
279
  except Exception as e:
282
280
  # Handle errors
283
- end_time = datetime.now(timezone.utc)
281
+ end_time = datetime.now(UTC)
284
282
  error_result = ToolResult(
285
283
  tool=call.tool,
286
284
  result=None,
@@ -288,10 +286,10 @@ class ToolExecutor:
288
286
  start_time=start_time,
289
287
  end_time=end_time,
290
288
  machine=machine,
291
- pid=pid
289
+ pid=pid,
292
290
  )
293
291
  await queue.put(error_result)
294
-
292
+
295
293
  try:
296
294
  if timeout:
297
295
  await asyncio.wait_for(stream_with_timeout(), timeout)
@@ -299,12 +297,12 @@ class ToolExecutor:
299
297
  else:
300
298
  await stream_with_timeout()
301
299
  logger.debug(f"Direct streaming {call.tool} completed (no timeout)")
302
- except asyncio.TimeoutError:
300
+ except TimeoutError:
303
301
  # Handle timeout
304
- end_time = datetime.now(timezone.utc)
302
+ end_time = datetime.now(UTC)
305
303
  actual_duration = (end_time - start_time).total_seconds()
306
304
  logger.debug(f"Direct streaming {call.tool} timed out after {actual_duration:.3f}s (limit: {timeout}s)")
307
-
305
+
308
306
  timeout_result = ToolResult(
309
307
  tool=call.tool,
310
308
  result=None,
@@ -312,14 +310,14 @@ class ToolExecutor:
312
310
  start_time=start_time,
313
311
  end_time=end_time,
314
312
  machine=machine,
315
- pid=pid
313
+ pid=pid,
316
314
  )
317
315
  await queue.put(timeout_result)
318
316
  except Exception as e:
319
317
  # Handle other errors
320
- end_time = datetime.now(timezone.utc)
318
+ end_time = datetime.now(UTC)
321
319
  logger.exception(f"Error in direct streaming {call.tool}: {e}")
322
-
320
+
323
321
  error_result = ToolResult(
324
322
  tool=call.tool,
325
323
  result=None,
@@ -327,16 +325,16 @@ class ToolExecutor:
327
325
  start_time=start_time,
328
326
  end_time=end_time,
329
327
  machine=machine,
330
- pid=pid
328
+ pid=pid,
331
329
  )
332
330
  await queue.put(error_result)
333
-
331
+
334
332
  async def shutdown(self) -> None:
335
333
  """Enhanced shutdown for ToolExecutor with strategy coordination."""
336
334
  logger.debug("Finalizing ToolExecutor operations")
337
-
335
+
338
336
  if hasattr(self.strategy, "shutdown") and callable(self.strategy.shutdown):
339
337
  try:
340
338
  await self.strategy.shutdown()
341
339
  except Exception as e:
342
- logger.debug(f"Strategy finalization completed: {e}")
340
+ logger.debug(f"Strategy finalization completed: {e}")
@@ -0,0 +1,42 @@
1
+ # chuk_tool_processor/execution/wrappers/__init__.py
2
+ """Execution wrappers for adding production features to tool execution."""
3
+
4
+ from chuk_tool_processor.execution.wrappers.caching import (
5
+ CacheInterface,
6
+ CachingToolExecutor,
7
+ InMemoryCache,
8
+ cacheable,
9
+ )
10
+ from chuk_tool_processor.execution.wrappers.circuit_breaker import (
11
+ CircuitBreakerConfig,
12
+ CircuitBreakerExecutor,
13
+ CircuitState,
14
+ )
15
+ from chuk_tool_processor.execution.wrappers.rate_limiting import (
16
+ RateLimitedToolExecutor,
17
+ RateLimiter,
18
+ )
19
+ from chuk_tool_processor.execution.wrappers.retry import (
20
+ RetryableToolExecutor,
21
+ RetryConfig,
22
+ retryable,
23
+ )
24
+
25
+ __all__ = [
26
+ # Caching
27
+ "CacheInterface",
28
+ "CachingToolExecutor",
29
+ "InMemoryCache",
30
+ "cacheable",
31
+ # Circuit breaker
32
+ "CircuitBreakerConfig",
33
+ "CircuitBreakerExecutor",
34
+ "CircuitState",
35
+ # Rate limiting
36
+ "RateLimitedToolExecutor",
37
+ "RateLimiter",
38
+ # Retry
39
+ "RetryableToolExecutor",
40
+ "RetryConfig",
41
+ "retryable",
42
+ ]