fix: injecter les tags réels du dossier dans le prompt CPAM pour éliminer les tags génériques [TYPE-N]

Le LLM générait des tags génériques [BIO-N], [TRT-N] au lieu des vrais tags du dossier,
causant des warnings "preuve non traçable". Corrigé en 3 points :
- cpam_context: liste exhaustive des tags disponibles injectée dans le prompt
- templates: remplacement des patterns génériques par {tags_disponibles_str}
- cpam_validation: guardian step 4b résout les tags génériques résiduels

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dom
2026-03-04 23:14:40 +01:00
parent 798cee463f
commit 542797a124
3 changed files with 78 additions and 3 deletions

View File

@@ -191,7 +191,12 @@ def _build_tagged_context(dossier: DossierMedical) -> tuple[str, dict[str, str]]
if not lines: if not lines:
return "", tag_map return "", tag_map
text = "ÉLÉMENTS CLINIQUES RÉFÉRENCÉS (cite le tag [XX-N] dans tes preuves) :\n" + "\n".join(lines) available = ", ".join(f"[{t}]" for t in sorted(tag_map.keys()))
text = (
"ÉLÉMENTS CLINIQUES RÉFÉRENCÉS :\n"
+ "\n".join(lines)
+ f"\n\nTAGS DISPONIBLES pour ce dossier (liste EXHAUSTIVE, n'en invente aucun) : {available}"
)
return text, tag_map return text, tag_map
@@ -839,6 +844,10 @@ def _build_cpam_prompt(
+ "\n".join(ext_lines) + "\n".join(ext_lines)
) )
tags_disponibles_str = (
", ".join(f"[{t}]" for t in sorted(tag_map.keys()))
if tag_map else "(aucun)"
)
prompt = CPAM_ARGUMENTATION.format( prompt = CPAM_ARGUMENTATION.format(
dossier_str=dossier_str, dossier_str=dossier_str,
asymetrie_str=asymetrie_str, asymetrie_str=asymetrie_str,
@@ -853,5 +862,6 @@ def _build_cpam_prompt(
extraction_str=extraction_str, extraction_str=extraction_str,
bio_confrontation_str=bio_confrontation, bio_confrontation_str=bio_confrontation,
numero_ogc=controle.numero_ogc, numero_ogc=controle.numero_ogc,
tags_disponibles_str=tags_disponibles_str,
) )
return prompt, tag_map return prompt, tag_map

View File

@@ -32,6 +32,48 @@ def _fuzzy_match_ref(ref: str, tag_map: dict[str, str]) -> str | None:
return None return None
GENERIC_TAG_RE = re.compile(r"\[([A-Z]+)-N\]")
def _resolve_generic_tag(
prefix: str, fait: str, tag_map: dict[str, str]
) -> str | None:
"""Résout un tag générique [PREFIX-N] vers le vrai tag le plus proche.
Cherche dans *tag_map* les tags commençant par *prefix* dont le contenu
partage des mots-clés significatifs avec *fait*.
"""
fait_lower = fait.lower()
fait_words = set(fait_lower.split())
# Mots trop communs à ignorer
stop = {"de", "du", "le", "la", "les", "un", "une", "des", "et", "ou", "en", "à", "=", "mg", "l", "g", "ml"}
fait_words -= stop
best_tag: str | None = None
best_score = 0
for tag, content in tag_map.items():
if not tag.startswith(prefix + "-"):
continue
content_lower = content.lower()
content_words = set(content_lower.split()) - stop
# Score = nombre de mots en commun + bonus substring
score = len(fait_words & content_words)
if fait_lower in content_lower or content_lower in fait_lower:
score += 3
if score > best_score:
best_score = score
best_tag = tag
# Fallback : si un seul tag de ce prefix existe, le prendre
if best_tag is None:
candidates = [t for t in tag_map if t.startswith(prefix + "-")]
if len(candidates) == 1:
best_tag = candidates[0]
return best_tag
def _validate_grounding(response_data: dict, tag_map: dict[str, str]) -> list[str]: def _validate_grounding(response_data: dict, tag_map: dict[str, str]) -> list[str]:
"""Vérifie que les références dans preuves correspondent à des tags existants. """Vérifie que les références dans preuves correspondent à des tags existants.
@@ -856,6 +898,28 @@ def _guardian_deterministic(
report["preuves_invalid_tags"].append(tag) report["preuves_invalid_tags"].append(tag)
penalties += 0.5 penalties += 0.5
# ===== 4b. Corriger les tags génériques [TYPE-N] → vrai tag =====
if tag_map:
for moyen in result.get("moyens_defense", []):
if not isinstance(moyen, dict):
continue
for preuve in moyen.get("preuves", []):
if not isinstance(preuve, dict):
continue
ref = str(preuve.get("ref", ""))
m = GENERIC_TAG_RE.search(ref)
if m:
prefix = m.group(1) # ex: "BIO"
fait = str(preuve.get("fait", "")).lower()
best_tag = _resolve_generic_tag(prefix, fait, tag_map)
if best_tag:
preuve["ref"] = f"[{best_tag}]"
report["tags_corrected"] = report.get("tags_corrected", 0) + 1
logger.info(
"Guardian 4b: [%s-N] → [%s] (fait: %s)",
prefix, best_tag, fait[:60],
)
# ===== 5. Nettoyage des champs texte libre ===== # ===== 5. Nettoyage des champs texte libre =====
# Remplacer les valeurs bio hallucinées dans les strings (conclusion, rappel, etc.) # Remplacer les valeurs bio hallucinées dans les strings (conclusion, rappel, etc.)
text_fields = [ text_fields = [

View File

@@ -329,7 +329,8 @@ CONSIGNES DE RÉDACTION DES MOYENS
6. JAMAIS d'argument sans preuve traçable — si tu n'as pas la preuve, NE FAIS PAS l'argument 6. JAMAIS d'argument sans preuve traçable — si tu n'as pas la preuve, NE FAIS PAS l'argument
7. Ton ASSERTIF mais factuel — pas de formules creuses ("il convient de noter que...") 7. Ton ASSERTIF mais factuel — pas de formules creuses ("il convient de noter que...")
8. Si un point CPAM est légitime, le reconnaître CLAIREMENT (R4) 8. Si un point CPAM est légitime, le reconnaître CLAIREMENT (R4)
9. Tags valides UNIQUEMENT : [DP], [DAS-N], [BIO-N], [IMG-N], [TRT-N], [ACTE-N], [ANT-N], [COMPL-N] 9. Tags valides UNIQUEMENT ceux listés ci-dessus : {tags_disponibles_str}
Si un élément n'a pas de tag, décris le fait en clair SANS inventer de tag
Réponds UNIQUEMENT avec un objet JSON : Réponds UNIQUEMENT avec un objet JSON :
{{ {{
@@ -346,7 +347,7 @@ Réponds UNIQUEMENT avec un objet JSON :
{{ {{
"numero": 1, "numero": 1,
"titre": "Titre court du moyen (ex: Le DP N17.8 est justifié par la biologie)", "titre": "Titre court du moyen (ex: Le DP N17.8 est justifié par la biologie)",
"argument": "Développement avec preuves tagées [XX-N], valeurs bio avec seuils, sources réglementaires", "argument": "Développement avec preuves tagées (utiliser les tags listés ci-dessus), valeurs bio avec seuils, sources réglementaires",
"preuves": [ "preuves": [
{{"ref": "[BIO-1]", "fait": "Créatinine = 280 µmol/L [norme 50-120]", "signification": "IRA confirmée"}} {{"ref": "[BIO-1]", "fait": "Créatinine = 280 µmol/L [norme 50-120]", "signification": "IRA confirmée"}}
], ],