feat: méthode TIM experte CPAM + moteur de règles étendu
CPAM — Méthode TIM (mémoire en défense) : - Réécriture CPAM_ARGUMENTATION avec raisonnement 5 passes TIM (contexte admin → motif réel → confrontation bio → hiérarchie → validation défensive) - _BIO_THRESHOLDS (19 entrées) + _build_bio_confrontation() pour confrontation biologie/diagnostic avec seuils chiffrés et verdicts - _format_response() dual format : nouveau TIM (moyens numérotés, tableau bio, codes non défendables, conclusion dispositive) + rétrocompat legacy - CPAM_ADVERSARIAL mis à jour pour vérifier honnêteté intellectuelle - Tests adaptés + 12 nouveaux tests (bio confrontation, format TIM) Moteur de règles : - Nouvelles règles YAML : demographic, diagnostic_conflicts, procedure_diagnosis, temporal, parcours - Bio extraction FAISS (synonymes vectoriels) - Veto engine enrichi (citations, Trackare skip, règles démographiques) - Decision engine : _apply_bio_rules_gen() + matchers analytiques Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,10 +19,12 @@ from src.config import (
|
||||
)
|
||||
from src.control.cpam_response import (
|
||||
_assess_dossier_strength,
|
||||
_build_bio_confrontation,
|
||||
_build_bio_summary,
|
||||
_build_correction_prompt,
|
||||
_build_cpam_prompt,
|
||||
_build_tagged_context,
|
||||
_BIO_THRESHOLDS,
|
||||
_check_das_bio_coherence,
|
||||
_extraction_pass,
|
||||
_format_response,
|
||||
@@ -138,14 +140,18 @@ class TestBuildPrompt:
|
||||
assert "CIM-10 FR 2026" in prompt
|
||||
assert "page 64" in prompt
|
||||
|
||||
def test_prompt_contains_three_axes(self):
|
||||
def test_prompt_contains_tim_passes(self):
|
||||
"""Le prompt TIM contient les 5 passes de raisonnement."""
|
||||
dossier = _make_dossier()
|
||||
controle = _make_controle()
|
||||
prompt, _ = _build_cpam_prompt(dossier, controle, [])
|
||||
|
||||
assert "AXE MÉDICAL" in prompt
|
||||
assert "AXE ASYMÉTRIE D'INFORMATION" in prompt
|
||||
assert "AXE RÉGLEMENTAIRE" in prompt
|
||||
assert "PASSE 1" in prompt
|
||||
assert "PASSE 2" in prompt
|
||||
assert "PASSE 3" in prompt
|
||||
assert "PASSE 4" in prompt
|
||||
assert "PASSE 5" in prompt
|
||||
assert "MÉMOIRE EN DÉFENSE" in prompt
|
||||
|
||||
def test_prompt_contains_traitements_imagerie_when_present(self):
|
||||
dossier = _make_dossier_complet()
|
||||
@@ -181,39 +187,44 @@ class TestBuildPrompt:
|
||||
|
||||
assert "ÉLÉMENTS DU DOSSIER NON TRANSMIS À LA CPAM" not in prompt
|
||||
|
||||
def test_prompt_json_format_new_fields(self):
|
||||
def test_prompt_json_format_tim_fields(self):
|
||||
"""Le format JSON demandé inclut les champs TIM."""
|
||||
dossier = _make_dossier()
|
||||
controle = _make_controle()
|
||||
prompt, _ = _build_cpam_prompt(dossier, controle, [])
|
||||
|
||||
assert "contre_arguments_medicaux" in prompt
|
||||
assert "contre_arguments_asymetrie" in prompt
|
||||
assert "contre_arguments_reglementaires" in prompt
|
||||
assert "moyens_defense" in prompt
|
||||
assert "confrontation_bio" in prompt
|
||||
assert "conclusion_dispositive" in prompt
|
||||
assert "codes_non_defendables" in prompt
|
||||
assert "rappel_faits" in prompt
|
||||
|
||||
def test_prompt_contains_cite_exacts(self):
|
||||
"""Le prompt renforcé demande des preuves exactes."""
|
||||
def test_prompt_contains_honesty_rules(self):
|
||||
"""Le prompt TIM contient les règles d'honnêteté intellectuelle."""
|
||||
dossier = _make_dossier()
|
||||
controle = _make_controle()
|
||||
prompt, _ = _build_cpam_prompt(dossier, controle, [])
|
||||
|
||||
assert "HONNÊTETÉ INTELLECTUELLE" in prompt
|
||||
assert "CITE" in prompt
|
||||
assert "EXACTS" in prompt
|
||||
assert "JAMAIS" in prompt
|
||||
|
||||
def test_prompt_contains_interdiction(self):
|
||||
"""Le prompt interdit les références inventées."""
|
||||
def test_prompt_contains_redaction_consignes(self):
|
||||
"""Le prompt TIM contient les consignes de rédaction numérotées."""
|
||||
dossier = _make_dossier()
|
||||
controle = _make_controle()
|
||||
prompt, _ = _build_cpam_prompt(dossier, controle, [])
|
||||
|
||||
assert "INTERDICTION ABSOLUE" in prompt
|
||||
assert "MOYENS DE DÉFENSE NUMÉROTÉS" in prompt
|
||||
assert "N'invente AUCUN tag" in prompt
|
||||
|
||||
def test_prompt_contains_preuves_dossier_field(self):
|
||||
"""Le format JSON demandé inclut preuves_dossier."""
|
||||
dossier = _make_dossier()
|
||||
def test_prompt_contains_bio_confrontation(self):
|
||||
"""Le prompt TIM inclut la section confrontation biologie."""
|
||||
dossier = _make_dossier_complet()
|
||||
controle = _make_controle()
|
||||
prompt, _ = _build_cpam_prompt(dossier, controle, [])
|
||||
|
||||
assert "preuves_dossier" in prompt
|
||||
assert "CONFRONTATION BIOLOGIE" in prompt
|
||||
|
||||
@patch("src.control.cpam_context.validate_code", return_value=(True, "Iléus paralytique et obstruction intestinale"))
|
||||
@patch("src.control.cpam_context.normalize_code", return_value="K56.0")
|
||||
@@ -365,6 +376,96 @@ class TestFormatResponse:
|
||||
assert "AVERTISSEMENT" in text
|
||||
assert "Manuel Imaginaire 2025" in text
|
||||
|
||||
# --- Tests nouveau format TIM ---
|
||||
|
||||
def test_tim_format_memoire_defense(self):
|
||||
"""Le format TIM produit un mémoire en défense structuré."""
|
||||
parsed = {
|
||||
"objet": "Contestation DAS — OGC 17 — Mémoire en défense",
|
||||
"rappel_faits": "Patient M, 65 ans, hospitalisé 5j pour cholécystite aiguë.",
|
||||
"moyens_defense": [
|
||||
{
|
||||
"numero": 1,
|
||||
"titre": "Le DP K81.0 est justifié par la biologie",
|
||||
"argument": "CRP à 180 mg/L confirme l'inflammation aiguë.",
|
||||
"preuves": [
|
||||
{"ref": "[BIO-1]", "fait": "CRP = 180 mg/L [norme < 5]", "signification": "inflammation sévère"}
|
||||
],
|
||||
"source_reglementaire": "[Guide Méthodologique MCO 2026 - p.45] citation",
|
||||
}
|
||||
],
|
||||
"confrontation_bio": [
|
||||
{"diagnostic": "K81.0", "test": "CRP", "valeur": 180, "seuil": "> 5", "verdict": "CONFIRMÉ"}
|
||||
],
|
||||
"asymetrie_information": "Bio non transmise à l'UCR",
|
||||
"reponse_points_cpam": "La CPAM a raison sur X, mais...",
|
||||
"codes_non_defendables": [],
|
||||
"references": [
|
||||
{"document": "Guide Méthodologique MCO 2026", "page": "45", "citation": "Le DAS doit..."}
|
||||
],
|
||||
"conclusion_dispositive": "Par conséquent, nous demandons le MAINTIEN du codage.",
|
||||
}
|
||||
text = _format_response(parsed)
|
||||
|
||||
assert "MÉMOIRE EN DÉFENSE" in text
|
||||
assert "RAPPEL DES FAITS" in text
|
||||
assert "MOYEN N°1" in text
|
||||
assert "K81.0" in text
|
||||
assert "Preuve" in text
|
||||
assert "Source" in text
|
||||
assert "CONFRONTATION BIOLOGIE" in text
|
||||
assert "CONFIRMÉ" in text
|
||||
assert "ASYMÉTRIE D'INFORMATION" in text
|
||||
assert "RÉPONSE AUX POINTS DE LA CPAM" in text
|
||||
assert "RÉFÉRENCES RÉGLEMENTAIRES" in text
|
||||
assert "CONCLUSION" in text
|
||||
assert "MAINTIEN" in text
|
||||
|
||||
def test_tim_format_codes_non_defendables(self):
|
||||
"""Les codes non défendables apparaissent dans le format TIM."""
|
||||
parsed = {
|
||||
"moyens_defense": [],
|
||||
"codes_non_defendables": [
|
||||
{"code": "D50.9", "raison": "Hb = 13.5, valeur NORMALE", "recommandation": "Retrait recommandé"}
|
||||
],
|
||||
"conclusion_dispositive": "Nous reconnaissons...",
|
||||
}
|
||||
text = _format_response(parsed)
|
||||
|
||||
assert "CODES NON DÉFENDABLES" in text
|
||||
assert "D50.9" in text
|
||||
assert "Retrait recommandé" in text
|
||||
|
||||
def test_tim_format_confrontation_table(self):
|
||||
"""Le tableau de confrontation bio est formaté en grille."""
|
||||
parsed = {
|
||||
"moyens_defense": [],
|
||||
"confrontation_bio": [
|
||||
{"diagnostic": "N17.8 IRA", "test": "Créatinine", "valeur": 280, "seuil": "> 130", "verdict": "CONFIRMÉ"},
|
||||
{"diagnostic": "E87.1 HypoNa", "test": "Sodium", "valeur": 138, "seuil": "< 135", "verdict": "NON CONFIRMÉ"},
|
||||
],
|
||||
"conclusion_dispositive": "Conclusion...",
|
||||
}
|
||||
text = _format_response(parsed)
|
||||
|
||||
assert "N17.8" in text
|
||||
assert "Créatinine" in text
|
||||
assert "CONFIRMÉ" in text
|
||||
assert "NON CONFIRMÉ" in text
|
||||
assert "┌" in text # table border
|
||||
|
||||
def test_tim_retrocompat_legacy_format(self):
|
||||
"""L'ancien format (sans moyens_defense) utilise le rendu legacy."""
|
||||
parsed = {
|
||||
"analyse_contestation": "Analyse...",
|
||||
"contre_arguments_medicaux": "Arguments médicaux...",
|
||||
"conclusion": "Conclusion...",
|
||||
}
|
||||
text = _format_response(parsed)
|
||||
|
||||
assert "CONTRE-ARGUMENTS MÉDICAUX" in text
|
||||
assert "MÉMOIRE EN DÉFENSE" not in text
|
||||
|
||||
|
||||
class TestValidateReferences:
|
||||
def test_valid_reference_no_warning(self):
|
||||
@@ -1061,6 +1162,81 @@ class TestCheckDasBioCoherence:
|
||||
assert "NORMAL" in prompt
|
||||
|
||||
|
||||
class TestBioConfrontation:
|
||||
"""Tests pour la confrontation biologie/diagnostic TIM."""
|
||||
|
||||
def test_confrontation_with_matching_bio(self):
|
||||
"""Code avec bio disponible et pathologique → CONFIRMÉ."""
|
||||
dossier = DossierMedical(
|
||||
source_file="test.pdf",
|
||||
sejour=Sejour(sexe="M", age=65),
|
||||
diagnostic_principal=Diagnostic(texte="IRA", cim10_suggestion="N17.8"),
|
||||
biologie_cle=[
|
||||
BiologieCle(test="Créatinine", valeur="280 µmol/L", anomalie=True),
|
||||
],
|
||||
)
|
||||
controle = ControleCPAM(
|
||||
numero_ogc=1, titre="Test", arg_ucr="Test",
|
||||
decision_ucr="Rejet", dp_ucr=None, da_ucr=None,
|
||||
)
|
||||
result = _build_bio_confrontation(dossier, controle)
|
||||
|
||||
assert "N17" in result
|
||||
assert "Créatinine" in result
|
||||
assert "280" in result
|
||||
assert "CONFIRMÉ" in result
|
||||
|
||||
def test_confrontation_normal_value(self):
|
||||
"""Code avec bio NORMALE → NON CONFIRMÉ."""
|
||||
dossier = DossierMedical(
|
||||
source_file="test.pdf",
|
||||
sejour=Sejour(sexe="F", age=70),
|
||||
diagnostic_principal=Diagnostic(texte="Hyponatrémie", cim10_suggestion="E87.1"),
|
||||
biologie_cle=[
|
||||
BiologieCle(test="Sodium", valeur="138 mmol/L", anomalie=False),
|
||||
],
|
||||
)
|
||||
controle = ControleCPAM(
|
||||
numero_ogc=1, titre="Test", arg_ucr="Test",
|
||||
decision_ucr="Rejet", dp_ucr=None, da_ucr=None,
|
||||
)
|
||||
result = _build_bio_confrontation(dossier, controle)
|
||||
|
||||
assert "E87.1" in result
|
||||
assert "NON CONFIRMÉ" in result
|
||||
|
||||
def test_confrontation_missing_bio(self):
|
||||
"""Code avec bio absente → NON DISPONIBLE."""
|
||||
dossier = DossierMedical(
|
||||
source_file="test.pdf",
|
||||
sejour=Sejour(sexe="M", age=50),
|
||||
diagnostic_principal=Diagnostic(texte="IRA", cim10_suggestion="N17.8"),
|
||||
biologie_cle=[],
|
||||
)
|
||||
controle = ControleCPAM(
|
||||
numero_ogc=1, titre="Test", arg_ucr="Test",
|
||||
decision_ucr="Rejet", dp_ucr=None, da_ucr=None,
|
||||
)
|
||||
result = _build_bio_confrontation(dossier, controle)
|
||||
|
||||
assert "NON DISPONIBLE" in result
|
||||
|
||||
def test_confrontation_no_threshold(self):
|
||||
"""Code sans seuil dans _BIO_THRESHOLDS → message par défaut."""
|
||||
dossier = DossierMedical(
|
||||
source_file="test.pdf",
|
||||
sejour=Sejour(),
|
||||
diagnostic_principal=Diagnostic(texte="Fracture", cim10_suggestion="S72.0"),
|
||||
)
|
||||
controle = ControleCPAM(
|
||||
numero_ogc=1, titre="Test", arg_ucr="Test",
|
||||
decision_ucr="Rejet", dp_ucr=None, da_ucr=None,
|
||||
)
|
||||
result = _build_bio_confrontation(dossier, controle)
|
||||
|
||||
assert "Aucun seuil" in result
|
||||
|
||||
|
||||
class TestPatientContext:
|
||||
"""Tests pour le contexte patient dans le prompt."""
|
||||
|
||||
@@ -1103,14 +1279,14 @@ class TestPatientContext:
|
||||
assert "ADMISSION EN URGENCE" in prompt
|
||||
|
||||
def test_context_consigne_in_prompt(self):
|
||||
"""Le prompt contient une consigne sur le contexte clinique."""
|
||||
"""Le prompt TIM contient les consignes sur le contexte patient."""
|
||||
dossier = _make_dossier()
|
||||
controle = _make_controle()
|
||||
prompt, _ = _build_cpam_prompt(dossier, controle, [])
|
||||
|
||||
assert "CONTEXTE CLINIQUE" in prompt
|
||||
assert "ÂGE" in prompt
|
||||
assert "MODE D'ENTRÉE" in prompt
|
||||
assert "CONTEXTE ADMINISTRATIF" in prompt
|
||||
assert "pédiatrie" in prompt.lower() or "Pédiatrie" in prompt
|
||||
assert "urgence" in prompt.lower()
|
||||
|
||||
|
||||
class TestExtractionPass:
|
||||
|
||||
Reference in New Issue
Block a user