feat: grounding CPAM — tags DP/DAS/ANT/COMPL + fuzzy matching CIM-10 + prompt renforcé
Cause racine du Tier C : le LLM inventait des tags ([C83.3], [Antécédents]) car _build_tagged_context() ne taguait que bio/img/trt/actes. Le DP, les DAS, antécédents et complications n'avaient aucun tag citable. - cpam_context: 4 nouveaux types de tags [DP], [DAS-N], [ANT-N], [COMPL-N] - cpam_validation: fuzzy matching — résout les refs CIM-10 nues vers le tag contenant ce code - templates: liste explicite des tags valides, interdiction d'inventer des tags - tests: 18 nouveaux tests (tags, fuzzy match, grounding DAS/DP) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -100,7 +100,8 @@ def _get_cim10_definitions(
|
||||
def _build_tagged_context(dossier: DossierMedical) -> tuple[str, dict[str, str]]:
|
||||
"""Construit un contexte clinique avec des tags de référence pour le grounding.
|
||||
|
||||
Chaque élément clinique reçoit un tag unique ([BIO-1], [IMG-1], [TRT-1], [ACTE-1])
|
||||
Chaque élément clinique reçoit un tag unique :
|
||||
[BIO-N], [IMG-N], [TRT-N], [ACTE-N], [DP], [DAS-N], [ANT-N], [COMPL-N]
|
||||
que le LLM doit citer dans ses preuves pour garantir la traçabilité.
|
||||
|
||||
Returns:
|
||||
@@ -156,6 +157,37 @@ def _build_tagged_context(dossier: DossierMedical) -> tuple[str, dict[str, str]]
|
||||
tag_map[tag] = content
|
||||
lines.append(f" [{tag}] {content}")
|
||||
|
||||
# Diagnostic principal
|
||||
if dossier.diagnostic_principal:
|
||||
dp = dossier.diagnostic_principal
|
||||
tag = "DP"
|
||||
code = f" ({dp.cim10_suggestion})" if dp.cim10_suggestion else ""
|
||||
content = f"{dp.texte}{code}"
|
||||
tag_map[tag] = content
|
||||
lines.append(f" [{tag}] {content}")
|
||||
|
||||
# Diagnostics associés
|
||||
for i, das in enumerate(dossier.diagnostics_associes, 1):
|
||||
tag = f"DAS-{i}"
|
||||
code = f" ({das.cim10_suggestion})" if das.cim10_suggestion else ""
|
||||
content = f"{das.texte}{code}"
|
||||
tag_map[tag] = content
|
||||
lines.append(f" [{tag}] {content}")
|
||||
|
||||
# Antécédents (top 10)
|
||||
for i, ant in enumerate(dossier.antecedents[:10], 1):
|
||||
tag = f"ANT-{i}"
|
||||
content = ant.texte
|
||||
tag_map[tag] = content
|
||||
lines.append(f" [{tag}] {content}")
|
||||
|
||||
# Complications
|
||||
for i, compl in enumerate(dossier.complications, 1):
|
||||
tag = f"COMPL-{i}"
|
||||
content = compl.texte
|
||||
tag_map[tag] = content
|
||||
lines.append(f" [{tag}] {content}")
|
||||
|
||||
if not lines:
|
||||
return "", tag_map
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ from .cpam_context import ( # noqa: F401
|
||||
_build_bio_summary,
|
||||
_check_das_bio_coherence,
|
||||
)
|
||||
from .cpam_validation import _CIM10_CODE_RE, _validate_adversarial as _validate_adversarial, _assess_quality_tier as _assess_quality_tier # noqa: F401
|
||||
from .cpam_validation import _CIM10_CODE_RE, _validate_adversarial as _validate_adversarial, _assess_quality_tier as _assess_quality_tier, _fuzzy_match_ref as _fuzzy_match_ref # noqa: F401
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -14,9 +14,29 @@ from ..prompts import CPAM_ADVERSARIAL
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _fuzzy_match_ref(ref: str, tag_map: dict[str, str]) -> str | None:
|
||||
"""Tente de résoudre une ref inventée vers un tag réel.
|
||||
|
||||
Stratégie : si la ref ressemble à un code CIM-10 (ex: "C83.3"),
|
||||
chercher dans tag_map un tag dont le contenu contient ce code.
|
||||
|
||||
Returns:
|
||||
Le tag réel trouvé, ou None si aucun match.
|
||||
"""
|
||||
ref_upper = ref.strip().upper()
|
||||
# Match par code CIM-10 dans le contenu des tags
|
||||
if re.match(r"^[A-Z]\d{2}\.?\d{0,2}$", ref_upper):
|
||||
for tag, content in tag_map.items():
|
||||
if ref_upper in content.upper() or ref in content:
|
||||
return tag
|
||||
return None
|
||||
|
||||
|
||||
def _validate_grounding(response_data: dict, tag_map: dict[str, str]) -> list[str]:
|
||||
"""Vérifie que les références dans preuves_dossier correspondent à des tags existants.
|
||||
|
||||
Applique un fuzzy matching par code CIM-10 avant de flaguer un warning.
|
||||
|
||||
Returns:
|
||||
Liste de warnings pour les références inventées.
|
||||
"""
|
||||
@@ -35,6 +55,10 @@ def _validate_grounding(response_data: dict, tag_map: dict[str, str]) -> list[st
|
||||
if not ref:
|
||||
continue
|
||||
if ref not in tag_map:
|
||||
matched_tag = _fuzzy_match_ref(ref, tag_map)
|
||||
if matched_tag:
|
||||
logger.info("Grounding : ref [%s] résolue vers [%s]", ref, matched_tag)
|
||||
continue # pas de warning
|
||||
valeur = p.get("valeur", "?")
|
||||
warnings.append(f"Preuve [{ref}] non traçable (« {valeur} »)")
|
||||
logger.warning("Grounding : preuve [%s] introuvable dans les tags du dossier", ref)
|
||||
|
||||
@@ -267,7 +267,9 @@ CONTEXTE CLINIQUE :
|
||||
|
||||
AXE MÉDICAL :
|
||||
- Analyse le bien-fondé médical du codage de l'établissement
|
||||
- CITE les éléments cliniques EXACTS du dossier en utilisant les tags [XX-N] fournis (ex: [BIO-1] CRP 180 mg/L)
|
||||
- CITE les éléments cliniques EXACTS du dossier en utilisant UNIQUEMENT les tags [XX-N] fournis dans la section ÉLÉMENTS CLINIQUES RÉFÉRENCÉS
|
||||
- Tags valides : [DP], [DAS-N], [BIO-N], [IMG-N], [TRT-N], [ACTE-N], [ANT-N], [COMPL-N]
|
||||
- N'invente JAMAIS un tag qui ne figure pas dans la liste ci-dessus. Si un élément n'a pas de tag, décris-le en texte libre SANS crochets.
|
||||
- Confronte l'argumentation CPAM aux sources CIM-10 et Guide Méthodologique fournies
|
||||
- Ne mentionne AUCUN élément qui ne figure pas dans les éléments référencés ci-dessus
|
||||
|
||||
@@ -295,7 +297,7 @@ Réponds UNIQUEMENT avec un objet JSON au format suivant :
|
||||
"points_accord": "Points CONCRETS où la CPAM a raison ou partiellement raison (JAMAIS 'Aucun' — il y a toujours au moins un point légitime à reconnaître)",
|
||||
"contre_arguments_medicaux": "Argumentation médicale en faveur du codage, en expliquant pourquoi les points d'accord ne suffisent pas à invalider le codage",
|
||||
"preuves_dossier": [
|
||||
{{"ref": "BIO-1", "element": "biologie|imagerie|traitement|acte|clinique", "valeur": "valeur exacte du dossier", "signification": "explication clinique"}}
|
||||
{{"ref": "BIO-1 ou DAS-3 ou DP (UNIQUEMENT un tag existant de la section ÉLÉMENTS CLINIQUES RÉFÉRENCÉS)", "element": "biologie|imagerie|traitement|acte|diagnostic|antécédent|complication", "valeur": "valeur exacte du dossier", "signification": "explication clinique"}}
|
||||
],
|
||||
"contre_arguments_asymetrie": "Éléments cliniques que la CPAM n'avait pas et qui justifient le codage",
|
||||
"contre_arguments_reglementaires": "Erreurs d'interprétation réglementaire de la CPAM, avec citations verbatim des sources",
|
||||
|
||||
Reference in New Issue
Block a user