feat: mode hybride Ollama — gemma3:27b pour CPAM, 12b pour codage
Le pipeline utilise désormais gemma3:12b (rapide) pour le codage CIM-10 et gemma3:27b (meilleur raisonnement) pour la contre-argumentation CPAM. Configurable via OLLAMA_MODEL_CPAM et OLLAMA_TIMEOUT_CPAM. Inclut aussi : traçabilité source/page DAS, niveaux CMA ATIH, sévérité, page tracker PDF, améliorations fusion et filtres DAS. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,8 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from ..config import ControleCPAM, DossierMedical, RAGSource
|
||||
from ..medical.ollama_client import call_ollama
|
||||
from ..config import ControleCPAM, DossierMedical, RAGSource, OLLAMA_MODEL_CPAM, OLLAMA_TIMEOUT_CPAM
|
||||
from ..medical.ollama_client import call_anthropic, call_ollama
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -244,33 +244,84 @@ CONSIGNES :
|
||||
|
||||
AXE MÉDICAL :
|
||||
- Analyse le bien-fondé médical du codage de l'établissement
|
||||
- CITE les éléments cliniques EXACTS du dossier : valeurs bio précises (ex: CRP 180 mg/L), résultats imagerie verbatim, traitements avec molécules et posologies
|
||||
- Confronte l'argumentation CPAM aux sources CIM-10 et Guide Méthodologique fournies
|
||||
- Identifie les points où la CPAM a éventuellement raison
|
||||
- Ne mentionne que les éléments réellement présents dans le dossier fourni
|
||||
|
||||
AXE ASYMÉTRIE D'INFORMATION :
|
||||
- La CPAM a fondé son analyse uniquement sur le CRH et les codes transmis
|
||||
- Démontre en quoi les éléments cliniques complémentaires (biologie, imagerie, traitements, actes) justifient le codage contesté
|
||||
- Pour chaque élément clinique pertinent, explique pourquoi il invalide ou nuance l'argumentation CPAM
|
||||
- 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
|
||||
|
||||
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
|
||||
- NE CITE AUCUNE référence qui ne figure pas dans les sources fournies
|
||||
|
||||
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 où la CPAM a raison (ou 'Aucun')",
|
||||
"contre_arguments_medicaux": "Argumentation médicale en faveur du codage",
|
||||
"preuves_dossier": [
|
||||
{{"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 des sources",
|
||||
"references": "Références EXACTES tirées des sources fournies (document, page, code)",
|
||||
"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 et position recommandée"
|
||||
}}"""
|
||||
|
||||
|
||||
def _format_response(parsed: dict) -> str:
|
||||
def _validate_references(parsed: dict, sources: list[dict]) -> list[str]:
|
||||
"""Vérifie que les références citées correspondent aux sources RAG fournies.
|
||||
|
||||
Returns:
|
||||
Liste d'avertissements pour les références non vérifiables.
|
||||
"""
|
||||
warnings = []
|
||||
refs = parsed.get("references")
|
||||
if not refs or not isinstance(refs, list):
|
||||
return warnings
|
||||
|
||||
# Construire un set des documents sources disponibles
|
||||
source_docs = set()
|
||||
for src in sources:
|
||||
doc_name = src.get("document", "")
|
||||
source_docs.add(doc_name)
|
||||
# Ajouter les noms lisibles aussi
|
||||
readable = {
|
||||
"cim10": "CIM-10 FR 2026",
|
||||
"cim10_alpha": "CIM-10 Index Alphabétique 2026",
|
||||
"guide_methodo": "Guide Méthodologique MCO 2026",
|
||||
"ccam": "CCAM PMSI V4 2025",
|
||||
}.get(doc_name, "")
|
||||
if readable:
|
||||
source_docs.add(readable)
|
||||
source_docs.add(readable.lower())
|
||||
|
||||
if not source_docs:
|
||||
return warnings
|
||||
|
||||
for ref in refs:
|
||||
if not isinstance(ref, dict):
|
||||
continue
|
||||
doc = ref.get("document", "")
|
||||
if doc and not any(sd in doc.lower() or doc.lower() in sd.lower() for sd in source_docs if sd):
|
||||
warnings.append(f"Référence non vérifiable : {doc}")
|
||||
logger.warning("CPAM : référence non vérifiable « %s »", doc)
|
||||
|
||||
return warnings
|
||||
|
||||
|
||||
def _format_response(parsed: dict, ref_warnings: list[str] | None = None) -> str:
|
||||
"""Formate la réponse LLM en texte lisible."""
|
||||
sections = []
|
||||
|
||||
@@ -287,6 +338,19 @@ def _format_response(parsed: dict) -> str:
|
||||
if contre_med:
|
||||
sections.append(f"CONTRE-ARGUMENTS MÉDICAUX\n{contre_med}")
|
||||
|
||||
# Preuves du dossier (nouveau champ structuré)
|
||||
preuves = parsed.get("preuves_dossier")
|
||||
if preuves and isinstance(preuves, list):
|
||||
preuves_lines = []
|
||||
for p in preuves:
|
||||
if isinstance(p, dict):
|
||||
elem = p.get("element", "")
|
||||
valeur = p.get("valeur", "")
|
||||
signif = p.get("signification", "")
|
||||
preuves_lines.append(f"- [{elem}] {valeur} → {signif}")
|
||||
if preuves_lines:
|
||||
sections.append(f"PREUVES DU DOSSIER\n" + "\n".join(preuves_lines))
|
||||
|
||||
contre_asym = parsed.get("contre_arguments_asymetrie")
|
||||
if contre_asym:
|
||||
sections.append(f"ASYMÉTRIE D'INFORMATION\n{contre_asym}")
|
||||
@@ -301,14 +365,33 @@ def _format_response(parsed: dict) -> str:
|
||||
if contre:
|
||||
sections.append(f"CONTRE-ARGUMENTS\n{contre}")
|
||||
|
||||
# Références structurées (nouveau format liste) ou ancien format string
|
||||
refs = parsed.get("references")
|
||||
if refs:
|
||||
sections.append(f"REFERENCES\n{refs}")
|
||||
if isinstance(refs, list):
|
||||
ref_lines = []
|
||||
for r in refs:
|
||||
if isinstance(r, dict):
|
||||
doc = r.get("document", "")
|
||||
page = r.get("page", "")
|
||||
citation = r.get("citation", "")
|
||||
ref_lines.append(f"- [{doc}, p.{page}] {citation}")
|
||||
else:
|
||||
ref_lines.append(f"- {r}")
|
||||
if ref_lines:
|
||||
sections.append(f"REFERENCES\n" + "\n".join(ref_lines))
|
||||
else:
|
||||
sections.append(f"REFERENCES\n{refs}")
|
||||
|
||||
conclusion = parsed.get("conclusion")
|
||||
if conclusion:
|
||||
sections.append(f"CONCLUSION\n{conclusion}")
|
||||
|
||||
# Avertissements sur les références non vérifiables
|
||||
if ref_warnings:
|
||||
warning_text = "\n".join(f"- {w}" for w in ref_warnings)
|
||||
sections.append(f"AVERTISSEMENT — REFERENCES NON VÉRIFIÉES\n{warning_text}")
|
||||
|
||||
return "\n\n".join(sections)
|
||||
|
||||
|
||||
@@ -335,8 +418,24 @@ def generate_cpam_response(
|
||||
# 2. Construction du prompt
|
||||
prompt = _build_cpam_prompt(dossier, controle, sources)
|
||||
|
||||
# 3. Appel Ollama
|
||||
result = call_ollama(prompt, temperature=0.1, max_tokens=3000)
|
||||
# 3. Appel LLM — Mode hybride : Ollama CPAM (27b) > Haiku > Ollama défaut
|
||||
result = None
|
||||
if OLLAMA_MODEL_CPAM:
|
||||
logger.info(" Contre-argumentation via Ollama %s (mode hybride)", OLLAMA_MODEL_CPAM)
|
||||
result = call_ollama(
|
||||
prompt, temperature=0.1, max_tokens=4000,
|
||||
model=OLLAMA_MODEL_CPAM, timeout=OLLAMA_TIMEOUT_CPAM,
|
||||
)
|
||||
if result is not None:
|
||||
logger.info(" Contre-argumentation via Ollama %s", OLLAMA_MODEL_CPAM)
|
||||
else:
|
||||
logger.info(" Ollama CPAM indisponible → fallback Anthropic Haiku")
|
||||
result = call_anthropic(prompt, temperature=0.1, max_tokens=4000)
|
||||
if result is not None:
|
||||
logger.info(" Contre-argumentation via Anthropic Haiku")
|
||||
else:
|
||||
logger.info(" Haiku indisponible → fallback Ollama défaut")
|
||||
result = call_ollama(prompt, temperature=0.1, max_tokens=3000)
|
||||
|
||||
# 4. Conversion des sources RAG
|
||||
rag_sources = [
|
||||
@@ -350,11 +449,16 @@ def generate_cpam_response(
|
||||
]
|
||||
|
||||
if result is None:
|
||||
logger.warning(" Ollama non disponible — contre-argumentation non générée")
|
||||
logger.warning(" LLM non disponible — contre-argumentation non générée")
|
||||
return "", rag_sources
|
||||
|
||||
# 5. Formater la réponse
|
||||
text = _format_response(result)
|
||||
# 5. Validation des références
|
||||
ref_warnings = _validate_references(result, sources)
|
||||
if ref_warnings:
|
||||
logger.warning(" CPAM : %d référence(s) non vérifiable(s)", len(ref_warnings))
|
||||
|
||||
# 6. Formater la réponse
|
||||
text = _format_response(result, ref_warnings)
|
||||
logger.info(" Contre-argumentation générée (%d caractères)", len(text))
|
||||
|
||||
return text, rag_sources
|
||||
|
||||
Reference in New Issue
Block a user