# COST: ZERO OpenAI tokens. # All embedding uses sentence-transformers 'all-MiniLM-L6-v2' running locally. # Similarity scoring is pure-Python cosine distance against diskcache entries. # No network call is made during cache lookup or save. from datetime import datetime, timezone from pathlib import Path import math import uuid import diskcache STORE_PATH = Path(__file__).parent / "query_store" _cache = diskcache.Cache(str(STORE_PATH)) # --- model singleton --- _model = None def _get_model(): global _model if _model is None: from sentence_transformers import SentenceTransformer _model = SentenceTransformer("all-MiniLM-L6-v2") return _model # --- math helpers --- def _cosine_similarity(a: list[float], b: list[float]) -> float: dot = sum(x * y for x, y in zip(a, b)) mag_a = math.sqrt(sum(x * x for x in a)) mag_b = math.sqrt(sum(y * y for y in b)) if mag_a == 0 or mag_b == 0: return 0.0 return dot / (mag_a * mag_b) # --- public API --- def embed_query_local(query: str) -> list[float]: return _get_model().encode(query, convert_to_numpy=True).tolist() def find_similar_cached(query: str, threshold: float = 0.92) -> dict | None: query_emb = embed_query_local(query) best_key = None best_score = -1.0 for key in _cache: entry = _cache[key] score = _cosine_similarity(query_emb, entry["query_embedding"]) if score > best_score: best_score = score best_key = key if best_key is None or best_score < threshold: return None entry = _cache[best_key] entry["hit_count"] += 1 _cache[best_key] = entry return entry def save_to_cache(query: str, answer: str, sources: list[str]) -> None: entry = { "query": query, "query_embedding": embed_query_local(query), "answer": answer, "sources": sources, "created_at": datetime.now(timezone.utc).isoformat(), "hit_count": 0, } _cache[str(uuid.uuid4())] = entry def get_cache_stats() -> dict: entries = [_cache[k] for k in _cache] total_hits = sum(e["hit_count"] for e in entries) most_asked = [ e["query"] for e in sorted(entries, key=lambda e: e["hit_count"], reverse=True)[:5] ] return { "total_entries": len(entries), "total_hits": total_hits, "most_asked": most_asked, }