feat: mode Validation DIM dans le viewer Flask

Permet aux médecins DIM de valider/corriger les codes CIM-10 extraits
par le pipeline pour construire un gold standard (50 dossiers).

- ValidationManager : gestion annotations JSON dans data/gold_standard/
- Script sélection 50 dossiers (25 CPAM + 25 stratifiés CMD/confiance)
- Routes /validation, /api/cim10/search, /api/validation/save, /validation/metrics
- Formulaire avec autocomplete CIM-10, boutons Correct/Modifier/Supprimer
- Dashboard métriques : precision, recall, F1, hallucination par confiance/source

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dom
2026-02-17 21:43:02 +01:00
parent aad925ebea
commit dbc5bdbaf4
7 changed files with 1488 additions and 0 deletions

View File

@@ -22,6 +22,7 @@ from ..config import (
)
from .. import config as cfg
from .referentiels import ReferentielManager
from .validation import ValidationManager
logger = logging.getLogger(__name__)
@@ -539,4 +540,158 @@ def create_app() -> Flask:
logger.exception("Erreur lors du rebuild de l'index")
return jsonify({"error": str(e)}), 500
# ------------------------------------------------------------------
# Routes validation DIM
# ------------------------------------------------------------------
val_manager = ValidationManager()
@app.route("/validation")
def validation_list():
groups = scan_dossiers()
selection = val_manager.load_selection()
annotations = {a["dossier_id"]: a for a in val_manager.list_annotations()}
# Construire la liste enrichie
items = []
for dossier_id in selection:
annot = annotations.get(dossier_id, {})
# Trouver les données pipeline
parts = dossier_id.split("/")
group_name = parts[0] if parts else ""
group_items = groups.get(group_name, [])
pipeline = None
for gi in group_items:
if "fusionne" in gi["name"]:
pipeline = gi
break
if not pipeline and group_items:
pipeline = group_items[0]
d = pipeline["dossier"] if pipeline else None
items.append({
"dossier_id": dossier_id,
"group_name": group_name,
"dp_code": d.diagnostic_principal.cim10_suggestion if d and d.diagnostic_principal else "",
"dp_texte": d.diagnostic_principal.texte if d and d.diagnostic_principal else "",
"dp_confidence": d.diagnostic_principal.cim10_confidence if d and d.diagnostic_principal else "",
"nb_das": len(d.diagnostics_associes) if d else 0,
"has_cpam": bool(d and d.controles_cpam),
"statut": annot.get("statut", "non_commence"),
"validateur": annot.get("validateur", ""),
"date_validation": annot.get("date_validation", ""),
})
total = len(items)
valides = sum(1 for i in items if i["statut"] == "valide")
en_cours = sum(1 for i in items if i["statut"] == "en_cours")
return render_template(
"validation_list.html",
items=items,
total=total,
valides=valides,
en_cours=en_cours,
groups=groups,
)
@app.route("/validation/<path:dossier_id>")
def validation_detail(dossier_id: str):
groups = scan_dossiers()
# Charger l'annotation
annotation = val_manager.load_annotation(dossier_id)
if not annotation:
abort(404)
# Charger les données pipeline
parts = dossier_id.split("/")
group_name = parts[0] if parts else ""
group_items = groups.get(group_name, [])
pipeline = None
for gi in group_items:
if "fusionne" in gi["name"]:
pipeline = gi
break
if not pipeline and group_items:
pipeline = group_items[0]
dossier = pipeline["dossier"] if pipeline else None
# Navigation : dossier précédent / suivant
selection = val_manager.load_selection()
current_idx = selection.index(dossier_id) if dossier_id in selection else -1
prev_id = selection[current_idx - 1] if current_idx > 0 else None
next_id = selection[current_idx + 1] if current_idx < len(selection) - 1 else None
return render_template(
"validation_detail.html",
annotation=annotation,
dossier=dossier,
dossier_id=dossier_id,
group_name=group_name,
prev_id=prev_id,
next_id=next_id,
groups=groups,
)
@app.route("/api/validation/save", methods=["POST"])
def api_validation_save():
data = request.get_json(silent=True)
if not data or "dossier_id" not in data:
return jsonify({"error": "dossier_id requis"}), 400
dossier_id = data["dossier_id"]
# Vérifier que le dossier fait partie de la sélection
selection = val_manager.load_selection()
if selection and dossier_id not in selection:
return jsonify({"error": "Dossier non sélectionné pour validation"}), 403
try:
val_manager.save_annotation(dossier_id, data)
return jsonify({"ok": True})
except Exception as e:
logger.exception("Erreur sauvegarde annotation %s", dossier_id)
return jsonify({"error": str(e)}), 500
@app.route("/api/cim10/search")
def api_cim10_search():
from ..medical.cim10_dict import load_dict, normalize_text
q = request.args.get("q", "").strip()
if len(q) < 2:
return jsonify({"results": []})
cim10 = load_dict()
q_norm = normalize_text(q)
q_upper = q.upper().strip()
results = []
# Recherche par code exact d'abord
for code, label in cim10.items():
if code.upper().startswith(q_upper):
results.append({"code": code, "label": label})
if len(results) >= 20:
break
# Puis recherche par texte normalisé
if len(results) < 20:
for code, label in cim10.items():
if any(r["code"] == code for r in results):
continue
if q_norm in normalize_text(label):
results.append({"code": code, "label": label})
if len(results) >= 20:
break
return jsonify({"results": results})
@app.route("/validation/metrics")
def validation_metrics():
groups = scan_dossiers()
metrics = val_manager.compute_metrics(groups)
selection = val_manager.load_selection()
return render_template(
"validation_metrics.html",
metrics=metrics,
total_selection=len(selection),
groups=groups,
)
return app