File size: 5,234 Bytes
939c0c0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | """
Cache Service β Unified Caching & Semantic Cache
=================================================
Unified caching layer with two backends:
- Redis (local dev / Docker) β uses REDIS_URL env var
- In-memory dict (HF Spaces) β used when REDIS_URL is not set
Features:
- Key-value caching with TTL
- Rate-limit counter tracking
- Semantic Caching (avoids redundant LLM generation for semantically matching queries)
"""
from __future__ import annotations
import asyncio
import json
import time
from typing import Any, Dict, Optional, Tuple, List
import numpy as np
import structlog
logger = structlog.get_logger(__name__)
class InMemoryCache:
"""Simple thread-safe TTL in-memory cache used when Redis is unavailable."""
def __init__(self):
self._store: Dict[str, Tuple[str, float]] = {}
self._lock = asyncio.Lock()
async def get(self, key: str) -> Optional[str]:
async with self._lock:
entry = self._store.get(key)
if entry is None:
return None
value, expiry = entry
if expiry > 0 and time.time() > expiry:
del self._store[key]
return None
return value
async def set(self, key: str, value: str, ttl: int = 300) -> None:
expiry = time.time() + ttl if ttl > 0 else -1
async with self._lock:
self._store[key] = (value, expiry)
async def delete(self, key: str) -> None:
async with self._lock:
self._store.pop(key, None)
async def exists(self, key: str) -> bool:
return await self.get(key) is not None
async def incr(self, key: str, ttl: int = 60) -> int:
async with self._lock:
entry = self._store.get(key)
if entry is None or (entry[1] > 0 and time.time() > entry[1]):
count = 1
else:
try:
count = int(entry[0]) + 1
except ValueError:
count = 1
expiry = time.time() + ttl
self._store[key] = (str(count), expiry)
return count
class CacheService:
"""
Auto-selects Redis or InMemoryCache.
Provides semantic caching via query vector similarity.
"""
def __init__(self):
self._backend = None
self._semantic_store: List[Dict[str, Any]] = []
def _init_backend(self):
if self._backend is None:
import os
redis_url = os.environ.get("REDIS_URL")
if redis_url:
logger.info("Using Redis cache", url=redis_url)
from services.cache import RedisCache
self._backend = RedisCache(redis_url)
else:
logger.info("Using in-memory cache (no Redis configured)")
self._backend = InMemoryCache()
return self._backend
async def get(self, key: str) -> Optional[str]:
return await self._init_backend().get(key)
async def set(self, key: str, value: Any, ttl: int = 300) -> None:
if not isinstance(value, str):
value = json.dumps(value)
await self._init_backend().set(key, value, ttl)
async def get_json(self, key: str) -> Optional[Any]:
raw = await self.get(key)
if raw is None:
return None
try:
return json.loads(raw)
except json.JSONDecodeError:
return raw
async def delete(self, key: str) -> None:
await self._init_backend().delete(key)
async def exists(self, key: str) -> bool:
return await self._init_backend().exists(key)
async def incr(self, key: str, ttl: int = 60) -> int:
return await self._init_backend().incr(key, ttl)
# ββ Semantic Caching βββββββββββββββββββββββββββββββββββββββ
def get_semantic(self, query_vector: List[float], similarity_threshold: float = 0.93) -> Optional[Dict[str, Any]]:
"""Look up cached response for semantically matching query vector."""
if not self._semantic_store:
return None
q_vec = np.array(query_vector)
for entry in self._semantic_store:
cached_vec = np.array(entry["vector"])
similarity = float(np.dot(q_vec, cached_vec) / (np.linalg.norm(q_vec) * np.linalg.norm(cached_vec)))
if similarity >= similarity_threshold:
logger.info("Semantic cache hit!", similarity=round(similarity, 4))
return entry["response"]
return None
def set_semantic(self, query_vector: List[float], response_data: Dict[str, Any]) -> None:
"""Store query vector and response in semantic cache."""
self._semantic_store.append({
"vector": query_vector,
"response": response_data,
"timestamp": time.time(),
})
# Keep store size bounded
if len(self._semantic_store) > 200:
self._semantic_store.pop(0)
# ββ Singleton instance βββββββββββββββββββββββββββββββββββββββββ
cache_service = CacheService()
|