from __future__ import annotations import hashlib, json, time, math from sqlalchemy import select, delete as sa_delete from app.models.exact_cache import ExactCache from app.models.semantic_cache import SemanticCache from app.services.storage import async_session_maker def compute_embedding(text: str) -> list[float]: """256-dim n-gram embedding using char-level n-grams (1-4).""" dim = 256 vec = [0.0] * dim text_lower = text.lower() for n in range(1, 5): for i in range(len(text_lower) - n + 1): gram = text_lower[i:i + n] idx = int(hashlib.md5(gram.encode()).hexdigest()[:8], 16) % dim vec[idx] += 1.0 norm = math.sqrt(sum(v * v for v in vec)) if norm > 0: vec = [v / norm for v in vec] return vec def cosine_similarity(a: list[float], b: list[float]) -> float: dot = sum(ai * bi for ai, bi in zip(a, b)) na = math.sqrt(sum(ai * ai for ai in a)) nb = math.sqrt(sum(bi * bi for bi in b)) if na == 0 or nb == 0: return 0.0 return dot / (na * nb) def make_cache_key(model: str, messages: list, **kwargs) -> str: raw = json.dumps({"model": model, "messages": messages, **kwargs}, sort_keys=True) return hashlib.sha256(raw.encode()).hexdigest() async def get_exact(session, key: str) -> dict | None: result = await session.execute(select(ExactCache).where(ExactCache.cache_key == key)) row = result.scalar_one_or_none() if not row: return None if time.time() >= row.expires_at: await session.delete(row) await session.commit() return None row.hit_count += 1 row.last_hit = time.time() await session.commit() return {"response": row.response, "hit_count": row.hit_count, "created_at": row.created_at} async def store_exact(session, key: str, response: str, ttl: int = 3600) -> None: now = time.time() existing = await session.execute(select(ExactCache).where(ExactCache.cache_key == key)) row = existing.scalar_one_or_none() if row: row.response = response row.expires_at = now + ttl row.created_at = now else: session.add(ExactCache( cache_key=key, response=response, created_at=now, expires_at=now + ttl, )) await session.commit() async def get_semantic(session, model: str, query: str, threshold: float = 0.95) -> dict | None: query_emb = compute_embedding(query) now = time.time() result = await session.execute( select(SemanticCache).where( SemanticCache.model == model, SemanticCache.expires_at > now, ) ) for row in result.scalars().all(): cached_emb = json.loads(row.embedding) sim = cosine_similarity(query_emb, cached_emb) if sim >= threshold: row.hit_count += 1 await session.commit() return {"response": row.response, "similarity": sim, "hit_count": row.hit_count} return None async def store_semantic(session, model: str, query: str, response: str, ttl: int = 3600) -> None: now = time.time() emb = compute_embedding(query) key = hashlib.sha256(json.dumps({"model": model, "query": query}, sort_keys=True).encode()).hexdigest() session.add(SemanticCache( cache_key=key, model=model, query_text=query, response=response, embedding=json.dumps(emb), created_at=now, expires_at=now + ttl, )) await session.commit() async def get_stats(session) -> dict: exact_count = await session.execute(select(ExactCache.cache_key)) exact_rows = exact_count.scalars().all() exact_active = 0 now = time.time() for row in exact_rows: r = await session.execute(select(ExactCache).where(ExactCache.cache_key == row)) obj = r.scalar_one_or_none() if obj and obj.expires_at > now: exact_active += 1 sem_count = await session.execute(select(SemanticCache.cache_key)) sem_rows = sem_count.scalars().all() sem_active = 0 for row in sem_rows: r = await session.execute(select(SemanticCache).where(SemanticCache.cache_key == row)) obj = r.scalar_one_or_none() if obj and obj.expires_at > now: sem_active += 1 return { "exact_total": len(exact_rows), "exact_active": exact_active, "semantic_total": len(sem_rows), "semantic_active": sem_active, } async def clear_exact(session) -> None: await session.execute(sa_delete(ExactCache)) await session.commit() async def clear_semantic(session) -> None: await session.execute(sa_delete(SemanticCache)) await session.commit() async def clear_all(session) -> None: await clear_exact(session) await clear_semantic(session) async def evict_expired(session) -> None: now = time.time() await session.execute(sa_delete(ExactCache).where(ExactCache.expires_at <= now)) await session.execute(sa_delete(SemanticCache).where(SemanticCache.expires_at <= now)) await session.commit()