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:
dom
2026-02-12 13:44:34 +01:00
parent a00e5f1147
commit a58398f5d4
25 changed files with 2872 additions and 97 deletions

146
tests/test_cpam_response.py Normal file
View 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 == []