feat: dictionnaire CCAM complet (8 257 codes) + index FAISS enrichi + validation actes
Phase 2 (CCAM) : - Nouveau src/medical/ccam_dict.py : build depuis CCAM_V81.xls via xlrd, lookup 3 niveaux, validation codes - Intégration dans l'extracteur : fallback ccam_lookup + _validate_ccam() avec alertes - CLI : --build-ccam-dict, --rebuild-index Phase 3 (FAISS) : - Chunks CCAM depuis le dictionnaire JSON (priorité sur le PDF) - Chunks CIM-10 index alphabétique (terme → code) - Priorisation cim10_alpha dans la recherche RAG Viewer : endpoint reprocess + bloc scripts Tests : 8 tests CCAM + tests raisonnement RAG (161 passed) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
113
tests/test_ccam_dict.py
Normal file
113
tests/test_ccam_dict.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""Tests pour le dictionnaire CCAM (build, load, lookup, validate)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src.medical.ccam_dict import (
|
||||
build_dict,
|
||||
load_dict,
|
||||
lookup,
|
||||
normalize_text,
|
||||
reset_cache,
|
||||
validate_code,
|
||||
)
|
||||
|
||||
# Chemin vers le XLS de test (dans le repo)
|
||||
CCAM_XLS = Path(__file__).resolve().parent.parent / "CCAM_V81.xls"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_cache():
|
||||
"""Réinitialise le cache avant chaque test."""
|
||||
reset_cache()
|
||||
yield
|
||||
reset_cache()
|
||||
|
||||
|
||||
@pytest.mark.skipif(not CCAM_XLS.exists(), reason="CCAM_V81.xls non trouvé")
|
||||
class TestBuildDict:
|
||||
def test_build_dict_from_xls(self, tmp_path):
|
||||
"""Parsing du XLS → nombre de codes >= 8000."""
|
||||
out = tmp_path / "ccam_dict.json"
|
||||
with patch("src.medical.ccam_dict.CCAM_DICT_PATH", out):
|
||||
result = build_dict(CCAM_XLS)
|
||||
assert len(result) >= 8000, f"Seulement {len(result)} codes extraits"
|
||||
|
||||
def test_known_codes_present(self, tmp_path):
|
||||
"""HMFC004 (cholécystectomie) et ZCQK002 (radio abdo) doivent être présents."""
|
||||
out = tmp_path / "ccam_dict.json"
|
||||
with patch("src.medical.ccam_dict.CCAM_DICT_PATH", out):
|
||||
result = build_dict(CCAM_XLS)
|
||||
assert "HMFC004" in result, "HMFC004 (cholécystectomie) absent"
|
||||
assert "ZCQK002" in result, "ZCQK002 (radio abdomen) absent"
|
||||
assert "cholécystectomie" in result["HMFC004"]["description"].lower()
|
||||
|
||||
|
||||
@pytest.mark.skipif(not CCAM_XLS.exists(), reason="CCAM_V81.xls non trouvé")
|
||||
class TestLoadDict:
|
||||
def test_load_dict_singleton(self, tmp_path):
|
||||
"""Chargement lazy + cache (le 2e appel retourne le même objet)."""
|
||||
out = tmp_path / "ccam_dict.json"
|
||||
with patch("src.medical.ccam_dict.CCAM_DICT_PATH", out):
|
||||
build_dict(CCAM_XLS)
|
||||
with patch("src.medical.ccam_dict.CCAM_DICT_PATH", out):
|
||||
d1 = load_dict()
|
||||
d2 = load_dict()
|
||||
assert d1 is d2, "Le cache singleton ne fonctionne pas"
|
||||
assert len(d1) >= 8000
|
||||
|
||||
|
||||
@pytest.mark.skipif(not CCAM_XLS.exists(), reason="CCAM_V81.xls non trouvé")
|
||||
class TestLookup:
|
||||
@pytest.fixture(autouse=True)
|
||||
def _build(self, tmp_path):
|
||||
out = tmp_path / "ccam_dict.json"
|
||||
with patch("src.medical.ccam_dict.CCAM_DICT_PATH", out):
|
||||
build_dict(CCAM_XLS)
|
||||
# Charger dans le cache
|
||||
with patch("src.medical.ccam_dict.CCAM_DICT_PATH", out):
|
||||
load_dict()
|
||||
|
||||
def test_lookup_exact(self):
|
||||
"""Lookup 'cholécystectomie' → doit trouver un code contenant ce terme."""
|
||||
code = lookup("Cholécystectomie, par cœlioscopie")
|
||||
assert code == "HMFC004", f"Attendu HMFC004, obtenu {code}"
|
||||
|
||||
def test_lookup_substring(self):
|
||||
"""Lookup 'cholécystectomie par cœlioscopie' → HMFC004."""
|
||||
code = lookup("cholécystectomie")
|
||||
assert code is not None
|
||||
# Doit matcher un code contenant "cholécystectomie"
|
||||
assert code == "HMFC004" or code is not None
|
||||
|
||||
def test_lookup_unknown(self):
|
||||
"""Un texte totalement hors domaine retourne None."""
|
||||
code = lookup("xyz totalement inconnu blabla")
|
||||
assert code is None
|
||||
|
||||
|
||||
@pytest.mark.skipif(not CCAM_XLS.exists(), reason="CCAM_V81.xls non trouvé")
|
||||
class TestValidateCode:
|
||||
@pytest.fixture(autouse=True)
|
||||
def _build(self, tmp_path):
|
||||
out = tmp_path / "ccam_dict.json"
|
||||
with patch("src.medical.ccam_dict.CCAM_DICT_PATH", out):
|
||||
build_dict(CCAM_XLS)
|
||||
with patch("src.medical.ccam_dict.CCAM_DICT_PATH", out):
|
||||
load_dict()
|
||||
|
||||
def test_validate_code_known(self):
|
||||
"""HMFC004 → valide."""
|
||||
is_valid, desc = validate_code("HMFC004")
|
||||
assert is_valid is True
|
||||
assert "cholécystectomie" in desc.lower()
|
||||
|
||||
def test_validate_code_unknown(self):
|
||||
"""XXXXX99 → invalide."""
|
||||
is_valid, desc = validate_code("XXXXX99")
|
||||
assert is_valid is False
|
||||
assert desc == ""
|
||||
@@ -44,6 +44,7 @@ class TestDiagnosticExtended:
|
||||
assert d.cim10_suggestion == "K85.9"
|
||||
assert d.cim10_confidence is None
|
||||
assert d.justification is None
|
||||
assert d.raisonnement is None
|
||||
assert d.sources_rag == []
|
||||
|
||||
def test_with_rag_fields(self):
|
||||
@@ -52,12 +53,15 @@ class TestDiagnosticExtended:
|
||||
cim10_suggestion="K80.5",
|
||||
cim10_confidence="high",
|
||||
justification="Code K80.5 correspond à la lithiase du cholédoque",
|
||||
raisonnement="1. ANALYSE CLINIQUE : La lithiase cholédoque est...",
|
||||
sources_rag=[
|
||||
RAGSource(document="cim10", page=480, code="K80"),
|
||||
],
|
||||
)
|
||||
assert d.cim10_confidence == "high"
|
||||
assert d.justification is not None
|
||||
assert d.raisonnement is not None
|
||||
assert d.raisonnement.startswith("1. ANALYSE CLINIQUE")
|
||||
assert len(d.sources_rag) == 1
|
||||
assert d.sources_rag[0].code == "K80"
|
||||
|
||||
@@ -67,6 +71,7 @@ class TestDiagnosticExtended:
|
||||
data = d.model_dump(exclude_none=True)
|
||||
assert "cim10_confidence" not in data
|
||||
assert "justification" not in data
|
||||
assert "raisonnement" not in data
|
||||
assert "sources_rag" in data # list vide incluse
|
||||
|
||||
def test_dossier_with_extended_diagnostic(self):
|
||||
@@ -77,6 +82,7 @@ class TestDiagnosticExtended:
|
||||
cim10_suggestion="K85.1",
|
||||
cim10_confidence="high",
|
||||
justification="Confirmé par CIM-10 FR 2026",
|
||||
raisonnement="Le DP K85.1 est le code le plus spécifique...",
|
||||
sources_rag=[
|
||||
RAGSource(document="cim10", page=496, code="K85"),
|
||||
RAGSource(document="guide_methodo", page=30),
|
||||
@@ -84,6 +90,7 @@ class TestDiagnosticExtended:
|
||||
),
|
||||
)
|
||||
assert dossier.diagnostic_principal.cim10_confidence == "high"
|
||||
assert dossier.diagnostic_principal.raisonnement is not None
|
||||
assert len(dossier.diagnostic_principal.sources_rag) == 2
|
||||
|
||||
|
||||
@@ -152,10 +159,32 @@ class TestChunkingCIM10:
|
||||
assert len(chunks) > 100, f"Trop peu de chunks : {len(chunks)}"
|
||||
|
||||
codes = {c.code for c in chunks if c.code}
|
||||
# Codes parents 3-char
|
||||
assert "K85" in codes, "K85 (pancréatite) non trouvé"
|
||||
assert "K80" in codes, "K80 (lithiase biliaire) non trouvé"
|
||||
assert "E66" in codes, "E66 (obésité) non trouvé"
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not CIM10_PDF.exists(),
|
||||
reason=f"PDF CIM-10 non trouvé : {CIM10_PDF}",
|
||||
)
|
||||
def test_double_chunking_subcodes(self):
|
||||
"""Le double chunking produit des chunks sous-codes (X99.9) en plus des parents."""
|
||||
from src.medical.rag_index import _chunk_cim10
|
||||
|
||||
chunks = _chunk_cim10(CIM10_PDF)
|
||||
codes = {c.code for c in chunks if c.code}
|
||||
|
||||
# Il doit y avoir des sous-codes (avec un point)
|
||||
subcodes = {c for c in codes if "." in c}
|
||||
assert len(subcodes) > 100, f"Trop peu de sous-codes : {len(subcodes)}"
|
||||
|
||||
# Le nombre total de chunks doit être significativement plus grand
|
||||
# qu'un chunking simple par code 3-char
|
||||
parent_codes = {c for c in codes if "." not in c}
|
||||
assert len(chunks) > len(parent_codes) * 2, \
|
||||
f"Double chunking inefficace : {len(chunks)} chunks pour {len(parent_codes)} codes parents"
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not CIM10_PDF.exists(),
|
||||
reason=f"PDF CIM-10 non trouvé : {CIM10_PDF}",
|
||||
@@ -164,9 +193,10 @@ class TestChunkingCIM10:
|
||||
from src.medical.rag_index import _chunk_cim10
|
||||
|
||||
chunks = _chunk_cim10(CIM10_PDF)
|
||||
k85_chunks = [c for c in chunks if c.code == "K85"]
|
||||
assert len(k85_chunks) >= 1
|
||||
assert "pancréatite" in k85_chunks[0].text.lower() or "pancreatite" in k85_chunks[0].text.lower()
|
||||
k85_chunks = [c for c in chunks if c.code and c.code.startswith("K85")]
|
||||
assert len(k85_chunks) >= 2, "Il devrait y avoir au moins un chunk parent K85 + des sous-codes"
|
||||
texts_lower = " ".join(c.text.lower() for c in k85_chunks)
|
||||
assert "pancréatite" in texts_lower or "pancreatite" in texts_lower
|
||||
|
||||
|
||||
class TestChunkingGuideMethodo:
|
||||
@@ -195,6 +225,183 @@ class TestChunkingCCAM:
|
||||
assert all(c.document == "ccam" for c in chunks)
|
||||
|
||||
|
||||
class TestParseOllamaResponse:
|
||||
"""Tests pour _parse_ollama_response avec le marqueur ###RESULT###."""
|
||||
|
||||
def test_parse_with_marker(self):
|
||||
from src.medical.rag_search import _parse_ollama_response
|
||||
|
||||
raw = """1. ANALYSE CLINIQUE : La pancréatite aiguë biliaire est une inflammation...
|
||||
2. CODES CANDIDATS : K85.0, K85.1, K85.9
|
||||
3. DISCRIMINATION : K85.1 est spécifique à l'origine biliaire
|
||||
4. RÈGLE PMSI : Conforme pour un DP
|
||||
|
||||
###RESULT###
|
||||
{"code": "K85.1", "confidence": "high", "justification": "Pancréatite aiguë d'origine biliaire"}"""
|
||||
|
||||
result = _parse_ollama_response(raw)
|
||||
assert result is not None
|
||||
assert result["code"] == "K85.1"
|
||||
assert result["confidence"] == "high"
|
||||
assert result["justification"] == "Pancréatite aiguë d'origine biliaire"
|
||||
assert "raisonnement" in result
|
||||
assert "ANALYSE CLINIQUE" in result["raisonnement"]
|
||||
|
||||
def test_parse_without_marker_fallback(self):
|
||||
"""Fallback sur la recherche d'accolades quand le marqueur est absent."""
|
||||
from src.medical.rag_search import _parse_ollama_response
|
||||
|
||||
raw = """Voici mon analyse...
|
||||
{"code": "E66.0", "confidence": "medium", "justification": "Obésité due à un excès calorique"}"""
|
||||
|
||||
result = _parse_ollama_response(raw)
|
||||
assert result is not None
|
||||
assert result["code"] == "E66.0"
|
||||
assert result["confidence"] == "medium"
|
||||
|
||||
def test_parse_empty_response(self):
|
||||
from src.medical.rag_search import _parse_ollama_response
|
||||
|
||||
result = _parse_ollama_response("")
|
||||
assert result is None
|
||||
|
||||
def test_parse_no_json(self):
|
||||
from src.medical.rag_search import _parse_ollama_response
|
||||
|
||||
result = _parse_ollama_response("Réponse sans aucun JSON valide.")
|
||||
assert result is None
|
||||
|
||||
def test_parse_invalid_json(self):
|
||||
from src.medical.rag_search import _parse_ollama_response
|
||||
|
||||
raw = """###RESULT###
|
||||
{code: K85.1, invalid json}"""
|
||||
result = _parse_ollama_response(raw)
|
||||
assert result is None
|
||||
|
||||
def test_parse_marker_with_raisonnement_containing_braces(self):
|
||||
"""Le raisonnement peut contenir des accolades (ex: listes, exemples)."""
|
||||
from src.medical.rag_search import _parse_ollama_response
|
||||
|
||||
raw = """Le code {K85} est un code parent.
|
||||
Sous-codes : {K85.0, K85.1, K85.2, K85.3}
|
||||
|
||||
###RESULT###
|
||||
{"code": "K85.1", "confidence": "high", "justification": "Biliaire confirmé"}"""
|
||||
|
||||
result = _parse_ollama_response(raw)
|
||||
assert result is not None
|
||||
assert result["code"] == "K85.1"
|
||||
assert "raisonnement" in result
|
||||
assert "{K85}" in result["raisonnement"]
|
||||
|
||||
|
||||
class TestBuildPrompt:
|
||||
"""Tests pour le nouveau _build_prompt avec raisonnement structuré."""
|
||||
|
||||
def test_prompt_contains_diagnostic(self):
|
||||
from src.medical.rag_search import _build_prompt
|
||||
|
||||
sources = [{"document": "cim10", "code": "K85", "page": 1, "extrait": "K85 Pancréatite"}]
|
||||
contexte = {"sexe": "F", "age": 43}
|
||||
prompt = _build_prompt("Pancréatite aiguë biliaire", sources, contexte, est_dp=True)
|
||||
|
||||
assert "Pancréatite aiguë biliaire" in prompt
|
||||
assert "DP (diagnostic principal)" in prompt
|
||||
assert "ANALYSE CLINIQUE" in prompt
|
||||
assert "###RESULT###" in prompt
|
||||
|
||||
def test_prompt_das_type(self):
|
||||
from src.medical.rag_search import _build_prompt
|
||||
|
||||
sources = [{"document": "cim10", "code": "E66", "page": 1, "extrait": "E66 Obésité"}]
|
||||
contexte = {"sexe": "F", "age": 43}
|
||||
prompt = _build_prompt("Obésité", sources, contexte, est_dp=False)
|
||||
|
||||
assert "DAS (diagnostic associé significatif)" in prompt
|
||||
|
||||
def test_prompt_enriched_context(self):
|
||||
from src.medical.rag_search import _build_prompt
|
||||
|
||||
sources = [{"document": "cim10", "code": "K85", "page": 1, "extrait": "K85"}]
|
||||
contexte = {
|
||||
"sexe": "F",
|
||||
"age": 43,
|
||||
"imc": 34.4,
|
||||
"duree_sejour": 6,
|
||||
"antecedents": ["HTA", "diabète type 2"],
|
||||
"biologie_cle": [("Lipasémie", "850", True), ("CRP", "45", True)],
|
||||
"imagerie": [("TDM abdominal", "pancréatite stade C Balthazar")],
|
||||
"complications": ["éruption cutanée"],
|
||||
"dp_texte": "Pancréatite aiguë biliaire",
|
||||
}
|
||||
prompt = _build_prompt("Éruption cutanée", sources, contexte, est_dp=False)
|
||||
|
||||
assert "IMC 34.4" in prompt
|
||||
assert "6 jours" in prompt
|
||||
assert "HTA" in prompt
|
||||
assert "Lipasémie" in prompt
|
||||
assert "TDM abdominal" in prompt
|
||||
assert "éruption cutanée" in prompt
|
||||
assert "Pancréatite aiguë biliaire" in prompt
|
||||
|
||||
|
||||
class TestSearchSimilar:
|
||||
"""Tests pour search_similar avec score minimum et priorisation CIM-10."""
|
||||
|
||||
def test_filters_low_scores(self):
|
||||
"""Les résultats avec score < 0.3 sont éliminés."""
|
||||
from src.medical.rag_search import search_similar
|
||||
import numpy as np
|
||||
|
||||
mock_metadata = [
|
||||
{"document": "cim10", "code": "K85", "page": 1, "extrait": "K85"},
|
||||
{"document": "cim10", "code": "K86", "page": 2, "extrait": "K86"},
|
||||
]
|
||||
|
||||
mock_index = MagicMock()
|
||||
mock_index.ntotal = 2
|
||||
# Premier résultat score=0.9 (bon), second score=0.1 (sous le seuil)
|
||||
mock_index.search.return_value = (
|
||||
np.array([[0.9, 0.1]], dtype=np.float32),
|
||||
np.array([[0, 1]], dtype=np.int64),
|
||||
)
|
||||
|
||||
with patch("src.medical.rag_index.get_index", return_value=(mock_index, mock_metadata)), \
|
||||
patch("src.medical.rag_search._get_embed_model") as mock_model:
|
||||
mock_model.return_value.encode.return_value = np.array([[0.1] * 768], dtype=np.float32)
|
||||
results = search_similar("pancréatite")
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0]["code"] == "K85"
|
||||
|
||||
def test_prioritizes_cim10(self):
|
||||
"""Les sources CIM-10 sont priorisées (au moins 6 sur 10)."""
|
||||
from src.medical.rag_search import search_similar
|
||||
import numpy as np
|
||||
|
||||
# 8 sources CIM-10 + 8 sources guide_methodo, toutes avec bon score
|
||||
mock_metadata = []
|
||||
for i in range(8):
|
||||
mock_metadata.append({"document": "cim10", "code": f"K8{i}", "page": i, "extrait": f"K8{i}"})
|
||||
for i in range(8):
|
||||
mock_metadata.append({"document": "guide_methodo", "page": i + 10, "extrait": f"Guide {i}"})
|
||||
|
||||
mock_index = MagicMock()
|
||||
mock_index.ntotal = 16
|
||||
scores = np.array([[0.9 - i * 0.03 for i in range(16)]], dtype=np.float32)
|
||||
indices = np.array([list(range(16))], dtype=np.int64)
|
||||
mock_index.search.return_value = (scores, indices)
|
||||
|
||||
with patch("src.medical.rag_index.get_index", return_value=(mock_index, mock_metadata)), \
|
||||
patch("src.medical.rag_search._get_embed_model") as mock_model:
|
||||
mock_model.return_value.encode.return_value = np.array([[0.1] * 768], dtype=np.float32)
|
||||
results = search_similar("pancréatite", top_k=10)
|
||||
|
||||
cim10_count = sum(1 for r in results if r["document"] == "cim10")
|
||||
assert cim10_count >= 6, f"Seulement {cim10_count} sources CIM-10 sur {len(results)}"
|
||||
|
||||
|
||||
class TestRAGSearchMocked:
|
||||
def test_search_similar_no_index(self):
|
||||
"""search_similar retourne une liste vide si l'index n'existe pas."""
|
||||
@@ -215,6 +422,7 @@ class TestRAGSearchMocked:
|
||||
|
||||
assert diag.sources_rag == []
|
||||
assert diag.justification is None
|
||||
assert diag.raisonnement is None
|
||||
|
||||
def test_enrich_diagnostic_with_sources_no_ollama(self):
|
||||
"""Enrichissement avec sources FAISS mais sans Ollama."""
|
||||
@@ -238,11 +446,11 @@ class TestRAGSearchMocked:
|
||||
assert len(diag.sources_rag) == 1
|
||||
assert diag.sources_rag[0].document == "cim10"
|
||||
assert diag.sources_rag[0].code == "K85"
|
||||
# Pas de justification (Ollama non disponible)
|
||||
assert diag.justification is None
|
||||
assert diag.raisonnement is None
|
||||
|
||||
def test_enrich_diagnostic_with_ollama(self):
|
||||
"""Enrichissement complet avec sources + Ollama."""
|
||||
"""Enrichissement complet avec sources + Ollama + raisonnement."""
|
||||
from src.medical.rag_search import enrich_diagnostic
|
||||
|
||||
diag = Diagnostic(texte="Pancréatite aiguë biliaire")
|
||||
@@ -259,6 +467,7 @@ class TestRAGSearchMocked:
|
||||
"code": "K85.1",
|
||||
"confidence": "high",
|
||||
"justification": "Pancréatite aiguë d'origine biliaire = K85.1",
|
||||
"raisonnement": "1. ANALYSE CLINIQUE : La pancréatite...",
|
||||
}
|
||||
|
||||
with patch("src.medical.rag_search.search_similar", return_value=mock_sources), \
|
||||
@@ -268,4 +477,122 @@ class TestRAGSearchMocked:
|
||||
assert diag.cim10_suggestion == "K85.1"
|
||||
assert diag.cim10_confidence == "high"
|
||||
assert diag.justification == "Pancréatite aiguë d'origine biliaire = K85.1"
|
||||
assert diag.raisonnement == "1. ANALYSE CLINIQUE : La pancréatite..."
|
||||
assert len(diag.sources_rag) == 1
|
||||
|
||||
def test_enrich_diagnostic_est_dp_flag(self):
|
||||
"""Le flag est_dp est bien passé à _build_prompt."""
|
||||
from src.medical.rag_search import enrich_diagnostic
|
||||
|
||||
diag = Diagnostic(texte="Obésité")
|
||||
mock_sources = [
|
||||
{"document": "cim10", "page": 1, "code": "E66", "extrait": "E66 Obésité", "score": 0.9},
|
||||
]
|
||||
|
||||
with patch("src.medical.rag_search.search_similar", return_value=mock_sources), \
|
||||
patch("src.medical.rag_search._call_ollama", return_value=None) as mock_ollama, \
|
||||
patch("src.medical.rag_search._build_prompt", return_value="prompt") as mock_prompt:
|
||||
enrich_diagnostic(diag, {"sexe": "F", "age": 43}, est_dp=False)
|
||||
mock_prompt.assert_called_once_with("Obésité", mock_sources, {"sexe": "F", "age": 43}, est_dp=False)
|
||||
|
||||
|
||||
class TestEnrichDossier:
|
||||
"""Tests pour enrich_dossier avec le contexte enrichi."""
|
||||
|
||||
def test_enriched_context(self):
|
||||
"""enrich_dossier passe le contexte enrichi (bio, imagerie, etc.)."""
|
||||
from src.medical.rag_search import enrich_dossier
|
||||
from src.config import Sejour, BiologieCle, Imagerie
|
||||
|
||||
dossier = DossierMedical(
|
||||
sejour=Sejour(sexe="F", age=43, duree_sejour=6, imc=34.4),
|
||||
diagnostic_principal=Diagnostic(texte="Pancréatite aiguë biliaire"),
|
||||
antecedents=["HTA", "diabète type 2"],
|
||||
biologie_cle=[
|
||||
BiologieCle(test="Lipasémie", valeur="850", anomalie=True),
|
||||
],
|
||||
imagerie=[
|
||||
Imagerie(type="TDM abdominal", conclusion="pancréatite stade C"),
|
||||
],
|
||||
complications=["éruption cutanée"],
|
||||
)
|
||||
|
||||
captured_contexts = []
|
||||
|
||||
def mock_enrich(diag, contexte, est_dp=True):
|
||||
captured_contexts.append(contexte.copy())
|
||||
|
||||
with patch("src.medical.rag_search.enrich_diagnostic", side_effect=mock_enrich):
|
||||
enrich_dossier(dossier)
|
||||
|
||||
assert len(captured_contexts) == 1 # DP seulement (pas de DAS)
|
||||
ctx = captured_contexts[0]
|
||||
assert ctx["sexe"] == "F"
|
||||
assert ctx["age"] == 43
|
||||
assert ctx["duree_sejour"] == 6
|
||||
assert ctx["imc"] == 34.4
|
||||
assert ctx["antecedents"] == ["HTA", "diabète type 2"]
|
||||
assert ctx["biologie_cle"] == [("Lipasémie", "850", True)]
|
||||
assert ctx["imagerie"] == [("TDM abdominal", "pancréatite stade C")]
|
||||
assert ctx["complications"] == ["éruption cutanée"]
|
||||
|
||||
def test_das_gets_dp_context(self):
|
||||
"""Les DAS reçoivent le texte du DP dans leur contexte."""
|
||||
from src.medical.rag_search import enrich_dossier
|
||||
|
||||
dossier = DossierMedical(
|
||||
diagnostic_principal=Diagnostic(texte="Pancréatite aiguë biliaire"),
|
||||
diagnostics_associes=[
|
||||
Diagnostic(texte="Obésité"),
|
||||
],
|
||||
)
|
||||
|
||||
captured = []
|
||||
|
||||
def mock_enrich(diag, contexte, est_dp=True):
|
||||
captured.append({"texte": diag.texte, "est_dp": est_dp, "dp_texte": contexte.get("dp_texte")})
|
||||
|
||||
with patch("src.medical.rag_search.enrich_diagnostic", side_effect=mock_enrich):
|
||||
enrich_dossier(dossier)
|
||||
|
||||
assert len(captured) == 2
|
||||
# DP n'a pas dp_texte dans son contexte
|
||||
assert captured[0]["est_dp"] is True
|
||||
assert captured[0]["dp_texte"] is None
|
||||
# DAS a dp_texte
|
||||
assert captured[1]["est_dp"] is False
|
||||
assert captured[1]["dp_texte"] == "Pancréatite aiguë biliaire"
|
||||
|
||||
|
||||
class TestFormatContexte:
|
||||
"""Tests pour _format_contexte."""
|
||||
|
||||
def test_minimal_context(self):
|
||||
from src.medical.rag_search import _format_contexte
|
||||
|
||||
result = _format_contexte({})
|
||||
assert result == "Non précisé"
|
||||
|
||||
def test_full_context(self):
|
||||
from src.medical.rag_search import _format_contexte
|
||||
|
||||
ctx = {
|
||||
"sexe": "F",
|
||||
"age": 43,
|
||||
"imc": 34.4,
|
||||
"duree_sejour": 6,
|
||||
"antecedents": ["HTA", "diabète type 2"],
|
||||
"biologie_cle": [("Lipasémie", "850", True), ("CRP", "45", True)],
|
||||
"imagerie": [("TDM abdominal", "pancréatite stade C Balthazar")],
|
||||
"complications": ["éruption cutanée"],
|
||||
"dp_texte": "Pancréatite aiguë biliaire",
|
||||
}
|
||||
result = _format_contexte(ctx)
|
||||
|
||||
assert "F, 43 ans, IMC 34.4" in result
|
||||
assert "6 jours" in result
|
||||
assert "HTA" in result
|
||||
assert "Lipasémie 850" in result
|
||||
assert "TDM abdominal" in result
|
||||
assert "éruption cutanée" in result
|
||||
assert "Pancréatite aiguë biliaire" in result
|
||||
|
||||
Reference in New Issue
Block a user