from __future__ import annotations import hashlib import json import os from collections import OrderedDict from dataclasses import dataclass from typing import Any import numpy as np def stable_hash(payload: Any) -> str: serialized = json.dumps(payload, sort_keys=True, default=str) return hashlib.sha256(serialized.encode("utf-8")).hexdigest() class LRUCache: def __init__(self, max_size: int = 256) -> None: self.max_size = max_size self._items: OrderedDict[str, Any] = OrderedDict() def get(self, key: str) -> Any | None: if key not in self._items: return None self._items.move_to_end(key) return self._items[key] def set(self, key: str, value: Any) -> None: self._items[key] = value self._items.move_to_end(key) if len(self._items) > self.max_size: self._items.popitem(last=False) @dataclass class SemanticCacheEntry: vector: list[float] value: Any class SemanticCache: def __init__(self, threshold: float = 0.92, max_size: int = 128) -> None: self.threshold = threshold self.max_size = max_size self._items: list[SemanticCacheEntry] = [] def get(self, vector: list[float]) -> Any | None: if not self._items: return None query = np.array(vector, dtype=np.float32) query_norm = np.linalg.norm(query) if query_norm == 0: return None best_score = -1.0 best_value = None for entry in self._items: candidate = np.array(entry.vector, dtype=np.float32) denom = query_norm * np.linalg.norm(candidate) score = float(np.dot(query, candidate) / denom) if denom else 0.0 if score > best_score: best_score = score best_value = entry.value return best_value if best_score >= self.threshold else None def set(self, vector: list[float], value: Any) -> None: self._items.append(SemanticCacheEntry(vector=vector, value=value)) if len(self._items) > self.max_size: self._items.pop(0) class RedisSemanticCache: def __init__(self, namespace: str, threshold: float = 0.92, max_size: int = 256) -> None: self.namespace = namespace self.threshold = threshold self.max_size = max_size self.enabled = False self._redis = None redis_url = os.getenv("REDIS_URL") if not redis_url: return try: import redis self._redis = redis.Redis.from_url(redis_url, decode_responses=True) self._redis.ping() self.enabled = True except Exception: self._redis = None self.enabled = False def get(self, vector: list[float]) -> Any | None: if not self.enabled or self._redis is None: return None query = np.array(vector, dtype=np.float32) query_norm = np.linalg.norm(query) if query_norm == 0: return None best_score = -1.0 best_value = None index_key = f"{self.namespace}:index" for item_key in self._redis.lrange(index_key, 0, -1): raw = self._redis.get(item_key) if not raw: continue item = json.loads(raw) candidate = np.array(item["vector"], dtype=np.float32) denom = query_norm * np.linalg.norm(candidate) score = float(np.dot(query, candidate) / denom) if denom else 0.0 if score > best_score: best_score = score best_value = item["value"] return best_value if best_score >= self.threshold else None def set(self, vector: list[float], value: Any) -> None: if not self.enabled or self._redis is None: return payload = {"vector": vector, "value": value} item_key = f"{self.namespace}:item:{stable_hash(payload)}" index_key = f"{self.namespace}:index" self._redis.set(item_key, json.dumps(payload, default=str)) self._redis.lpush(index_key, item_key) self._redis.ltrim(index_key, 0, self.max_size - 1)