chuk-tool-processor 0.1.5__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.
- chuk_tool_processor/core/processor.py +345 -132
- chuk_tool_processor/execution/strategies/inprocess_strategy.py +512 -68
- chuk_tool_processor/execution/strategies/subprocess_strategy.py +523 -63
- chuk_tool_processor/execution/tool_executor.py +282 -24
- chuk_tool_processor/execution/wrappers/caching.py +465 -123
- chuk_tool_processor/execution/wrappers/rate_limiting.py +199 -86
- chuk_tool_processor/execution/wrappers/retry.py +133 -23
- chuk_tool_processor/logging/__init__.py +83 -10
- chuk_tool_processor/logging/context.py +218 -22
- chuk_tool_processor/logging/formatter.py +56 -13
- chuk_tool_processor/logging/helpers.py +91 -16
- chuk_tool_processor/logging/metrics.py +75 -6
- chuk_tool_processor/mcp/mcp_tool.py +80 -35
- chuk_tool_processor/mcp/register_mcp_tools.py +74 -56
- chuk_tool_processor/mcp/setup_mcp_sse.py +41 -36
- chuk_tool_processor/mcp/setup_mcp_stdio.py +39 -37
- chuk_tool_processor/mcp/stream_manager.py +28 -0
- chuk_tool_processor/models/execution_strategy.py +52 -3
- chuk_tool_processor/models/streaming_tool.py +110 -0
- chuk_tool_processor/models/tool_call.py +56 -4
- chuk_tool_processor/models/tool_result.py +115 -9
- chuk_tool_processor/models/validated_tool.py +15 -13
- chuk_tool_processor/plugins/discovery.py +115 -70
- chuk_tool_processor/plugins/parsers/base.py +13 -5
- chuk_tool_processor/plugins/parsers/{function_call_tool_plugin.py → function_call_tool.py} +39 -20
- chuk_tool_processor/plugins/parsers/json_tool.py +50 -0
- chuk_tool_processor/plugins/parsers/openai_tool.py +88 -0
- chuk_tool_processor/plugins/parsers/xml_tool.py +74 -20
- chuk_tool_processor/registry/__init__.py +46 -7
- chuk_tool_processor/registry/auto_register.py +92 -28
- chuk_tool_processor/registry/decorators.py +134 -11
- chuk_tool_processor/registry/interface.py +48 -14
- chuk_tool_processor/registry/metadata.py +52 -6
- chuk_tool_processor/registry/provider.py +75 -36
- chuk_tool_processor/registry/providers/__init__.py +49 -10
- chuk_tool_processor/registry/providers/memory.py +59 -48
- chuk_tool_processor/registry/tool_export.py +208 -39
- chuk_tool_processor/utils/validation.py +18 -13
- chuk_tool_processor-0.1.7.dist-info/METADATA +401 -0
- chuk_tool_processor-0.1.7.dist-info/RECORD +58 -0
- {chuk_tool_processor-0.1.5.dist-info → chuk_tool_processor-0.1.7.dist-info}/WHEEL +1 -1
- chuk_tool_processor/plugins/parsers/json_tool_plugin.py +0 -38
- chuk_tool_processor/plugins/parsers/openai_tool_plugin.py +0 -76
- chuk_tool_processor-0.1.5.dist-info/METADATA +0 -462
- chuk_tool_processor-0.1.5.dist-info/RECORD +0 -57
- {chuk_tool_processor-0.1.5.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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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 =
|
|
32
|
+
registry: Optional[ToolRegistryInterface] = None,
|
|
33
|
+
default_timeout: float = 10.0,
|
|
20
34
|
strategy: Optional[ExecutionStrategy] = None,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
|
44
|
-
|
|
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
|
-
|
|
303
|
+
if hasattr(self.strategy, "shutdown") and callable(self.strategy.shutdown):
|
|
304
|
+
await self.strategy.shutdown()
|