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:
@@ -98,11 +98,21 @@ def extract_medical_info(
|
||||
anonymized_text: str,
|
||||
edsnlp_result: Optional[EdsnlpResult] = None,
|
||||
use_rag: bool = False,
|
||||
page_tracker=None,
|
||||
raw_text: str | None = None,
|
||||
) -> DossierMedical:
|
||||
"""Extrait les informations médicales structurées depuis les données parsées et le texte."""
|
||||
"""Extrait les informations médicales structurées depuis les données parsées et le texte.
|
||||
|
||||
Args:
|
||||
page_tracker: PageTracker pour la traçabilité page/extrait (optionnel).
|
||||
raw_text: Texte brut avant anonymisation (pour recherche page source).
|
||||
"""
|
||||
dossier = DossierMedical()
|
||||
dossier.document_type = parsed_data.get("type", "")
|
||||
|
||||
# Texte de référence pour la recherche de pages (raw_text préféré, sinon anonymized)
|
||||
search_text = raw_text or anonymized_text
|
||||
|
||||
_extract_sejour(parsed_data, dossier)
|
||||
_extract_diagnostics(parsed_data, anonymized_text, dossier, edsnlp_result)
|
||||
_extract_actes(anonymized_text, dossier)
|
||||
@@ -140,6 +150,10 @@ def extract_medical_info(
|
||||
# Post-processing : retirer DAS dont le code est identique au DP
|
||||
_remove_das_equal_dp(dossier)
|
||||
|
||||
# Post-processing : traçabilité source (page + extrait)
|
||||
if page_tracker:
|
||||
_apply_source_tracking(dossier, page_tracker, search_text)
|
||||
|
||||
return dossier
|
||||
|
||||
|
||||
@@ -331,10 +345,12 @@ def _extract_diagnostics(
|
||||
elif edsnlp_codes:
|
||||
# Utiliser la première entité CIM-10 edsnlp comme DP
|
||||
code, texte = next(iter(edsnlp_codes.items()))
|
||||
dossier.diagnostic_principal = Diagnostic(
|
||||
texte=texte.capitalize(), cim10_suggestion=code,
|
||||
source="edsnlp",
|
||||
)
|
||||
texte_clean = texte.capitalize()
|
||||
if is_valid_diagnostic_text(texte_clean):
|
||||
dossier.diagnostic_principal = Diagnostic(
|
||||
texte=texte_clean, cim10_suggestion=code,
|
||||
source="edsnlp",
|
||||
)
|
||||
|
||||
# Diagnostics associés depuis le texte (regex)
|
||||
das = _find_diagnostics_associes(text_lower, conclusion, dossier)
|
||||
@@ -881,18 +897,46 @@ def _apply_code_corrections(dossier: DossierMedical) -> None:
|
||||
diag.cim10_suggestion = corrected
|
||||
|
||||
|
||||
def _is_dp_family_redundant(das_code: str, dp_code: str) -> bool:
|
||||
"""True si le DAS est redondant avec le DP (même code, parent/enfant, ou même famille)."""
|
||||
if das_code == dp_code:
|
||||
return True
|
||||
# Relation parent/enfant → toujours redondant
|
||||
das_norm = das_code.replace(".", "")
|
||||
dp_norm = dp_code.replace(".", "")
|
||||
if das_norm.startswith(dp_norm) or dp_norm.startswith(das_norm):
|
||||
return True
|
||||
# Même famille 3 chars, sauf exceptions
|
||||
dp_family = dp_code[:3]
|
||||
if das_code[:3] == dp_family:
|
||||
# S/T (trauma) : sites différents → garder
|
||||
if dp_family[0] in ("S", "T"):
|
||||
return False
|
||||
# E10-E14 (diabète) : complications différentes → garder
|
||||
if dp_family[0] == "E" and dp_family[1:].isdigit() and 10 <= int(dp_family[1:]) <= 14:
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _remove_das_equal_dp(dossier: DossierMedical) -> None:
|
||||
"""Retire les DAS dont le code CIM-10 est identique au DP (violation règle PMSI)."""
|
||||
"""Retire les DAS redondants avec le DP (même code, famille, ou sémantique)."""
|
||||
from .das_filter import apply_semantic_dedup
|
||||
|
||||
dp_code = dossier.diagnostic_principal.cim10_suggestion if dossier.diagnostic_principal else None
|
||||
if not dp_code:
|
||||
return
|
||||
before = len(dossier.diagnostics_associes)
|
||||
dossier.diagnostics_associes = [
|
||||
d for d in dossier.diagnostics_associes if d.cim10_suggestion != dp_code
|
||||
d for d in dossier.diagnostics_associes
|
||||
if not d.cim10_suggestion or not _is_dp_family_redundant(d.cim10_suggestion, dp_code)
|
||||
]
|
||||
removed = before - len(dossier.diagnostics_associes)
|
||||
if removed:
|
||||
logger.info(" DAS=DP : %d DAS retiré(s) (code %s identique au DP)", removed, dp_code)
|
||||
logger.info(" DAS≈DP : %d DAS retiré(s) (famille %s du DP)", removed, dp_code[:3])
|
||||
|
||||
# Redondances sémantiques entre DAS
|
||||
dossier.diagnostics_associes = apply_semantic_dedup(dossier.diagnostics_associes)
|
||||
|
||||
|
||||
def _apply_noncumul_rules(dossier: DossierMedical) -> None:
|
||||
@@ -945,3 +989,33 @@ def _is_abnormal(test: str, value: str) -> bool | None:
|
||||
lo, hi = BIO_NORMALS[test]
|
||||
return val > hi or val < lo
|
||||
return None
|
||||
|
||||
|
||||
def _apply_source_tracking(dossier: DossierMedical, page_tracker, search_text: str) -> None:
|
||||
"""Ajoute la traçabilité source (page + extrait) à chaque diagnostic.
|
||||
|
||||
Cherche le texte du diagnostic dans le texte source pour retrouver
|
||||
la page d'origine et extraire un passage contextualisé.
|
||||
"""
|
||||
all_diags: list[Diagnostic] = []
|
||||
if dossier.diagnostic_principal:
|
||||
all_diags.append(dossier.diagnostic_principal)
|
||||
all_diags.extend(dossier.diagnostics_associes)
|
||||
|
||||
tracked = 0
|
||||
for diag in all_diags:
|
||||
if diag.source_page is not None:
|
||||
continue # déjà renseigné
|
||||
|
||||
texte = diag.texte
|
||||
if not texte:
|
||||
continue
|
||||
|
||||
page = page_tracker.find_page_for_text(texte, search_text)
|
||||
if page:
|
||||
diag.source_page = page
|
||||
diag.source_excerpt = page_tracker.extract_excerpt(texte, search_text)
|
||||
tracked += 1
|
||||
|
||||
if tracked:
|
||||
logger.info(" Traçabilité source : %d/%d diagnostics localisés", tracked, len(all_diags))
|
||||
|
||||
Reference in New Issue
Block a user