chuk-tool-processor 0.9.2__py3-none-any.whl → 0.10__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/__init__.py +114 -0
- chuk_tool_processor/core/processor.py +363 -44
- chuk_tool_processor/logging/__init__.py +5 -8
- chuk_tool_processor/logging/context.py +2 -5
- chuk_tool_processor/mcp/__init__.py +3 -0
- chuk_tool_processor/mcp/mcp_tool.py +8 -3
- chuk_tool_processor/mcp/models.py +87 -0
- chuk_tool_processor/mcp/setup_mcp_stdio.py +92 -12
- chuk_tool_processor/mcp/stream_manager.py +94 -0
- chuk_tool_processor/mcp/transport/http_streamable_transport.py +2 -2
- chuk_tool_processor/models/tool_export_mixin.py +4 -4
- chuk_tool_processor/observability/metrics.py +3 -3
- chuk_tool_processor/observability/tracing.py +13 -12
- chuk_tool_processor/py.typed +0 -0
- chuk_tool_processor/registry/interface.py +7 -7
- chuk_tool_processor/registry/providers/__init__.py +2 -1
- chuk_tool_processor/registry/tool_export.py +1 -6
- {chuk_tool_processor-0.9.2.dist-info → chuk_tool_processor-0.10.dist-info}/METADATA +775 -159
- {chuk_tool_processor-0.9.2.dist-info → chuk_tool_processor-0.10.dist-info}/RECORD +21 -19
- {chuk_tool_processor-0.9.2.dist-info → chuk_tool_processor-0.10.dist-info}/WHEEL +0 -0
- {chuk_tool_processor-0.9.2.dist-info → chuk_tool_processor-0.10.dist-info}/top_level.txt +0 -0
chuk_tool_processor/__init__.py
CHANGED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CHUK Tool Processor - Async-native framework for processing LLM tool calls.
|
|
3
|
+
|
|
4
|
+
This package provides a production-ready framework for:
|
|
5
|
+
- Processing tool calls from various LLM output formats
|
|
6
|
+
- Executing tools with timeouts, retries, and rate limiting
|
|
7
|
+
- Connecting to remote MCP servers
|
|
8
|
+
- Caching results and circuit breaking
|
|
9
|
+
|
|
10
|
+
Quick Start:
|
|
11
|
+
>>> import asyncio
|
|
12
|
+
>>> from chuk_tool_processor import ToolProcessor
|
|
13
|
+
>>>
|
|
14
|
+
>>> async def main():
|
|
15
|
+
... async with ToolProcessor() as processor:
|
|
16
|
+
... llm_output = '<tool name="calculator" args=\'{"a": 5, "b": 3}\'/>'
|
|
17
|
+
... results = await processor.process(llm_output)
|
|
18
|
+
... print(results[0].result)
|
|
19
|
+
>>>
|
|
20
|
+
>>> asyncio.run(main())
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
24
|
+
|
|
25
|
+
# Version
|
|
26
|
+
__version__ = "0.9.7"
|
|
27
|
+
|
|
28
|
+
# Core processor
|
|
29
|
+
from chuk_tool_processor.core.processor import ToolProcessor
|
|
30
|
+
|
|
31
|
+
# Execution strategies
|
|
32
|
+
from chuk_tool_processor.execution.strategies.inprocess_strategy import InProcessStrategy
|
|
33
|
+
from chuk_tool_processor.execution.strategies.subprocess_strategy import SubprocessStrategy
|
|
34
|
+
from chuk_tool_processor.execution.strategies.subprocess_strategy import SubprocessStrategy as IsolatedStrategy
|
|
35
|
+
|
|
36
|
+
# MCP setup helpers
|
|
37
|
+
from chuk_tool_processor.mcp import (
|
|
38
|
+
setup_mcp_http_streamable,
|
|
39
|
+
setup_mcp_sse,
|
|
40
|
+
setup_mcp_stdio,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Stream manager for advanced MCP usage
|
|
44
|
+
from chuk_tool_processor.mcp.stream_manager import StreamManager
|
|
45
|
+
|
|
46
|
+
# Models (commonly used)
|
|
47
|
+
from chuk_tool_processor.models.tool_call import ToolCall
|
|
48
|
+
from chuk_tool_processor.models.tool_result import ToolResult
|
|
49
|
+
|
|
50
|
+
# Registry functions
|
|
51
|
+
from chuk_tool_processor.registry import (
|
|
52
|
+
ToolRegistryProvider,
|
|
53
|
+
get_default_registry,
|
|
54
|
+
initialize,
|
|
55
|
+
)
|
|
56
|
+
from chuk_tool_processor.registry.auto_register import register_fn_tool
|
|
57
|
+
|
|
58
|
+
# Decorators for registering tools
|
|
59
|
+
from chuk_tool_processor.registry.decorators import register_tool
|
|
60
|
+
|
|
61
|
+
# Type checking imports (not available at runtime)
|
|
62
|
+
if TYPE_CHECKING:
|
|
63
|
+
# Advanced models for type hints
|
|
64
|
+
# Execution strategies
|
|
65
|
+
from chuk_tool_processor.execution.strategies.inprocess_strategy import InProcessStrategy
|
|
66
|
+
from chuk_tool_processor.execution.strategies.subprocess_strategy import SubprocessStrategy
|
|
67
|
+
|
|
68
|
+
# Retry config
|
|
69
|
+
from chuk_tool_processor.execution.wrappers.retry import RetryConfig
|
|
70
|
+
from chuk_tool_processor.models.streaming_tool import StreamingTool
|
|
71
|
+
from chuk_tool_processor.models.tool_spec import ToolSpec
|
|
72
|
+
from chuk_tool_processor.models.validated_tool import ValidatedTool
|
|
73
|
+
|
|
74
|
+
# Registry interface
|
|
75
|
+
from chuk_tool_processor.registry.interface import ToolRegistryInterface
|
|
76
|
+
|
|
77
|
+
# Public API
|
|
78
|
+
__all__ = [
|
|
79
|
+
# Version
|
|
80
|
+
"__version__",
|
|
81
|
+
# Core classes
|
|
82
|
+
"ToolProcessor",
|
|
83
|
+
"StreamManager",
|
|
84
|
+
# Models
|
|
85
|
+
"ToolCall",
|
|
86
|
+
"ToolResult",
|
|
87
|
+
# Registry
|
|
88
|
+
"initialize",
|
|
89
|
+
"get_default_registry",
|
|
90
|
+
"ToolRegistryProvider",
|
|
91
|
+
# Decorators
|
|
92
|
+
"register_tool",
|
|
93
|
+
"register_fn_tool",
|
|
94
|
+
# Execution strategies
|
|
95
|
+
"InProcessStrategy",
|
|
96
|
+
"IsolatedStrategy",
|
|
97
|
+
"SubprocessStrategy",
|
|
98
|
+
# MCP setup
|
|
99
|
+
"setup_mcp_stdio",
|
|
100
|
+
"setup_mcp_sse",
|
|
101
|
+
"setup_mcp_http_streamable",
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
# Type checking exports (documentation only)
|
|
105
|
+
if TYPE_CHECKING:
|
|
106
|
+
__all__ += [
|
|
107
|
+
"ValidatedTool",
|
|
108
|
+
"StreamingTool",
|
|
109
|
+
"ToolSpec",
|
|
110
|
+
"InProcessStrategy",
|
|
111
|
+
"SubprocessStrategy",
|
|
112
|
+
"ToolRegistryInterface",
|
|
113
|
+
"RetryConfig",
|
|
114
|
+
]
|
|
@@ -34,13 +34,62 @@ from chuk_tool_processor.registry import ToolRegistryInterface, ToolRegistryProv
|
|
|
34
34
|
class ToolProcessor:
|
|
35
35
|
"""
|
|
36
36
|
Main class for processing tool calls from LLM responses.
|
|
37
|
-
|
|
37
|
+
|
|
38
|
+
ToolProcessor combines parsing, execution, and result handling with full async support.
|
|
39
|
+
It provides production-ready features including timeouts, retries, caching, rate limiting,
|
|
40
|
+
and circuit breaking.
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
Basic usage with context manager:
|
|
44
|
+
|
|
45
|
+
>>> import asyncio
|
|
46
|
+
>>> from chuk_tool_processor import ToolProcessor, register_tool
|
|
47
|
+
>>>
|
|
48
|
+
>>> @register_tool(name="calculator")
|
|
49
|
+
... class Calculator:
|
|
50
|
+
... async def execute(self, a: int, b: int) -> dict:
|
|
51
|
+
... return {"result": a + b}
|
|
52
|
+
>>>
|
|
53
|
+
>>> async def main():
|
|
54
|
+
... async with ToolProcessor() as processor:
|
|
55
|
+
... llm_output = '<tool name="calculator" args=\'{"a": 5, "b": 3}\'/>'
|
|
56
|
+
... results = await processor.process(llm_output)
|
|
57
|
+
... print(results[0].result) # {'result': 8}
|
|
58
|
+
>>>
|
|
59
|
+
>>> asyncio.run(main())
|
|
60
|
+
|
|
61
|
+
Production configuration:
|
|
62
|
+
|
|
63
|
+
>>> processor = ToolProcessor(
|
|
64
|
+
... default_timeout=30.0,
|
|
65
|
+
... enable_caching=True,
|
|
66
|
+
... cache_ttl=600,
|
|
67
|
+
... enable_rate_limiting=True,
|
|
68
|
+
... global_rate_limit=100, # 100 requests/minute
|
|
69
|
+
... enable_retries=True,
|
|
70
|
+
... max_retries=3,
|
|
71
|
+
... enable_circuit_breaker=True,
|
|
72
|
+
... )
|
|
73
|
+
|
|
74
|
+
Manual cleanup:
|
|
75
|
+
|
|
76
|
+
>>> processor = ToolProcessor()
|
|
77
|
+
>>> try:
|
|
78
|
+
... results = await processor.process(llm_output)
|
|
79
|
+
... finally:
|
|
80
|
+
... await processor.close()
|
|
81
|
+
|
|
82
|
+
Attributes:
|
|
83
|
+
registry: Tool registry containing registered tools
|
|
84
|
+
strategy: Execution strategy (InProcess or Subprocess)
|
|
85
|
+
executor: Wrapped executor with caching, retries, etc.
|
|
86
|
+
parsers: List of parser plugins for extracting tool calls
|
|
38
87
|
"""
|
|
39
88
|
|
|
40
89
|
def __init__(
|
|
41
90
|
self,
|
|
42
91
|
registry: ToolRegistryInterface | None = None,
|
|
43
|
-
strategy=None,
|
|
92
|
+
strategy: Any | None = None, # Strategy can be InProcessStrategy or SubprocessStrategy
|
|
44
93
|
default_timeout: float = 10.0,
|
|
45
94
|
max_concurrency: int | None = None,
|
|
46
95
|
enable_caching: bool = True,
|
|
@@ -61,21 +110,60 @@ class ToolProcessor:
|
|
|
61
110
|
|
|
62
111
|
Args:
|
|
63
112
|
registry: Tool registry to use. If None, uses the global registry.
|
|
64
|
-
strategy: Optional execution strategy (default: InProcessStrategy)
|
|
113
|
+
strategy: Optional execution strategy (default: InProcessStrategy).
|
|
114
|
+
Use SubprocessStrategy for isolated execution of untrusted code.
|
|
65
115
|
default_timeout: Default timeout for tool execution in seconds.
|
|
116
|
+
Individual tools can override this. Default: 10.0
|
|
66
117
|
max_concurrency: Maximum number of concurrent tool executions.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
118
|
+
If None, uses unlimited concurrency. Default: None
|
|
119
|
+
enable_caching: Whether to enable result caching. Caches results
|
|
120
|
+
based on tool name and arguments. Default: True
|
|
121
|
+
cache_ttl: Default cache TTL in seconds. Results older than this
|
|
122
|
+
are evicted. Default: 300 (5 minutes)
|
|
123
|
+
enable_rate_limiting: Whether to enable rate limiting. Prevents
|
|
124
|
+
API abuse and quota exhaustion. Default: False
|
|
70
125
|
global_rate_limit: Optional global rate limit (requests per minute).
|
|
126
|
+
Applies to all tools unless overridden. Default: None
|
|
71
127
|
tool_rate_limits: Dict mapping tool names to (limit, period) tuples.
|
|
72
|
-
|
|
73
|
-
|
|
128
|
+
Example: {"api_call": (10, 60)} = 10 requests per 60 seconds
|
|
129
|
+
enable_retries: Whether to enable automatic retries on transient
|
|
130
|
+
failures. Uses exponential backoff. Default: True
|
|
131
|
+
max_retries: Maximum number of retry attempts. Total attempts will
|
|
132
|
+
be max_retries + 1 (initial + retries). Default: 3
|
|
133
|
+
retry_config: Optional custom retry configuration. If provided,
|
|
134
|
+
overrides max_retries. See RetryConfig for details.
|
|
74
135
|
enable_circuit_breaker: Whether to enable circuit breaker pattern.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
136
|
+
Opens circuit after repeated failures to prevent cascading
|
|
137
|
+
failures. Default: False
|
|
138
|
+
circuit_breaker_threshold: Number of consecutive failures before
|
|
139
|
+
opening circuit. Default: 5
|
|
140
|
+
circuit_breaker_timeout: Seconds to wait before attempting recovery
|
|
141
|
+
(transition from OPEN to HALF_OPEN). Default: 60.0
|
|
142
|
+
parser_plugins: List of parser plugin names to use. If None, uses
|
|
143
|
+
all available parsers (XML, OpenAI, JSON). Default: None
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
ImportError: If required dependencies are not installed.
|
|
147
|
+
|
|
148
|
+
Example:
|
|
149
|
+
>>> # Production configuration with all features
|
|
150
|
+
>>> processor = ToolProcessor(
|
|
151
|
+
... default_timeout=30.0,
|
|
152
|
+
... max_concurrency=20,
|
|
153
|
+
... enable_caching=True,
|
|
154
|
+
... cache_ttl=600,
|
|
155
|
+
... enable_rate_limiting=True,
|
|
156
|
+
... global_rate_limit=100,
|
|
157
|
+
... tool_rate_limits={
|
|
158
|
+
... "expensive_api": (10, 60),
|
|
159
|
+
... "free_api": (100, 60),
|
|
160
|
+
... },
|
|
161
|
+
... enable_retries=True,
|
|
162
|
+
... max_retries=3,
|
|
163
|
+
... enable_circuit_breaker=True,
|
|
164
|
+
... circuit_breaker_threshold=5,
|
|
165
|
+
... circuit_breaker_timeout=60.0,
|
|
166
|
+
... )
|
|
79
167
|
"""
|
|
80
168
|
self.logger = get_logger("chuk_tool_processor.processor")
|
|
81
169
|
|
|
@@ -97,11 +185,11 @@ class ToolProcessor:
|
|
|
97
185
|
self.circuit_breaker_timeout = circuit_breaker_timeout
|
|
98
186
|
self.parser_plugin_names = parser_plugins
|
|
99
187
|
|
|
100
|
-
# Placeholder for initialized components
|
|
101
|
-
self.registry = None
|
|
102
|
-
self.strategy = None
|
|
103
|
-
self.executor = None
|
|
104
|
-
self.parsers = []
|
|
188
|
+
# Placeholder for initialized components (typed as Optional for type safety)
|
|
189
|
+
self.registry: ToolRegistryInterface | None = None
|
|
190
|
+
self.strategy: Any | None = None # Strategy type is complex, use Any for now
|
|
191
|
+
self.executor: Any | None = None # Executor type is complex, use Any for now
|
|
192
|
+
self.parsers: list[Any] = [] # Parser types vary, use Any for now
|
|
105
193
|
|
|
106
194
|
# Flag for tracking initialization state
|
|
107
195
|
self._initialized = False
|
|
@@ -114,13 +202,8 @@ class ToolProcessor:
|
|
|
114
202
|
This method ensures all components are properly initialized before use.
|
|
115
203
|
It is called automatically by other methods if needed.
|
|
116
204
|
"""
|
|
117
|
-
# Fast path if already initialized
|
|
118
|
-
if self._initialized:
|
|
119
|
-
return
|
|
120
|
-
|
|
121
205
|
# Ensure only one initialization happens at a time
|
|
122
206
|
async with self._init_lock:
|
|
123
|
-
# Double-check pattern after acquiring lock
|
|
124
207
|
if self._initialized:
|
|
125
208
|
return
|
|
126
209
|
|
|
@@ -217,21 +300,87 @@ class ToolProcessor:
|
|
|
217
300
|
request_id: str | None = None,
|
|
218
301
|
) -> list[ToolResult]:
|
|
219
302
|
"""
|
|
220
|
-
Process tool calls from various
|
|
221
|
-
|
|
222
|
-
This method handles different input types:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
303
|
+
Process tool calls from various LLM output formats.
|
|
304
|
+
|
|
305
|
+
This method handles different input types from various LLM providers:
|
|
306
|
+
|
|
307
|
+
**String Input (Anthropic Claude style)**:
|
|
308
|
+
Parses tool calls from XML-like text using registered parsers.
|
|
309
|
+
|
|
310
|
+
Example:
|
|
311
|
+
>>> llm_output = '<tool name="calculator" args=\'{"a": 5, "b": 3}\'/>'
|
|
312
|
+
>>> results = await processor.process(llm_output)
|
|
313
|
+
|
|
314
|
+
**Dict Input (OpenAI style)**:
|
|
315
|
+
Processes an OpenAI-style tool_calls object.
|
|
316
|
+
|
|
317
|
+
Example:
|
|
318
|
+
>>> openai_output = {
|
|
319
|
+
... "tool_calls": [
|
|
320
|
+
... {
|
|
321
|
+
... "type": "function",
|
|
322
|
+
... "function": {
|
|
323
|
+
... "name": "calculator",
|
|
324
|
+
... "arguments": '{"a": 5, "b": 3}'
|
|
325
|
+
... }
|
|
326
|
+
... }
|
|
327
|
+
... ]
|
|
328
|
+
... }
|
|
329
|
+
>>> results = await processor.process(openai_output)
|
|
330
|
+
|
|
331
|
+
**List[Dict] Input (Direct tool calls)**:
|
|
332
|
+
Processes a list of individual tool call dictionaries.
|
|
333
|
+
|
|
334
|
+
Example:
|
|
335
|
+
>>> direct_calls = [
|
|
336
|
+
... {"tool": "calculator", "arguments": {"a": 5, "b": 3}},
|
|
337
|
+
... {"tool": "weather", "arguments": {"city": "London"}}
|
|
338
|
+
... ]
|
|
339
|
+
>>> results = await processor.process(direct_calls)
|
|
226
340
|
|
|
227
341
|
Args:
|
|
228
|
-
data: Input data containing tool calls
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
342
|
+
data: Input data containing tool calls. Can be:
|
|
343
|
+
- String: XML/text format (e.g., Anthropic Claude)
|
|
344
|
+
- Dict: OpenAI tool_calls format
|
|
345
|
+
- List[Dict]: Direct tool call list
|
|
346
|
+
timeout: Optional timeout in seconds for tool execution.
|
|
347
|
+
Overrides default_timeout if provided. Default: None
|
|
348
|
+
use_cache: Whether to use cached results. If False, forces
|
|
349
|
+
fresh execution even if cached results exist. Default: True
|
|
350
|
+
request_id: Optional request ID for tracing and logging.
|
|
351
|
+
If not provided, a UUID will be generated. Default: None
|
|
232
352
|
|
|
233
353
|
Returns:
|
|
234
|
-
List of
|
|
354
|
+
List of ToolResult objects. Each result contains:
|
|
355
|
+
- tool: Name of the tool that was executed
|
|
356
|
+
- result: The tool's output (None if error)
|
|
357
|
+
- error: Error message if execution failed (None if success)
|
|
358
|
+
- duration: Execution time in seconds
|
|
359
|
+
- cached: Whether result was retrieved from cache
|
|
360
|
+
|
|
361
|
+
**Always returns a list** (never None), even if empty.
|
|
362
|
+
|
|
363
|
+
Raises:
|
|
364
|
+
ToolNotFoundError: If a tool is not registered in the registry
|
|
365
|
+
ToolTimeoutError: If tool execution exceeds timeout
|
|
366
|
+
ToolCircuitOpenError: If circuit breaker is open for a tool
|
|
367
|
+
ToolRateLimitedError: If rate limit is exceeded
|
|
368
|
+
|
|
369
|
+
Example:
|
|
370
|
+
>>> async with ToolProcessor() as processor:
|
|
371
|
+
... # Process Claude-style XML
|
|
372
|
+
... results = await processor.process(
|
|
373
|
+
... '<tool name="echo" args=\'{"message": "hello"}\'/>'
|
|
374
|
+
... )
|
|
375
|
+
...
|
|
376
|
+
... # Check results
|
|
377
|
+
... for result in results:
|
|
378
|
+
... if result.error:
|
|
379
|
+
... print(f"Error: {result.error}")
|
|
380
|
+
... else:
|
|
381
|
+
... print(f"Success: {result.result}")
|
|
382
|
+
... print(f"Duration: {result.duration}s")
|
|
383
|
+
... print(f"From cache: {result.cached}")
|
|
235
384
|
"""
|
|
236
385
|
# Ensure initialization
|
|
237
386
|
await self.initialize()
|
|
@@ -260,7 +409,11 @@ class ToolProcessor:
|
|
|
260
409
|
args = {"raw": args_str}
|
|
261
410
|
|
|
262
411
|
if name:
|
|
263
|
-
|
|
412
|
+
# Build ToolCall kwargs, only include id if present
|
|
413
|
+
call_kwargs: dict[str, Any] = {"tool": name, "arguments": args}
|
|
414
|
+
if "id" in tc and tc["id"]:
|
|
415
|
+
call_kwargs["id"] = tc["id"]
|
|
416
|
+
calls.append(ToolCall(**call_kwargs))
|
|
264
417
|
else:
|
|
265
418
|
# Assume it's a single tool call
|
|
266
419
|
calls = [ToolCall(**data)]
|
|
@@ -268,7 +421,9 @@ class ToolProcessor:
|
|
|
268
421
|
# List of tool calls
|
|
269
422
|
calls = [ToolCall(**tc) for tc in data]
|
|
270
423
|
else:
|
|
271
|
-
|
|
424
|
+
# Defensive: handle unexpected types at runtime
|
|
425
|
+
# This shouldn't happen per type signature, but helps with debugging
|
|
426
|
+
self.logger.warning(f"Unsupported input type: {type(data)}") # type: ignore[unreachable]
|
|
272
427
|
return []
|
|
273
428
|
|
|
274
429
|
if not calls:
|
|
@@ -279,6 +434,10 @@ class ToolProcessor:
|
|
|
279
434
|
|
|
280
435
|
# Execute tool calls
|
|
281
436
|
async with log_context_span("tool_execution", {"num_calls": len(calls)}):
|
|
437
|
+
# Assert that initialization completed successfully
|
|
438
|
+
assert self.registry is not None, "Registry must be initialized"
|
|
439
|
+
assert self.executor is not None, "Executor must be initialized"
|
|
440
|
+
|
|
282
441
|
# Check if any tools are unknown - search across all namespaces
|
|
283
442
|
unknown_tools = []
|
|
284
443
|
all_tools = await self.registry.list_tools() # Returns list of (namespace, name) tuples
|
|
@@ -292,7 +451,7 @@ class ToolProcessor:
|
|
|
292
451
|
self.logger.debug(f"Unknown tools: {unknown_tools}")
|
|
293
452
|
|
|
294
453
|
# Execute tools
|
|
295
|
-
results = await self.executor.execute(calls, timeout=timeout)
|
|
454
|
+
results: list[ToolResult] = await self.executor.execute(calls, timeout=timeout)
|
|
296
455
|
|
|
297
456
|
# Log metrics for each tool call
|
|
298
457
|
for call, result in zip(calls, results, strict=False):
|
|
@@ -348,22 +507,66 @@ class ToolProcessor:
|
|
|
348
507
|
"""
|
|
349
508
|
Execute a list of ToolCall objects directly.
|
|
350
509
|
|
|
510
|
+
This is a lower-level method for executing tool calls when you already
|
|
511
|
+
have parsed ToolCall objects. For most use cases, prefer process()
|
|
512
|
+
which handles parsing automatically.
|
|
513
|
+
|
|
351
514
|
Args:
|
|
352
|
-
calls: List of
|
|
353
|
-
|
|
354
|
-
|
|
515
|
+
calls: List of ToolCall objects to execute. Each call must have:
|
|
516
|
+
- tool: Name of the tool to execute
|
|
517
|
+
- arguments: Dictionary of arguments for the tool
|
|
518
|
+
timeout: Optional timeout in seconds for tool execution.
|
|
519
|
+
Overrides default_timeout if provided. Default: None
|
|
520
|
+
use_cache: Whether to use cached results. If False, forces
|
|
521
|
+
fresh execution even if cached results exist. Default: True
|
|
355
522
|
|
|
356
523
|
Returns:
|
|
357
|
-
List of
|
|
524
|
+
List of ToolResult objects, one per input ToolCall.
|
|
525
|
+
**Always returns a list** (never None), even if empty.
|
|
526
|
+
|
|
527
|
+
Each result contains:
|
|
528
|
+
- tool: Name of the tool that was executed
|
|
529
|
+
- result: The tool's output (None if error)
|
|
530
|
+
- error: Error message if execution failed (None if success)
|
|
531
|
+
- duration: Execution time in seconds
|
|
532
|
+
- cached: Whether result was retrieved from cache
|
|
533
|
+
|
|
534
|
+
Raises:
|
|
535
|
+
RuntimeError: If processor is not initialized
|
|
536
|
+
ToolNotFoundError: If a tool is not registered
|
|
537
|
+
ToolTimeoutError: If tool execution exceeds timeout
|
|
538
|
+
ToolCircuitOpenError: If circuit breaker is open
|
|
539
|
+
ToolRateLimitedError: If rate limit is exceeded
|
|
540
|
+
|
|
541
|
+
Example:
|
|
542
|
+
>>> from chuk_tool_processor import ToolCall
|
|
543
|
+
>>>
|
|
544
|
+
>>> # Create tool calls directly
|
|
545
|
+
>>> calls = [
|
|
546
|
+
... ToolCall(tool="calculator", arguments={"a": 5, "b": 3}),
|
|
547
|
+
... ToolCall(tool="weather", arguments={"city": "London"}),
|
|
548
|
+
... ]
|
|
549
|
+
>>>
|
|
550
|
+
>>> async with ToolProcessor() as processor:
|
|
551
|
+
... results = await processor.execute(calls)
|
|
552
|
+
... for result in results:
|
|
553
|
+
... print(f"{result.tool}: {result.result}")
|
|
358
554
|
"""
|
|
359
555
|
# Ensure initialization
|
|
360
556
|
await self.initialize()
|
|
361
557
|
|
|
558
|
+
# Safety check: ensure we have an executor
|
|
559
|
+
if self.executor is None:
|
|
560
|
+
raise RuntimeError("Executor not initialized. Call initialize() first.")
|
|
561
|
+
|
|
362
562
|
# Execute with the configured executor
|
|
363
|
-
|
|
563
|
+
results = await self.executor.execute(
|
|
364
564
|
calls=calls, timeout=timeout, use_cache=use_cache if hasattr(self.executor, "use_cache") else True
|
|
365
565
|
)
|
|
366
566
|
|
|
567
|
+
# Ensure we always return a list (never None)
|
|
568
|
+
return results if results is not None else []
|
|
569
|
+
|
|
367
570
|
async def _extract_tool_calls(self, text: str) -> list[ToolCall]:
|
|
368
571
|
"""
|
|
369
572
|
Extract tool calls from text using all available parsers.
|
|
@@ -391,7 +594,8 @@ class ToolProcessor:
|
|
|
391
594
|
for result in parser_results:
|
|
392
595
|
if isinstance(result, Exception):
|
|
393
596
|
continue
|
|
394
|
-
|
|
597
|
+
# At this point, result is list[ToolCall], not an exception
|
|
598
|
+
if result and isinstance(result, list):
|
|
395
599
|
all_calls.extend(result)
|
|
396
600
|
|
|
397
601
|
# ------------------------------------------------------------------ #
|
|
@@ -410,7 +614,7 @@ class ToolProcessor:
|
|
|
410
614
|
|
|
411
615
|
return list(unique_calls.values())
|
|
412
616
|
|
|
413
|
-
async def _try_parser(self, parser, text: str) -> list[ToolCall]:
|
|
617
|
+
async def _try_parser(self, parser: Any, text: str) -> list[ToolCall]:
|
|
414
618
|
"""Try a single parser with metrics and logging."""
|
|
415
619
|
parser_name = parser.__class__.__name__
|
|
416
620
|
|
|
@@ -419,7 +623,7 @@ class ToolProcessor:
|
|
|
419
623
|
|
|
420
624
|
try:
|
|
421
625
|
# Try to parse
|
|
422
|
-
calls = await parser.try_parse(text)
|
|
626
|
+
calls: list[ToolCall] = await parser.try_parse(text)
|
|
423
627
|
|
|
424
628
|
# Log success
|
|
425
629
|
duration = time.time() - start_time
|
|
@@ -444,6 +648,121 @@ class ToolProcessor:
|
|
|
444
648
|
self.logger.debug(f"Parser {parser_name} failed: {str(e)}")
|
|
445
649
|
return []
|
|
446
650
|
|
|
651
|
+
# ------------------------------------------------------------------ #
|
|
652
|
+
# Tool discovery and introspection #
|
|
653
|
+
# ------------------------------------------------------------------ #
|
|
654
|
+
async def list_tools(self) -> list[str]:
|
|
655
|
+
"""
|
|
656
|
+
List all registered tool names.
|
|
657
|
+
|
|
658
|
+
This method provides programmatic access to all tools in the registry.
|
|
659
|
+
|
|
660
|
+
Returns:
|
|
661
|
+
List of tool names (strings).
|
|
662
|
+
|
|
663
|
+
Example:
|
|
664
|
+
>>> async with ToolProcessor() as processor:
|
|
665
|
+
... tools = await processor.list_tools()
|
|
666
|
+
... for name in tools:
|
|
667
|
+
... print(f"Available tool: {name}")
|
|
668
|
+
|
|
669
|
+
Raises:
|
|
670
|
+
RuntimeError: If processor is not initialized. Call initialize()
|
|
671
|
+
or use the processor in a context manager.
|
|
672
|
+
"""
|
|
673
|
+
await self.initialize()
|
|
674
|
+
|
|
675
|
+
if self.registry is None:
|
|
676
|
+
raise RuntimeError("Registry not initialized")
|
|
677
|
+
|
|
678
|
+
# Get tool tuples and extract names
|
|
679
|
+
tool_tuples = await self.registry.list_tools()
|
|
680
|
+
return [name for _, name in tool_tuples]
|
|
681
|
+
|
|
682
|
+
async def get_tool_count(self) -> int:
|
|
683
|
+
"""
|
|
684
|
+
Get the number of registered tools.
|
|
685
|
+
|
|
686
|
+
Returns:
|
|
687
|
+
Number of registered tools.
|
|
688
|
+
|
|
689
|
+
Example:
|
|
690
|
+
>>> async with ToolProcessor() as processor:
|
|
691
|
+
... count = await processor.get_tool_count()
|
|
692
|
+
... print(f"Total tools: {count}")
|
|
693
|
+
"""
|
|
694
|
+
await self.initialize()
|
|
695
|
+
|
|
696
|
+
if self.registry is None:
|
|
697
|
+
raise RuntimeError("Registry not initialized")
|
|
698
|
+
|
|
699
|
+
tool_tuples = await self.registry.list_tools()
|
|
700
|
+
return len(tool_tuples)
|
|
701
|
+
|
|
702
|
+
# ------------------------------------------------------------------ #
|
|
703
|
+
# Context manager support for automatic cleanup #
|
|
704
|
+
# ------------------------------------------------------------------ #
|
|
705
|
+
async def __aenter__(self):
|
|
706
|
+
"""Context manager entry - ensures initialization."""
|
|
707
|
+
await self.initialize()
|
|
708
|
+
return self
|
|
709
|
+
|
|
710
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
711
|
+
"""Context manager exit with automatic cleanup."""
|
|
712
|
+
await self.close()
|
|
713
|
+
return False
|
|
714
|
+
|
|
715
|
+
async def close(self) -> None:
|
|
716
|
+
"""
|
|
717
|
+
Close the processor and clean up resources.
|
|
718
|
+
|
|
719
|
+
This method ensures proper cleanup of executor resources, caches,
|
|
720
|
+
and any other stateful components.
|
|
721
|
+
"""
|
|
722
|
+
self.logger.debug("Closing tool processor")
|
|
723
|
+
|
|
724
|
+
try:
|
|
725
|
+
# Close the executor if it has a close method
|
|
726
|
+
if self.executor and hasattr(self.executor, "close"):
|
|
727
|
+
close_method = self.executor.close
|
|
728
|
+
if asyncio.iscoroutinefunction(close_method):
|
|
729
|
+
await close_method()
|
|
730
|
+
elif callable(close_method):
|
|
731
|
+
close_method()
|
|
732
|
+
|
|
733
|
+
# Close the strategy if it has a close method
|
|
734
|
+
if self.strategy and hasattr(self.strategy, "close"):
|
|
735
|
+
close_method = self.strategy.close
|
|
736
|
+
if asyncio.iscoroutinefunction(close_method):
|
|
737
|
+
await close_method()
|
|
738
|
+
elif callable(close_method):
|
|
739
|
+
result = close_method()
|
|
740
|
+
# Check if the result is a coroutine and await it
|
|
741
|
+
if asyncio.iscoroutine(result):
|
|
742
|
+
await result
|
|
743
|
+
|
|
744
|
+
# Clear cached results if using caching
|
|
745
|
+
if self.enable_caching and self.executor:
|
|
746
|
+
# Walk the executor chain to find the CachingToolExecutor
|
|
747
|
+
current = self.executor
|
|
748
|
+
while current:
|
|
749
|
+
if isinstance(current, CachingToolExecutor):
|
|
750
|
+
if hasattr(current.cache, "clear"):
|
|
751
|
+
clear_method = current.cache.clear
|
|
752
|
+
if asyncio.iscoroutinefunction(clear_method):
|
|
753
|
+
await clear_method()
|
|
754
|
+
else:
|
|
755
|
+
clear_result = clear_method()
|
|
756
|
+
if asyncio.iscoroutine(clear_result):
|
|
757
|
+
await clear_result
|
|
758
|
+
break
|
|
759
|
+
current = getattr(current, "executor", None)
|
|
760
|
+
|
|
761
|
+
self.logger.debug("Tool processor closed successfully")
|
|
762
|
+
|
|
763
|
+
except Exception as e:
|
|
764
|
+
self.logger.error(f"Error during processor cleanup: {e}")
|
|
765
|
+
|
|
447
766
|
|
|
448
767
|
# Create a global processor instance
|
|
449
768
|
_global_processor: ToolProcessor | None = None
|
|
@@ -19,14 +19,11 @@ import sys
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
# Auto-initialize shutdown error suppression when logging package is imported
|
|
22
|
-
def _initialize_shutdown_fixes():
|
|
22
|
+
def _initialize_shutdown_fixes() -> None:
|
|
23
23
|
"""Initialize shutdown error suppression when the package is imported."""
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
_setup_shutdown_error_suppression()
|
|
28
|
-
except ImportError:
|
|
29
|
-
pass
|
|
24
|
+
# Note: _setup_shutdown_error_suppression removed as it's no longer needed
|
|
25
|
+
# Keeping this function as a no-op for backward compatibility
|
|
26
|
+
pass
|
|
30
27
|
|
|
31
28
|
|
|
32
29
|
# Initialize when package is imported
|
|
@@ -64,7 +61,7 @@ __all__ = [
|
|
|
64
61
|
async def setup_logging(
|
|
65
62
|
level: int = logging.INFO,
|
|
66
63
|
structured: bool = True,
|
|
67
|
-
log_file: str = None,
|
|
64
|
+
log_file: str | None = None,
|
|
68
65
|
) -> None:
|
|
69
66
|
"""
|
|
70
67
|
Set up the logging system.
|