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