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.

Files changed (56) hide show
  1. chuk_tool_processor/core/__init__.py +1 -1
  2. chuk_tool_processor/core/exceptions.py +10 -4
  3. chuk_tool_processor/core/processor.py +97 -97
  4. chuk_tool_processor/execution/strategies/inprocess_strategy.py +142 -150
  5. chuk_tool_processor/execution/strategies/subprocess_strategy.py +200 -205
  6. chuk_tool_processor/execution/tool_executor.py +82 -84
  7. chuk_tool_processor/execution/wrappers/caching.py +102 -103
  8. chuk_tool_processor/execution/wrappers/rate_limiting.py +45 -42
  9. chuk_tool_processor/execution/wrappers/retry.py +23 -25
  10. chuk_tool_processor/logging/__init__.py +23 -17
  11. chuk_tool_processor/logging/context.py +40 -45
  12. chuk_tool_processor/logging/formatter.py +22 -21
  13. chuk_tool_processor/logging/helpers.py +24 -38
  14. chuk_tool_processor/logging/metrics.py +11 -13
  15. chuk_tool_processor/mcp/__init__.py +8 -12
  16. chuk_tool_processor/mcp/mcp_tool.py +124 -112
  17. chuk_tool_processor/mcp/register_mcp_tools.py +17 -17
  18. chuk_tool_processor/mcp/setup_mcp_http_streamable.py +11 -13
  19. chuk_tool_processor/mcp/setup_mcp_sse.py +11 -13
  20. chuk_tool_processor/mcp/setup_mcp_stdio.py +7 -9
  21. chuk_tool_processor/mcp/stream_manager.py +168 -204
  22. chuk_tool_processor/mcp/transport/__init__.py +4 -4
  23. chuk_tool_processor/mcp/transport/base_transport.py +43 -58
  24. chuk_tool_processor/mcp/transport/http_streamable_transport.py +145 -163
  25. chuk_tool_processor/mcp/transport/sse_transport.py +217 -255
  26. chuk_tool_processor/mcp/transport/stdio_transport.py +188 -190
  27. chuk_tool_processor/models/__init__.py +1 -1
  28. chuk_tool_processor/models/execution_strategy.py +16 -21
  29. chuk_tool_processor/models/streaming_tool.py +28 -25
  30. chuk_tool_processor/models/tool_call.py +19 -34
  31. chuk_tool_processor/models/tool_export_mixin.py +22 -8
  32. chuk_tool_processor/models/tool_result.py +40 -77
  33. chuk_tool_processor/models/validated_tool.py +14 -16
  34. chuk_tool_processor/plugins/__init__.py +1 -1
  35. chuk_tool_processor/plugins/discovery.py +10 -10
  36. chuk_tool_processor/plugins/parsers/__init__.py +1 -1
  37. chuk_tool_processor/plugins/parsers/base.py +1 -2
  38. chuk_tool_processor/plugins/parsers/function_call_tool.py +13 -8
  39. chuk_tool_processor/plugins/parsers/json_tool.py +4 -3
  40. chuk_tool_processor/plugins/parsers/openai_tool.py +12 -7
  41. chuk_tool_processor/plugins/parsers/xml_tool.py +4 -4
  42. chuk_tool_processor/registry/__init__.py +12 -12
  43. chuk_tool_processor/registry/auto_register.py +22 -30
  44. chuk_tool_processor/registry/decorators.py +127 -129
  45. chuk_tool_processor/registry/interface.py +26 -23
  46. chuk_tool_processor/registry/metadata.py +27 -22
  47. chuk_tool_processor/registry/provider.py +17 -18
  48. chuk_tool_processor/registry/providers/__init__.py +16 -19
  49. chuk_tool_processor/registry/providers/memory.py +18 -25
  50. chuk_tool_processor/registry/tool_export.py +42 -51
  51. chuk_tool_processor/utils/validation.py +15 -16
  52. {chuk_tool_processor-0.6.12.dist-info → chuk_tool_processor-0.6.14.dist-info}/METADATA +1 -1
  53. chuk_tool_processor-0.6.14.dist-info/RECORD +60 -0
  54. chuk_tool_processor-0.6.12.dist-info/RECORD +0 -60
  55. {chuk_tool_processor-0.6.12.dist-info → chuk_tool_processor-0.6.14.dist-info}/WHEEL +0 -0
  56. {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, timezone
22
- from typing import Any, Dict, List, Optional, Tuple, Set, Union
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: Optional[datetime] = Field(None, description="Expiration timestamp")
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) -> Optional[Any]:
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: Optional[int] = None,
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: Optional[str] = None) -> None:
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) -> Dict[str, Any]:
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: Optional[int] = 300) -> None:
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: Dict[str, Dict[str, CacheEntry]] = {}
141
+ self._cache: dict[str, dict[str, CacheEntry]] = {}
140
142
  self._default_ttl = default_ttl
141
143
  self._lock = asyncio.Lock()
142
- self._stats: Dict[str, int] = {
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) -> Optional[Any]:
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: Optional[int] = None,
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
- async def invalidate(self, tool: str, arguments_hash: Optional[str] = None) -> None:
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) -> Dict[str, Any]:
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: Optional[int] = None,
328
- tool_ttls: Optional[Dict[str, int]] = None,
329
- cacheable_tools: Optional[List[str]] = None,
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: Dict[str, Any]) -> str:
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) -> Optional[int]:
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: List[ToolCall],
398
+ calls: list[ToolCall],
400
399
  *,
401
- timeout: Optional[float] = None,
400
+ timeout: float | None = None,
402
401
  use_cache: bool = True,
403
- ) -> List[ToolResult]:
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: List[Tuple[int, ToolResult]] = []
423
- uncached: List[Tuple[int, ToolCall]] = []
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(timezone.utc)
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 (idx, call), result in zip(uncached, uncached_results):
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
- setattr(result, "cached", False)
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: List[Optional[ToolResult]] = [None] * len(calls)
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: Optional[int] = None):
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: Optional[Dict[str, Any]] = None):
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