Spaces:
Running
Running
| """ | |
| 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 "" | |
| 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} | |
| 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) | |