feat: architecture multi-modèles LLM + quality engine + benchmark

- Multi-modèles : 4 rôles LLM (coding=gemma3:27b-cloud, cpam=gemma3:27b-cloud,
  validation=deepseek-v3.2:cloud, qc=gemma3:12b) avec get_model(role)
- Prompts externalisés : 7 templates dans src/prompts/templates.py
- Cache Ollama : modèle stocké par entrée (migration auto ancien format)
- call_ollama() : paramètre role= (priorité: model > role > global)
- Quality engine : veto_engine + decision_engine + rules_router (YAML)
- Benchmark qualité : scripts/benchmark_quality.py (A/B, métriques CIM-10)
- Fix biologie : valeurs qualitatives (troponine négative) non filtrées
- Fix CPAM : gemma3:27b-cloud au lieu de deepseek (JSON tronqué par thinking)
- CPAM max_tokens 4000→6000, viewer admin multi-modèles
- Benchmark 10 dossiers : 100% DAS valides, 10/10 CPAM, 243s/dossier

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dom
2026-02-20 00:21:09 +01:00
parent 5c8c2817ec
commit 909e051cc9
39 changed files with 5092 additions and 574 deletions

View File

@@ -8,7 +8,7 @@ import re
from pathlib import Path
import requests
from flask import Flask, abort, render_template, request, jsonify
from flask import Flask, Response, abort, render_template, request, jsonify
from markupsafe import Markup
from werkzeug.utils import secure_filename
@@ -16,7 +16,8 @@ from werkzeug.utils import secure_filename
from collections import Counter
from ..config import (
ANONYMIZED_DIR, STRUCTURED_DIR, OLLAMA_URL, CCAM_DICT_PATH, DossierMedical,
ANONYMIZED_DIR, STRUCTURED_DIR, INPUT_DIR, REPORTS_DIR,
OLLAMA_URL, CCAM_DICT_PATH, DossierMedical,
ALLOWED_EXTENSIONS, UPLOAD_MAX_SIZE_MB,
CIM10_PDF, GUIDE_METHODO_PDF, CCAM_PDF, CIM10_DICT_PATH, CIM10_SUPPLEMENTS_PATH,
)
@@ -463,7 +464,11 @@ def create_app() -> Flask:
@app.route("/admin/models", methods=["GET"])
def list_models():
models = fetch_ollama_models()
return jsonify({"models": models, "current": cfg.OLLAMA_MODEL})
return jsonify({
"models": models,
"current": cfg.OLLAMA_MODEL,
"roles": dict(cfg.OLLAMA_MODELS),
})
@app.route("/admin/models", methods=["POST"])
def set_model():
@@ -471,8 +476,15 @@ def create_app() -> Flask:
new_model = data.get("model", "").strip()
if not new_model:
return jsonify({"error": "Champ 'model' requis"}), 400
role = data.get("role", "").strip()
if role:
if role not in cfg.OLLAMA_MODELS:
return jsonify({"error": f"Rôle inconnu : {role}"}), 400
cfg.OLLAMA_MODELS[role] = new_model
logger.info("Modèle Ollama pour rôle '%s' changé : %s", role, new_model)
return jsonify({"ok": True, "role": role, "model": new_model, "roles": dict(cfg.OLLAMA_MODELS)})
cfg.OLLAMA_MODEL = new_model
logger.info("Modèle Ollama changé : %s", new_model)
logger.info("Modèle Ollama global changé : %s", new_model)
return jsonify({"ok": True, "model": cfg.OLLAMA_MODEL})
@app.route("/reprocess/<path:filepath>", methods=["POST"])
@@ -615,6 +627,44 @@ def create_app() -> Flask:
logger.warning("Impossible de lire %s", txt_path)
return jsonify(result)
# ------------------------------------------------------------------
# API PDF caviardé
# ------------------------------------------------------------------
@app.route("/api/pdf/<path:dossier_id>/<filename>")
def serve_redacted_pdf(dossier_id: str, filename: str):
"""Sert un PDF avec les données personnelles caviardées (rectangles noirs).
Query params optionnels :
- highlight : texte à surligner en jaune
- page : numéro de page (1-indexed) pour cibler le surlignage
"""
from .pdf_redactor import load_entities_from_report, redact_pdf, highlight_text
# Sécurité path traversal
safe_dir = (INPUT_DIR / dossier_id).resolve()
if not safe_dir.is_relative_to(INPUT_DIR.resolve()):
abort(403)
pdf_path = safe_dir / filename
if not pdf_path.exists() or pdf_path.suffix.lower() != ".pdf":
abort(404)
# Charger les entités depuis le rapport d'anonymisation
stem = Path(filename).stem.replace(" ", "_")
report_path = REPORTS_DIR / dossier_id / f"{stem}_report.json"
entities = load_entities_from_report(report_path) if report_path.exists() else set()
pdf_bytes = redact_pdf(pdf_path, entities)
# Surlignage optionnel
highlight = request.args.get("highlight", "")
page_num = request.args.get("page", type=int)
if highlight:
pdf_bytes = highlight_text(pdf_bytes, highlight, page_num)
return Response(pdf_bytes, mimetype="application/pdf")
# ------------------------------------------------------------------
# Routes admin référentiels
# ------------------------------------------------------------------