""" In-memory TTL cache for RAG responses. Keyed on (collection_name, normalised query). Only used for stateless queries (no conversation history). """ import hashlib import time from typing import Any, Optional _store: dict[str, tuple[Any, float]] = {} _index: dict[str, set[str]] = {} # collection_name → set of cache keys _MAX_ENTRIES = 500 TTL = 6 * 3600 # 6 hours _set_ctor = set # save builtin before it's shadowed by our module-level `set` function def _make_key(collection_name: str, query: str) -> str: raw = f"{collection_name}::{query.lower().strip()}" return hashlib.sha256(raw.encode()).hexdigest() def get(collection_name: str, query: str) -> Optional[Any]: k = _make_key(collection_name, query) entry = _store.get(k) if entry is None: return None value, expires_at = entry if time.monotonic() > expires_at: _store.pop(k, None) _index.get(collection_name, _set_ctor()).discard(k) return None return value def set(collection_name: str, query: str, value: Any) -> None: if len(_store) >= _MAX_ENTRIES: now = time.monotonic() expired = [k for k, (_, exp) in _store.items() if exp < now] for k in expired: _store.pop(k, None) if len(_store) >= _MAX_ENTRIES: oldest = min(_store, key=lambda k: _store[k][1]) _store.pop(oldest, None) k = _make_key(collection_name, query) _store[k] = (value, time.monotonic() + TTL) _index.setdefault(collection_name, _set_ctor()).add(k) def invalidate(collection_name: str) -> int: """Drop all cached entries for a collection — call after any KB update.""" keys = _index.pop(collection_name, _set_ctor()) for k in keys: _store.pop(k, None) return len(keys)