""" backend/api/semantic_cache.py — S-CACHE-1: Cross-Node Semantic Cache Ottimizzazione ZeroGPU: evita ricalcoli identici tra nodi A-E. """ import hashlib import json import logging import time from typing import Optional from fastapi import APIRouter, Depends from .auth_guard import require_role, AuthRole from redis import redis_get, redis_set _logger = logging.getLogger("api.semantic_cache") router = APIRouter(prefix="/api/cache", tags=["cache"], dependencies=[Depends(require_role(AuthRole.MACHINE))]) # TTL: 24 ore per risposte semantiche _CACHE_TTL = 86400 def _gen_key(messages: list) -> str: """Genera una chiave hash basata sull'ultimo messaggio user (semplificato).""" try: # Prendiamo solo l'ultimo messaggio user per la cache semantica "veloce" user_msgs = [m for m in messages if m.get("role") == "user"] if not user_msgs: return "" last_content = user_msgs[-1].get("content", "").strip().lower() h = hashlib.sha256(last_content.encode()).hexdigest() return f"semcache:v1:{h[:16]}" except Exception: return "" @router.post("/lookup") async def cache_lookup(payload: dict): """Verifica se esiste una risposta cachata per il prompt corrente.""" messages = payload.get("messages", []) key = _gen_key(messages) if not key: return {"hit": False} cached = await redis_get(key) if cached: _logger.info(f"[cache] Hit semantica per chiave {key}") try: return {"hit": True, "content": json.loads(cached)} except: return {"hit": True, "content": cached} return {"hit": False} @router.post("/store") async def cache_store(payload: dict): """Salva una risposta nella cache semantica condivisa.""" messages = payload.get("messages", []) content = payload.get("content", "") if not content or len(content) < 50: # Non cachiamo risposte troppo brevi return {"ok": False} key = _gen_key(messages) if not key: return {"ok": False} # Salviamo come JSON per gestire metadati futuri ok = await redis_set(key, json.dumps(content), ttl=_CACHE_TTL) return {"ok": ok} async def get_cached_response(messages: list) -> Optional[str]: """Helper interno per AIClient.""" key = _gen_key(messages) if not key: return None cached = await redis_get(key) if cached: try: return json.loads(cached) except: return cached return None async def set_cached_response(messages: list, content: str): """Helper interno per AIClient.""" key = _gen_key(messages) if key and content and len(content) >= 50: await redis_set(key, json.dumps(content), ttl=_CACHE_TTL)