feat: Phase 4 — viewer enrichi, non-cumul CCAM, fusion multi-PDFs + rebuild FAISS (21 141 vecteurs)

- Viewer : badges compteurs (DAS, actes, alertes, CMA), raisonnement LLM pliable, regroupement CCAM, navigation patient, alertes NON-CUMUL en rouge
- Non-cumul CCAM : 3 règles heuristiques (même base, même regroupement/jour, paires incompatibles)
- Fusion multi-PDFs : merge_dossiers() avec priorité Trackare, spécificité CIM-10, déduplication, champ source_files
- Index FAISS reconstruit : 21 141 vecteurs (CCAM dict 8 257 + CIM-10 alpha 306)
- 192 tests unitaires passent

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dom
2026-02-11 12:43:34 +01:00
parent 7e69f994b0
commit 9d07894c6f
12 changed files with 1013 additions and 26 deletions

View File

@@ -10,7 +10,7 @@ import requests
from flask import Flask, abort, render_template, request, jsonify
from markupsafe import Markup
from ..config import STRUCTURED_DIR, OLLAMA_URL, DossierMedical
from ..config import STRUCTURED_DIR, OLLAMA_URL, CCAM_DICT_PATH, DossierMedical
from .. import config as cfg
logger = logging.getLogger(__name__)
@@ -20,11 +20,53 @@ logger = logging.getLogger(__name__)
# Helpers
# ---------------------------------------------------------------------------
def compute_group_stats(items: list[dict]) -> dict:
"""Calcule des statistiques agrégées pour un groupe de dossiers.
Returns:
{das_count, alertes_count, actes_count, cma_count}
"""
das_count = 0
alertes_count = 0
actes_count = 0
cma_count = 0
for item in items:
d = item["dossier"]
das_count += len(d.diagnostics_associes)
alertes_count += len(d.alertes_codage)
actes_count += len(d.actes_ccam)
for diag in d.diagnostics_associes:
if diag.est_cma:
cma_count += 1
if d.diagnostic_principal and d.diagnostic_principal.est_cma:
cma_count += 1
return {
"das_count": das_count,
"alertes_count": alertes_count,
"actes_count": actes_count,
"cma_count": cma_count,
}
def load_ccam_dict() -> dict[str, dict]:
"""Charge le dictionnaire CCAM pour les regroupements."""
if CCAM_DICT_PATH.exists():
try:
data = json.loads(CCAM_DICT_PATH.read_text(encoding="utf-8"))
return data
except Exception:
logger.warning("Impossible de charger le dictionnaire CCAM")
return {}
def scan_dossiers() -> dict[str, list[dict]]:
"""Scanne output/structured/ et retourne les fichiers groupés par sous-dossier.
Returns:
{"racine": [{name, path_rel, dossier}, ...], "sous-dossier": [...]}
Chaque groupe contient aussi une clé "stats" avec les compteurs agrégés.
"""
groups: dict[str, list[dict]] = {}
@@ -112,6 +154,24 @@ def confidence_label(value: str | None) -> str:
return _CONFIDENCE_LABELS.get(value, value)
_SEVERITY_STYLES = {
"severe": ("Sévère", "#dc2626", "#fee2e2"),
"modere": ("Modéré", "#92400e", "#fef3c7"),
"leger": ("Léger", "#065f46", "#d1fae5"),
}
def severity_badge(value: str | None) -> Markup:
if not value or value not in _SEVERITY_STYLES:
return Markup("")
label, fg, bg = _SEVERITY_STYLES[value]
return Markup(
f'<span style="display:inline-block;padding:2px 8px;border-radius:9999px;'
f'font-size:0.75rem;font-weight:600;color:{fg};background:{bg}">'
f'{label}</span>'
)
# ---------------------------------------------------------------------------
# App factory
# ---------------------------------------------------------------------------
@@ -121,16 +181,35 @@ def create_app() -> Flask:
app.jinja_env.filters["confidence_badge"] = confidence_badge
app.jinja_env.filters["confidence_label"] = confidence_label
app.jinja_env.filters["severity_badge"] = severity_badge
ccam_dict = load_ccam_dict()
@app.route("/")
def index():
groups = scan_dossiers()
return render_template("index.html", groups=groups)
group_stats = {name: compute_group_stats(items) for name, items in groups.items()}
return render_template("index.html", groups=groups, group_stats=group_stats)
@app.route("/dossier/<path:filepath>")
def detail(filepath: str):
dossier = load_dossier(filepath)
return render_template("detail.html", dossier=dossier, filepath=filepath)
# Trouver les fichiers du même groupe pour la navigation
groups = scan_dossiers()
siblings = []
current_group = None
rel_parts = Path(filepath).parts
if len(rel_parts) > 1:
current_group = str(Path(*rel_parts[:-1]))
siblings = groups.get(current_group, [])
return render_template(
"detail.html",
dossier=dossier,
filepath=filepath,
ccam_dict=ccam_dict,
siblings=siblings,
current_group=current_group,
)
@app.route("/admin/models", methods=["GET"])
def list_models():