feat: BIO_NORMALS 33 analytes + interprétations cliniques + cohérence DAS/bio étendue

- BIO_NORMALS passe de 13 à 33 tests (cardio, infectio, métabo, thyroïde, hémato, hépatique)
- _BIO_INTERPRETATION synchronisé (33 entrées, 3 clés high/low/normal chacune)
- _DAS_BIO_CHECKS étendu de 13 à 38 patterns (sepsis, infarctus, EP, diabète, thyroïde, etc.)
- lab_value_sanity.yaml étendu avec 20 garde-fous plausibilité nouveaux tests
- tests/test_bio_normals.py : 32 tests (complétude, concordance, _is_abnormal)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dom
2026-02-20 11:00:53 +01:00
parent 3c070f3c1d
commit 1a3c523987
4 changed files with 318 additions and 9 deletions

View File

@@ -60,3 +60,85 @@ tests:
bilirubine totale: bilirubine totale:
hard_min: 0 hard_min: 0
hard_max: 2000 hard_max: 2000
# --- Nouveaux tests (Phase 5) ---
bilirubine directe:
hard_min: 0
hard_max: 1000
ldh:
hard_min: 0
hard_max: 10000
vs:
hard_min: 0
hard_max: 200
tp:
hard_min: 0
hard_max: 150
tca:
hard_min: 10
hard_max: 200
ferritine:
hard_min: 0
hard_max: 100000 # surcharge massive possible (hémochromatose)
uree:
hard_min: 0
hard_max: 200
troponine:
hard_min: 0
hard_max: 1000
bnp:
hard_min: 0
hard_max: 50000
nt-probnp:
hard_min: 0
hard_max: 100000
d-dimeres:
hard_min: 0
hard_max: 100000
inr:
hard_min: 0.1
hard_max: 20
fibrinogene:
hard_min: 0
hard_max: 20
procalcitonine:
hard_min: 0
hard_max: 1000
lactate:
hard_min: 0
hard_max: 30
glycemie:
hard_min: 0
hard_max: 100
hba1c:
hard_min: 2
hard_max: 20
albumine:
hard_min: 0
hard_max: 100
acide urique:
hard_min: 0
hard_max: 2000
tsh:
hard_min: 0
hard_max: 500

View File

@@ -165,18 +165,48 @@ def _build_tagged_context(dossier: DossierMedical) -> tuple[str, dict[str, str]]
# Interprétations cliniques pour le résumé bio déterministe # Interprétations cliniques pour le résumé bio déterministe
_BIO_INTERPRETATION: dict[str, dict[str, str]] = { _BIO_INTERPRETATION: dict[str, dict[str, str]] = {
"CRP": {"high": "infection/inflammation active", "low": "normal", "normal": "pas d'inflammation"}, # --- Hépatique / digestif ---
"Hémoglobine": {"high": "polyglobulie", "low": "anémie", "normal": "pas d'anémie"},
"Plaquettes": {"high": "thrombocytose", "low": "thrombopénie", "normal": "numération normale"},
"Leucocytes": {"high": "hyperleucocytose", "low": "leucopénie", "normal": "numération normale"},
"Créatinine": {"high": "insuffisance rénale", "low": "normal", "normal": "fonction rénale conservée"},
"Potassium": {"high": "hyperkaliémie", "low": "hypokaliémie", "normal": "kaliémie normale"},
"Sodium": {"high": "hypernatrémie", "low": "hyponatrémie", "normal": "natrémie normale"},
"Lipasémie": {"high": "pancréatite probable", "low": "normal", "normal": "pas de pancréatite"}, "Lipasémie": {"high": "pancréatite probable", "low": "normal", "normal": "pas de pancréatite"},
"ASAT": {"high": "cytolyse hépatique", "low": "normal", "normal": "pas de cytolyse"}, "ASAT": {"high": "cytolyse hépatique", "low": "normal", "normal": "pas de cytolyse"},
"ALAT": {"high": "cytolyse hépatique", "low": "normal", "normal": "pas de cytolyse"}, "ALAT": {"high": "cytolyse hépatique", "low": "normal", "normal": "pas de cytolyse"},
"GGT": {"high": "cholestase/atteinte hépatique", "low": "normal", "normal": "pas de cholestase"}, "GGT": {"high": "cholestase/atteinte hépatique", "low": "normal", "normal": "pas de cholestase"},
"PAL": {"high": "cholestase/atteinte osseuse", "low": "normal", "normal": "pas de cholestase"},
"Bilirubine totale": {"high": "ictère/cholestase", "low": "normal", "normal": "pas d'ictère"}, "Bilirubine totale": {"high": "ictère/cholestase", "low": "normal", "normal": "pas d'ictère"},
"Bilirubine directe": {"high": "cholestase/obstruction biliaire", "low": "normal", "normal": "pas de cholestase"},
"LDH": {"high": "cytolyse/hémolyse", "low": "normal", "normal": "pas de cytolyse"},
# --- Inflammatoire ---
"CRP": {"high": "infection/inflammation active", "low": "normal", "normal": "pas d'inflammation"},
"VS": {"high": "inflammation", "low": "normal", "normal": "pas d'inflammation"},
# --- Ionogramme ---
"Sodium": {"high": "hypernatrémie", "low": "hyponatrémie", "normal": "natrémie normale"},
"Potassium": {"high": "hyperkaliémie", "low": "hypokaliémie", "normal": "kaliémie normale"},
# --- Hématologie ---
"Hémoglobine": {"high": "polyglobulie", "low": "anémie", "normal": "pas d'anémie"},
"Plaquettes": {"high": "thrombocytose", "low": "thrombopénie", "normal": "numération normale"},
"Leucocytes": {"high": "hyperleucocytose", "low": "leucopénie", "normal": "numération normale"},
"TP": {"high": "normal", "low": "insuffisance hépatique/CIVD", "normal": "coagulation normale"},
"TCA": {"high": "hypocoagulabilité", "low": "normal", "normal": "coagulation normale"},
"Ferritine": {"high": "surcharge en fer/inflammation", "low": "carence en fer", "normal": "réserves en fer normales"},
# --- Rénal ---
"Créatinine": {"high": "insuffisance rénale", "low": "normal", "normal": "fonction rénale conservée"},
"Urée": {"high": "insuffisance rénale/catabolisme", "low": "normal", "normal": "fonction rénale conservée"},
# --- Cardiologie ---
"Troponine": {"high": "nécrose myocardique (SCA/IDM)", "low": "normal", "normal": "pas de souffrance myocardique"},
"BNP": {"high": "insuffisance cardiaque", "low": "normal", "normal": "pas d'insuffisance cardiaque"},
"NT-proBNP": {"high": "insuffisance cardiaque", "low": "normal", "normal": "pas d'insuffisance cardiaque"},
"D-dimères": {"high": "activation coagulation (EP/TVP possible)", "low": "normal", "normal": "EP/TVP peu probable"},
"INR": {"high": "hypocoagulabilité/surdosage AVK", "low": "hypercoagulabilité", "normal": "coagulation normale"},
"Fibrinogène": {"high": "inflammation/risque thrombotique", "low": "CIVD/insuffisance hépatique", "normal": "normal"},
# --- Infectiologie ---
"Procalcitonine": {"high": "infection bactérienne", "low": "normal", "normal": "pas d'infection bactérienne"},
"Lactate": {"high": "hypoperfusion/choc", "low": "normal", "normal": "pas d'hypoperfusion"},
# --- Métabolisme ---
"Glycémie": {"high": "hyperglycémie/diabète", "low": "hypoglycémie", "normal": "glycémie normale"},
"HbA1c": {"high": "diabète mal équilibré", "low": "normal", "normal": "équilibre glycémique correct"},
"Albumine": {"high": "déshydratation", "low": "dénutrition/insuffisance hépatique", "normal": "état nutritionnel conservé"},
"Acide urique": {"high": "hyperuricémie/goutte", "low": "normal", "normal": "uricémie normale"},
# --- Thyroïde ---
"TSH": {"high": "hypothyroïdie", "low": "hyperthyroïdie", "normal": "fonction thyroïdienne normale"},
} }
@@ -243,6 +273,7 @@ def _check_das_bio_coherence(dossier: DossierMedical) -> list[str]:
# Patterns DAS → (test bio attendu, direction attendue) # Patterns DAS → (test bio attendu, direction attendue)
_DAS_BIO_CHECKS: dict[str, tuple[str, str]] = { _DAS_BIO_CHECKS: dict[str, tuple[str, str]] = {
# Hématologie
"leucocytose": ("Leucocytes", "high"), "leucocytose": ("Leucocytes", "high"),
"leucopénie": ("Leucocytes", "low"), "leucopénie": ("Leucocytes", "low"),
"leucopenie": ("Leucocytes", "low"), "leucopenie": ("Leucocytes", "low"),
@@ -254,8 +285,42 @@ def _check_das_bio_coherence(dossier: DossierMedical) -> list[str]:
"anémie": ("Hémoglobine", "low"), "anémie": ("Hémoglobine", "low"),
"anemie": ("Hémoglobine", "low"), "anemie": ("Hémoglobine", "low"),
"polyglobulie": ("Hémoglobine", "high"), "polyglobulie": ("Hémoglobine", "high"),
"carence en fer": ("Ferritine", "low"),
"carence martiale": ("Ferritine", "low"),
# Ionogramme
"hyperkaliémie": ("Potassium", "high"), "hyperkaliémie": ("Potassium", "high"),
"hypokaliémie": ("Potassium", "low"), "hypokaliémie": ("Potassium", "low"),
"hypernatrémie": ("Sodium", "high"),
"hyponatrémie": ("Sodium", "low"),
"hyponatremie": ("Sodium", "low"),
# Rénal
"insuffisance rénale": ("Créatinine", "high"),
"insuffisance renale": ("Créatinine", "high"),
# Digestif
"pancréatite": ("Lipasémie", "high"),
"pancreatite": ("Lipasémie", "high"),
# Infectiologie
"sepsis": ("CRP", "high"),
"choc septique": ("Lactate", "high"),
# Cardiologie
"infarctus": ("Troponine", "high"),
"syndrome coronarien": ("Troponine", "high"),
"embolie pulmonaire": ("D-dimères", "high"),
"insuffisance cardiaque": ("BNP", "high"),
# Métabolisme / nutrition
"dénutrition": ("Albumine", "low"),
"denutrition": ("Albumine", "low"),
"diabète": ("Glycémie", "high"),
"diabete": ("Glycémie", "high"),
"hyperuricémie": ("Acide urique", "high"),
"goutte": ("Acide urique", "high"),
# Thyroïde
"hypothyroïdie": ("TSH", "high"),
"hypothyroidie": ("TSH", "high"),
"hyperthyroïdie": ("TSH", "low"),
"hyperthyroidie": ("TSH", "low"),
# Coagulation
"civd": ("Fibrinogène", "low"),
} }
# Indexer les valeurs bio disponibles # Indexer les valeurs bio disponibles

View File

@@ -6,20 +6,48 @@ from __future__ import annotations
# Plages de référence biologiques (min, max) — utilisées par _is_abnormal() # Plages de référence biologiques (min, max) — utilisées par _is_abnormal()
# et exportées pour le formatage du contexte LLM dans rag_search.py # et exportées pour le formatage du contexte LLM dans rag_search.py
BIO_NORMALS: dict[str, tuple[float, float]] = { BIO_NORMALS: dict[str, tuple[float, float]] = {
# --- Hépatique / digestif ---
"Lipasémie": (0, 60), "Lipasémie": (0, 60),
"CRP": (0, 5),
"ASAT": (0, 40), "ASAT": (0, 40),
"ALAT": (0, 40), "ALAT": (0, 40),
"GGT": (0, 60), "GGT": (0, 60),
"PAL": (0, 150), "PAL": (0, 150),
"Bilirubine totale": (0, 17), "Bilirubine totale": (0, 17),
# Ionogramme (fallback adulte ; les règles de décision utilisent reference_ranges.yaml) "Bilirubine directe": (0, 5), # µmol/L
"LDH": (120, 250), # UI/L
# --- Inflammatoire ---
"CRP": (0, 5),
"VS": (0, 20), # mm/h
# --- Ionogramme (fallback adulte ; les règles de décision utilisent reference_ranges.yaml) ---
"Sodium": (135, 145), "Sodium": (135, 145),
"Potassium": (3.5, 5.0), "Potassium": (3.5, 5.0),
# --- Hématologie ---
"Hémoglobine": (12, 17), "Hémoglobine": (12, 17),
"Plaquettes": (150, 400), "Plaquettes": (150, 400),
"Leucocytes": (4, 10), "Leucocytes": (4, 10),
"TP": (70, 100), # %
"TCA": (25, 35), # secondes
"Ferritine": (20, 300), # µg/L
# --- Rénal ---
"Créatinine": (50, 120), "Créatinine": (50, 120),
"Urée": (2.5, 7.5), # mmol/L
# --- Cardiologie ---
"Troponine": (0, 0.04), # ng/mL (seuil hs-TnI)
"BNP": (0, 100), # pg/mL
"NT-proBNP": (0, 300), # pg/mL
"D-dimères": (0, 500), # ng/mL
"INR": (0.8, 1.2), # ratio
"Fibrinogène": (2, 4), # g/L
# --- Infectiologie ---
"Procalcitonine": (0, 0.5), # ng/mL
"Lactate": (0.5, 2.0), # mmol/L
# --- Métabolisme ---
"Glycémie": (3.9, 5.5), # mmol/L (à jeun)
"HbA1c": (4.0, 6.0), # %
"Albumine": (35, 50), # g/L
"Acide urique": (150, 420), # µmol/L
# --- Thyroïde ---
"TSH": (0.4, 4.0), # mUI/L
} }

134
tests/test_bio_normals.py Normal file
View File

@@ -0,0 +1,134 @@
"""Tests unitaires pour les plages de référence biologiques."""
import pytest
from src.medical.bio_normals import BIO_NORMALS, _is_abnormal
from src.control.cpam_context import _BIO_INTERPRETATION
class TestBioNormalsCompleteness:
"""Vérifie la complétude et la cohérence de BIO_NORMALS."""
def test_has_33_analytes(self):
assert len(BIO_NORMALS) == 33
def test_all_tuples_are_valid(self):
for name, (lo, hi) in BIO_NORMALS.items():
assert isinstance(lo, (int, float)), f"{name}: lo doit être numérique"
assert isinstance(hi, (int, float)), f"{name}: hi doit être numérique"
assert lo <= hi, f"{name}: lo ({lo}) > hi ({hi})"
def test_known_analytes_present(self):
expected = {
"CRP", "Hémoglobine", "Plaquettes", "Leucocytes", "Créatinine",
"Sodium", "Potassium", "ASAT", "ALAT", "GGT", "PAL",
"Bilirubine totale", "Lipasémie",
# Nouveaux Phase 5
"Troponine", "BNP", "NT-proBNP", "D-dimères", "INR", "Fibrinogène",
"Procalcitonine", "Lactate", "Glycémie", "HbA1c", "Albumine",
"Urée", "Acide urique", "TP", "TCA", "Ferritine", "LDH",
"Bilirubine directe", "TSH", "VS",
}
assert set(BIO_NORMALS.keys()) == expected
class TestBioInterpretationConcordance:
"""Vérifie que BIO_NORMALS et _BIO_INTERPRETATION sont synchronisés."""
def test_every_normal_has_interpretation(self):
missing = set(BIO_NORMALS) - set(_BIO_INTERPRETATION)
assert not missing, f"BIO_NORMALS sans interprétation: {missing}"
def test_every_interpretation_has_normal(self):
extra = set(_BIO_INTERPRETATION) - set(BIO_NORMALS)
assert not extra, f"Interprétation sans BIO_NORMALS: {extra}"
def test_all_interpretations_have_three_keys(self):
for name, interp in _BIO_INTERPRETATION.items():
assert "high" in interp, f"{name}: manque 'high'"
assert "low" in interp, f"{name}: manque 'low'"
assert "normal" in interp, f"{name}: manque 'normal'"
class TestIsAbnormal:
"""Tests pour _is_abnormal() sur les nouveaux et anciens analytes."""
def test_crp_high(self):
assert _is_abnormal("CRP", "180") is True
def test_crp_normal(self):
assert _is_abnormal("CRP", "3") is False
def test_hemoglobine_low(self):
assert _is_abnormal("Hémoglobine", "8.5") is True
def test_hemoglobine_normal(self):
assert _is_abnormal("Hémoglobine", "14.5") is False
# Nouveaux analytes
def test_troponine_high(self):
assert _is_abnormal("Troponine", "0.15") is True
def test_troponine_normal(self):
assert _is_abnormal("Troponine", "0.02") is False
def test_bnp_high(self):
assert _is_abnormal("BNP", "500") is True
def test_bnp_normal(self):
assert _is_abnormal("BNP", "50") is False
def test_lactate_high(self):
assert _is_abnormal("Lactate", "4.5") is True
def test_lactate_normal(self):
assert _is_abnormal("Lactate", "1.2") is False
def test_glycemie_high(self):
assert _is_abnormal("Glycémie", "8.0") is True
def test_glycemie_low(self):
assert _is_abnormal("Glycémie", "2.5") is True
def test_glycemie_normal(self):
assert _is_abnormal("Glycémie", "4.5") is False
def test_tsh_high(self):
assert _is_abnormal("TSH", "10.0") is True
def test_tsh_low(self):
assert _is_abnormal("TSH", "0.1") is True
def test_tsh_normal(self):
assert _is_abnormal("TSH", "2.5") is False
def test_inr_high(self):
assert _is_abnormal("INR", "3.5") is True
def test_inr_normal(self):
assert _is_abnormal("INR", "1.0") is False
def test_albumine_low(self):
assert _is_abnormal("Albumine", "20") is True
def test_albumine_normal(self):
assert _is_abnormal("Albumine", "42") is False
def test_ferritine_low(self):
assert _is_abnormal("Ferritine", "5") is True
def test_ferritine_normal(self):
assert _is_abnormal("Ferritine", "150") is False
# Valeurs textuelles
def test_text_negative(self):
assert _is_abnormal("CRP", "négative") is False
def test_text_positive(self):
assert _is_abnormal("CRP", "positive") is True
def test_unknown_test(self):
assert _is_abnormal("TestInconnu", "42") is None
def test_unparseable_value(self):
assert _is_abnormal("CRP", "non dosé") is None