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