chuk-tool-processor 0.6.12__py3-none-any.whl → 0.6.14__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/__init__.py +1 -1
- chuk_tool_processor/core/exceptions.py +10 -4
- chuk_tool_processor/core/processor.py +97 -97
- chuk_tool_processor/execution/strategies/inprocess_strategy.py +142 -150
- chuk_tool_processor/execution/strategies/subprocess_strategy.py +200 -205
- chuk_tool_processor/execution/tool_executor.py +82 -84
- chuk_tool_processor/execution/wrappers/caching.py +102 -103
- chuk_tool_processor/execution/wrappers/rate_limiting.py +45 -42
- chuk_tool_processor/execution/wrappers/retry.py +23 -25
- chuk_tool_processor/logging/__init__.py +23 -17
- chuk_tool_processor/logging/context.py +40 -45
- chuk_tool_processor/logging/formatter.py +22 -21
- chuk_tool_processor/logging/helpers.py +24 -38
- chuk_tool_processor/logging/metrics.py +11 -13
- chuk_tool_processor/mcp/__init__.py +8 -12
- chuk_tool_processor/mcp/mcp_tool.py +124 -112
- chuk_tool_processor/mcp/register_mcp_tools.py +17 -17
- chuk_tool_processor/mcp/setup_mcp_http_streamable.py +11 -13
- chuk_tool_processor/mcp/setup_mcp_sse.py +11 -13
- chuk_tool_processor/mcp/setup_mcp_stdio.py +7 -9
- chuk_tool_processor/mcp/stream_manager.py +168 -204
- chuk_tool_processor/mcp/transport/__init__.py +4 -4
- chuk_tool_processor/mcp/transport/base_transport.py +43 -58
- chuk_tool_processor/mcp/transport/http_streamable_transport.py +145 -163
- chuk_tool_processor/mcp/transport/sse_transport.py +217 -255
- chuk_tool_processor/mcp/transport/stdio_transport.py +188 -190
- chuk_tool_processor/models/__init__.py +1 -1
- chuk_tool_processor/models/execution_strategy.py +16 -21
- chuk_tool_processor/models/streaming_tool.py +28 -25
- chuk_tool_processor/models/tool_call.py +19 -34
- chuk_tool_processor/models/tool_export_mixin.py +22 -8
- chuk_tool_processor/models/tool_result.py +40 -77
- chuk_tool_processor/models/validated_tool.py +14 -16
- chuk_tool_processor/plugins/__init__.py +1 -1
- chuk_tool_processor/plugins/discovery.py +10 -10
- chuk_tool_processor/plugins/parsers/__init__.py +1 -1
- chuk_tool_processor/plugins/parsers/base.py +1 -2
- chuk_tool_processor/plugins/parsers/function_call_tool.py +13 -8
- chuk_tool_processor/plugins/parsers/json_tool.py +4 -3
- chuk_tool_processor/plugins/parsers/openai_tool.py +12 -7
- chuk_tool_processor/plugins/parsers/xml_tool.py +4 -4
- chuk_tool_processor/registry/__init__.py +12 -12
- chuk_tool_processor/registry/auto_register.py +22 -30
- chuk_tool_processor/registry/decorators.py +127 -129
- chuk_tool_processor/registry/interface.py +26 -23
- chuk_tool_processor/registry/metadata.py +27 -22
- chuk_tool_processor/registry/provider.py +17 -18
- chuk_tool_processor/registry/providers/__init__.py +16 -19
- chuk_tool_processor/registry/providers/memory.py +18 -25
- chuk_tool_processor/registry/tool_export.py +42 -51
- chuk_tool_processor/utils/validation.py +15 -16
- {chuk_tool_processor-0.6.12.dist-info → chuk_tool_processor-0.6.14.dist-info}/METADATA +1 -1
- chuk_tool_processor-0.6.14.dist-info/RECORD +60 -0
- chuk_tool_processor-0.6.12.dist-info/RECORD +0 -60
- {chuk_tool_processor-0.6.12.dist-info → chuk_tool_processor-0.6.14.dist-info}/WHEEL +0 -0
- {chuk_tool_processor-0.6.12.dist-info → chuk_tool_processor-0.6.14.dist-info}/top_level.txt +0 -0
|
@@ -11,31 +11,32 @@ This module provides:
|
|
|
11
11
|
Results retrieved from cache are marked with `cached=True` and `machine="cache"`
|
|
12
12
|
for easy detection.
|
|
13
13
|
"""
|
|
14
|
+
|
|
14
15
|
from __future__ import annotations
|
|
15
16
|
|
|
16
17
|
import asyncio
|
|
17
18
|
import hashlib
|
|
18
19
|
import json
|
|
19
|
-
import logging
|
|
20
20
|
from abc import ABC, abstractmethod
|
|
21
|
-
from datetime import datetime, timedelta
|
|
22
|
-
from typing import Any
|
|
21
|
+
from datetime import UTC, datetime, timedelta
|
|
22
|
+
from typing import Any
|
|
23
23
|
|
|
24
24
|
from pydantic import BaseModel, Field
|
|
25
25
|
|
|
26
|
+
from chuk_tool_processor.logging import get_logger
|
|
26
27
|
from chuk_tool_processor.models.tool_call import ToolCall
|
|
27
28
|
from chuk_tool_processor.models.tool_result import ToolResult
|
|
28
|
-
from chuk_tool_processor.logging import get_logger
|
|
29
29
|
|
|
30
30
|
logger = get_logger("chuk_tool_processor.execution.wrappers.caching")
|
|
31
31
|
|
|
32
|
+
|
|
32
33
|
# --------------------------------------------------------------------------- #
|
|
33
34
|
# Cache primitives
|
|
34
35
|
# --------------------------------------------------------------------------- #
|
|
35
36
|
class CacheEntry(BaseModel):
|
|
36
37
|
"""
|
|
37
38
|
Model representing a cached tool result.
|
|
38
|
-
|
|
39
|
+
|
|
39
40
|
Attributes:
|
|
40
41
|
tool: Name of the tool
|
|
41
42
|
arguments_hash: Hash of the tool arguments
|
|
@@ -43,29 +44,30 @@ class CacheEntry(BaseModel):
|
|
|
43
44
|
created_at: When the entry was created
|
|
44
45
|
expires_at: When the entry expires (None = no expiration)
|
|
45
46
|
"""
|
|
47
|
+
|
|
46
48
|
tool: str = Field(..., description="Tool name")
|
|
47
49
|
arguments_hash: str = Field(..., description="MD5 hash of arguments")
|
|
48
50
|
result: Any = Field(..., description="Cached result value")
|
|
49
51
|
created_at: datetime = Field(..., description="Creation timestamp")
|
|
50
|
-
expires_at:
|
|
52
|
+
expires_at: datetime | None = Field(None, description="Expiration timestamp")
|
|
51
53
|
|
|
52
54
|
|
|
53
55
|
class CacheInterface(ABC):
|
|
54
56
|
"""
|
|
55
57
|
Abstract interface for tool result caches.
|
|
56
|
-
|
|
58
|
+
|
|
57
59
|
All cache implementations must be async-native and thread-safe.
|
|
58
60
|
"""
|
|
59
61
|
|
|
60
62
|
@abstractmethod
|
|
61
|
-
async def get(self, tool: str, arguments_hash: str) ->
|
|
63
|
+
async def get(self, tool: str, arguments_hash: str) -> Any | None:
|
|
62
64
|
"""
|
|
63
65
|
Get a cached result by tool name and arguments hash.
|
|
64
|
-
|
|
66
|
+
|
|
65
67
|
Args:
|
|
66
68
|
tool: Tool name
|
|
67
69
|
arguments_hash: Hash of the arguments
|
|
68
|
-
|
|
70
|
+
|
|
69
71
|
Returns:
|
|
70
72
|
Cached result value or None if not found
|
|
71
73
|
"""
|
|
@@ -78,11 +80,11 @@ class CacheInterface(ABC):
|
|
|
78
80
|
arguments_hash: str,
|
|
79
81
|
result: Any,
|
|
80
82
|
*,
|
|
81
|
-
ttl:
|
|
83
|
+
ttl: int | None = None,
|
|
82
84
|
) -> None:
|
|
83
85
|
"""
|
|
84
86
|
Set a cache entry.
|
|
85
|
-
|
|
87
|
+
|
|
86
88
|
Args:
|
|
87
89
|
tool: Tool name
|
|
88
90
|
arguments_hash: Hash of the arguments
|
|
@@ -92,29 +94,29 @@ class CacheInterface(ABC):
|
|
|
92
94
|
pass
|
|
93
95
|
|
|
94
96
|
@abstractmethod
|
|
95
|
-
async def invalidate(self, tool: str, arguments_hash:
|
|
97
|
+
async def invalidate(self, tool: str, arguments_hash: str | None = None) -> None:
|
|
96
98
|
"""
|
|
97
99
|
Invalidate cache entries.
|
|
98
|
-
|
|
100
|
+
|
|
99
101
|
Args:
|
|
100
102
|
tool: Tool name
|
|
101
103
|
arguments_hash: Optional arguments hash. If None, all entries for the tool are invalidated.
|
|
102
104
|
"""
|
|
103
105
|
pass
|
|
104
|
-
|
|
106
|
+
|
|
105
107
|
async def clear(self) -> None:
|
|
106
108
|
"""
|
|
107
109
|
Clear all cache entries.
|
|
108
|
-
|
|
110
|
+
|
|
109
111
|
Default implementation raises NotImplementedError.
|
|
110
112
|
Override in subclasses to provide an efficient implementation.
|
|
111
113
|
"""
|
|
112
114
|
raise NotImplementedError("Cache clear not implemented")
|
|
113
|
-
|
|
114
|
-
async def get_stats(self) ->
|
|
115
|
+
|
|
116
|
+
async def get_stats(self) -> dict[str, Any]:
|
|
115
117
|
"""
|
|
116
118
|
Get cache statistics.
|
|
117
|
-
|
|
119
|
+
|
|
118
120
|
Returns:
|
|
119
121
|
Dict with cache statistics (implementation-specific)
|
|
120
122
|
"""
|
|
@@ -124,46 +126,46 @@ class CacheInterface(ABC):
|
|
|
124
126
|
class InMemoryCache(CacheInterface):
|
|
125
127
|
"""
|
|
126
128
|
In-memory cache implementation with async thread-safety.
|
|
127
|
-
|
|
129
|
+
|
|
128
130
|
This cache uses a two-level dictionary structure with asyncio locks
|
|
129
131
|
to ensure thread safety. Entries can have optional TTL values.
|
|
130
132
|
"""
|
|
131
133
|
|
|
132
|
-
def __init__(self, default_ttl:
|
|
134
|
+
def __init__(self, default_ttl: int | None = 300) -> None:
|
|
133
135
|
"""
|
|
134
136
|
Initialize the in-memory cache.
|
|
135
|
-
|
|
137
|
+
|
|
136
138
|
Args:
|
|
137
139
|
default_ttl: Default time-to-live in seconds (None = no expiration)
|
|
138
140
|
"""
|
|
139
|
-
self._cache:
|
|
141
|
+
self._cache: dict[str, dict[str, CacheEntry]] = {}
|
|
140
142
|
self._default_ttl = default_ttl
|
|
141
143
|
self._lock = asyncio.Lock()
|
|
142
|
-
self._stats:
|
|
144
|
+
self._stats: dict[str, int] = {
|
|
143
145
|
"hits": 0,
|
|
144
146
|
"misses": 0,
|
|
145
147
|
"sets": 0,
|
|
146
148
|
"invalidations": 0,
|
|
147
149
|
"expirations": 0,
|
|
148
150
|
}
|
|
149
|
-
|
|
151
|
+
|
|
150
152
|
logger.debug(f"Initialized InMemoryCache with default_ttl={default_ttl}s")
|
|
151
153
|
|
|
152
154
|
# ---------------------- Helper methods ------------------------ #
|
|
153
155
|
def _is_expired(self, entry: CacheEntry) -> bool:
|
|
154
156
|
"""Check if an entry is expired."""
|
|
155
157
|
return entry.expires_at is not None and entry.expires_at < datetime.now()
|
|
156
|
-
|
|
158
|
+
|
|
157
159
|
async def _prune_expired(self) -> int:
|
|
158
160
|
"""
|
|
159
161
|
Remove all expired entries.
|
|
160
|
-
|
|
162
|
+
|
|
161
163
|
Returns:
|
|
162
164
|
Number of entries removed
|
|
163
165
|
"""
|
|
164
166
|
now = datetime.now()
|
|
165
167
|
removed = 0
|
|
166
|
-
|
|
168
|
+
|
|
167
169
|
async with self._lock:
|
|
168
170
|
for tool in list(self._cache.keys()):
|
|
169
171
|
tool_cache = self._cache[tool]
|
|
@@ -173,42 +175,42 @@ class InMemoryCache(CacheInterface):
|
|
|
173
175
|
del tool_cache[arg_hash]
|
|
174
176
|
removed += 1
|
|
175
177
|
self._stats["expirations"] += 1
|
|
176
|
-
|
|
178
|
+
|
|
177
179
|
# Remove empty tool caches
|
|
178
180
|
if not tool_cache:
|
|
179
181
|
del self._cache[tool]
|
|
180
|
-
|
|
182
|
+
|
|
181
183
|
return removed
|
|
182
184
|
|
|
183
185
|
# ---------------------- CacheInterface implementation ------------------------ #
|
|
184
|
-
async def get(self, tool: str, arguments_hash: str) ->
|
|
186
|
+
async def get(self, tool: str, arguments_hash: str) -> Any | None:
|
|
185
187
|
"""
|
|
186
188
|
Get a cached result, checking expiration.
|
|
187
|
-
|
|
189
|
+
|
|
188
190
|
Args:
|
|
189
191
|
tool: Tool name
|
|
190
192
|
arguments_hash: Hash of the arguments
|
|
191
|
-
|
|
193
|
+
|
|
192
194
|
Returns:
|
|
193
195
|
Cached result value or None if not found or expired
|
|
194
196
|
"""
|
|
195
197
|
async with self._lock:
|
|
196
198
|
entry = self._cache.get(tool, {}).get(arguments_hash)
|
|
197
|
-
|
|
199
|
+
|
|
198
200
|
if not entry:
|
|
199
201
|
self._stats["misses"] += 1
|
|
200
202
|
return None
|
|
201
|
-
|
|
203
|
+
|
|
202
204
|
if self._is_expired(entry):
|
|
203
205
|
# Prune expired entry
|
|
204
206
|
del self._cache[tool][arguments_hash]
|
|
205
207
|
if not self._cache[tool]:
|
|
206
208
|
del self._cache[tool]
|
|
207
|
-
|
|
209
|
+
|
|
208
210
|
self._stats["expirations"] += 1
|
|
209
211
|
self._stats["misses"] += 1
|
|
210
212
|
return None
|
|
211
|
-
|
|
213
|
+
|
|
212
214
|
self._stats["hits"] += 1
|
|
213
215
|
return entry.result
|
|
214
216
|
|
|
@@ -218,11 +220,11 @@ class InMemoryCache(CacheInterface):
|
|
|
218
220
|
arguments_hash: str,
|
|
219
221
|
result: Any,
|
|
220
222
|
*,
|
|
221
|
-
ttl:
|
|
223
|
+
ttl: int | None = None,
|
|
222
224
|
) -> None:
|
|
223
225
|
"""
|
|
224
226
|
Set a cache entry with optional custom TTL.
|
|
225
|
-
|
|
227
|
+
|
|
226
228
|
Args:
|
|
227
229
|
tool: Tool name
|
|
228
230
|
arguments_hash: Hash of the arguments
|
|
@@ -231,11 +233,11 @@ class InMemoryCache(CacheInterface):
|
|
|
231
233
|
"""
|
|
232
234
|
async with self._lock:
|
|
233
235
|
now = datetime.now()
|
|
234
|
-
|
|
236
|
+
|
|
235
237
|
# Calculate expiration
|
|
236
238
|
use_ttl = ttl if ttl is not None else self._default_ttl
|
|
237
239
|
expires_at = now + timedelta(seconds=use_ttl) if use_ttl is not None else None
|
|
238
|
-
|
|
240
|
+
|
|
239
241
|
# Create entry
|
|
240
242
|
entry = CacheEntry(
|
|
241
243
|
tool=tool,
|
|
@@ -244,20 +246,17 @@ class InMemoryCache(CacheInterface):
|
|
|
244
246
|
created_at=now,
|
|
245
247
|
expires_at=expires_at,
|
|
246
248
|
)
|
|
247
|
-
|
|
249
|
+
|
|
248
250
|
# Store in cache
|
|
249
251
|
self._cache.setdefault(tool, {})[arguments_hash] = entry
|
|
250
252
|
self._stats["sets"] += 1
|
|
251
|
-
|
|
252
|
-
logger.debug(
|
|
253
|
-
f"Cached result for {tool} (TTL: "
|
|
254
|
-
f"{use_ttl if use_ttl is not None else 'none'}s)"
|
|
255
|
-
)
|
|
256
253
|
|
|
257
|
-
|
|
254
|
+
logger.debug(f"Cached result for {tool} (TTL: {use_ttl if use_ttl is not None else 'none'}s)")
|
|
255
|
+
|
|
256
|
+
async def invalidate(self, tool: str, arguments_hash: str | None = None) -> None:
|
|
258
257
|
"""
|
|
259
258
|
Invalidate cache entries for a tool.
|
|
260
|
-
|
|
259
|
+
|
|
261
260
|
Args:
|
|
262
261
|
tool: Tool name
|
|
263
262
|
arguments_hash: Optional arguments hash. If None, all entries for the tool are invalidated.
|
|
@@ -265,7 +264,7 @@ class InMemoryCache(CacheInterface):
|
|
|
265
264
|
async with self._lock:
|
|
266
265
|
if tool not in self._cache:
|
|
267
266
|
return
|
|
268
|
-
|
|
267
|
+
|
|
269
268
|
if arguments_hash:
|
|
270
269
|
# Invalidate specific entry
|
|
271
270
|
self._cache[tool].pop(arguments_hash, None)
|
|
@@ -279,7 +278,7 @@ class InMemoryCache(CacheInterface):
|
|
|
279
278
|
del self._cache[tool]
|
|
280
279
|
self._stats["invalidations"] += count
|
|
281
280
|
logger.debug(f"Invalidated all cache entries for {tool} ({count} entries)")
|
|
282
|
-
|
|
281
|
+
|
|
283
282
|
async def clear(self) -> None:
|
|
284
283
|
"""Clear all cache entries."""
|
|
285
284
|
async with self._lock:
|
|
@@ -287,11 +286,11 @@ class InMemoryCache(CacheInterface):
|
|
|
287
286
|
self._cache.clear()
|
|
288
287
|
self._stats["invalidations"] += count
|
|
289
288
|
logger.debug(f"Cleared entire cache ({count} entries)")
|
|
290
|
-
|
|
291
|
-
async def get_stats(self) ->
|
|
289
|
+
|
|
290
|
+
async def get_stats(self) -> dict[str, Any]:
|
|
292
291
|
"""
|
|
293
292
|
Get cache statistics.
|
|
294
|
-
|
|
293
|
+
|
|
295
294
|
Returns:
|
|
296
295
|
Dict with hits, misses, sets, invalidations, and entry counts
|
|
297
296
|
"""
|
|
@@ -300,20 +299,21 @@ class InMemoryCache(CacheInterface):
|
|
|
300
299
|
stats["implemented"] = True
|
|
301
300
|
stats["entry_count"] = sum(len(entries) for entries in self._cache.values())
|
|
302
301
|
stats["tool_count"] = len(self._cache)
|
|
303
|
-
|
|
302
|
+
|
|
304
303
|
# Calculate hit rate
|
|
305
304
|
total_gets = stats["hits"] + stats["misses"]
|
|
306
305
|
stats["hit_rate"] = stats["hits"] / total_gets if total_gets > 0 else 0.0
|
|
307
|
-
|
|
306
|
+
|
|
308
307
|
return stats
|
|
309
308
|
|
|
309
|
+
|
|
310
310
|
# --------------------------------------------------------------------------- #
|
|
311
311
|
# Executor wrapper
|
|
312
312
|
# --------------------------------------------------------------------------- #
|
|
313
313
|
class CachingToolExecutor:
|
|
314
314
|
"""
|
|
315
315
|
Executor wrapper that transparently caches successful tool results.
|
|
316
|
-
|
|
316
|
+
|
|
317
317
|
This wrapper intercepts tool calls, checks if results are available in cache,
|
|
318
318
|
and only executes uncached calls. Successful results are automatically stored
|
|
319
319
|
in the cache for future use.
|
|
@@ -324,13 +324,13 @@ class CachingToolExecutor:
|
|
|
324
324
|
executor: Any,
|
|
325
325
|
cache: CacheInterface,
|
|
326
326
|
*,
|
|
327
|
-
default_ttl:
|
|
328
|
-
tool_ttls:
|
|
329
|
-
cacheable_tools:
|
|
327
|
+
default_ttl: int | None = None,
|
|
328
|
+
tool_ttls: dict[str, int] | None = None,
|
|
329
|
+
cacheable_tools: list[str] | None = None,
|
|
330
330
|
) -> None:
|
|
331
331
|
"""
|
|
332
332
|
Initialize the caching executor.
|
|
333
|
-
|
|
333
|
+
|
|
334
334
|
Args:
|
|
335
335
|
executor: The underlying executor to wrap
|
|
336
336
|
cache: Cache implementation to use
|
|
@@ -343,21 +343,20 @@ class CachingToolExecutor:
|
|
|
343
343
|
self.default_ttl = default_ttl
|
|
344
344
|
self.tool_ttls = tool_ttls or {}
|
|
345
345
|
self.cacheable_tools = set(cacheable_tools) if cacheable_tools else None
|
|
346
|
-
|
|
346
|
+
|
|
347
347
|
logger.debug(
|
|
348
|
-
f"Initialized CachingToolExecutor with {len(self.tool_ttls)} custom TTLs, "
|
|
349
|
-
f"default TTL={default_ttl}s"
|
|
348
|
+
f"Initialized CachingToolExecutor with {len(self.tool_ttls)} custom TTLs, default TTL={default_ttl}s"
|
|
350
349
|
)
|
|
351
350
|
|
|
352
351
|
# ---------------------------- helpers ----------------------------- #
|
|
353
352
|
@staticmethod
|
|
354
|
-
def _hash_arguments(arguments:
|
|
353
|
+
def _hash_arguments(arguments: dict[str, Any]) -> str:
|
|
355
354
|
"""
|
|
356
355
|
Generate a stable hash for tool arguments.
|
|
357
|
-
|
|
356
|
+
|
|
358
357
|
Args:
|
|
359
358
|
arguments: Tool arguments dict
|
|
360
|
-
|
|
359
|
+
|
|
361
360
|
Returns:
|
|
362
361
|
MD5 hash of the sorted JSON representation
|
|
363
362
|
"""
|
|
@@ -372,22 +371,22 @@ class CachingToolExecutor:
|
|
|
372
371
|
def _is_cacheable(self, tool: str) -> bool:
|
|
373
372
|
"""
|
|
374
373
|
Check if a tool is cacheable.
|
|
375
|
-
|
|
374
|
+
|
|
376
375
|
Args:
|
|
377
376
|
tool: Tool name
|
|
378
|
-
|
|
377
|
+
|
|
379
378
|
Returns:
|
|
380
379
|
True if the tool should be cached, False otherwise
|
|
381
380
|
"""
|
|
382
381
|
return self.cacheable_tools is None or tool in self.cacheable_tools
|
|
383
382
|
|
|
384
|
-
def _ttl_for(self, tool: str) ->
|
|
383
|
+
def _ttl_for(self, tool: str) -> int | None:
|
|
385
384
|
"""
|
|
386
385
|
Get the TTL for a specific tool.
|
|
387
|
-
|
|
386
|
+
|
|
388
387
|
Args:
|
|
389
388
|
tool: Tool name
|
|
390
|
-
|
|
389
|
+
|
|
391
390
|
Returns:
|
|
392
391
|
Tool-specific TTL or default TTL
|
|
393
392
|
"""
|
|
@@ -396,31 +395,31 @@ class CachingToolExecutor:
|
|
|
396
395
|
# ------------------------------ API ------------------------------- #
|
|
397
396
|
async def execute(
|
|
398
397
|
self,
|
|
399
|
-
calls:
|
|
398
|
+
calls: list[ToolCall],
|
|
400
399
|
*,
|
|
401
|
-
timeout:
|
|
400
|
+
timeout: float | None = None,
|
|
402
401
|
use_cache: bool = True,
|
|
403
|
-
) ->
|
|
402
|
+
) -> list[ToolResult]:
|
|
404
403
|
"""
|
|
405
404
|
Execute tool calls with caching.
|
|
406
|
-
|
|
405
|
+
|
|
407
406
|
Args:
|
|
408
407
|
calls: List of tool calls to execute
|
|
409
408
|
timeout: Optional timeout for execution
|
|
410
409
|
use_cache: Whether to use cached results
|
|
411
|
-
|
|
410
|
+
|
|
412
411
|
Returns:
|
|
413
412
|
List of tool results in the same order as calls
|
|
414
413
|
"""
|
|
415
414
|
# Handle empty calls
|
|
416
415
|
if not calls:
|
|
417
416
|
return []
|
|
418
|
-
|
|
417
|
+
|
|
419
418
|
# ------------------------------------------------------------------
|
|
420
419
|
# 1. Split calls into cached / uncached buckets
|
|
421
420
|
# ------------------------------------------------------------------
|
|
422
|
-
cached_hits:
|
|
423
|
-
uncached:
|
|
421
|
+
cached_hits: list[tuple[int, ToolResult]] = []
|
|
422
|
+
uncached: list[tuple[int, ToolCall]] = []
|
|
424
423
|
|
|
425
424
|
if use_cache:
|
|
426
425
|
for idx, call in enumerate(calls):
|
|
@@ -428,10 +427,10 @@ class CachingToolExecutor:
|
|
|
428
427
|
logger.debug(f"Tool {call.tool} is not cacheable, executing directly")
|
|
429
428
|
uncached.append((idx, call))
|
|
430
429
|
continue
|
|
431
|
-
|
|
430
|
+
|
|
432
431
|
h = self._hash_arguments(call.arguments)
|
|
433
432
|
cached_val = await self.cache.get(call.tool, h)
|
|
434
|
-
|
|
433
|
+
|
|
435
434
|
if cached_val is None:
|
|
436
435
|
# Cache miss
|
|
437
436
|
logger.debug(f"Cache miss for {call.tool}")
|
|
@@ -439,7 +438,7 @@ class CachingToolExecutor:
|
|
|
439
438
|
else:
|
|
440
439
|
# Cache hit
|
|
441
440
|
logger.debug(f"Cache hit for {call.tool}")
|
|
442
|
-
now = datetime.now(
|
|
441
|
+
now = datetime.now(UTC)
|
|
443
442
|
cached_hits.append(
|
|
444
443
|
(
|
|
445
444
|
idx,
|
|
@@ -473,21 +472,19 @@ class CachingToolExecutor:
|
|
|
473
472
|
executor_kwargs = {"timeout": timeout}
|
|
474
473
|
if hasattr(self.executor, "use_cache"):
|
|
475
474
|
executor_kwargs["use_cache"] = False
|
|
476
|
-
|
|
477
|
-
uncached_results = await self.executor.execute(
|
|
478
|
-
[call for _, call in uncached], **executor_kwargs
|
|
479
|
-
)
|
|
475
|
+
|
|
476
|
+
uncached_results = await self.executor.execute([call for _, call in uncached], **executor_kwargs)
|
|
480
477
|
|
|
481
478
|
# ------------------------------------------------------------------
|
|
482
479
|
# 3. Insert fresh results into cache
|
|
483
480
|
# ------------------------------------------------------------------
|
|
484
481
|
if use_cache:
|
|
485
482
|
cache_tasks = []
|
|
486
|
-
for (
|
|
483
|
+
for (_idx, call), result in zip(uncached, uncached_results, strict=False):
|
|
487
484
|
if result.error is None and self._is_cacheable(call.tool):
|
|
488
485
|
ttl = self._ttl_for(call.tool)
|
|
489
486
|
logger.debug(f"Caching result for {call.tool} with TTL={ttl}s")
|
|
490
|
-
|
|
487
|
+
|
|
491
488
|
# Create task but don't await yet (for concurrent caching)
|
|
492
489
|
task = self.cache.set(
|
|
493
490
|
call.tool,
|
|
@@ -496,14 +493,14 @@ class CachingToolExecutor:
|
|
|
496
493
|
ttl=ttl,
|
|
497
494
|
)
|
|
498
495
|
cache_tasks.append(task)
|
|
499
|
-
|
|
496
|
+
|
|
500
497
|
# Flag as non-cached so callers can tell
|
|
501
498
|
if hasattr(result, "cached"):
|
|
502
499
|
result.cached = False
|
|
503
500
|
else:
|
|
504
501
|
# For older ToolResult objects that might not have cached attribute
|
|
505
|
-
|
|
506
|
-
|
|
502
|
+
result.cached = False
|
|
503
|
+
|
|
507
504
|
# Wait for all cache operations to complete
|
|
508
505
|
if cache_tasks:
|
|
509
506
|
await asyncio.gather(*cache_tasks)
|
|
@@ -511,10 +508,10 @@ class CachingToolExecutor:
|
|
|
511
508
|
# ------------------------------------------------------------------
|
|
512
509
|
# 4. Merge cached-hits + fresh results in original order
|
|
513
510
|
# ------------------------------------------------------------------
|
|
514
|
-
merged:
|
|
511
|
+
merged: list[ToolResult | None] = [None] * len(calls)
|
|
515
512
|
for idx, hit in cached_hits:
|
|
516
513
|
merged[idx] = hit
|
|
517
|
-
for (idx, _), fresh in zip(uncached, uncached_results):
|
|
514
|
+
for (idx, _), fresh in zip(uncached, uncached_results, strict=False):
|
|
518
515
|
merged[idx] = fresh
|
|
519
516
|
|
|
520
517
|
# If calls was empty, merged remains []
|
|
@@ -524,22 +521,23 @@ class CachingToolExecutor:
|
|
|
524
521
|
# --------------------------------------------------------------------------- #
|
|
525
522
|
# Convenience decorators
|
|
526
523
|
# --------------------------------------------------------------------------- #
|
|
527
|
-
def cacheable(ttl:
|
|
524
|
+
def cacheable(ttl: int | None = None):
|
|
528
525
|
"""
|
|
529
526
|
Decorator to mark a tool class as cacheable.
|
|
530
|
-
|
|
527
|
+
|
|
531
528
|
Example:
|
|
532
529
|
@cacheable(ttl=600) # Cache for 10 minutes
|
|
533
530
|
class WeatherTool:
|
|
534
531
|
async def execute(self, location: str) -> Dict[str, Any]:
|
|
535
532
|
# Implementation
|
|
536
|
-
|
|
533
|
+
|
|
537
534
|
Args:
|
|
538
535
|
ttl: Optional custom time-to-live in seconds
|
|
539
|
-
|
|
536
|
+
|
|
540
537
|
Returns:
|
|
541
538
|
Decorated class with caching metadata
|
|
542
539
|
"""
|
|
540
|
+
|
|
543
541
|
def decorator(cls):
|
|
544
542
|
cls._cacheable = True # Runtime flag picked up by higher-level code
|
|
545
543
|
if ttl is not None:
|
|
@@ -549,21 +547,22 @@ def cacheable(ttl: Optional[int] = None):
|
|
|
549
547
|
return decorator
|
|
550
548
|
|
|
551
549
|
|
|
552
|
-
def invalidate_cache(tool: str, arguments:
|
|
550
|
+
def invalidate_cache(tool: str, arguments: dict[str, Any] | None = None):
|
|
553
551
|
"""
|
|
554
552
|
Create an async function that invalidates specific cache entries.
|
|
555
|
-
|
|
553
|
+
|
|
556
554
|
Example:
|
|
557
555
|
invalidator = invalidate_cache("weather", {"location": "London"})
|
|
558
556
|
await invalidator(cache) # Call with a cache instance
|
|
559
|
-
|
|
557
|
+
|
|
560
558
|
Args:
|
|
561
559
|
tool: Tool name
|
|
562
560
|
arguments: Optional arguments dict. If None, all entries for the tool are invalidated.
|
|
563
|
-
|
|
561
|
+
|
|
564
562
|
Returns:
|
|
565
563
|
Async function that takes a cache instance and invalidates entries
|
|
566
564
|
"""
|
|
565
|
+
|
|
567
566
|
async def _invalidate(cache: CacheInterface):
|
|
568
567
|
if arguments is not None:
|
|
569
568
|
h = hashlib.md5(json.dumps(arguments, sort_keys=True, default=str).encode()).hexdigest()
|
|
@@ -573,4 +572,4 @@ def invalidate_cache(tool: str, arguments: Optional[Dict[str, Any]] = None):
|
|
|
573
572
|
await cache.invalidate(tool)
|
|
574
573
|
logger.debug(f"Invalidated all cache entries for {tool}")
|
|
575
574
|
|
|
576
|
-
return _invalidate
|
|
575
|
+
return _invalidate
|