feat: architecture multi-modèles LLM + externalisation des prompts

- Ajout OLLAMA_MODELS (coding/cpam/validation/qc) dans config.py avec get_model()
- Paramètre role= dans call_ollama() pour dispatch par rôle
- Cache Ollama : modèle stocké par entrée (migration auto de l'ancien format)
- 7 prompts externalisés dans src/prompts/templates.py (format str.format)
- Viewer : admin multi-modèles, endpoint PDF avec redaction, source texte
- Documentation prompts dans docs/prompts.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dom
2026-02-19 20:51:52 +01:00
parent 5c8c2817ec
commit 540e0cb400
17 changed files with 1221 additions and 353 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,17 +464,30 @@ 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():
data = request.get_json(silent=True) or {}
role = data.get("role", "").strip()
new_model = data.get("model", "").strip()
if not new_model:
return jsonify({"error": "Champ 'model' requis"}), 400
cfg.OLLAMA_MODEL = new_model
logger.info("Modèle Ollama changé : %s", new_model)
return jsonify({"ok": True, "model": cfg.OLLAMA_MODEL})
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 rôle '%s' changé : %s", role, new_model)
return jsonify({"ok": True, "role": role, "model": new_model})
else:
# Backward compat : changer le modèle global (fallback)
cfg.OLLAMA_MODEL = 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"])
def reprocess(filepath: str):
@@ -615,6 +629,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
# ------------------------------------------------------------------