From f5a612249554163d38d0dbc9e4546c5b96c24d9d Mon Sep 17 00:00:00 2001 From: dom Date: Sun, 8 Mar 2026 14:16:38 +0100 Subject: [PATCH] feat: timings granulaires appels LLM (RAG, DAS, DP, QC) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/medical/rag_search.py | 88 ++++++++++++++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 10 deletions(-) diff --git a/src/medical/rag_search.py b/src/medical/rag_search.py index 3617f9c..ffa7600 100644 --- a/src/medical/rag_search.py +++ b/src/medical/rag_search.py @@ -5,6 +5,7 @@ from __future__ import annotations import logging import os import threading +import time from concurrent.futures import ThreadPoolExecutor, as_completed from ..config import ( @@ -610,7 +611,9 @@ def enrich_diagnostic( Modifie le diagnostic en place. Fallback gracieux si FAISS ou Ollama échouent. """ + t0 = time.monotonic() diag_type = "dp" if est_dp else "das" + label = f"RAG-{diag_type.upper()}" # 1. Vérifier le cache cached = cache.get(diagnostic.texte, diag_type) if cache else None @@ -626,6 +629,8 @@ def enrich_diagnostic( if cached is not None: logger.info("Cache hit (sans sources FAISS) pour %s : « %s »", diag_type.upper(), diagnostic.texte) _apply_llm_result_diagnostic(diagnostic, cached) + elapsed = time.monotonic() - t0 + logger.info("⏱ [%s] %.1fs — %s (no FAISS)", label, elapsed, diagnostic.texte[:60]) return # 3. Stocker les sources RAG @@ -643,11 +648,15 @@ def enrich_diagnostic( if cached is not None: logger.info("Cache hit pour %s : « %s »", diag_type.upper(), diagnostic.texte) _apply_llm_result_diagnostic(diagnostic, cached) + elapsed = time.monotonic() - t0 + logger.info("⏱ [%s] %.1fs — %s (cache hit)", label, elapsed, diagnostic.texte[:60]) return # 5. Appel Ollama pour justification avec raisonnement structuré prompt = _build_prompt(diagnostic.texte, sources, contexte, est_dp=est_dp) + t_llm = time.monotonic() llm_result = _call_ollama(prompt) + llm_elapsed = time.monotonic() - t_llm if llm_result: _apply_llm_result_diagnostic(diagnostic, llm_result) @@ -656,6 +665,9 @@ def enrich_diagnostic( else: 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: """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. """ + t0 = time.monotonic() + # 1. Vérifier le cache 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: logger.info("Cache hit pour CCAM : « %s »", acte.texte) _apply_llm_result_acte(acte, cached) + elapsed = time.monotonic() - t0 + logger.info("⏱ [RAG-CCAM] %.1fs — %s (cache hit)", elapsed, acte.texte[:60]) return # 5. Appel Ollama pour justification avec raisonnement structuré prompt = _build_prompt_ccam(acte.texte, sources, contexte) + t_llm = time.monotonic() llm_result = _call_ollama(prompt) + llm_elapsed = time.monotonic() - t_llm if 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: 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: """Troncature intelligente : garde le début + les sections finales importantes. @@ -790,6 +811,8 @@ def extract_das_llm( """ import hashlib + t0 = time.monotonic() + # Clé de cache basée sur le hash du texte text_hash = hashlib.md5(text[:4000].encode()).hexdigest()[:16] cache_key_text = f"das_extract::{text_hash}" @@ -798,15 +821,19 @@ def extract_das_llm( if cache is not None: cached = cache.get(cache_key_text, "das_llm") 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", []) # Construire le prompt et appeler Ollama 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") + llm_elapsed = time.monotonic() - t_llm 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 [] das_list = result.get("diagnostics_supplementaires", []) @@ -818,25 +845,52 @@ def extract_das_llm( if cache is not None: 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 -def enrich_dossier(dossier: DossierMedical) -> None: - """Enrichit le DP et tous les DAS d'un dossier via le RAG. +def enrich_dp(dossier: DossierMedical, cache: OllamaCache | None = None) -> None: + """Enrichit SEULEMENT le DP d'un dossier via le RAG (Phase 1). - Utilise un cache persistant et parallélise les appels Ollama - pour les DAS et actes CCAM (max_workers = OLLAMA_MAX_PARALLEL). + Peut être exécuté en parallèle avec d'autres tâches (DAS LLM, DP selector) + 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) - # Phase 1 : DP seul (le contexte DAS en dépend) if dossier.diagnostic_principal: logger.info("RAG enrichissement DP : %s", dossier.diagnostic_principal.texte) 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 if dossier.diagnostic_principal: contexte["dp_texte"] = dossier.diagnostic_principal.texte @@ -846,7 +900,6 @@ def enrich_dossier(dossier: DossierMedical) -> None: if d.cim10_suggestion ] - # Phase 2 : DAS + Actes en parallèle das_list = dossier.diagnostics_associes actes_list = dossier.actes_ccam @@ -863,3 +916,18 @@ def enrich_dossier(dossier: DossierMedical) -> None: f.result() # propage les exceptions 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)