chuk-tool-processor 0.1.4__py3-none-any.whl → 0.1.6__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.
- chuk_tool_processor/core/processor.py +67 -60
- chuk_tool_processor/mcp/stream_manager.py +28 -0
- {chuk_tool_processor-0.1.4.dist-info → chuk_tool_processor-0.1.6.dist-info}/METADATA +1 -1
- {chuk_tool_processor-0.1.4.dist-info → chuk_tool_processor-0.1.6.dist-info}/RECORD +6 -6
- {chuk_tool_processor-0.1.4.dist-info → chuk_tool_processor-0.1.6.dist-info}/WHEEL +1 -1
- {chuk_tool_processor-0.1.4.dist-info → chuk_tool_processor-0.1.6.dist-info}/top_level.txt +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# chuk_tool_processor/core/processor.py
|
|
2
2
|
import asyncio
|
|
3
3
|
import time
|
|
4
|
+
import json
|
|
5
|
+
import hashlib
|
|
4
6
|
from typing import Any, Dict, List, Optional, Type, Union
|
|
5
7
|
|
|
6
|
-
#
|
|
8
|
+
# imports
|
|
7
9
|
from chuk_tool_processor.models.tool_call import ToolCall
|
|
8
10
|
from chuk_tool_processor.models.tool_result import ToolResult
|
|
9
11
|
from chuk_tool_processor.registry import ToolRegistryInterface, ToolRegistryProvider
|
|
@@ -21,6 +23,7 @@ class ToolProcessor:
|
|
|
21
23
|
Main class for processing tool calls from LLM responses.
|
|
22
24
|
Combines parsing, execution, and result handling.
|
|
23
25
|
"""
|
|
26
|
+
|
|
24
27
|
def __init__(
|
|
25
28
|
self,
|
|
26
29
|
registry: Optional[ToolRegistryInterface] = None,
|
|
@@ -33,11 +36,11 @@ class ToolProcessor:
|
|
|
33
36
|
tool_rate_limits: Optional[Dict[str, tuple]] = None,
|
|
34
37
|
enable_retries: bool = True,
|
|
35
38
|
max_retries: int = 3,
|
|
36
|
-
parser_plugins: Optional[List[str]] = None
|
|
39
|
+
parser_plugins: Optional[List[str]] = None,
|
|
37
40
|
):
|
|
38
41
|
"""
|
|
39
42
|
Initialize the tool processor.
|
|
40
|
-
|
|
43
|
+
|
|
41
44
|
Args:
|
|
42
45
|
registry: Tool registry to use. If None, uses the global registry.
|
|
43
46
|
default_timeout: Default timeout for tool execution in seconds.
|
|
@@ -53,55 +56,55 @@ class ToolProcessor:
|
|
|
53
56
|
If None, uses all available parsers.
|
|
54
57
|
"""
|
|
55
58
|
self.logger = get_logger("chuk_tool_processor.processor")
|
|
56
|
-
|
|
59
|
+
|
|
57
60
|
# Use provided registry or global registry
|
|
58
61
|
self.registry = registry or ToolRegistryProvider.get_registry()
|
|
59
|
-
|
|
62
|
+
|
|
60
63
|
# Create base executor with in-process strategy
|
|
61
64
|
self.strategy = InProcessStrategy(
|
|
62
65
|
registry=self.registry,
|
|
63
66
|
default_timeout=default_timeout,
|
|
64
|
-
max_concurrency=max_concurrency
|
|
67
|
+
max_concurrency=max_concurrency,
|
|
65
68
|
)
|
|
66
|
-
|
|
69
|
+
|
|
67
70
|
self.executor = ToolExecutor(
|
|
68
71
|
registry=self.registry,
|
|
69
72
|
default_timeout=default_timeout,
|
|
70
|
-
strategy=self.strategy
|
|
73
|
+
strategy=self.strategy,
|
|
71
74
|
)
|
|
72
|
-
|
|
75
|
+
|
|
73
76
|
# Apply optional wrappers
|
|
74
77
|
if enable_retries:
|
|
75
78
|
self.logger.debug("Enabling retry logic")
|
|
76
79
|
self.executor = RetryableToolExecutor(
|
|
77
80
|
executor=self.executor,
|
|
78
|
-
default_config=RetryConfig(max_retries=max_retries)
|
|
81
|
+
default_config=RetryConfig(max_retries=max_retries),
|
|
79
82
|
)
|
|
80
|
-
|
|
83
|
+
|
|
81
84
|
if enable_rate_limiting:
|
|
82
85
|
self.logger.debug("Enabling rate limiting")
|
|
83
86
|
rate_limiter = RateLimiter(
|
|
84
87
|
global_limit=global_rate_limit,
|
|
85
|
-
tool_limits=tool_rate_limits
|
|
88
|
+
tool_limits=tool_rate_limits,
|
|
86
89
|
)
|
|
87
90
|
self.executor = RateLimitedToolExecutor(
|
|
88
91
|
executor=self.executor,
|
|
89
|
-
rate_limiter=rate_limiter
|
|
92
|
+
rate_limiter=rate_limiter,
|
|
90
93
|
)
|
|
91
|
-
|
|
94
|
+
|
|
92
95
|
if enable_caching:
|
|
93
96
|
self.logger.debug("Enabling result caching")
|
|
94
97
|
cache = InMemoryCache(default_ttl=cache_ttl)
|
|
95
98
|
self.executor = CachingToolExecutor(
|
|
96
99
|
executor=self.executor,
|
|
97
100
|
cache=cache,
|
|
98
|
-
default_ttl=cache_ttl
|
|
101
|
+
default_ttl=cache_ttl,
|
|
99
102
|
)
|
|
100
|
-
|
|
103
|
+
|
|
101
104
|
# Discover plugins if not already done
|
|
102
105
|
if not plugin_registry.list_plugins().get("parser", []):
|
|
103
106
|
discover_default_plugins()
|
|
104
|
-
|
|
107
|
+
|
|
105
108
|
# Get parser plugins
|
|
106
109
|
if parser_plugins:
|
|
107
110
|
self.parsers = [
|
|
@@ -112,63 +115,59 @@ class ToolProcessor:
|
|
|
112
115
|
else:
|
|
113
116
|
parser_names = plugin_registry.list_plugins().get("parser", [])
|
|
114
117
|
self.parsers = [
|
|
115
|
-
plugin_registry.get_plugin("parser", name)
|
|
116
|
-
for name in parser_names
|
|
118
|
+
plugin_registry.get_plugin("parser", name) for name in parser_names
|
|
117
119
|
]
|
|
118
|
-
|
|
120
|
+
|
|
119
121
|
self.logger.debug(f"Initialized with {len(self.parsers)} parser plugins")
|
|
120
|
-
|
|
122
|
+
|
|
121
123
|
async def process_text(
|
|
122
124
|
self,
|
|
123
125
|
text: str,
|
|
124
126
|
timeout: Optional[float] = None,
|
|
125
127
|
use_cache: bool = True,
|
|
126
|
-
request_id: Optional[str] = None
|
|
128
|
+
request_id: Optional[str] = None,
|
|
127
129
|
) -> List[ToolResult]:
|
|
128
130
|
"""
|
|
129
131
|
Process text to extract and execute tool calls.
|
|
130
|
-
|
|
132
|
+
|
|
131
133
|
Args:
|
|
132
134
|
text: Text to process.
|
|
133
135
|
timeout: Optional timeout for execution.
|
|
134
136
|
use_cache: Whether to use cached results.
|
|
135
137
|
request_id: Optional request ID for logging.
|
|
136
|
-
|
|
138
|
+
|
|
137
139
|
Returns:
|
|
138
140
|
List of tool results.
|
|
139
141
|
"""
|
|
140
142
|
# Create request context
|
|
141
143
|
with request_logging(request_id) as req_id:
|
|
142
144
|
self.logger.debug(f"Processing text ({len(text)} chars)")
|
|
143
|
-
|
|
145
|
+
|
|
144
146
|
# Extract tool calls
|
|
145
147
|
calls = await self._extract_tool_calls(text)
|
|
146
|
-
|
|
148
|
+
|
|
147
149
|
if not calls:
|
|
148
150
|
self.logger.debug("No tool calls found")
|
|
149
151
|
return []
|
|
150
|
-
|
|
152
|
+
|
|
151
153
|
self.logger.debug(f"Found {len(calls)} tool calls")
|
|
152
|
-
|
|
154
|
+
|
|
153
155
|
# Execute tool calls
|
|
154
156
|
with log_context_span("tool_execution", {"num_calls": len(calls)}):
|
|
155
157
|
# Check if any tools are unknown
|
|
156
|
-
tool_names =
|
|
157
|
-
unknown_tools = [
|
|
158
|
-
|
|
159
|
-
if not self.registry.get_tool(name)
|
|
160
|
-
]
|
|
161
|
-
|
|
158
|
+
tool_names = {call.tool for call in calls}
|
|
159
|
+
unknown_tools = [name for name in tool_names if not self.registry.get_tool(name)]
|
|
160
|
+
|
|
162
161
|
if unknown_tools:
|
|
163
162
|
self.logger.warning(f"Unknown tools: {unknown_tools}")
|
|
164
|
-
|
|
163
|
+
|
|
165
164
|
# Execute tools
|
|
166
165
|
results = await self.executor.execute(calls, timeout=timeout)
|
|
167
|
-
|
|
166
|
+
|
|
168
167
|
# Log metrics for each tool call
|
|
169
168
|
for call, result in zip(calls, results):
|
|
170
169
|
log_tool_call(call, result)
|
|
171
|
-
|
|
170
|
+
|
|
172
171
|
# Record metrics
|
|
173
172
|
duration = (result.end_time - result.start_time).total_seconds()
|
|
174
173
|
metrics.log_tool_execution(
|
|
@@ -177,47 +176,47 @@ class ToolProcessor:
|
|
|
177
176
|
duration=duration,
|
|
178
177
|
error=result.error,
|
|
179
178
|
cached=getattr(result, "cached", False),
|
|
180
|
-
attempts=getattr(result, "attempts", 1)
|
|
179
|
+
attempts=getattr(result, "attempts", 1),
|
|
181
180
|
)
|
|
182
|
-
|
|
181
|
+
|
|
183
182
|
return results
|
|
184
|
-
|
|
183
|
+
|
|
185
184
|
async def _extract_tool_calls(self, text: str) -> List[ToolCall]:
|
|
186
185
|
"""
|
|
187
186
|
Extract tool calls from text using all available parsers.
|
|
188
|
-
|
|
187
|
+
|
|
189
188
|
Args:
|
|
190
189
|
text: Text to parse.
|
|
191
|
-
|
|
190
|
+
|
|
192
191
|
Returns:
|
|
193
192
|
List of tool calls.
|
|
194
193
|
"""
|
|
195
|
-
all_calls = []
|
|
196
|
-
|
|
194
|
+
all_calls: List[ToolCall] = []
|
|
195
|
+
|
|
197
196
|
# Try each parser
|
|
198
197
|
with log_context_span("parsing", {"text_length": len(text)}):
|
|
199
198
|
for parser in self.parsers:
|
|
200
199
|
parser_name = parser.__class__.__name__
|
|
201
|
-
|
|
200
|
+
|
|
202
201
|
with log_context_span(f"parser.{parser_name}", log_duration=True):
|
|
203
202
|
start_time = time.time()
|
|
204
|
-
|
|
203
|
+
|
|
205
204
|
try:
|
|
206
205
|
# Try to parse
|
|
207
206
|
calls = parser.try_parse(text)
|
|
208
|
-
|
|
207
|
+
|
|
209
208
|
# Log success
|
|
210
209
|
duration = time.time() - start_time
|
|
211
210
|
metrics.log_parser_metric(
|
|
212
211
|
parser=parser_name,
|
|
213
212
|
success=True,
|
|
214
213
|
duration=duration,
|
|
215
|
-
num_calls=len(calls)
|
|
214
|
+
num_calls=len(calls),
|
|
216
215
|
)
|
|
217
|
-
|
|
216
|
+
|
|
218
217
|
# Add calls to result
|
|
219
218
|
all_calls.extend(calls)
|
|
220
|
-
|
|
219
|
+
|
|
221
220
|
except Exception as e:
|
|
222
221
|
# Log failure
|
|
223
222
|
duration = time.time() - start_time
|
|
@@ -225,16 +224,24 @@ class ToolProcessor:
|
|
|
225
224
|
parser=parser_name,
|
|
226
225
|
success=False,
|
|
227
226
|
duration=duration,
|
|
228
|
-
num_calls=0
|
|
227
|
+
num_calls=0,
|
|
229
228
|
)
|
|
230
229
|
self.logger.error(f"Parser {parser_name} failed: {str(e)}")
|
|
231
|
-
|
|
232
|
-
#
|
|
233
|
-
|
|
230
|
+
|
|
231
|
+
# ------------------------------------------------------------------ #
|
|
232
|
+
# Remove duplicates – use a stable digest instead of hashing a
|
|
233
|
+
# frozenset of argument items (which breaks on unhashable types).
|
|
234
|
+
# ------------------------------------------------------------------ #
|
|
235
|
+
def _args_digest(args: Dict[str, Any]) -> str:
|
|
236
|
+
"""Return a stable hash for any JSON-serialisable payload."""
|
|
237
|
+
blob = json.dumps(args, sort_keys=True, default=str)
|
|
238
|
+
return hashlib.md5(blob.encode()).hexdigest()
|
|
239
|
+
|
|
240
|
+
unique_calls: Dict[str, ToolCall] = {}
|
|
234
241
|
for call in all_calls:
|
|
235
|
-
key = f"{call.tool}:{
|
|
242
|
+
key = f"{call.tool}:{_args_digest(call.arguments)}"
|
|
236
243
|
unique_calls[key] = call
|
|
237
|
-
|
|
244
|
+
|
|
238
245
|
return list(unique_calls.values())
|
|
239
246
|
|
|
240
247
|
|
|
@@ -246,17 +253,17 @@ async def process_text(
|
|
|
246
253
|
text: str,
|
|
247
254
|
timeout: Optional[float] = None,
|
|
248
255
|
use_cache: bool = True,
|
|
249
|
-
request_id: Optional[str] = None
|
|
256
|
+
request_id: Optional[str] = None,
|
|
250
257
|
) -> List[ToolResult]:
|
|
251
258
|
"""
|
|
252
259
|
Process text with the default processor.
|
|
253
|
-
|
|
260
|
+
|
|
254
261
|
Args:
|
|
255
262
|
text: Text to process.
|
|
256
263
|
timeout: Optional timeout for execution.
|
|
257
264
|
use_cache: Whether to use cached results.
|
|
258
265
|
request_id: Optional request ID for logging.
|
|
259
|
-
|
|
266
|
+
|
|
260
267
|
Returns:
|
|
261
268
|
List of tool results.
|
|
262
269
|
"""
|
|
@@ -264,5 +271,5 @@ async def process_text(
|
|
|
264
271
|
text=text,
|
|
265
272
|
timeout=timeout,
|
|
266
273
|
use_cache=use_cache,
|
|
267
|
-
request_id=request_id
|
|
274
|
+
request_id=request_id,
|
|
268
275
|
)
|
|
@@ -174,6 +174,34 @@ class StreamManager:
|
|
|
174
174
|
|
|
175
175
|
def get_server_info(self) -> List[Dict[str, Any]]:
|
|
176
176
|
return self.server_info
|
|
177
|
+
|
|
178
|
+
async def list_tools(self, server_name: str) -> List[Dict[str, Any]]:
|
|
179
|
+
"""
|
|
180
|
+
List all tools available from a specific server.
|
|
181
|
+
|
|
182
|
+
This method is required by ProxyServerManager for proper tool discovery.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
server_name: Name of the server to query
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
List of tool definitions from the server
|
|
189
|
+
"""
|
|
190
|
+
if server_name not in self.transports:
|
|
191
|
+
logger.error(f"Server '{server_name}' not found in transports")
|
|
192
|
+
return []
|
|
193
|
+
|
|
194
|
+
# Get the transport for this server
|
|
195
|
+
transport = self.transports[server_name]
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
# Call the get_tools method on the transport
|
|
199
|
+
tools = await transport.get_tools()
|
|
200
|
+
logger.debug(f"Found {len(tools)} tools for server {server_name}")
|
|
201
|
+
return tools
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.error(f"Error listing tools for server {server_name}: {e}")
|
|
204
|
+
return []
|
|
177
205
|
|
|
178
206
|
# ------------------------------------------------------------------ #
|
|
179
207
|
# EXTRA HELPERS – ping / resources / prompts #
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
chuk_tool_processor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
chuk_tool_processor/core/__init__.py,sha256=slM7pZna88tyZrF3KtN22ApYyCqGNt5Yscv-knsLOOA,38
|
|
3
3
|
chuk_tool_processor/core/exceptions.py,sha256=h4zL1jpCY1Ud1wT8xDeMxZ8GR8ttmkObcv36peUHJEA,1571
|
|
4
|
-
chuk_tool_processor/core/processor.py,sha256=
|
|
4
|
+
chuk_tool_processor/core/processor.py,sha256=fT3Qj8vmeUWoqOqHmWroi7lfJsMl52DpFQz8LT9UQME,10280
|
|
5
5
|
chuk_tool_processor/execution/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
chuk_tool_processor/execution/tool_executor.py,sha256=e1EHE-744uJuB1XeZZF_6VT25Yg1RCd8XI3v8uOrOSo,1794
|
|
7
7
|
chuk_tool_processor/execution/strategies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -21,7 +21,7 @@ chuk_tool_processor/mcp/mcp_tool.py,sha256=TvZEudgQvaev2jaPw6OGsqAR5GNu6_cPaUCgq
|
|
|
21
21
|
chuk_tool_processor/mcp/register_mcp_tools.py,sha256=ofE7pEn6sKDH8HWvNamVOaXsitLOaG48M5GhcpqCBbs,2801
|
|
22
22
|
chuk_tool_processor/mcp/setup_mcp_sse.py,sha256=Ep2IKRdH1Y299bCxt9G0NtwnsvguYP6mpraZyUJ8OKU,2643
|
|
23
23
|
chuk_tool_processor/mcp/setup_mcp_stdio.py,sha256=NjTvAFqQHxxN3XubsTgYY3lTrvPVWlnwCzkzbz7WE_M,2747
|
|
24
|
-
chuk_tool_processor/mcp/stream_manager.py,sha256=
|
|
24
|
+
chuk_tool_processor/mcp/stream_manager.py,sha256=mrmlG54P_xLbDYz_rBjdu-OPMnbi916dgyJg7BrIbjM,12798
|
|
25
25
|
chuk_tool_processor/mcp/transport/__init__.py,sha256=7QQqeSKVKv0N9GcyJuYF0R4FDZeooii5RjggvFFg5GY,296
|
|
26
26
|
chuk_tool_processor/mcp/transport/base_transport.py,sha256=1E29LjWw5vLQrPUDF_9TJt63P5dxAAN7n6E_KiZbGUY,3427
|
|
27
27
|
chuk_tool_processor/mcp/transport/sse_transport.py,sha256=bryH9DOWOn5qr6LsimTriukDC4ix2kuRq6bUv9qOV20,7645
|
|
@@ -51,7 +51,7 @@ chuk_tool_processor/registry/providers/__init__.py,sha256=_0dg4YhyfAV0TXuR_i4ewX
|
|
|
51
51
|
chuk_tool_processor/registry/providers/memory.py,sha256=29aI5uvykjDmn9ymIukEdUtmTC9SXOAsDu9hw36XF44,4474
|
|
52
52
|
chuk_tool_processor/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
53
|
chuk_tool_processor/utils/validation.py,sha256=7ezn_o-3IHDrzOD3j6ttsAn2s3zS-jIjeBTuqicrs6A,3775
|
|
54
|
-
chuk_tool_processor-0.1.
|
|
55
|
-
chuk_tool_processor-0.1.
|
|
56
|
-
chuk_tool_processor-0.1.
|
|
57
|
-
chuk_tool_processor-0.1.
|
|
54
|
+
chuk_tool_processor-0.1.6.dist-info/METADATA,sha256=XsvUbxDUKZHtefun8o-xsg6HvAm5hxqrEhkTFrhkjLI,13703
|
|
55
|
+
chuk_tool_processor-0.1.6.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
|
|
56
|
+
chuk_tool_processor-0.1.6.dist-info/top_level.txt,sha256=7lTsnuRx4cOW4U2sNJWNxl4ZTt_J1ndkjTbj3pHPY5M,20
|
|
57
|
+
chuk_tool_processor-0.1.6.dist-info/RECORD,,
|
|
File without changes
|