feat: mode hybride Ollama — gemma3:27b pour CPAM, 12b pour codage
Le pipeline utilise désormais gemma3:12b (rapide) pour le codage CIM-10 et gemma3:27b (meilleur raisonnement) pour la contre-argumentation CPAM. Configurable via OLLAMA_MODEL_CPAM et OLLAMA_TIMEOUT_CPAM. Inclut aussi : traçabilité source/page DAS, niveaux CMA ATIH, sévérité, page tracker PDF, améliorations fusion et filtres DAS. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -305,6 +305,13 @@ _SEVERITY_STYLES = {
|
||||
"leger": ("Léger", "#065f46", "#d1fae5"),
|
||||
}
|
||||
|
||||
_CMA_LEVEL_STYLES = {
|
||||
1: ("1", "#6b7280", "#f3f4f6"), # gris — pas CMA
|
||||
2: ("2", "#065f46", "#d1fae5"), # vert
|
||||
3: ("3", "#92400e", "#fef3c7"), # jaune/orange
|
||||
4: ("4", "#dc2626", "#fee2e2"), # rouge
|
||||
}
|
||||
|
||||
|
||||
def format_duration(seconds: float | None) -> str:
|
||||
"""Formate une durée en secondes vers un format lisible (ex: 2min 30s)."""
|
||||
@@ -330,13 +337,24 @@ def severity_badge(value: str | None) -> Markup:
|
||||
)
|
||||
|
||||
|
||||
def cma_level_badge(value: int | None) -> Markup:
|
||||
"""Badge CMA niveau 1-4 avec couleurs graduées."""
|
||||
if value is None or value < 1:
|
||||
return Markup("")
|
||||
level = min(value, 4)
|
||||
label, fg, bg = _CMA_LEVEL_STYLES.get(level, _CMA_LEVEL_STYLES[1])
|
||||
title = {1: "Pas CMA", 2: "CMA niveau 2", 3: "CMA niveau 3", 4: "CMA niveau 4"}.get(level, "")
|
||||
return Markup(
|
||||
f'<span title="{title}" style="display:inline-block;padding:2px 8px;border-radius:9999px;'
|
||||
f'font-size:0.75rem;font-weight:600;color:{fg};background:{bg}">'
|
||||
f'CMA {label}</span>'
|
||||
)
|
||||
|
||||
|
||||
def format_dossier_name(name: str) -> str:
|
||||
"""Transforme un nom de dossier en nom lisible (ex: 15_23096332 → Dossier 23096332)."""
|
||||
"""Retourne le nom complet du dossier (ex: 1_23096332)."""
|
||||
if name == "racine":
|
||||
return "Non classés"
|
||||
m = re.match(r"^\d+_(\d+)$", name)
|
||||
if m:
|
||||
return f"Dossier {m.group(1)}"
|
||||
return name
|
||||
|
||||
|
||||
@@ -364,6 +382,7 @@ 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
|
||||
app.jinja_env.filters["cma_level_badge"] = cma_level_badge
|
||||
app.jinja_env.filters["format_duration"] = format_duration
|
||||
app.jinja_env.filters["format_dossier_name"] = format_dossier_name
|
||||
app.jinja_env.filters["format_doc_name"] = format_doc_name
|
||||
@@ -445,13 +464,16 @@ def create_app() -> Flask:
|
||||
return jsonify({"error": f"PDF source '{source_file}' introuvable"}), 404
|
||||
|
||||
try:
|
||||
anonymized_text, new_dossier, report = process_pdf(pdf_path)
|
||||
pdf_results = process_pdf(pdf_path)
|
||||
stem = pdf_path.stem.replace(" ", "_")
|
||||
subdir = None
|
||||
if pdf_path.parent != input_dir:
|
||||
subdir = pdf_path.parent.name
|
||||
write_outputs(stem, anonymized_text, new_dossier, report, subdir=subdir)
|
||||
return jsonify({"ok": True, "message": "Traitement terminé"})
|
||||
multi = len(pdf_results) > 1
|
||||
for part_idx, (anonymized_text, new_dossier, report) in enumerate(pdf_results):
|
||||
part_stem = f"{stem}_part{part_idx + 1}" if multi else stem
|
||||
write_outputs(part_stem, anonymized_text, new_dossier, report, subdir=subdir)
|
||||
return jsonify({"ok": True, "message": f"Traitement terminé ({len(pdf_results)} dossier(s))"})
|
||||
except Exception as e:
|
||||
logger.exception("Erreur lors du retraitement")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
background: #0f172a;
|
||||
color: #cbd5e1;
|
||||
background: #1e293b;
|
||||
color: #e2e8f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
@@ -30,16 +30,16 @@
|
||||
}
|
||||
.sidebar-header {
|
||||
padding: 1.25rem 1rem;
|
||||
border-bottom: 1px solid #1e293b;
|
||||
border-bottom: 1px solid #334155;
|
||||
}
|
||||
.sidebar-header h1 {
|
||||
font-size: 1.1rem;
|
||||
color: #e2e8f0;
|
||||
color: #f1f5f9;
|
||||
font-weight: 700;
|
||||
}
|
||||
.sidebar-header p {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
color: #94a3b8;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.sidebar-nav {
|
||||
@@ -52,15 +52,16 @@
|
||||
font-size: 0.65rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: #475569;
|
||||
color: #94a3b8;
|
||||
font-weight: 700;
|
||||
}
|
||||
.sidebar-nav a {
|
||||
display: block;
|
||||
padding: 0.4rem 1rem;
|
||||
color: #94a3b8;
|
||||
color: #cbd5e1;
|
||||
text-decoration: none;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
border-left: 3px solid transparent;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
@@ -68,45 +69,45 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.sidebar-nav a:hover {
|
||||
color: #e2e8f0;
|
||||
background: #1e293b;
|
||||
color: #f8fafc;
|
||||
background: #334155;
|
||||
border-left-color: #3b82f6;
|
||||
}
|
||||
.sidebar-nav a.sidebar-fusionne {
|
||||
color: #60a5fa;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.sidebar-search {
|
||||
padding: 0.75rem 1rem 0.5rem;
|
||||
border-bottom: 1px solid #1e293b;
|
||||
border-bottom: 1px solid #334155;
|
||||
}
|
||||
.sidebar-search input {
|
||||
width: 100%;
|
||||
padding: 0.45rem 0.6rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #334155;
|
||||
background: #1e293b;
|
||||
border: 1px solid #475569;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
font-size: 0.8rem;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.sidebar-search input::placeholder { color: #475569; }
|
||||
.sidebar-search input::placeholder { color: #64748b; }
|
||||
.sidebar-search input:focus { border-color: #3b82f6; }
|
||||
|
||||
/* Admin section */
|
||||
.sidebar-admin {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid #1e293b;
|
||||
border-top: 1px solid #334155;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.sidebar-admin label {
|
||||
display: block;
|
||||
margin-bottom: 0.35rem;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
color: #cbd5e1;
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
@@ -115,8 +116,8 @@
|
||||
width: 100%;
|
||||
padding: 0.4rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #334155;
|
||||
background: #1e293b;
|
||||
border: 1px solid #475569;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 0.5rem;
|
||||
@@ -145,7 +146,6 @@
|
||||
margin-left: 280px;
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
max-width: 1100px;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
@@ -249,17 +249,17 @@
|
||||
<nav class="sidebar-nav" id="sidebar-nav">
|
||||
{% block sidebar %}{% endblock %}
|
||||
</nav>
|
||||
<div class="sidebar-admin" style="border-top:1px solid #1e293b;padding:0.5rem 1rem;">
|
||||
<a href="/dashboard" style="display:block;color:#94a3b8;text-decoration:none;font-size:0.8rem;padding:0.35rem 0;transition:color 0.15s;"
|
||||
onmouseover="this.style.color='#e2e8f0'" onmouseout="this.style.color='#94a3b8'">
|
||||
<div class="sidebar-admin" style="border-top:1px solid #334155;padding:0.5rem 1rem;">
|
||||
<a href="/dashboard" style="display:block;color:#cbd5e1;text-decoration:none;font-size:0.8rem;font-weight:600;padding:0.35rem 0;transition:color 0.15s;"
|
||||
onmouseover="this.style.color='#f8fafc'" onmouseout="this.style.color='#cbd5e1'">
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="/cpam" style="display:block;color:#94a3b8;text-decoration:none;font-size:0.8rem;padding:0.35rem 0;transition:color 0.15s;"
|
||||
onmouseover="this.style.color='#e2e8f0'" onmouseout="this.style.color='#94a3b8'">
|
||||
<a href="/cpam" style="display:block;color:#cbd5e1;text-decoration:none;font-size:0.8rem;font-weight:600;padding:0.35rem 0;transition:color 0.15s;"
|
||||
onmouseover="this.style.color='#f8fafc'" onmouseout="this.style.color='#cbd5e1'">
|
||||
Contrôles CPAM
|
||||
</a>
|
||||
<a href="/admin/referentiels" style="display:block;color:#94a3b8;text-decoration:none;font-size:0.8rem;padding:0.35rem 0;transition:color 0.15s;"
|
||||
onmouseover="this.style.color='#e2e8f0'" onmouseout="this.style.color='#94a3b8'">
|
||||
<a href="/admin/referentiels" style="display:block;color:#cbd5e1;text-decoration:none;font-size:0.8rem;font-weight:600;padding:0.35rem 0;transition:color 0.15s;"
|
||||
onmouseover="this.style.color='#f8fafc'" onmouseout="this.style.color='#cbd5e1'">
|
||||
Référentiels RAG
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
{% if siblings %}
|
||||
<div class="group-title" style="margin-top:1rem;">{{ current_group }}</div>
|
||||
{% for sib in siblings %}
|
||||
<a href="/dossier/{{ sib.path_rel }}" {% if sib.path_rel == filepath %}style="color:#e2e8f0;border-left-color:#3b82f6;background:#1e293b;"{% endif %}>
|
||||
<a href="/dossier/{{ sib.path_rel }}" {% if sib.path_rel == filepath %}style="color:#f8fafc;border-left-color:#3b82f6;background:#334155;"{% endif %}>
|
||||
{{ sib.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
@@ -213,7 +213,11 @@
|
||||
{% if dp.cim10_suggestion %}
|
||||
<span class="badge" style="background:#dbeafe;color:#1d4ed8;font-size:0.85rem;">{{ dp.cim10_suggestion }}</span>
|
||||
{{ dp.cim10_confidence | confidence_badge }}
|
||||
{% if dp.est_cma %}<span class="badge" style="background:#fee2e2;color:#dc2626;font-size:0.75rem;">CMA</span>{% endif %}
|
||||
{% if dp.niveau_cma and dp.niveau_cma > 1 %}
|
||||
{{ dp.niveau_cma | cma_level_badge }}
|
||||
{% elif dp.est_cma %}
|
||||
<span class="badge" style="background:#fee2e2;color:#dc2626;font-size:0.75rem;">CMA</span>
|
||||
{% endif %}
|
||||
{{ dp.niveau_severite | severity_badge }}
|
||||
{% endif %}
|
||||
{% if dp.justification %}
|
||||
@@ -242,22 +246,40 @@
|
||||
<div class="card section">
|
||||
<h3>Diagnostics associés ({{ dossier.diagnostics_associes|length }})</h3>
|
||||
<table>
|
||||
<thead><tr><th>Texte</th><th>CIM-10</th><th>Confiance</th><th>Sévérité</th><th>Justification</th></tr></thead>
|
||||
<thead><tr><th>Texte</th><th>CIM-10</th><th>Confiance</th><th>CMA</th><th>Source</th><th>Justification</th></tr></thead>
|
||||
<tbody>
|
||||
{% for das in dossier.diagnostics_associes %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ das.texte }}
|
||||
{% if das.est_cma %}<span class="badge" style="background:#fee2e2;color:#dc2626;font-size:0.7rem;margin-left:0.3rem;">CMA</span>{% endif %}
|
||||
</td>
|
||||
<td>{{ das.texte }}</td>
|
||||
<td>{% if das.cim10_suggestion %}<span class="badge" style="background:#dbeafe;color:#1d4ed8;">{{ das.cim10_suggestion }}</span>{% endif %}</td>
|
||||
<td>{{ das.cim10_confidence | confidence_badge }}</td>
|
||||
<td>{{ das.niveau_severite | severity_badge }}</td>
|
||||
<td>
|
||||
{% if das.niveau_cma and das.niveau_cma > 1 %}
|
||||
{{ das.niveau_cma | cma_level_badge }}
|
||||
{% elif das.est_cma %}
|
||||
<span class="badge" style="background:#fee2e2;color:#dc2626;font-size:0.7rem;">CMA</span>
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if das.source %}
|
||||
<span class="badge" style="background:#e0e7ff;color:#3730a3;font-size:0.7rem;">{{ das.source }}</span>
|
||||
{% endif %}
|
||||
{% if das.source_page %}
|
||||
<span style="font-size:0.7rem;color:#64748b;">p.{{ das.source_page }}</span>
|
||||
{% endif %}
|
||||
{% if das.source_excerpt %}
|
||||
<details style="margin-top:0.2rem;"><summary style="font-size:0.7rem;color:#94a3b8;cursor:pointer;">extrait</summary>
|
||||
<pre style="font-size:0.7rem;white-space:pre-wrap;max-width:300px;color:#475569;">{{ das.source_excerpt }}</pre>
|
||||
</details>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="font-size:0.8rem;color:#475569;">{{ das.justification or '' }}</td>
|
||||
</tr>
|
||||
{% if das.raisonnement %}
|
||||
<tr>
|
||||
<td colspan="5" style="padding:0 0.75rem 0.5rem;">
|
||||
<td colspan="6" style="padding:0 0.75rem 0.5rem;">
|
||||
<details>
|
||||
<summary>Raisonnement LLM</summary>
|
||||
<pre>{{ das.raisonnement }}</pre>
|
||||
@@ -267,7 +289,7 @@
|
||||
{% endif %}
|
||||
{% if das.sources_rag %}
|
||||
<tr>
|
||||
<td colspan="5" style="padding:0 0.75rem 0.5rem;">
|
||||
<td colspan="6" style="padding:0 0.75rem 0.5rem;">
|
||||
<details>
|
||||
<summary>Sources RAG ({{ das.sources_rag|length }})</summary>
|
||||
{% for src in das.sources_rag %}
|
||||
|
||||
Reference in New Issue
Block a user