feat: timings granulaires appels LLM (RAG, DAS, DP, QC)

Ajoute des mesures time.monotonic() autour de chaque appel LLM dans
rag_search.py : enrich_diagnostic, enrich_acte, extract_das_llm.
Format de log : logger.info("⏱ [RAG-DP] 14.2s — texte")

Découpe enrich_dossier() en 2 fonctions exportées :
- enrich_dp() : enrichit seulement le DP (parallélisable)
- enrich_das_and_actes() : enrichit DAS + actes en parallèle
L'ancienne enrich_dossier() reste comme wrapper rétro-compatible.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dom
2026-03-08 14:16:38 +01:00
parent 0cafb47e8d
commit f5a6122495

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import logging import logging
import os import os
import threading import threading
import time
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
from ..config import ( from ..config import (
@@ -610,7 +611,9 @@ def enrich_diagnostic(
Modifie le diagnostic en place. Fallback gracieux si FAISS ou Ollama échouent. Modifie le diagnostic en place. Fallback gracieux si FAISS ou Ollama échouent.
""" """
t0 = time.monotonic()
diag_type = "dp" if est_dp else "das" diag_type = "dp" if est_dp else "das"
label = f"RAG-{diag_type.upper()}"
# 1. Vérifier le cache # 1. Vérifier le cache
cached = cache.get(diagnostic.texte, diag_type) if cache else None cached = cache.get(diagnostic.texte, diag_type) if cache else None
@@ -626,6 +629,8 @@ def enrich_diagnostic(
if cached is not None: if cached is not None:
logger.info("Cache hit (sans sources FAISS) pour %s : « %s »", diag_type.upper(), diagnostic.texte) logger.info("Cache hit (sans sources FAISS) pour %s : « %s »", diag_type.upper(), diagnostic.texte)
_apply_llm_result_diagnostic(diagnostic, cached) _apply_llm_result_diagnostic(diagnostic, cached)
elapsed = time.monotonic() - t0
logger.info("⏱ [%s] %.1fs — %s (no FAISS)", label, elapsed, diagnostic.texte[:60])
return return
# 3. Stocker les sources RAG # 3. Stocker les sources RAG
@@ -643,11 +648,15 @@ def enrich_diagnostic(
if cached is not None: if cached is not None:
logger.info("Cache hit pour %s : « %s »", diag_type.upper(), diagnostic.texte) logger.info("Cache hit pour %s : « %s »", diag_type.upper(), diagnostic.texte)
_apply_llm_result_diagnostic(diagnostic, cached) _apply_llm_result_diagnostic(diagnostic, cached)
elapsed = time.monotonic() - t0
logger.info("⏱ [%s] %.1fs — %s (cache hit)", label, elapsed, diagnostic.texte[:60])
return return
# 5. Appel Ollama pour justification avec raisonnement structuré # 5. Appel Ollama pour justification avec raisonnement structuré
prompt = _build_prompt(diagnostic.texte, sources, contexte, est_dp=est_dp) prompt = _build_prompt(diagnostic.texte, sources, contexte, est_dp=est_dp)
t_llm = time.monotonic()
llm_result = _call_ollama(prompt) llm_result = _call_ollama(prompt)
llm_elapsed = time.monotonic() - t_llm
if llm_result: if llm_result:
_apply_llm_result_diagnostic(diagnostic, llm_result) _apply_llm_result_diagnostic(diagnostic, llm_result)
@@ -656,6 +665,9 @@ def enrich_diagnostic(
else: else:
logger.info("Ollama non disponible — sources FAISS conservées sans justification LLM") logger.info("Ollama non disponible — sources FAISS conservées sans justification LLM")
elapsed = time.monotonic() - t0
logger.info("⏱ [%s] %.1fs (LLM %.1fs) — %s", label, elapsed, llm_elapsed, diagnostic.texte[:60])
def _apply_llm_result_acte(acte: ActeCCAM, llm_result: dict) -> None: def _apply_llm_result_acte(acte: ActeCCAM, llm_result: dict) -> None:
"""Applique un résultat LLM (frais ou caché) à un ActeCCAM.""" """Applique un résultat LLM (frais ou caché) à un ActeCCAM."""
@@ -687,6 +699,8 @@ def enrich_acte(acte: ActeCCAM, contexte: dict, cache: OllamaCache | None = None
Modifie l'acte en place. Fallback gracieux si FAISS ou Ollama échouent. Modifie l'acte en place. Fallback gracieux si FAISS ou Ollama échouent.
""" """
t0 = time.monotonic()
# 1. Vérifier le cache # 1. Vérifier le cache
cached = cache.get(acte.texte, "ccam") if cache else None cached = cache.get(acte.texte, "ccam") if cache else None
@@ -712,11 +726,15 @@ def enrich_acte(acte: ActeCCAM, contexte: dict, cache: OllamaCache | None = None
if cached is not None: if cached is not None:
logger.info("Cache hit pour CCAM : « %s »", acte.texte) logger.info("Cache hit pour CCAM : « %s »", acte.texte)
_apply_llm_result_acte(acte, cached) _apply_llm_result_acte(acte, cached)
elapsed = time.monotonic() - t0
logger.info("⏱ [RAG-CCAM] %.1fs — %s (cache hit)", elapsed, acte.texte[:60])
return return
# 5. Appel Ollama pour justification avec raisonnement structuré # 5. Appel Ollama pour justification avec raisonnement structuré
prompt = _build_prompt_ccam(acte.texte, sources, contexte) prompt = _build_prompt_ccam(acte.texte, sources, contexte)
t_llm = time.monotonic()
llm_result = _call_ollama(prompt) llm_result = _call_ollama(prompt)
llm_elapsed = time.monotonic() - t_llm
if llm_result: if llm_result:
_apply_llm_result_acte(acte, llm_result) _apply_llm_result_acte(acte, llm_result)
@@ -725,6 +743,9 @@ def enrich_acte(acte: ActeCCAM, contexte: dict, cache: OllamaCache | None = None
else: else:
logger.info("Ollama non disponible — sources FAISS CCAM conservées sans justification LLM") logger.info("Ollama non disponible — sources FAISS CCAM conservées sans justification LLM")
elapsed = time.monotonic() - t0
logger.info("⏱ [RAG-CCAM] %.1fs (LLM %.1fs) — %s", elapsed, llm_elapsed, acte.texte[:60])
def _smart_truncate(text: str, max_chars: int = 6000) -> str: def _smart_truncate(text: str, max_chars: int = 6000) -> str:
"""Troncature intelligente : garde le début + les sections finales importantes. """Troncature intelligente : garde le début + les sections finales importantes.
@@ -790,6 +811,8 @@ def extract_das_llm(
""" """
import hashlib import hashlib
t0 = time.monotonic()
# Clé de cache basée sur le hash du texte # Clé de cache basée sur le hash du texte
text_hash = hashlib.md5(text[:4000].encode()).hexdigest()[:16] text_hash = hashlib.md5(text[:4000].encode()).hexdigest()[:16]
cache_key_text = f"das_extract::{text_hash}" cache_key_text = f"das_extract::{text_hash}"
@@ -798,15 +821,19 @@ def extract_das_llm(
if cache is not None: if cache is not None:
cached = cache.get(cache_key_text, "das_llm") cached = cache.get(cache_key_text, "das_llm")
if cached is not None: if cached is not None:
logger.info("Cache hit pour extraction DAS LLM") elapsed = time.monotonic() - t0
logger.info("⏱ [DAS-LLM] %.1fs — cache hit", elapsed)
return cached.get("diagnostics_supplementaires", []) return cached.get("diagnostics_supplementaires", [])
# Construire le prompt et appeler Ollama # Construire le prompt et appeler Ollama
prompt = _build_prompt_das_extraction(text, contexte, existing_das, dp_texte) prompt = _build_prompt_das_extraction(text, contexte, existing_das, dp_texte)
t_llm = time.monotonic()
result = call_ollama(prompt, temperature=0.1, max_tokens=2000, role="coding") result = call_ollama(prompt, temperature=0.1, max_tokens=2000, role="coding")
llm_elapsed = time.monotonic() - t_llm
if result is None: if result is None:
logger.warning("Extraction DAS LLM : Ollama non disponible") elapsed = time.monotonic() - t0
logger.warning("⏱ [DAS-LLM] %.1fs — Ollama non disponible", elapsed)
return [] return []
das_list = result.get("diagnostics_supplementaires", []) das_list = result.get("diagnostics_supplementaires", [])
@@ -818,25 +845,52 @@ def extract_das_llm(
if cache is not None: if cache is not None:
cache.put(cache_key_text, "das_llm", result) cache.put(cache_key_text, "das_llm", result)
logger.info("Extraction DAS LLM : %d diagnostics supplémentaires détectés", len(das_list)) elapsed = time.monotonic() - t0
logger.info("⏱ [DAS-LLM] %.1fs (LLM %.1fs) — %d diagnostics détectés", elapsed, llm_elapsed, len(das_list))
return das_list return das_list
def enrich_dossier(dossier: DossierMedical) -> None: def enrich_dp(dossier: DossierMedical, cache: OllamaCache | None = None) -> None:
"""Enrichit le DP et tous les DAS d'un dossier via le RAG. """Enrichit SEULEMENT le DP d'un dossier via le RAG (Phase 1).
Utilise un cache persistant et parallélise les appels Ollama Peut être exécuté en parallèle avec d'autres tâches (DAS LLM, DP selector)
pour les DAS et actes CCAM (max_workers = OLLAMA_MAX_PARALLEL). car il ne dépend que du DP existant.
Args:
dossier: Dossier médical à enrichir (modifié en place).
cache: Cache Ollama optionnel (créé si None).
""" """
cache = OllamaCache(OLLAMA_CACHE_PATH, get_model("coding")) t0 = time.monotonic()
if cache is None:
cache = OllamaCache(OLLAMA_CACHE_PATH, get_model("coding"))
contexte = build_enriched_context(dossier) contexte = build_enriched_context(dossier)
# Phase 1 : DP seul (le contexte DAS en dépend)
if dossier.diagnostic_principal: if dossier.diagnostic_principal:
logger.info("RAG enrichissement DP : %s", dossier.diagnostic_principal.texte) logger.info("RAG enrichissement DP : %s", dossier.diagnostic_principal.texte)
enrich_diagnostic(dossier.diagnostic_principal, contexte, est_dp=True, cache=cache) enrich_diagnostic(dossier.diagnostic_principal, contexte, est_dp=True, cache=cache)
cache.save()
elapsed = time.monotonic() - t0
logger.info("⏱ [RAG-DP-PHASE] %.1fs — enrichissement DP terminé", elapsed)
def enrich_das_and_actes(dossier: DossierMedical, cache: OllamaCache | None = None) -> None:
"""Enrichit les DAS et actes CCAM d'un dossier via le RAG (Phase 2).
Parallélise les appels Ollama (max_workers = OLLAMA_MAX_PARALLEL).
Doit être appelé APRES enrich_dp() et après l'ajout des DAS LLM.
Args:
dossier: Dossier médical à enrichir (modifié en place).
cache: Cache Ollama optionnel (créé si None).
"""
t0 = time.monotonic()
if cache is None:
cache = OllamaCache(OLLAMA_CACHE_PATH, get_model("coding"))
contexte = build_enriched_context(dossier)
# Mettre à jour le contexte avec le DP pour les DAS # Mettre à jour le contexte avec le DP pour les DAS
if dossier.diagnostic_principal: if dossier.diagnostic_principal:
contexte["dp_texte"] = dossier.diagnostic_principal.texte contexte["dp_texte"] = dossier.diagnostic_principal.texte
@@ -846,7 +900,6 @@ def enrich_dossier(dossier: DossierMedical) -> None:
if d.cim10_suggestion if d.cim10_suggestion
] ]
# Phase 2 : DAS + Actes en parallèle
das_list = dossier.diagnostics_associes das_list = dossier.diagnostics_associes
actes_list = dossier.actes_ccam actes_list = dossier.actes_ccam
@@ -863,3 +916,18 @@ def enrich_dossier(dossier: DossierMedical) -> None:
f.result() # propage les exceptions f.result() # propage les exceptions
cache.save() cache.save()
elapsed = time.monotonic() - t0
n_das = len(das_list) if das_list else 0
n_actes = len(actes_list) if actes_list else 0
logger.info("⏱ [RAG-DAS-PHASE] %.1fs — %d DAS + %d actes enrichis", elapsed, n_das, n_actes)
def enrich_dossier(dossier: DossierMedical) -> None:
"""Enrichit le DP et tous les DAS d'un dossier via le RAG.
Wrapper rétro-compatible qui appelle enrich_dp() puis enrich_das_and_actes().
Utilise un cache persistant partagé entre les deux phases.
"""
cache = OllamaCache(OLLAMA_CACHE_PATH, get_model("coding"))
enrich_dp(dossier, cache=cache)
enrich_das_and_actes(dossier, cache=cache)