feat: RAG CPAM dédié avec requêtes multi-ciblées + prompt 3 axes
- Nouvelle search_similar_cpam() : priorité Guide Méthodo, seuil 0.40, déduplication par code CIM-10, fetch élargi top_k*3 - _search_rag_for_control() refactoré : 2-3 requêtes ciblées (codes contestés, argument CPAM, contexte clinique) au lieu d'1 fourre-tout - Fusion dédupliquée par (document, code, page), top 10 résultats - Prompt CPAM enrichi : 3 axes (médical, asymétrie, réglementaire), section asymétrie d'information, format réponse structuré - 9 nouveaux tests unitaires pour la logique RAG multi-requêtes Élimine les sources CIM-10 hors-sujet (F45, F98.1, F50.5 sur pancréatite) au profit de résultats Guide Méthodo et référentiels pertinents. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,8 @@ _embed_model = None
|
||||
|
||||
# Score minimum de similarité FAISS pour retenir un résultat
|
||||
_MIN_SCORE = 0.3
|
||||
# Seuil rehaussé pour le contexte CPAM (filtrage plus agressif du bruit)
|
||||
_MIN_SCORE_CPAM = 0.40
|
||||
|
||||
|
||||
def _get_embed_model():
|
||||
@@ -149,6 +151,73 @@ def search_similar_ccam(query: str, top_k: int = 8) -> list[dict]:
|
||||
return final
|
||||
|
||||
|
||||
def search_similar_cpam(query: str, top_k: int = 8) -> list[dict]:
|
||||
"""Recherche RAG spécifique au contexte CPAM (contre-argumentation).
|
||||
|
||||
Différences avec search_similar() :
|
||||
- Priorité Guide Méthodologique (min 3 résultats) plutôt que CIM-10
|
||||
- Seuil de score rehaussé (0.40 vs 0.30) pour éliminer le bruit
|
||||
- Fetch élargi (top_k * 3) car filtrage plus agressif
|
||||
- Déduplication par code CIM-10 (garde le meilleur score par code)
|
||||
"""
|
||||
from .rag_index import get_index
|
||||
import numpy as np
|
||||
|
||||
result = get_index()
|
||||
if result is None:
|
||||
logger.warning("Index FAISS non disponible")
|
||||
return []
|
||||
|
||||
faiss_index, metadata = result
|
||||
|
||||
model = _get_embed_model()
|
||||
query_vec = model.encode([query], normalize_embeddings=True)
|
||||
query_vec = np.array(query_vec, dtype=np.float32)
|
||||
|
||||
# Fetch élargi pour compenser le filtrage agressif
|
||||
fetch_k = min(top_k * 3, faiss_index.ntotal)
|
||||
scores, indices = faiss_index.search(query_vec, fetch_k)
|
||||
|
||||
raw_results = []
|
||||
for score, idx in zip(scores[0], indices[0]):
|
||||
if idx < 0:
|
||||
continue
|
||||
if float(score) < _MIN_SCORE_CPAM:
|
||||
continue
|
||||
meta = metadata[idx].copy()
|
||||
meta["score"] = float(score)
|
||||
raw_results.append(meta)
|
||||
|
||||
# Dédupliquer par code CIM-10 (garder meilleur score par code)
|
||||
seen_codes: dict[str, dict] = {}
|
||||
deduped = []
|
||||
for r in raw_results:
|
||||
code = r.get("code")
|
||||
if code:
|
||||
if code in seen_codes:
|
||||
if r["score"] > seen_codes[code]["score"]:
|
||||
seen_codes[code] = r
|
||||
else:
|
||||
seen_codes[code] = r
|
||||
else:
|
||||
deduped.append(r) # pas de code → garder (guide_methodo, etc.)
|
||||
deduped.extend(seen_codes.values())
|
||||
deduped.sort(key=lambda r: r["score"], reverse=True)
|
||||
|
||||
# Prioriser le Guide Méthodologique (min 3 résultats)
|
||||
guide_results = [r for r in deduped if r["document"] == "guide_methodo"]
|
||||
other_results = [r for r in deduped if r["document"] != "guide_methodo"]
|
||||
|
||||
min_guide = min(3, len(guide_results))
|
||||
final = guide_results[:min_guide]
|
||||
remaining_slots = top_k - len(final)
|
||||
remaining = guide_results[min_guide:] + other_results
|
||||
remaining.sort(key=lambda r: r["score"], reverse=True)
|
||||
final.extend(remaining[:remaining_slots])
|
||||
|
||||
return final
|
||||
|
||||
|
||||
def _format_contexte(contexte: dict) -> str:
|
||||
"""Formate le contexte patient de manière structurée pour le prompt."""
|
||||
lines = []
|
||||
|
||||
Reference in New Issue
Block a user