""" TTL Cache module for Agent Tools. Provides an in-memory cache with time-to-live eviction to avoid redundant API calls and rate limiting. """ import json import threading import time from dataclasses import dataclass from typing import Optional @dataclass class CacheEntry: """A single cache entry with its stored value and creation timestamp.""" value: str timestamp: float class TTLCache: """In-memory cache with time-to-live eviction. Stores string results keyed by function name + parameters. Thread-safe via threading.Lock on all read/write operations. """ def __init__(self, default_ttl: int = 300, max_age: int = 900): """Initialize the cache. Args: default_ttl: Time-to-live in seconds (default 300 = 5 minutes). Entries older than this are considered expired on get(). max_age: Maximum entry age before forced eviction (default 900 = 15 minutes). Entries older than this are removed during eviction sweeps. """ self.default_ttl = default_ttl self.max_age = max_age self._store: dict[str, CacheEntry] = {} self._lock = threading.Lock() def make_key(self, func_name: str, **kwargs) -> str: """Generate a deterministic cache key from function name and parameters. Uses sorted JSON serialization to ensure the same parameters always produce the same key regardless of argument order. Args: func_name: The name of the tool function. **kwargs: The parameters passed to the function. Returns: A string key in the format "func_name:{sorted_params_json}". """ sorted_params_json = json.dumps(kwargs, sort_keys=True) return f"{func_name}:{sorted_params_json}" def get(self, key: str) -> Optional[str]: """Return cached value if it exists and is within TTL, else None. Always triggers _evict_stale() to clean up old entries. Args: key: The cache key to look up. Returns: The cached string value if valid, or None if expired/missing. """ with self._lock: self._evict_stale() entry = self._store.get(key) if entry is None: return None if time.time() - entry.timestamp > self.default_ttl: return None return entry.value def set(self, key: str, value: str) -> None: """Store a value in the cache with the current timestamp. Args: key: The cache key. value: The string value to cache. """ with self._lock: self._store[key] = CacheEntry(value=value, timestamp=time.time()) def _evict_stale(self) -> None: """Remove entries older than max_age (15 minutes by default). This method is called internally and assumes the lock is already held. """ current_time = time.time() stale_keys = [ key for key, entry in self._store.items() if current_time - entry.timestamp > self.max_age ] for key in stale_keys: del self._store[key] def clear(self) -> None: """Remove all entries from the cache. Useful for testing.""" with self._lock: self._store.clear()