- Réorganisation data/referentiels/ : pdfs/, dicts/, user/ (structure unifiée) - Fix badges "Source absente" sur page admin référentiels - Ré-indexation COCOA 2025 (555 → 1451 chunks, couverture 94%) - Fix VRAM OOM : embeddings forcés CPU via T2A_EMBED_CPU - Nouveaux modules : document_router, docx_extractor, image_extractor, ocr_engine - Module complétude (quality/completude.py + config YAML) - Template DIM (synthèse dimensionnelle) - Gunicorn config + systemd service t2a-viewer - Suppression t2a_install_rag_cleanup/ (copie obsolète) - Suppression scripts/ et scripts_t2a_v2/ (anciens benchmarks) - Suppression 81 fichiers _doc.txt de test - Cache Ollama : TTL configurable, corrections loader YAML - Dashboard : améliorations templates (base, index, detail, cpam, validation) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
365 lines
12 KiB
Python
365 lines
12 KiB
Python
"""Tests d'intégration : détection dénutrition HAS/FFN 2021 sur dossiers réalistes.
|
|
|
|
4 cas cliniques réalistes passés dans le pipeline complet extract_medical_info.
|
|
Vérifie l'interaction entre regex bio, détection HAS 2021, conflits, et sévérité CMA.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from src.config import DossierMedical
|
|
from src.medical.cim10_extractor import extract_medical_info
|
|
|
|
|
|
# ── Cas 1 : Personne âgée dénutrie (≥70 ans, seuils gériatriques) ───
|
|
|
|
|
|
class TestCas1PersonneAgeeDenutrie:
|
|
"""Mme D., 81 ans, hospitalisée pour pneumopathie.
|
|
|
|
IMC 20.5 → sous le seuil gériatrique < 22 → dénutrition modérée.
|
|
Albumine 27 g/L → < 30 → critère de sévérité → upgrade vers E43.
|
|
Attendu : E43 (dénutrition sévère) détecté par HAS 2021.
|
|
"""
|
|
|
|
@pytest.fixture
|
|
def dossier(self) -> DossierMedical:
|
|
parsed = {
|
|
"type": "crh",
|
|
"patient": {
|
|
"sexe": "F",
|
|
"date_naissance": "15/03/1943",
|
|
},
|
|
"sejour": {
|
|
"date_entree": "10/01/2025",
|
|
"date_sortie": "20/01/2025",
|
|
},
|
|
"diagnostics": [],
|
|
"signes_vitaux": {"imc": 20.5, "poids_kg": 48, "taille_cm": 153},
|
|
}
|
|
text = """\
|
|
Votre patiente née le 15/03/1943 a été hospitalisée du 10/01/2025 au 20/01/2025.
|
|
|
|
Antécédents :
|
|
- Hypertension artérielle
|
|
- Fibrillation auriculaire paroxystique
|
|
- Arthrose invalidante
|
|
|
|
Au total : Pneumopathie basale droite d'évolution favorable sous antibiothérapie.
|
|
|
|
Biologie d'entrée :
|
|
CRP = 145 mg/L
|
|
Albumine = 27 g/L
|
|
Créatinine = 95 µmol/L
|
|
Sodium = 138 mmol/L
|
|
Potassium = 4.1 mmol/L
|
|
Hémoglobine = 10.2 g/dL
|
|
|
|
IMC: 20.5
|
|
|
|
TTT de sortie :
|
|
Amoxicilline 1g matin et soir pendant 5 jours
|
|
Paracétamol si besoin
|
|
|
|
Devenir : retour à domicile."""
|
|
return extract_medical_info(parsed, text)
|
|
|
|
def test_age_81_ans(self, dossier):
|
|
assert dossier.sejour.age == 81
|
|
|
|
def test_imc_extrait(self, dossier):
|
|
assert dossier.sejour.imc == 20.5
|
|
|
|
def test_albumine_extraite(self, dossier):
|
|
"""L'albumine doit être extraite par le nouveau regex."""
|
|
alb = [b for b in dossier.biologie_cle if b.test == "Albumine"]
|
|
assert len(alb) >= 1
|
|
assert alb[0].valeur_num == 27.0
|
|
|
|
def test_denutrition_severe_E43(self, dossier):
|
|
"""IMC 20.5 (modéré ≥70 ans) + albumine 27 (<30) → E43 sévère."""
|
|
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
|
|
assert "E43" in codes, f"E43 attendu, trouvé : {codes}"
|
|
|
|
def test_source_has2021(self, dossier):
|
|
has_diags = [d for d in dossier.diagnostics_associes if d.source == "has2021"]
|
|
assert len(has_diags) == 1
|
|
assert has_diags[0].cim10_suggestion == "E43"
|
|
|
|
def test_alerte_has2021(self, dossier):
|
|
assert any("HAS 2021" in a for a in dossier.alertes_codage)
|
|
|
|
def test_pneumopathie_detectee(self, dossier):
|
|
"""Le DP/DAS pneumopathie ne doit pas être impacté."""
|
|
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
|
|
# Pneumopathie J18.9 ou DP
|
|
all_codes = codes.copy()
|
|
if dossier.diagnostic_principal and dossier.diagnostic_principal.cim10_suggestion:
|
|
all_codes.add(dossier.diagnostic_principal.cim10_suggestion)
|
|
assert "J18.9" in all_codes
|
|
|
|
def test_hta_detectee(self, dossier):
|
|
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
|
|
assert "I10" in codes
|
|
|
|
|
|
# ── Cas 2 : Patient obèse ET dénutri (conflit E66 + E43 = MEDIUM) ───
|
|
|
|
|
|
class TestCas2ObeseDenutri:
|
|
"""M. B., 58 ans, hospitalisé pour pancréatite aiguë sur obésité.
|
|
|
|
IMC 35.2 → obèse (E66.0). Dénutrition sévère mentionnée dans le texte (E43).
|
|
Le conflit E66/dénutrition doit être MEDIUM (pas HARD) selon HAS 2021.
|
|
La coexistence est cliniquement possible (sarcopénie de l'obèse).
|
|
|
|
DP fourni via Trackare (K85.9) pour éviter que NUKE-3 ne réorganise les codes.
|
|
"""
|
|
|
|
@pytest.fixture
|
|
def dossier(self) -> DossierMedical:
|
|
parsed = {
|
|
"type": "trackare",
|
|
"patient": {
|
|
"sexe": "M",
|
|
"date_naissance": "22/07/1967",
|
|
},
|
|
"sejour": {
|
|
"date_entree": "05/02/2025",
|
|
"date_sortie": "12/02/2025",
|
|
},
|
|
"diagnostics": [
|
|
{
|
|
"type": "Principal",
|
|
"statut": "actif",
|
|
"code_cim10": "K85.9",
|
|
"libelle": "Pancréatite aiguë",
|
|
}
|
|
],
|
|
"signes_vitaux": {"imc": 35.2, "poids_kg": 110, "taille_cm": 177},
|
|
}
|
|
text = """\
|
|
Pancréatite aiguë sur terrain d'obésité morbide.
|
|
Dénutrition sévère protéique avec sarcopénie documentée,
|
|
perte de poids de 15 kg en 3 mois.
|
|
|
|
Antécédents :
|
|
- Diabète type 2
|
|
- Tabagisme actif
|
|
|
|
Biologie :
|
|
CRP = 145 mg/L
|
|
Albumine = 25 g/L
|
|
Lipasémie = 1200 UI/L
|
|
HbA1c = 7.8 %
|
|
Hémoglobine = 13.5 g/dL
|
|
|
|
IMC: 35.2
|
|
|
|
TTT de sortie :
|
|
Metformine 1000mg matin et soir
|
|
Paracétamol si besoin
|
|
|
|
Devenir : retour à domicile."""
|
|
return extract_medical_info(parsed, text)
|
|
|
|
def test_dp_pancreatite(self, dossier):
|
|
"""DP = K85.9 (fourni par Trackare)."""
|
|
assert dossier.diagnostic_principal is not None
|
|
assert dossier.diagnostic_principal.cim10_suggestion == "K85.9"
|
|
|
|
def test_obesite_E66_detectee(self, dossier):
|
|
"""L'obésité doit être détectée via IMC ≥ 30."""
|
|
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
|
|
assert "E66.0" in codes
|
|
|
|
def test_denutrition_severe_regex_E43(self, dossier):
|
|
"""'Dénutrition sévère' dans le texte → E43 via regex."""
|
|
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
|
|
assert "E43" in codes, f"E43 attendu via regex 'denutrition severe', trouvé : {codes}"
|
|
|
|
def test_coexistence_E66_E43(self, dossier):
|
|
"""E66.0 et E43 doivent coexister (pas de blocage HARD)."""
|
|
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
|
|
assert "E66.0" in codes and "E43" in codes
|
|
|
|
def test_albumine_extraite(self, dossier):
|
|
alb = [b for b in dossier.biologie_cle if b.test == "Albumine"]
|
|
assert len(alb) >= 1
|
|
assert alb[0].valeur_num == 25.0
|
|
|
|
def test_diabete_detecte(self, dossier):
|
|
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
|
|
assert "E11.9" in codes
|
|
|
|
def test_tabagisme_detecte(self, dossier):
|
|
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
|
|
assert "F17.2" in codes
|
|
|
|
|
|
# ── Cas 3 : Adulte dénutri modéré sans albumine (IMC seul) ──────────
|
|
|
|
|
|
class TestCas3AdulteDenutriModere:
|
|
"""Mme L., 45 ans, hospitalisée pour angiocholite.
|
|
|
|
IMC 17.8 → entre 17 et 18.5 → dénutrition modérée (E44.0) par HAS 2021.
|
|
Pas d'albumine → pas d'upgrade de sévérité.
|
|
"""
|
|
|
|
@pytest.fixture
|
|
def dossier(self) -> DossierMedical:
|
|
parsed = {
|
|
"type": "crh",
|
|
"patient": {
|
|
"sexe": "F",
|
|
"date_naissance": "12/09/1979",
|
|
},
|
|
"sejour": {
|
|
"date_entree": "15/03/2025",
|
|
"date_sortie": "21/03/2025",
|
|
},
|
|
"diagnostics": [],
|
|
"signes_vitaux": {"imc": 17.8, "poids_kg": 48, "taille_cm": 164},
|
|
}
|
|
text = """\
|
|
Votre patiente née le 12/09/1979 a été hospitalisée du 15/03/2025 au 21/03/2025.
|
|
|
|
Antécédents :
|
|
- Lithiases vésiculaires
|
|
- Anorexie restrictive ancienne (adolescence)
|
|
|
|
Au total : Angiocholite sur lithiase du cholédoque traitée par CPRE
|
|
puis cholécystectomie par cœlioscopie.
|
|
|
|
Biologie d'entrée :
|
|
CRP = 92 mg/L
|
|
ASAT = 180 UI/L
|
|
ALAT = 210 UI/L
|
|
Bilirubine totale = 45 µmol/L
|
|
Lipasémie = 890 UI/L
|
|
Hémoglobine = 11.8 g/dL
|
|
Leucocytes = 12.5 G/L
|
|
|
|
IMC: 17.8
|
|
|
|
TTT de sortie :
|
|
Paracétamol 1g x3/jour
|
|
Spasfon si besoin
|
|
|
|
Devenir : retour à domicile."""
|
|
return extract_medical_info(parsed, text)
|
|
|
|
def test_imc_178(self, dossier):
|
|
assert dossier.sejour.imc == 17.8
|
|
|
|
def test_denutrition_moderee_E44(self, dossier):
|
|
"""IMC 17.8 adulte → E44.0 (modéré, 17 < IMC < 18.5)."""
|
|
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
|
|
assert "E44.0" in codes, f"E44.0 attendu, trouvé : {codes}"
|
|
|
|
def test_pas_E43(self, dossier):
|
|
"""Pas de sévère sans albumine basse ni IMC ≤ 17."""
|
|
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
|
|
assert "E43" not in codes
|
|
|
|
def test_pas_albumine_extraite(self, dossier):
|
|
"""Pas d'albumine dans le texte → pas d'extraction."""
|
|
alb = [b for b in dossier.biologie_cle if b.test == "Albumine"]
|
|
assert len(alb) == 0
|
|
|
|
def test_angiocholite_detectee(self, dossier):
|
|
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
|
|
all_codes = codes.copy()
|
|
if dossier.diagnostic_principal and dossier.diagnostic_principal.cim10_suggestion:
|
|
all_codes.add(dossier.diagnostic_principal.cim10_suggestion)
|
|
assert "K83.0" in all_codes
|
|
|
|
def test_cholecystectomie_detectee(self, dossier):
|
|
acte_codes = {a.code_ccam_suggestion for a in dossier.actes_ccam}
|
|
assert "HMFC004" in acte_codes
|
|
|
|
def test_alerte_has2021(self, dossier):
|
|
assert any("HAS 2021" in a for a in dossier.alertes_codage)
|
|
|
|
|
|
# ── Cas 4 : IMC normal, pas de dénutrition (contrôle négatif) ───────
|
|
|
|
|
|
class TestCas4ControleNegatif:
|
|
"""M. R., 55 ans, hospitalisé pour colique hépatique.
|
|
|
|
IMC 26.3 → au-dessus de tous les seuils → aucune dénutrition.
|
|
Albumine 38 g/L → normale.
|
|
Vérifie que la détection HAS 2021 ne produit pas de faux positif.
|
|
"""
|
|
|
|
@pytest.fixture
|
|
def dossier(self) -> DossierMedical:
|
|
parsed = {
|
|
"type": "crh",
|
|
"patient": {
|
|
"sexe": "M",
|
|
"date_naissance": "30/11/1969",
|
|
},
|
|
"sejour": {
|
|
"date_entree": "01/04/2025",
|
|
"date_sortie": "03/04/2025",
|
|
},
|
|
"diagnostics": [],
|
|
"signes_vitaux": {"imc": 26.3, "poids_kg": 82, "taille_cm": 176},
|
|
}
|
|
text = """\
|
|
Votre patient né le 30/11/1969 a été hospitalisé du 01/04/2025 au 03/04/2025.
|
|
|
|
Antécédents :
|
|
- Hypertension artérielle
|
|
- Dyslipidémie
|
|
|
|
Au total : Colique hépatique sur lithiase vésiculaire.
|
|
Bonne évolution. Cholécystectomie programmée à distance.
|
|
|
|
Biologie :
|
|
CRP = 12 mg/L
|
|
Albumine = 38 g/L
|
|
ASAT = 35 UI/L
|
|
ALAT = 42 UI/L
|
|
Hémoglobine = 14.5 g/dL
|
|
Créatinine = 78 µmol/L
|
|
|
|
IMC: 26.3
|
|
|
|
TTT de sortie :
|
|
Paracétamol si besoin
|
|
Spasfon si besoin
|
|
|
|
Devenir : retour à domicile."""
|
|
return extract_medical_info(parsed, text)
|
|
|
|
def test_imc_normal(self, dossier):
|
|
assert dossier.sejour.imc == 26.3
|
|
|
|
def test_albumine_normale_extraite(self, dossier):
|
|
"""Albumine 38 g/L → extraite mais normale."""
|
|
alb = [b for b in dossier.biologie_cle if b.test == "Albumine"]
|
|
assert len(alb) >= 1
|
|
assert alb[0].valeur_num == 38.0
|
|
|
|
def test_pas_de_denutrition(self, dossier):
|
|
"""IMC 26.3 + albumine 38 → aucun code E40-E46."""
|
|
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
|
|
nutrition_codes = {c for c in codes if c and c.startswith("E4") and c[:3] in
|
|
("E40", "E41", "E42", "E43", "E44", "E45", "E46")}
|
|
assert not nutrition_codes, f"Faux positif dénutrition : {nutrition_codes}"
|
|
|
|
def test_pas_alerte_has2021(self, dossier):
|
|
"""Aucune alerte HAS 2021 ne doit apparaître."""
|
|
assert not any("HAS 2021" in a for a in dossier.alertes_codage)
|
|
|
|
def test_hta_detectee(self, dossier):
|
|
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
|
|
assert "I10" in codes
|
|
|
|
def test_dyslipidemie_detectee(self, dossier):
|
|
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
|
|
assert "E78.5" in codes
|