feat: évaluation force probante dossier + seuils qualité relaxés pour dossiers faibles

Score 0-10 basé sur les preuves objectives (bio/img/trt/actes).
Dossier faible (score < 3) : prompt LLM adapté + seuil adversarial
abaissé (score 2-3 → Tier B au lieu de C). Les éléments contextuels
(âge, IMC, urgence) restent dans le prompt mais hors du scoring car
ils ne constituent pas des preuves opposables à un contrôleur CPAM.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dom
2026-02-23 09:19:43 +01:00
parent 1844d1be7e
commit d192af74ec
4 changed files with 266 additions and 3 deletions

View File

@@ -18,6 +18,7 @@ from src.config import (
Traitement,
)
from src.control.cpam_response import (
_assess_dossier_strength,
_build_bio_summary,
_build_correction_prompt,
_build_cpam_prompt,
@@ -2319,3 +2320,163 @@ class TestSanitizeUnauthorizedCodes:
# Puis valide → 0 warning
warnings = _validate_codes_in_response(parsed, dossier, controle)
assert len(warnings) == 0
class TestAssessDossierStrength:
"""Tests pour l'évaluation de la force probante du dossier."""
def test_empty_dossier_is_weak(self):
"""Dossier vide → score 0, is_weak=True."""
dossier = DossierMedical(source_file="test.pdf")
result = _assess_dossier_strength(dossier)
assert result["score"] == 0
assert result["is_weak"] is True
assert len(result["missing"]) > 0
def test_rich_dossier_not_weak(self):
"""Dossier complet → is_weak=False, score >= 3."""
dossier = _make_dossier_complet()
result = _assess_dossier_strength(dossier)
assert result["is_weak"] is False
assert result["score"] >= 3
def test_dp_only_dossier_is_weak(self):
"""Dossier avec DP seulement (pas de bio/img/trt/actes) → faible."""
dossier = DossierMedical(
source_file="test.pdf",
diagnostic_principal=Diagnostic(texte="DP test", cim10_suggestion="K81.0"),
)
result = _assess_dossier_strength(dossier)
assert result["is_weak"] is True
assert result["score"] == 0
def test_bio_only_few_values(self):
"""Dossier avec 1-2 bio → score faible mais contribue."""
dossier = DossierMedical(
source_file="test.pdf",
biologie_cle=[
BiologieCle(test="CRP", valeur="180 mg/L"),
],
)
result = _assess_dossier_strength(dossier)
assert result["score"] == 1 # 1 bio = 1 point
assert result["is_weak"] is True
def test_bio_many_values(self):
"""Dossier avec 4+ bio → max 4 points pour la bio."""
dossier = DossierMedical(
source_file="test.pdf",
biologie_cle=[
BiologieCle(test="CRP", valeur="180"),
BiologieCle(test="Créatinine", valeur="120"),
BiologieCle(test="Hémoglobine", valeur="12"),
BiologieCle(test="Plaquettes", valeur="200"),
BiologieCle(test="Leucocytes", valeur="10"),
],
)
result = _assess_dossier_strength(dossier)
assert result["score"] >= 4 # bio capped at 4
def test_missing_categories_reported(self):
"""Les catégories manquantes sont listées."""
dossier = DossierMedical(source_file="test.pdf")
result = _assess_dossier_strength(dossier)
assert "biologie" in " ".join(result["missing"]).lower()
assert "imagerie" in " ".join(result["missing"]).lower()
def test_actes_contribute(self):
"""Les actes CCAM contribuent au score (max 2)."""
dossier = DossierMedical(
source_file="test.pdf",
actes_ccam=[
ActeCCAM(texte="Cholécystectomie", code_ccam_suggestion="HMFC004"),
ActeCCAM(texte="Drainage biliaire", code_ccam_suggestion="HHFA001"),
ActeCCAM(texte="Exploration"),
],
)
result = _assess_dossier_strength(dossier)
assert result["score"] == 2 # actes capped at 2
class TestQualityTierWeakDossier:
"""Tests pour les seuils de qualité relaxés sur dossier faible."""
def test_score_3_normal_dossier_is_c(self):
"""Score adversarial 3 sur dossier normal → tier C (critique)."""
tier, review, warnings = _assess_quality_tier(
parsed={},
ref_warnings=[],
grounding_warnings=[],
code_warnings=[],
adversarial_result={"coherent": False, "erreurs": ["Bio faible"], "score_confiance": 3},
is_weak_dossier=False,
)
assert tier == "C"
assert review is True
assert any("[CRITIQUE]" in w for w in warnings)
def test_score_3_weak_dossier_is_b(self):
"""Score adversarial 3 sur dossier faible → tier B (mineur attendu)."""
tier, review, warnings = _assess_quality_tier(
parsed={},
ref_warnings=[],
grounding_warnings=[],
code_warnings=[],
adversarial_result={"coherent": False, "erreurs": ["Bio faible"], "score_confiance": 3},
is_weak_dossier=True,
)
assert tier == "B"
assert review is False
assert any("attendu" in w.lower() for w in warnings)
def test_score_2_weak_dossier_is_b(self):
"""Score adversarial 2 sur dossier faible → tier B."""
tier, review, warnings = _assess_quality_tier(
parsed={},
ref_warnings=[],
grounding_warnings=[],
code_warnings=[],
adversarial_result={"coherent": False, "erreurs": ["Données insuffisantes"], "score_confiance": 2},
is_weak_dossier=True,
)
assert tier == "B"
assert review is False
def test_score_1_weak_dossier_is_c(self):
"""Score adversarial 1 sur dossier faible → tier C (même relaxé)."""
tier, review, warnings = _assess_quality_tier(
parsed={},
ref_warnings=[],
grounding_warnings=[],
code_warnings=[],
adversarial_result={"coherent": False, "erreurs": ["Incohérent"], "score_confiance": 1},
is_weak_dossier=True,
)
assert tier == "C"
assert review is True
def test_code_warnings_override_weak(self):
"""Code hors périmètre → tier C même si dossier faible (critique non relaxable)."""
tier, review, warnings = _assess_quality_tier(
parsed={},
ref_warnings=[],
grounding_warnings=[],
code_warnings=["Code Z45.8 hors périmètre"],
adversarial_result={"coherent": True, "erreurs": [], "score_confiance": 5},
is_weak_dossier=True,
)
assert tier == "C"
assert review is True
def test_score_7_weak_dossier_is_a(self):
"""Score adversarial 7 sur dossier faible → tier A (pas de warnings)."""
tier, review, warnings = _assess_quality_tier(
parsed={},
ref_warnings=[],
grounding_warnings=[],
code_warnings=[],
adversarial_result={"coherent": True, "erreurs": [], "score_confiance": 7},
is_weak_dossier=True,
)
assert tier == "A"
assert review is False