feat: architecture multi-modèles LLM + quality engine + benchmark

- Multi-modèles : 4 rôles LLM (coding=gemma3:27b-cloud, cpam=gemma3:27b-cloud,
  validation=deepseek-v3.2:cloud, qc=gemma3:12b) avec get_model(role)
- Prompts externalisés : 7 templates dans src/prompts/templates.py
- Cache Ollama : modèle stocké par entrée (migration auto ancien format)
- call_ollama() : paramètre role= (priorité: model > role > global)
- Quality engine : veto_engine + decision_engine + rules_router (YAML)
- Benchmark qualité : scripts/benchmark_quality.py (A/B, métriques CIM-10)
- Fix biologie : valeurs qualitatives (troponine négative) non filtrées
- Fix CPAM : gemma3:27b-cloud au lieu de deepseek (JSON tronqué par thinking)
- CPAM max_tokens 4000→6000, viewer admin multi-modèles
- Benchmark 10 dossiers : 100% DAS valides, 10/10 CPAM, 243s/dossier

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dom
2026-02-20 00:21:09 +01:00
parent 5c8c2817ec
commit 909e051cc9
39 changed files with 5092 additions and 574 deletions

34
config/bio_rules.yaml Normal file
View File

@@ -0,0 +1,34 @@
version: 2
# Règles biologiques (contradiction bio ⇒ ruled_out)
# + garde-fou "preuve manquante" (diag d'ionogramme sans valeur extraite ⇒ NEED_INFO)
#
# Objectif: éviter des FAIL "bêtes" quand la biologie contredit clairement un diagnostic,
# et éviter des PASS "trop optimistes" quand on n'a même pas la valeur biologique.
#
# Hiérarchie des seuils:
# - Priorité aux normes du document (ex: [N: 135-145])
# - Sinon fallback config/reference_ranges.yaml
# - Si âge inconnu/enfant: safe zones conservatrices (reference_ranges.yaml)
missing_evidence:
enabled: true
veto: VETO-17
severity: LOW
score_penalty: 2
rules:
hyponatremia:
enabled: true
codes: ["E87.1"] # hyponatrémie
analyte: sodium
hyperkalemia:
enabled: true
codes: ["E87.5"] # hyperkaliémie
analyte: potassium
hypokalemia:
enabled: true
codes: ["E87.6"] # hypokaliémie
analyte: potassium

View File

@@ -0,0 +1,62 @@
# Garde-fous de parsing des valeurs biologiques
# ------------------------------------------------
# Objectif: éviter des faux positifs dus à des artefacts PDF/OCR
# (ex: "8" au lieu de "4.8" pour le potassium).
#
# IMPORTANT:
# - Ce fichier ne définit PAS des "normes biologiques" (ça c'est reference_ranges.yaml)
# - Ici on définit des bornes *plausibles* très larges + quelques heuristiques "anti-OCR".
#
# Clés des tests: minuscules, sans accents, ex: potassium, sodium, plaquettes, hemoglobine...
version: 1
policy:
drop_out_of_range: true # écarte les valeurs hors bornes plausibles du dossier
keep_suspect: true # conserve les valeurs suspectes (audit) mais les règles privilégient les valeurs ok
tests:
potassium:
hard_min: 0.5
hard_max: 9.0
suspect:
single_digit_over: 6.0 # "8" seul est souvent une décimale perdue (4,8 -> 8)
sodium:
hard_min: 90
hard_max: 200
plaquettes:
hard_min: 5
hard_max: 2000
hemoglobine:
hard_min: 3
hard_max: 25
creatinine:
hard_min: 1
hard_max: 5000
crp:
hard_min: 0
hard_max: 1000
alat:
hard_min: 0
hard_max: 5000
asat:
hard_min: 0
hard_max: 5000
ggt:
hard_min: 0
hard_max: 5000
pal:
hard_min: 0
hard_max: 5000
bilirubine totale:
hard_min: 0
hard_max: 2000

View File

@@ -0,0 +1,30 @@
# Références biologiques (fallback) pour règles de qualité (VETO-09 / ruled_out)
# Ordre de priorité recommandé:
# 1) Normes du document (ex: "[N: 135-145]")
# 2) Fallback ci-dessous (par bande d'âge)
# 3) Safe zones conservatrices si âge inconnu (évite les faux "barrés")
version: 1
age_bands:
adult_min_years: 18
fallback_ranges:
adult:
platelets: { low: 150, high: 450, unit: "G/L" }
sodium: { low: 135, high: 145, unit: "mmol/L" }
potassium: { low: 3.5, high: 5.0, unit: "mmol/L" }
# Pédiatrie: à affiner par tranches d'âge si besoin.
# NB: pour les décisions "ruled_out" avec âge inconnu, on applique plutôt les safe zones.
child:
platelets: { low: 150, high: 450, unit: "G/L" }
sodium: { low: 135, high: 145, unit: "mmol/L" }
potassium: { low: 3.5, high: 5.0, unit: "mmol/L" }
# Seuils "safe" quand l'âge n'est pas connu (plus conservateurs que les bornes normales)
safe_zones_unknown_age:
platelets_ruled_out_low: 170 # si PLT >= 170 -> thrombopénie ruled_out
sodium_ruled_out_low: 138 # si Na >= 138 -> hyponatrémie ruled_out
potassium_ruled_out_high: 4.9 # si K <= 4.9 -> hyperkaliémie ruled_out
potassium_ruled_out_low: 3.7 # si K >= 3.7 -> hypokaliémie ruled_out

68
config/rules/README.md Normal file
View File

@@ -0,0 +1,68 @@
# Règles (vetos + décisions)
Ce dossier contient la configuration "métier" pour piloter le moteur qualité.
## Fichiers
- `base.yaml` : socle commun (règles activées par défaut).
- `enabled.yaml` : choisit les overlays à activer (site/spécialité).
- `specialties/*.yaml` : overrides par spécialité.
- `sites/*.yaml` : overrides par établissement.
## Principe
- Une règle **non listée** est considérée **activée**.
- Ça évite de casser le comportement historique lors d'une montée de version.
- Une règle listée peut être :
- `enabled: false` → désactivée
- (VETO) `force_severity: "HARD"|"MEDIUM"|"LOW"` → force la sévérité
## Exemple d'override
Créer `config/rules/sites/chu_poitiers.yaml` :
```yaml
version: 1
rules:
VETO-12:
enabled: false
VETO-09:
force_severity: "HARD"
```
Puis activer dans `enabled.yaml` :
```yaml
active:
site: "chu_poitiers"
specialty: ""
extra: []
```
## Routage automatique (router.yaml)
Le fichier `router.yaml` permet dactiver automatiquement des **packs** de règles en fonction des signaux du dossier (codes, biologie, extraits). Concrètement :
- Par défaut, seuls les packs listés dans `defaults.enabled_packs` sont actifs.
- Quand un trigger match, on ajoute ses `enable_packs`.
- Le routage est appliqué **par dossier** (et re-appliqué sur la version fusionnée).
### Mode strict
Quand `mode: strict`, une règle *non listée* dans `base.yaml` est considérée **désactivée** dès que le routage runtime est actif.
Ça force une approche “catalogue explicite” : tout ce qui tourne en prod est visible et gouvernable.
### Exemple
Activer les règles ionogramme uniquement si un code `E87.*` est détecté ou si la biologie mentionne Sodium/Potassium :
```yaml
triggers:
- id: TRG-ELECTROLYTES
enable_packs: [bio_electrolytes]
when_any:
codes_prefix: ["E87."]
lab_tests: ["sodium", "potassium"]
```

82
config/rules/base.yaml Normal file
View File

@@ -0,0 +1,82 @@
version: 1
# Catalogue "socle" de règles.
#
# Objectif : piloter (sans toucher au code) :
# - l'activation/désactivation de règles (vetos + décisions)
# - éventuellement un forçage de sévérité pour un VETO
#
# Important : si une règle n'est pas listée ici, elle est considérée activée.
# (=> comportement historique conservé)
packs:
vetos_core:
enabled: true
rules:
VETO-02:
enabled: true
description: "Code sans preuve exploitable"
VETO-03:
enabled: true
description: "Conditionnel / négation / contradictions dans la preuve"
VETO-06:
enabled: true
description: "DP dupliqué dans les DAS"
VETO-07:
enabled: true
description: "Doublons DAS"
VETO-09:
enabled: true
description: "Contradiction biologique (plaquettes/créat)"
# force_severity: "HARD" # Optionnel : forcer la sévérité globale
VETO-12:
enabled: true
description: "Sur-confiance (high sans preuve)"
VETO-15:
enabled: true
description: "Preuve issue d'un score/test (risque de sur-codage)"
VETO-16:
enabled: true
description: "Heuristique libellé→code (hors-sujet probable)"
VETO-17:
enabled: true
description: "Preuve biologique manquante => NEED_INFO (non bloquant)"
decisions_core:
enabled: true
rules:
RULE-D50-NEEDS-IRON:
enabled: true
description: "D50 sans preuve martiale => downgrade D64.9 + NEED_INFO"
RULE-D69.6-PLT-NORMAL:
enabled: true
description: "D69.6 incompatible avec plaquettes normales => ruled_out (barré)"
bio_electrolytes:
enabled: true
rules:
RULE-E87.1-NA-NORMAL:
enabled: true
description: "E87.1 suggérée mais Na normal => ruled_out"
RULE-E87.1-MISSING-NA:
enabled: true
description: "E87.1 suggérée mais Na absent => NEED_INFO"
RULE-E87.5-K-NORMAL:
enabled: true
description: "E87.5 suggérée mais K normal => ruled_out"
RULE-E87.5-MISSING-K:
enabled: true
description: "E87.5 suggérée mais K absent => NEED_INFO"
RULE-E87.6-K-NORMAL:
enabled: true
description: "E87.6 suggérée mais K normal => ruled_out"
RULE-E87.6-MISSING-K:
enabled: true
description: "E87.6 suggérée mais K absent => NEED_INFO"
placeholders_future:
enabled: false
rules:
RULE-PDF-PROTECTED-NEED_INFO:
enabled: false
description: "PDF protégé => NEED_INFO (à implémenter si besoin)"

12
config/rules/enabled.yaml Normal file
View File

@@ -0,0 +1,12 @@
version: 1
# Sélection d'overlays (facile à brancher plus tard sur une UI).
#
# - specialty : charge config/rules/specialties/<specialty>.yaml
# - site : charge config/rules/sites/<site>.yaml
# - extra : charge des fichiers YAML additionnels (chemins relatifs à config/rules/)
active:
specialty: ""
site: ""
extra: []

35
config/rules/router.yaml Normal file
View File

@@ -0,0 +1,35 @@
version: 1
# 'strict' => si un rule_id n'est pas listé dans base.yaml, il est considéré OFF
# quand le routage runtime est actif (objectif: pro / pas de surprise).
mode: strict
defaults:
# Socle pro: toujours actif (peu coûteux, structure la contestabilité)
enabled_packs:
- vetos_core
- decisions_core
# (Optionnel) règles toujours ON même si leur pack n'est pas actif
always_on_rules: []
# Triggers : activer des packs additionnels seulement si le dossier a des signaux pertinents
triggers:
- id: TRG-ELECTROLYTES
enable_packs: ["bio_electrolytes"]
when_any:
# Codes souvent porteurs d'ionogramme (hyponatrémie/hyperkaliémie/hypokaliémie)
codes_prefix: ["E87."]
# Ou biologie présente
lab_tests: ["ionogramme", "sodium", "potassium", "na", "k"]
# Ou texte
keywords:
- "ionogramme"
- "hypokali"
- "hyperkali"
- "hyponatr"
- "hypernatr"
- "kaliémie"
- "natrémie"
- "sodium"
- "potassium"

View File

@@ -0,0 +1,9 @@
version: 1
# Overlay établissement (ex: chu_poitiers, clinique_x, etc.)
# Ce fichier ne contient que des overrides.
rules:
# Exemple : forcer VETO-09 en HARD
# VETO-09:
# force_severity: "HARD"

View File

@@ -0,0 +1,13 @@
version: 1
# Overlay spécialité (ex: digestif, cardio, pneumo, onco...)
# Ce fichier ne contient que des overrides.
rules:
# Exemple : être plus strict sur le conditionnel
# VETO-03:
# force_severity: "MEDIUM"
# Exemple : désactiver un downgrade jugé trop agressif
# RULE-D50-NEEDS-IRON:
# enabled: false