FFGEN-Demo / cache_manager.py
Matis Codjia
Fix: analystics
c6390d0
"""
Cache Manager - Gère Hit/Miss et distillation locale
"""
import numpy as np
from typing import Dict, List, Any
import uuid
from datetime import datetime
from config import DISTANCE_THRESHOLD, TOP_K_RESULTS, CONFIDENCE_THRESHOLD_WARNING
class CacheManager:
def __init__(self, chroma_collection, encoder_fn, threshold=None):
"""
Args:
chroma_collection: Collection ChromaDB
encoder_fn: Fonction pour encoder du texte en embedding
threshold: Custom similarity threshold
"""
self.collection = chroma_collection
self.encoder_fn = encoder_fn
self.threshold = threshold if threshold is not None else DISTANCE_THRESHOLD
def calculate_confidence(self, distances: List[float]) -> float:
"""Convertit la distance Chroma (Cosine) en score de confiance [0, 1]."""
if not distances:
return 0.0
# Avec hnsw:space="cosine", distance = 1 - similarity.
avg_distance = np.mean(distances)
return max(0.0, 1.0 - avg_distance)
def query_cache(self, code: str, context: Dict[str, Any]) -> Dict[str, Any]:
"""
Recherche dans le cache (Pipeline Hybride: Exact Match -> Vector Search -> Code Comparison)
"""
# --- NIVEAU 1 : CHECK RAPIDE (String Exact Match) ---
try:
if len(code) < 5000:
exact_matches = self.collection.get(where={"code": code}, limit=1)
if exact_matches and len(exact_matches['ids']) > 0:
return {
"status": "perfect_match",
"results": [{
"feedback": exact_matches['documents'][0],
"code": code,
"distance": 0.0,
"rank": 1,
"metadata": exact_matches['metadatas'][0]
}],
"confidence": 1.0,
"needs_warning": False,
"closest_distance": 0.0
}
except Exception as e:
print(f"Warning exact match check: {e}")
# --- NIVEAU 2 : RETRIEVAL (Vectorielle) ---
query_embedding = self.encoder_fn(code)
# On récupère les candidats (basé sur la proximité Code Input -> Feedback Embedding)
query_results = self.collection.query(
query_embeddings=[query_embedding],
n_results=TOP_K_RESULTS
)
distances = query_results['distances'][0] if query_results['distances'] else []
documents = query_results['documents'][0] if query_results['documents'] else []
metadatas = query_results['metadatas'][0] if query_results['metadatas'] else []
# --- NIVEAU 3 : ANALYSE FINE (Code vs Code) ---
is_code_hit = False
code_distance = 1.0 # Pire cas par défaut
# On vérifie si le code du meilleur candidat est sémantiquement proche du code utilisateur
if metadatas and metadatas[0].get('code'):
ref_code = metadatas[0].get('code')
if ref_code and ref_code != 'N/A':
# On encode le code de référence pour comparer avec le code d'entrée
ref_code_embedding = self.encoder_fn(ref_code)
# Distance Cosine entre les deux codes
# Note: np.dot sur vecteurs normalisés = Cosine Similarity. Distance = 1 - Sim.
similarity = float(np.dot(query_embedding, ref_code_embedding))
code_distance = max(0.0, 1.0 - similarity)
# Seuil très strict pour dire "C'est le même code" (mais écrit différemment)
if code_distance < 0.1: # Correspond à > 90% de similarité
is_code_hit = True
# --- DÉCISION FINALE ---
is_hit = False
hit_type = "miss"
# Priorité 1 : Code quasi-identique vectoriellement
if is_code_hit:
is_hit = True
hit_type = "code_hit"
# Priorité 2 : Feedback pertinent (Standard RAG) selon le slider
elif distances and distances[0] < self.threshold:
is_hit = True
hit_type = "feedback_hit"
# Formatage des résultats pour l'affichage
formatted_results = []
for i, (feedback, metadata, dist) in enumerate(zip(documents, metadatas, distances)):
formatted_results.append({
"rank": i + 1,
"feedback": feedback,
"code": metadata.get('code', 'N/A'),
"distance": round(dist, 4),
"metadata": metadata
})
if is_hit:
confidence = self.calculate_confidence(distances)
# Boost de confiance si c'est un code hit
if hit_type == "code_hit":
confidence = max(confidence, 0.95)
return {
"status": hit_type,
"results": formatted_results,
"confidence": round(confidence, 3),
"needs_warning": False if hit_type == "code_hit" else (confidence < CONFIDENCE_THRESHOLD_WARNING),
"closest_distance": round(distances[0], 4)
}
else:
return {
"status": "miss",
"results": formatted_results,
"confidence": 0.0,
"needs_warning": False,
"closest_distance": round(distances[0], 4) if distances else 1.0
}
def add_to_cache(self, code: str, feedback: str, metadata: Dict[str, Any], embedding: List[float]) -> bool:
"""
Ajoute au cache local pour la session courante (Active Learning).
"""
try:
doc_id = f"learned_{uuid.uuid4().hex[:8]}"
safe_metadata = {
"code": code[:10000],
"timestamp": datetime.now().isoformat(),
"source": "active_learning",
"theme": str(metadata.get("theme", "")),
"difficulty": str(metadata.get("difficulty", ""))
}
self.collection.add(
embeddings=[embedding],
documents=[feedback],
metadatas=[safe_metadata],
ids=[doc_id]
)
print(f"✅ Learned new feedback: {doc_id}")
return True
except Exception as e:
print(f"❌ Error adding to cache: {e}")
return False