feat: enrichissement contre-argumentation CPAM — libellés CIM-10, RAG ciblé, reprocess complet
- Résolution des libellés CIM-10 pour les codes contestés (dp_ucr, da_ucr, dr_ucr) - Fallback DP depuis dp_ucr quand le pipeline n'extrait pas de diagnostic principal - Troncature arg_ucr augmentée de 200 à 500 chars pour conserver les citations de règles - Requête RAG 4 : définitions CIM-10 (inclusion/exclusion) des codes contestés - Requête RAG 5 : extraction et recherche des règles nommées (RègleT7, Annexe, etc.) - Cap résultats RAG de 10 à 12 pour absorber les nouvelles requêtes - Reprocess viewer : pipeline complet (fusion + GHM + CPAM) pour dossiers multi-PDF - Affichage structuré response_data dans le viewer (analyse, preuves, références) - 7 nouveaux tests CPAM, 6 nouveaux tests viewer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,8 +3,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from ..config import ControleCPAM, DossierMedical, RAGSource
|
||||
from ..medical.cim10_dict import normalize_code, validate_code
|
||||
from ..medical.ollama_client import call_anthropic, call_ollama
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -42,7 +44,7 @@ def _search_rag_for_control(controle: ControleCPAM, dossier: DossierMedical) ->
|
||||
query_parts_arg = []
|
||||
if controle.titre:
|
||||
query_parts_arg.append(controle.titre)
|
||||
arg_short = controle.arg_ucr[:200] if controle.arg_ucr else ""
|
||||
arg_short = controle.arg_ucr[:500] if controle.arg_ucr else ""
|
||||
if arg_short:
|
||||
query_parts_arg.append(arg_short)
|
||||
query_arg = " ".join(query_parts_arg)
|
||||
@@ -65,6 +67,43 @@ def _search_rag_for_control(controle: ControleCPAM, dossier: DossierMedical) ->
|
||||
logger.debug(" RAG requête clinique : %d résultats", len(results_clinique))
|
||||
all_results.extend(results_clinique)
|
||||
|
||||
# Requête 4 — Définitions CIM-10 des codes contestés
|
||||
contested_codes = []
|
||||
for field in (controle.dp_ucr, controle.da_ucr, controle.dr_ucr):
|
||||
if field:
|
||||
contested_codes.extend(re.split(r"[,;\s]+", field.strip()))
|
||||
for raw_code in contested_codes:
|
||||
raw_code = raw_code.strip()
|
||||
if not raw_code:
|
||||
continue
|
||||
norm = normalize_code(raw_code)
|
||||
is_valid, label = validate_code(norm)
|
||||
if is_valid and label:
|
||||
query_def = f"CIM-10 {norm} {label} définition inclusion exclusion"
|
||||
else:
|
||||
query_def = f"CIM-10 {norm} définition codage"
|
||||
results_def = search_similar_cpam(query_def, top_k=3)
|
||||
logger.debug(" RAG requête CIM-10 %s : %d résultats", norm, len(results_def))
|
||||
all_results.extend(results_def)
|
||||
|
||||
# Requête 5 — Règles explicitement citées dans l'argument CPAM
|
||||
if controle.arg_ucr:
|
||||
rule_patterns = [
|
||||
r'(?:R[eè]gle\s*T?\s*\d+)',
|
||||
r'(?:Annexe[\s-]*\d+[A-Za-z]*)',
|
||||
r'(?:Situation de soins?\s+[^.]{5,40})',
|
||||
]
|
||||
rules_found = []
|
||||
for pattern in rule_patterns:
|
||||
rules_found.extend(re.findall(pattern, controle.arg_ucr, re.IGNORECASE))
|
||||
if rules_found:
|
||||
rules_unique = list(dict.fromkeys(rules_found))
|
||||
query_rules = " ".join(rules_unique) + " guide méthodologique codage PMSI"
|
||||
results_rules = search_similar_cpam(query_rules, top_k=4)
|
||||
logger.debug(" RAG requête règles (%s) : %d résultats",
|
||||
", ".join(rules_unique), len(results_rules))
|
||||
all_results.extend(results_rules)
|
||||
|
||||
if not all_results:
|
||||
return []
|
||||
|
||||
@@ -79,7 +118,29 @@ def _search_rag_for_control(controle: ControleCPAM, dossier: DossierMedical) ->
|
||||
seen[key] = r
|
||||
|
||||
merged = sorted(seen.values(), key=lambda r: r["score"], reverse=True)
|
||||
return merged[:10]
|
||||
return merged[:12]
|
||||
|
||||
|
||||
def _get_code_label(code_str: str) -> str:
|
||||
"""Résout le libellé CIM-10 pour un ou plusieurs codes."""
|
||||
codes = re.split(r"[,;\s]+", code_str.strip())
|
||||
labels = []
|
||||
for raw in codes:
|
||||
raw = raw.strip()
|
||||
if not raw:
|
||||
continue
|
||||
norm = normalize_code(raw)
|
||||
is_valid, label = validate_code(norm)
|
||||
if is_valid and label:
|
||||
labels.append(f"{norm} — {label}")
|
||||
else:
|
||||
labels.append(norm)
|
||||
if not labels:
|
||||
return ""
|
||||
if len(labels) == 1:
|
||||
parts = labels[0].split(" — ", 1)
|
||||
return f" — {parts[1]}" if len(parts) > 1 else ""
|
||||
return "\n " + "\n ".join(labels)
|
||||
|
||||
|
||||
def _build_cpam_prompt(
|
||||
@@ -95,6 +156,12 @@ def _build_cpam_prompt(
|
||||
dp = dossier.diagnostic_principal
|
||||
dp_code = f" ({dp.cim10_suggestion})" if dp.cim10_suggestion else ""
|
||||
dossier_lines.append(f"- DP : {dp.texte}{dp_code}")
|
||||
elif controle.dp_ucr:
|
||||
dp_label = _get_code_label(controle.dp_ucr)
|
||||
dossier_lines.append(
|
||||
f"- DP : code {controle.dp_ucr}{dp_label} "
|
||||
f"(codé par l'établissement, contesté par la CPAM)"
|
||||
)
|
||||
|
||||
if dossier.diagnostics_associes:
|
||||
das_parts = []
|
||||
@@ -192,14 +259,14 @@ def _build_cpam_prompt(
|
||||
+ "\n".join(asymetrie_lines)
|
||||
)
|
||||
|
||||
# Codes contestés par la CPAM
|
||||
# Codes contestés par la CPAM (avec libellés CIM-10 résolus)
|
||||
codes_contestes = []
|
||||
if controle.dp_ucr:
|
||||
codes_contestes.append(f"DP proposé par UCR : {controle.dp_ucr}")
|
||||
codes_contestes.append(f"DP proposé par UCR : {controle.dp_ucr}{_get_code_label(controle.dp_ucr)}")
|
||||
if controle.da_ucr:
|
||||
codes_contestes.append(f"DA proposés par UCR : {controle.da_ucr}")
|
||||
codes_contestes.append(f"DA proposés par UCR : {controle.da_ucr}{_get_code_label(controle.da_ucr)}")
|
||||
if controle.dr_ucr:
|
||||
codes_contestes.append(f"DR proposé par UCR : {controle.dr_ucr}")
|
||||
codes_contestes.append(f"DR proposé par UCR : {controle.dr_ucr}{_get_code_label(controle.dr_ucr)}")
|
||||
if controle.actes_ucr:
|
||||
codes_contestes.append(f"Actes proposés par UCR : {controle.actes_ucr}")
|
||||
codes_str = "\n".join(codes_contestes) if codes_contestes else "Aucun code spécifique proposé"
|
||||
@@ -263,6 +330,10 @@ AXE ASYMÉTRIE D'INFORMATION :
|
||||
- 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
|
||||
@@ -406,7 +477,7 @@ def _format_response(parsed: dict, ref_warnings: list[str] | None = None) -> str
|
||||
def generate_cpam_response(
|
||||
dossier: DossierMedical,
|
||||
controle: ControleCPAM,
|
||||
) -> tuple[str, list[RAGSource]]:
|
||||
) -> tuple[str, dict | None, list[RAGSource]]:
|
||||
"""Génère une contre-argumentation pour un contrôle CPAM.
|
||||
|
||||
Args:
|
||||
@@ -414,7 +485,7 @@ def generate_cpam_response(
|
||||
controle: Le contrôle CPAM à contester.
|
||||
|
||||
Returns:
|
||||
Tuple (texte de contre-argumentation, sources RAG utilisées).
|
||||
Tuple (texte de contre-argumentation, dict LLM structuré ou None, sources RAG utilisées).
|
||||
"""
|
||||
logger.info("CPAM : génération contre-argumentation pour OGC %d — %s",
|
||||
controle.numero_ogc, controle.titre)
|
||||
@@ -449,7 +520,7 @@ def generate_cpam_response(
|
||||
|
||||
if result is None:
|
||||
logger.warning(" LLM non disponible — contre-argumentation non générée")
|
||||
return "", rag_sources
|
||||
return "", None, rag_sources
|
||||
|
||||
# 5. Validation des références
|
||||
ref_warnings = _validate_references(result, sources)
|
||||
@@ -460,4 +531,4 @@ def generate_cpam_response(
|
||||
text = _format_response(result, ref_warnings)
|
||||
logger.info(" Contre-argumentation générée (%d caractères)", len(text))
|
||||
|
||||
return text, rag_sources
|
||||
return text, result, rag_sources
|
||||
|
||||
Reference in New Issue
Block a user