BrejBala's picture
Claude Sonnet 4.6
feat: deploy Tiers 2 & 3 — CRAG, faithfulness, streaming, Prometheus, eval-driven retrieval
6686f13
Raw History Blame Contribute Delete
4.11 kB
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__)
# get_settings() requires PINECONE_* env vars; wrap so the module can be
# imported in test environments without secrets. Defaults to enabled (safe —
# the in-process cache starts empty, so all lookups will miss).
try:
_CACHE_ENABLED: bool = get_settings().CACHE_ENABLED
except Exception:
_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,
}