chuk-tool-processor 0.1.6__py3-none-any.whl → 0.1.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 (45) hide show
  1. chuk_tool_processor/core/processor.py +345 -132
  2. chuk_tool_processor/execution/strategies/inprocess_strategy.py +512 -68
  3. chuk_tool_processor/execution/strategies/subprocess_strategy.py +523 -63
  4. chuk_tool_processor/execution/tool_executor.py +282 -24
  5. chuk_tool_processor/execution/wrappers/caching.py +465 -123
  6. chuk_tool_processor/execution/wrappers/rate_limiting.py +199 -86
  7. chuk_tool_processor/execution/wrappers/retry.py +133 -23
  8. chuk_tool_processor/logging/__init__.py +83 -10
  9. chuk_tool_processor/logging/context.py +218 -22
  10. chuk_tool_processor/logging/formatter.py +56 -13
  11. chuk_tool_processor/logging/helpers.py +91 -16
  12. chuk_tool_processor/logging/metrics.py +75 -6
  13. chuk_tool_processor/mcp/mcp_tool.py +80 -35
  14. chuk_tool_processor/mcp/register_mcp_tools.py +74 -56
  15. chuk_tool_processor/mcp/setup_mcp_sse.py +41 -36
  16. chuk_tool_processor/mcp/setup_mcp_stdio.py +39 -37
  17. chuk_tool_processor/models/execution_strategy.py +52 -3
  18. chuk_tool_processor/models/streaming_tool.py +110 -0
  19. chuk_tool_processor/models/tool_call.py +56 -4
  20. chuk_tool_processor/models/tool_result.py +115 -9
  21. chuk_tool_processor/models/validated_tool.py +15 -13
  22. chuk_tool_processor/plugins/discovery.py +115 -70
  23. chuk_tool_processor/plugins/parsers/base.py +13 -5
  24. chuk_tool_processor/plugins/parsers/{function_call_tool_plugin.py → function_call_tool.py} +39 -20
  25. chuk_tool_processor/plugins/parsers/json_tool.py +50 -0
  26. chuk_tool_processor/plugins/parsers/openai_tool.py +88 -0
  27. chuk_tool_processor/plugins/parsers/xml_tool.py +74 -20
  28. chuk_tool_processor/registry/__init__.py +46 -7
  29. chuk_tool_processor/registry/auto_register.py +92 -28
  30. chuk_tool_processor/registry/decorators.py +134 -11
  31. chuk_tool_processor/registry/interface.py +48 -14
  32. chuk_tool_processor/registry/metadata.py +52 -6
  33. chuk_tool_processor/registry/provider.py +75 -36
  34. chuk_tool_processor/registry/providers/__init__.py +49 -10
  35. chuk_tool_processor/registry/providers/memory.py +59 -48
  36. chuk_tool_processor/registry/tool_export.py +208 -39
  37. chuk_tool_processor/utils/validation.py +18 -13
  38. chuk_tool_processor-0.1.7.dist-info/METADATA +401 -0
  39. chuk_tool_processor-0.1.7.dist-info/RECORD +58 -0
  40. {chuk_tool_processor-0.1.6.dist-info → chuk_tool_processor-0.1.7.dist-info}/WHEEL +1 -1
  41. chuk_tool_processor/plugins/parsers/json_tool_plugin.py +0 -38
  42. chuk_tool_processor/plugins/parsers/openai_tool_plugin.py +0 -76
  43. chuk_tool_processor-0.1.6.dist-info/METADATA +0 -462
  44. chuk_tool_processor-0.1.6.dist-info/RECORD +0 -57
  45. {chuk_tool_processor-0.1.6.dist-info → chuk_tool_processor-0.1.7.dist-info}/top_level.txt +0 -0
@@ -1,46 +1,304 @@
1
+ #!/usr/bin/env python
1
2
  # chuk_tool_processor/execution/tool_executor.py
2
- from typing import List, Optional
3
+ """
4
+ Modified ToolExecutor with true streaming support and duplicate prevention.
5
+
6
+ This version accesses streaming tools' stream_execute method directly
7
+ to enable true item-by-item streaming behavior, while preventing duplicates.
8
+ """
9
+ import asyncio
10
+ from datetime import datetime, timezone
11
+ from typing import Any, Dict, List, Optional, AsyncIterator, Set
3
12
 
4
- # Lazy import of in-process strategy to allow monkeypatching
5
- import chuk_tool_processor.execution.strategies.inprocess_strategy as inprocess_mod
6
13
  from chuk_tool_processor.models.execution_strategy import ExecutionStrategy
7
14
  from chuk_tool_processor.models.tool_call import ToolCall
8
15
  from chuk_tool_processor.models.tool_result import ToolResult
9
16
  from chuk_tool_processor.registry.interface import ToolRegistryInterface
17
+ from chuk_tool_processor.logging import get_logger
18
+
19
+ logger = get_logger("chuk_tool_processor.execution.tool_executor")
20
+
10
21
 
11
22
  class ToolExecutor:
12
23
  """
13
- Wraps an ExecutionStrategy (in‐process or subprocess) and provides
14
- a default_timeout shortcut for convenience.
24
+ Async-native executor that selects and uses a strategy for tool execution.
25
+
26
+ This class provides a unified interface for executing tools using different
27
+ execution strategies, with special support for streaming tools.
15
28
  """
29
+
16
30
  def __init__(
17
31
  self,
18
- registry: ToolRegistryInterface,
19
- default_timeout: float = 1.0,
32
+ registry: Optional[ToolRegistryInterface] = None,
33
+ default_timeout: float = 10.0,
20
34
  strategy: Optional[ExecutionStrategy] = None,
21
- # allow passing through to SubprocessStrategy if needed:
22
- strategy_kwargs: dict = {}
23
- ):
24
- # If user supplied a strategy, use it; otherwise default to in-process
25
- if strategy is not None:
26
- self.strategy = strategy
27
- else:
28
- # Use module-level InProcessStrategy, so monkeypatching works
29
- # Pass positional args to match patched FakeInProcess signature
30
- self.strategy = inprocess_mod.InProcessStrategy(
35
+ strategy_kwargs: Optional[Dict[str, Any]] = None,
36
+ ) -> None:
37
+ """
38
+ Initialize the tool executor.
39
+
40
+ Args:
41
+ registry: Tool registry to use for tool lookups
42
+ default_timeout: Default timeout for tool execution
43
+ strategy: Optional execution strategy (default: InProcessStrategy)
44
+ strategy_kwargs: Additional arguments for the strategy constructor
45
+ """
46
+ self.registry = registry
47
+ self.default_timeout = default_timeout
48
+
49
+ # Create strategy if not provided
50
+ if strategy is None:
51
+ # Lazy import to allow for circular imports
52
+ import chuk_tool_processor.execution.strategies.inprocess_strategy as _inprocess_mod
53
+
54
+ if registry is None:
55
+ raise ValueError("Registry must be provided if strategy is not")
56
+
57
+ strategy_kwargs = strategy_kwargs or {}
58
+ strategy = _inprocess_mod.InProcessStrategy(
31
59
  registry,
32
- default_timeout,
33
- **strategy_kwargs
60
+ default_timeout=default_timeout,
61
+ **strategy_kwargs,
34
62
  )
35
- self.registry = registry
63
+
64
+ self.strategy = strategy
65
+
66
+ @property
67
+ def supports_streaming(self) -> bool:
68
+ """Check if this executor supports streaming execution."""
69
+ return hasattr(self.strategy, "supports_streaming") and self.strategy.supports_streaming
36
70
 
37
71
  async def execute(
38
72
  self,
39
73
  calls: List[ToolCall],
40
- timeout: Optional[float] = None
74
+ timeout: Optional[float] = None,
75
+ use_cache: bool = True,
41
76
  ) -> List[ToolResult]:
42
77
  """
43
- Execute the list of calls with the underlying strategy.
44
- `timeout` here overrides the strategy's default_timeout.
78
+ Execute tool calls using the configured strategy.
79
+
80
+ Args:
81
+ calls: List of tool calls to execute
82
+ timeout: Optional timeout for execution (overrides default_timeout)
83
+ use_cache: Whether to use cached results (for caching wrappers)
84
+
85
+ Returns:
86
+ List of tool results in the same order as calls
87
+ """
88
+ if not calls:
89
+ return []
90
+
91
+ # Use the provided timeout or fall back to default
92
+ effective_timeout = timeout if timeout is not None else self.default_timeout
93
+
94
+ logger.debug(f"Executing {len(calls)} tool calls with timeout {effective_timeout}s")
95
+
96
+ # Delegate to the strategy
97
+ return await self.strategy.run(calls, timeout=effective_timeout)
98
+
99
+ async def stream_execute(
100
+ self,
101
+ calls: List[ToolCall],
102
+ timeout: Optional[float] = None,
103
+ ) -> AsyncIterator[ToolResult]:
104
+ """
105
+ Execute tool calls and yield results as they become available.
106
+
107
+ For streaming tools, this directly accesses their stream_execute method
108
+ to yield individual results as they are produced, rather than collecting
109
+ them into lists.
110
+
111
+ Args:
112
+ calls: List of tool calls to execute
113
+ timeout: Optional timeout for execution
114
+
115
+ Yields:
116
+ Tool results as they become available
117
+ """
118
+ if not calls:
119
+ return
120
+
121
+ # Use the provided timeout or fall back to default
122
+ effective_timeout = timeout if timeout is not None else self.default_timeout
123
+
124
+ # There are two possible ways to handle streaming:
125
+ # 1. Use the strategy's stream_run if available
126
+ # 2. Use direct streaming for streaming tools
127
+ # We'll choose one approach based on the tool types to avoid duplicates
128
+
129
+ # Check if strategy supports streaming
130
+ if hasattr(self.strategy, "stream_run") and self.strategy.supports_streaming:
131
+ # Check for streaming tools
132
+ streaming_tools = []
133
+ non_streaming_tools = []
134
+
135
+ for call in calls:
136
+ # Check if the tool is a streaming tool
137
+ tool_impl = await self.registry.get_tool(call.tool, call.namespace)
138
+ if tool_impl is None:
139
+ # Tool not found - treat as non-streaming
140
+ non_streaming_tools.append(call)
141
+ continue
142
+
143
+ # Instantiate if class
144
+ tool = tool_impl() if callable(tool_impl) else tool_impl
145
+
146
+ # Check for streaming support
147
+ if hasattr(tool, "supports_streaming") and tool.supports_streaming and hasattr(tool, "stream_execute"):
148
+ streaming_tools.append((call, tool))
149
+ else:
150
+ non_streaming_tools.append(call)
151
+
152
+ # If we have streaming tools, handle them directly
153
+ if streaming_tools:
154
+ # Create a tracking queue for all results
155
+ queue = asyncio.Queue()
156
+
157
+ # Track processing to avoid duplicates
158
+ processed_calls = set()
159
+
160
+ # For streaming tools, create direct streaming tasks
161
+ pending_tasks = set()
162
+ for call, tool in streaming_tools:
163
+ # Add to processed list to avoid duplication
164
+ processed_calls.add(call.id)
165
+
166
+ # Create task for direct streaming
167
+ task = asyncio.create_task(self._direct_stream_tool(
168
+ call, tool, queue, effective_timeout
169
+ ))
170
+ pending_tasks.add(task)
171
+ task.add_done_callback(pending_tasks.discard)
172
+
173
+ # For non-streaming tools, use the strategy's stream_run
174
+ if non_streaming_tools:
175
+ async def strategy_streamer():
176
+ async for result in self.strategy.stream_run(non_streaming_tools, timeout=effective_timeout):
177
+ await queue.put(result)
178
+
179
+ strategy_task = asyncio.create_task(strategy_streamer())
180
+ pending_tasks.add(strategy_task)
181
+ strategy_task.add_done_callback(pending_tasks.discard)
182
+
183
+ # Yield results as they arrive in the queue
184
+ while pending_tasks:
185
+ try:
186
+ # Wait a short time for a result, then check task status
187
+ result = await asyncio.wait_for(queue.get(), 0.1)
188
+ yield result
189
+ except asyncio.TimeoutError:
190
+ # Check if tasks have completed
191
+ if not pending_tasks:
192
+ break
193
+
194
+ # Check for completed tasks
195
+ done, pending_tasks = await asyncio.wait(
196
+ pending_tasks, timeout=0, return_when=asyncio.FIRST_COMPLETED
197
+ )
198
+
199
+ # Handle any exceptions
200
+ for task in done:
201
+ try:
202
+ await task
203
+ except Exception as e:
204
+ logger.exception(f"Error in streaming task: {e}")
205
+ else:
206
+ # No streaming tools, use the strategy's stream_run for all
207
+ async for result in self.strategy.stream_run(calls, timeout=effective_timeout):
208
+ yield result
209
+ else:
210
+ # Strategy doesn't support streaming, fall back to executing all at once
211
+ results = await self.execute(calls, timeout=effective_timeout)
212
+ for result in results:
213
+ yield result
214
+
215
+ async def _direct_stream_tool(
216
+ self,
217
+ call: ToolCall,
218
+ tool: Any,
219
+ queue: asyncio.Queue,
220
+ timeout: Optional[float]
221
+ ) -> None:
222
+ """
223
+ Stream results directly from a streaming tool.
224
+
225
+ Args:
226
+ call: Tool call to execute
227
+ tool: Tool instance
228
+ queue: Queue to put results into
229
+ timeout: Optional timeout in seconds
230
+ """
231
+ start_time = datetime.now(timezone.utc)
232
+ machine = "direct-stream"
233
+ pid = 0
234
+
235
+ # Create streaming task with timeout
236
+ async def stream_with_timeout():
237
+ try:
238
+ async for result in tool.stream_execute(**call.arguments):
239
+ # Create a ToolResult for each result
240
+ end_time = datetime.now(timezone.utc)
241
+ tool_result = ToolResult(
242
+ tool=call.tool,
243
+ result=result,
244
+ error=None,
245
+ start_time=start_time,
246
+ end_time=end_time,
247
+ machine=machine,
248
+ pid=pid
249
+ )
250
+ await queue.put(tool_result)
251
+ except Exception as e:
252
+ # Handle errors
253
+ end_time = datetime.now(timezone.utc)
254
+ error_result = ToolResult(
255
+ tool=call.tool,
256
+ result=None,
257
+ error=f"Streaming error: {str(e)}",
258
+ start_time=start_time,
259
+ end_time=end_time,
260
+ machine=machine,
261
+ pid=pid
262
+ )
263
+ await queue.put(error_result)
264
+
265
+ try:
266
+ if timeout:
267
+ await asyncio.wait_for(stream_with_timeout(), timeout)
268
+ else:
269
+ await stream_with_timeout()
270
+ except asyncio.TimeoutError:
271
+ # Handle timeout
272
+ end_time = datetime.now(timezone.utc)
273
+ timeout_result = ToolResult(
274
+ tool=call.tool,
275
+ result=None,
276
+ error=f"Streaming timeout after {timeout}s",
277
+ start_time=start_time,
278
+ end_time=end_time,
279
+ machine=machine,
280
+ pid=pid
281
+ )
282
+ await queue.put(timeout_result)
283
+ except Exception as e:
284
+ # Handle other errors
285
+ end_time = datetime.now(timezone.utc)
286
+ error_result = ToolResult(
287
+ tool=call.tool,
288
+ result=None,
289
+ error=f"Streaming error: {str(e)}",
290
+ start_time=start_time,
291
+ end_time=end_time,
292
+ machine=machine,
293
+ pid=pid
294
+ )
295
+ await queue.put(error_result)
296
+
297
+ async def shutdown(self) -> None:
298
+ """
299
+ Gracefully shut down the executor and any resources used by the strategy.
300
+
301
+ This should be called during application shutdown to ensure proper cleanup.
45
302
  """
46
- return await self.strategy.run(calls, timeout=timeout)
303
+ if hasattr(self.strategy, "shutdown") and callable(self.strategy.shutdown):
304
+ await self.strategy.shutdown()