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:
@@ -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
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user