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:
@@ -191,7 +191,12 @@ def _build_tagged_context(dossier: DossierMedical) -> tuple[str, dict[str, str]]
|
||||
if not lines:
|
||||
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
|
||||
|
||||
|
||||
@@ -839,6 +844,10 @@ def _build_cpam_prompt(
|
||||
+ "\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(
|
||||
dossier_str=dossier_str,
|
||||
asymetrie_str=asymetrie_str,
|
||||
@@ -853,5 +862,6 @@ def _build_cpam_prompt(
|
||||
extraction_str=extraction_str,
|
||||
bio_confrontation_str=bio_confrontation,
|
||||
numero_ogc=controle.numero_ogc,
|
||||
tags_disponibles_str=tags_disponibles_str,
|
||||
)
|
||||
return prompt, tag_map
|
||||
|
||||
@@ -32,6 +32,48 @@ def _fuzzy_match_ref(ref: str, tag_map: dict[str, str]) -> str | 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]:
|
||||
"""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)
|
||||
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 =====
|
||||
# Remplacer les valeurs bio hallucinées dans les strings (conclusion, rappel, etc.)
|
||||
text_fields = [
|
||||
|
||||
@@ -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
|
||||
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)
|
||||
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 :
|
||||
{{
|
||||
@@ -346,7 +347,7 @@ Réponds UNIQUEMENT avec un objet JSON :
|
||||
{{
|
||||
"numero": 1,
|
||||
"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": [
|
||||
{{"ref": "[BIO-1]", "fait": "Créatinine = 280 µmol/L [norme 50-120]", "signification": "IRA confirmée"}}
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user