feat: architecture multi-modèles LLM + quality engine + benchmark
- Multi-modèles : 4 rôles LLM (coding=gemma3:27b-cloud, cpam=gemma3:27b-cloud, validation=deepseek-v3.2:cloud, qc=gemma3:12b) avec get_model(role) - Prompts externalisés : 7 templates dans src/prompts/templates.py - Cache Ollama : modèle stocké par entrée (migration auto ancien format) - call_ollama() : paramètre role= (priorité: model > role > global) - Quality engine : veto_engine + decision_engine + rules_router (YAML) - Benchmark qualité : scripts/benchmark_quality.py (A/B, métriques CIM-10) - Fix biologie : valeurs qualitatives (troponine négative) non filtrées - Fix CPAM : gemma3:27b-cloud au lieu de deepseek (JSON tronqué par thinking) - CPAM max_tokens 4000→6000, viewer admin multi-modèles - Benchmark 10 dossiers : 100% DAS valides, 10/10 CPAM, 243s/dossier Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ from ..config import ControleCPAM, DossierMedical, RAGSource
|
||||
from ..medical.cim10_dict import normalize_code, validate_code
|
||||
from ..medical.cim10_extractor import BIO_NORMALS
|
||||
from ..medical.ollama_client import call_anthropic, call_ollama
|
||||
from ..prompts import CPAM_EXTRACTION, CPAM_ARGUMENTATION, CPAM_ADVERSARIAL
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -602,88 +603,18 @@ def _build_cpam_prompt(
|
||||
+ "\n".join(ext_lines)
|
||||
)
|
||||
|
||||
prompt = f"""Tu es un médecin DIM (Département d'Information Médicale) expert en contentieux T2A.
|
||||
Tu dois produire une analyse ÉQUILIBRÉE ET CRÉDIBLE de la contestation CPAM, puis contre-argumenter en mobilisant trois axes : médical, asymétrie d'information, et réglementaire.
|
||||
|
||||
IMPORTANT — CRÉDIBILITÉ DE L'ANALYSE :
|
||||
Une contre-argumentation crédible reconnaît TOUJOURS au moins un point valide dans le raisonnement adverse.
|
||||
Répondre "Aucun point d'accord" décrédibilise l'ensemble de l'argumentation. Tu DOIS identifier au moins un élément où la CPAM a un point légitime (même partiel), puis expliquer pourquoi cela ne suffit pas à invalider le codage.
|
||||
|
||||
IMPORTANT — CODES CIM-10 :
|
||||
Ne parle JAMAIS de « codage initial » ou « codage contesté » sans citer explicitement le code CIM-10 et son libellé (ex: Z45.80 — Ajustement et entretien d'un dispositif implantable).
|
||||
Chaque argument doit désigner précisément quel code est défendu ou contesté, avec son libellé complet.
|
||||
|
||||
DOSSIER MÉDICAL DE L'ÉTABLISSEMENT :
|
||||
{dossier_str}
|
||||
{asymetrie_str}
|
||||
{tagged_str}
|
||||
|
||||
OBJET DU DÉSACCORD : {controle.titre}
|
||||
|
||||
ARGUMENTATION DE LA CPAM (UCR) :
|
||||
{controle.arg_ucr}
|
||||
|
||||
DÉCISION UCR : {controle.decision_ucr}
|
||||
|
||||
CODES CONTESTÉS :
|
||||
{codes_str}
|
||||
{definitions_str}
|
||||
|
||||
SOURCES RÉGLEMENTAIRES (Guide méthodologique, CIM-10) :
|
||||
{sources_text}
|
||||
{extraction_str}
|
||||
|
||||
CONSIGNES :
|
||||
|
||||
CONTEXTE CLINIQUE :
|
||||
- Prends en compte l'ÂGE du patient (pédiatrie < 18 ans, personne âgée >= 80 ans), le MODE D'ENTRÉE (urgence vs programmé), et la DURÉE DE SÉJOUR pour contextualiser ton analyse
|
||||
- En pédiatrie, les normes biologiques et les codages peuvent différer de l'adulte
|
||||
- Une admission en urgence implique un contexte clinique aigu qui influence le choix du DP
|
||||
|
||||
ÉTAPE 1 — ANALYSE HONNÊTE (avant de contre-argumenter) :
|
||||
- Identifie ce que la CPAM a compris correctement dans le dossier
|
||||
- Reconnais les points où leur raisonnement est fondé, même partiellement
|
||||
- Explique ENSUITE pourquoi ces points ne justifient pas leur conclusion
|
||||
|
||||
AXE MÉDICAL :
|
||||
- Analyse le bien-fondé médical du codage de l'établissement
|
||||
- CITE les éléments cliniques EXACTS du dossier en utilisant les tags [XX-N] fournis (ex: [BIO-1] CRP 180 mg/L)
|
||||
- Confronte l'argumentation CPAM aux sources CIM-10 et Guide Méthodologique fournies
|
||||
- Ne mentionne AUCUN élément qui ne figure pas dans les éléments référencés ci-dessus
|
||||
|
||||
AXE ASYMÉTRIE D'INFORMATION :
|
||||
- La CPAM a fondé son analyse uniquement sur le CRH et les codes transmis
|
||||
- Pour CHAQUE élément clinique pertinent, cite les VALEURS EXACTES et explique leur signification clinique
|
||||
- Démontre en quoi ces éléments complémentaires (biologie, imagerie, traitements, actes) justifient le codage contesté
|
||||
- Ne mentionne AUCUN élément qui n'est pas dans le dossier fourni
|
||||
|
||||
MISE EN FORME :
|
||||
- Structure chaque section avec des tirets pour lister les arguments distincts
|
||||
- Un argument par puce, avec la preuve ou la référence associée
|
||||
|
||||
AXE RÉGLEMENTAIRE :
|
||||
- Identifie si l'UCR fait une interprétation restrictive non fondée d'une règle
|
||||
- Confronte le raisonnement CPAM au texte EXACT des sources fournies
|
||||
- Format OBLIGATOIRE pour chaque référence : [Document - page N] suivi d'une CITATION VERBATIM du passage pertinent
|
||||
- INTERDICTION ABSOLUE de citer une référence qui ne figure pas dans les sources fournies ci-dessus
|
||||
- Si aucune source pertinente n'est disponible → écrire explicitement "Pas de source réglementaire disponible"
|
||||
- Relève les contradictions entre l'argumentation CPAM et les règles officielles
|
||||
|
||||
Réponds UNIQUEMENT avec un objet JSON au format suivant :
|
||||
{{
|
||||
"analyse_contestation": "Résumé de ce que conteste la CPAM et sur quelle base",
|
||||
"points_accord": "Points CONCRETS où la CPAM a raison ou partiellement raison (JAMAIS 'Aucun' — il y a toujours au moins un point légitime à reconnaître)",
|
||||
"contre_arguments_medicaux": "Argumentation médicale en faveur du codage, en expliquant pourquoi les points d'accord ne suffisent pas à invalider le codage",
|
||||
"preuves_dossier": [
|
||||
{{"ref": "BIO-1", "element": "biologie|imagerie|traitement|acte|clinique", "valeur": "valeur exacte du dossier", "signification": "explication clinique"}}
|
||||
],
|
||||
"contre_arguments_asymetrie": "Éléments cliniques que la CPAM n'avait pas et qui justifient le codage",
|
||||
"contre_arguments_reglementaires": "Erreurs d'interprétation réglementaire de la CPAM, avec citations verbatim des sources",
|
||||
"references": [
|
||||
{{"document": "nom du document source", "page": "numéro de page", "citation": "citation verbatim du passage"}}
|
||||
],
|
||||
"conclusion": "Synthèse en citant EXPLICITEMENT les codes CIM-10 défendus (ex: DP Z45.80 — libellé) : points reconnus à la CPAM, puis pourquoi ce codage précis est néanmoins justifié"
|
||||
}}"""
|
||||
prompt = CPAM_ARGUMENTATION.format(
|
||||
dossier_str=dossier_str,
|
||||
asymetrie_str=asymetrie_str,
|
||||
tagged_str=tagged_str,
|
||||
titre=controle.titre,
|
||||
arg_ucr=controle.arg_ucr,
|
||||
decision_ucr=controle.decision_ucr,
|
||||
codes_str=codes_str,
|
||||
definitions_str=definitions_str,
|
||||
sources_text=sources_text,
|
||||
extraction_str=extraction_str,
|
||||
)
|
||||
return prompt, tag_map
|
||||
|
||||
|
||||
@@ -845,35 +776,19 @@ def _validate_adversarial(
|
||||
normes_lines.append(f" {test}: {lo}-{hi}")
|
||||
normes_section = "NORMES BIOLOGIQUES DE RÉFÉRENCE :\n" + "\n".join(normes_lines)
|
||||
|
||||
prompt = f"""Tu es un relecteur critique. Vérifie la cohérence de cette contre-argumentation CPAM.
|
||||
dp_ucr_line = f"DP UCR : {controle.dp_ucr}" if controle.dp_ucr else ""
|
||||
da_ucr_line = f"DA UCR : {controle.da_ucr}" if controle.da_ucr else ""
|
||||
|
||||
RÉPONSE GÉNÉRÉE :
|
||||
{response_json}
|
||||
|
||||
{factual_section}
|
||||
|
||||
{normes_section}
|
||||
|
||||
CODES CONTESTÉS :
|
||||
{f"DP UCR : {controle.dp_ucr}" if controle.dp_ucr else ""}
|
||||
{f"DA UCR : {controle.da_ucr}" if controle.da_ucr else ""}
|
||||
|
||||
Vérifie STRICTEMENT :
|
||||
1. Chaque valeur bio/imagerie/traitement citée dans les preuves existe dans les éléments factuels
|
||||
2. Si une valeur bio est qualifiée de "élevée", "basse" ou "anormale", vérifie qu'elle est RÉELLEMENT hors normes selon les normes ci-dessus (ex: CRP 5 = NORMAL, pas élevé)
|
||||
3. La conclusion est cohérente avec l'argumentation développée
|
||||
4. Les points d'accord ne contredisent pas les contre-arguments
|
||||
5. Les codes CIM-10 mentionnés dans la conclusion sont cohérents avec le reste
|
||||
|
||||
Réponds UNIQUEMENT en JSON :
|
||||
{{
|
||||
"coherent": true ou false,
|
||||
"erreurs": ["description précise de chaque incohérence trouvée"],
|
||||
"score_confiance": 0 à 10
|
||||
}}"""
|
||||
prompt = CPAM_ADVERSARIAL.format(
|
||||
response_json=response_json,
|
||||
factual_section=factual_section,
|
||||
normes_section=normes_section,
|
||||
dp_ucr_line=dp_ucr_line,
|
||||
da_ucr_line=da_ucr_line,
|
||||
)
|
||||
|
||||
logger.debug(" Validation adversariale")
|
||||
result = call_ollama(prompt, temperature=0.0, max_tokens=800)
|
||||
result = call_ollama(prompt, temperature=0.0, max_tokens=800, role="validation")
|
||||
if result is None:
|
||||
result = call_anthropic(prompt, temperature=0.0, max_tokens=800)
|
||||
if result is None:
|
||||
@@ -924,36 +839,22 @@ def _extraction_pass(
|
||||
# Contexte tagué (réutilise la même fonction)
|
||||
tagged_text, _ = _build_tagged_context(dossier)
|
||||
|
||||
prompt = f"""Tu es un médecin DIM expert. Analyse cette contestation CPAM sans argumenter.
|
||||
dp_ucr_line = f"DP proposé UCR : {controle.dp_ucr}" if controle.dp_ucr else ""
|
||||
da_ucr_line = f"DA proposés UCR : {controle.da_ucr}" if controle.da_ucr else ""
|
||||
|
||||
DOSSIER :
|
||||
- DP : {dp_str or "Non extrait"}
|
||||
- DAS : {das_str or "Aucun"}
|
||||
{tagged_text}
|
||||
|
||||
CONTESTATION CPAM :
|
||||
Titre : {controle.titre}
|
||||
Argument : {controle.arg_ucr}
|
||||
Décision : {controle.decision_ucr}
|
||||
{f"DP proposé UCR : {controle.dp_ucr}" if controle.dp_ucr else ""}
|
||||
{f"DA proposés UCR : {controle.da_ucr}" if controle.da_ucr else ""}
|
||||
|
||||
Réponds UNIQUEMENT en JSON :
|
||||
{{
|
||||
"comprehension_contestation": "Résumé factuel : que conteste la CPAM et pourquoi",
|
||||
"elements_cliniques_pertinents": [
|
||||
{{"tag": "BIO-1 ou texte libre", "pertinence": "en quoi cet élément est pertinent pour le codage contesté"}}
|
||||
],
|
||||
"points_accord_potentiels": ["points où la CPAM a partiellement raison"],
|
||||
"codes_en_jeu": {{
|
||||
"dp_etablissement": "code + libellé",
|
||||
"dp_ucr": "code + libellé si proposé",
|
||||
"difference_cle": "explication de la différence entre les deux codages"
|
||||
}}
|
||||
}}"""
|
||||
prompt = CPAM_EXTRACTION.format(
|
||||
dp_str=dp_str or "Non extrait",
|
||||
das_str=das_str or "Aucun",
|
||||
tagged_text=tagged_text,
|
||||
titre=controle.titre,
|
||||
arg_ucr=controle.arg_ucr,
|
||||
decision_ucr=controle.decision_ucr,
|
||||
dp_ucr_line=dp_ucr_line,
|
||||
da_ucr_line=da_ucr_line,
|
||||
)
|
||||
|
||||
logger.debug(" Passe 1 — extraction structurée")
|
||||
result = call_ollama(prompt, temperature=0.0, max_tokens=1500)
|
||||
result = call_ollama(prompt, temperature=0.0, max_tokens=1500, role="cpam")
|
||||
if result is None:
|
||||
result = call_anthropic(prompt, temperature=0.0, max_tokens=1500)
|
||||
if result is not None:
|
||||
@@ -990,13 +891,13 @@ def generate_cpam_response(
|
||||
# 3. Construction du prompt (passe 2 — argumentation)
|
||||
prompt, tag_map = _build_cpam_prompt(dossier, controle, sources, extraction)
|
||||
|
||||
# 4. Appel LLM — Ollama (modèle par défaut) > Haiku fallback
|
||||
result = call_ollama(prompt, temperature=0.1, max_tokens=4000)
|
||||
# 4. Appel LLM — Ollama (rôle cpam) > Haiku fallback
|
||||
result = call_ollama(prompt, temperature=0.1, max_tokens=6000, role="cpam")
|
||||
if result is not None:
|
||||
logger.info(" Contre-argumentation via Ollama")
|
||||
else:
|
||||
logger.info(" Ollama indisponible → fallback Anthropic Haiku")
|
||||
result = call_anthropic(prompt, temperature=0.1, max_tokens=4000)
|
||||
result = call_anthropic(prompt, temperature=0.1, max_tokens=6000)
|
||||
if result is not None:
|
||||
logger.info(" Contre-argumentation via Anthropic Haiku")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user