Spaces:
Sleeping
Sleeping
| import json | |
| from threading import Lock | |
| from typing import Any, Dict, Hashable, Optional, Tuple | |
| from cachetools import TTLCache | |
| from app.core.config import get_settings | |
| from app.core.logging import get_logger | |
| logger = get_logger(__name__) | |
| _settings = get_settings() | |
| _CACHE_ENABLED: bool = getattr(_settings, "CACHE_ENABLED", True) | |
| # TTLs are intentionally short and in-code defaults; no env required. | |
| _SEARCH_TTL_SECONDS = 60 | |
| _CHAT_TTL_SECONDS = 60 | |
| _search_cache: TTLCache = TTLCache(maxsize=1024, ttl=_SEARCH_TTL_SECONDS) | |
| _chat_cache: TTLCache = TTLCache(maxsize=512, ttl=_CHAT_TTL_SECONDS) | |
| _lock = Lock() | |
| _search_hits: int = 0 | |
| _search_misses: int = 0 | |
| _chat_hits: int = 0 | |
| _chat_misses: int = 0 | |
| def cache_enabled() -> bool: | |
| return _CACHE_ENABLED | |
| def _make_search_key( | |
| namespace: str, | |
| query: str, | |
| top_k: int, | |
| filters: Optional[Dict[str, Any]], | |
| ) -> Hashable: | |
| filters_json = ( | |
| json.dumps(filters, sort_keys=True, separators=(",", ":")) | |
| if filters is not None | |
| else "" | |
| ) | |
| return (namespace, query, int(top_k), filters_json) | |
| def _make_chat_key( | |
| namespace: str, | |
| query: str, | |
| top_k: int, | |
| min_score: float, | |
| use_web_fallback: bool, | |
| ) -> Hashable: | |
| return (namespace, query, int(top_k), float(min_score), bool(use_web_fallback)) | |
| def get_search_cached( | |
| namespace: str, | |
| query: str, | |
| top_k: int, | |
| filters: Optional[Dict[str, Any]], | |
| ) -> Optional[Any]: | |
| """Return cached search results or None.""" | |
| global _search_hits, _search_misses | |
| if not _CACHE_ENABLED: | |
| return None | |
| key = _make_search_key(namespace, query, top_k, filters) | |
| with _lock: | |
| if key in _search_cache: | |
| _search_hits += 1 | |
| logger.info( | |
| "Search cache hit namespace='%s' query='%s' top_k=%d", | |
| namespace, | |
| query, | |
| top_k, | |
| ) | |
| return _search_cache[key] | |
| _search_misses += 1 | |
| logger.info( | |
| "Search cache miss namespace='%s' query='%s' top_k=%d", | |
| namespace, | |
| query, | |
| top_k, | |
| ) | |
| return None | |
| def set_search_cached( | |
| namespace: str, | |
| query: str, | |
| top_k: int, | |
| filters: Optional[Dict[str, Any]], | |
| value: Any, | |
| ) -> None: | |
| if not _CACHE_ENABLED: | |
| return | |
| key = _make_search_key(namespace, query, top_k, filters) | |
| with _lock: | |
| _search_cache[key] = value | |
| def get_chat_cached( | |
| namespace: str, | |
| query: str, | |
| top_k: int, | |
| min_score: float, | |
| use_web_fallback: bool, | |
| ) -> Optional[Any]: | |
| """Return cached chat response or None. | |
| Only used when chat_history is empty. | |
| """ | |
| global _chat_hits, _chat_misses | |
| if not _CACHE_ENABLED: | |
| return None | |
| key = _make_chat_key(namespace, query, top_k, min_score, use_web_fallback) | |
| with _lock: | |
| if key in _chat_cache: | |
| _chat_hits += 1 | |
| logger.info( | |
| "Chat cache hit namespace='%s' query='%s' top_k=%d", | |
| namespace, | |
| query, | |
| top_k, | |
| ) | |
| return _chat_cache[key] | |
| _chat_misses += 1 | |
| logger.info( | |
| "Chat cache miss namespace='%s' query='%s' top_k=%d", | |
| namespace, | |
| query, | |
| top_k, | |
| ) | |
| return None | |
| def set_chat_cached( | |
| namespace: str, | |
| query: str, | |
| top_k: int, | |
| min_score: float, | |
| use_web_fallback: bool, | |
| value: Any, | |
| ) -> None: | |
| if not _CACHE_ENABLED: | |
| return | |
| key = _make_chat_key(namespace, query, top_k, min_score, use_web_fallback) | |
| with _lock: | |
| _chat_cache[key] = value | |
| def get_cache_stats() -> Dict[str, int]: | |
| """Return a snapshot of cache hit/miss counters.""" | |
| return { | |
| "search_hits": _search_hits, | |
| "search_misses": _search_misses, | |
| "chat_hits": _chat_hits, | |
| "chat_misses": _chat_misses, | |
| } |