feat: guardian déterministe + config modèles locaux + prompt TIM R1-R6

Guardian déterministe post-LLM (0 appel modèle, <1ms) :
- Corrige les valeurs bio hallucinées via confrontation dossier
- Step 1b : vérifie l'association test↔diagnostic via _BIO_THRESHOLDS
- Chemin bidirectionnel : CONFIRMÉ↔NON CONFIRMÉ selon bio réelle
- Force R3 : codes bio-infirmés → codes_non_defendables
- Step 2b : retire les codes bio-confirmés de codes_non_defendables
- Retire les moyens défendant des codes bio-contredits
- _safe_bio_replace() : regex protégeant les normes [X-Y]
- Nettoyage texte libre (conclusion, rappel, codes_nd, raisonnement)
- Score factuel déterministe avec pénalités

Config modèles pour déploiement local (DGX Spark) :
- CPAM : mistral-small3.2:24b (TIM complet, bonne précision bio)
- Validation : qwen3:32b (rapide, LOGIC-3 actif)
- Timeout : 120s → 600s pour modèles locaux

Ollama : migration /api/generate → /api/chat (messages format)

Prompt CPAM_ARGUMENTATION restructuré :
- R1-R6 non-négociables en tête (avant données)
- Champ raisonnement_interne (chain-of-thought structuré)
- 5 passes TIM avec références explicites aux règles

Test cpam_quality : métriques guardian dans le résumé

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dom
2026-03-04 22:00:40 +01:00
parent ce7a9650af
commit 798cee463f
7 changed files with 688 additions and 62 deletions

View File

@@ -53,17 +53,17 @@ NER_CONFIDENCE_THRESHOLD = float(os.environ.get("T2A_NER_THRESHOLD", "0.80"))
# --- Configuration Ollama ---
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "gemma3:27b-cloud")
OLLAMA_TIMEOUT = int(os.environ.get("OLLAMA_TIMEOUT", "120"))
OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "gemma3:27b")
OLLAMA_TIMEOUT = int(os.environ.get("OLLAMA_TIMEOUT", "600"))
OLLAMA_CACHE_PATH = BASE_DIR / "data" / "ollama_cache.json"
OLLAMA_MAX_PARALLEL = int(os.environ.get("OLLAMA_MAX_PARALLEL", "2"))
# --- Modèles par rôle LLM ---
OLLAMA_MODELS: dict[str, str] = {
"coding": os.environ.get("T2A_MODEL_CODING", "gemma3:27b-cloud"),
"cpam": os.environ.get("T2A_MODEL_CPAM", "gemma3:27b-cloud"),
"validation": os.environ.get("T2A_MODEL_VALIDATION", "deepseek-v3.2:cloud"),
"coding": os.environ.get("T2A_MODEL_CODING", "gemma3:27b"),
"cpam": os.environ.get("T2A_MODEL_CPAM", "mistral-small3.2:24b"),
"validation": os.environ.get("T2A_MODEL_VALIDATION", "qwen3:32b"),
"qc": os.environ.get("T2A_MODEL_QC", "gemma3:12b"),
}

View File

@@ -30,6 +30,7 @@ from .cpam_validation import (
_build_correction_prompt,
_format_response,
_assess_quality_tier,
_guardian_deterministic,
)
# Backward compat — sera retiré dans un commit futur
@@ -172,6 +173,9 @@ def generate_cpam_response(
logger.info(" CPAM : %d code(s) hors périmètre supprimé(s) : %s",
len(sanitized), ", ".join(sanitized))
# 6b. Gardien déterministe — corrige hallucinations bio, force R3
result = _guardian_deterministic(result, dossier, controle, tag_map)
# 7. Validation des références RAG
ref_warnings = _validate_references(result, sources)
if ref_warnings:
@@ -239,6 +243,7 @@ def generate_cpam_response(
result = corrected
validation = validation2
_sanitize_unauthorized_codes(result, dossier, controle)
result = _guardian_deterministic(result, dossier, controle, tag_map)
ref_warnings = _validate_references(result, sources)
grounding_warnings = _validate_grounding(result, tag_map)
code_warnings = _validate_codes_in_response(result, dossier, controle)

View File

@@ -603,6 +603,350 @@ def _is_new_tim_format(parsed: dict) -> bool:
return "moyens_defense" in parsed
# ---------------------------------------------------------------------------
# Gardien déterministe — validation/correction post-LLM sans appel modèle
# ---------------------------------------------------------------------------
def _guardian_deterministic(
result: dict,
dossier: DossierMedical,
controle: ControleCPAM,
tag_map: dict[str, str],
) -> dict:
"""Gardien déterministe post-LLM : corrige les hallucinations factuelles.
Opérations (0 appel LLM, < 1ms) :
1. Corrige les valeurs bio hallucinées dans confrontation_bio
2. Force la cohérence R3 (bio NON CONFIRMÉ → codes_non_defendables)
3. Retire des moyens_defense les codes bio-contredits
4. Vérifie les tags dans les preuves
5. Calcule un score factuel déterministe
Returns:
dict avec les corrections appliquées + champ "guardian_report" ajouté.
"""
from .cpam_context import _BIO_THRESHOLDS
if not _is_new_tim_format(result):
return result
report: dict = {
"bio_corrections": [],
"codes_moved_to_nd": [],
"preuves_invalid_tags": [],
"score_factuel": 10,
}
penalties = 0
# --- Indexer les valeurs bio réelles du dossier ---
bio_reelles: dict[str, float] = {}
for b in dossier.biologie_cle:
if b.test and b.valeur_num is not None:
bio_reelles[b.test] = b.valeur_num
elif b.test and b.valeur:
try:
bio_reelles[b.test] = float(b.valeur.replace(",", ".").split()[0])
except (ValueError, AttributeError):
pass
# ===== 1. Corriger confrontation_bio =====
confrontation = result.get("confrontation_bio", [])
codes_infirmes: set[str] = set() # codes dont la bio est normale
codes_confirmes: set[str] = set() # codes dont la bio est pathologique
if isinstance(confrontation, list):
for i, entry in enumerate(confrontation):
if not isinstance(entry, dict):
continue
test_name = str(entry.get("test", ""))
valeur_llm = entry.get("valeur")
diagnostic = str(entry.get("diagnostic", ""))
# --- 1b. Vérifier l'association test↔diagnostic via _BIO_THRESHOLDS ---
code_match = re.match(r"([A-Z]\d{2}(?:\.\d{1,2})?)", diagnostic)
if code_match:
diag_code = code_match.group(1)
# Chercher dans _BIO_THRESHOLDS (préfixe 3 ou 5 chars)
expected = (_BIO_THRESHOLDS.get(diag_code)
or _BIO_THRESHOLDS.get(diag_code[:3]))
if expected:
expected_test = expected["test"].lower()
if expected_test not in test_name.lower() and test_name.lower() not in expected_test:
# Le LLM a associé le mauvais test → corriger
old_test = test_name
entry["test"] = expected["test"]
test_name = expected["test"]
# Chercher la bonne valeur dans le dossier
for bio_key, bio_val in bio_reelles.items():
if expected_test in bio_key.lower() or bio_key.lower() in expected_test:
entry["valeur"] = bio_val
valeur_llm = bio_val
entry["seuil"] = expected.get("condition", "")
break
report["bio_corrections"].append({
"test": old_test,
"correction": f"mauvais test pour {diag_code} : {old_test}{expected['test']}",
"type": "wrong_test_mapping",
})
penalties += 2
logger.info(" Gardien : %s → test corrigé de '%s' vers '%s'",
diag_code, old_test, expected["test"])
# Chercher la valeur réelle dans le dossier
valeur_reelle = None
for bio_key, bio_val in bio_reelles.items():
if bio_key.lower() in test_name.lower() or test_name.lower() in bio_key.lower():
valeur_reelle = bio_val
break
if valeur_reelle is not None:
# Vérifier si le LLM a inventé une valeur différente
try:
v_llm = float(str(valeur_llm).replace(",", ".").split()[0]) if valeur_llm else None
except (ValueError, TypeError):
v_llm = None
if v_llm is not None and abs(v_llm - valeur_reelle) > 0.1:
report["bio_corrections"].append({
"test": test_name,
"valeur_llm": v_llm,
"valeur_reelle": valeur_reelle,
"diagnostic": diagnostic,
})
entry["valeur"] = valeur_reelle
penalties += 2
# Recalculer le verdict avec la valeur réelle
matched_bio_key = None
for bio_key in bio_reelles:
if bio_key.lower() in test_name.lower() or test_name.lower() in bio_key.lower():
matched_bio_key = bio_key
break
if matched_bio_key and matched_bio_key in BIO_NORMALS:
lo, hi = BIO_NORMALS[matched_bio_key]
is_normal = lo <= valeur_reelle <= hi
old_verdict = str(entry.get("verdict", "")).upper()
if is_normal and "CONFIRMÉ" in old_verdict and "NON" not in old_verdict:
# Verdict CONFIRMÉ mais valeur normale → correction
entry["verdict"] = "NON CONFIRMÉ — valeur NORMALE"
report["bio_corrections"].append({
"test": test_name,
"verdict_corrige": "CONFIRMÉ → NON CONFIRMÉ",
"valeur": valeur_reelle,
"norme": f"[{lo}-{hi}]",
})
penalties += 2
elif not is_normal and "NON" in old_verdict and "CONFIRMÉ" in old_verdict:
# Verdict NON CONFIRMÉ mais valeur pathologique → correction inverse
entry["verdict"] = "CONFIRMÉ — valeur PATHOLOGIQUE"
report["bio_corrections"].append({
"test": test_name,
"verdict_corrige": "NON CONFIRMÉ → CONFIRMÉ",
"valeur": valeur_reelle,
"norme": f"[{lo}-{hi}]",
})
penalties += 2
code_match_v = re.match(r"([A-Z]\d{2}(?:\.\d{1,2})?)", diagnostic)
if is_normal:
# Extraire le code CIM-10 du champ diagnostic
if code_match_v:
codes_infirmes.add(code_match_v.group(1))
elif not is_normal and code_match_v:
# Bio pathologique → ce code est CONFIRMÉ, pas infirmé
codes_confirmes.add(code_match_v.group(1))
# ===== 2. Force R3 : codes infirmés → codes_non_defendables =====
codes_nd = result.get("codes_non_defendables", [])
if not isinstance(codes_nd, list):
codes_nd = []
result["codes_non_defendables"] = codes_nd
codes_nd_existants = {
nd.get("code", "") for nd in codes_nd if isinstance(nd, dict)
}
for code_infirme in codes_infirmes:
if code_infirme not in codes_nd_existants:
# Chercher la valeur bio pour le message
bio_detail = ""
for bio_key, bio_val in bio_reelles.items():
if bio_key in BIO_NORMALS:
lo, hi = BIO_NORMALS[bio_key]
# Vérifier si ce test est lié au code via _BIO_THRESHOLDS
prefix = code_infirme[:3]
threshold = _BIO_THRESHOLDS.get(prefix) or _BIO_THRESHOLDS.get(code_infirme[:5] if len(code_infirme) >= 5 else code_infirme)
if threshold and bio_key.lower() in threshold.get("test", "").lower():
bio_detail = f"{bio_key} = {bio_val} [norme {lo}-{hi}] — valeur NORMALE"
break
is_valid, label = validate_code(normalize_code(code_infirme))
label_str = f" ({label})" if is_valid and label else ""
codes_nd.append({
"code": code_infirme,
"raison": f"{bio_detail or 'Bio normale'}, diagnostic non confirmé biologiquement",
"recommandation": "Retrait recommandé — code indéfendable (gardien déterministe)",
"_source": "guardian",
})
report["codes_moved_to_nd"].append(code_infirme)
penalties += 1
# ===== 2b. Retirer de codes_non_defendables les codes bio-CONFIRMÉS =====
if codes_confirmes and codes_nd:
codes_nd_before = len(codes_nd)
result["codes_non_defendables"] = [
nd for nd in codes_nd
if not isinstance(nd, dict)
or nd.get("code", "") not in codes_confirmes
]
codes_nd = result["codes_non_defendables"]
removed = codes_nd_before - len(codes_nd)
if removed:
report.setdefault("codes_rescued_from_nd", list(codes_confirmes))
logger.info(" Gardien : %d code(s) retirés de non-défendables (bio pathologique) : %s",
removed, ", ".join(codes_confirmes))
# ===== 3. Retirer des moyens_defense les codes bio-contredits =====
moyens = result.get("moyens_defense", [])
if isinstance(moyens, list) and codes_infirmes:
moyens_filtres = []
for moyen in moyens:
if not isinstance(moyen, dict):
moyens_filtres.append(moyen)
continue
titre = str(moyen.get("titre", "")).upper()
argument = str(moyen.get("argument", "")).upper()
moyen_text = titre + " " + argument
# Vérifier si ce moyen défend un code infirmé
defends_infirme = False
for code_inf in codes_infirmes:
if code_inf in moyen_text:
defends_infirme = True
logger.info(" Gardien : moyen retiré (défend %s, bio normale)", code_inf)
penalties += 1
break
if not defends_infirme:
moyens_filtres.append(moyen)
result["moyens_defense"] = moyens_filtres
# Renuméroter
for idx, moyen in enumerate(result["moyens_defense"]):
if isinstance(moyen, dict):
moyen["numero"] = idx + 1
# ===== 4. Vérifier les tags dans les preuves =====
if tag_map:
for moyen in result.get("moyens_defense", []):
if not isinstance(moyen, dict):
continue
for preuve in moyen.get("preuves", []):
if not isinstance(preuve, dict):
continue
ref = str(preuve.get("ref", ""))
# Extraire le tag (ex: [BIO-1] → BIO-1)
tag_match = re.findall(r"\[([A-Z]+-\d+)\]", ref)
for tag in tag_match:
if tag not in tag_map:
report["preuves_invalid_tags"].append(tag)
penalties += 0.5
# ===== 5. Nettoyage des champs texte libre =====
# Remplacer les valeurs bio hallucinées dans les strings (conclusion, rappel, etc.)
text_fields = [
"rappel_faits", "conclusion_dispositive", "reponse_points_cpam",
"asymetrie_information",
]
bio_replacements: list[tuple[str, str]] = [] # (pattern_llm, valeur_reelle)
for corr in report["bio_corrections"]:
v_llm = corr.get("valeur_llm")
v_reelle = corr.get("valeur_reelle")
if v_llm is not None and v_reelle is not None:
fmt_llm = str(int(v_llm)) if v_llm == int(v_llm) else str(v_llm)
fmt_reel = str(int(v_reelle)) if v_reelle == int(v_reelle) else str(v_reelle)
if fmt_llm != fmt_reel:
bio_replacements.append((fmt_llm, fmt_reel))
def _safe_bio_replace(text: str, old: str, new: str) -> str:
"""Remplace une valeur bio en évitant les contextes de norme [X-Y].
Ne remplace PAS si le nombre est précédé de '-' (borne haute d'une norme)
ou suivi de '-' sans espace (borne basse d'une norme).
Utilise des word boundaries pour plus de sécurité.
"""
# Pattern : old_value précédé de word boundary ou espace, pas de tiret collé
pattern = r"(?<![-\d])\b" + re.escape(old) + r"\b(?![-])"
return re.sub(pattern, new, text)
if bio_replacements:
n_replacements = 0
for field in text_fields:
val = result.get(field)
if isinstance(val, str):
for old, new in bio_replacements:
new_val = _safe_bio_replace(val, old, new)
if new_val != val:
n_replacements += 1
val = new_val
result[field] = val
# Aussi dans les arguments des moyens restants
for moyen in result.get("moyens_defense", []):
if isinstance(moyen, dict):
for key in ("titre", "argument"):
txt = moyen.get(key)
if isinstance(txt, str):
for old, new in bio_replacements:
txt = _safe_bio_replace(txt, old, new)
moyen[key] = txt
# Et dans raisonnement_interne
ri = result.get("raisonnement_interne")
if isinstance(ri, dict):
for k, v in ri.items():
if isinstance(v, str):
for old, new in bio_replacements:
v = _safe_bio_replace(v, old, new)
ri[k] = v
# Et dans codes_non_defendables (raison, recommandation)
for nd in result.get("codes_non_defendables", []):
if isinstance(nd, dict):
for key in ("raison", "recommandation"):
txt = nd.get(key)
if isinstance(txt, str):
for old, new in bio_replacements:
txt = _safe_bio_replace(txt, old, new)
nd[key] = txt
report["text_replacements"] = n_replacements
if n_replacements:
logger.info(" Gardien : %d remplacement(s) texte dans champs libres", n_replacements)
# ===== 6. Score factuel déterministe =====
report["score_factuel"] = max(0, round(10 - penalties))
result["guardian_report"] = report
# Log synthèse
n_bio = len(report["bio_corrections"])
n_moved = len(report["codes_moved_to_nd"])
n_tags = len(report["preuves_invalid_tags"])
if n_bio or n_moved or n_tags:
logger.warning(
" Gardien déterministe : %d correction(s) bio, %d code(s) → ND, %d tag(s) invalide(s), score factuel %d/10",
n_bio, n_moved, n_tags, report["score_factuel"],
)
else:
logger.info(" Gardien déterministe : aucune correction nécessaire, score factuel 10/10")
return result
def _format_response(
parsed: dict,
ref_warnings: list[str] | None = None,

View File

@@ -168,10 +168,10 @@ def call_ollama(
for attempt in range(3):
try:
response = requests.post(
f"{OLLAMA_URL}/api/generate",
f"{OLLAMA_URL}/api/chat",
json={
"model": use_model,
"prompt": prompt,
"messages": [{"role": "user", "content": prompt}],
"stream": False,
"format": "json",
"options": {
@@ -190,7 +190,7 @@ def call_ollama(
continue
response.raise_for_status()
resp_data = response.json()
raw = resp_data.get("response", "")
raw = resp_data.get("message", {}).get("content", "")
done_reason = resp_data.get("done_reason", "")
eval_count = resp_data.get("eval_count", 0)
if done_reason == "length":

View File

@@ -226,41 +226,38 @@ CPAM_ARGUMENTATION = """\
Tu es un médecin DIM senior expert en contentieux T2A. Tu rédiges un MÉMOIRE EN DÉFENSE \
structuré et argumenté pour répondre à la contestation CPAM ci-dessous.
Ta méthode suit les 5 passes de raisonnement expert TIM :
══════════════════════════════════════════════════════════════
RÈGLE ABSOLUE — HONNÊTETÉ INTELLECTUELLE (lire AVANT tout raisonnement)
══════════════════════════════════════════════════════════════
Ces règles sont NON NÉGOCIABLES et s'appliquent à CHAQUE code, CHAQUE argument :
PASSE 1 — CONTEXTE ADMINISTRATIF :
Analyse le contexte du séjour (âge, sexe, durée, mode d'entrée/sortie, actes) pour cadrer \
ton raisonnement. En pédiatrie (< 18 ans), les normes biologiques et codages diffèrent. \
Une admission en urgence implique un contexte aigu influençant le DP.
R1. BIOLOGIE NORMALE = CODE INDÉFENDABLE sur cet axe
Si une valeur biologique est dans les normes alors que le diagnostic l'exige pathologique
→ ce code va dans "codes_non_defendables", PAS dans "moyens_defense"
PASSE 2 — MOTIF D'HOSPITALISATION RÉEL :
Distingue le motif d'entrée déclaré du motif réel en te posant :
- Pourquoi CE patient a été hospitalisé CE JOUR (événement déclencheur)
- Quel acte thérapeutique principal a été réalisé
- Le DP retenu est-il cohérent avec cet acte et la durée de séjour
R2. ABSENCE DE PREUVE OBJECTIVE = SIGNAL OBLIGATOIRE
Si un diagnostic n'a aucune preuve (pas de bio, pas d'imagerie, pas d'acte CCAM)
→ tu DOIS écrire : "Ce diagnostic repose sur le seul jugement clinique du médecin,
sans preuve biologique ou paraclinique dans le dossier transmis"
PASSE 3 — CONFRONTATION BIOLOGIE / DIAGNOSTIC :
Pour chaque diagnostic contesté, confronte aux seuils biologiques :
{bio_confrontation_str}
- Une valeur normale CONTREDIT un diagnostic actif basé sur cette biologie
- Une valeur pathologique SANS diagnostic est un sous-codage potentiel
- CITE les seuils exacts et les valeurs du dossier
R3. COHÉRENCE CROISÉE OBLIGATOIRE
Tout code présent dans "confrontation_bio" avec verdict INFIRMÉ ou CONTREDIT
→ DOIT figurer dans "codes_non_defendables" — toute contradiction entre ces deux
champs est une erreur critique
PASSE 4 — HIÉRARCHIE DIAGNOSTIQUE :
- Le DP est le diagnostic qui a CONSOMMÉ LE PLUS DE RESSOURCES (pas le plus grave)
- Spécifique exclut générique (K81.0 présent → retirer K81.9)
- Codes R (symptômes) INTERDITS en DP si étiologie identifiée
- Chaque DAS doit répondre OUI à au moins une question :
1. Traitement spécifique pendant ce séjour ?
2. Allongement de la durée de séjour ?
3. Modification de la surveillance ou des examens ?
R4. CONCESSION CRÉDIBILISANTE
Si la CPAM a raison sur un point, le reconnaître explicitement dans
"reponse_points_cpam" — la crédibilité globale du mémoire en dépend
PASSE 5 — VALIDATION DÉFENSIVE (regard CPAM) :
Pour CHAQUE code défendu, tu DOIS répondre aux 4 questions :
1. Ce diagnostic est-il documenté EXPLICITEMENT dans le dossier, ou DÉDUIT ?
2. Y a-t-il une preuve OBJECTIVE (valeur bio, imagerie, acte CCAM) ?
3. Le code est-il COHÉRENT avec la durée de séjour et les actes réalisés ?
4. Quel DOCUMENT du dossier cite-t-on en premier face à la CPAM ?
R5. PRINCIPE TIM
"Mieux vaut un code moins précis mais défendable qu'un code précis mais indéfendable"
Ne jamais forcer un argument que le dossier ne soutient pas objectivement
R6. ZÉRO INVENTION
N'invente AUCUN tag, valeur biologique, code ou source absents du dossier fourni
══════════════════════════════════════════════════════════════
DONNÉES DU DOSSIER :
DOSSIER MÉDICAL : {dossier_str}
{asymetrie_str}
@@ -278,22 +275,52 @@ CODES EN JEU : {codes_str}
SOURCES RÉGLEMENTAIRES : {sources_text}
{extraction_str}
RÈGLE ABSOLUE — HONNÊTETÉ INTELLECTUELLE :
Un mémoire crédible ne force JAMAIS un argument que le dossier ne soutient pas.
- Si une valeur biologique est NORMALE alors que le diagnostic l'exige pathologique → \
tu DOIS le signaler et NE PAS défendre ce code sur cet axe
- Si un diagnostic n'a AUCUNE preuve objective (pas de bio, pas d'imagerie, pas d'acte) → \
tu écris : "Ce diagnostic repose sur le seul jugement clinique, sans preuve biologique ou \
paraclinique dans le dossier"
- Si la confrontation bio CONTREDIT un diagnostic → tu NE LE DÉFENDS PAS et tu le signales \
dans le champ "codes_non_defendables"
- Si la CPAM a RAISON sur un point → tu le reconnais clairement. Mieux vaut concéder un \
point indéfendable et gagner en crédibilité sur les points solides
- Principe TIM : "Mieux vaut un code moins précis mais défendable qu'un code précis mais \
indéfendable"
SEUILS BIOLOGIQUES DE RÉFÉRENCE :
{bio_confrontation_str}
CONSIGNES DE RÉDACTION :
══════════════════════════════════════════════════════════════
MÉTHODE DE RAISONNEMENT — 5 PASSES TIM
══════════════════════════════════════════════════════════════
Effectue ces 5 passes MENTALEMENT avant de rédiger le JSON.
Externalise ton raisonnement dans le champ "raisonnement_interne" du JSON.
PASSE 1 — CONTEXTE ADMINISTRATIF :
Analyse le contexte du séjour (âge, sexe, durée, mode d'entrée/sortie, actes).
En pédiatrie (< 18 ans), les normes biologiques et codages diffèrent.
Une admission en urgence implique un contexte aigu influençant le DP.
PASSE 2 — MOTIF D'HOSPITALISATION RÉEL :
- Pourquoi CE patient a été hospitalisé CE JOUR (événement déclencheur)
- Quel acte thérapeutique principal a été réalisé
- Le DP retenu est-il cohérent avec cet acte et la durée de séjour
PASSE 3 — CONFRONTATION BIOLOGIE / DIAGNOSTIC (appliquer R1 et R3) :
Pour CHAQUE diagnostic contesté, comparer aux seuils ci-dessus.
DÉCISION BINAIRE pour chaque code :
→ valeur pathologique confirmée : le code est DÉFENDABLE sur cet axe
→ valeur normale ou absente : le code va dans codes_non_defendables (R1)
Citer les seuils exacts et les valeurs du dossier — jamais de valeur inventée (R6)
PASSE 4 — HIÉRARCHIE DIAGNOSTIQUE :
- Le DP est le diagnostic qui a CONSOMMÉ LE PLUS DE RESSOURCES (pas le plus grave)
- Spécifique exclut générique (K81.0 présent → retirer K81.9)
- Codes R (symptômes) INTERDITS en DP si étiologie identifiée
- Chaque DAS doit répondre OUI à au moins une :
1. Traitement spécifique pendant ce séjour ?
2. Allongement de la durée de séjour ?
3. Modification de la surveillance ou des examens ?
PASSE 5 — VALIDATION DÉFENSIVE (regard CPAM) :
Pour CHAQUE code que tu envisages de défendre, répondre aux 4 questions :
1. Ce diagnostic est-il documenté EXPLICITEMENT dans le dossier, ou DÉDUIT ?
2. Y a-t-il une preuve OBJECTIVE (valeur bio, imagerie, acte CCAM) ?
3. Le code est-il COHÉRENT avec la durée de séjour et les actes réalisés ?
4. Quel DOCUMENT du dossier cite-t-on en premier face à la CPAM ?
Si une réponse est NON ou DÉDUIT → appliquer R2, ne pas construire de moyen sur cet axe
══════════════════════════════════════════════════════════════
CONSIGNES DE RÉDACTION DES MOYENS
══════════════════════════════════════════════════════════════
1. STRUCTURE EN MOYENS DE DÉFENSE NUMÉROTÉS (pas de prose libre)
2. Chaque moyen = un argument autonome avec sa preuve FORMELLEMENT DOCUMENTÉE dans le dossier
3. CITE les codes CIM-10 avec libellé complet (ex: N17.8 — Autre insuffisance rénale aiguë)
@@ -301,14 +328,19 @@ CONSIGNES DE RÉDACTION :
5. CITE les sources réglementaires au format [Document - page N] "citation verbatim"
6. JAMAIS d'argument sans preuve traçable — si tu n'as pas la preuve, NE FAIS PAS l'argument
7. Ton ASSERTIF mais factuel — pas de formules creuses ("il convient de noter que...")
8. Si un point CPAM est légitime, le reconnaître CLAIREMENT — la crédibilité globale en dépend
9. N'invente AUCUN tag, code ou source qui n'est pas fourni ci-dessus
10. NE JAMAIS qualifier une valeur NORMALE comme pathologique ni extrapoler au-delà des faits
11. Tags valides : [DP], [DAS-N], [BIO-N], [IMG-N], [TRT-N], [ACTE-N], [ANT-N], [COMPL-N]
8. Si un point CPAM est légitime, le reconnaître CLAIREMENT (R4)
9. Tags valides UNIQUEMENT : [DP], [DAS-N], [BIO-N], [IMG-N], [TRT-N], [ACTE-N], [ANT-N], [COMPL-N]
Réponds UNIQUEMENT avec un objet JSON :
{{
"objet": "Contestation {titre} — OGC {numero_ogc} — Mémoire en défense",
"raisonnement_interne": {{
"passe1_contexte": "synthèse de la passe 1 : profil patient, durée séjour, mode entrée/sortie, actes clés",
"passe2_motif_reel": "événement déclencheur identifié, cohérence DP/actes/durée",
"passe3_bio": "pour chaque code contesté : valeur bio vs seuil → DÉFENDABLE ou NON DÉFENDABLE",
"passe4_hierarchie": "validation hiérarchie DP/DAS, exclusions symptômes, critères ressources",
"passe5_validation": "liste des codes retenus pour défense avec justification des 4 questions, liste des codes écartés avec raison"
}},
"rappel_faits": "Résumé factuel du séjour en 3-5 lignes : motif, actes, durée, issue",
"moyens_defense": [
{{
@@ -325,7 +357,7 @@ Réponds UNIQUEMENT avec un objet JSON :
{{"diagnostic": "N17.8 IRA", "test": "Créatinine", "valeur": 280, "seuil": "> 130 µmol/L", "verdict": "CONFIRMÉ"}}
],
"asymetrie_information": "Éléments cliniques que la CPAM n'avait PAS (bio, imagerie, actes) — brièvement",
"reponse_points_cpam": "Pour chaque point légitime de la CPAM : reconnaissance CLAIRE + réfutation factuelle OU concession si indéfendable",
"reponse_points_cpam": "Pour chaque point légitime de la CPAM : reconnaissance CLAIRE + réfutation factuelle OU concession explicite si indéfendable",
"codes_non_defendables": [
{{"code": "D50.9", "raison": "Hb = 13.5 g/dL [norme > 12 F] — valeur NORMALE, anémie non confirmée biologiquement", "recommandation": "Retrait recommandé — code indéfendable face à la CPAM"}}
],