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.
@@ -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
- # imports
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 = set(call.tool for call in calls)
157
- unknown_tools = [
158
- name for name in tool_names
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
- # Remove duplicates
233
- unique_calls = {}
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}:{hash(frozenset(call.arguments.items()))}"
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chuk-tool-processor
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -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=ud7ezONnUFh_aDSapiBGNx-LtZfhAFpYjFuw2m_tFXk,10165
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=qIWzsQCTlu1SQQBExAdvBHGB3T5isQDyMhj29WkfbKQ,11779
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.4.dist-info/METADATA,sha256=ekQNpVXyJrLw9kaLnhHW4iI1Q5do07T6Ol2QfeRsQn0,13703
55
- chuk_tool_processor-0.1.4.dist-info/WHEEL,sha256=GHB6lJx2juba1wDgXDNlMTyM13ckjBMKf-OnwgKOCtA,91
56
- chuk_tool_processor-0.1.4.dist-info/top_level.txt,sha256=7lTsnuRx4cOW4U2sNJWNxl4ZTt_J1ndkjTbj3pHPY5M,20
57
- chuk_tool_processor-0.1.4.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.3.0)
2
+ Generator: setuptools (80.3.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5