"""In-memory response cache with TTL for the FusionAGI API.""" import hashlib import json import time from typing import Any class ResponseCache: """LRU-like response cache with configurable TTL. For production, replace with Redis-backed cache. """ def __init__(self, max_size: int = 1000, ttl_seconds: float = 300.0) -> None: self._cache: dict[str, tuple[float, Any]] = {} self._max_size = max_size self._ttl = ttl_seconds @staticmethod def _make_key(prompt: str, session_id: str, tenant_id: str = "default") -> str: """Generate a cache key from prompt + session context.""" raw = json.dumps({"prompt": prompt, "session": session_id, "tenant": tenant_id}, sort_keys=True) return hashlib.sha256(raw.encode()).hexdigest() def get(self, prompt: str, session_id: str, tenant_id: str = "default") -> Any | None: """Get cached response if it exists and hasn't expired.""" key = self._make_key(prompt, session_id, tenant_id) entry = self._cache.get(key) if entry is None: return None ts, value = entry if time.time() - ts > self._ttl: del self._cache[key] return None return value def set(self, prompt: str, session_id: str, value: Any, tenant_id: str = "default") -> None: """Cache a response.""" if len(self._cache) >= self._max_size: oldest_key = min(self._cache, key=lambda k: self._cache[k][0]) del self._cache[oldest_key] key = self._make_key(prompt, session_id, tenant_id) self._cache[key] = (time.time(), value) def invalidate(self, prompt: str, session_id: str, tenant_id: str = "default") -> bool: """Remove a specific cache entry.""" key = self._make_key(prompt, session_id, tenant_id) return self._cache.pop(key, None) is not None def clear(self) -> int: """Clear all cache entries. Returns count of cleared entries.""" count = len(self._cache) self._cache.clear() return count def stats(self) -> dict[str, int]: """Return cache statistics.""" now = time.time() active = sum(1 for ts, _ in self._cache.values() if now - ts <= self._ttl) return {"total": len(self._cache), "active": active, "max_size": self._max_size}