feat: qualité DP Phase 2 — filtre OCR étendu, abréviations médicales, promotion DAS→DP

- Filtre OCR : regex étendu (opérateurs +-*/), artefacts temporels (années),
  seuil digits abaissé 0.50→0.48
- Dictionnaire 41 abréviations médicales françaises (BMR, BPCO, SDRA, OAP,
  IDM, SCA, AVC, ACFA, SIDA, TDAH, etc.) avec expand_medical_abbreviations()
  appelé sur diagnostics Trackare et DAS LLM
- Promotion DAS→DP : si aucun DP extrait, le meilleur DAS (scoring
  pertinence/confiance/spécificité) est promu avec traçabilité RULE-DAS-TO-DP
- 95 nouveaux tests (OCR, abréviations, promotion, scoring, non-régression)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dom
2026-02-20 08:37:10 +01:00
parent 6c036ed7f1
commit 1b680e9592
6 changed files with 360 additions and 5 deletions

View File

@@ -312,6 +312,35 @@ def _iron_evidence_blob(dossier: DossierMedical, diag: Diagnostic) -> str:
return _norm("\n".join(parts))
def _das_promotion_score(das: Diagnostic) -> tuple[int, int, int]:
"""Score de pertinence pour la promotion DAS→DP.
Retourne (pertinence_clinique, confiance, spécificité) :
- Pertinence : pathologie (2) > symptôme R (1) > Z-code (0)
- Confiance : high (3) > medium (2) > low (1)
- Spécificité : longueur du code (sans point) — plus long = plus spécifique
"""
code = das.cim10_final or ""
letter = code[0] if code else ""
# Pertinence clinique
if letter == "Z":
pertinence = 0
elif letter == "R":
pertinence = 1
else:
pertinence = 2
# Confiance
conf = (das.cim10_confidence or "").lower()
confiance = {"high": 3, "medium": 2, "low": 1}.get(conf, 1)
# Spécificité (longueur du code)
specificite = len(code.replace(".", ""))
return (pertinence, confiance, specificite)
def apply_decisions(dossier: DossierMedical) -> None:
"""Applique des décisions finales sur DP/DAS.
@@ -579,6 +608,40 @@ def apply_decisions(dossier: DossierMedical) -> None:
applied_rules=["RULE-E87.6-K-NORMAL"],
)
# --- Règle: promotion DAS→DP quand aucun DP n'a été extrait ---
if rule_enabled("RULE-DAS-TO-DP"):
if dossier.diagnostic_principal is None and dossier.diagnostics_associes:
candidates = [
das for das in dossier.diagnostics_associes
if das.cim10_final
and das.status not in ("ruled_out", "needs_info")
]
if candidates:
best = max(candidates, key=_das_promotion_score)
dossier.diagnostic_principal = Diagnostic(
texte=best.texte,
cim10_suggestion=best.cim10_suggestion,
cim10_confidence=best.cim10_confidence,
cim10_final=best.cim10_final,
justification=best.justification,
raisonnement=best.raisonnement,
source=best.source,
source_page=best.source_page,
source_excerpt=best.source_excerpt,
preuves_cliniques=best.preuves_cliniques,
sources_rag=best.sources_rag,
cim10_decision=CodeDecision(
action="PROMOTE_DP",
final_code=best.cim10_final,
applied_rules=["RULE-DAS-TO-DP"],
reason=f"DAS promu en DP (score {_das_promotion_score(best)})",
),
)
dossier.diagnostics_associes.remove(best)
logger.warning(
"PROMOTE_DP: DAS %s (%s) promu en DP — aucun DP extrait",
best.cim10_final, best.texte,
)
def decision_summaries(dossier: DossierMedical) -> list[str]:
@@ -612,6 +675,8 @@ def decision_summaries(dossier: DossierMedical) -> list[str]:
if dec.needs_info:
for q in dec.needs_info:
lines.append(f"DECISION: besoin_info: {q}")
elif dec.action == "PROMOTE_DP":
lines.append(f"DECISION: {where} {dec.final_code} promu en DP ({', '.join(dec.applied_rules)})")
if dossier.diagnostic_principal:
_summ("diagnostic_principal", dossier.diagnostic_principal)