feat: cache Ollama + parallélisation ThreadPool + filtrage DAS renforcé + modules GHM/CPAM/export RUM
- Cache persistant JSON thread-safe pour les résultats Ollama (invalidation par modèle) - Parallélisation des appels Ollama (ThreadPoolExecutor, 2 workers) - 6 nouvelles règles de filtrage DAS parasites (doublons, ponctuation, OCR, labo, fragments) - Client Ollama centralisé (mode JSON natif + retry) - Module GHM (estimation CMD/sévérité) - Module contrôle CPAM (parser + contre-argumentation RAG) - Export RUM (format RSS) - Viewer enrichi (détail dossier) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
146
tests/test_cpam_response.py
Normal file
146
tests/test_cpam_response.py
Normal file
@@ -0,0 +1,146 @@
|
||||
"""Tests pour la génération de contre-argumentation CPAM."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src.config import ControleCPAM, Diagnostic, DossierMedical, RAGSource, Sejour
|
||||
from src.control.cpam_response import _build_cpam_prompt, _format_response, generate_cpam_response
|
||||
|
||||
|
||||
def _make_dossier() -> DossierMedical:
|
||||
"""Crée un dossier médical de test."""
|
||||
return DossierMedical(
|
||||
source_file="test.pdf",
|
||||
document_type="crh",
|
||||
sejour=Sejour(sexe="M", age=65, duree_sejour=5),
|
||||
diagnostic_principal=Diagnostic(
|
||||
texte="Cholécystite aiguë",
|
||||
cim10_suggestion="K81.0",
|
||||
),
|
||||
diagnostics_associes=[
|
||||
Diagnostic(texte="Iléus réflexe", cim10_suggestion="K56.0"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _make_controle() -> ControleCPAM:
|
||||
"""Crée un contrôle CPAM de test."""
|
||||
return ControleCPAM(
|
||||
numero_ogc=17,
|
||||
titre="Désaccord sur les DAS",
|
||||
arg_ucr="L'UCR confirme l'avis des médecins contrôleurs au motif que le DAS K56.0 n'est pas justifié.",
|
||||
decision_ucr="UCR confirme avis médecins contrôleurs",
|
||||
dp_ucr=None,
|
||||
da_ucr="K56.0",
|
||||
)
|
||||
|
||||
|
||||
class TestBuildPrompt:
|
||||
def test_prompt_contains_dossier_info(self):
|
||||
dossier = _make_dossier()
|
||||
controle = _make_controle()
|
||||
prompt = _build_cpam_prompt(dossier, controle, [])
|
||||
|
||||
assert "Cholécystite aiguë" in prompt
|
||||
assert "K81.0" in prompt
|
||||
assert "Iléus réflexe" in prompt
|
||||
assert "65 ans" in prompt
|
||||
|
||||
def test_prompt_contains_cpam_argument(self):
|
||||
dossier = _make_dossier()
|
||||
controle = _make_controle()
|
||||
prompt = _build_cpam_prompt(dossier, controle, [])
|
||||
|
||||
assert controle.arg_ucr in prompt
|
||||
assert controle.decision_ucr in prompt
|
||||
|
||||
def test_prompt_contains_codes_contestes(self):
|
||||
dossier = _make_dossier()
|
||||
controle = _make_controle()
|
||||
prompt = _build_cpam_prompt(dossier, controle, [])
|
||||
|
||||
assert "DA proposés par UCR : K56.0" in prompt
|
||||
|
||||
def test_prompt_contains_rag_sources(self):
|
||||
dossier = _make_dossier()
|
||||
controle = _make_controle()
|
||||
sources = [
|
||||
{"document": "guide_methodo", "page": 64, "extrait": "Texte du guide..."},
|
||||
{"document": "cim10", "code": "K56.0", "extrait": "Iléus paralytique..."},
|
||||
]
|
||||
prompt = _build_cpam_prompt(dossier, controle, sources)
|
||||
|
||||
assert "Guide Méthodologique MCO 2026" in prompt
|
||||
assert "CIM-10 FR 2026" in prompt
|
||||
assert "page 64" in prompt
|
||||
|
||||
|
||||
class TestFormatResponse:
|
||||
def test_full_response(self):
|
||||
parsed = {
|
||||
"analyse_contestation": "La CPAM conteste le DAS K56.0",
|
||||
"points_accord": "Aucun",
|
||||
"contre_arguments": "Le guide méthodologique précise...",
|
||||
"references": "Guide métho p.64",
|
||||
"conclusion": "Le DAS est justifié",
|
||||
}
|
||||
text = _format_response(parsed)
|
||||
|
||||
assert "ANALYSE DE LA CONTESTATION" in text
|
||||
assert "CONTRE-ARGUMENTS" in text
|
||||
assert "CONCLUSION" in text
|
||||
# "Aucun" ne doit pas générer la section points d'accord
|
||||
assert "POINTS D'ACCORD" not in text
|
||||
|
||||
def test_partial_response(self):
|
||||
parsed = {
|
||||
"contre_arguments": "Arguments...",
|
||||
"conclusion": "Conclusion...",
|
||||
}
|
||||
text = _format_response(parsed)
|
||||
|
||||
assert "CONTRE-ARGUMENTS" in text
|
||||
assert "CONCLUSION" in text
|
||||
|
||||
def test_empty_response(self):
|
||||
text = _format_response({})
|
||||
assert text == ""
|
||||
|
||||
|
||||
class TestGenerateResponse:
|
||||
@patch("src.control.cpam_response.call_ollama")
|
||||
@patch("src.control.cpam_response._search_rag_for_control")
|
||||
def test_generate_success(self, mock_rag, mock_ollama):
|
||||
mock_rag.return_value = [
|
||||
{"document": "guide_methodo", "page": 64, "extrait": "Texte guide"},
|
||||
]
|
||||
mock_ollama.return_value = {
|
||||
"analyse_contestation": "Analyse...",
|
||||
"contre_arguments": "Contre-arguments...",
|
||||
"conclusion": "Conclusion...",
|
||||
}
|
||||
|
||||
dossier = _make_dossier()
|
||||
controle = _make_controle()
|
||||
|
||||
text, sources = generate_cpam_response(dossier, controle)
|
||||
|
||||
assert "Contre-arguments..." in text
|
||||
assert len(sources) == 1
|
||||
assert sources[0].document == "guide_methodo"
|
||||
mock_ollama.assert_called_once()
|
||||
|
||||
@patch("src.control.cpam_response.call_ollama")
|
||||
@patch("src.control.cpam_response._search_rag_for_control")
|
||||
def test_generate_ollama_unavailable(self, mock_rag, mock_ollama):
|
||||
mock_rag.return_value = []
|
||||
mock_ollama.return_value = None
|
||||
|
||||
dossier = _make_dossier()
|
||||
controle = _make_controle()
|
||||
|
||||
text, sources = generate_cpam_response(dossier, controle)
|
||||
|
||||
assert text == ""
|
||||
assert sources == []
|
||||
Reference in New Issue
Block a user