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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user