backup: snapshot post-démo GHT 2026-05-19
Some checks failed
tests / Lint (ruff + black) (push) Successful in 1m50s
tests / Tests unitaires (sans GPU) (push) Failing after 1m50s
tests / Tests sécurité (critique) (push) Has been skipped

Backup état complet après enregistrement vidéo démo de bout en bout.
À utiliser comme point de référence pour la consolidation post-démo.

Changements majeurs de la session 18-19 mai :
- AIVA-URGENCE : page autonome avec preset URL + auto-focus chain
- Workflow Demo_urgence_3_db : merge linux_db + steps AIVA + pause humaine NoMachine
- Bypass LLM (static_result / static_text) dans replay_engine
  pour démos déterministes sans appel Ollama
- Fix api_stream:3013 — replay_paused au premier polling /next
- dag_execute : lift duration_ms vers top-level pour wait runtime
- NPM bypass auth /aiva-urgence/ via location ^~ (proxy_host/10.conf hors git)
- scripts/cancel-replays.sh — workaround Stop VWB qui ne purge pas la queue

Anchors visuels (468) forcés dans le commit pour garantir restorabilité.
DB workflows actuelle + ~12 .bak DB de la journée incluses.

Sujets identifiés pour consolidation post-démo (TODO) :
1. Bug VWB recapture anchor ne régénère pas le PNG
2. Léa client accumule état mémoire (restart périodique requis)
3. Stop VWB ne purge pas la queue serveur (lien manquant vers /replay/cancel)
4. Bug coord client mss tronqué 2560x60 → mapping Y cassé
5. delay_before/delay_after ignorés au runtime (fix partiel duration_ms)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-05-19 14:55:06 +02:00
parent f2212e77e3
commit 5ea4960e65
627 changed files with 211348 additions and 169 deletions

View File

@@ -0,0 +1,205 @@
# Audit BDD workflow Urgence_aiva_demo — 2026-05-10
Heure d'audit : 2026-05-10 15:42 CEST.
Audit factuel post-dry-run 15:08 (replay_free_1957dd59), pas de modification BDD.
Workflow_id : `wf_a38aeebea5e6_1778162737`
BDD : `visual_workflow_builder/backend/instance/workflows.db` (SQLite 3.x).
Périmètre : steps 8, 10, 12, 14, 18 (les blocages observés au runtime).
---
## 1. Schéma BDD pertinent
```sql
CREATE TABLE workflows (
id VARCHAR(64) PRIMARY KEY,
name, description, tags_json, trigger_examples_json,
created_at, updated_at, is_active, source,
review_status, review_feedback, reviewed_at
);
CREATE TABLE steps (
id VARCHAR(64) PRIMARY KEY,
workflow_id REFERENCES workflows(id),
action_type, -- click_anchor, extract_text, key_combo, etc.
"order" INTEGER,
position_x, position_y, -- coords édition canvas (pas écran)
parameters_json TEXT, -- ⚠ blob JSON : by_text, vlm_desc, visual_anchor.bounding_box, …
anchor_id REFERENCES visual_anchors(id),
label
);
CREATE TABLE visual_anchors (
id VARCHAR(64) PRIMARY KEY,
image_path, thumbnail_path,
bbox_x, bbox_y, bbox_width, bbox_height, -- zone d'ancrage sur l'image source
screen_width, screen_height, -- résolution d'enregistrement
description, target_text, ocr_description,
confidence_threshold, capture_method, created_at
);
```
**Pas de colonne** `resolve_order`, `verify_params`, `region_hint` dans la BDD : tout passe par `parameters_json` (champ texte JSON libre côté `steps`) si présent, sinon **defaults serveur**.
---
## 2. Métadonnées workflow
| Champ | Valeur |
|---|---|
| name | Urgence_aiva_demo |
| description | Démo GHT Sud 95 — workflow simplifié aligné sur maquette aiva-vision (1 DPI textarea + Justification + Analyser). Construit le 7 mai 2026. |
| source | manual |
| created_at | 2026-05-07 16:05:37 |
| updated_at | 2026-05-07 16:05:37 |
| is_active | 1 |
| Total steps | 22 (DB) — 23 dans le replay (1 wait_before_start ajouté serveur) |
---
## 3. Dump des steps problématiques
### Step 8 — `Onglet Examens cliniques` (action_type=click_anchor)
```json
parameters_json = {"by_text": "Examens cliniques"}
```
| Champ ancre | Valeur |
|---|---|
| anchor_id | `anchor_c3929b00816a_1778161050` |
| bbox (x, y, w, h) | **(6, 467, 2538, 643)** — **TOUTE LA PAGE** |
| image source | `anchor_c3929b00816a_1778161050_full.png` |
| screen_width × height | 2560 × 1600 |
| target_text | « Motif d'admission Examens cliniques Imagerie Notes médicales Synthèse Urgences Codage > Examens Cliniques Questionnaire Score Réponse significative … (~600 chars) » |
### Step 10 — `Onglet Imagerie` (action_type=click_anchor)
```json
parameters_json = {"by_text": "Imagerie"}
```
| Champ ancre | Valeur |
|---|---|
| anchor_id | **`anchor_c3929b00816a_1778161050`** ⚠ **MÊME ANCRE QUE STEP 8** |
| bbox | **(6, 467, 2538, 643)** — toute la page |
| target_text | identique à step 8 (la même chaîne contenant tous les tabs) |
### Step 12 — `Onglet Notes médicales` (action_type=click_anchor)
```json
parameters_json = {"by_text": "Notes médicales"}
```
| Champ ancre | Valeur |
|---|---|
| anchor_id | `anchor_0438bd2d9bdd_1778161174` |
| bbox (x, y, w, h) | (444, 424, 146, 48) — **petite zone tabs** |
| target_text | « ine Né(e) le 14/03/1947 I 77 ans **es Imagerie Notes médical** J scan, echograj phie » (OCR brut tronqué) |
### Step 14 — `Onglet Synthèse Urgences` (action_type=click_anchor)
```json
parameters_json = {
"by_text": "Synthèse Urgences",
"visual_anchor": {
"anchor_id": "anchor_6a2591e7c51c_1778229076",
"bounding_box": {"x": 580, "y": 423, "width": 192, "height": 47}
}
}
```
| Champ ancre | Valeur |
|---|---|
| anchor_id | `anchor_6a2591e7c51c_1778229076` |
| bbox (x, y, w, h) | (580, 423, 192, 47) — **petite zone tabs** |
| target_text | « à 14/03/1947 I 77 ans I I Sexe F Arrivé **ie Notes médicales Synthèse Ur** » (OCR brut tronqué) |
### Step 18 — `Cliquer textarea DPI` (action_type=click_anchor, page codage.html)
```json
parameters_json = {
"by_text": "Coller ou saisir le dossier patient",
"visual_anchor": {
"anchor_id": "anchor_8d1822fbaad3_1778164480",
"bounding_box": {"x": 41, "y": 690, "width": 301, "height": 32}
}
}
```
| Champ ancre | Valeur |
|---|---|
| anchor_id | `anchor_8d1822fbaad3_1778164480` |
| bbox (x, y, w, h) | (41, 690, 301, 32) — petite zone textarea |
| target_text | « **Coller ou saisir le dossier patient** Coller ou saisir le dossier patient » (placeholder italique gris) |
---
## 4. Tableau d'analyse — contrainte BDD vs laxiste
| Step | Cible | Ancre restrictive ? | OCR contraint à zone ? | resolve_order ? | verify_params ? | Risque BDD |
|---|---|---|---|---|---|---|
| 8 Examens cliniques | tab Easily | ❌ bbox = full page (2538×643) | ❌ pas de region_hint | ❌ default serveur | ❌ default pHash | **élevé** : OCR libre matche n'importe quelle occurrence du mot |
| 10 Imagerie | tab Easily | ❌ bbox = full page (**même ancre** que step 8) | ❌ | ❌ | ❌ | **élevé** : idem |
| 12 Notes médicales | tab Easily | ✅ bbox 146×48 zone tabs | ⚠ contraint via anchor mais target_text imprécis (OCR tronqué) | ❌ | ❌ | **moyen** |
| 14 Synthèse Urgences | tab Easily | ✅ bbox 192×47 zone tabs | ⚠ idem step 12 | ❌ | ❌ | **moyen** |
| 18 textarea DPI | placeholder italique | ✅ bbox 301×32 textarea | ⚠ contraint mais cible non-OCRisable au runtime | ❌ | ❌ | **élevé** : placeholder gris non rendu pour OCR |
**Constats généraux** :
- Aucun step n'a de `resolve_order` explicite → tout passe par la cascade par défaut serveur (OCR-DIRECT → template → VLM)
- Aucun step n'a de `verify_params` explicite → VERIFY = pHash global laxiste « any-change »
- Steps 8 et 10 partagent la même ancre `anchor_c3929b00816a_1778161050` (anomalie d'enregistrement, ou choix volontaire mais alors la cible n'est pas zonée)
---
## 5. Diagnostic par step (BDD vs runtime observé)
### Step 8 — Examens cliniques
- **BDD intention** : OCR libre sur full page, by_text="Examens cliniques"
- **Runtime** : clic à (590, 447) — **dans la rangée tabs Easily**, OK
- **Diagnostic** : **BDD imprécise mais runtime correct par chance** — docTR a renvoyé la première occurrence top-down qui se trouve être le tab. Fragile.
### Step 10 — Imagerie
- **BDD intention** : MÊME ancre full page, by_text="Imagerie"
- **Runtime** : clic à (606, **156**) — **bandeau Edge en haut**, ❌ raté
- **Diagnostic** : **BDD fautive** — OCR libre sur la même grande zone retourne une occurrence "Imagerie" différente cette fois (probablement le titre Edge ou un breadcrumb URL). Pas de chance comme step 8.
- **À ré-enregistrer** : avec bbox restreinte à la rangée tabs (~y 425-475)
### Step 12 — Notes médicales
- **BDD intention** : ancre bbox 146×48 zone tabs, target_text imprécis
- **Runtime** : clic à (516, 447) résolu par template_matching score 0.998
- **Diagnostic** : **BDD acceptable par chance** — la bbox est petite, template_matching a matché OK. Mais `target_text` enregistré pointe sur « **Imagerie Notes médical** » (cf. REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md §6 anomalie 1 : la bbox est décalée d'un cran à gauche, le crop montre Imagerie au lieu de Notes médicales).
- **À ré-enregistrer après-démo** pour fiabilité.
### Step 14 — Synthèse Urgences
- **BDD intention** : ancre bbox 192×47 zone tabs, target_text imprécis
- **Runtime** : clic à (689, 446) — REPORT success=True via template
- **MAIS** : `t_synthese` extrait à 1110 chars **identique en taille à `t_imagerie`****le tab n'a probablement pas vraiment basculé**, le panneau de contenu n'a pas changé
- **Diagnostic** : **BDD imprécise + VERIFY laxiste laisse passer un faux positif** — impossible de distinguer "tab Synthèse activé" vs "tab Synthèse cliqué mais l'app a ignoré l'event" avec le pHash global.
- REPLAY_BLOCAGE_NOTES §6 anomalie 1 dit aussi que cette bbox crop "Notes médicales" pas "Synthèse Urgences" → ré-enregistrer.
### Step 18 — Cliquer textarea DPI
- **BDD intention** : ancre bbox 301×32, target_text = placeholder italique « Coller ou saisir le dossier patient »
- **Runtime** : OCR-DIRECT NON TROUVÉ, VLM échoué (21s), template échoué, retry échoué → mode apprentissage humain (timeout 120s) → final `target_not_found`
- **Diagnostic** : **BDD fautive structurellement** — le placeholder italique gris d'un `<input>`/`<textarea>` n'est pas rendu de la même façon dans le DOM/canvas que du texte normal. docTR ne le détecte pas. VLM ne le voit pas non plus. Le template matching dépend de la similarité pixel exacte, qui peut varier (focus, couleur).
- **À ré-enregistrer** : cliquer **sur la zone textarea elle-même** (pas le texte placeholder) lors de l'enregistrement → ancre image = la zone visible du textarea + bbox restrictive. Idéalement, ajouter un label visible (h2 "DPI complet" au-dessus) sur la maquette `codage.html` qui sert de cible OCR fiable.
---
## 6. Recommandations à débattre (post-démo)
### Steps à ré-enregistrer en priorité
1. **Step 10 (Imagerie)** : créer une ancre dédiée avec bbox restrictive (~ 700, 425, 130, 50 d'après l'app Easily)
2. **Step 18 (textarea DPI)** : changer la cible — soit cliquer sur un label visible au-dessus du textarea, soit ajouter un repère visuel non-placeholder dans la maquette
3. **Steps 12 et 14** : à reposer en VWB record session pour éliminer les anomalies de bbox décalées d'un cran
### Améliorations BDD/serveur à proposer (post-démo, à débattre)
- Ajouter un champ `verify_strategy` dans `parameters_json` avec valeurs : `phash_global`, `region_phash`, `ocr_title`, `dom_class` (pour les pages web maquettes)
- Ajouter un champ `region_hint` dans `parameters_json` permettant de contraindre l'OCR à une zone même quand l'ancre est large (par exemple `{"region_hint": {"y_min": 0.25, "y_max": 0.32}}` pour la barre tabs)
- Forcer `resolve_order` explicite à l'enregistrement plutôt que de laisser le default serveur (dette : le default est implicite et peut changer)
### Constat fondamental
**La BDD ne contient pas de garde-fous explicites contre les ambiguïtés OCR.** Quand `by_text` est un mot court qui apparaît plusieurs fois sur la page (« Imagerie » dans le titre Edge + dans la barre tabs + dans le contenu), rien n'empêche docTR de matcher la mauvaise occurrence. Le seul filtre est l'ordre top-down de docTR, qui est implicite et fragile.
**Pas de modification BDD ni de code dans cet audit.** Document factuel à débattre par Dom post-démo.

737
docs/BENCH_MEDGEMMA.json Normal file
View File

@@ -0,0 +1,737 @@
[
{
"task": "cim10",
"case_id": "v1_idm_inferieur",
"model": "medgemma:4b",
"expected_exact": "I21.1",
"expected_family": "I21",
"predicted": "I21.9",
"score": "family",
"latency_s": 0.34,
"raw_output": "```json\n{\"code\": \"I21.9\", \"label\": \"Angine de poitrine, non spécifiée\"}\n```"
},
{
"task": "cim10",
"case_id": "v1_idm_inferieur",
"model": "pmsi-coder-v2:latest",
"expected_exact": "I21.1",
"expected_family": "I21",
"predicted": "I21.0",
"score": "family",
"latency_s": 5.52,
"raw_output": "{\"code\": \"I21.0\", \"label\": \"Infarctus transmural aigu du myocarde, de la paroi antérieure\"}"
},
{
"task": "cim10",
"case_id": "v1_idm_inferieur",
"model": "qwen2.5:7b",
"expected_exact": "I21.1",
"expected_family": "I21",
"predicted": "X00.0",
"score": "wrong",
"latency_s": 9.31,
"raw_output": "{\"code\": \"X00.0\", \"label\": \"Infarctus du myocarde\"}"
},
{
"task": "cim10",
"case_id": "v1_idm_inferieur",
"model": "gemma4:latest",
"expected_exact": "I21.1",
"expected_family": "I21",
"predicted": "I21.9",
"score": "family",
"latency_s": 25.41,
"raw_output": "```json\n{\"code\": \"I21.9\", \"label\": \"Infarctus du myocarde aigu\"}\n```"
},
{
"task": "cim10",
"case_id": "v2_pneumopathie",
"model": "medgemma:4b",
"expected_exact": "J13",
"expected_family": "J13",
"predicted": "J18.9",
"score": "wrong",
"latency_s": 4.93,
"raw_output": "```json\n{\"code\": \"J18.9\", \"label\": \"Pneumonie bactérienne, non spécifiée\"}\n```"
},
{
"task": "cim10",
"case_id": "v2_pneumopathie",
"model": "pmsi-coder-v2:latest",
"expected_exact": "J13",
"expected_family": "J13",
"predicted": "J13",
"score": "exact",
"latency_s": 4.39,
"raw_output": "{\"code\": \"J13\", \"label\": \"Pneumopathie due à des streptocoques, groupe B, non classée ailleurs\"}"
},
{
"task": "cim10",
"case_id": "v2_pneumopathie",
"model": "qwen2.5:7b",
"expected_exact": "J13",
"expected_family": "J13",
"predicted": "X00.0",
"score": "wrong",
"latency_s": 5.04,
"raw_output": "{\"code\": \"X00.0\", \"label\": \"PNEUMONIE ALVEOLAIRE\"}"
},
{
"task": "cim10",
"case_id": "v2_pneumopathie",
"model": "gemma4:latest",
"expected_exact": "J13",
"expected_family": "J13",
"predicted": "J13.0",
"score": "family",
"latency_s": 21.11,
"raw_output": "```json\n{\n \"code\": \"J13.0\",\n \"label\": \"Pneumonie lobaire\"\n}\n```"
},
{
"task": "cim10",
"case_id": "v3_avc_ischemique",
"model": "medgemma:4b",
"expected_exact": "I63.5",
"expected_family": "I63",
"predicted": "I63.9",
"score": "family",
"latency_s": 4.9,
"raw_output": "```json\n{\"code\": \"I63.9\", \"label\": \"Hémiplégie droite, sans spécification\"}\n```"
},
{
"task": "cim10",
"case_id": "v3_avc_ischemique",
"model": "pmsi-coder-v2:latest",
"expected_exact": "I63.5",
"expected_family": "I63",
"predicted": "I60.0",
"score": "wrong",
"latency_s": 4.56,
"raw_output": "{\"code\": \"I60.0\", \"label\": \"Hémorragie sous-arachnoïdienne de l'artère carotide\"}"
},
{
"task": "cim10",
"case_id": "v3_avc_ischemique",
"model": "qwen2.5:7b",
"expected_exact": "I63.5",
"expected_family": "I63",
"predicted": "X00.0",
"score": "wrong",
"latency_s": 5.02,
"raw_output": "{\"code\": \"X00.0\", \"label\": \"Infarctus du cerveau\"}"
},
{
"task": "cim10",
"case_id": "v3_avc_ischemique",
"model": "gemma4:latest",
"expected_exact": "I63.5",
"expected_family": "I63",
"predicted": "I63",
"score": "family",
"latency_s": 16.93,
"raw_output": "```json\n{\n \"code\": \"I63\",\n \"label\": \"Infarctus cérébral\"\n}\n```"
},
{
"task": "cim10",
"case_id": "v4_decompensation_cardiaque",
"model": "medgemma:4b",
"expected_exact": "I50.1",
"expected_family": "I50",
"predicted": "I50.9",
"score": "family",
"latency_s": 4.9,
"raw_output": "```json\n{\"code\": \"I50.9\", \"label\": \"Insuffisance cardiaque congestive, non spécifiée\"}\n```"
},
{
"task": "cim10",
"case_id": "v4_decompensation_cardiaque",
"model": "pmsi-coder-v2:latest",
"expected_exact": "I50.1",
"expected_family": "I50",
"predicted": "J96.0",
"score": "wrong",
"latency_s": 4.26,
"raw_output": "{\"code\": \"J96.0\", \"label\": \"Insuffisance respiratoire aiguë, type I [hypoxique]\"}"
},
{
"task": "cim10",
"case_id": "v4_decompensation_cardiaque",
"model": "qwen2.5:7b",
"expected_exact": "I50.1",
"expected_family": "I50",
"predicted": "X00.0",
"score": "wrong",
"latency_s": 4.94,
"raw_output": "{\"code\": \"X00.0\", \"label\": \"PNEUMONIE\"}"
},
{
"task": "cim10",
"case_id": "v4_decompensation_cardiaque",
"model": "gemma4:latest",
"expected_exact": "I50.1",
"expected_family": "I50",
"predicted": "I50.9",
"score": "family",
"latency_s": 22.26,
"raw_output": "```json\n{\n \"code\": \"I50.9\",\n \"label\": \"Insuffisance cardiaque\"\n}\n```"
},
{
"task": "cim10",
"case_id": "v5_dyspnee_symptome",
"model": "medgemma:4b",
"expected_exact": "R06.0",
"expected_family": "R06",
"predicted": "R06.0",
"score": "exact",
"latency_s": 4.84,
"raw_output": "```json\n{\"code\": \"R06.0\", \"label\": \"Dyspnée\"}\n```"
},
{
"task": "cim10",
"case_id": "v5_dyspnee_symptome",
"model": "pmsi-coder-v2:latest",
"expected_exact": "R06.0",
"expected_family": "R06",
"predicted": "X00.0",
"score": "wrong",
"latency_s": 4.43,
"raw_output": "{\"code\": \"X00.0\", \"label\": \"exposition à la fumée, au feu et aux flammes, intention non\"}"
},
{
"task": "cim10",
"case_id": "v5_dyspnee_symptome",
"model": "qwen2.5:7b",
"expected_exact": "R06.0",
"expected_family": "R06",
"predicted": "X00.0",
"score": "wrong",
"latency_s": 4.95,
"raw_output": "{\"code\": \"X00.0\", \"label\": \"Autres dyspnées\"}"
},
{
"task": "cim10",
"case_id": "v5_dyspnee_symptome",
"model": "gemma4:latest",
"expected_exact": "R06.0",
"expected_family": "R06",
"predicted": "J96.0",
"score": "wrong",
"latency_s": 15.77,
"raw_output": "```json\n{\"code\": \"J96.0\", \"label\": \"Dyspnée\"}\n```"
},
{
"task": "summary",
"case_id": "67_23001636",
"model": "medgemma:4b",
"n_bullets": 0,
"n_chars": 555,
"latency_s": 6.04,
"output": "Voici un résumé concis du compte-rendu :\n\n1. Maintien à domicile compliqué en l'absence de son époux hospitalisé, avec altération de l'état général et troubles de la marche.\n2. Maladie neurodégénérative, hypertension artérielle, syndrome anxieux, athérome carotidien.\n3. Vertiges, érysipèle du coude gauche, et détresse psychologique liée à la séparation de son époux.\n4. AMOXICILLINE, arrêt HYDROCHLOROTHIAZIDE, OXAZEPAM, SERESTA, KARDEGIC, ROSUVASTATINE, LERCANIDIPINE.\n5. Orientation vers SSR La Concha, avec suivi et ajustement de la médication.\n"
},
{
"task": "summary",
"case_id": "67_23001636",
"model": "pmsi-coder-v2:latest",
"n_bullets": 5,
"n_chars": 120,
"latency_s": 9.78,
"output": "- DP : Érysipèle du bras gauche\n- DA : Syndrome anxieux\n- T1 : Amoxicilline\n- T2 : Hydrochlorothiazide\n- T3 : Bisoprolol"
},
{
"task": "summary",
"case_id": "67_23001636",
"model": "qwen2.5:7b",
"n_bullets": 5,
"n_chars": 704,
"latency_s": 9.99,
"output": "- Motif d'admission : Altération de l'état général, troubles de la marche, maintien à domicile compliqué en absence de son époux hospitalisé.\n- Antécédents pertinents : Maladie neurodégénérative avec troubles cognitifs modérés, hypertension artérielle, athérome carotidien, syndrome anxieux.\n- Diagnostic(s) retenu(s) : Vertiges d'origine non localisée, érysipèle du coude gauche.\n- Traitements engagés : Amoxicilline pour l'erysipèle, arrêt de l'Hydrochlorothiazide, oxazepam et Seresta pour la gestion anxieuse.\n- Évolution / orientation : La patiente a bien répondu au traitement antibactérien, elle est capable de marcher sans symptôme neurologique. Elle est orientée au SSR La Concha avec son époux."
},
{
"task": "summary",
"case_id": "67_23001636",
"model": "gemma4:latest",
"n_bullets": 5,
"n_chars": 780,
"latency_s": 29.74,
"output": "- Motif d'admission : Faiblesse générale et vertiges, dans un contexte de maintien à domicile jugé précaire en l'absence de son époux.\n- Antécédents pertinents : Maladie neurodégénérative avec troubles cognitifs modérés, Hypertension artérielle, Athérome carotidien et syndrome anxieux.\n- Diagnostic(s) retenu(s) : Erysipèle du coude gauche (traité), Vertiges, et difficulté de maintien à domicile en l'absence de son conjoint.\n- Traitements engagés : Mise sous AMOXICILLINE (3 jours), BISOPROLOL, ACIDE ACETYLSALICYLIQUE, LERCANIDIPINE et ROSUVASTATINE. Ajout de SERESTA/OXAZEPAM PRN pour gérer l'anxiété et la déambulation nocturne.\n- Évolution / orientation : Évolution neurologique favorable avec récupération complète des symptômes. La patiente est orientée au SSR La Concha."
},
{
"task": "summary",
"case_id": "103_23056749",
"model": "medgemma:4b",
"n_bullets": 5,
"n_chars": 678,
"latency_s": 6.14,
"output": "Voici un résumé concis du compte-rendu :\n\n- Maintien à domicile difficile en raison de troubles anxio-dépressifs, d'aboulie, de clinophilie et de troubles cognitifs.\n- Antécédents de syndrome anxio-dépressif majeur avec hallucinations acoustico-verbales et idées de persécution en 2020.\n- Bilan neurologique pour tremblements en cours, mais sans argument pour une maladie de Parkinson ou une synucléopathie.\n- Le patient(e) présente une personnalité anxieuse, une anxiété majeure, une plainte mnésique et une suspicion de maladie neuro dégénérative.\n- Transfert en psychiatrie à [VILLE] avec prescription de médicaments (alprazolam, modopar, théralène, venlafaxine, zopiclone).\n"
},
{
"task": "summary",
"case_id": "103_23056749",
"model": "pmsi-coder-v2:latest",
"n_bullets": 5,
"n_chars": 637,
"latency_s": 25.91,
"output": "- 1. Motif d'admission : Maintien à domicile difficile chez une patiente âgée avec des antécédents de troubles cognitifs et un syndrome anxio-dépressif.\n- 2. Antécédents pertinents : Syndrome anxio-dépressif ancien, tremblements, bilan neurologique, et prise en charge psychiatrique.\n- 3. Diagnostic(s) retenu(s) : MADD (Maladie d'Alzheimer avec début précoce), syndrome anxio-dépressif, et possible trouble neurodégénératif.\n- 4. Traitements engagés : Test d'évaluation du risque de démence, et potentiellement traitement psychiatrique.\n- 5. Évolution / orientation : Transfert en psychiatrie pour évaluation et suivi du trouble mental."
},
{
"task": "summary",
"case_id": "103_23056749",
"model": "qwen2.5:7b",
"n_bullets": 5,
"n_chars": 498,
"latency_s": 9.07,
"output": "- Motif d'admission : maintien à domicile difficile\n- Antécédents pertinents : syndrome anxio dépressif, hospitalisation en 2020 pour syndrome dépressif majeur avec hallucinations, fracture des deux humérus en 2017\n- Diagnostic(s) retenu(s) : maladie dépressive majeure (MDD), syndrome anxio dépressif ancien, troubles cognitifs débutants\n- Traitements engagés : alprazolam, modopar, théralène, venlafaxine, zopiclone\n- Évolution / orientation : transfert en psychiatrie à [ETABLISSEMENT] à [VILLE]"
},
{
"task": "summary",
"case_id": "103_23056749",
"model": "gemma4:latest",
"n_bullets": 5,
"n_chars": 703,
"latency_s": 28.99,
"output": "- Motif d'admission : Difficulté à maintenir à domicile en raison d'une pathologie anxio-dépressive et de troubles cognitifs évolutifs.\n- Antécédents : Syndrome anxio-dépressif sévère (avec hallucinations en 2020), bilan neurologique de tremblements (DAT scan négatif).\n- Diagnostic : Syndrome anxio-dépressif majeur (MDD) avec troubles cognitifs débutants. Suspicion de maladie neurodégénérative écartée ou faussée par les neuroleptiques.\n- Traitements : Mise en place d'un traitement polythérapeutique (ALPRAZOLAM, VENLAFAXINE, MODOPAR, THERALENE, ZOPICLONE).\n- Évolution / Orientation : Transfert en psychiatrie pour prise en charge spécialisée et coordination des aides à domicile (AS de proximité)."
},
{
"task": "summary",
"case_id": "407_23116460",
"model": "medgemma:4b",
"n_bullets": 5,
"n_chars": 669,
"latency_s": 6.61,
"output": "Voici un résumé concis du compte-rendu :\n\n- Motif d'admission : Diabète de type 2 compliqué, AVC en mai 2023, introduction de GLP1 en neurologie.\n- Antécédents pertinents : Hypertension artérielle, insuffisance rénnelle chronique, AVC, rétinopathie minime et maculopathie, cardio.\n- Diagnostic(s) retenu(s) : Diabète de type 2 compliqué sur le plan vasculaire, syndrome dépressif.\n- Traitements engagés : Pas de modification du traitement antidiabétique, introduction de seroplex, supplémentation en uvedose, vaccination non faite.\n- Évolution / orientation : Patient(e) quitte le service avec les consignes d'usage, suivi HbA1c tous les 3 mois, CS cardio à organiser.\n"
},
{
"task": "summary",
"case_id": "407_23116460",
"model": "pmsi-coder-v2:latest",
"n_bullets": 5,
"n_chars": 64,
"latency_s": 29.84,
"output": "- 19/06/2023\n- 23/06/2023\n- 23/06/2023\n- 23/06/2023\n- 23/06/2023"
},
{
"task": "summary",
"case_id": "407_23116460",
"model": "qwen2.5:7b",
"n_bullets": 5,
"n_chars": 821,
"latency_s": 12.95,
"output": "- Motif d'admission : Première éducation dans le cadre d'un diabète de type 2, introduction de GLP1 après AVC en mai 2023 (HbA1c 7,9%)\n- Antécédents pertinents : Diabète de type 2, hypertension artérielle, AVC récent, rétinopathie minime et maculopathie, traitement antidiabétique modifié\n- Diagnostic(s) retenu(s) : Diabète de type 2 compliqué sur le plan vasculaire, syndrome dépressif\n- Traitements engagés : Gliclazide, Trulicity, Eliquis, Nebivolol, Perindopril, Amlodipine, Pravastatine, supplémentation en uvedose et introduction de Seroplex\n- Évolution / orientation : Patient quitte le service avec son traitement antidiabétique maintenu, suivi HbA1C tous les 3 mois, objectif 7,5%, organisation de la prise d'IVT, éducation médicamenteuse pour prévenir les effets indésirables et optimiser l'usage des piluliers"
},
{
"task": "summary",
"case_id": "407_23116460",
"model": "gemma4:latest",
"n_bullets": 5,
"n_chars": 793,
"latency_s": 33.85,
"output": "- **Motif d'admission :** Première éducation thérapeutique et optimisation du traitement du diabète de type 2, suite à un AVC survenu en mai 2023.\n- **Antécédents pertinents :** Diabète de type 2 (IMC 33,4 kg/m²), HTA, antécédent d'AVC, rétinopathie/maculopathie, et syndrome dépressif.\n- **Diagnostic(s) retenu(s) :** Diabète de type 2 compliqué sur le plan vasculaire, actuellement bien équilibré grâce à l'introduction d'un agent GLP-1.\n- **Traitements engagés :** Traitement multivarié incluant Metformine, Gliclazide, Trulicity (GLP-1), Nebivolol, Perindopril, Amlodipine, Eliquis, Pravastatine, et Seroplex.\n- **Évolution / orientation :** Reprise du suivi des comorbidités (Cardio, Néphro, Ophtalmo). Maintien de l'éducation thérapeutique et suivi régulier de l'HbA1c (objectif < 7,5%)."
},
{
"task": "extraction",
"case_id": "67_23001636",
"model": "medgemma:4b",
"conformity": "conforme",
"filled_fields": 7,
"parsed": {
"motif_admission": "Vertiges, maintien à domicile difficile en l'absence de son époux.",
"diagnostics": [
"Maladie neurodégénérative avec troubles cognitifs modérés.",
"Hypertension artérielle.",
"Syndrome anxieux.",
"Atherosclérose carotidienne.",
"Fébricule à 38°C avec érysipèle du bras gauche.",
"Vertiges."
],
"antecedents": [
"Maladie neurodégénérative avec troubles cognitifs modérés.",
"Hypertension artérielle.",
"Atherosclérose carotidienne.",
"Syndrome anxieux."
],
"traitements": [
"KARDEGIC 75MG 1 LE MATIN",
"ROSUVASTATINE 5MG 1 LE SOIR",
"BISOPROLOL/HYDROCHLOROTIAZIDE 10/6.25MG 1 LE MATIN",
"LERCAN 10MG 1 LE SOIR",
"CLINUTREN 2/JOUR",
"CARTEOL 2% : 1GOUTTE DANS CHAQUE ŒIL LE MATIN",
"AMOXICILLINE",
"AMOXICILLINE ARW 1G",
"BISOPROLOL ARL 10MG",
"ACIDE ACETYLSALICYLIQUE",
"LERCANIDIPINE ACT",
"ROSUVASTATINE ARW",
"SERESTA 10MG CPR (ORALE) 0,5 CPR A administrer : toutes les 6 heure(s) si agitation o angoisse pendant 30 jour(s)",
"OXAZEPAM - SERESTA 10MG CPR (ORALE) 0,5 CPR A administrer : toutes les 6 heure(s) si agitation o angoisse pendant 30 jour(s)"
],
"date_admission": "05/06/2023",
"date_sortie": "12/06/2023",
"duree_sejour_jours": 7
},
"latency_s": 8.27,
"raw_output": "```json\n{\n \"motif_admission\": \"Vertiges, maintien à domicile difficile en l'absence de son époux.\",\n \"diagnostics\": [\n \"Maladie neurodégénérative avec troubles cognitifs modérés.\",\n \"Hypertension artérielle.\",\n \"Syndrome anxieux.\",\n \"Atherosclérose carotidienne.\",\n \"Fébricule à 38°C avec érysipèle du bras gauche.\",\n \"Vertiges.\"\n ],\n \"antecedents\": [\n \"Maladie neurodégénérative avec troubles cognitifs modérés.\",\n \"Hypertension artérielle.\",\n \"Atherosclérose carotidienne.\",\n \"Syndrome anxieux.\"\n ],\n \"traitements\": [\n \"KARDEGIC 75MG 1 LE MATIN\",\n \"ROSUVASTATINE 5MG 1 LE SOIR\",\n \"BISOPROLOL/HYDROCHLOROTIAZIDE 10/6.25MG 1 LE MATIN\",\n \"LERCAN 10MG 1 LE SOIR\",\n \"CLINUTREN 2/JOUR\",\n \"CARTEOL 2% : 1GOUTTE DANS CHAQUE ŒIL LE MATIN\",\n \"AMOXICIL"
},
{
"task": "extraction",
"case_id": "67_23001636",
"model": "pmsi-coder-v2:latest",
"conformity": "conforme",
"filled_fields": 7,
"parsed": {
"motif_admission": "La patiente présente des symptômes neurologiques et un érythème, nécessitant une évaluation et une prise en charge.",
"diagnostics": "Les diagnostics retenus sont : érythème, fièvre, infection, hémorragie, occlusion, et les anomalies biologiques (CRP, ASAT, ALAT, hémoglobine, plaquettes, leucocytes, créatinine). Le diagnostic principal est l'érythème, et les autres sont des diagnostics associés.",
"antecedents": "Les antécédents notables sont l'épilepsie, les accidents ischémiques cérébraux transitoires (AICs), et les complications neurologiques. La patiente a également des antécédents de traitement médicamenteux (hydrochlorothiazide, levetiracetam, etc.).",
"traitements": "Les traitements engagés pendant le séjour comprennent l'arrêt de l'hydrochlorothiazide, la mise en place d'un cathéter, et potentiellement d'autres interventions médicales. Le traitement de l'érythème est également à considérer.",
"date_admission": "2023-06-05",
"date_sortie": "2023-06-12",
"duree_sejour_jours": 7
},
"latency_s": 30.25,
"raw_output": "{\"motif_admission\": \"La patiente présente des symptômes neurologiques et un érythème, nécessitant une évaluation et une prise en charge.\", \"diagnostics\": \"Les diagnostics retenus sont : érythème, fièvre, infection, hémorragie, occlusion, et les anomalies biologiques (CRP, ASAT, ALAT, hémoglobine, plaquettes, leucocytes, créatinine). Le diagnostic principal est l'érythème, et les autres sont des diagnostics associés.\", \"antecedents\": \"Les antécédents notables sont l'épilepsie, les accidents ischémiques cérébraux transitoires (AICs), et les complications neurologiques. La patiente a également des antécédents de traitement médicamenteux (hydrochlorothiazide, levetiracetam, etc.).\", \"traitements\": \"Les traitements engagés pendant le séjour comprennent l'arrêt de l'hydrochlorothiazide, la mise "
},
{
"task": "extraction",
"case_id": "67_23001636",
"model": "qwen2.5:7b",
"conformity": "conforme",
"filled_fields": 4,
"parsed": {
"motif_admission": "Altération de l'état général avec troubles de la marche, maintien à domicile compliqué en l'absence de son époux hospitalisé.",
"diagnostics": [
"Maladie neurodégénérative avec troubles cognitifs modérés",
"Hypertension artérielle",
"Athérome carotidien",
"Syndrome anxieux"
],
"antecedents": [
"Maintien à domicile compliqué en l'absence de son époux hospitalisé",
"Hospitalisation précédente du 05/06 au 12/06/2023"
],
"traitements": [
"KARDEGIC 75MG 1 LE MATIN",
"ROSUVASTATINE 5MG 1 LE SOIR",
"BISOPROLOL/HYDROCHLOROTIAZIDE 10/6.25MG 1 LE MATIN",
"LERCAN 10MG 1 LE SOIR",
"CLINUTREN 2/JOUR",
"CARTEOL 2% : 1GOUTTE DANS CHAQUE ŒIL LE MATIN",
"AMOXICILLINE",
"OXAZEPAM",
"SERESTA 10MG CPR (ORALE) 0,5 CPR A administrer : toutes les 6 heure(s) pendant 30 jour(s)",
"AMOXICILLINE ARW 1G CPR DISP (ORALE) 1 G A administrer : matin midi soir pendant 3 jour(s)",
"BISOPROLOL ARL 10MG CPR (ORALE) 1 CPR A administrer : matin [8h] A partir du 13/06/2023 pendant 30 jour(s)",
"ACIDE ACETYLSALICYLIQUE KARDEGIC 75MG PDR ORALE 1 SAC A administrer : matin [8h] A partir du 13/06/2023 pendant 30 jour(s)",
"LERCANIDIPINE ACT 10MG CPR (ORALE) 1 CPR A administrer : soir [19h] pendant 30 jour(s)",
"ROSUVASTATINE ARW 5MG CPR (ORALE) 1 CPR A administrer : prise unique à 19h pendant 30 jour(s)"
],
"date_admission": null,
"date_sortie": null,
"duree_sejour_jours": null
},
"latency_s": 19.05,
"raw_output": "{\n \"motif_admission\": \"Altération de l'état général avec troubles de la marche, maintien à domicile compliqué en l'absence de son époux hospitalisé.\",\n \"diagnostics\": [\"Maladie neurodégénérative avec troubles cognitifs modérés\", \"Hypertension artérielle\", \"Athérome carotidien\", \"Syndrome anxieux\"],\n \"antecedents\": [\"Maintien à domicile compliqué en l'absence de son époux hospitalisé\", \"Hospitalisation précédente du 05/06 au 12/06/2023\"],\n \"traitements\": [\"KARDEGIC 75MG 1 LE MATIN\", \"ROSUVASTATINE 5MG 1 LE SOIR\", \"BISOPROLOL/HYDROCHLOROTIAZIDE 10/6.25MG 1 LE MATIN\", \"LERCAN 10MG 1 LE SOIR\", \"CLINUTREN 2/JOUR\", \"CARTEOL 2% : 1GOUTTE DANS CHAQUE ŒIL LE MATIN\", \"AMOXICILLINE\", \"OXAZEPAM\", \"SERESTA 10MG CPR (ORALE) 0,5 CPR A administrer : toutes les 6 heure(s) pendant 30 jour(s)\", \"AMOXICIL"
},
{
"task": "extraction",
"case_id": "67_23001636",
"model": "gemma4:latest",
"conformity": "conforme",
"filled_fields": 7,
"parsed": {
"motif_admission": "Altération de létat général avec troubles de la marche",
"diagnostics": [
"Erysipèle du coude gauche",
"Vertiges",
"Altération de létat général"
],
"antecedents": [
"Maladie neurodégénérative avec troubles cognitifs modérés",
"Hypertension artérielle",
"Athérome carotidien",
"Syndrome anxieux"
],
"traitements": [
"AMOXICILLINE",
"OXAZEPAM",
"SERESTA",
"BISOPROLOL",
"KARDEGIC",
"LERCANIDIPINE",
"ROSUVASTATINE"
],
"date_admission": "05/06/2023",
"date_sortie": "12/06/2023",
"duree_sejour_jours": 7
},
"latency_s": 38.94,
"raw_output": "```json\n{\n \"motif_admission\": \"Altération de létat général avec troubles de la marche\",\n \"diagnostics\": [\n \"Erysipèle du coude gauche\",\n \"Vertiges\",\n \"Altération de létat général\"\n ],\n \"antecedents\": [\n \"Maladie neurodégénérative avec troubles cognitifs modérés\",\n \"Hypertension artérielle\",\n \"Athérome carotidien\",\n \"Syndrome anxieux\"\n ],\n \"traitements\": [\n \"AMOXICILLINE\",\n \"OXAZEPAM\",\n \"SERESTA\",\n \"BISOPROLOL\",\n \"KARDEGIC\",\n \"LERCANIDIPINE\",\n \"ROSUVASTATINE\"\n ],\n \"date_admission\": \"05/06/2023\",\n \"date_sortie\": \"12/06/2023\",\n \"duree_sejour_jours\": 7\n}\n```"
},
{
"task": "extraction",
"case_id": "103_23056749",
"model": "medgemma:4b",
"conformity": "conforme",
"filled_fields": 7,
"parsed": {
"motif_admission": "maintien à domicile difficile",
"diagnostics": [
"syndrome anxio depressif",
"tremblements",
"fracture des deux humérus",
"maladie de Parkinson atypique",
"maladie de Parkinson",
"maladie de Parkinson faussée par le neuroleptique",
"MDD",
"syndrome dépressif majeur avec hallucinations acoustico verbales et idées de persécution"
],
"antecedents": [
"syndrome anxio depressif suivi Dr [NOM] hospitalisation en 2020 pour syndrome depressif majeur avec hallucinations acoustico verbales et idées de persécution ([ETABLISSEMENT])",
"bilan neurologique Dr [NOM] pour tremblements : IRMc leucopathie vasculaire, DAT scan RAS, mise en place dun traitement dépreuve par MODOPAR dépreuve (éléments frontaux et syndrome parkinsonien atypique)",
"fracture des deux humérus en 2017",
"divorcée, une fille qui vit à Bayonne",
"ancienne journaliste radio france",
"aucune aide, pas dide, pas daide ménagère",
"syndrome anxio depressif ancien avec tr attentionnels majeurs"
],
"traitements": [
"ALPRAZOLAM ARW 0,25MG CPR [30] COMPRIME(S) 1 COMPRIME (ORALE) matin midi soir",
"MODOPAR 125 (100MG/25MG) CPR 1 CPRDISP (ORALE) matin midi soir",
"THERALENE 40MG/ML SOL BUV 30ML [1200] Flacon(s) 20 GOUTTE (ORALE) nuit [21h]",
"VENLAFAXINE ARW LP 75MG GELULE [30] Gelule(s) 1 GELULE (ORALE) soir [19h]",
"ZOPICLONE EG 7,5MG CPR [14] COMPRIME(S) 1 COMPRIME (ORALE) nuit [21h]"
],
"date_admission": "18/03/2023",
"date_sortie": "21/03/2023",
"duree_sejour_jours": 3
},
"latency_s": 8.73,
"raw_output": "```json\n{\n \"motif_admission\": \"maintien à domicile difficile\",\n \"diagnostics\": [\n \"syndrome anxio depressif\",\n \"tremblements\",\n \"fracture des deux humérus\",\n \"maladie de Parkinson atypique\",\n \"maladie de Parkinson\",\n \"maladie de Parkinson faussée par le neuroleptique\",\n \"MDD\",\n \"syndrome dépressif majeur avec hallucinations acoustico verbales et idées de persécution\"\n ],\n \"antecedents\": [\n \"syndrome anxio depressif suivi Dr [NOM] hospitalisation en 2020 pour syndrome depressif majeur avec hallucinations acoustico verbales et idées de persécution ([ETABLISSEMENT])\",\n \"bilan neurologique Dr [NOM] pour tremblements : IRMc leucopathie vasculaire, DAT scan RAS, mise en place dun traitement dépreuve par MODOPAR dépreuve (éléments frontaux et syndrome parkinso"
},
{
"task": "extraction",
"case_id": "103_23056749",
"model": "pmsi-coder-v2:latest",
"conformity": "conforme",
"filled_fields": 7,
"parsed": {
"motif_admission": "Mise en observation pour suspicion de maladie neurodégénérative",
"diagnostics": "Syndrome anxio-dépressif ancien, tremblements essentiels, bilan neurologique avec DAT scan négatif, absence de confirmation de maladie neurodégénérative",
"antecedents": "Antécédents de syndrome anxio-dépressif, tremblements essentiels, bilan neurologique, traitement par Théralène",
"traitements": "Mise en observation, bilan neurologique, traitement par Théralène",
"date_admission": "2022-12-18",
"date_sortie": "2022-12-21",
"duree_sejour_jours": 3
},
"latency_s": 25.49,
"raw_output": "{\"motif_admission\": \"Mise en observation pour suspicion de maladie neurodégénérative\", \"diagnostics\": \"Syndrome anxio-dépressif ancien, tremblements essentiels, bilan neurologique avec DAT scan négatif, absence de confirmation de maladie neurodégénérative\", \"antecedents\": \"Antécédents de syndrome anxio-dépressif, tremblements essentiels, bilan neurologique, traitement par Théralène\", \"traitements\": \"Mise en observation, bilan neurologique, traitement par Théralène\", \"date_admission\": \"2022-12-18\", \"date_sortie\": \"2022-12-21\", \"duree_sejour_jours\": 3}"
},
{
"task": "extraction",
"case_id": "103_23056749",
"model": "qwen2.5:7b",
"conformity": "conforme",
"filled_fields": 7,
"parsed": {
"motif_admission": "maintien à domicile difficile",
"diagnostics": [
"MDD",
"syndrome anxio depressif",
"troubles cognitifs débutants"
],
"antecedents": [
"fracture des deux humérus en 2017",
"syndrome depressif majeur avec hallucinations acoustico verbales et idées de persécution (2020)",
"tremblements (2023)"
],
"traitements": [
"ALPRAZOLAM ARW 0,25MG CPR",
"MODOPAR 125",
"THERALENE 40MG/ML SOL BUV",
"VENLAFAXINE ARW LP",
"ZOPICLONE EG"
],
"date_admission": "18/03/2023",
"date_sortie": "21/03/2023",
"duree_sejour_jours": 3
},
"latency_s": 10.76,
"raw_output": "{\n \"motif_admission\": \"maintien à domicile difficile\",\n \"diagnostics\": [\"MDD\", \"syndrome anxio depressif\", \"troubles cognitifs débutants\"],\n \"antecedents\": [\"fracture des deux humérus en 2017\", \"syndrome depressif majeur avec hallucinations acoustico verbales et idées de persécution (2020)\", \"tremblements (2023)\"],\n \"traitements\": [\"ALPRAZOLAM ARW 0,25MG CPR\", \"MODOPAR 125\", \"THERALENE 40MG/ML SOL BUV\", \"VENLAFAXINE ARW LP\", \"ZOPICLONE EG\"],\n \"date_admission\": \"18/03/2023\",\n \"date_sortie\": \"21/03/2023\",\n \"duree_sejour_jours\": 3\n}"
},
{
"task": "extraction",
"case_id": "103_23056749",
"model": "gemma4:latest",
"conformity": "conforme",
"filled_fields": 7,
"parsed": {
"motif_admission": "Maintien à domicile difficile",
"diagnostics": [
"Trouble anxio depressif (MDD)",
"Troubles cognitifs débutants"
],
"antecedents": [
"Syndrome anxio depressif (hospitalisation 2020)",
"Bilan neurologique pour tremblements (IRMc leucopathie vasculaire, DAT scan RAS)",
"Fracture des deux humérus (2017)"
],
"traitements": [
"ALPRAZOLAM ARW 0,25MG",
"MODOPAR 125",
"THERALENE 40MG/ML",
"VENLAFAXINE ARW LP 75MG",
"ZOPICLONE EG 7,5MG"
],
"date_admission": "18/03/2023",
"date_sortie": "21/03/2023",
"duree_sejour_jours": 3
},
"latency_s": 38.27,
"raw_output": "```json\n{\n \"motif_admission\": \"Maintien à domicile difficile\",\n \"diagnostics\": [\n \"Trouble anxio depressif (MDD)\",\n \"Troubles cognitifs débutants\"\n ],\n \"antecedents\": [\n \"Syndrome anxio depressif (hospitalisation 2020)\",\n \"Bilan neurologique pour tremblements (IRMc leucopathie vasculaire, DAT scan RAS)\",\n \"Fracture des deux humérus (2017)\"\n ],\n \"traitements\": [\n \"ALPRAZOLAM ARW 0,25MG\",\n \"MODOPAR 125\",\n \"THERALENE 40MG/ML\",\n \"VENLAFAXINE ARW LP 75MG\",\n \"ZOPICLONE EG 7,5MG\"\n ],\n \"date_admission\": \"18/03/2023\",\n \"date_sortie\": \"21/03/2023\",\n \"duree_sejour_jours\": 3\n}\n```"
},
{
"task": "extraction",
"case_id": "407_23116460",
"model": "medgemma:4b",
"conformity": "conforme",
"filled_fields": 7,
"parsed": {
"motif_admission": "Première éducation dans le cadre d'un diabète de type 2",
"diagnostics": [
"Diabète de type 2",
"AVC",
"Hypertension artérielle",
"Carence en vitamine D",
"Syndrome dépressif"
],
"antecedents": [
"Diabète de type 2",
"AVC",
"Hypertension artérielle",
"Maladie cardiaque",
"Onychomycose",
"Pallesthésie",
"Poids excessif",
"Pas de vaccination contre la grippe"
],
"traitements": [
"GLP1",
"Metformine",
"Gliclazide",
"Trulicity",
"Nebivolol",
"Pravastatine",
"Eliquis",
"Perindopril",
"Amlodipine",
"Velmetia",
"Seroplex",
"Uvedose",
"IVT",
"Uvedose"
],
"date_admission": "19/06/2023",
"date_sortie": "23/06/2023",
"duree_sejour_jours": 4
},
"latency_s": 7.53,
"raw_output": "```json\n{\n \"motif_admission\": \"Première éducation dans le cadre d'un diabète de type 2\",\n \"diagnostics\": [\n \"Diabète de type 2\",\n \"AVC\",\n \"Hypertension artérielle\",\n \"Carence en vitamine D\",\n \"Syndrome dépressif\"\n ],\n \"antecedents\": [\n \"Diabète de type 2\",\n \"AVC\",\n \"Hypertension artérielle\",\n \"Maladie cardiaque\",\n \"Onychomycose\",\n \"Pallesthésie\",\n \"Poids excessif\",\n \"Pas de vaccination contre la grippe\"\n ],\n \"traitements\": [\n \"GLP1\",\n \"Metformine\",\n \"Gliclazide\",\n \"Trulicity\",\n \"Nebivolol\",\n \"Pravastatine\",\n \"Eliquis\",\n \"Perindopril\",\n \"Amlodipine\",\n \"Velmetia\",\n \"Seroplex\",\n \"Uvedose\",\n \"IVT\",\n \"Uvedose\"\n ],\n \"date_admission\": \"19/06/2023\",\n \"date_sortie\": \"23/06/2023\",\n \"duree_sejour_jours\": 4\n}\n``"
},
{
"task": "extraction",
"case_id": "407_23116460",
"model": "pmsi-coder-v2:latest",
"conformity": "conforme",
"filled_fields": 7,
"parsed": {
"motif_admission": "diabète de type 2",
"diagnostics": "diabète de type 2, obésité (imc 33.442)",
"antecedents": "antécédents familiaux de diabète de type 2",
"traitements": "insuline, glucagon",
"date_admission": "2023-06-19",
"date_sortie": "2023-06-23",
"duree_sejour_jours": 4
},
"latency_s": 35.23,
"raw_output": "{\"motif_admission\": \"diabète de type 2\", \"diagnostics\": \"diabète de type 2, obésité (imc 33.442)\", \"antecedents\": \"antécédents familiaux de diabète de type 2\", \"traitements\": \"insuline, glucagon\", \"date_admission\": \"2023-06-19\", \"date_sortie\": \"2023-06-23\", \"duree_sejour_jours\": 4}"
},
{
"task": "extraction",
"case_id": "407_23116460",
"model": "qwen2.5:7b",
"conformity": "conforme",
"filled_fields": 7,
"parsed": {
"motif_admission": "Première éducation dans le cadre d'un diabète de type 2",
"diagnostics": [
"Diabète de type 2 compliqué sur le plan vasculaire",
"Syndrome dépressif"
],
"antecedents": [
"AVC fin mai",
"Hypertension artérielle (HTA)",
"SAOS appareillé",
"Diabète",
"Hyperlipidémie",
"Ralentisement idéomoteur",
"Onychomycose"
],
"traitements": [
"Nebivolol 5 mg 1-0-0",
"Pravastatine 40 mg 1 le soir",
"Eliquis 5 : 1 cp matin et soir",
"Perindopril 10mg 1-0-0",
"Amlodipine 5 le matin",
"Velmetia 50/1000mg 1-0-1",
"Gliclazide 30mg 2-0-0",
"Trulicity 1,5 les mardis",
"Seroplex",
"Uvedose"
],
"date_admission": "19/06/2023",
"date_sortie": "23/06/2023",
"duree_sejour_jours": 4
},
"latency_s": 14.27,
"raw_output": "{\n \"motif_admission\": \"Première éducation dans le cadre d'un diabète de type 2\",\n \"diagnostics\": [\"Diabète de type 2 compliqué sur le plan vasculaire\", \"Syndrome dépressif\"],\n \"antecedents\": [\"AVC fin mai\", \"Hypertension artérielle (HTA)\", \"SAOS appareillé\", \"Diabète\", \"Hyperlipidémie\", \"Ralentisement idéomoteur\", \"Onychomycose\"],\n \"traitements\": [\"Nebivolol 5 mg 1-0-0\", \"Pravastatine 40 mg 1 le soir\", \"Eliquis 5 : 1 cp matin et soir\", \"Perindopril 10mg 1-0-0\", \"Amlodipine 5 le matin\", \"Velmetia 50/1000mg 1-0-1\", \"Gliclazide 30mg 2-0-0\", \"Trulicity 1,5 les mardis\", \"Seroplex\", \"Uvedose\"],\n \"date_admission\": \"19/06/2023\",\n \"date_sortie\": \"23/06/2023\",\n \"duree_sejour_jours\": 4\n}"
},
{
"task": "extraction",
"case_id": "407_23116460",
"model": "gemma4:latest",
"conformity": "conforme",
"filled_fields": 7,
"parsed": {
"motif_admission": "Première éducation dans le cadre d'un diabète de type 2",
"diagnostics": [
"Diabète de type 2",
"HTA",
"AVC",
"Rétinopathie et maculopathie",
"Syndrome dépressif"
],
"antecedents": [
"Diabète de type 2",
"HTA",
"AVC",
"Ralentisement idéomoteur"
],
"traitements": [
"Pravastatine 40 mg",
"Eliquis 5",
"Perindopril 10mg",
"Amlodipine 5",
"Velmetia 50/1000mg",
"Gliclazide 30mg",
"Metformine 1000mg",
"Nebivolol 5 mg",
"Trulicity 1,5 mg",
"Seroplex"
],
"date_admission": "19/06/2023",
"date_sortie": "23/06/2023",
"duree_sejour_jours": 4
},
"latency_s": 39.71,
"raw_output": "```json\n{\n \"motif_admission\": \"Première éducation dans le cadre d'un diabète de type 2\",\n \"diagnostics\": [\n \"Diabète de type 2\",\n \"HTA\",\n \"AVC\",\n \"Rétinopathie et maculopathie\",\n \"Syndrome dépressif\"\n ],\n \"antecedents\": [\n \"Diabète de type 2\",\n \"HTA\",\n \"AVC\",\n \"Ralentisement idéomoteur\"\n ],\n \"traitements\": [\n \"Pravastatine 40 mg\",\n \"Eliquis 5\",\n \"Perindopril 10mg\",\n \"Amlodipine 5\",\n \"Velmetia 50/1000mg\",\n \"Gliclazide 30mg\",\n \"Metformine 1000mg\",\n \"Nebivolol 5 mg\",\n \"Trulicity 1,5 mg\",\n \"Seroplex\"\n ],\n \"date_admission\": \"19/06/2023\",\n \"date_sortie\": \"23/06/2023\",\n \"duree_sejour_jours\": 4\n}\n```"
}
]

542
docs/BENCH_MEDGEMMA.md Normal file
View File

@@ -0,0 +1,542 @@
# Benchmark medgemma:4b — démo médicale
_Généré le 2026-04-26 23:54:37_
## 1. Codage CIM-10 (5 vignettes)
| Modèle | Exact | Famille | Faux | Parse error | Latence moy. |
|---|---:|---:|---:|---:|---:|
| `gemma4:latest` | 0/5 | 4/5 | 1/5 | 0/5 | 20.3s |
| `medgemma:4b` | 1/5 | 3/5 | 1/5 | 0/5 | 4.0s |
| `pmsi-coder-v2:latest` | 1/5 | 1/5 | 3/5 | 0/5 | 4.6s |
| `qwen2.5:7b` | 0/5 | 0/5 | 5/5 | 0/5 | 5.9s |
### Détail par vignette
**v1_idm_inferieur** — attendu `I21.1` (Infarctus du myocarde inférieur)
| Modèle | Prédit | Score | Latence |
|---|---|---|---:|
| `medgemma:4b` | `I21.9` | family | 0.34s |
| `pmsi-coder-v2:latest` | `I21.0` | family | 5.52s |
| `qwen2.5:7b` | `X00.0` | wrong | 9.31s |
| `gemma4:latest` | `I21.9` | family | 25.41s |
**v2_pneumopathie** — attendu `J13` (Pneumonie à pneumocoque)
| Modèle | Prédit | Score | Latence |
|---|---|---|---:|
| `medgemma:4b` | `J18.9` | wrong | 4.93s |
| `pmsi-coder-v2:latest` | `J13` | exact | 4.39s |
| `qwen2.5:7b` | `X00.0` | wrong | 5.04s |
| `gemma4:latest` | `J13.0` | family | 21.11s |
**v3_avc_ischemique** — attendu `I63.5` (AVC ischémique sylvien gauche)
| Modèle | Prédit | Score | Latence |
|---|---|---|---:|
| `medgemma:4b` | `I63.9` | family | 4.9s |
| `pmsi-coder-v2:latest` | `I60.0` | wrong | 4.56s |
| `qwen2.5:7b` | `X00.0` | wrong | 5.02s |
| `gemma4:latest` | `I63` | family | 16.93s |
**v4_decompensation_cardiaque** — attendu `I50.1` (Insuffisance cardiaque gauche décompensée)
| Modèle | Prédit | Score | Latence |
|---|---|---|---:|
| `medgemma:4b` | `I50.9` | family | 4.9s |
| `pmsi-coder-v2:latest` | `J96.0` | wrong | 4.26s |
| `qwen2.5:7b` | `X00.0` | wrong | 4.94s |
| `gemma4:latest` | `I50.9` | family | 22.26s |
**v5_dyspnee_symptome** — attendu `R06.0` (Dyspnée (symptôme isolé, étiologie non retrouvée))
| Modèle | Prédit | Score | Latence |
|---|---|---|---:|
| `medgemma:4b` | `R06.0` | exact | 4.84s |
| `pmsi-coder-v2:latest` | `X00.0` | wrong | 4.43s |
| `qwen2.5:7b` | `X00.0` | wrong | 4.95s |
| `gemma4:latest` | `J96.0` | wrong | 15.77s |
## 2. Résumé de CRH (3 dossiers anonymisés)
| Modèle | Latence moy. | Longueur moy. | Puces moy. |
|---|---:|---:|---:|
| `gemma4:latest` | 30.9s | 759 car. | 5.0 |
| `medgemma:4b` | 6.3s | 634 car. | 3.3 |
| `pmsi-coder-v2:latest` | 21.8s | 274 car. | 5.0 |
| `qwen2.5:7b` | 10.7s | 674 car. | 5.0 |
### Sortie complète par modèle (à juger qualitativement)
#### 67_23001636 — `medgemma:4b` (6.04s)
```
Voici un résumé concis du compte-rendu :
1. Maintien à domicile compliqué en l'absence de son époux hospitalisé, avec altération de l'état général et troubles de la marche.
2. Maladie neurodégénérative, hypertension artérielle, syndrome anxieux, athérome carotidien.
3. Vertiges, érysipèle du coude gauche, et détresse psychologique liée à la séparation de son époux.
4. AMOXICILLINE, arrêt HYDROCHLOROTHIAZIDE, OXAZEPAM, SERESTA, KARDEGIC, ROSUVASTATINE, LERCANIDIPINE.
5. Orientation vers SSR La Concha, avec suivi et ajustement de la médication.
```
#### 67_23001636 — `pmsi-coder-v2:latest` (9.78s)
```
- DP : Érysipèle du bras gauche
- DA : Syndrome anxieux
- T1 : Amoxicilline
- T2 : Hydrochlorothiazide
- T3 : Bisoprolol
```
#### 67_23001636 — `qwen2.5:7b` (9.99s)
```
- Motif d'admission : Altération de l'état général, troubles de la marche, maintien à domicile compliqué en absence de son époux hospitalisé.
- Antécédents pertinents : Maladie neurodégénérative avec troubles cognitifs modérés, hypertension artérielle, athérome carotidien, syndrome anxieux.
- Diagnostic(s) retenu(s) : Vertiges d'origine non localisée, érysipèle du coude gauche.
- Traitements engagés : Amoxicilline pour l'erysipèle, arrêt de l'Hydrochlorothiazide, oxazepam et Seresta pour la gestion anxieuse.
- Évolution / orientation : La patiente a bien répondu au traitement antibactérien, elle est capable de marcher sans symptôme neurologique. Elle est orientée au SSR La Concha avec son époux.
```
#### 67_23001636 — `gemma4:latest` (29.74s)
```
- Motif d'admission : Faiblesse générale et vertiges, dans un contexte de maintien à domicile jugé précaire en l'absence de son époux.
- Antécédents pertinents : Maladie neurodégénérative avec troubles cognitifs modérés, Hypertension artérielle, Athérome carotidien et syndrome anxieux.
- Diagnostic(s) retenu(s) : Erysipèle du coude gauche (traité), Vertiges, et difficulté de maintien à domicile en l'absence de son conjoint.
- Traitements engagés : Mise sous AMOXICILLINE (3 jours), BISOPROLOL, ACIDE ACETYLSALICYLIQUE, LERCANIDIPINE et ROSUVASTATINE. Ajout de SERESTA/OXAZEPAM PRN pour gérer l'anxiété et la déambulation nocturne.
- Évolution / orientation : Évolution neurologique favorable avec récupération complète des symptômes. La patiente est orientée au SSR La Concha.
```
#### 103_23056749 — `medgemma:4b` (6.14s)
```
Voici un résumé concis du compte-rendu :
- Maintien à domicile difficile en raison de troubles anxio-dépressifs, d'aboulie, de clinophilie et de troubles cognitifs.
- Antécédents de syndrome anxio-dépressif majeur avec hallucinations acoustico-verbales et idées de persécution en 2020.
- Bilan neurologique pour tremblements en cours, mais sans argument pour une maladie de Parkinson ou une synucléopathie.
- Le patient(e) présente une personnalité anxieuse, une anxiété majeure, une plainte mnésique et une suspicion de maladie neuro dégénérative.
- Transfert en psychiatrie à [VILLE] avec prescription de médicaments (alprazolam, modopar, théralène, venlafaxine, zopiclone).
```
#### 103_23056749 — `pmsi-coder-v2:latest` (25.91s)
```
- 1. Motif d'admission : Maintien à domicile difficile chez une patiente âgée avec des antécédents de troubles cognitifs et un syndrome anxio-dépressif.
- 2. Antécédents pertinents : Syndrome anxio-dépressif ancien, tremblements, bilan neurologique, et prise en charge psychiatrique.
- 3. Diagnostic(s) retenu(s) : MADD (Maladie d'Alzheimer avec début précoce), syndrome anxio-dépressif, et possible trouble neurodégénératif.
- 4. Traitements engagés : Test d'évaluation du risque de démence, et potentiellement traitement psychiatrique.
- 5. Évolution / orientation : Transfert en psychiatrie pour évaluation et suivi du trouble mental.
```
#### 103_23056749 — `qwen2.5:7b` (9.07s)
```
- Motif d'admission : maintien à domicile difficile
- Antécédents pertinents : syndrome anxio dépressif, hospitalisation en 2020 pour syndrome dépressif majeur avec hallucinations, fracture des deux humérus en 2017
- Diagnostic(s) retenu(s) : maladie dépressive majeure (MDD), syndrome anxio dépressif ancien, troubles cognitifs débutants
- Traitements engagés : alprazolam, modopar, théralène, venlafaxine, zopiclone
- Évolution / orientation : transfert en psychiatrie à [ETABLISSEMENT] à [VILLE]
```
#### 103_23056749 — `gemma4:latest` (28.99s)
```
- Motif d'admission : Difficulté à maintenir à domicile en raison d'une pathologie anxio-dépressive et de troubles cognitifs évolutifs.
- Antécédents : Syndrome anxio-dépressif sévère (avec hallucinations en 2020), bilan neurologique de tremblements (DAT scan négatif).
- Diagnostic : Syndrome anxio-dépressif majeur (MDD) avec troubles cognitifs débutants. Suspicion de maladie neurodégénérative écartée ou faussée par les neuroleptiques.
- Traitements : Mise en place d'un traitement polythérapeutique (ALPRAZOLAM, VENLAFAXINE, MODOPAR, THERALENE, ZOPICLONE).
- Évolution / Orientation : Transfert en psychiatrie pour prise en charge spécialisée et coordination des aides à domicile (AS de proximité).
```
#### 407_23116460 — `medgemma:4b` (6.61s)
```
Voici un résumé concis du compte-rendu :
- Motif d'admission : Diabète de type 2 compliqué, AVC en mai 2023, introduction de GLP1 en neurologie.
- Antécédents pertinents : Hypertension artérielle, insuffisance rénnelle chronique, AVC, rétinopathie minime et maculopathie, cardio.
- Diagnostic(s) retenu(s) : Diabète de type 2 compliqué sur le plan vasculaire, syndrome dépressif.
- Traitements engagés : Pas de modification du traitement antidiabétique, introduction de seroplex, supplémentation en uvedose, vaccination non faite.
- Évolution / orientation : Patient(e) quitte le service avec les consignes d'usage, suivi HbA1c tous les 3 mois, CS cardio à organiser.
```
#### 407_23116460 — `pmsi-coder-v2:latest` (29.84s)
```
- 19/06/2023
- 23/06/2023
- 23/06/2023
- 23/06/2023
- 23/06/2023
```
#### 407_23116460 — `qwen2.5:7b` (12.95s)
```
- Motif d'admission : Première éducation dans le cadre d'un diabète de type 2, introduction de GLP1 après AVC en mai 2023 (HbA1c 7,9%)
- Antécédents pertinents : Diabète de type 2, hypertension artérielle, AVC récent, rétinopathie minime et maculopathie, traitement antidiabétique modifié
- Diagnostic(s) retenu(s) : Diabète de type 2 compliqué sur le plan vasculaire, syndrome dépressif
- Traitements engagés : Gliclazide, Trulicity, Eliquis, Nebivolol, Perindopril, Amlodipine, Pravastatine, supplémentation en uvedose et introduction de Seroplex
- Évolution / orientation : Patient quitte le service avec son traitement antidiabétique maintenu, suivi HbA1C tous les 3 mois, objectif 7,5%, organisation de la prise d'IVT, éducation médicamenteuse pour prévenir les effets indésirables et optimiser l'usage des piluliers
```
#### 407_23116460 — `gemma4:latest` (33.85s)
```
- **Motif d'admission :** Première éducation thérapeutique et optimisation du traitement du diabète de type 2, suite à un AVC survenu en mai 2023.
- **Antécédents pertinents :** Diabète de type 2 (IMC 33,4 kg/m²), HTA, antécédent d'AVC, rétinopathie/maculopathie, et syndrome dépressif.
- **Diagnostic(s) retenu(s) :** Diabète de type 2 compliqué sur le plan vasculaire, actuellement bien équilibré grâce à l'introduction d'un agent GLP-1.
- **Traitements engagés :** Traitement multivarié incluant Metformine, Gliclazide, Trulicity (GLP-1), Nebivolol, Perindopril, Amlodipine, Eliquis, Pravastatine, et Seroplex.
- **Évolution / orientation :** Reprise du suivi des comorbidités (Cardio, Néphro, Ophtalmo). Maintien de l'éducation thérapeutique et suivi régulier de l'HbA1c (objectif < 7,5%).
```
## 3. Extraction structurée JSON
| Modèle | Conformes | Champs remplis moy. | Latence moy. |
|---|---:|---:|---:|
| `gemma4:latest` | 3/3 | 7.0/7 | 39.0s |
| `medgemma:4b` | 3/3 | 7.0/7 | 8.2s |
| `pmsi-coder-v2:latest` | 3/3 | 7.0/7 | 30.3s |
| `qwen2.5:7b` | 3/3 | 6.0/7 | 14.7s |
### Détail JSON parsé par cas
#### 67_23001636 — `medgemma:4b` (conforme, 8.27s)
```json
{
"motif_admission": "Vertiges, maintien à domicile difficile en l'absence de son époux.",
"diagnostics": [
"Maladie neurodégénérative avec troubles cognitifs modérés.",
"Hypertension artérielle.",
"Syndrome anxieux.",
"Atherosclérose carotidienne.",
"Fébricule à 38°C avec érysipèle du bras gauche.",
"Vertiges."
],
"antecedents": [
"Maladie neurodégénérative avec troubles cognitifs modérés.",
"Hypertension artérielle.",
"Atherosclérose carotidienne.",
"Syndrome anxieux."
],
"traitements": [
"KARDEGIC 75MG 1 LE MATIN",
"ROSUVASTATINE 5MG 1 LE SOIR",
"BISOPROLOL/HYDROCHLOROTIAZIDE 10/6.25MG 1 LE MATIN",
"LERCAN 10MG 1 LE SOIR",
"CLINUTREN 2/JOUR",
"CARTEOL 2% : 1GOUTTE DANS CHAQUE ŒIL LE MATIN",
"AMOXICILLINE",
"AMOXICILLINE ARW 1G",
"BISOPROLOL ARL 10MG",
"ACIDE ACETYLSALICYLIQUE",
"LERCANIDIPINE ACT",
"ROSUVASTATINE ARW",
"SERESTA 10MG CPR (ORALE) 0,5 CPR A administrer : toutes les 6 heure(s) si agitation o angoisse pendant 30 jour(s)",
"OXAZEPAM - SERESTA 10MG CPR (ORALE) 0,5 CPR A administrer : toutes les 6 heure(s) si agitation o angoisse pendant 30 jour(s)"
],
"date_admission": "05/06/2023",
"date_sortie": "12/06/2023",
"duree_sejour_jours": 7
}
```
#### 67_23001636 — `pmsi-coder-v2:latest` (conforme, 30.25s)
```json
{
"motif_admission": "La patiente présente des symptômes neurologiques et un érythème, nécessitant une évaluation et une prise en charge.",
"diagnostics": "Les diagnostics retenus sont : érythème, fièvre, infection, hémorragie, occlusion, et les anomalies biologiques (CRP, ASAT, ALAT, hémoglobine, plaquettes, leucocytes, créatinine). Le diagnostic principal est l'érythème, et les autres sont des diagnostics associés.",
"antecedents": "Les antécédents notables sont l'épilepsie, les accidents ischémiques cérébraux transitoires (AICs), et les complications neurologiques. La patiente a également des antécédents de traitement médicamenteux (hydrochlorothiazide, levetiracetam, etc.).",
"traitements": "Les traitements engagés pendant le séjour comprennent l'arrêt de l'hydrochlorothiazide, la mise en place d'un cathéter, et potentiellement d'autres interventions médicales. Le traitement de l'érythème est également à considérer.",
"date_admission": "2023-06-05",
"date_sortie": "2023-06-12",
"duree_sejour_jours": 7
}
```
#### 67_23001636 — `qwen2.5:7b` (conforme, 19.05s)
```json
{
"motif_admission": "Altération de l'état général avec troubles de la marche, maintien à domicile compliqué en l'absence de son époux hospitalisé.",
"diagnostics": [
"Maladie neurodégénérative avec troubles cognitifs modérés",
"Hypertension artérielle",
"Athérome carotidien",
"Syndrome anxieux"
],
"antecedents": [
"Maintien à domicile compliqué en l'absence de son époux hospitalisé",
"Hospitalisation précédente du 05/06 au 12/06/2023"
],
"traitements": [
"KARDEGIC 75MG 1 LE MATIN",
"ROSUVASTATINE 5MG 1 LE SOIR",
"BISOPROLOL/HYDROCHLOROTIAZIDE 10/6.25MG 1 LE MATIN",
"LERCAN 10MG 1 LE SOIR",
"CLINUTREN 2/JOUR",
"CARTEOL 2% : 1GOUTTE DANS CHAQUE ŒIL LE MATIN",
"AMOXICILLINE",
"OXAZEPAM",
"SERESTA 10MG CPR (ORALE) 0,5 CPR A administrer : toutes les 6 heure(s) pendant 30 jour(s)",
"AMOXICILLINE ARW 1G CPR DISP (ORALE) 1 G A administrer : matin midi soir pendant 3 jour(s)",
"BISOPROLOL ARL 10MG CPR (ORALE) 1 CPR A administrer : matin [8h] A partir du 13/06/2023 pendant 30 jour(s)",
"ACIDE ACETYLSALICYLIQUE KARDEGIC 75MG PDR ORALE 1 SAC A administrer : matin [8h] A partir du 13/06/2023 pendant 30 jour(s)",
"LERCANIDIPINE ACT 10MG CPR (ORALE) 1 CPR A administrer : soir [19h] pendant 30 jour(s)",
"ROSUVASTATINE ARW 5MG CPR (ORALE) 1 CPR A administrer : prise unique à 19h pendant 30 jour(s)"
],
"date_admission": null,
"date_sortie": null,
"duree_sejour_jours": null
}
```
#### 67_23001636 — `gemma4:latest` (conforme, 38.94s)
```json
{
"motif_admission": "Altération de létat général avec troubles de la marche",
"diagnostics": [
"Erysipèle du coude gauche",
"Vertiges",
"Altération de létat général"
],
"antecedents": [
"Maladie neurodégénérative avec troubles cognitifs modérés",
"Hypertension artérielle",
"Athérome carotidien",
"Syndrome anxieux"
],
"traitements": [
"AMOXICILLINE",
"OXAZEPAM",
"SERESTA",
"BISOPROLOL",
"KARDEGIC",
"LERCANIDIPINE",
"ROSUVASTATINE"
],
"date_admission": "05/06/2023",
"date_sortie": "12/06/2023",
"duree_sejour_jours": 7
}
```
#### 103_23056749 — `medgemma:4b` (conforme, 8.73s)
```json
{
"motif_admission": "maintien à domicile difficile",
"diagnostics": [
"syndrome anxio depressif",
"tremblements",
"fracture des deux humérus",
"maladie de Parkinson atypique",
"maladie de Parkinson",
"maladie de Parkinson faussée par le neuroleptique",
"MDD",
"syndrome dépressif majeur avec hallucinations acoustico verbales et idées de persécution"
],
"antecedents": [
"syndrome anxio depressif suivi Dr [NOM] hospitalisation en 2020 pour syndrome depressif majeur avec hallucinations acoustico verbales et idées de persécution ([ETABLISSEMENT])",
"bilan neurologique Dr [NOM] pour tremblements : IRMc leucopathie vasculaire, DAT scan RAS, mise en place dun traitement dépreuve par MODOPAR dépreuve (éléments frontaux et syndrome parkinsonien atypique)",
"fracture des deux humérus en 2017",
"divorcée, une fille qui vit à Bayonne",
"ancienne journaliste radio france",
"aucune aide, pas dide, pas daide ménagère",
"syndrome anxio depressif ancien avec tr attentionnels majeurs"
],
"traitements": [
"ALPRAZOLAM ARW 0,25MG CPR [30] COMPRIME(S) 1 COMPRIME (ORALE) matin midi soir",
"MODOPAR 125 (100MG/25MG) CPR 1 CPRDISP (ORALE) matin midi soir",
"THERALENE 40MG/ML SOL BUV 30ML [1200] Flacon(s) 20 GOUTTE (ORALE) nuit [21h]",
"VENLAFAXINE ARW LP 75MG GELULE [30] Gelule(s) 1 GELULE (ORALE) soir [19h]",
"ZOPICLONE EG 7,5MG CPR [14] COMPRIME(S) 1 COMPRIME (ORALE) nuit [21h]"
],
"date_admission":
```
#### 103_23056749 — `pmsi-coder-v2:latest` (conforme, 25.49s)
```json
{
"motif_admission": "Mise en observation pour suspicion de maladie neurodégénérative",
"diagnostics": "Syndrome anxio-dépressif ancien, tremblements essentiels, bilan neurologique avec DAT scan négatif, absence de confirmation de maladie neurodégénérative",
"antecedents": "Antécédents de syndrome anxio-dépressif, tremblements essentiels, bilan neurologique, traitement par Théralène",
"traitements": "Mise en observation, bilan neurologique, traitement par Théralène",
"date_admission": "2022-12-18",
"date_sortie": "2022-12-21",
"duree_sejour_jours": 3
}
```
#### 103_23056749 — `qwen2.5:7b` (conforme, 10.76s)
```json
{
"motif_admission": "maintien à domicile difficile",
"diagnostics": [
"MDD",
"syndrome anxio depressif",
"troubles cognitifs débutants"
],
"antecedents": [
"fracture des deux humérus en 2017",
"syndrome depressif majeur avec hallucinations acoustico verbales et idées de persécution (2020)",
"tremblements (2023)"
],
"traitements": [
"ALPRAZOLAM ARW 0,25MG CPR",
"MODOPAR 125",
"THERALENE 40MG/ML SOL BUV",
"VENLAFAXINE ARW LP",
"ZOPICLONE EG"
],
"date_admission": "18/03/2023",
"date_sortie": "21/03/2023",
"duree_sejour_jours": 3
}
```
#### 103_23056749 — `gemma4:latest` (conforme, 38.27s)
```json
{
"motif_admission": "Maintien à domicile difficile",
"diagnostics": [
"Trouble anxio depressif (MDD)",
"Troubles cognitifs débutants"
],
"antecedents": [
"Syndrome anxio depressif (hospitalisation 2020)",
"Bilan neurologique pour tremblements (IRMc leucopathie vasculaire, DAT scan RAS)",
"Fracture des deux humérus (2017)"
],
"traitements": [
"ALPRAZOLAM ARW 0,25MG",
"MODOPAR 125",
"THERALENE 40MG/ML",
"VENLAFAXINE ARW LP 75MG",
"ZOPICLONE EG 7,5MG"
],
"date_admission": "18/03/2023",
"date_sortie": "21/03/2023",
"duree_sejour_jours": 3
}
```
#### 407_23116460 — `medgemma:4b` (conforme, 7.53s)
```json
{
"motif_admission": "Première éducation dans le cadre d'un diabète de type 2",
"diagnostics": [
"Diabète de type 2",
"AVC",
"Hypertension artérielle",
"Carence en vitamine D",
"Syndrome dépressif"
],
"antecedents": [
"Diabète de type 2",
"AVC",
"Hypertension artérielle",
"Maladie cardiaque",
"Onychomycose",
"Pallesthésie",
"Poids excessif",
"Pas de vaccination contre la grippe"
],
"traitements": [
"GLP1",
"Metformine",
"Gliclazide",
"Trulicity",
"Nebivolol",
"Pravastatine",
"Eliquis",
"Perindopril",
"Amlodipine",
"Velmetia",
"Seroplex",
"Uvedose",
"IVT",
"Uvedose"
],
"date_admission": "19/06/2023",
"date_sortie": "23/06/2023",
"duree_sejour_jours": 4
}
```
#### 407_23116460 — `pmsi-coder-v2:latest` (conforme, 35.23s)
```json
{
"motif_admission": "diabète de type 2",
"diagnostics": "diabète de type 2, obésité (imc 33.442)",
"antecedents": "antécédents familiaux de diabète de type 2",
"traitements": "insuline, glucagon",
"date_admission": "2023-06-19",
"date_sortie": "2023-06-23",
"duree_sejour_jours": 4
}
```
#### 407_23116460 — `qwen2.5:7b` (conforme, 14.27s)
```json
{
"motif_admission": "Première éducation dans le cadre d'un diabète de type 2",
"diagnostics": [
"Diabète de type 2 compliqué sur le plan vasculaire",
"Syndrome dépressif"
],
"antecedents": [
"AVC fin mai",
"Hypertension artérielle (HTA)",
"SAOS appareillé",
"Diabète",
"Hyperlipidémie",
"Ralentisement idéomoteur",
"Onychomycose"
],
"traitements": [
"Nebivolol 5 mg 1-0-0",
"Pravastatine 40 mg 1 le soir",
"Eliquis 5 : 1 cp matin et soir",
"Perindopril 10mg 1-0-0",
"Amlodipine 5 le matin",
"Velmetia 50/1000mg 1-0-1",
"Gliclazide 30mg 2-0-0",
"Trulicity 1,5 les mardis",
"Seroplex",
"Uvedose"
],
"date_admission": "19/06/2023",
"date_sortie": "23/06/2023",
"duree_sejour_jours": 4
}
```
#### 407_23116460 — `gemma4:latest` (conforme, 39.71s)
```json
{
"motif_admission": "Première éducation dans le cadre d'un diabète de type 2",
"diagnostics": [
"Diabète de type 2",
"HTA",
"AVC",
"Rétinopathie et maculopathie",
"Syndrome dépressif"
],
"antecedents": [
"Diabète de type 2",
"HTA",
"AVC",
"Ralentisement idéomoteur"
],
"traitements": [
"Pravastatine 40 mg",
"Eliquis 5",
"Perindopril 10mg",
"Amlodipine 5",
"Velmetia 50/1000mg",
"Gliclazide 30mg",
"Metformine 1000mg",
"Nebivolol 5 mg",
"Trulicity 1,5 mg",
"Seroplex"
],
"date_admission": "19/06/2023",
"date_sortie": "23/06/2023",
"duree_sejour_jours": 4
}
```

View File

@@ -0,0 +1,54 @@
# Bench mini-LLM NLP fr pour Léa
Test : 14 commandes représentatives (traite N, code N, traite tous, IPP spécifique, stop, questions).
## Résultats
| Modèle | Accuracy | Parse JSON | p50 latence | p95 latence |
|---|---|---|---|---|
| `llama3.2:1b` | 29% | 100% | 416 ms | 1548 ms |
| `qwen2.5:0.5b` | 14% | 100% | 400 ms | 1452 ms |
| `gemma3:1b` | 93% | 100% | 377 ms | 1500 ms |
## Verdict
1. **gemma3:1b** — accuracy 93%, p50 377 ms
2. **llama3.2:1b** — accuracy 29%, p50 416 ms
3. **qwen2.5:0.5b** — accuracy 14%, p50 400 ms
**Recommandation : `gemma3:1b`**
## Cas d'échec (top 5 par modèle)
### llama3.2:1b
- `Léa, traite-moi 3 dossiers` → wrong_count: got None expected 3
- `code-moi les 4 premiers dossiers` → wrong_count: got None expected 4
- `Léa peux-tu traiter 2 dossiers s'il te plaît ?` → wrong_count: got None expected 2
- `fais-moi les 3 premiers` → wrong_count: got None expected 3
- `j'aimerais que tu traites 6 dossiers` → wrong_order: got 'all' expected 'first'
### qwen2.5:0.5b
- `Léa, traite-moi 3 dossiers` → wrong_order: got 'all' expected 'first'
- `traite 5 dossiers` → wrong_action: got None expected 'process_patients'
- `code-moi les 4 premiers dossiers` → wrong_count: got 1 expected 4
- `Léa peux-tu traiter 2 dossiers s'il te plaît ?` → wrong_count: got None expected 2
- `fais-moi les 3 premiers` → wrong_count: got 1 expected 3
### gemma3:1b
- `comment tu traites un dossier ?` → wrong_action: got 'process_patients' expected 'unknown'
## Prompt système utilisé
```
Tu es un parseur d'intentions pour Léa, assistant RPA médical.
Réponds UNIQUEMENT en JSON valide, sans texte avant/après, selon ce schéma :
{"action": "process_patients" | "stop" | "unknown", "count": <int|null>, "order": "first" | "last" | "all" | "specific" | null, "ipp": "<string>" | null}
Règles :
- "traite N dossiers" / "code N dossiers" / "fais les N premiers" → action=process_patients, count=N, order="first"
- "traite tous les dossiers" → action=process_patients, count=null, order="all"
- "traite le dossier 25003364" → action=process_patients, count=1, order="specific", ipp="25003364"
- "stop" / "arrête" / "annule" → action=stop
- Question ("comment", "pourquoi") → action=unknown
- Si tu ne comprends pas → action=unknown
```

View File

@@ -0,0 +1,91 @@
# Bench LLM décision T2A — 11 dossiers GHT Sud 95
_Généré le 2026-05-05 19:00 — **18 modèles** × 11 DPI consolidés (5 UHCD / 6 Forfait)_
> ⚠️ **Vérité-terrain corrigée 2026-05-05** : `25003284` reclassé **FORFAIT** (sortie domicile en 3h37, J12.1 VRS) — auparavant à tort UHCD. Tous les scores ci-dessous reflètent la nouvelle vérité-terrain.
## 🏆 Classement complet
| # | Modèle | Acc | p50 | Err HTTP | Parse | Calib | Verdict |
|---|---|---:|---:|:---:|:---:|:---:|---|
| #1 | `gemma3:27b-cloud` | **8/11 (73%)** | 10.6s | ✅ | ✅ | ❌ 2 | 🟢 **Recommandé démo** |
| #2 | `qwen3:8b` | **7/11 (64%)** | 7.6s | ✅ | ✅ | ❌ 1 | 🟡 Acceptable + garde-fou |
| #3 | `qwen2.5:7b` | **7/11 (64%)** | 10.0s | ✅ | ✅ | ❌ 2 | 🟡 Acceptable + garde-fou |
| #4 | `qwen3-vl:235b-instruct-cloud` | **7/11 (64%)** | 20.3s | ✅ | ✅ | ❌ 4 | 🟡 Acceptable + garde-fou |
| #5 | `qwen3.5:9b` | **7/11 (64%)** | 25.8s | ✅ | ⚠1 | ❌ 1 | 🟡 Acceptable + garde-fou |
| #6 | `t2a-gemma3-27b-q4:latest` | **7/11 (64%)** | 152.2s | ⚠1 | ✅ | ❌ 3 | 🟡 Acceptable + garde-fou |
| #7 | `thiagomoraes/medgemma-27b-it:Q4_K_S` | **7/11 (64%)** | 181.6s | ⚠1 | ✅ | ❌ 3 | 🟡 Acceptable + garde-fou |
| #8 | `gemma4:latest` | **6/11 (55%)** | 17.9s | ✅ | ✅ | ❌ 3 | 🔴 Insuffisant |
| #9 | `pmsi-runpod:latest` | **6/11 (55%)** | 18.8s | ✅ | ⚠1 | ❌ 4 | 🔴 Insuffisant |
| #10 | `qwen2.5:14b` | **6/11 (55%)** | 68.0s | ✅ | ✅ | ❌ 1 | 🔴 Insuffisant |
| #11 | `pmsi-coder:latest` | **5/11 (45%)** | 18.1s | ✅ | ⚠1 | ❌ 5 | 🔴 Insuffisant |
| #12 | `medgemma:4b` | **4/11 (36%)** | 4.4s | ✅ | ⚠6 | ❌ 1 | 🟠 Format JSON cassé via Ollama |
| #13 | `gpt-oss:120b-cloud` | **3/11 (27%)** | 16.0s | ✅ | ⚠5 | ❌ 3 | 🟠 Format JSON cassé via Ollama |
| #14 | `pmsi-coder-v2:latest` | **3/11 (27%)** | 23.4s | ✅ | ⚠5 | ✅ | 🟠 Format JSON cassé via Ollama |
| #15 | `qwen3:14b` | **1/11 (9%)** | 1.2s | ✅ | ⚠1 | ❌ 1 | 🔴 Insuffisant |
| #16 | `charlestang06/openbiollm:latest` | **1/11 (9%)** | 6.0s | ✅ | ✅ | ❌ 3 | 🔴 Insuffisant |
| #17 | `gpt-oss:20b-cloud` | **0/11 (0%)** | 0.0s | ✅ | ⚠11 | ✅ | 🟠 Format JSON cassé via Ollama |
| #18 | `qwen3-next:80b-cloud` | **0/11 (0%)** | 0.0s | ✅ | ⚠11 | ✅ | 🟠 Format JSON cassé via Ollama |
## 🎯 Recommandation pour la démo
**Modèle retenu : `gemma3:27b-cloud`** — 8/11 (73%), p50 10.6s
**Backup local si cloud KO** : `qwen3:8b` (7/11, 7.6s, 5 GB seulement → tient large dans 12 GB GPU).
## 📊 Détail dossier-par-dossier (top 5)
| IPP | Cas | Vérité | `gemma3` | `qwen3` | `qwen2.5` | `qwen3-vl` | `qwen3.5` |
|---|---|:---:|:---:|:---:|:---:|:---:|:---:|
| 25003284 | Pneumo VRS — Forfait | **Forfait** | ❌ UHCD | ❌ UHCD | ❌ UHCD | ❌ UHCD | ✅ Forfait |
| 25003362 | Intox PE2 | **Forfait** | ✅ Forfait | ✅ Forfait | ✅ Forfait | ✅ Forfait | ✅ Forfait |
| 25003364 | Pneumo SLA — UHCD | **UHCD** | ✅ UHCD | ✅ UHCD | ✅ UHCD | ✅ UHCD | ✅ UHCD |
| 25003451 | Plaie SU2 | **Forfait** | ✅ Forfait | ✅ Forfait | ❌ UHCD | ✅ Forfait | ✅ Forfait |
| 25003475 | Aura migr. — UHCD | **UHCD** | ✅ UHCD | ✅ UHCD | ❌ Forfait | ❌ Forfait | ❌ Forfait |
| 25005866 | Trauma hockey — UHCD | **UHCD** | ✅ UHCD | ❌ Forfait | ✅ UHCD | ✅ UHCD | ✅ UHCD |
| 25010621 | Laryngite PE2 | **Forfait** | ✅ Forfait | ✅ Forfait | ✅ Forfait | ✅ Forfait | ✅ Forfait |
| 25012257 | Douleur abdo — UHCD | **UHCD** | ❌ Forfait | ✅ UHCD | ✅ UHCD | ✅ UHCD | ⚠️ parse |
| 25048485 | Convulsion PE2 | **Forfait** | ✅ Forfait | ❌ UHCD | ✅ Forfait | ✅ Forfait | ✅ Forfait |
| 25056615 | Salpingite std | **Forfait** | ❌ UHCD | ❌ UHCD | ❌ UHCD | ❌ UHCD | ❌ UHCD |
| 25151530 | Colique std | **Forfait** | ✅ Forfait | ✅ Forfait | ✅ Forfait | ❌ UHCD | ❌ UHCD |
## ⚠️ Cas problématiques universels (3+ modèles top se trompent)
- **`25003284` (Pneumo VRS — Forfait, vérité Forfait)** : 4/5 modèles top se trompent → DPI à enrichir OU vérité-terrain à challenger avec Pauline
- **`25003475` (Aura migr. — UHCD, vérité UHCD)** : 3/5 modèles top se trompent → DPI à enrichir OU vérité-terrain à challenger avec Pauline
- **`25056615` (Salpingite std, vérité Forfait)** : 5/5 modèles top se trompent → DPI à enrichir OU vérité-terrain à challenger avec Pauline
## 🔬 Limites du bench (transparence)
Ce bench est **suffisant pour trier les modèles candidats** mais **PAS rigoureusement validant** :
- **n=11** dossiers — échantillon trop petit pour statistique robuste (cible : 50-100)
- **1 inférence par dossier** — pas de variance mesurée (un même DPI peut donner 2 réponses différentes)
- **Vérité-terrain dérivée** — partiellement corrigée mais pas encore validée à 100% par DIM
- **DPI source partiellement fictif** — voir `REVUE_DOSSIERS_PAULINE.md` (40+ noms inventés, 4 hallucinations cliniques graves, constantes tronquées). **Le bench tourne donc sur du contenu non-fidèle.** Re-bench prévu après reconstruction `data.js`.
- **Pas de cross-validation** — pas de split train/test
- **Pas de calibration formelle** — % de "elevee" fausses noté mais pas Calibration Error Score
- **Bench externes non utilisés** — MedQA, MedFrenchBenchmark, etc. pourraient compléter
**Pour un vrai bench de validation produit (post-démo)** :
1. Étendre à 50-100 dossiers anonymisés diversifiés (Pauline + DIM partenaires)
2. 3 inférences par dossier (mesure variance)
3. Cross-validation k-fold
4. Inter-rater agreement humain comme baseline
5. Tests robustesse (DPI avec fautes, abréviations atypiques)
## 🧪 Modèles non testés intéressants à explorer
- **MedGemma 1.5** (jan 2026, 91% MedQA — surpasse Med-PaLM 2) — disponible HuggingFace, à pull si compatible Ollama
- **DeepSeek-R1** (top open-source reasoning 2026) — cloud Ollama 403 sans abonnement
- **Modèles vllm** : qwen3-next, gpt-oss-120b ont eu 100% parse errors via Ollama, peut-être fonctionnels via vllm avec format JSON propre
- **Fine-tune T2A custom étendu** : `t2a-gemma3-27b` (28 GB non-quantized) sur DGX Spark
## 📦 Annexes
- Trace brute local : `/tmp/bench_t2a_full.json`
- Trace brute cloud : `/tmp/bench_t2a_cloud.json`
- Trace brute extra : `/tmp/bench_t2a_extra.json`
- Trace brute retry : `/tmp/bench_t2a_retry.json`
- Scripts : `/tmp/bench_t2a*.py`
- DPI consolidés : `/tmp/dpis.json`

View File

@@ -0,0 +1,115 @@
# Inspiration frameworks RPA visuels — état des lieux externe
**Date :** 2026-05-10
**Auteur :** Claude (chat) via Dom
**Statut :** à débattre post-démo, pas urgent
## 1. Ce qu'on cherchait
Identifier 2-3 patterns d'archi de projets RPA visuels open source qui pourraient inspirer rpa_vision_v3 SANS remettre en cause son existant. Posture : neutre, factuelle.
J'ai exploré 4 projets parmi les plus matures de l'écosystème RPA visuel / GUI agent open source en 2025-2026 :
1. **OpenAdapt** (OpenAdaptAI, GitHub ~7k stars) — RPA générative avec VLM, pensé pour démos humaines
2. **Skyvern** (Skyvern-AI, GitHub ~12k stars) — automation browser via vision + LLM, pas DOM
3. **OmniParser** (Microsoft Research, GitHub ~22k stars) — parsing structuré d'écrans pour GUI agents
4. **TagUI** (AI Singapore) — RPA cross-OS multilangue, plus mature mais moins LLM-first
Référence aux benchmarks : WindowsAgentArena, ScreenSpot et ScreenSpot-Pro, WebVoyager (Skyvern à 85.85%).
## 2. Convergences fortes avec rpa_vision_v3
### 2.1 Policy / Grounding Separation
> "The Policy decides what to do; Grounding determines where to do it."
Chez nous : le VWB (workflow déclaratif "il faut cliquer sur Notes médicales") = Policy. La cascade `_resolve_target` (OCR → template → grounding VLM) = Grounding.
Ce qu'on fait déjà bien : la séparation existe en pratique. Le Policy ne sait pas comment le Grounding fait son travail.
Suggestion : nommer ces deux composants comme tels dans la doc — c'est devenu un vocabulaire standard dans l'écosystème.
### 2.2 Safety Gate
> "Runtime validation layer before action execution (confirm mode for high-risk actions)."
Chez nous : Léa qui vérifie l'ancre visuelle avant de cliquer. Philosophie produit identique.
Pour le pitch démo : "Safety Gate" est un vocabulaire reconnu dans l'écosystème. Healthtech = parfait fit. À mettre en avant face à DG/DSI/médecins : "Léa ne clique jamais à l'aveugle, elle confirme visuellement avant chaque action — c'est notre Safety Gate."
### 2.3 Abstraction Ladder
> "Progressive generalization from literal replay to goal-level automation."
Chez nous : replay déclaratif (VWB workflow étape par étape), avec vision long-terme d'un agent autonome (ORA = observe_reason_act).
Suggestion : ne pas opposer les deux. Replay (low abstraction) et ORA (high abstraction) sont les deux extrémités d'une échelle.
## 3. Patterns Skyvern intéressants
### 3.1 Planner-Actor-Validator Loop
Skyvern décompose chaque tâche en 3 rôles : Planner, Actor, Validator (vérifie que le step a réussi avant de passer au suivant).
Chez nous : VWB ≈ Planner statique. Léa ≈ Actor + une partie de Validator (vérification ancre). Le VERIFY ≈ Validator post-action.
**Note critique 2026-05-10 :** le bug observé sur le step 10 (Imagerie cliqué dans le bandeau Edge mais REPORT success=True) révèle que notre Validator est laxiste (pHash global au lieu de vérification sémantique du tab actif). C'est exactement le type de défaillance que la formalisation Validator-as-component aiderait à diagnostiquer.
### 3.2 Visual Workflow Builder
Skyvern parle explicitement de "Visual workflow builder - Create automations without writing code through a point-and-click interface".
C'est exactement notre VWB. Convergence indépendante = bon signe qu'on est sur le bon design pattern.
## 4. Patterns OmniParser intéressants
### 4.1 Tokenizing UI screenshots
OmniParser transforme l'écran en éléments structurés (liste d'interactable elements + captions sémantiques) AVANT que le VLM principal ne soit appelé.
Suggestion modeste applicable rapidement : logger systématiquement, à chaque appel `_resolve_target`, la liste des candidats détectés par chaque étage de la cascade. On aurait une "vue parsée" implicite, sans changer l'archi. Utile pour debug, utile pour démo.
### 4.2 OmniTool : VM Windows + agent
OmniTool de Microsoft contrôle une VM Windows 11 entière, avec un VLM externe. Séparation "VM Windows distante / agent côté serveur".
Chez nous : machine Windows distante via NoMachine + agent côté Linux. Convergence indépendante.
Pour le pitch : "isolation forte entre poste utilisateur et moteur d'IA" est un argument healthtech / RGPD / sécurité hospitalière fort.
## 5. Ce que les autres font et qu'on ne fait pas (à creuser)
- **Evaluation-Driven Feedback (OpenAdapt)** : "Success traces become new training data". Nos runs réussis pourraient alimenter une banque de cas de référence. Pas urgent.
- **Prompt Caching (Skyvern)** : mémoriser les actions passées et les rejouer. On a déjà un `TargetMemoryStore` (cf. Phase 1 apprentissage). Concept similaire à comparer.
- **MCP server architecture** : Skyvern et d'autres exposent leur RPA comme un serveur MCP. Roadmap long-terme.
## 6. Questions pour la session de recul méthodologique post-démo
1. Formaliser Policy / Grounding / Safety Gate / Validator comme des composants nommés dans la doc et dans le code ?
2. L'écart Replay engine VWB vs ORA doit-il être réduit ou maintenu ?
3. L'ajout d'une couche "Screen Parser unifié" type OmniParser serait-elle utile à terme ?
4. L'archi Léa (agent contrôlé sur Windows distant) est un asset commercial fort dans le secteur healthtech. Devrait-on la mettre plus en avant dans le pitch ?
## 7. Convergence : on n'est pas seul
L'archi rpa_vision_v3 converge fortement avec ce que les meilleurs projets open source 2025-2026 formalisent comme bonnes pratiques.
Ce que les autres ont en plus :
- Formalisation explicite des composants → adoptable sans refonte
- Communication mainstream des concepts → utile pour le pitch
- Pre-built templates / use cases → maturité de marché
Ce qu'on a en plus :
- Spécialisation healthtech → moat naturel
- Agent contrôlé qui valide visuellement → philosophie produit forte
- VWB visuel → différenciateur UX par rapport au tout-LLM
- Stack on-premise complète → réponse RGPD / souveraineté que les SaaS américains ne peuvent pas donner
## 8. Liens
- OpenAdapt v1.0 : https://github.com/OpenAdaptAI/OpenAdapt
- OmniParser V2 : https://github.com/microsoft/OmniParser
- Skyvern : https://github.com/Skyvern-AI/skyvern
- WindowsAgentArena (benchmark)

View File

@@ -0,0 +1,68 @@
# Template mail à Pauline — préparation visio démo GHT
> _À adapter selon ton ton habituel avec Pauline. Les blocs entre {{}} sont à remplir._
---
**Objet :** Démo GHT Sud 95 — bilan d'avancement + 7 points à valider avant la visio
Bonjour Pauline,
Petit point d'avancement avant notre visio {{date/heure}} pour préparer la démo.
## 1. Ce qui est prêt
- 🟢 **Maquette aiva-vision** : interface de décision facturation (Forfait/UHCD) déployée, intégrée à l'écosystème Easily Assure (URL : `https://urgence.labs.laurinebazin.design/codage.html?id=XXX`). Tu peux la tester dossier par dossier.
- 🟢 **Pipeline IA Léa** : Léa peut ouvrir Chrome, lire la liste des patients en OCR, traiter chaque dossier, soumettre à analyse LLM, lire la décision. Toute la chaîne fonctionne en bout-en-bout (validé ce matin sur le PC de démo).
- 🟢 **Bench LLM décisionnel** : 18 modèles testés sur les 11 dossiers que tu nous as fournis (rapport complet en pièce jointe : `BENCH_T2A_DECISION_11DOSSIERS.md`).
- Modèle retenu pour la démo : **`gemma3:27b-cloud`** (8/11 = 73% de bonnes décisions).
- Backup local rapide : `qwen3:8b` (7/11 = 64%, 7.6s/dossier, 5 GB).
- Aucun modèle n'atteint 100% → le **garde-fou humain (médecin valide chaque proposition de Léa) est indispensable**, et c'est précisément le récit produit qu'on porte.
## 2. Limite identifiée — qualité des données source
J'ai fait une revue ligne à ligne entre tes captures d'écran et ce qui est dans `data.js` (la base interne de la maquette). Trouvailles :
- **40+ noms de soignants inventés** lors de la première anonymisation
- **Plusieurs hallucinations cliniques graves** sur quelques dossiers (sens inversé : "anhydrose" vs "ankylose", TDM "sans" vs "avec injection", etc.)
- **Constantes vitales tronquées** sur 5 dossiers (3-7 colonnes dans tes captures, 2 dans data.js)
- **Vérité-terrain incorrecte sur 25003284** : initialement classé UHCD, c'est en fait un Forfait (sortie domicile en 3h37, J12.1 Pneumopathie VRS — l'asthme n'est qu'un antécédent)
→ Reconstruction propre de `data.js` en cours. Rapport détaillé en pj : `REVUE_DOSSIERS_PAULINE.md`.
## 3. Questions concrètes pour notre visio
| # | Question | Pourquoi c'est important |
|---|---|---|
| 1 | **Vérité-terrain validée** : tu confirmes la nouvelle classification 5 UHCD / 6 Forfait (avec 25003284 reclassé Forfait) ? | Référence pour tous nos benchs |
| 2 | **Cas piège démo** : sur les 11, lequel est *typique* d'un cas où le DIM se ferait piéger (UHCD ratée → forfait facturé) ? Lequel est *limite cliniquement* ? Lequel est *évident* ? | Sélectionner les 3 dossiers à présenter le jour J |
| 3 | **25048485 (NGUYEN Léo)** : ta capture montre 2 motifs distincts (1ère CTCG puis 2ème CTCG le même jour). C'est 2 passages distincts ou 1 ? | Risque de fausse classification |
| 4 | **Onglet Imagerie** : on a ajouté un onglet dédié pour les CR scan/radio/échographie (avant noyés dans Notes médicales). Tu valides la pertinence ? | Impact UI |
| 5 | **Workflow garde-fou** : Léa propose une décision avec confiance (élevée/moyenne/faible). Pour les "faibles", elle demande validation médecin. Tu valides ? | Argument de sécurité face à Carvella (DSI) |
| 6 | **Position juridique** : qui est responsable du codage final ? Le médecin DIM (Léa propose) ? | Point anti-FUD pour Carvella |
| 7 | **ROI estimé** : 100k€/mois mentionnés dans les premiers échanges — tu peux confirmer le calcul (nb dossiers/jour × % UHCD ratés × valorisation) ? | Slide ROI |
## 4. Demande matérielle
Pour finaliser la reconstruction des dossiers j'ai 2 questions :
-**3 dossiers manquants** : c'est OK, je les ai retrouvés dans `RPU UHCD IA (2)/RPU UHCD IA.docx` — pas besoin de captures supplémentaires.
-**Si tu as le temps** : pour les 11 dossiers, est-ce que tu pourrais nous valider à l'écrit le contenu reconstruit (1 ligne suffit par dossier : "OK" / "à modifier sur tel point") ? Idéalement d'ici J-2 (le {{date}}).
## 5. Planning démo restant
- **Aujourd'hui** : reconstruction `data.js` avec ta revue
- **Demain** : enregistrement du workflow Urgence sur le PC de démo
- **J-1** : tests E2E répétés
- **Jour J** : démo {{lieu, heure, audience}}
Si tu vois quoi que ce soit à challenger ou améliorer, n'hésite pas. On compte beaucoup sur ton expertise DIM pour faire la différence face à Carvella.
À tout à l'heure,
{{Dominique}}
---
## Pièces jointes suggérées
- `docs/BENCH_T2A_DECISION_11DOSSIERS.md` (rapport bench 18 modèles)
- `docs/REVUE_DOSSIERS_PAULINE.md` (revue qualité 11 dossiers)
- `docs/RECONSTRUCTION_DATA_JS_REPORT.md` (à créer après reconstruction — pas encore prêt)

View File

@@ -0,0 +1,128 @@
**Aiva-vision — Benchmark des modèles économiques RPA et propositions de positionnement**
***Note d'envoi*** * : document interne Aivanov, à destination de la direction financière. Synthèse marché + 3 pistes de positionnement pour le pricing d'Aiva-vision. Lecture estimée : 10 minutes.*
 *
 Données collectées le 28 avril 2026 sur les pages tarifaires officielles ou les rapports publics des éditeurs concernés.* * OPENAI et ENTROPIC sont hors périmétre mais ils existent*
*Létude ne se concentre pas uniquement sur le secteur santé doù les chiffres qui peuvent interroger.*
![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAnEAAAACCAYAAAA3pIp+AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAANElEQVR4nO3OUQmAABBAsSdYxKYXx1gmEBOIFfwTYUuwZWa2ag8AgL841uquzq8nAAC8dj05WgYLQTzjnAAAAABJRU5ErkJggg==)
**1. Synthèse marché — modèles économiques des principaux éditeurs RPA**
| | | | | |
|-|-|-|-|-|
| **Éditeur** | **Statut société** | **Unité de facturation principale** | **Prix publics affichés** | **Caractéristique du modèle** |
| **Microsoft Power Automate** | Filiale Microsoft | **Triple compteur** : per user + per bot + add-ons cloisonnés | **15 $/user/mois** (Premium) — **150 $/bot/mois** (Process, non-attendu) — **215 $/bot/mois** (Hosted, VM Azure incluse) — **5 000 $/tenant/mois** (Process Mining add-on) | Tout est en facturation annuelle. Le client cumule plusieurs lignes de facturation. AI Builder et connecteurs premium se facturent en crédits supplémentaires. |
| **UiPath** | Coté NYSE (PATH), CA ~1,3 Md $ FY24 | **Multi-dimensions** : utilisateurs + robots + tenants + agents IA + testeurs | À partir de **25 $/mois** (Basic, 2 robots maximum) — au-delà, devis personnalisé non public | Cloud SaaS par défaut. On-Premise réservé aux niveaux Standard et Enterprise. Modèle "à la consommation déguisé" : la complexité tarifaire (5+ unités) sert d'instrument de négociation. |
| **Automation Anywhere** | Privée, valorisation déclinante depuis 2022 | Per-bot/an + services IA additionnels | Aucun prix public — pages pricing non accessibles aujourd'hui | Cloud-first. Levées avec SoftBank et Goldman Sachs. Modèle historique 5 000 à 15 000 $/bot/an selon estimations analystes. |
| **SS&C Blue Prism** | Filiale SS&C Technologies depuis 2022 (rachat 1,6 Md $) | Per-bot historique, perpétuel ou abonnement entreprise | Aucun prix public | Modèle vieillissant, désormais une ligne de la suite SS&C. Déploiement souvent on-premise. Historique 10 000 à 20 000 $/bot/an selon estimations. |
| **Bizagi***(EU UK / global)* | Privée | **Consumption-based** sur PaaS | Aucun prix public | Utilisateurs et applications **illimités** dans la souscription, mais **facturation à la consommation** (transactions / exécutions). PaaS cloud uniquement. Modèle opaque, devis sur demande. |
| **Digital Workforce***(EU Finlande)* | Cotée Helsinki, CA ~70 M€ en 2023 | **Managed Service** "Outsmart" + prestations | Aucun prix public | Modèle BPO déguisé : ils opèrent les robots pour le client en mode managed service 24/7. Le client paie un service mensuel, n'opère rien lui-même. Forte dépendance opérationnelle. |
| **Servicetrace → MuleSoft RPA***(EU origine)* | Filiale Salesforce depuis 2021 | Souscription couplée à MuleSoft Anypoint, à la consommation | Dilué dans le pricing Salesforce | L'acteur originel allemand a disparu en tant qu'indépendant. Modèle absorbé dans la suite Salesforce. |
 
![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAnEAAAACCAYAAAA3pIp+AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAM0lEQVR4nO3OUQmAABBAsaeI2MKqV8RyJrGCfyJsCbbMzFldAQDwF/dWrdXx9QQAgNf2B/NkAzRb7P0YAAAAAElFTkSuQmCC)
**2. Quatre patterns de marché — et leur défaut commun**
**A — Per-bot annuel** (UiPath unattended, Automation Anywhere, Blue Prism)
 
 → 1 800 à 15 000 $/bot/an. Le client a peur de déployer.
**B — Per-user mensuel** (Microsoft Premium, UiPath Basic)
 
 → La facture grimpe avec les utilisateurs, ne capture pas la valeur des automatisations non-attendues.
**C — Consumption-based pur** (Bizagi, MuleSoft RPA)
 
 → Imprévisible pour le client, génère des "factures surprises" qui plombent la relation commerciale.
**D — Managed Service / RaaS** (Digital Workforce, Outsmart)
 
 → Dépendance totale au prestataire, perte de souveraineté opérationnelle, marges plus faibles à long terme.
 
**Le défaut structurel commun**
Dans les quatre modèles, **le client paie davantage à mesure qu'il déploie**.
 
 Plus de bots = plus cher. Plus d'utilisateurs = plus cher. Plus de tokens IA = plus cher.
**L'incitation est désalignée** : l'éditeur gagne quand le client fait grossir l'usage, le directeur financier freine pour cette raison exacte. C'est la cause racine de la mauvaise réputation des projets RPA en termes de ROI réel.
![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAnEAAAACCAYAAAA3pIp+AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAANUlEQVR4nO3OMQ2AABAAsSPBCj5fFyM6mJHAjAU2QtIq6DIzW7UHAMBfnGt1V8fXEwAAXrsexOEF35f1aEgAAAAASUVORK5CYII=)
**3. Trois pistes de positionnement pour Aiva-vision**
Trois modèles cohérents avec l'architecture 100 % locale d'Aiva-vision (pas d'API cloud, pas de tokens IA payés à l'externe, ré-ajustements automatiques sans intervention payante). Ces pistes sont mutuellement exclusives à terme, mais peuvent coexister en phase de pénétration.
**Piste 1 — Forfait par site, tout inclus ***(le plus disruptif)*
*« Un forfait annuel par établissement. Utilisateurs illimités, automatisations illimitées, ré-ajustements illimités. Aucun compteur, aucune surprise. »*
| | | |
|-|-|-|
| **Tier** | **Périmètre** | **Ordre de grandeur indicatif** |
| Site unique | 1 établissement, POC ou production | **30 à 60 k€ / an** |
| Multi-sites | Groupement de 2 à 4 établissements | **80 à 150 k€ / an** |
| Régional | Plateforme régionale (ARS, GRADeS, groupe) | **250 à 500 k€ / an** |
 
**Avantages**
- Message direction financière imparable : *"votre coût est connu à la signature"*.
- Barrière structurelle aux acteurs US (modèle qu'ils ne savent pas faire — leurs structures de coûts internes sont indexées sur la consommation).
- Aligne l'incitation : Aivanov gagne quand le client réussit, pas quand il déploie davantage.
**Risques**
- Sous-monétiser un grand groupe qui aurait payé 10 fois plus chez UiPath.
- Difficile de chiffrer la valeur si le périmètre "usages illimités" n'est pas borné contractuellement.
![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAnEAAAACCAYAAAA3pIp+AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAANUlEQVR4nO3OMQ2AABAAsSNhZscVjnidKEAGFtgISaugy8zs1RkAAH9xr9VWHV9PAAB47XoAor8EPg1yCpUAAAAASUVORK5CYII=)
 
 
**Piste 2 — Modules métier ***(le plus lisible commercialement)*
*« Un module = un usage métier. Vous achetez ce que vous utilisez. »*
| | |
|-|-|
| **Composant** | **Ordre de grandeur indicatif** |
| Plateforme socle (apprentissage Aiva-vision + VWB pour les équipes techniques internes) | **25 k€ / an / site** |
| Module codage urgences | **40 k€ / an / site** |
| Module facturation établissement | **30 k€ / an / site** |
| Module anonymisation documentaire | **20 k€ / an / site** |
| Modules futurs (à définir) | À tarifer module par module |
 
**Avantages**
- Facile à expliquer, à comparer, à benchmarker face aux acteurs SaaS classiques.
- Permet l'upsell module par module — courbe de revenus progressive.
- Supporte une logique "preuve par module" en phase de pénétration.
**Risques**
- Le client peut découper et n'acheter qu'un module — moins de valeur captée par contrat.
- Modèle ressemblant au SaaS classique, perd une partie de la différenciation versus la concurrence.
![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAnEAAAACCAYAAAA3pIp+AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAANElEQVR4nO3OQQmAABRAsad4FCtY9ecwnkms4E2ELcGWmTmrKwAA/uLeqrU6vp4AAPDa/gDzUgM9+S8z3AAAAABJRU5ErkJggg==)
**Piste 3 — Setup + licence + maintenance incluse ***(le plus classique et rassurant)*
*« Frais d'apprentissage initial. Licence annuelle. Maintenance évolutive comprise dans la licence — y compris les ré-ajustements lorsqu'un éditeur tiers fait évoluer son interface. »*
| | |
|-|-|
| **Composant** | **Ordre de grandeur indicatif** |
| Setup d'apprentissage initial *(one-shot, ~1 semaine d'observation passive du métier client)* | **~25 k€** |
| Licence annuelle de base *(jusqu'à N utilisateurs, M automatisations)* | **~40 k€ / an / site** |
| Maintenance évolutive *(ré-ajustements automatiques inclus)* | **Inclus** |
| Paliers supérieurs au-delà de N/M | À définir par tranches |
 
**Avantages**
- Format familier pour les directions des systèmes d'information — comparable ligne à ligne avec les éditeurs RPA classiques.
- Sécurise un revenu non-récurrent à la signature *(setup)*.
- **La maintenance évolutive incluse est la principale fonctionnalité de différenciation versus les concurrents** : chez les éditeurs classiques, chaque évolution d'interface tierce déclenche 2 à 5 jours d'intervention payante par un prestataire intégrateur. Chez Aiva-vision, c'est inclus.
**Risques**
- Plus difficile à différencier visuellement *(ressemble au marché)*.
- Le directeur financier peut tiquer sur les paliers utilisateurs si non bornés.
![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAnEAAAACCAYAAAA3pIp+AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAANklEQVR4nO3OMQ2AABAAsSNBCkJfFEIwwIgHRiywEZJWQZeZ2ao9AAD+4lyruzq+ngAA8Nr1AOHsBegrsOrIAAAAAElFTkSuQmCC)
**4. Recommandation**
**Discours commercial et matériel de présentation à destination des directions financières → Piste 1** *(forfait site, tout inclus)*
C'est le seul modèle vraiment disruptif et le seul qui tue frontalement l'argument "facturation à l'usage" présent partout dans le tableau du paragraphe 1. Il porte le récit produit.
**Contractualisation réelle en début de cycle commercial → Piste 3** *(setup + licence + maintenance)*
Les directions hospitalières et les directeurs des systèmes d'information sont rassurés par ce format en phase de premier achat. Le commercial sait le défendre, ligne à ligne, face à un benchmark UiPath ou Microsoft.
**Concrètement**
***Sur le pitch :*** * * *"un forfait annuel, c'est tout, vous savez ce que vous payez à la signature."*
***Dans le contrat :*** * * *"décomposition technique en setup + licence + maintenance incluse, alignée sur la pratique de marché."*
Ce n'est pas un double discours : le forfait *est* l'enveloppe annuelle ; la décomposition technique n'est que la mécanique comptable interne au contrat. Les deux sont parfaitement cohérents.
**À moyen terme (12 à 24 mois)** : basculer progressivement les contrats matures vers la **Piste 1 pure**, lorsque la maturité produit et la base installée le permettent. La Piste 2 *(modules)* peut être utilisée en parallèle pour les segments où le client veut acheter à la carte.
![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAnEAAAACCAYAAAA3pIp+AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAANUlEQVR4nO3OQQmAABRAsSd49m4v6wg/pwmMYQVvImwJtszMXp0BAPAX91pt1fH1BACA164Hoq8EQMMPmF8AAAAASUVORK5CYII=)
 
**5. Questions ouvertes — décisions stratégiques à prendre**
1. **Objectif business prioritaire** : pénétration rapide *(Piste 1, prix d'appel agressif)* ou capture de valeur sur un nombre restreint de clients *(Piste 3, prix plus élevé, marges supérieures)* ?
2. **Politique POC** : un POC client est-il à prix symbolique *(quelques milliers d'euros, accélérateur commercial)*, ou à prix réel *(40 à 60 k€ / an, contrat de référence pour le suivant)* ?
3. **Pricing des extensions multi-sites** : quel coefficient appliquer au passage 1 → 2 → 4 sites au sein d'un même groupe ? Linéaire, dégressif, palier ? Cette décision détermine la stratégie de pénétration des groupements.
4. **Modèle de maintenance** : maintenance incluse intégralement *(argument différenciant fort, marge réduite)* ou maintenance plafonnée à N interventions par an avec dépassement facturé *(cohérent avec la pratique du marché, marge protégée)* ?
5. **Cohérence pricing inter-modules de la plateforme** : Aiva-vision n'est pas la seule brique de la plateforme Aivanov. Il est essentiel que la grille tarifaire soit **cohérente** entre les briques — un client qui compare plusieurs offres internes ne doit pas trouver d'incohérence de logique de pricing entre elles.
![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAnEAAAACCAYAAAA3pIp+AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAANUlEQVR4nO3OQQmAABRAsSfYxKK/kYXEkyk8WcGbCFuCLTOzVXsAAPzFuVZ3dXw9AQDgtesB/v8F8JQadPwAAAAASUVORK5CYII=)
**Annexe — méthode et sources**
**Sources consultées le 28 avril 2026 :**
- Page tarifaire officielle Microsoft Power Automate *(prix vérifiés, en USD)*
- Page tarifaire officielle UiPath *(prix d'appel vérifiés, prix avancés sur demande uniquement)*
- Page plateforme Bizagi *(modèle confirmé, prix non publics)*
- Page services Digital Workforce *(modèle managed service confirmé)*
- Wikipedia — historique de SS&C Blue Prism *(rachats, capitalisations vérifiés)*
- Pages Automation Anywhere et MuleSoft RPA non accessibles à la consultation publique aujourd'hui — données complétées par sources analystes secondaires.
**Limites :**
- Les prix négociés réels diffèrent significativement des prix d'appel affichés *(remises 30 à 60 % usuelles dans les contrats entreprise)*.
- Les modèles évoluent rapidement, en particulier sur les composants IA *(Document Understanding, AI Builder, Agent IA)* qui sont surajoutés aux modèles RPA historiques.
- Aucun prix mentionné dans ce document ne doit être communiqué tel quel à un client externe sans vérification préalable, en raison du caractère négocié des grilles réelles.

View File

@@ -0,0 +1,173 @@
# Points suspects par dossier — à valider avec Pauline
_Revue 2026-05-06 — comparaison `data.js` actuel vs captures Pauline source_
**Méthodo (rappel)** : les noms/prénoms substitués sont des **pseudonymes volontaires** d'anonymisation, ils ne sont **pas signalés**. Je remonte uniquement :
- 🔴 **Hallucinations cliniques** (sens inversé, valeurs inventées) — risque démo
- 🔴 **Constantes vitales tronquées** (capture > data.js) — affecte le DPI fourni au LLM
- 🔴 **Vérité-terrain à corriger** (déjà fait pour 25003284)
- 🟡 **Imagerie à déplacer** (CR à structurer dans nouvel onglet `imagerie`) — je peux faire moi-même
- 🟡 **Question structure** (ex: 2 passages mêlés)
- 🟢 **OK** (rassurance)
**Imagerie** : je peux corriger directement (déplacement de contenu, pas modification clinique). Pour chaque CR détecté, je liste contenu à conserver tel quel.
---
## Vue d'ensemble — sévérité par dossier
| IPP | Cas | VT | Hallu cliniques | Constantes | Imagerie | Autres |
|---|---|---|:---:|:---:|:---:|---|
| 25003284 | Pneumo VRS | Forfait ✅ | 🟢 | 🟢 OK 2/2 | 🟡 RX thorax | 🟡 étiquette workflow imprécise |
| 25003362 | PE2 intox | Forfait | 🟢 | 🟢 OK 1/1 | 🟢 aucune | — |
| 25003364 | UHCD pneumo SLA | UHCD | 🟢 | 🔴 2/4 cols | 🟡 RX pulmonaire | — |
| 25003451 | SU2 plaie | Forfait | 🟢 | 🟢 OK 1/1 | 🟢 aucune | — |
| 25003475 | UHCD aura migr. | UHCD | 🔴🔴 **3 graves** | 🟢 OK 1/1 | 🟡 Scanner cérébral | — |
| 25005866 | UHCD trauma | UHCD | 🟠 1 typo + 1 douteux | 🔴 2/5 cols | 🟡 3 CR (TDMc x2 + RX) | "médecins du sport" : à valider |
| 25010621 | PE2 laryngite | Forfait | 🟢 | 🟢 OK 2/2 | 🟢 aucune | — |
| 25012257 | UHCD douleur abdo | UHCD | 🟢 | 🟠 2/4 cols | 🟡 TDM AP + ECG | — |
| 25048485 | PE2 convulsion | Forfait | 🟢 | 🔴 2/5 cols | 🟢 aucune | 🔴 **2 motifs CTCG** : 2 passages ? |
| 25056615 | Standard salpingite | Forfait | 🟢 | 🟠 2/3 cols | 🟡 TDM AP injecté | — |
| 25151530 | Standard colique | Forfait | 🔴 **TDM avec/sans injection** + ATCD oubliés | 🔴 2/7 cols | 🟡 TDM AP injecté | — |
---
## Détail par dossier
### 25003284 — Pneumopathie VRS / **Forfait Urgences**
- 🟢 **Constantes** : 2 cols data.js = 2 cols capture (05:13 + 03:25). Conforme.
- 🟢 **Aucune hallucination clinique** : tous contenus fidèles, valeurs OK.
- 🟡 **Imagerie à déplacer** : CR de RX thorax actuellement dans `notes_medicales[3]`. À mettre dans `imagerie`. Contenu à garder tel quel : *"Petite plage de condensation parenchymateuse basithoracique gauche avec émoussement du cul-de-sac pleural : minime foyer infectieux dans le contexte ? Pas d'anomalie pleuroparenchymateuse visible à droite. Intégrité de la silhouette cardiomédiastinale."*
- 🟡 **Étiquette workflow** : la liste workflows.js dit "UHCD asthme" alors que c'est en réalité une **consultation externe** (sortie 06:49 = 3h37) avec **diag J12.1 Pneumopathie VRS** (asthme = ATCD). Question Pauline : on relabellise "Pneumopathie VRS" / "Dyspnée fébrile" ?
---
### 25003362 — Intoxication enfant / Forfait PE2
- 🟢 **Constantes** : 1 col data.js = 1 col capture. Conforme.
- 🟢 **Aucune hallucination clinique** : Y14.9, contenu narratif cohérent.
- 🟢 **Aucune imagerie** dans ce dossier.
**Aucun point critique.**
---
### 25003364 — Pneumopathie SLA / UHCD
- 🔴 **Constantes tronquées** : 2 cols data.js, **4 cols capture** (21:02 / 19:45 / 18:44 / 14:45).
- 19:45 : volume miction 700 (oublié)
- 18:44 : T 71, FC 87, PA 168/92, débit O2 2 L (oublié)
- **Question Pauline** : faut-il intégrer les 2 cols manquantes ?
- 🟡 **Imagerie à déplacer** : RP foyer condensation lobaire droite (actuellement `notes_medicales[0]`). À structurer dans `imagerie`.
- 🟢 **Aucune hallucination clinique** dans le contenu narratif.
---
### 25003451 — Plaie suturée / Forfait SU2
- 🟢 **Constantes** : 1 col data.js = 1 col capture (20:36). Conforme.
- 🟢 **Aucune hallucination clinique** : MH/Histoire/Examen/PEC/Conclusion fidèles.
- 🟢 **Aucune imagerie** (suture pure).
**Aucun point critique.**
---
### 25003475 — Aura migraineuse / UHCD
🔴🔴 **3 hallucinations cliniques GRAVES** détectées par le rapport revue précédent — à confirmer/infirmer en visio :
| # | Champ data.js | Texte data.js (faux ?) | Texte capture (vrai ?) |
|---|---|---|---|
| 1 | Histoire maladie | "sensation d'**anhydrose** au talon supérieur" | "sensation d'**ankylose** du **membre supérieur gauche**" |
| 2 | Note IDE 03:09 | "Pansement compressif possible si perfusé" | "RAD possible. **déperfusé**" (sortie possible, désperfusé) |
| 3 | Symptômes orientation | "**Migraines** de membre / déficit / symptômes pouvant évoquer un AVC" | "**Faiblesse** de membre / déficit / symptômes pouvant évoquer un AVC" |
**Sens cliniques INVERSÉS sur les 3 cas**. À corriger d'urgence si confirmé. **Question Pauline** : on prend la version capture ?
- 🟢 **Constantes** : 1 col = 1 col. Conforme.
- 🟡 **Imagerie à déplacer** : Scanner cérébral sans injection (actuellement note médicale "Bio et scan sans anomalie"). À structurer dans `imagerie`.
---
### 25005866 — Trauma crânien hockey / UHCD
- 🔴 **Constantes tronquées** : 2 cols data.js, **5 cols capture** (10:56 / 08:20 / 06:25 / 02:00 / 23:01). Manquent surveillance neuro intermédiaires (08:20, 06:25, 02:00). Pauline avait déjà signalé.
- 🟠 **Note IDE typo** : data.js "VVP posée avant **lits** G" / capture "VVP posée avant **bras** G" (côté). Petite typo.
- 🟠 **"Médecins du sport 0559447669"** : 1er rapport revue dit que c'est inventé. 2e sous-agent dit que c'est dans la capture. **Contradiction interne** → à trancher en visio.
- 🟡 **Imagerie à déplacer** : 3 examens distincts
- TDM cérébral non injecté (01:53) — pas de lésion hémorragique, pas de collection, structures médianes en place
- Radiographie thoracique (01:54) — pas de lésion osseuse, pas de foyer
- TDM cérébral de contrôle (10:18) — absence de lésion post-traumatique décelable
---
### 25010621 — Laryngite enfant / Forfait PE2
- 🟢 **Constantes** : 2 cols data.js = 2 cols capture (15:15, 12:50). Conforme.
- 🟢 **Aucune hallucination clinique** : ATCD asthme + laryngite hospi Bordeaux 25/11/24, TTT FLIXOTIDE/AERIUS, allergie Cefpodoxime, examen ORL/neuro/cardio/respi/digestif — tout fidèle.
- 🟢 **Aucune imagerie** (Examens compl = Aucun).
**Aucun point critique.**
---
### 25012257 — Douleur abdo / UHCD
- 🟠 **Constantes** : 2 cols data.js, **4 cols capture** (10:09 / 09:53 / 08:32 / 06:44). Cols 10:09 (multistix urinaire détaillé : SNG 3+, NIT +, LEU 1+, PRO 2+, pH 6, DEN 1.015) et 08:32 (T 36,3 / FC 60 / PA 165/82) partiellement présentes mais pas séparées. **Question Pauline** : faut-il les séparer ?
- 🟢 **Aucune hallucination clinique**.
- 🟡 **Imagerie à déplacer** :
- **TDM abdomino-pelvien sans injection** (allergie iode) — CR : *"pas de syndrome occlusif, stase stercorale colique globale, stigmates de néphrectomie gauche, dilatation urétéro-pyélo-calicielle droite jusqu'au méat (2 microlithiases), hypertrophie prostatique, surcharge athéromateuse aorto-bi-iliaque. Conclusion : absence de syndrome occlusif radiologique."*
- **ECG** (réalisé 07:45) — résultat RAS
---
### 25048485 — CTCG ado / Forfait PE2
- 🔴 **Question STRUCTURE** : la capture montre **2 motifs distincts** (`1ère CTCG` puis `2ème CTCG`) le même jour. La 2e mentionne *"Sorti d'UP ce matin pour 1er épisode... cette après-midi... récidive d'une CTCG avec cyanose"*. **data.js ne reflète qu'un seul épisode**. → **Question Pauline urgente** : est-ce 2 passages distincts (et il en manque un dans la maquette) ou 1 passage avec 2 épisodes ?
- 🔴 **Constantes tronquées** : 2 cols data.js, **5 cols capture** (12:09 / 10:58 / 10:54 / 10:53 / 10:52) + ligne PA Latéralité (Droite/Gauche) absente.
- 🟢 **Aucune hallucination clinique**.
- 🟢 **Aucune imagerie** (ECG + EEG sans CR détaillé).
---
### 25056615 — Salpingite / Forfait Standard
- 🟠 **Constantes** : 2 cols data.js, **3 cols capture** (20:09 / 19:07 / 16:33). Col 19:07 manquante (T 38,9 / FC 113 / PA 132/86 / SpO2 98 / EVA 3) + ligne PA Latéralité (Droite).
- 🟢 **Aucune hallucination clinique**.
- 🟡 **Imagerie à déplacer** : **Scanner abdomino-pelvien avec injection** — CR complet est actuellement la "Note d'évolution" 20:02. À déplacer dans `imagerie` quasi tel quel.
---
### 25151530 — Colique néphrétique / Forfait Standard
🔴 **2 hallucinations cliniques** + 1 perte d'info :
1. **TDM sans/avec injection ?** : data.js (recap codage) dit "TDM **sans injection**", la capture du CR dit *"Examen réalisé **avec injection** de produit de contraste"*. → ÉLÉMENT IMPORTANT pour PMSI (acte CCAM différent). **Question Pauline** : confirmer.
2. **ATCD/TTT oubliés** : capture montre `"ATCD : RGO / TTT : ESOMEPRAZOLE"` (dans Histoire de la maladie). data.js dit `"atcd et ttt : 0"` (depuis l'obs IDE). **Information clinique perdue** dans data.js.
- 🔴 **Constantes très tronquées** : 2 cols data.js, **7 cols capture** (08:15 / 07:37 / 06:41 / 06:00 / 04:45 / 04:01 / 03:25). Manque 5 cols. L'évolution de la douleur (EN passant de 7 → 0 → 5 → 10 → 6 → 4) totalement aplatie. → **Pour le LLM, c'est important** : un EN qui rebondit à 10 est un signal différent d'une douleur stable.
- 🟡 **Imagerie à déplacer** : TDM abdomino-pelvien (préciser injection après réponse Pauline). CR : *"dilatation urétéro-pyélo-calicielle droite en amont d'une microlithiase du méat urétéro-vésical 2 mm. Pyélon droit 17 mm. Pas de collection périrénale. Aspect de colique néphrétique droite."*
---
## ❓ Questions ouvertes consolidées pour la visio Pauline
| # | Question | Dossier(s) | Priorité |
|---|---|---|---|
| 1 | **25003475 hallucinations cliniques** : "anhydrose"→"ankylose", "Pansement compressif"→"RAD désperfusé", "Migraines"→"Faiblesse" — on bascule sur la version capture ? | 25003475 | 🔴 critique |
| 2 | **25151530 TDM avec/sans injection** ? | 25151530 | 🔴 critique (acte CCAM) |
| 3 | **25048485 — 2 passages distincts ou 1 ?** (capture montre 2 motifs CTCG le même jour) | 25048485 | 🔴 critique (structure) |
| 4 | **25151530 — ATCD RGO + TTT Ésoméprazole à intégrer** ou laisser comme "atcd et ttt : 0" du IAO ? | 25151530 | 🟡 |
| 5 | **Constantes tronquées** : sur 25003364 (2/4), 25005866 (2/5), 25048485 (2/5), 25151530 (2/7), faut-il intégrer toutes les colonnes ou simplifier la démo ? | 4 dossiers | 🟡 |
| 6 | **25005866 "médecins du sport 0559447669"** : présent dans la capture (vu par sous-agent #2) ou inventé (rapport revue #1) ? Contradiction interne. | 25005866 | 🟠 |
| 7 | **25003284 étiquette workflow** : "UHCD asthme" → reformuler en "Pneumopathie VRS" ou "Dyspnée fébrile" ? Diagnostic principal = J12.1 Pneumopathie VRS, asthme = ATCD. | 25003284 | 🟡 |
| 8 | **Onglet Imagerie** : niveau de détail attendu — CR complet par examen (date/protocole/résultats/conclusion) ou résumé condensé ? | tous | 🟡 |
---
## Ce que je peux faire moi-même sans Pauline (déplacement de contenu)
Pour les 7 dossiers avec imagerie à déplacer, je peux extraire le CR de `notes_medicales` ou `motif.diagnostics` et le mettre tel quel dans un nouveau champ `imagerie: [{date, type, par, role, horodatage, contenu}, ...]`. **Pas de modification clinique**, juste un déplacement structurel. Tu valides que je fais cette étape sans attendre Pauline ?

View File

@@ -0,0 +1,438 @@
# Revue qualité — 11 dossiers GHT (vs captures Pauline)
_Date : 2026-05-05_
_Auteur : agent revue qualité Léa_
---
## Synthèse
- **8 dossiers documentés par Pauline** (captures vérité-terrain) : tous présentent **des hallucinations sur les noms de médecins/IDE** (le LLM qui a généré data.js a inventé un grand nombre d'identités à la place de celles présentes dans les captures Pauline). De nombreuses captures montrent **plus de colonnes de constantes** que ce qu'on a dans data.js (jusqu'à 5-7 colonnes vs 2). **6 dossiers sur 8 contiennent un compte-rendu d'imagerie** qui doit être déplacé dans un nouvel onglet `imagerie`.
- **3 dossiers complétés a posteriori** (captures retrouvées dans `RPU UHCD IA.docx`, ajout 2026-05-05) : 25003284 (UHCD VRS), 25003451 (Forfait SU2 plaie), 25010621 (Forfait PE2 laryngite). Mêmes constats : noms hallucinés, contenu narratif globalement OK, 1 CR de RX thorax à déplacer dans onglet `imagerie`.
---
## Détail par dossier (les 8 documentés)
### IPP 25003362 — FORFAIT PE2 (intoxication enfant LAFFONT Alice)
**Captures lues** : 3 PNG (motif/IDE/diag/constantes ; histoire maladie ; détails PEC)
**Identité, passage, sortie** : ✅ matche (Alice LAFFONT, 3 ans, 01/01/2025 13:49→18:30)
**Hallucinations détectées** :
- IAO : data.js = "BLANC Élise" / capture = **"BACQUE Lise"**
- Médecin néphrologue suiveur : data.js = "Dr MERCIER" / capture = **"Dr DIDAILLER"**
- Médecin diagnostic / PEC / décision : data.js = "THOMAS Sandrine" / capture = **"SEIGNE Marie"**
- Notes médicales par "DR. Sandrine THOMAS" / capture probable = "Marie SEIGNE"
- Examen clinique modif par "STHOMAS" / capture = "MSEIGNE" probable
**Signes vitaux** : ✅ valeurs et nb colonnes (1) cohérents
**Diagnostic CIM-10** : ✅ Y14.9
**Examen clinique** : ✅ contenu cohérent
**Codage / synthèse PE2** : ✅ logique cohérente
**Imagerie à déplacer** : aucune (pas de scan/radio dans ce dossier)
---
### IPP 25003364 — UHCD pneumopathie SLA (LEROY Bernard)
**Captures lues** : 5 PNG (motif/diag/constantes ; notes médicales ; examens cliniques + notes IDE ; CR radio ; détails PEC)
**Identité, passage** : ✅ matche (71 ans, 01/01/2025 13:55, sortie hospi UHCD)
**Hallucinations détectées** :
- IAO : data.js = "ROBIN Coralie" / capture = **"ARAMENDY Maya"**
- Médecin PEC : data.js = "SIMON Hélène" / capture = **"VIRTO Isabelle"**
- Médecin diagnostic J18.9 (21:12) : data.js = "DR. Lucas MORIN" / capture = **"DR. Thomas MEZZANO"**
- Note d'évolution 20:25 : data.js = DR. Lucas MORIN / capture = **DR. Thomas MEZZANO**
- Note d'évolution 20:04 : data.js = DR. Hélène SIMON / capture = **DR. Isabelle VIRTO**
- Histoire maladie 17:35 : data.js = DR. Hélène SIMON / capture = **DR. Isabelle VIRTO**
- Examen clinique modif par "HSIMON" / capture = **"IVIRTO"**
- Notes IDE : data.js = "MEYER Fanny" + "CHARPENTIER Aline" / capture = **"Fanny DARRIEUX" + "Sophie NACABAL"**
- Médecin traitant : data.js = "Dr DUVAL" / capture = **"Dr Olivier ANSQUER"**
- Neurologue référent : data.js = "Dr MARTIN" / capture = **"Dr Jean Marie BARNETCHE MT"**
- Médecin décision : data.js = "MORIN Lucas" / capture probable = **"VIRTO Isabelle"** (Détails PEC montre VIRTO)
**Signes vitaux** : valeurs ✅, mais data.js a **2 colonnes** alors que la capture en montre **4** (21:02, 19:45, 18:44, 14:45) — colonne 19:45 (volume miction 700) et 18:44 (T 71, FC 87, PA 168/92, débit O2 2 L) **manquantes dans data.js**.
**Diagnostic CIM-10** : ✅ J18.9
**Avis cardiologie** : ✅ FEVG
**Imagerie à déplacer** : ⚠️ **RADIOGRAPHIE PULMONAIRE** (CR complet visible dans capture 095342) — actuellement le contenu radio est dispersé dans `notes_medicales` (DR. Lucas MORIN 20:25 "RP foyer..."). À structurer dans `imagerie`.
---
### IPP 25003475 — UHCD aura migraineuse (PERRIN Marion)
**Captures lues** : 5 PNG (motif/IDE/diag/constantes ; notes médicales ; notes paramédicales ; CR scanner ; détails PEC)
**Identité, passage** : ✅ matche (Marion PERRIN, 34 ans, 01/01/2025 23:27 → 02/01/2025 03:30)
**Hallucinations détectées** :
- IAO : data.js = "ANDRE Anaïs" / capture = **"ZENON Sandra"**
- Médecin PEC / histoire maladie : data.js = "MEUNIER David" / capture = **"CAZALA Stéphane"**
- Médecin diagnostic R51 : data.js = "DR. Lucas MORIN" / capture = **"DR. Thomas MEZZANO"**
- Note d'évolution 02:46 : data.js = DR. Lucas MORIN / capture = **DR. Thomas MEZZANO**
- Histoire maladie 00:54 : data.js = DR. David MEUNIER / capture = **DR. Stéphane CAZALA**
- Notes IDE : data.js = "POULAIN Antoine" / capture = **"Antoine LEONARD"**
- Avis neurologue : data.js = "Dr ARNAUD" / capture = **"Dr Bannier"**
- Médecin décision : data.js = "MORIN Lucas" / capture = **"CAZALA Stéphane"**
**Symptôme orientation** : data.js = `"- Migraines de membre / déficit / symptômes pouvant évoquer un AVC"` → ⚠️ TYPO (capture = `"- Faiblesse de membre / déficit / symptômes pouvant évoquer un AVC"`)
**Hallucination du contenu narratif** : data.js dit `"sensation d'anhydrose au talon supérieur"` / capture = `"sensation d'ankylose du membre supérieur gauche"`**erreur OCR/LLM grave qui dénature totalement le sens clinique**.
**Note IDE 03:09** : data.js dit "Pansement compressif possible si perfusé" / capture = **"--> RAD possible. dé perfusé"** (sortie possible, désperfusé) — autre erreur de sens.
**Signes vitaux** : ✅ 1 col, valeurs cohérentes
**Diagnostic** : ✅ R51
**Imagerie à déplacer** : ⚠️ **SCANNER CÉRÉBRAL** sans injection (CR visible 095526) — actuellement dispersé dans note médicale DR. Lucas MORIN ("Bio et scan sans anomalie"). À déplacer dans `imagerie` avec le CR complet.
---
### IPP 25005866 — UHCD trauma crânien hockey (MARTINS Diego)
**Captures lues** : 5 PNG (motif+constantes ; histoire maladie ; notes médicales (3) ; notes paramédicales ; détails PEC)
**Identité, passage** : ✅ matche (Diego MARTINS, 17 ans, 03/01/2025 22:59 → 04/01/2025 11:00)
**Hallucinations détectées** ⚠️ (Pauline avait déjà signalé certaines) :
- IAO : data.js = "ANDRE Anaïs" / capture = **"FIANDINO Anaïs"**
- Médecin PEC / histoire maladie : data.js = "CHEVALIER Margaux" / capture = **"BESSON Aude"**
- Médecin diagnostic S06.0 / Conclusion : data.js = "DR. Julien ROBERT" / capture = **"DR. Jean-Baptiste DUFOUR"**
- "Médecin du sport au 0559447669" → CONFIRMÉ FAUX par Pauline (numéro inventé, pas de "médecins du sport" mentionnés)
- Avis spé : data.js = "Dr LEFEVRE (médecin du sport)" / capture = **"Dr PECASTAING"** (allo dans la note d'histoire de la maladie) — pas de médecin du sport en avis spé
- Notes IDE : data.js = "MOULIN Laetitia" / capture = **"Laetitia DOYHENARD"**
- Notes IDE : data.js = "BLOT Sylvaine" / capture = **"Sylvaine MOURE"**
- Notes IDE : data.js = "DA SILVA Juliette" / capture = **"Juliette SCHEFFLER"**
- AS : data.js = "CARPENTIER Jérémy" / capture = **"Jérémy SAUGER"**
**Signes vitaux** ⚠️ (Pauline avait signalé) : data.js a **2 colonnes** / capture en montre **5** (10:56, 08:20, 06:25, 02:00, 23:01). Colonnes 08:20, 06:25, 02:00 **manquantes** (dont les valeurs intermédiaires de surveillance neuro).
**Diagnostic** : ✅ S06.0
**Imagerie à déplacer** : ⚠️ **TDM cérébral** (initial + contrôle) + **Radiographie thoracique** — visibles dans la note d'évolution DUFOUR (10:18) qui contient le CR scanner de contrôle. Actuellement enrobé dans `notes_medicales`. À structurer dans `imagerie` (3 examens : TDMc 01:53 par Aude BESSON, RX thorax 01:54 par Aude BESSON, TDM contrôle 10:18 par DUFOUR).
---
### IPP 25012257 — UHCD douleur abdo polypathologique (BRUNEL Henri)
**Captures lues** : 8 PNG (motif/diag ; constantes ; histoire maladie ; note d'évolution ; suite histoire ; notes IDE+AS ; CR scanner ; détails PEC)
**Identité, passage** : ✅ matche (Henri BRUNEL, 76 ans, 11/01/2025 06:40→14:00)
**Hallucinations détectées** :
- IAO : data.js = "SCHMITT Léa" / capture = **"BAREA Celine"**
- Médecin PEC / histoire maladie : data.js = "DUMONT Sarah" / capture = **"BUSCAGLIA Laura"**
- Médecin diagnostic R10.1 / note d'évolution / décision : data.js = "DR. Justine COLIN" / capture = **"DR. Léna MELAINE"**
- Cardiologue posant la bioprothèse : data.js = "Dr LAMBERT" / capture = **"Dr BARANDON"**
- Avis urologue : data.js = "Dr CARRE" / capture = **"Dr Lammertyn"**
- Notes IDE : data.js = "JACQUET Laurie" / capture = **"Laurie QUESNEL"**
- Notes IDE : data.js = "DUFRESNE Julie" / capture = **"Julie MASOUNAVE"**
- AS : data.js = "POIRIER Franck" / capture = **"Franck LARTIGUE"**
- AS : data.js = "BENOIT Sandra" / capture = **"Fatiha CHABANE"**
**Signes vitaux** : data.js a **2 cols** / capture a **4 cols** (10:09, 09:53, 08:32, 06:44). Cols 10:09 (multistix urinaire détaillé : SNG 3+, NIT +, LEU 1+, PRO 2+, pH 6, DEN 1.015) et 08:32 (T 36,3, FC 60, PA 165/82) partiellement représentées dans la 1ère col data.js mais pas explicitement séparées. La col 06:44 a aussi T=36,5 et taille 169/poids 80 mentionnés, ✅.
**Note IDE 11/01/2025 08:01** (capture) : "Entrée pour suspicion d'occlusion / BS+VVP / ECG / arrêt selles 3 jours quelques gaz / Douleur HCG / A reçu Acupan au SSR" — ✅ retrouvé dans data.js (DUFRESNE Julie 07:50)
**Diagnostic** : ✅ R10.1
**Imagerie à déplacer** : ⚠️ **TDM abdomino-pelvien sans injection (allergie iode)** — CR complet présent dans data.js dans note d'évolution Justine COLIN. À structurer dans `imagerie`. ✅ **ECG** également mentionné comme examen complémentaire (sans CR détaillé).
**Hyperhallucination structure data.js** : la note d'évolution 09:54 contient à la fois la **réévaluation clinique**, le **bilan biologique**, le **CR TDM AP**, la **BU**, l'**avis Dr CARRE/Lammertyn** et le **AU TOTAL** — c'est très tassé. Pour clarifier, à minima : déplacer le CR TDM dans `imagerie`, garder reste dans note d'évolution.
---
### IPP 25048485 — FORFAIT PE2 (CTCG ado NGUYEN Léo)
**Captures lues** : 6 PNG (motif/diag/constantes ; histoire 1 ; examens compl 1 ; histoire 2 ; examens compl 2 (vide) ; détails PEC)
**Identité, passage** : ✅ matche (Léo NGUYEN, 13 ans, 28/02/2025 10:40→17:30)
**Hallucinations détectées** :
- IAO : data.js = "BERTRAND Olivia" / capture = **"SALLES Olivia"** (prénom OK, nom faux)
- Médecin PEC / diag G40.9 / conclusion : data.js = "MARCHAND Sophie" / capture = **"AZAOU Nadia"**
- Histoire maladie : data.js par "DR. Sophie MARCHAND" / capture = **probable "Nadia AZAOU"**
- Avis neuropédiatre : data.js = "Dr BLANC" / capture = **"Dr PICAMILH"**
- Médecin traitant : data.js absent / capture = **"Dr Gafsi Martin"** (mentionné dans la 2ème histoire)
- Examen clinique modif par : data.js = "SMARCHAND" / capture probable = "NAZAOU"
**Signes vitaux** : data.js a **2 cols** / capture a **5 cols** (12:09, 10:58, 10:54, 10:53, 10:52) avec ligne **PA Latéralité (Droite/Gauche)** non présente dans data.js. Colonnes 10:58, 10:54, 10:53 manquantes.
**Diagnostic** : ✅ G40.9
**Examen clinique** : ✅ contenu pédiatrique cohérent
**Particularité importante manquée** : la capture montre **2 motifs de consultation distincts** (`1ère CTCG` puis `2ème CTGC`) — ce dossier semble couvrir **2 épisodes**. Le 2ème (capture 094424) mentionne notamment "Sorti d'UP ce matin pour 1er épisode... cette après-midi... récidive d'une CTGC avec cyanose". **data.js ne reflète qu'un seul épisode.** À clarifier avec Pauline : est-ce le même passage ou 2 passages distincts ?
**Imagerie à déplacer** : aucune (ECG + EEG sans CR imagerie ; pas de scan).
---
### IPP 25056615 — FORFAIT Standard salpingite (PIRES Camille)
**Captures lues** : 4 PNG (motif/diag/constantes ; obs IDE détaillée ; reprise note IDE ; CR scanner)
**Identité, passage** : ✅ matche (Camille PIRES, 39 ans, 12/03/2025 16:30→21:00 transfert gynéco)
**Hallucinations détectées** :
- IAO : data.js = "ANDRE Anaïs" / capture = **"NACABAL Sophie"**
- Médecin PEC / diag N70.0 / note d'évolution / décision : data.js = "MORIN Lucas" / capture = **"DR. Thomas MEZZANO"** (capture confirme par diag)
- Avis gyné : data.js = "Avis gynécologique :" sans nom / capture pas de nom non plus, ✅
- Notes IDE 17:15 : data.js par "ANDRE Anaïs" / capture = probablement **"NACABAL Sophie"**
**Signes vitaux** : data.js a **2 cols** (20:09 + 16:33) / capture a **3 cols** (20:09, 19:07, 16:33). Col 19:07 manquante (T 38,9 / FC 113 / PA 132/86 / SpO2 98 / EVA 3 / EN). Aussi : ligne **PA Latéralité (Droite)** sur 19:07 non présente dans data.js.
**Diagnostic** : ✅ N70.0
**Imagerie à déplacer** : ⚠️ **SCANNER ABDOMINO-PELVIEN avec injection** (CR complet en capture 094711). Actuellement le CR scanner constitue l'intégralité de la "Note d'évolution" 20:02 dans `notes_medicales` (style hybride note + CR imagerie). À déplacer dans `imagerie`.
---
### IPP 25151530 — FORFAIT Standard colique néphrétique (DA SILVA Manuel)
**Captures lues** : 5 PNG (motif/diag/constantes ; histoire maladie ; notes médicales ; avis spé+notes IDE ; CR scanner)
**Identité, passage** : ✅ matche (Manuel DA SILVA, 58 ans, 25/07/2025 03:25→09:46)
**Hallucinations détectées** :
- IAO : data.js = "SCHMITT Léa" / capture = **"BAREA Celine"**
- Médecin PEC / diag N23 / conclusion : data.js = "ROUSSEAU Camille" / capture = **"JOBERT Marine"**
- Note d'évolution 07:59 : data.js par "DR. Aurélie DUBOIS" / capture = **"Isabelle REY"**
- Avis urologue : data.js = "Dr CARRE" / capture = **"Dr Mascle"**
- Examen clinique modif par : data.js = "CROUSSEAU" / capture probable = "MJOBERT"
- Notes IDE : data.js = "MUNOZ Sabrina" / capture = **"Elisabeth GIL"**
- Notes IDE : data.js = "DAVID Marc" / capture = **"marc CASTANCHOA"**
- Notes IDE : data.js = "BARBIER Camille" / capture = **"Camille MALAPELLE"**
**Signes vitaux** ⚠️ : data.js a **2 cols** / capture a **7 cols** (08:15, 07:37, 06:41, 06:00, 04:45, 04:01, 03:25). Très grosse perte d'information : seulement les 1ère et avant-dernière colonnes représentées. Évolution complète de la douleur (EN passant de 7 → 0 → 5 → 10 → 6 → 4) totalement aplatie.
**ATCD/TTT** :
- Obs IDE capture = `"allergie 0 / atcd et ttt : 0"`**MAIS** histoire maladie capture précise `"ATCD : - RGO / TTT : - ESOMEPRAZOLE"`. **data.js suit la capture obs IDE (atcd et ttt: 0) mais OUBLIE de mentionner RGO/Ésoméprazole** dans `notes_medicales`/histoire maladie.
**Diagnostic** : ✅ N23
**Imagerie à déplacer** : ⚠️ **TDM abdomino-pelvien avec injection** (capture 094941 a un CR distinct de la note d'évolution). Note : la capture 094941 dit "avec injection" alors que data.js dit "sans injection" — **vérifier** : la note d'évolution 07:59 capture dit "TDM..." sans préciser, mais le CR formel dit "Examen réalisé avec injection" → data.js a inversé !
---
## Récap général
### Hallucinations à corriger (consolidées)
**Tous les noms de soignants (médecins, IAO, IDE, AS, spécialistes) sont à reprendre.** Le LLM qui a généré data.js a fabriqué des identités fictives au lieu d'utiliser celles présentes dans les captures Pauline. **Pour la démo Pauline il faut absolument repartir des vrais noms** car elle reconnaîtra ses collègues.
| Type | data.js | Vraie valeur capture |
|---|---|---|
| **25003362 IAO** | BLANC Élise | BACQUE Lise |
| **25003362 médecin néphro** | Dr MERCIER | Dr DIDAILLER |
| **25003362 médecin PEC** | THOMAS Sandrine | SEIGNE Marie |
| **25003364 IAO** | ROBIN Coralie | ARAMENDY Maya |
| **25003364 médecin PEC** | SIMON Hélène | VIRTO Isabelle |
| **25003364 médecin diag** | MORIN Lucas | MEZZANO Thomas |
| **25003364 IDE** | MEYER Fanny / CHARPENTIER Aline | DARRIEUX Fanny / NACABAL Sophie |
| **25003364 médecin traitant** | Dr DUVAL | Dr Olivier ANSQUER |
| **25003364 neurologue** | Dr MARTIN | Dr Jean Marie BARNETCHE |
| **25003475 IAO** | ANDRE Anaïs | ZENON Sandra |
| **25003475 médecin PEC** | MEUNIER David | CAZALA Stéphane |
| **25003475 médecin diag** | MORIN Lucas | MEZZANO Thomas |
| **25003475 IDE** | POULAIN Antoine | LEONARD Antoine |
| **25003475 neurologue** | Dr ARNAUD | Dr Bannier |
| **25005866 IAO** | ANDRE Anaïs | FIANDINO Anaïs |
| **25005866 médecin PEC** | CHEVALIER Margaux | BESSON Aude |
| **25005866 médecin diag** | ROBERT Julien | DUFOUR Jean-Baptiste |
| **25005866 contact ext** | Dr LEFEVRE (médecin du sport) | Dr PECASTAING |
| **25005866 IDE x3** | MOULIN/BLOT/DA SILVA | DOYHENARD/MOURE/SCHEFFLER |
| **25005866 AS** | CARPENTIER Jérémy | SAUGER Jérémy |
| **25012257 IAO** | SCHMITT Léa | BAREA Celine |
| **25012257 médecin PEC** | DUMONT Sarah | BUSCAGLIA Laura |
| **25012257 médecin diag** | COLIN Justine | MELAINE Léna |
| **25012257 cardio op** | Dr LAMBERT | Dr BARANDON |
| **25012257 urologue** | Dr CARRE | Dr Lammertyn |
| **25012257 IDE/AS** | JACQUET/DUFRESNE/POIRIER/BENOIT | QUESNEL/MASOUNAVE/LARTIGUE/CHABANE |
| **25048485 IAO** | BERTRAND Olivia | SALLES Olivia |
| **25048485 médecin PEC** | MARCHAND Sophie | AZAOU Nadia |
| **25048485 neuropéd** | Dr BLANC | Dr PICAMILH |
| **25048485 médecin traitant** | (absent) | Dr Gafsi Martin |
| **25056615 IAO** | ANDRE Anaïs | NACABAL Sophie |
| **25056615 médecin PEC** | MORIN Lucas | MEZZANO Thomas |
| **25151530 IAO** | SCHMITT Léa | BAREA Celine |
| **25151530 médecin PEC** | ROUSSEAU Camille | JOBERT Marine |
| **25151530 note évo** | Dr Aurélie DUBOIS | Isabelle REY |
| **25151530 urologue** | Dr CARRE | Dr Mascle |
| **25151530 IDE x3** | MUNOZ/DAVID/BARBIER | GIL/CASTANCHOA/MALAPELLE |
| **25003284 IAO** | CARON Sandrine | BARRAGUE Virginie |
| **25003284 médecin PEC/diag** | BONNET Antoine | LONCA Pierre |
| **25003284 médecin RX** | DR. Marc GIRARD (prescripteur) + Dr Sébastien PETIT (signature) | DR. Pierre VARTABEDIAN (prescripteur) + Dr LAURENT Charles (signature) |
| **25003284 IDE puéri** | Marie LEGRAND | Florence DESGIGOT |
| **25003284 cardio suiveur** | Dr Cohen | Dr Darcy |
| **25003451 IAO** | CARON Sandrine | BARRAGUE Virginie |
| **25003451 médecin PEC/diag** | ROBERT Julien | DUFOUR Jean-Baptiste (cohérent avec 25005866) |
| **25010621 IAO** | ROCHE Émilie | ILHARRAMOUNHO Laure |
| **25010621 médecin PEC/diag** | MARCHAND Sophie | AZAOU Nadia (cohérent avec 25048485) |
| **25010621 senior** | DURAND | BELLEAU |
### Hallucinations narratives (sens clinique)
| IPP | Erreur data.js | Vrai capture |
|---|---|---|
| 25003475 | "sensation d'**anhydrose** au talon supérieur" | "sensation d'**ankylose** du **membre supérieur gauche**" |
| 25003475 | Note IDE 03:09 : "Pansement compressif possible si perfusé" | "RAD possible. déperfusé" (sortie possible, désperfusé) |
| 25003475 | Symptômes orientation : "Migraines de membre / déficit..." | "**Faiblesse** de membre / déficit / symptômes pouvant évoquer un AVC" |
| 25005866 | Phrase "Contacter les médecins du sport au 0559447669" (FR + EN) | **Téléphone et "médecins du sport" inventés** — confirmé par Pauline |
| 25005866 | Note IDE "VVP posée avant lits G" | "VVP posée avant bras G" (côté gauche) |
| 25151530 | TDM "sans injection" (recap codage) | CR capture = "**avec injection** de produit de contraste" |
### Signes vitaux : colonnes manquantes
Toutes les captures montrent plus de colonnes que data.js. Synthèse :
| IPP | data.js | capture | écart |
|---|---|---|---|
| 25003284 | 2 cols | 2 cols | ✅ OK |
| 25003362 | 1 col | 1 col | ✅ OK |
| 25003451 | 1 col | 1 col | ✅ OK |
| 25003364 | 2 cols | 4 cols | -2 cols (19:45, 18:44) |
| 25003475 | 1 col | 1 col | ✅ OK |
| 25005866 | 2 cols | **5 cols** | -3 cols (08:20, 06:25, 02:00) ⚠️ Pauline a signalé |
| 25012257 | 2 cols | 4 cols | -2 cols (10:09 multistix détaillé, 08:32) |
| 25048485 | 2 cols | **5 cols** | -3 cols (10:58, 10:54, 10:53) + ligne PA Latéralité |
| 25056615 | 2 cols | 3 cols | -1 col (19:07) |
| 25151530 | 2 cols | **7 cols** | -5 cols (08:15, 07:37, 06:00, 04:45, 04:01) ⚠️ très gros |
| 25010621 | 2 cols | 2 cols | ✅ OK |
### Contenus imagerie à déplacer dans nouvel onglet
| IPP | Type | Date capture | Source actuelle dans data.js | Contenu à mettre |
|---|---|---|---|---|
| 25003284 | Radiographie thoracique | 01/01/2025 (avant 06:31) | `notes_medicales[3]` (DR. Marc GIRARD 06:31 — "RESULTATS : Petite plage de condensation parenchymateuse...") | CR : "Radiographie thoracique — Indication : pneumopathie. Résultats : petite plage de condensation parenchymateuse basithoracique gauche avec émoussement du cul-de-sac pleural ; minime foyer infectieux dans le contexte ? Pas d'anomalie pleuroparenchymateuse visible à droite. Intégrité de la silhouette cardiomédiastinale. Conclusion : minime foyer basithoracique gauche. Prescrit par DR. Pierre VARTABEDIAN, signé Dr Charles LAURENT." |
| 25003364 | Radiographie pulmonaire | 01/01/2025 (avant 20:25) | `notes_medicales[0]` (DR. Lucas MORIN 20:25 — "RP foyer condensation lobaire droite...") | CR : "Radiographie pulmonaire — Indication : bilan de dyspnée. Résultats : surcroît d'opacité en projection inférieure droite compatible avec un foyer de condensation alvéolaire en cours d'organisation. Pas de signe d'épanchement pleural." |
| 25003475 | Scanner cérébral sans injection | 02/01/2025 (avant 02:46) | `notes_medicales[0]` (note évo "Bio et scan sans anomalie") | CR : "Scanner cérébral — Indication : bilan de céphalées évocatrices d'aura migraineuse. Protocole : sans injection. Résultats : pas d'hyperdensité spontanée axiale ou extra-axiale anormale, pas d'anomalie de la densité parenchymateuse, structures médianes en place, système ventriculaire respecté, pas de syndrome de masse. Conclusion : scanner cérébral normal." |
| 25005866 | TDM cérébral initial | 04/01/2025 01:53 | `notes_medicales[2]` (histoire maladie) | "TDM cérébral non injecté — pas de lésion hémorragique, absence de collection péri-cérébrale, pas d'anomalie densité parenchymateuse, structures médianes en place, absence de lésion osseuse traumatique. Conclusion : absence de lésion post-traumatique." |
| 25005866 | Radiographies thoraciques | 04/01/2025 01:54 | `notes_medicales[2]` | "Radiographies thoraciques — Indication : traumatisme. Absence de lésion osseuse récente sur ce bilan uniquement de face. Pas de foyer de condensation. Pas d'épanchement pleural." |
| 25005866 | TDM cérébral de contrôle | 04/01/2025 10:18 | `notes_medicales[1]` (note évo Réévaluation) | "TDM cérébral de contrôle — Conclusion : absence de lésion post-traumatique décelable." par DR. Jean-Baptiste DUFOUR |
| 25012257 | Scanner abdomino-pelvien sans injection | 11/01/2025 (avant 09:54) | `notes_medicales[0]` (DR. Justine COLIN — bloc TDM AP) | CR : "Scanner abdomino-pelvien — Indication : douleurs flanc gauche, suspicion syndrome occlusif. Protocole : sans injection (insuffisance rénale + allergie iode). Résultats : pas de syndrome occlusif, stase stercorale colique globale, stigmates de néphrectomie gauche, dilatation urétéro-pyélo-calicielle droite jusqu'au méat (2 microlithiases), hypertrophie prostatique, surcharge athéromateuse aorto-bi-iliaque. Conclusion : absence de syndrome occlusif radiologique." |
| 25012257 | ECG | 11/01/2025 ~07:45 | `notes_paramedicales` (POIRIER Franck "ECG fait et montré") | À ajouter en imagerie : "ECG — réalisé 07:45, signé Franck LARTIGUE (AS). Résultat : RAS." |
| 25056615 | Scanner abdomino-pelvien avec injection | 12/03/2025 (avant 20:02) | `notes_medicales[0]` (DR. Lucas MORIN — note d'évolution = CR scanner brut) | Tout le contenu actuel de la note d'évolution 20:02 sauf décision finale. |
| 25151530 | Scanner abdomino-pelvien avec injection | 25/07/2025 (avant 07:59) | `notes_medicales[1]` (DR. Aurélie DUBOIS — bloc TDM dans note évo) | CR : "Scanner abdomino-pelvien — Indication : suspicion de colique néphrétique droite. Protocole : avec injection. Résultats : dilatation urétéro-pyélo-calicielle droite en amont d'une microlithiase du méat urétéro-vésical 2 mm. Pyélon droit 17 mm. Pas de collection périrénale. Conclusion : aspect de colique néphrétique droite." |
### Questions ouvertes pour Pauline (visio J-2)
1. **25048485 NGUYEN Léo — 1 ou 2 passages ?** La capture montre 2 motifs de consultation (1ère CTCG + 2ème CTCG sortant d'UP le matin, puis récidive l'après-midi). Est-ce 2 dossiers distincts ou 1 dossier avec 2 épisodes ? Si 2 dossiers, il en manque un dans la maquette.
2. **25151530 DA SILVA Manuel — TDM avec ou sans injection ?** Le CR capture dit "avec injection" mais le recap data.js dit "sans injection". Confirmer.
3. **25005866 MARTINS Diego — qui est Dr PECASTAING ?** Le médecin appelé par allo dans la capture (et data.js a inventé "Dr LEFEVRE médecin du sport" + un faux numéro 0559447669). Pauline confirme les noms de l'équipe sportive.
4. **Ordre des colonnes constantes** : pour 25005866 et 25151530, faut-il intégrer toutes les colonnes manquantes (5 et 7) ou simplifier la démo ?
5. **Onglet Imagerie** : niveau de détail attendu — un CR par examen avec date/par/protocole/résultats/conclusion comme dans la capture, ou résumé condensé pour la démo ?
6. **Tous les noms** : Pauline peut-elle valider que les noms en captures correspondent aux vrais noms de l'équipe ou sont déjà des pseudos cohérents ? Si pseudos, lesquels reprendre.
7. **Dates 25003364 (LEROY Bernard)** : data.js indique sortie 21:30 / la capture détails PEC montre "01/01/2025 13:55" pour PEC mais pas la sortie. À confirmer la date/heure de sortie.
8. **25003284 — étiquette "UHCD asthme" trompeuse** : c'est en réalité une **consultation externe (sortie 06:49 = 3h37 de passage)** avec **diag principal J12.1 Pneumopathie VRS** ; l'asthme est un ATCD. Faut-il relabelliser dans la liste workflows (`workflows.js` ligne 2010) en "Pneumopathie VRS" ou en "Dyspnée fébrile" ? Et requalifier hors UHCD ?
9. **Cohérence noms inter-dossiers** : DUFOUR Jean-Baptiste apparaît sur 25005866 ET 25003451 → médecin réel récurrent ? AZAOU Nadia apparaît sur 25048485 ET 25010621 → idem ? BARRAGUE Virginie apparaît sur 25003284 (nuit) et 25003451 (soir) le même jour 01/01/2025 — IDE de garde 24h ? Pauline confirme.
### 3 dossiers complétés a posteriori (captures retrouvées dans RPU UHCD IA.docx)
Ces 3 dossiers étaient initialement classés comme "sans captures vérité-terrain". Les captures sources ont été retrouvées le 2026-05-05 dans le docx `RPU UHCD IA.docx` (~/Téléchargements/) sous forme de PNG embarqués (9 images au total). **Voir section "Ajout — 3 dossiers manquants" ci-dessous pour le détail.**
- **25003284** : 4 captures (motif/diag/constantes ; examens cliniques + IDE puéri ; notes médicales ; synthèse PEC). **Diagnostic réel** = J12.1 Pneumopathie VRS (et non "asthme" — l'asthme est ATCD). Hallucinations identité = même profil que les autres dossiers.
- **25003451** : 2 captures (motif/IDE/diag/constantes/histoire ; détails PEC). Hallucinations identité.
- **25010621** : 3 captures (motif/diag/constantes ; histoire maladie ; détails PEC). Hallucinations identité.
L'enjeu reste : **demander à Pauline si les noms en captures sont les vrais noms de l'équipe ou des pseudos validés**.
---
---
## Ajout — 3 dossiers manquants (récupérés du docx, 2026-05-05)
Captures sources : `/tmp/captures_pauline_3manquants/image[1-9].png` (extraites de `~/Téléchargements/RPU UHCD IA (2) (4)/RPU UHCD IA/RPU UHCD IA.docx`). Mapping confirmé par lecture des images :
- images 1-4 → **25003284**
- images 5-6 → **25003451**
- images 7-9 → **25010621**
### IPP 25003284 — UHCD/Consult Pneumopathie VRS (data.js : MOREL Catherine)
**Captures lues** : 4 PNG (motif/IDE/diag/constantes ; examens cliniques + note IDE puéri ; notes médicales ; synthèse PEC)
**Identité, passage** : la capture ne montre **aucun nom patient** (anonymisé en source) ; data.js = MOREL Catherine 77 ans. Dates et heures confirmées : arrivée 03:12, IAO 03:25, sortie 06:49 décision "Consultation externe", US "UC CONSULT.URGENCES" ✅. Note : c'est étiqueté UHCD dans la liste (`workflows.js`) mais en réalité c'est une **consultation externe sortie à 06:49** (3h37 de passage) — la mention "UHCD" dans l'étiquette est imprécise, à reformuler "consult longue" ou similaire.
**Diagnostic réel vs étiquette** : ⚠️ data.js liste motif "Asthme" / capture **diag principal = J12.1 Pneumopathie VRS** (l'asthme n'est qu'un ATCD). data.js a déjà le bon J12.1 dans `motif.diagnostics` et `synthese.diagnostics_synthese`, mais le `motif_court` (passage = "Asthme") et le label dans la liste workflows ("UHCD asthme") sont incohérents avec le vrai diagnostic. **À clarifier** : l'asthme est-il le motif d'arrivée pour Pauline ou faut-il relabelliser "Pneumopathie VRS / dyspnée fébrile" ?
**Hallucinations détectées** :
- IAO : data.js = "CARON Sandrine" / capture = **"BARRAGUE Virginie"**
- Médecin PEC / diag J12.1 / Conclusion / Histoire maladie / Notes évo : data.js = "BONNET Antoine" / capture = **"DR. Pierre LONCA"**
- Note d'évolution 06:31 (RESULTATS RX thorax) : data.js par "DR. Marc GIRARD" / capture = **"DR. Pierre VARTABEDIAN"** — **et le CR est signé en bas par "Dr LAURENT Charles"** alors que data.js indique "Dr Sébastien PETIT"
- Note IDE puéri 04:01 : data.js = "Marie LEGRAND" (Infirmier en puériculture) / capture = **"Florence DESGIGOT"**
- Examen clinique modif par : data.js = "ABONNET" (01/01/2025 04:06) / capture = **"PLONCA"** (cohérent avec LONCA Pierre)
- ATCD insuffisance coronarienne suiveur : data.js = "(Dr Cohen)" / capture = **"(Dr Darcy)"**
- Critère 2 codage cite "Note IDE Marie LEGRAND" → à corriger en "Florence DESGIGOT"
- Critère 3 codage cite "compte rendu Dr PETIT" → à corriger en "Dr LAURENT Charles"
**Signes vitaux** : ✅ 2 cols (05:13, 03:25), valeurs cohérentes (T 39,4/39,3 ; Pouls 117/91 ; PA 136/61/86 à 03:25 ; SpO2 96/95 ; FR 20 ; Ventilation spontanée Air ambiant à 03:25). Aucun écart notable.
**Notes médicales** : contenu narratif globalement ✅ (Conclusion VRS + augmentin + Ventoline 1 bouffée x3/j + Refaire le point à 48h ; Note d'évo "Nettement amélioré par les aérosols" ; Note d'évo Bio 11000GB CRP 25 PCR VRS+ ; HDM toux et dyspnée fébrile, ATCD asthme + insuf coronarienne, TTT amlodipine/resitune/innovair/singulair/pulmicort, Allergies 0). Une nuance : capture HDM mentionne "**Vacciné grippe et COVID il y a 3 sem**" ✅ data.js OK.
**Imagerie à déplacer** : ⚠️ **RADIOGRAPHIE THORACIQUE** (CR complet présent dans la note d'évo 06:31). Texte capture : *"Petite plage de condensation parenchymateuse basithoracique gauche avec émoussement du cul-de-sac pleural : minime foyer infectieux dans le contexte ? Pas d'anomalie pleuroparenchymateuse visible à droite. Intégrité de la silhouette cardiomédiastinale. — Dr LAURENT Charles"*. À déplacer dans onglet `imagerie` avec : type "Radiographie thoracique", indication "bilan pneumopathie", protocole "face debout", résultats + conclusion ci-dessus, signé Dr Charles LAURENT, prescrit par DR. Pierre VARTABEDIAN. **Note PCR VRS** : aussi un examen biologique, à laisser dans note évo ou à structurer dans un futur onglet `biologie`.
---
### IPP 25003451 — FORFAIT SU2 plaie arcade sourcilière (data.js : ROUX Lou 3 ans)
**Captures lues** : 2 PNG (motif/IDE/diag/constantes/histoire-PEC-conclusion ; détails PEC)
**Identité, passage** : capture ne montre aucun nom patient (anonymisé). data.js = ROUX Lou 3 ans F. Dates ✅ (arrivée 20:19, IAO 20:40, PEC 20:19, diag posé 22:19, sortie data.js 22:19 → la capture détails PEC ne montre que le bloc "Détails de la prise en charge" sans la décision médicale, donc on ne peut pas vérifier l'heure de sortie 22:19 ni la décision finale).
**Hallucinations détectées** :
- IAO : data.js = "CARON Sandrine" / capture = **"BARRAGUE Virginie"** (même IDE que sur 25003284 → cohérence horaire : 25003284 sortie 06:49, 25003451 arrivée 20:19, possible que ce soit la même IDE de garde dans la journée)
- Médecin PEC / diag S01.1 / Histoire de la maladie : data.js = "ROBERT Julien" (passage), "DR. Julien ROBERT" (note médicale + diag) / capture = **"DR. Jean-Baptiste DUFOUR"** (image 5 + image 6)
- Médecin décision : data.js = "ROBERT Julien" / capture = **"DUFOUR Jean-Baptiste"** (image 6)
**Important** : c'est le **même DUFOUR Jean-Baptiste** que sur le dossier **25005866** (MARTINS Diego trauma crânien). Cohérence donc — c'est probablement un médecin réel de l'équipe. Vérifier avec Pauline.
**Signes vitaux** : ✅ 1 col (20:36) avec Poids 17 kg + Identité vérifiée Oui. Capture identique à data.js.
**Diagnostic** : ✅ S01.1 Plaie ouverte de la paupière et de la région péri-oculaire — Principal — capture confirme.
**Note médicale (Histoire/Examen/PEC/Conclusion)** : ✅ contenu cohérent
- MH = plaie de l'arcade sourcilière gauche
- Histoire : "Chute de sa hauteur avec traumatisme de la face contre table basse. Plaie de l'arcade sourcilière gauche. pas de perte de connaissance."
- Examen : "Plaie de l'arcade sourcilière gauche d'environ 5cm, linéaire, à suturer. Pas d'atteinte de l'œil"
- PEC : "Anesthésie locale avec Lidocaïne. Pose de 5 points de suture avec fil 5.0 non résorbable à faire retirer dans 5 jours. Nettoyage et pose de vaseline."
- Conclusion : ✅ identique
**Obs IDE** : data.js = `"plaie arcade sourcilière G suite à chute contre table basse / ATCD = 0 // ALLERGIE = recherche en cours / PAS DE PC / PLAIE à suturer ; VAT OK / ATG 1 donné dose / poids"` ✅ identique à capture.
**Imagerie à déplacer** : aucune (suture pure, pas de RX/scan).
**Codage SU2** : recap data.js ✅ (CCMU 2, GEMSA 2, suture CCAM, VAT OK, ATG 1 dose poids).
---
### IPP 25010621 — FORFAIT PE2 Laryngite (data.js : FAURE Tom 5 ans)
**Captures lues** : 3 PNG (motif/IDE/diag/constantes ; histoire de la maladie ; détails PEC)
**Identité, passage** : capture confirme implicitement enfant 5 ans (cf "Enfant de 5 ans adressé par le MT") avec poids 16 kg, taille 110,8 cm — cohérent avec data.js FAURE Tom 03/11/2019. Dates ✅ : arrivée 12:50, IAO 12:54, PEC 13:01, diag posé 15:39 (identique entre data.js et capture).
**Hallucinations détectées** :
- IAO : data.js = "ROCHE Émilie" / capture = **"ILHARRAMOUNHO Laure"**
- Médecin PEC / diag J04.0 / Histoire de la maladie : data.js = "MARCHAND Sophie" (passage + note médicale + décision) / capture = **"AZAOU Nadia"** (images 7, 8, 9)
- Senior dans note "Nom interne + senior" : data.js = `"MARCHAND / DURAND"` / capture = **"AZAOU / BELLEAU"**
- Médecin décision : data.js = "MARCHAND Sophie" / capture = **"AZAOU Nadia"** (image 9)
**Important** : AZAOU Nadia est aussi le médecin réel du dossier **25048485** (NGUYEN Léo — où data.js a aussi halluciné MARCHAND Sophie). **Confirme que MARCHAND Sophie est une identité fictive systématiquement substituée à AZAOU Nadia**. Cohérence à valider avec Pauline.
**Signes vitaux** : ✅ 2 cols (15:15, 12:50). Valeurs identiques entre data.js et capture (T 37,7 ; Pouls 94→123 ; PA 113/52 latéralité Droite ; SpO2 98/98 ; FR 24 ; EVENDOL 0 ; Taille 110,8 ; Poids 16 ; Identité Oui).
**Diagnostic** : ✅ J04.0 Laryngite (aiguë ou sans précision) — Principal.
**Note médicale (Histoire/Examen)** : ✅ contenu globalement cohérent et fidèle à la capture
- Motif "Toux rauque" ✅
- ATCD personnels : asthme + laryngite hospitalisée en réa Bordeaux 25/11/24 (LNHD + CPAP 12h) ✅ identique
- ATCD familiaux : sœur Syndrome Pierre-Marie-Robin ✅
- TTT : FLIXOTIDE 50 2-0-2 + AERIUS 2.5MG 1/J ✅
- Allergies : Cefpodoxime (rash cutané) ✅
- Histoire : 07/01 rhinite sans fièvre, 09/01 toux rauque en quinte, 1mg/kg solupred, parent inquiet ✅
- Examen ORL/Neuro/Cardio/Respi/Digestif : ✅ globalement identique
- Hypothèse : laryngite aiguë ✅
- Examens compl : Aucun ✅
- Évolution : surveillance 15h en box + aérosol pulmicort, persistance toux, pas de désaturation ✅
- Conclusion : laryngite aiguë mineure ne nécessitant pas d'aérosol d'adrénaline ✅
**Obs IDE** : ✅ identique (`"Adressé par médecin laryngite (cf courrier) / 9h30 : solupred / Ici, toux rauque, absence SDL / Atcd : laryngite en réanimation"`)
**Imagerie à déplacer** : aucune (Examens compl = Aucun ✅).
**Codage PE2** : recap data.js ✅ (CCMU 2, GEMSA 2, supplément pédiatrique laryngite enfant 5 ans).
---
## Recommandation tactique pour la démo Pauline
1. **Priorité 1** : corriger les noms (médecins, IAO, IDE) sur les 8 dossiers documentés. Pauline reconnaîtra immédiatement ses collègues et inversement.
2. **Priorité 2** : créer l'onglet `imagerie` et y déplacer les 6 CR scanner/radio listés.
3. **Priorité 3** : compléter les colonnes de constantes manquantes pour 25005866, 25048485, 25151530 (les plus dégradés).
4. **Priorité 4** : corriger les hallucinations narratives (anhydrose→ankylose 25003475, médecins du sport 25005866, RGO/Ésoméprazole 25151530, "avec/sans injection" 25151530).
5. **Lors de la visio** : poser les 7 questions ouvertes ci-dessus + obtenir captures pour 25003284 / 25003451 / 25010621.

View File

@@ -0,0 +1,129 @@
# HANDOFF CLAUDE CHAT — 2026-05-10 fin de journée
## Ce qui a marché aujourd'hui
- Diagnostic clair de l'archi rpa_vision_v3 : projet mature,
beaucoup de composants codés mais non branchés au runtime,
surdocumentation. Recadrage Dom : "le code surpasse la doc".
- Identification du vrai grounder prod (InfiGUI sur core/grounding/,
mais pas câblé au replay engine côté agent_v0/server_v1/ — assumé
pour l'instant)
- Bug #1 (cannot unpack non-iterable NoneType) résolu indirectement
par redémarrage propre Léa via Lea.bat
- Bug #2 (timeout HTTP 30s pendant t2a_decision 26s) identifié,
à corriger pour démo (passer à 60s ou dynamique)
- 3 livrables agent VWB validés et rapatriés :
* Numérotation steps sur canvas (badges 1..N en bleu)
* Ancre zoomable au survol miniature dans Properties (tooltip
600px)
* Nouveau type de step extract_text_scroll (capture haut +
Ctrl+End + capture bas + concat + Ctrl+Home, scroll_pause_ms
configurable, 500ms default)
- Découverte du bug UX VWB : capture.py:select_anchor appelle
silencieusement qwen2.5vl:3b à chaque ancre capturée pour
générer va.description, qui est ensuite affichée sous les
nodes du canvas. VLM hallucine régulièrement, induit Dom en
erreur pendant la construction. À débattre post-démo.
- Doc inspiration frameworks externes en
docs/INSPIRATION_FRAMEWORKS_2026-05-10.md (OpenAdapt, Skyvern,
OmniParser, OpenCUA — convergences fortes avec rpa_vision_v3)
## État machine à la coupure (~20h45 CEST)
- InfiGUI démarré (PID 3079017, ~3934 MiB VRAM, non appelé par
le replay)
- Ollama actif (idle, charge à la demande)
- rpa-streaming actif (PID 3169493)
- Léa Windows polle
- BDD VWB sauvegardée dans
docs/handoffs/2026-05-10_workflows_etat_soir.db (point de
référence avant reprise demain)
## Ce qui reste à clarifier demain matin
PRIORITÉ ABSOLUE : trancher la divergence entre ce que Dom dit
avoir saisi (variables correctes, monitor GPU sans activité VLM
pendant la session de reconstruction) et ce que Claude Code
rapportait de la BDD (incohérences sur step 7, steps 14/17
sans by_text, ordre des extract_text_scroll vs t2a_decision).
Hypothèses à confronter :
- Bug VWB de persistance silencieux (modifs Dom non écrites en BDD)
- Cache frontend obsolète après hot-reload
- Claude Code en dérive de fin de session (lecture biaisée d'une
BDD qui était en réalité conforme)
- Affichage canvas trompeur (sous-labels = descriptions VLM
hallucinées, mais step.label et by_text en BDD sont corrects)
Méthode pour démêler :
1. Cliquer sur chaque node click_anchor du workflow et lire le
champ "Cible textuelle" / by_text dans Properties (pas le
sous-label canvas)
2. Comparer avec ce que Dom voulait
3. Si écart : corriger en repassant par le VWB en se fiant
uniquement à Properties
4. Si pas d'écart : passer directement au dry-run
## 4 points de fix potentiels (à valider d'abord)
1. Step 7 (extract_text) : variable_name = t_notes_medicales
alors que label "Lire Synthèse Urgences" → si confirmé,
renommer en t_synthese_urgences pour cohérence avec
t2a_decision et type_text DPI
2. Steps 14 et 17 (click_anchor) : by_text vide → ajouter le
texte cible
3. Steps 15-19 (4 extract_text_scroll consécutifs) : seulement
2 click_anchor entre eux → vérifier que chaque scroll lit
bien un onglet différent, pas le même contenu 2 fois
4. Step 8 (t2a_decision) : positionné AVANT les steps 15-19 qui
remplissent ses variables d'entrée → soit déplacer après, soit
c'est conforme à un autre design qu'il faut documenter
## Méthode pour la reprise demain matin
- Cerveau frais avant 9h, si possible avant la première vérif
- Ne se fier qu'au by_text dans Properties, jamais aux sous-labels
canvas
- Cycles courts : 1 modif → vérif BDD via Claude Code → valide →
modif suivante
- Si Claude Code retourne quelque chose qui surprend, demander
un dump SQL brut sans interprétation pour vérifier soi-même
- 30-45 min max pour les fix, 15 min dry-run, 1h marge avant démo
(l'après-midi)
## Points méthodo à retenir pour les prochaines sessions
- Claude Code et moi (Claude chat) dérivons après 12h+ de session
continue, comme Dom. Si retour suspect demain matin, redémarrer
une session fraîche plutôt que continuer.
- Le pattern "agent qui valide superficiellement le code mais
pas la fonctionnalité" est récurrent. La vérification
fonctionnelle reste à Dom (et à moi pour l'aider à la cadrer).
- Asymétrie mémoire Dom→Claude vs Claude→Dom : sujet à traiter
en session dédiée.
- Dom a un assistant de longue date appelé Alice, à intégrer
comme référence dans la construction de ma mémoire.
## État des dettes ouvertes
- DETTE-006 (bug d'échelle pixel grounding) : path Ollama, hors
scope démo car cascade ne descend pas jusqu'au grounding VLM
sur cibles textuelles. À fixer post-démo.
- Migration Qwen3-VL-8B-Instruct : non urgente. InfiGUI-G1-3B
satisfait les besoins courants.
- DETTE-014 (smart_resize calé sur Qwen2VLImageProcessor non-Fast) :
à reclasser, factor=28 correcte pour cible Ollama qwen2.5vl
actuelle.
- Asymétrie VWB-direct (utilise UI-DETR-1) vs Replay-distant (ne
l'utilise pas) : asset architectural sous-utilisé. À débattre
post-démo si branchement au runtime sans figer l'apprentissage
est possible.
- Bug UX VWB capture.py : description VLM hallucinée affichée
comme sous-label. Désactiver l'appel VLM (1 commentaire dans
capture.py:225-272) ou changer le frontend.
## Coupure
Dom coupe à 20h45 CEST après 13h de session focus exceptionnelle.
Démo interne lundi après-midi devant DG/DSI/médecins/DIM/TIM.
Marge confortable pour reprise lundi matin avec cerveau frais.

View File

@@ -0,0 +1,20 @@
# Handoff fin de journée — 2026-05-10
**Heure de coupure** : 2026-05-10 20:53 CEST.
## Sauvegarde brute BDD
- **Chemin** : `docs/handoffs/2026-05-10_workflows_etat_soir.db`
- **Source** : `visual_workflow_builder/backend/instance/workflows.db`
- **Statut** : copie binaire intégrale, aucune modification de l'originale.
## Note utilisateur (Dom)
Workflow `Urgence_aiva_demo` reconstruit ce soir. Dom confirme avoir saisi les bonnes variables. Monitor GPU sans activité VLM pendant la session. Divergences rapportées par Claude Code à investiguer demain matin avec cerveau frais — possible bug Claude Code après 13h de session ou possible bug VWB de persistance silencieux.
## 4 points à vérifier demain matin
1. **Step 7** (badge UI 8) `extract_text` — label « Lire Synthèse Urgences » vs `variable_name = "t_notes_medicales"`. Mismatch label/variable à clarifier.
2. **Steps 14 et 17** (badges UI 15 et 18) `click_anchor``by_text` vide en BDD, seulement une ancre visuelle. Vérifier que c'est l'intention.
3. **Ordre** `extract_text_scroll` (steps 15, 16, 18, 19) vs `t2a_decision` (step 8) — `t2a_decision` lit des variables qui ne sont remplies qu'aux steps suivants en BDD. Vérifier l'ordre d'exécution prévu.
4. **Divergence intent utilisateur ↔ BDD** — Dom rapporte que ses variables saisies étaient correctes ; les valeurs en BDD montrent des écarts. Origine à investiguer (frontend, backend, persistance silencieuse).

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,207 @@
# Exploration de correction — bug grounding "fenêtre fantôme 2560×108"
**Date** : 2026-05-11 (fin de journée)
**Statut** : livrable d'exploration. Aucun patch implémenté. Aucune rustine.
**Méthode** : 4 agents experts en parallèle (architecte logiciel, expert robotique, forensique chronologique, expert vision/template_matching). Investigation lecture seule.
---
## 1. Synthèse exécutive
**Cause racine identifiée (confiance HAUTE, convergence des 4 angles)** :
Le pipeline de grounding côté Léa Windows **crop la zone à analyser selon la fenêtre Win32 active** (`GetForegroundWindow()`), **sans vérifier que la cible recherchée est physiquement contenue dans cette fenêtre**. Quand la fenêtre Win32 active à l'instant T est la taskbar / un overlay / la fenêtre Léa elle-même (typiquement 2560×108 en bas d'écran), `cv2.matchTemplate` cherche un élément visuellement similaire **dans cette bande étroite**, trouve un faux positif à score=1.00, et le clic atterrit dans la barre des tâches.
**L'hypothèse "injection de coordonnées fixes" est INFIRMÉE.** Aucun bout de code ne court-circuite la chaîne 100% visuel par injection de coords hardcoded. Le score=1.00 observé est un VRAI score `cv2.matchTemplate (TM_CCOEFF_NORMED)`. Le label `template_matching` n'est pas usurpé. Le bug n'est pas un bypass, c'est une **erreur de cadrage de la zone d'analyse**.
**Note importante** : un MÉCANISME DE CRISTALLISATION existe (`TargetMemoryStore`) qui pourrait pérenniser le bug. Pas encore déclenché aujourd'hui (BDD intacte du 17 avril), mais à corriger pour éviter récidive.
---
## 2. Mécanisme précis (preuve mathématique)
### Coordonnées observées
- Cible BDD attendue : `x_pct=0.0314, y_pct=0.3519` → pixel `(80, 563)` sur écran 2560×1600.
- Coordonnée retournée par le grounding : `x_pct=0.0314, y_pct=0.9563` → pixel `(80, 1530)`.
### Reconstitution étape par étape
1. **Côté Léa** (`agent_v0/agent_v1/core/grounding.py:135-157`) :
- Appel `get_active_window_rect()` → retourne `[left=0, top=1492, right=2560, bottom=1600]`
- `window_rect = {left=0, top=1492, width=2560, height=108}`
- Capture screenshot **uniquement** de cette zone : image 2560×108 envoyée au serveur
2. **Côté serveur** (`agent_v0/server_v1/resolve_engine.py:63-200`) :
- `cv2.matchTemplate(image=2560×108, template=anchor)` → trouve un match parfait à `cy ≈ 37` (un élément visuellement similaire au template existe vraiment dans la taskbar — chiffres, icônes, etc.)
- Retourne `y_pct = 37 / 108 ≈ 0.3426` ← presque identique au 0.3519 demandé **par coïncidence numérique**
- `score = 1.00` ← VRAI score CV2, pas hardcoded
3. **Reprojection côté client** (`grounding.py:181-183`) :
- `abs_y = window_top + y_pct_serveur × window_height = 1492 + 0.3426 × 108 ≈ 1529`
- `y_pct_final = 1529 / screen_height = 1529 / 1600 ≈ 0.9556` (cohérent avec 0.9563 observé)
4. **Click final** (`executor.py:1058-1066`) :
- `real_y = 0.9563 × 1600 = 1530` ✓ — correspond exactement aux logs
### Pourquoi x_pct est identique à l'entrée et à la sortie
Coïncidence géométrique pure : `window_left=0` et `window_width=screen_width=2560`. La formule de conversion x est neutre dans ce cas particulier. **Ce n'est PAS un passthrough**, c'est un artefact de la fenêtre détectée qui coïncide avec la largeur écran.
---
## 3. Détail des 4 investigations
### 3.1 — Chronologique (agent forensique)
**Verdict** : PAS de régression aujourd'hui. Le bug `2560×108 (0, 1492)` est apparu **dès le premier click step_a196 de la journée à 13:56:05**, AVANT toute interaction utilisateur, AVANT tout apprentissage. Donc :
- `target_memory.db` (intact depuis le 17 avril) n'est PAS la cause initiale aujourd'hui
- Aucun store d'apprentissage modifié pendant la journée
- Aucun cache CV2 / template_matching cumulatif
**La cause est purement le Z-order Windows à l'instant T du click**. Quand Chrome a le focus → 2560×1490 → OK. Quand Léa Chat / taskbar / overlay a le focus → 2560×108 → bug.
### 3.2 — Architecte logiciel
**6 chemins identifiés** entre `Léa reçoit action click_anchor` et `pyautogui.click(x, y)` :
1. UIA local (court-circuit Win32) — non utilisé sur web Easily Assure
2. GroundingEngine local avec crop fenêtre — **chemin actif aujourd'hui, défaillant**
3. Memory lookup (TargetMemoryStore) — passthrough sans CV — **risque FUTUR** mais inactif aujourd'hui
4. ExecutionPlan resolve_order pré-compilé — legitimate
5. Drift bypass "high confidence" — amplificateur (désactive la garde de proximité si score ≥ 0.95)
6. Fallback heartbeat — trou de filtre pour wrong-window > 1200 px
**Hypothèse classée haute** : chemins 2 + 5 combinés. Le grounding crop dans la mauvaise fenêtre (chemin 2), template trouve un match parfait par coïncidence visuelle (`score=1.00`), la garde de drift est désactivée par le score élevé (chemin 5), résultat aberrant accepté.
### 3.3 — Expert robotique / contrat 100% visuel
**5 mécanismes de bypass potentiels** :
1. `TargetMemoryStore` — passthrough complet sans CV. Activation : `success_count ≥ 2`. Stocke les coords `human_supervised` sans distinction. **Risque ÉLEVÉ pour pérenniser le bug** mais INACTIF aujourd'hui (BDD non touchée).
2. Score=1.0 hardcoded dans `_resolve_by_ocr_text` (ligne 1450) — légitime sur match texte exact, mais propage 1.0 vers la mémoire
3. `memory_record_success` accepte `report.actual_position` même quand `resolution_method=="human_supervised"` — pollue la mémoire avec coords humaines
4. Fallback `recorded_coords` — apparemment éliminé par commit `40440f1ca`. Vérifié, pas d'occurrence active
5. `execution_mode=supervised` forcé — n'affecte pas la résolution de coordonnées
### 3.4 — Expert vision / template_matching
**Le score=1.00 est un VRAI `cv2.matchTemplate`** (TM_CCOEFF_NORMED). Le label `template_matching` n'est pas usurpé. Pas de chemin passthrough qui produit un faux score parfait.
**Bug racine** : le screenshot 2560×108 envoyé au serveur **ne contient PAS l'élément cherché** (y=563 hors de la fenêtre top=1492). Le serveur trouve un autre élément visuellement quasi-identique dans cette bande → score=1.00 sur le faux positif → le client clique sur le mauvais élément.
**Aucun garde-fou ne vérifie que la cible originale `(fallback_x, fallback_y)` est CONTENUE dans `window_rect` avant le crop**. C'est la pièce manquante.
---
## 4. Pistes de correction (sans coder, à arbitrer)
### Piste A — Garde-fou de contention (PRIORITÉ HAUTE, simple)
**Avant de cropper la fenêtre, vérifier que la cible demandée est contenue dans `window_rect`. Sinon fallback sur écran entier.**
- Localisation : `grounding.py:155` environ
- Logique : `if not (window.left ≤ fallback_x_abs ≤ window.right and window.top ≤ fallback_y_abs ≤ window.bottom): window_rect = None`
- Coût estimé : ~5 lignes
- Risque : très faible (cas normal préservé, cas dégénéré évite le crop)
- Effet attendu : la fenêtre 2560×108 (0, 1492) ne contient pas y=563 → fallback écran entier → grounding sur écran complet → match correct
- Adresse le problème **observé aujourd'hui** sans changer le contrat 100% visuel
### Piste B — Filtre de pertinence sur la fenêtre détectée (complémentaire)
**`get_active_window_rect()` doit rejeter les fenêtres "non métier"** :
- Ratio `width/height > 5` (bande absurde type taskbar)
- Hauteur `< 400 px` (trop petite pour une appli métier)
- Titre contient "Léa", "Assistant", "Taskbar", "Progman", "Shell_TrayWnd" (auto-référence Léa, environnement Windows)
- Process Léa elle-même (`pythonw.exe`) refusée pour s'auto-clamper
- Localisation : `window_info_crossplatform.py:106` (`_get_window_rect_windows`) ou wrapper appelant
- Coût : ~15 lignes
- Risque : faible
- Effet : préviendrait le cas dégénéré à la source
### Piste C — Observabilité minimale (PRIORITÉ MAXIMALE, trivial)
**Logger `title` et `app_name` de la fenêtre détectée**.
- Localisation : `grounding.py:152`
- Logique : modifier le log existant pour inclure `win_info.get("title")` et `win_info.get("app_name")`
- Coût : 1 ligne modifiée
- Risque : zéro
- Effet : connaître précisément le coupable Windows à chaque occurrence du bug. Sans ce log, on tâtonne.
- **À faire absolument avant toute autre intervention**
### Piste D — Refactor "écran entier par défaut" (CHANTIER DE FOND)
**Inverser le contrat : chercher sur l'écran entier par défaut, contraindre à une fenêtre UNIQUEMENT en présence d'une garantie sur le titre/processus.**
- Coût : refactor de `grounding.py` et impacts en cascade
- Risque : élevé (changement comportement étendu)
- Effet : élimine la classe entière de bugs liés à `GetForegroundWindow()`
- À envisager post-démo, pas dans l'urgence
### Piste E — Désactiver mémorisation des `human_supervised` (PRÉVENTION)
**Refuser `memory_record_success` quand `resolution_method == "human_supervised"`** pour éviter qu'une correction humaine sur la mauvaise fenêtre pollue la mémoire et pérennise le bug.
- Localisation : `api_stream.py:3592-3625`
- Coût : ~3 lignes
- Risque : faible
- Effet : la mémoire reste un cristallisateur de coords résolues par grounding visuel réel, pas par correction manuelle
### Piste F — Re-vérification des hits mémoire (RÉSILIENCE)
**Avant de retourner les coords issues de `TargetMemoryStore`, faire un quick `cv2.matchTemplate` de vérification à cette position.**
- Localisation : `resolve_engine.py:1541-1554` autour de `memory_lookup`
- Coût : ~10 lignes
- Risque : faible
- Effet : la mémoire devient un accélérateur, pas un bypass
---
## 5. Recommandation hiérarchisée
| Priorité | Piste | Coût | Risque | Effet sur bug actuel |
|---|---|---|---|---|
| **1** | **C — Observabilité (log title/app_name)** | 1 ligne | nul | aucun direct, mais débloque le diagnostic précis |
| **2** | **A — Garde-fou de contention** | ~5 lignes | très faible | **résout le bug observé aujourd'hui** |
| **3** | **B — Filtre pertinence fenêtre** | ~15 lignes | faible | complète A, robustesse |
| **4** | **E — Bloquer mémorisation human_supervised** | ~3 lignes | faible | prévention récidive |
| **5** | F — Re-vérif hits mémoire | ~10 lignes | faible | résilience long terme |
| **6** | D — Refactor fond | important | élevé | post-démo, chantier S3 |
**Note de méthode** : aucune de ces pistes n'est une rustine. Toutes sont des ajouts de **garantie / observabilité** qui respectent le contrat 100% visuel en le renforçant. La piste A en particulier ferme un trou contractuel : "le grounding ne doit chercher QUE dans une zone qui contient potentiellement la cible", ce qui devrait être un invariant du système.
---
## 6. Points à NE PAS faire
- **Ne pas réactiver `skip_verify` sur les clicks** (déjà testé aujourd'hui, masque le bug au lieu de le résoudre)
- **Ne pas réinjecter des coordonnées recorded comme fallback aveugle** (régression `b584bbabc` déjà curée par commit `40440f1ca`)
- **Ne pas ajouter de `time.sleep` en attendant un focus** (rustine timing, pas une solution)
- **Ne pas implémenter un fix sans avoir d'abord la piste C** : sans log du `title`, on ne sait toujours pas QUOI est ramassé exactement, et toute hypothèse reste partiellement spéculative
---
## 7. Fichiers clés référencés par les 4 agents
- `agent_v0/agent_v1/core/grounding.py:131-187`**point de défaillance principal** (calcul `window_rect`, conversion fenêtre→écran)
- `agent_v0/agent_v1/window_info_crossplatform.py:91-138``_get_window_rect_windows()`, utilise `GetForegroundWindow()` sans filtre sémantique
- `agent_v0/agent_v1/core/executor.py:865-884, 1058-1066` — appel grounding + log + click final
- `agent_v0/server_v1/resolve_engine.py:63-200` — vrai `cv2.matchTemplate`, retour `score`, `method=template_matching`
- `agent_v0/server_v1/resolve_engine.py:1541-1554``memory_lookup` (passthrough sans CV — inactif aujourd'hui, risque futur)
- `agent_v0/server_v1/resolve_engine.py:2330-2340` — drift gate désactivée si score ≥ 0.95 (amplificateur)
- `agent_v0/server_v1/replay_memory.py:142-206``TargetMemoryStore.lookup` / `record_success`
- `agent_v0/server_v1/api_stream.py:3592-3625``memory_record_success` accepte `human_supervised`
- `data/learning/target_memory.db` — BDD persistante des coords apprises (intact aujourd'hui)
---
## 8. Conclusion
**Le contrat 100% visuel n'a pas été violé par un bypass code.** Il a été **mal appliqué sur une zone incorrecte**. Le grounding fait son travail consciencieusement, mais sur le mauvais cadrage. La pièce manquante = vérification que la cible recherchée est dans la zone d'analyse avant de la cropper.
Les 4 agents convergent sur cette analyse. Aucune divergence factuelle. Les pistes A et C sont les plus rentables (bug actuel résolu + diagnostic futur facilité), à coût minimal et risque faible.
À arbitrer à froid demain matin, avec recul, sans pression de démo.

View File

@@ -0,0 +1,148 @@
# Handoff fin de journée — 11 mai 2026
## 1. Contexte de la journée
- **Objectif initial** : démo Urgence_aiva_demo (rpa_vision_v3) prévue 16h pour l'équipe AIVANOV interne (DG, DSI, médecins, DIM, TIM).
- **Décision en cours d'après-midi** : démo reportée, vidéo commentée à enregistrer ultérieurement.
- **Workflow utilisé** : `Demo_urgence_2` (`wf_d04d2dc7c118_1778493082`), 18 steps, créé/recordé le matin.
- **Sessions** :
- Claude Chat (cette session, audit méthode)
- Claude Code (cette session, exécution / diagnostics / patches)
- Autre session Claude Chat pour S2 arbre de décision (séparée).
---
## 2. État actuel de la stack (snapshot 2026-05-11 17:59 CEST)
| Service | Port | PID | État |
|---|---|---|---|
| Frontend VWB Vite | 3002 | 935784 (node) | ✅ LISTEN |
| Backend Flask VWB | 5002 | 4095891 | ✅ LISTEN |
| Agent chat / bus SocketIO | 5004 | 3568779 | ✅ LISTEN, bus ESTAB avec Léa (192.168.1.11:1031) |
| Streaming server | 5005 | 3830788 | ✅ LISTEN, 61 TIME-WAIT (polling Léa actif) |
| Ollama | 11434 | 3169222 + 423007 | ✅ LISTEN |
| Léa Windows | — | — | ✅ Polling actif depuis 17:51:23 (dernier démarrage observé). Bus connecté. SSH ESTAB. |
Dernier événement backend VWB : `17:59:28 GET /api/v3/replay/state/replay_free_b63cf1aa 200`.
---
## 3. Modifications de code appliquées aujourd'hui (toutes machines)
| # | Fichier | Lignes / nature | Statut | Backup | Validation runtime |
|---|---|---|---|---|---|
| 3.1 | `agent_v0/agent_v1/ui/chat_window.py` | Ajout `self.hide()` dans `_on_paused_resume` après émission resume (L1011) | **ROLLBACKÉ** en fin de journée pour test diagnostique (hypothèse focus système). Hypothèse **infirmée** mais rollback maintenu. | `docs/handoffs/2026-05-11_chat_window_avant_fix.py` | OUI (minimisation observée à 15:13/15:48). Mais cause indirecte suspectée puis disculpée. |
| 3.2 | `agent_v0/agent_v1/core/executor.py` | Ajout `time.sleep(0.5)` avant capture screenshot post-click (correction asymétrie click vs type/wait/scroll, L1289) | **ACTIF** côté Windows (déployé via SCP) | `docs/handoffs/2026-05-11_executor_avant_sleep_stabilisation.py` | PARTIELLE (a permis de débloquer le step 3 sans retry forcé en partie). |
| 3.3 | `agent_v0/server_v1/api_stream.py` | Extension `skip_verify` à `click` / `click_anchor` (L3409) | **ROLLBACKÉ** après découverte que la vérif pixel masquait un autre bug (clic à mauvaise position validé comme succès). | `docs/handoffs/2026-05-11_api_stream_avant_skipverify_click.py` | INVALIDÉ (a fait apparaître le bug "clic n'importe où"). |
| 3.4 | `visual_workflow_builder/backend/api_v3/dag_execute.py` | Ajout helper `_expand_extract_text_scroll_in_workflow` (+97 lignes) appelé dans `execute_dag()` (L554) | **INERTE** : endpoint `execute-dag` jamais appelé par le bouton "Exécuter" du frontend. | `docs/handoffs/2026-05-11_dag_execute_avant_expand.py` | NON (jamais exécuté). |
| 3.5 | `visual_workflow_builder/backend/api_v3/dag_execute.py` (suite) | Ajout helper `_expand_extract_text_scroll_in_actions` (+103 lignes) et appel en tête de `execute_windows()` (L1029) | **ACTIF**. C'est ce patch qui expanse réellement les `extract_text_scroll` en 6 sous-actions atomiques. | `docs/handoffs/2026-05-11_dag_execute_avant_patch_execute_windows.py` | PARTIELLE (jamais exécuté complètement à cause d'autres bloqueurs en amont — window-detection cassée). |
| 3.6 | BDD `workflows.db` | Renumérotation `Step.order` de Demo_urgence_2 : 18 steps passés de `order=0` (tous) à `order=0..17` distincts | **ACTIF** | `docs/handoffs/2026-05-11_pre_renum_demo_urgence_2.db` | OUI (badges UI 1..18 corrects, ordre respecté à l'exécution). |
| 3.7 | BDD `workflows.db` | Injection `by_text` sur 8 click_anchor de Demo_urgence_2 (orders 2, 4, 6, 8, 10, 13, 14, 16) | **ACTIF** | `docs/handoffs/2026-05-11_pre_inject_bytext_demo_urgence_2.db` | PARTIELLE (présent dans payload, mais pas suffisant pour rattraper la window-detection cassée). |
---
## 4. Diagnostics confirmés ou écartés aujourd'hui
### Confirmés
- Le frontend VWB appelle `/execute-windows`, **pas** `/execute-dag` ni `/execute/start`. Les deux derniers endpoints sont **dormants** (legacy/test).
- `execute_workflow_thread` itère les steps par `Step.order` SQL croissant (linéaire), les arêtes du canvas ne sont pas persistées en BDD et ne sont pas consultées par le runtime.
- `extract_text_scroll` n'était pas implémenté côté agent Léa Windows. Patch d'expansion serveur appliqué (cf 3.5).
- Léa fait du template_matching dans une **fenêtre cible** retournée par `get_active_window_rect()` (Win32 `GetForegroundWindow()`). Critère unique = foreground window à l'instant t. Aucun filtre sémantique (titre, ratio, taille minimale autre que `w>50, h>50`).
- À **5 occurrences sur 6** dans l'après-midi (15:48, 16:23, 17:02, 17:34, 17:53), `get_active_window_rect()` a retourné une bounding box absurde (typiquement `2560×108 (0, 1492)`). Conséquence : grounding `score=1.00` dans cette bande aberrante → clic à `(80, 1530)` au lieu de `(80, 563)`.
- L'absence de `expected_after` sur les click_anchor permet à Léa de continuer le workflow même quand un clic a atteint la mauvaise zone (l'écran "change" pixel-wise sans avoir atteint la page cible).
### Infirmés
- L'hypothèse "le fix `self.hide()` causait le déplacement de focus système et donc la mauvaise fenêtre détectée" n'est **pas** validée. Rollback de `chat_window.py` n'a pas restauré la bonne détection de fenêtre (bug `2560×108` réapparu à 17:53:32 après rollback).
### Non identifié
- Quel processus Windows exactement est retourné par `GetForegroundWindow()` lors des occurrences foireuses. Le code grounding ne loggue ni `title` ni `app_name` (trou d'observabilité documenté en S3.2.4).
---
## 5. Dette technique S3 — bugs structurels rpa_vision_v3
À traiter en priorité post-démo. Liste exhaustive de tout ce qu'on a identifié aujourd'hui.
### S3.1 — Architecture replay (PRIORITÉ HAUTE)
- **S3.1.1** Le `Step.order` BDD n'est pas synchronisé avec l'ordre visuel du canvas VWB. Les modifications d'ordre via drag-and-drop sur le canvas ne mettent pas à jour `step_order` côté BDD. L'agent exécute selon `Step.order` SQL ; le canvas est cosmétique pour l'exécution.
- **S3.1.2** Les nouveaux steps créés via le VWB ont systématiquement `step_order = 0`. Le code de création de step n'attribue jamais d'order incrémental. Bug structurel découvert en runtime aujourd'hui.
- **S3.1.3** Trois endpoints `/execute-windows`, `/execute-dag`, `/execute/start` coexistent ; un seul est utilisé. Architecture confuse. À simplifier (supprimer les morts ou unifier).
- **S3.1.4** Le helper `_expand_extract_text_scroll_in_workflow` patché aujourd'hui sur `execute-dag` est inerte. Si `execute-dag` est réactivé un jour, il faudra synchroniser avec `_expand_extract_text_scroll_in_actions` de `execute-windows`.
- **S3.1.5** Le contrat entre `execute_windows` et le streaming server n'est pas clair : certaines actions lisent les paramètres au top-level, d'autres dans `parameters`. Une "rustine défensive" duplique les valeurs aux deux endroits (cf `_expand_extract_text_scroll_in_actions`). À nettoyer.
- **S3.1.6** L'action `_concat_text_vars` est marquée serveur-only dans `_SERVER_SIDE_ACTION_TYPES` mais peut être envoyée à Léa par le chemin DAG. Asymétrie à corriger.
### S3.2 — Window-detection et grounding (PRIORITÉ TRÈS HAUTE)
- **S3.2.1** `get_active_window_rect()` dans `window_info_crossplatform.py` se base uniquement sur `GetForegroundWindow()` Win32. Aucun filtre sémantique : titre, process, ratio d'aspect, taille minimale réaliste, comparaison à la fenêtre attendue. Conséquence : bug récurrent `2560×108 (0, 1492)` qui a bloqué la démo aujourd'hui.
- **S3.2.2** Le mode "apprentissage par observation" de Léa (existant et fonctionnel) a une logique sophistiquée de détection de fenêtre (focus, titre, contexte, réflexes). Le mode "replay via VWB" hérite d'une logique window-detection minimaliste qui n'a jamais été unifiée. Chantier d'unification observation ↔ replay = priorité de fond.
- **S3.2.3** Le seul filtre actuel dans `grounding.py:144` est `w > 50 and h > 50`. Pas de docstring, ressemble à un check défensif minimal. À renforcer (h > 400, ratio < 5:1, comparaison au titre attendu).
- **S3.2.4** Aucun logging du `title` / `app_name` retourné par `_get_window_rect_windows()`. Trou d'observabilité majeur qui a empêché le diagnostic précis aujourd'hui. **Quick win pour demain** : ajouter 1 ligne de log.
- **S3.2.5** Le code path `click_anchor` peut emprunter un chemin "coordonnées passthrough" qui applique des `x_pct`/`y_pct` sur la fenêtre détectée sans vérification croisée par template_matching réel ni OCR du `by_text`. Le `score=1.00` retourné dans ce cas n'est pas un vrai score de match. Le contrat "valider visuellement avant cliquer" est de fait court-circuité quand la fenêtre détectée est correcte ; il est catastrophique quand elle est aberrante. **À investiguer** : identifier précisément la condition qui déclenche ce chemin passthrough vs le chemin "vrai grounding".
### S3.3 — Contrat de validation post-action (PRIORITÉ HAUTE)
- **S3.3.1** Un `click_anchor` est validé "réussi" dès que l'écran change pixel-wise. Aucune vérification que la page d'après contient bien le contenu attendu. Léa peut cliquer dans la barre des tâches Windows et déclarer "succès" parce que quelque chose a changé.
- **S3.3.2** Le mécanisme `expected_after` (texte attendu sur la page post-clic, vérifié par OCR) existe dans le code mais n'est exposé ni dans le VWB ni utilisé sur les workflows actuels. À activer et à imposer pour les click_anchor critiques.
- **S3.3.3** La vérification pixel post-click (`verify_action`) compare AVANT et APRÈS et déclenche un retry si le diff est "ambigu". En cas de navigation entre pages, cette logique génère des retries inutiles. Repensé via `skip_verify` aujourd'hui puis rollbacké. À revisiter.
### S3.4 — Recording VWB (PRIORITÉ MOYENNE)
- **S3.4.1** Le recording VWB doit générer automatiquement le `by_text` quand un élément textuel est cliqué (LLM caption ou OCR du voisinage de l'ancre). Sans ça, workflows fragiles (vérifié aujourd'hui : workflow recordé sans `by_text` → comportements aberrants).
- **S3.4.2** L'UI Propriétés VWB n'expose pas le champ `by_text` pour édition manuelle. À ajouter.
- **S3.4.3** Le champ `variable_name` du VWB accepte des templates `{{...}}` qui sont stockés littéralement (ex : `"{{t_motif_admission}}"`). Devrait soit refuser, soit stripper, soit clarifier le sens du champ.
- **S3.4.4** Le VWB n'a pas de validation de cohérence entre l'ordre du canvas et l'ordre BDD à la sauvegarde. Devrait soit refuser la sauvegarde si incohérent, soit recalculer.
- **S3.4.5** `extract_text_scroll` peut être créé dans le VWB sans `anchor_id`. Le runtime peut l'expanser via le patch d'aujourd'hui, mais comportement à valider.
- **S3.4.6** Hallucination VLM systématique sur les chiffres longs : "25003284" lu comme "250003284" (8 vs 9 chiffres). Probablement lié à `qwen2.5vl:3b` (modèle léger). Affecte la description d'ancre, pas le `target_text` OCR ni le `by_text`. Cosmétique mais visible.
### S3.5 — Synchronisation UI VWB ↔ Léa (PRIORITÉ BASSE)
- **S3.5.1** Le PauseDialog du VWB et la bulle Léa s'affichent **en parallèle** sur `pause_for_human`. Aucun n'a la priorité, le premier à répondre gagne. Confusion UX. Quatre options de fix identifiées (A/B/C/D), aucune implémentée aujourd'hui.
- **S3.5.2** La bulle Léa cache parfois ses boutons Continuer/Annuler sous le scroll. UX à fixer (boutons en sticky).
- **S3.5.3** Le bus SocketIO côté Léa est conditionné par la variable d'environnement `LEA_FEEDBACK_BUS=1`. Si non défini, le bus n'est pas instancié → `pause_for_human` ne peut pas reprendre via Léa. Comportement non documenté.
- **S3.5.4** Le service `agent_chat` (port 5004) n'est pas systématiquement démarré. Si absent, le bus reste déconnecté même avec `LEA_FEEDBACK_BUS=1`. À automatiser.
### S3.6 — Observabilité et traçabilité (PRIORITÉ MOYENNE)
- **S3.6.1** Le backend Flask VWB ne loggue pas les payloads des requêtes (uniquement werkzeug INFO). Impossible de reconstituer une session passée. Ajouter `@app.before_request` qui loggue body + URL en debug.
- **S3.6.2** Le champ `workflows.updated_at` n'est pas rafraîchi lors des modifications de steps. Trompeur. À corriger.
- **S3.6.3** `ReplayVerifier` est instancié globalement sans paramètres dans `api_stream.py:52`. Les seuils sont hardcodés dans `replay_verifier.py`, les env vars sont ignorées. Bug architectural à corriger.
- **S3.6.4** Le canvas VWB affiche le badge `step.order + 1` (`StepNode.tsx:104`). Bug cosmétique qui crée une divergence entre les badges visibles et l'order BDD. Convention à uniformiser (0-based ou 1-based partout).
---
## 6. Plan de reprise demain matin
### Priorité 1 — Observabilité minimale (15 min)
Ajouter dans `grounding.py:152` un log qui affiche `win_info["title"]` et `win_info["app_name"]`. Redéployer Léa. Au prochain run, on saura précisément quelle fenêtre Windows est ramassée à tort.
### Priorité 2 — Sanity check window-detection (30 min)
Dans `_get_window_rect_windows()` ou dans le caller : rejeter les fenêtres dont le ratio `width/height > 5` ou hauteur `< 400 px`. Fallback : écran entier. Permet de débloquer le runtime sans refonte profonde.
### Priorité 3 — Unification observation ↔ replay (chantier de fond, plusieurs jours)
Identifier dans le code de l'apprentissage par observation les fonctions de détection de fenêtre par titre/process. Les exposer en API interne. Refactor du replay pour les consommer. **C'est le vrai chantier.**
---
## 7. Choses qui marchent bien (à préserver)
- Le diagnostic SQL sur la BDD `workflows.db` (commandes `sqlite3` directes ont permis de comprendre rapidement l'état du workflow).
- La méthode "investigation lecture seule puis décision" appliquée plusieurs fois aujourd'hui.
- Le système de backups binaires de la BDD avant chaque modif structurelle.
- La séparation Linux (VWB / streaming / Ollama) et Windows (Léa) qui a permis d'isoler les fixes.
- Le watch SQL en monitoring pendant le recording (à utiliser systématiquement à l'avenir).
---
## 8. Choses à NE PAS faire demain
- Reprendre les diagnostics où on en est ce soir. **Repartir frais.**
- Tenter un fix sans avoir d'abord rétabli l'observabilité (S3.6).
- Modifier le code Léa Windows et le code Linux dans le même incrément sans tester chaque étape.
- Repousser les nettoyages S3 "encore un peu" → ils sont la cause des problèmes d'aujourd'hui.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,761 @@
// =====================================================================
// Maquette Easily Assure — moteur de rendu (data-driven)
// =====================================================================
// - Lit le dossier ciblé via ?id=XXXXX (par défaut : premier ORDRE_DOSSIERS)
// - Injecte le bandeau patient, les 4 onglets et la page de codage
// - Volontairement sans dépendance pour rester portable Windows / Linux
// =====================================================================
(function () {
// ----- Helpers -----
function qs(name) {
const m = new RegExp('[?&]' + name + '=([^&]+)').exec(location.search);
return m ? decodeURIComponent(m[1]) : null;
}
function el(id) { return document.getElementById(id); }
function setHTML(id, html) { const e = el(id); if (e) e.innerHTML = html; }
function escapeAttr(s) { return String(s).replace(/"/g, '&quot;'); }
// ID actif (commun à toutes les pages)
const idActif = qs('id') || (typeof ORDRE_DOSSIERS !== 'undefined' ? ORDRE_DOSSIERS[0] : null);
const dossier = (typeof DOSSIERS !== 'undefined' && idActif) ? DOSSIERS[idActif] : null;
// Bouton "Reset" du header — exposé tôt pour rester accessible même
// sur index.html (où l'on quitte la IIFE par early return ligne 50).
// resetCodage est une function declaration : hoistée dans la IIFE.
window.resetCodage = resetCodage;
// ----- Page : index.html (liste patients) -----
const tbody = el('liste-patients');
if (tbody && typeof ORDRE_DOSSIERS !== 'undefined') {
const lignes = ORDRE_DOSSIERS.map(ipp => {
const d = DOSSIERS[ipp];
const enAttente = d.statut_attente;
const statutHtml = enAttente
? '<span style="color:#b25000; font-weight:bold;">En attente</span>'
: '<span style="color:#2e7d32; font-weight:bold;">' + d.passage.statut + '</span>';
return `
<tr class="row-clickable" onclick="window.location.href='dossier.html?id=${ipp}'">
<td><a href="dossier.html?id=${ipp}" class="link">${ipp}</a></td>
<td>${d.identite.nom}</td>
<td>${d.identite.prenom}</td>
<td>${d.identite.ne_le}</td>
<td>${d.passage.arrivee}</td>
<td>${d.passage.motif_court}</td>
<td>${d.passage.medecin}</td>
<td>${statutHtml}</td>
</tr>`;
}).join('');
tbody.innerHTML = lignes;
}
// Si pas de dossier ciblé sur dossier.html / codage.html, on s'arrête là
if (!dossier) {
return;
}
// ----- Bandeau patient (commun dossier.html / codage.html) -----
const banner = el('patient-banner');
if (banner) {
const id = dossier.identite;
const p = dossier.passage;
banner.innerHTML = `
<div class="ipp">IPP : ${id.ipp}</div>
<div class="nom">${id.nom} ${id.prenom}</div>
<div class="info-bloc">Né(e) le <b>${id.ne_le}</b> &nbsp;|&nbsp; <b>${id.age}</b> &nbsp;|&nbsp; Sexe : <b>${id.sexe}</b></div>
<div class="info-bloc">Arrivée : <b>${p.arrivee}</b> &nbsp;|&nbsp; IAO : <b>${p.iao} (${p.iao_heure})</b></div>
<div class="info-bloc">Médecin : <b>${p.medecin}</b> &nbsp;|&nbsp; Sortie : <b>${p.sortie}</b></div>
`;
}
// Bandeau attente
if (dossier.statut_attente) {
const att = el('bandeau-attente');
if (att) att.style.display = '';
document.title = 'Dossier ' + dossier.identite.ipp + ' (en attente) — Easily Assure';
} else {
document.title = 'Dossier ' + dossier.identite.ipp + ' — '
+ dossier.identite.nom + ' ' + dossier.identite.prenom + ' — Easily Assure';
}
// Liens internes : transmettre l'?id=
['lien-codage', 'tab-vers-codage', 'btn-retour-dossier', 'btn-vers-dossier',
'lien-dossier', 'tab-vers-dossier'].forEach(idLien => {
const e = el(idLien);
if (e && e.tagName === 'A') {
const base = e.getAttribute('href').split('?')[0];
e.setAttribute('href', base + '?id=' + idActif);
}
});
// =================================================================
// PAGE DOSSIER : injection des onglets (5 onglets : Motif d'admission,
// Examens cliniques, Imagerie, Notes médicales, Synthèse Urgences)
// =================================================================
if (el('tab-motif')) {
renderMotif(dossier);
renderExamens(dossier);
renderImagerie(dossier);
renderNotesMed(dossier);
renderSynthese(dossier);
installTabs();
}
// =================================================================
// PAGE CODAGE : injection des 3 critères + verdict
// =================================================================
if (el('dpi-input')) {
installAiva(dossier);
}
// -----------------------------------------------------------------
// Onglet 1 : Motif & IDE
// -----------------------------------------------------------------
function renderMotif(d) {
const m = d.motif;
const diagRows = (m.diagnostics.length ? m.diagnostics : [{
code: '<i>Aucun diagnostic enregistré.</i>', type: '', date: '', par: ''
}]).map(diag => `
<tr>
<td><a class="link">${diag.code}</a> ${diag.type ? '&nbsp;-&nbsp; ' + diag.type : ''}</td>
<td style="width:180px;">${diag.date}</td>
<td style="width:200px;">${diag.par}</td>
</tr>`).join('');
// Nb colonnes dynamique : on itère sur signes_vitaux_dates pour gérer
// les dossiers riches (jusqu'à 7 colonnes pour 25151530, 5 pour 25005866 et 25048485, 4 pour 25003364 et 25012257).
const nSvCols = (m.signes_vitaux_dates || []).length || 2;
const sv = m.signes_vitaux.length
? m.signes_vitaux.map(l => {
const cells = [];
for (let i = 1; i <= nSvCols; i++) {
cells.push(`<td class="num">${l['v' + i] !== undefined ? l['v' + i] : ''}</td>`);
}
return `<tr><td>${l.item}</td>${cells.join('')}</tr>`;
}).join('')
: `<tr><td colspan="${nSvCols + 1}" style="color:#888;"><i>Aucun signe vital enregistré.</i></td></tr>`;
const svHeaders = (m.signes_vitaux_dates || []).map(d => `<th>${d || ''}</th>`).join('') || '<th></th><th></th>';
setHTML('tab-motif', `
<div class="section">
<div class="section-header">Motif de la venue</div>
<div class="section-body">
<table class="data">
<tr>
<td style="width:230px;">Symptômes à l'orientation :<br>${m.symptomes_orientation}</td>
<td style="width:200px;">${m.symptomes_date}</td>
<td>${m.symptomes_par}</td>
</tr>
</table>
</div>
</div>
<div class="section">
<div class="section-header">Observations IDE</div>
<div class="section-body">
<table class="data">
<thead><tr>
<th style="width:60%;">Description</th>
<th style="width:20%;">Date</th>
<th>Dernière modif par</th>
</tr></thead>
<tbody><tr>
<td style="white-space:pre-line; line-height:1.5;">${m.obs_ide}</td>
<td>${m.obs_ide_date}</td>
<td>${m.obs_ide_par}</td>
</tr></tbody>
</table>
</div>
</div>
<div class="section">
<div class="section-header">Diagnostics</div>
<div class="section-body">
<table class="data">${diagRows}</table>
</div>
</div>
<div class="section">
<div class="section-header">Signes vitaux - résumé</div>
<div class="section-body">
<table class="data">
<thead><tr>
<th>Item de surveillance</th>
${svHeaders}
</tr></thead>
<tbody>${sv}</tbody>
</table>
</div>
</div>
`);
}
// -----------------------------------------------------------------
// Onglet 2 : Examens cliniques
// (Notes paramédicales : déplacées dans l'onglet Notes médicales —
// review Pauline 04/05/2026)
// -----------------------------------------------------------------
function renderExamens(d) {
const e = d.examens;
const qRows = e.questionnaires.length ? e.questionnaires.map(q => `
<tr>
<td><a class="link">${q.nom}</a></td>
<td class="num">${q.score}</td>
<td style="white-space:pre-line; line-height:1.6;">${q.reponse}</td>
<td>${q.etat}</td>
<td>${q.derniere_modif}</td>
<td></td>
</tr>`).join('')
: `<tr><td colspan="6" style="color:#888;"><i>Aucun questionnaire enregistré.</i></td></tr>`;
const avis = e.avis_specialises.length
? e.avis_specialises.map(a => `<div>${a}</div>`).join('')
: `<p style="color:#888; margin-top:8px;"><i>Aucun avis spécialisé enregistré.</i></p>`;
setHTML('tab-examens', `
<div class="section">
<div class="section-header">Examens Cliniques</div>
<div class="section-body">
<table class="data">
<thead><tr>
<th>Questionnaire</th><th>Score</th>
<th style="width:50%;">Réponse significative 1</th>
<th>Etat</th><th>Dernière modification le</th><th>Motif de suppression</th>
</tr></thead>
<tbody>${qRows}</tbody>
</table>
</div>
</div>
<div class="section">
<div class="section-header">Avis Spécialisés</div>
<div class="section-body">
<a class="link">Recherche</a>
${avis}
</div>
</div>
`);
}
// -----------------------------------------------------------------
// Onglet 3 : Imagerie (review Pauline 04/05/2026 — nouveau)
// Comptes-rendus de scan, radio, échographie regroupés ici
// pour une recherche rapide dédiée.
// -----------------------------------------------------------------
function renderImagerie(d) {
const items = (d.imagerie || []);
const body = items.length
? items.map(im => `
<div style="margin-top:10px; padding:10px; background:#fff; border:1px solid #d0d8e0;">
<div style="margin-bottom:6px;">
<b>${im.date || ''}</b> &nbsp;
<a class="link">${im.type || 'Imagerie'}</a> &nbsp;
<span>noté par <b>${im.par || ''}</b>${im.role ? ' (' + im.role + ')' : ''}${im.horodatage ? ', ' + im.horodatage : ''}</span>
</div>
<div style="white-space:pre-line; line-height:1.6; margin-left:6px;">${im.contenu || ''}</div>
</div>`).join('')
: `<p style="color:#888;"><i>Aucun examen d'imagerie enregistré pour ce passage.</i></p>`;
setHTML('tab-imagerie', `
<div class="section">
<div class="section-header">Comptes-rendus d'imagerie (radio, scan, échographie)</div>
<div class="section-body">
<a class="link">Recherche</a>
${body}
</div>
</div>
`);
}
// -----------------------------------------------------------------
// Onglet 4 : Notes médicales
// Inclut désormais les Notes paramédicales (review Pauline 04/05/2026 :
// déplacées depuis l'onglet Examens cliniques pour regrouper toute la
// rédaction soignante au même endroit).
// -----------------------------------------------------------------
function renderNotesMed(d) {
const blocs = d.notes_medicales.length
? d.notes_medicales.map(n => `
<div class="note-bloc" style="margin-top:10px; padding:10px; background:#fff; border:1px solid #d0d8e0;">
<div><b>${n.date}</b> &nbsp; <a class="link">${n.type}</a> &nbsp; noté par <b>${n.par}</b> (${n.role}), ${n.horodatage}</div>
<div style="white-space:pre-line; line-height:1.6; margin-top:6px;">${n.contenu}</div>
</div>`).join('')
: `<p style="color:#888;"><i>Aucune note médicale enregistrée.</i></p>`;
const para = ((d.examens && d.examens.notes_paramedicales) || []);
const paraBlocs = para.length
? para.map(n => `
<div class="note-bloc" style="margin-top:10px; padding:10px; background:#fff; border:1px solid #d0d8e0;">
<div><b>${n.date}</b> &nbsp; <a class="link">${n.type}</a> &nbsp; noté par <b>${n.par}</b> (${n.role}), ${n.horodatage}</div>
<div style="white-space:pre-line; line-height:1.6; margin-top:6px;">${n.contenu}</div>
</div>`).join('')
: `<p style="color:#888;"><i>Aucune note paramédicale enregistrée.</i></p>`;
setHTML('tab-notes', `
<div class="section">
<div class="section-header">Notes médicales</div>
<div class="section-body">
<a class="link">Recherche</a>
${blocs}
</div>
</div>
<div class="section">
<div class="section-header">Notes paramédicales</div>
<div class="section-body">
<a class="link">Recherche</a>
${paraBlocs}
</div>
</div>
`);
}
// -----------------------------------------------------------------
// Onglet 4 : Synthèse Urgences
// -----------------------------------------------------------------
function renderSynthese(d) {
const s = d.synthese;
function txt(v) { return `<input type="text" value="${escapeAttr(v || '')}">`; }
function dateHeure(date, heure) {
return `<div style="display:flex; gap:6px; align-items:center; padding:0 6px;">
<input type="text" value="${escapeAttr(date || '')}" style="max-width:120px;">
<span>à</span>
<input type="text" value="${escapeAttr(heure || '')}" style="max-width:80px;">
</div>`;
}
function ta(v) { return `<textarea>${escapeAttr(v || '')}</textarea>`; }
setHTML('tab-synthese', `
<div class="synthese-titre">Synthèse Urgences</div>
<table>
<tr class="row-titre"><td colspan="2">Détails de l'épisode</td></tr>
<tr><td class="label">Episode - Date</td><td class="value">${dateHeure(s.episode_date, s.episode_heure)}</td></tr>
<tr><td class="label">Mode de transport à l'arrivée</td><td class="value">${txt(s.mode_transport)}</td></tr>
<tr><td class="label">Médicalisation du transport</td><td class="value">${txt(s.medicalisation_transport)}</td></tr>
<tr><td class="label">Mode d'entrée</td><td class="value">${txt(s.mode_entree)}</td></tr>
<tr><td class="label">Origine du transfert</td><td class="value">${txt(s.origine_transfert)}</td></tr>
</table>
<table>
<tr class="row-titre"><td colspan="2">Détails de l'orientation aux Urgences</td></tr>
<tr><td class="label">Date d'orientation</td><td class="value">${dateHeure(s.orientation_date, s.orientation_heure)}</td></tr>
<tr><td class="label">IAO</td><td class="value">${txt(s.iao)}</td></tr>
<tr><td class="label">Priorité</td><td class="value"><input type="text" value="${escapeAttr(s.priorite || '')}" style="max-width:300px;"></td></tr>
<tr><td class="label">Episode - Sous-type</td><td class="value">${txt(s.sous_type)}</td></tr>
<tr><td class="label">Circonstances</td><td class="value">${txt(s.circonstances)}</td></tr>
<tr><td class="label">Motif de prise en charge</td><td class="value">${ta(s.motif_pec)}</td></tr>
<tr><td class="label">Observ. IDE Urg</td><td class="value">${ta(s.obs_ide_urg)}</td></tr>
</table>
<table>
<tr class="row-titre"><td colspan="2">Détails de la prise en charge</td></tr>
<tr><td class="label">Médecin de la prise en charge médicale</td><td class="value">${txt(s.medecin_pec)}</td></tr>
<tr><td class="label">Date de la prise en charge médicale</td><td class="value">${dateHeure(s.pec_date, s.pec_heure)}</td></tr>
<tr><td class="label">CCMU</td><td class="value">${txt(s.ccmu)}</td></tr>
<tr><td class="label">GEMSA</td><td class="value">${txt(s.gemsa)}</td></tr>
<tr><td class="label">Diagnostics</td><td class="value">${ta(s.diagnostics_synthese)}</td></tr>
</table>
<table>
<tr class="row-titre"><td colspan="2">Décision médicale</td></tr>
<tr><td class="label">Médecin de la décision médicale</td><td class="value">${txt(s.medecin_decision)}</td></tr>
<tr><td class="label">Date de décision médicale</td><td class="value">${dateHeure(s.decision_date, s.decision_heure)}</td></tr>
<tr><td class="label">Décision médicale</td><td class="value">${txt(s.decision)}</td></tr>
<tr><td class="label">Orientation du patient</td><td class="value">${txt(s.orientation)}</td></tr>
<tr><td class="label">US de destination</td><td class="value">${txt(s.us_destination)}</td></tr>
</table>
<div style="text-align:right; margin-top:14px;">
<a href="codage.html?id=${idActif}" class="btn large">Coder &gt;</a>
</div>
`);
}
// -----------------------------------------------------------------
// Onglets dossier — pilotables aussi par #hash
// -----------------------------------------------------------------
function installTabs() {
const tabs = document.querySelectorAll('.tabs .tab[data-tab]');
if (!tabs.length) return;
function activate(target) {
tabs.forEach(t => t.classList.toggle('active', t.getAttribute('data-tab') === target));
document.querySelectorAll('.tab-content').forEach(c => {
c.style.display = (c.id === 'tab-' + target) ? '' : 'none';
});
}
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const target = tab.getAttribute('data-tab');
history.replaceState(null, '', '#' + target + location.search);
activate(target);
window.scrollTo(0, 0);
});
});
// Sections collapsibles
document.querySelectorAll('.section-header').forEach(h => {
h.addEventListener('click', () => h.parentElement.classList.toggle('collapsed'));
});
const initial = (location.hash || '#motif').slice(1).split('?')[0];
if (el('tab-' + initial)) activate(initial);
}
// -----------------------------------------------------------------
// AIVA-VISION — Aide à la décision de facturation urgences
//
// Le bouton "Analyser" envoie le DPI au backend Flask local
// (POST /api/analyse) qui appelle core.llm.t2a_decision (LLM
// qwen2.5:7b par défaut, ~5 s). Le backend enrichit la réponse
// avec verite_terrain + concordance.
//
// En cas d'erreur backend, fallback sur un rendu dérivé localement
// des champs data.js (utilise type_forfait, codage.preuves, etc.)
// pour garder la maquette démontrable hors-ligne.
// -----------------------------------------------------------------
function installAiva(d) {
const ipp = (d.identite && d.identite.ipp) || '';
// ===== 1) DPI vide au chargement =====
// (avant 2026-05-07 : pré-remplissage automatique via buildDpiResume.
// Pour la démo GHT, c'est Léa qui colle le DPI via Ctrl+V — la zone
// doit donc rester vide à l'ouverture. La fonction buildDpiResume
// reste disponible pour le bouton "Reset" et un usage futur.)
const dpiInput = el('dpi-input');
if (dpiInput) dpiInput.value = '';
// ===== 2) État initial : panneau résultat visible avec valeurs neutres
// (textarea Justification éditable AVANT analyse — Léa peut
// y écrire sa propre justification que l'analyse respectera).
// =========================================================
const loading = el('aiva-result-loading');
const result = el('aiva-result');
if (loading) loading.classList.add('aiva-result-hidden');
if (result) result.classList.remove('aiva-result-hidden');
setNeutralState();
// ===== 3) Suppression du bouton Analyser : analyse auto-déclenchée
// - sur paste (Léa colle le DPI via Ctrl+V)
// - sur blur (humain quitte la zone)
// - sur Ctrl+Entrée (raccourci clavier)
// =========================================================
const btn = el('btn-analyser');
if (btn) btn.remove();
let isRunning = false;
let lastAnalysed = '';
let pasteTimer = null;
async function triggerAnalyse() {
const dpi = (dpiInput && dpiInput.value || '').trim();
if (!dpi || isRunning || dpi === lastAnalysed) return;
isRunning = true;
lastAnalysed = dpi;
// Le panneau résultat reste visible (avec ses valeurs neutres ou
// précédentes) — on superpose le spinner par-dessus pour signaler
// l'analyse en cours sans cacher la justification utilisateur.
if (loading) loading.classList.remove('aiva-result-hidden');
try {
const resp = await fetch('/api/analyse', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dpi: dpi, ipp: ipp })
});
const json = await resp.json();
if (!resp.ok) {
throw new Error(json.error || ('HTTP ' + resp.status));
}
renderBackendResult(json);
} catch (err) {
console.warn('Backend /api/analyse indisponible, fallback local :', err);
renderFallback(d);
} finally {
if (loading) loading.classList.add('aiva-result-hidden');
isRunning = false;
}
}
if (dpiInput) {
// Paste → debounce 300ms → analyse
dpiInput.addEventListener('paste', () => {
clearTimeout(pasteTimer);
pasteTimer = setTimeout(triggerAnalyse, 300);
});
// Blur → analyse si contenu changé
dpiInput.addEventListener('blur', triggerAnalyse);
// Ctrl/Cmd+Entrée → analyse immédiate
dpiInput.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
triggerAnalyse();
}
});
}
// ===== 4a) Rendu du résultat backend (LLM réel) =====
function renderBackendResult(r) {
const isUhcd = (r.decision === 'REQUALIFICATION_HOSPITALISATION');
const banner = el('aiva-decision-banner');
banner.className = isUhcd ? 'banner-uhcd' : 'banner-forfait';
banner.textContent = isUhcd
? 'REQUALIFICATION HOSPITALISATION (UHCD)'
: 'FORFAIT URGENCES (FFU/ATU)';
const valor = el('aiva-valorisation');
valor.innerHTML = isUhcd
? '→ valorisation séjour MCO court (UHCD ≥ 24 h, GHM)'
: '→ valorisation forfaitaire (30-200 €)';
const vt = el('aiva-verite-terrain');
if (vt) {
if (r.verite_terrain) {
const cls = r.concordance ? 'concordance-ok' : 'concordance-ko';
const txt = r.concordance ? 'concordance OK' : '⚠ écart vérité-terrain';
vt.style.display = '';
vt.innerHTML = 'Vérité-terrain : <code>' + r.verite_terrain + '</code> — '
+ '<span class="' + cls + '">' + txt + '</span>';
} else {
vt.style.display = 'none';
}
}
el('aiva-confiance').textContent = r.confiance || '—';
const h = r.duree_passage_heures;
el('aiva-duree').textContent = (typeof h === 'number') ? (h.toFixed(1) + ' h') : '—';
const s = r._elapsed_s;
el('aiva-latence').textContent = (typeof s === 'number') ? (s.toFixed(1) + ' s') : '—';
// #aiva-justification est une textarea éditable : si l'utilisateur
// (Léa ou humain) y a déjà écrit avant le clic Analyser, on
// PRÉSERVE sa valeur — elle a la priorité sur la justification LLM.
const justifEl = el('aiva-justification');
if (justifEl && !justifEl.value.trim()) {
justifEl.value = r.justification || '';
}
renderBackendList('aiva-elements-hospi', r.elements_pour_hospitalisation);
renderBackendList('aiva-elements-forfait', r.elements_pour_forfait);
}
function renderBackendList(id, items) {
const ul = el(id);
if (!ul) return;
const arr = Array.isArray(items) ? items : [];
if (!arr.length) {
ul.innerHTML = '<li style="color:#888; font-style:italic;">Aucun élément cité par le LLM.</li>';
} else {
ul.innerHTML = arr.map(t => '<li>' + escapeHtml(String(t)) + '</li>').join('');
}
}
function escapeHtml(s) {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
// ===== 4b) Fallback hors-ligne (dérivé local) =====
function renderFallback(d) {
const cod = d.codage || {};
const typeForfait = cod.type_forfait || null;
const isForfait = typeForfait !== null;
const banner = el('aiva-decision-banner');
const valor = el('aiva-valorisation');
if (isForfait) {
let label = 'FORFAIT URGENCES (FFU/ATU)';
if (typeForfait === 'PE2') label = 'FORFAIT URGENCES — PE2 (pédiatrique)';
else if (typeForfait === 'SU2') label = 'FORFAIT URGENCES — SU2 (acte CCAM)';
banner.className = 'banner-forfait';
banner.textContent = label;
valor.innerHTML = '→ valorisation forfaitaire (30-200 €)';
} else {
banner.className = 'banner-uhcd';
banner.textContent = 'REQUALIFICATION HOSPITALISATION MCO';
valor.innerHTML = '→ valorisation séjour MCO court (UHCD ≥ 24 h, GHM)';
}
const vt = el('aiva-verite-terrain');
const vtCode = isForfait ? 'FORFAIT_URGENCE' : 'REQUALIFICATION_HOSPITALISATION';
vt.innerHTML = 'Vérité-terrain : <code>' + vtCode + '</code> — '
+ '<span class="concordance-ok">concordance OK</span> '
+ '<span style="color:#b25000; font-size:11px;">(mode hors-ligne)</span>';
el('aiva-confiance').textContent = 'elevee';
el('aiva-duree').textContent = computeDureeHeures(d) + ' h';
el('aiva-latence').textContent = '— (offline)';
// #aiva-justification est désormais une textarea : on retire les balises
// HTML (ex. <b>) renvoyées par buildJustification() avant l'affectation.
// Idem renderBackendResult : préserver la saisie utilisateur si déjà remplie.
const justifEl = el('aiva-justification');
if (justifEl && !justifEl.value.trim()) {
const justifHtml = buildJustification(d, isForfait);
justifEl.value = justifHtml.replace(/<[^>]+>/g, '');
}
renderBullets('aiva-elements-hospi', extractBullets(cod.critere1_preuves).concat(extractBullets(cod.critere2_preuves)));
renderBullets('aiva-elements-forfait', extractBullets(cod.critere3_preuves));
}
}
// -------- Helpers de dérivation --------
function buildDpiResume(d) {
// Texte clinique synthétique pour la zone gauche : motif + obs IDE
// + diagnostic principal + 1 note médicale clé + récap décisionnel.
const id = d.identite, p = d.passage, m = d.motif;
const lines = [];
const sexeMotMr = id.sexe === 'F' ? 'Mme' : 'M.';
lines.push(sexeMotMr + ' ' + (id.nom || '').slice(0,1) + '. (' + (id.age || 'âge non précisé') + '), passage urgences ' + (p.arrivee || ''));
lines.push('Motif : ' + (p.motif_court || ''));
if (m.obs_ide) {
lines.push('');
lines.push('Observations IDE :');
lines.push(m.obs_ide);
}
if (m.diagnostics && m.diagnostics.length) {
lines.push('');
lines.push('Diagnostic : ' + m.diagnostics[0].code + ' (' + (m.diagnostics[0].par || '') + ').');
}
// Première note médicale (Conclusion ou Histoire de la maladie)
const conclusion = (d.notes_medicales || []).find(n => /Conclusion/i.test(n.type)) ||
(d.notes_medicales || [])[0];
if (conclusion) {
lines.push('');
lines.push((conclusion.type || 'Note') + ' (' + (conclusion.par || '') + ') :');
// Strip tags HTML du contenu pour la zone DPI texte plat
const text = (conclusion.contenu || '').replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ');
lines.push(text);
}
// Synthèse décisionnelle
if (d.synthese) {
const s = d.synthese;
lines.push('');
lines.push('CCMU ' + (s.ccmu || '').slice(0, 1) + ' / GEMSA ' + (s.gemsa || '').slice(0, 1)
+ '. Décision : ' + (s.decision || '') + (s.orientation ? ' — ' + s.orientation : '') + '.');
}
return lines.join('\n');
}
function computeDureeHeures(d) {
// Cherche la ligne "Durée totale du passage" dans recap_rpu et extrait
// le format "XhYY" pour le convertir en heures décimales.
const recap = (d.codage && d.codage.recap_rpu) || [];
const ligne = recap.find(([k]) => /durée/i.test(k));
if (!ligne) return '—';
const m = String(ligne[1]).match(/(\d+)\s*h\s*(\d+)?/i);
if (!m) return '—';
const h = parseInt(m[1], 10);
const min = m[2] ? parseInt(m[2], 10) : 0;
return (h + min / 60).toFixed(1);
}
function buildJustification(d, isForfait) {
// Construit une phrase de justification courte, à partir du diagnostic + CCMU.
const m = d.motif, s = d.synthese || {};
const diag = (m.diagnostics && m.diagnostics[0] && m.diagnostics[0].code) || 'diagnostic à préciser';
const ccmu = (s.ccmu || '').slice(0, 1);
if (isForfait) {
return 'Le passage est conforme à un <b>forfait urgences</b> : '
+ 'CCMU ' + ccmu + ', état stable à la sortie, prise en charge ambulatoire (' + diag + '). '
+ 'Pas de critère d\'hospitalisation continue justifié sur l\'ensemble des 3 axes (pathologie évolutive / surveillance / actes).';
}
return 'Les 3 critères UHCD sont réunis : pathologie évolutive avec risque d\'aggravation '
+ '(CCMU ' + ccmu + '), surveillance médicale et paramédicale rapprochée, et examens/actes '
+ 'complémentaires réalisés (' + diag + '). <b>Requalification en hospitalisation MCO court séjour.</b>';
}
function extractBullets(html) {
// Convertit les preuves HTML (<br>• item<br>) en tableau de strings
// nettoyés (sans tags). Évite les bullets vides.
if (!html) return [];
const text = String(html)
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<[^>]+>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>');
return text.split('\n')
.map(s => s.replace(/^\s*[•·\-]\s*/, '').trim())
.filter(s => s && !/^Preuves|^Actes (diagnostiques|thérapeutiques) ?:?$/i.test(s));
}
function renderBullets(targetId, items) {
const ul = el(targetId);
if (!ul) return;
if (!items.length) {
ul.innerHTML = '<li style="color:#888; font-style:italic;">Aucun élément retenu.</li>';
return;
}
ul.innerHTML = items.map(s => '<li>' + s + '</li>').join('');
}
// -----------------------------------------------------------------
// Reset démo (bouton "Reset" du header)
//
// Vide la zone DPI et le résultat aiva-vision pour pouvoir rejouer
// la démo Léa plusieurs fois sans recharger la page. Exposé sur
// window pour rester accessible depuis l'attribut onclick="" du
// bouton injecté dans le header de chaque page.
// -----------------------------------------------------------------
function resetCodage() {
// Zone DPI (page Codage uniquement)
const dpiInput = el('dpi-input');
if (dpiInput) {
dpiInput.value = '';
dpiInput.focus();
}
// Panneau résultat → reste VISIBLE, repasse à l'état neutre
// (textarea Justification éditable comme au chargement initial).
const loading = el('aiva-result-loading');
const result = el('aiva-result');
if (loading) loading.classList.add('aiva-result-hidden');
if (result) result.classList.remove('aiva-result-hidden');
setNeutralState();
}
window.resetCodage = resetCodage;
// -----------------------------------------------------------------
// setNeutralState — remet le panneau #aiva-result à l'état "en
// attente d'analyse" (banner gris, métriques —, listes vides,
// textarea Justification vide). Utilisé à l'init et au Reset pour
// garantir que le textarea Justification est éditable AVANT toute
// analyse.
// -----------------------------------------------------------------
function setNeutralState() {
const banner = el('aiva-decision-banner');
if (banner) {
banner.className = 'banner-empty';
banner.textContent = "— En attente d'analyse —";
}
const valor = el('aiva-valorisation');
if (valor) valor.innerHTML = '&nbsp;';
const vt = el('aiva-verite-terrain');
if (vt) vt.innerHTML = 'Vérité-terrain : <code>—</code>';
['aiva-confiance', 'aiva-duree', 'aiva-latence'].forEach(idMetric => {
const e = el(idMetric);
if (e) e.textContent = '—';
});
// #aiva-justification : textarea vide et éditable
const justif = el('aiva-justification');
if (justif) {
if ('value' in justif) justif.value = '';
justif.textContent = '';
}
// Listes éléments → placeholder neutre
const placeholder = '<li class="aiva-list-empty">Aucun élément pour le moment</li>';
['aiva-elements-hospi', 'aiva-elements-forfait'].forEach(idList => {
const ul = el(idList);
if (ul) ul.innerHTML = placeholder;
});
}
})();

View File

@@ -0,0 +1,729 @@
# Audit complet — Chaîne décision t2a (démo GHT Sud 95)
**Date** : 2026-05-12
**Branche** : `feature/qw-suite-mai`
**Workflow audité** : `Demo_urgence_2` (id `wf_d04d2dc7c118_1778493082`)
**Sauvegardes** : `docs/handoffs/2026-05-12_pre_top_decision_{workflows.db,t2a_decision.py,replay_engine.py}`
**Sources officielles archivées** : `docs/clients/ght_sud_95/referentiels_officiels/` (12 PDF)
---
## TL;DR — 6 trouvailles capitales
1. **La règle "2/3 critères ⇒ UHCD" est officiellement FAUSSE.** Source primaire : Instruction DGOS/R1/DSS/1A/2020/52 Annexe 3 + Guide ATIH MCO 2025 et 2026 §1.2. Vraie règle : **conjonction ET (3/3 cumulatifs)** appréciée AVANT admission UHCD. La règle 2/3 expose à requalification CPAM.
2. **Aucun seuil de durée opposable en droit français.** Le "6h" évoqué (DIM sénior) relève du folklore / Clinical Decision Units anglo-saxonnes. DMS cible < 24h, tolérance jusqu'à 36h/2 nuits (recommandation SFMU, pas opposable).
3. **🔴 TWIST MOREL Catherine** : selon règle officielle, les 3 critères cumulatifs sont remplis ⇒ **UHCD légalement justifiable (~375€)**. La VT actuelle `server.py:32` (FORFAIT) est probablement à inverser. Le vrai bug = mode_sortie codé "Consultation externe" au lieu de "Domicile" (mode 8) attendu pour UHCD ⇒ perte de recettes détectable, **VRAI ROI démo**.
4. **"FF1/FF2/FF3" n'existent PAS** dans la nomenclature officielle. Vraie nomenclature : **FU0/FU1/FU2/FU3/FU4** (forfaits âge) + suppléments **SU2/SU3, PE1/PE2, SUB/SB2/SB3, SIM/SIC, SUN/SUF, SUM, SAS, SSN/SSF**.
5. **PE2 ≠ matériel pédiatrique** : c'est un supplément clinique pédiatrique (diagnostic CIM-10 de la liste 2 annexe 19), pas un forfait matériel.
6. **5 causes racines bugs MOREL** : règle PMSI mal formulée (1) + non-déterminisme Ollama T=0.1 sans seed (2) + confusion durée_symptômes/passage (3) + pas de verrou cohérence step 17 (4) + listes hospi/forfait non mutuellement exclusives (5).
---
## Rapport 1 — DIM sénior (audit clinique PMSI)
### Verdict global
- **PROMPT 1** (t2a_decision) : **5/10**. Règle 2/3 fausse, durée sous-spécifiée, critère décisif (mode sortie) manquant, listes hospi/forfait non contraintes.
- **PROMPT 2** (Résumé) : **7/10**. Globalement bon, 3 lignes à ajouter.
- **PROMPT 3** (Justification) : **4/10**. Verrouillage insuffisant, pas d'auto-vérification, conflit de cardinalité décision/preuves.
### Diagnostic MOREL (lecture stricte avant validation référentiel officiel)
- Critère 1 (pathologie évolutive) : OUI (77 ans, insuf coronarienne, asthme, fièvre 39.4°C, peakflow 260, tachycardie 117)
- Critère 2 (surveillance >6h) : NON selon DIM sénior (durée 3h37)
- Critère 3 (actes/examens) : OUI (radio, PCR VRS, bio, aérosols, antibio)
- **DIM sénior tranche FORFAIT** car durée < 6h + RAD ⇒ critère 2 invalidé ⇒ pas 2/3.
- **⚠️ Référentiel officiel postérieur (rapport 4) contredit** : le seuil 6h n'est pas opposable, donc critère 2 reste OUI (surveillance hospitalière documentée par aérosols ×3 + ATB initiée + monitoring) ⇒ 3/3 ⇒ UHCD.
### Amendements PROMPT 1 proposés par DIM
Ajouter avant la liste des critères :
```
RÈGLE ÉLIMINATOIRE — DURÉE ET MODE DE SORTIE :
- Si durée_passage < 6h ET mode sortie = "domicile/RAD" ⇒ FORFAIT_URGENCE,
sauf surveillance médicale continue documentée explicitement.
- Si durée_passage > 24h ⇒ hospitalisation conventionnelle (hors UHCD).
- "durée du passage" = horaire ADMISSION → horaire SORTIE.
N'EST PAS la durée des symptômes pré-hospitaliers (« douleurs depuis 23h » ≠ durée passage).
```
⚠️ **Cette règle éliminatoire ne doit PAS être adoptée telle quelle** car le seuil 6h n'a aucune base réglementaire. À retenir uniquement : la distinction durée_symptômes / durée_passage, et l'importance du mode de sortie.
### Amendements PROMPT 1 — ajouts dans INSTRUCTIONS STRICTES
```
- `elements_pour_hospitalisation` = éléments qui PROLONGENT le séjour ou imposent surveillance hospitalière
(ex: « scope continu », « réévaluation H+2 et H+4 », « transfert UHCD demandé »)
- `elements_pour_forfait` = éléments qui RACCOURCISSENT ou ORIENTENT vers la sortie
(ex: « RAD à H+3:30 », « constantes stabilisées », « sortie domicile avec ordonnance »)
- Une « sortie en consultation externe à 48h » est un argument POUR le forfait, jamais pour l'hospitalisation.
- decision_court DOIT être strictement couplé à decision (jamais "UHCD" + FORFAIT).
```
### Amendements PROMPT 2 — 3 lignes à ajouter
```
11. Mentionne explicitement la durée du passage aux urgences en heures et minutes (admission → sortie),
distincte de la durée des symptômes rapportés par le patient.
12. Cite explicitement le mode de sortie (retour à domicile, UHCD, mutation MCO, transfert) si présent au dossier.
13. Liste les actes techniques et examens complémentaires effectués (biologie, imagerie, gestes CCAM).
```
### Amendements PROMPT 3 — verrouillage anti-divergence
```
RÈGLES STRICTES :
1. La décision EST {{dec.decision}} ({{dec.decision_court}}). Tu ne la discutes pas, tu ne la nuances pas,
tu ne la contredis pas, même implicitement. Toute formulation suggérant la décision opposée est interdite.
2. Source unique : RESUME_CLINIQUE et PREUVES. N'invente aucun argument.
3. Rédige 4 à 7 phrases.
4. Structure imposée :
a) Phrase 1-2 : éléments en faveur de la décision (citer le résumé).
b) Phrase 3-4 : éléments contradictoires, EXPLICITEMENT minorés.
c) Phrase finale OBLIGATOIRE : "En conclusion, ce passage relève de {{dec.decision_court}} au regard de
[résumé en une phrase du critère décisif]."
5. Si FORFAIT_URGENCE : phrase finale doit contenir "forfait" et NE PAS contenir "UHCD"/"requalification"/"hospitalisation"
autrement qu'en négation explicite.
6. Si REQUALIFICATION_HOSPITALISATION : inversement.
7. Avant de répondre, relis ta dernière phrase et vérifie qu'elle est cohérente avec la décision imposée.
```
### Cas PE2/SU2 (4 dossiers démo)
Pipeline actuel ne les gère pas. Recommandation : branche déterministe CCAM en amont OU exclusion du scope démo.
---
## Rapport 2 — Ingénieur prompt (robustesse + verrou)
### Cause racine oscillation MOREL (4 runs / 2 décisions)
- T=0.1 + pas de seed + `format=json` + bruit OCR variable cross-runs
- Combo nécessaire pour déterminisme : `temperature=0, seed=42, top_p=1.0, top_k=0`
- **Geler `dpi_canonique` en variable de session** (SQLite `replay_state.variables`) pour tuer la variance OCR.
### Cause racine contradiction step 3 ("justifie l'UHCD" alors decision=FORFAIT)
Combinaison de :
- **A** (conflit prompt vs context) : secondaire
- **B** (séparation prompt/context dans `LLMActionHandler.generate_text`) : significative
- **C** (pas de clause de refus / loi de cardinalité contextuelle) : **cause structurelle**
Le LLM, dressé à produire un texte cohérent, **doit** émettre quelque chose. Si les preuves disent "hospitalisation" et la décision dit "forfait", il rationalise vers la masse de signaux (les preuves gagnent).
### PROMPT 1 réécrit complet (à coller dans `core/llm/t2a_decision.py`)
```
Tu es médecin DIM senior, expert PMSI/T2A urgences hospitalières en France. Tu produis une décision de
facturation auditable et reproductible.
# RÈGLES PMSI URGENCES
Deux décisions possibles, mutuellement exclusives :
- FORFAIT_URGENCE → libellé court "Forfait Urgences" → passage simple, sortie domicile, pas de soins continus.
- REQUALIFICATION_HOSPITALISATION → libellé court "UHCD" → séjour MCO requis.
Trois critères UHCD (ATIH) :
C1. Pathologie potentiellement évolutive (instabilité hémodynamique, terrain à risque, traitement nécessitant
adaptation).
C2. Surveillance médicale ou paramédicale prolongée (constantes itératives, observations IDE/médecin
répétées, durée de surveillance > 6 h).
C3. Examens complémentaires ou actes thérapeutiques significatifs (biologie, imagerie, sutures,
gestes techniques).
RÈGLE DE DÉCISION (algorithme, à appliquer LITTÉRALEMENT, dans cet ordre) :
1. Évalue C1, C2, C3 indépendamment à partir des citations littérales du DPI.
2. Compte N = nombre de critères validés à true.
3. Si N >= 2 → decision = "REQUALIFICATION_HOSPITALISATION", decision_court = "UHCD".
4. Sinon → decision = "FORFAIT_URGENCE", decision_court = "Forfait Urgences".
5. decision_court DOIT être strictement couplé à decision.
# DURÉE DU PASSAGE
duree_passage_heures = (heure de sortie OU heure de transfert) (heure d'admission OU heure d'arrivée).
- Cherche EXACTEMENT les champs d'admission/sortie du dossier (libellés "Admission", "Arrivée", "Entrée",
"Sortie", "Transfert", "Sortie effective").
- N'utilise JAMAIS l'heure d'un examen, d'une biologie, ou d'un soin comme borne.
- Si une borne manque → duree_passage_heures = null (NE PAS deviner).
- Ne confonds JAMAIS la durée des symptômes (anamnèse) avec la durée du séjour.
# CITATIONS
Chaque preuve_critereN doit contenir AU MOINS UNE citation littérale entre « » d'un fragment du DPI.
Pas de paraphrase silencieuse. Si critère non validé, cite ce qui MANQUE (ex: « Sortie à H+2 »).
# LISTES MUTUELLEMENT EXCLUSIVES
elements_pour_hospitalisation et elements_pour_forfait sont mutuellement exclusives. Un même fait ne peut
pas apparaître dans les deux. Un fait ne peut être dans elements_pour_hospitalisation QUE s'il argumente
pour REQUALIFICATION (ex: « surveillance scope 12 h », « bilan biologique répété »). Un fait orientant vers
la sortie (« retour domicile », « sortie consultation externe à 48 h ») doit aller dans elements_pour_forfait.
# ANTI-HALLUCINATION
Si une information manque, NE LA FABRIQUE PAS. Préfère null, [] ou "NON_RENSEIGNE". Toute valeur numérique
non présente dans le dossier est interdite.
# EXEMPLE 1 — FORFAIT (passage court, retour domicile)
DPI (extrait) : « Admission 14h12, motif: entorse cheville droite. Examen: œdème modéré, pas de douleur
osseuse. Rx: pas de fracture. Bandage. Sortie 16h05 retour domicile. »
Sortie attendue :
{"duree_passage_heures": 1.9, "elements_pour_hospitalisation": [],
"elements_pour_forfait": ["Sortie 16h05 retour domicile", "Rx: pas de fracture", "Bandage"],
"preuve_critere1": "Motif « entorse cheville droite », pas de terrain à risque cité. Pathologie bénigne
non évolutive. Critère non validé.", "critere1_valide": false,
"preuve_critere2": "Surveillance courte : « Admission 14h12 » → « Sortie 16h05 », durée 1.9 h. Pas
d'observation IDE itérative. Critère non validé.", "critere2_valide": false,
"preuve_critere3": "Un seul acte : « Rx: pas de fracture » et « Bandage ». Acte technique mineur isolé.
Critère non validé.", "critere3_valide": false,
"decision": "FORFAIT_URGENCE", "decision_court": "Forfait Urgences",
"justification": "Passage court 1.9 h avec « Sortie 16h05 retour domicile ». Aucun des 3 critères UHCD
validé. Forfait urgences applicable.", "confiance": "elevee"}
# EXEMPLE 2 — REQUALIFICATION (3 critères validés)
DPI (extrait) : « Admission 22h40, patient 78 ans, ATCD insuffisance cardiaque. Dyspnée stade IV. FC 124,
TA 95/58. ECG: AC/FA rapide. NT-proBNP 4800. Scope, O2 4L. Surveillance horaire. Transfert USIC 09h15. »
Sortie attendue :
{"duree_passage_heures": 10.6,
"elements_pour_hospitalisation": ["ATCD insuffisance cardiaque", "Dyspnée stade IV", "FC 124, TA 95/58",
"AC/FA rapide", "Scope, O2 4L", "Surveillance horaire", "Transfert USIC 09h15"],
"elements_pour_forfait": [],
"preuve_critere1": "Terrain à risque : « patient 78 ans, ATCD insuffisance cardiaque ». Instabilité :
« FC 124, TA 95/58 » et « AC/FA rapide ». Critère validé.", "critere1_valide": true,
"preuve_critere2": "Durée 10.6 h (« Admission 22h40 » → « Transfert USIC 09h15 ») avec « Scope, O2 4L »
et « Surveillance horaire ». Critère validé.", "critere2_valide": true,
"preuve_critere3": "Bilan : « ECG: AC/FA rapide », « NT-proBNP 4800 ». Examens significatifs orientant le
diagnostic. Critère validé.", "critere3_valide": true,
"decision": "REQUALIFICATION_HOSPITALISATION", "decision_court": "UHCD",
"justification": "3 critères UHCD validés. Terrain « 78 ans, ATCD insuffisance cardiaque » avec
décompensation aiguë, surveillance prolongée 10.6 h et transfert USIC. Séjour MCO justifié.",
"confiance": "elevee"}
# FORMAT DE SORTIE (JSON STRICT, ordre des clés OBLIGATOIRE — preuves AVANT décision)
{
"duree_passage_heures": <number|null>,
"preuve_critere1": "<2-3 phrases avec citation « ... »>",
"critere1_valide": <true|false>,
"preuve_critere2": "<2-3 phrases avec citation « ... »>",
"critere2_valide": <true|false>,
"preuve_critere3": "<2-3 phrases avec citation « ... »>",
"critere3_valide": <true|false>,
"elements_pour_hospitalisation": [<phrases littérales>],
"elements_pour_forfait": [<phrases littérales>],
"decision": "FORFAIT_URGENCE" | "REQUALIFICATION_HOSPITALISATION",
"decision_court": "UHCD" | "Forfait Urgences",
"justification": "<2-3 phrases avec au moins une citation>",
"confiance": "elevee" | "moyenne" | "faible"
}
# DOSSIER PATIENT
{dpi}
```
⚠️ **À ré-arbitrer avec rapport 4** : la règle "N >= 2 ⇒ UHCD" de ce PROMPT 1 est cliniquement fausse selon les sources officielles. À remplacer par la règle cumulative ET (texte Section F du rapport 4).
### PROMPT 2 réécrit (DB workflow step 14)
```
Tu es médecin urgentiste senior. Tu produis un résumé clinique fidèle, propre et auditable du dossier
ci-dessous.
Tu ne prends AUCUNE décision de facturation. Tu ne produis AUCUN JSON. Tu ne fais AUCUNE déduction
diagnostique non explicitement présente dans le dossier.
RÈGLES STRICTES :
1. Source unique : le dossier ci-dessous. Tout fait non présent est interdit.
2. Bruit OCR : ignore menus, libellés d'interface, fragments isolés, doublons, lignes incohérentes.
3. Déduplication : si la même information apparaît plusieurs fois sous des formes proches, garde la plus
précise et lisible.
4. Conflit OCR : si deux passages donnent des valeurs incompatibles pour le même fait (ex: FC 110 vs FC 010),
écris la valeur la plus plausible suivie de "(à confirmer)".
5. Horaires : reproduis LITTÉRALEMENT les heures d'admission, sortie ou transfert (format HHhMM).
Ne paraphrase JAMAIS un horaire.
6. Constantes : reproduis les valeurs numériques telles quelles avec leur unité (FC en bpm, TA en mmHg,
SpO2 en %, T° en °C).
7. Style : français médical clair, fluide, sans puces, sans titres, sans JSON.
8. Longueur : 8 à 14 phrases.
9. Ordre clinique : motif d'admission → contexte/ATCD → examen et constantes → examens complémentaires →
traitements/gestes → évolution et orientation.
10. Si une section est absente, ne la mentionne pas.
11. Commence par le motif d'admission. Termine par l'orientation (sortie domicile, hospitalisation, transfert).
DOSSIER :
{context}
```
### PROMPT 3 réécrit (DB workflow step 17)
```
Tu es médecin DIM. Tu rédiges UNE justification PMSI courte qui défend une décision déjà prise. Tu n'as
PAS le droit de la changer.
<DECISION_IMPOSEE>
decision = {{dec.decision}}
decision_court = {{dec.decision_court}}
</DECISION_IMPOSEE>
<RESUME_CLINIQUE>
{{resume_patient}}
</RESUME_CLINIQUE>
<PREUVES>
C1 : {{dec.preuve_critere1}}
C2 : {{dec.preuve_critere2}}
C3 : {{dec.preuve_critere3}}
</PREUVES>
RÈGLES STRICTES :
1. Ta sortie DOIT défendre la décision = {{dec.decision}} et se terminer par une phrase finale citant
explicitement « {{dec.decision_court}} ».
2. Source unique : RESUME_CLINIQUE et PREUVES. N'invente AUCUN argument.
3. Pas de JSON, pas de liste à puces, pas de titre. Texte fluide, 4 à 7 phrases.
4. Structure :
- Phrase 1-2 : éléments en faveur de {{dec.decision}} (citations courtes).
- Phrase 3-4 : éléments contradictoires ou limitants, traités factuellement.
- Phrase finale : conclusion citant exactement « {{dec.decision_court}} ».
5. Si FORFAIT_URGENCE : explique pourquoi UHCD/hospitalisation n'est pas requise.
6. Si REQUALIFICATION_HOSPITALISATION : explique pourquoi le passage dépasse un simple forfait.
VERROU DE COHÉRENCE — OBLIGATOIRE :
Avant d'émettre, vérifie que ta dernière phrase contient littéralement « {{dec.decision_court}} » et défend
la décision imposée. Si tu détectes que les preuves contredisent gravement la décision imposée au point
que tu ne peux PAS rédiger une justification honnête, tu DOIS répondre EXACTEMENT et SEULEMENT cette ligne :
INCOHERENCE_DETECTEE: la décision {{dec.decision}} ne correspond pas aux preuves fournies, validation
humaine requise.
Sinon, rédige la justification. Aucun autre format de sortie n'est accepté.
```
### Paramètres Ollama recommandés
PROMPT 1 (déterminisme maximal) :
```
temperature: 0
top_p: 1.0
top_k: 0
repeat_penalty: 1.0
seed: 42
num_predict: 2000
num_ctx: 16384
keep_alive: "30m"
```
PROMPT 2 (un peu de fluidité) :
```
temperature: 0.2
top_p: 0.9
top_k: 40
seed: 42
num_predict: 800
```
PROMPT 3 (texte démonstratif court) :
```
temperature: 0.1
top_p: 0.9
top_k: 40
seed: 42
num_predict: 500
```
### Garde-fous serveur (à appliquer dans `_handle_t2a_decision_action`, replay_engine.py:1204)
```python
# Après result = analyze_dpi(...) :
# 1. Validation schéma minimal
required = {"decision", "decision_court", "critere1_valide", "critere2_valide",
"critere3_valide", "duree_passage_heures"}
if not required.issubset(result.keys()):
result["_schema_error"] = sorted(required - result.keys())
# 2. Coercion decision_court ↔ decision (single source of truth = decision)
MAP = {"FORFAIT_URGENCE": "Forfait Urgences",
"REQUALIFICATION_HOSPITALISATION": "UHCD"}
if result.get("decision") in MAP:
expected = MAP[result["decision"]]
if result.get("decision_court") != expected:
result["_coerced_decision_court"] = result.get("decision_court")
result["decision_court"] = expected
# 3. Règle 2/3 — recompute decision depuis les flags (À ADAPTER selon règle officielle ATIH)
n_valid = sum(bool(result.get(f"critere{i}_valide")) for i in (1, 2, 3))
expected_decision = ("REQUALIFICATION_HOSPITALISATION" if n_valid >= 3
else "FORFAIT_URGENCE") # ⚠️ Changé en 3/3 selon rapport 4
if result.get("decision") != expected_decision:
result["_rule_violation"] = {
"n_critere_valides": n_valid,
"decision_llm": result.get("decision"),
"decision_attendue": expected_decision,
}
result["decision"] = expected_decision
result["decision_court"] = MAP[expected_decision]
# 4. Détection contradiction justification ↔ decision (regex)
import re
justif = (result.get("justification") or "").lower()
if result["decision"] == "FORFAIT_URGENCE" and re.search(
r"\b(requalification|uhcd|hospitalis)", justif):
result["_justif_contradiction"] = True
if result["decision"] == "REQUALIFICATION_HOSPITALISATION" and re.search(
r"\bforfait\b", justif) and not re.search(r"dépasse|au[- ]delà", justif):
result["_justif_contradiction"] = True
# 5. Sanity check durée
duree = result.get("duree_passage_heures")
if duree is not None and (duree < 0 or duree > 72):
result["_duree_suspect"] = duree
# 6. Retry automatique 1 fois si contradiction grave
contradictions = any(k.startswith("_") and k not in ("_elapsed_s", "_model", "_eval_count")
for k in result)
if contradictions and not result.get("_retry"):
result_retry = analyze_dpi(dpi_text, model=..., seed=43)
result_retry["_retry"] = True
if not any(k.startswith("_rule_violation") for k in result_retry):
result = result_retry
```
### Coherence check optionnel pour step 17 (dans `_handle_llm_generate_action`)
```python
if action.parameters.get("coherence_check") == True:
imposed = (params.get("imposed_decision_court") or "").strip() # ex: "UHCD"
if imposed and imposed.lower() not in generated.lower():
generated_retry = handler.generate_text(prompt=prompt, context=context, ...)
if imposed.lower() in generated_retry.lower():
generated = generated_retry
else:
generated = (f"Décision retenue : {imposed}. "
f"Voir critères PMSI dans le détail technique. "
f"Justification automatique non concluante — révision humaine recommandée.")
```
### 3 actions prioritaires (effort/impact)
1. **(15 min)** Fixer `temperature=0, seed=42, top_p=1` dans `t2a_decision.py` → tue l'oscillation.
2. **(30 min)** Geler `dpi_canonique` en variable de session après première extraction.
3. **(1 h)** Remplacer PROMPT 1 + ajouter garde-fous serveur → tue les contradictions structurelles.
---
## Rapport 3 — Analyste données (chronologie + dates)
### Cohérence des 11 dossiers
- **Âges ↔ dates de naissance ↔ date passage** : OK pour tous les 11. Aucun off-by-one anniversaire.
- **Horaires arrivée→sortie** : toutes durées positives, cohérentes avec recap_rpu.
### Incohérences trouvées dans data.js
| # | Dossier | Ligne | Problème | Risque |
|---|---------|-------|----------|--------|
| a | MOREL 25003284 | 228 | « Terrain à risque : 78 ans » alors qu'âge = 77 ans | LLM peut halluciner âge |
| b | MOREL | 43, 178, 201 | « depuis 23h » répété 3× | Piège durée (hallucination 23h au lieu de 3h37) |
| c | PERRIN 25003475 | 1519-1524, 1549 | GEMSA 4 (hospitalisé) vs decision "RAD" vs us_destination "UC CONSULT.URGENCES" vs recap codage "hospitalisée UHCD" | Codage admin incohérent |
| d | DA SILVA 25151530 | 1064 vs 1184 | arrivee `03:25` vs episode_heure `03:00` (Δ 25 min) | 2 horaires admission différents pour LLM |
| e | PIRES 25056615 | 923, 940, 988, 1013, 1031 | Statut "Transfert" + « depuis 1 semaine » répété | Risque conversion 168h en durée_passage |
| f | LEROY 25003364 | 1253, 1358, 1360 | « depuis 15 jours » répété + episode 13:55 vs orientation 14:47 | Risque 360h en durée_passage |
| g | BRUNEL 25012257 | 1784, 1788, 1841, 1896 | « >48h », « >72h », « >24h », « depuis 10/01 », « depuis 30/09/2024 » | Densité maximale de marqueurs temporels |
| h | NGUYEN 25048485 | 788 | Note IDE « vu la nuit » sans heure | Information temporelle floue |
| i | MARTINS 25005866 | 1648-1651 | Note bilingue anglais + « IRM il y a 2 semaines » + « amnésie de plusieurs jours » | Confusion durées possible |
| j | ORDRE_DOSSIERS | 2025 | Commentaire « 25003284 — UHCD asthme » désynchronisé avec VT actuelle FORFAIT (server.py:34) | Confusion si lecture à l'écran |
### Solution recommandée — préprocesseur Python `build_dpi_enriched`
Approche **HYBRIDE** : Python calcule les valeurs critiques (durée, âge), les injecte en tête du DPI dans un bloc `FAITS_CALCULÉS`, le LLM garde la liberté d'argumenter sur les preuves cliniques.
```python
import re
from datetime import datetime
DATE_RE = re.compile(r"(\d{2}/\d{2}/\d{4})\s+(\d{2}:\d{2})")
ARR_RE = re.compile(r"Arriv[ée]e\s*[:\-]?\s*(\d{2}/\d{2}/\d{4})\s+(\d{2}:\d{2})", re.I)
SOR_RE = re.compile(r"Sortie\s*[:\-]?\s*(\d{2}/\d{2}/\d{4})\s+(\d{2}:\d{2})", re.I)
PEC_RE = re.compile(r"prise en charge.*?(\d{2}/\d{2}/\d{4})\s+(\d{2}:\d{2})", re.I|re.S)
DEC_RE = re.compile(r"d[ée]cision.*?(\d{2}/\d{2}/\d{4})\s+(\d{2}:\d{2})", re.I|re.S)
NE_RE = re.compile(r"N[ée]\(?e?\)?\s+le\s+(\d{2}/\d{2}/\d{4})", re.I)
def parse_dt(d, h):
return datetime.strptime(f"{d} {h}", "%d/%m/%Y %H:%M")
def calc_facts(dpi: str) -> dict:
arr = ARR_RE.search(dpi) or PEC_RE.search(dpi)
sor = SOR_RE.search(dpi) or DEC_RE.search(dpi)
ne = NE_RE.search(dpi)
out = {}
if arr and sor:
a = parse_dt(*arr.groups()); s = parse_dt(*sor.groups())
if s < a:
return {"_warn_negative": True}
dur_h = round((s - a).total_seconds()/3600, 2)
out["arrivee"] = a.strftime("%d/%m/%Y %H:%M")
out["sortie"] = s.strftime("%d/%m/%Y %H:%M")
out["duree_passage_heures"] = dur_h
if ne and arr:
n = datetime.strptime(ne.group(1), "%d/%m/%Y")
a = parse_dt(*arr.groups())
age = a.year - n.year - ((a.month, a.day) < (n.month, n.day))
out["age_au_passage_ans"] = age
return out
def build_dpi_enriched(dpi_raw: str) -> str:
facts = calc_facts(dpi_raw)
if not facts:
return dpi_raw # fallback : LLM calcule lui-même
bloc = "===== FAITS CALCULÉS (déterministes — à utiliser tels quels) =====\n"
if "duree_passage_heures" in facts:
bloc += f"DUREE_PASSAGE_HEURES = {facts['duree_passage_heures']}\n"
bloc += f"ADMISSION = {facts['arrivee']}\n"
bloc += f"SORTIE = {facts['sortie']}\n"
if "age_au_passage_ans" in facts:
bloc += f"AGE_AU_PASSAGE = {facts['age_au_passage_ans']} ans\n"
bloc += "Note : ces valeurs prévalent sur toute durée de symptômes mentionnée dans le texte clinique.\n"
bloc += "=====================================================\n\n"
return bloc + dpi_raw
```
Et ajouter à `PROMPT_TEMPLATE` :
> Si un bloc `FAITS_CALCULÉS` est présent en tête du dossier, utilise EXACTEMENT `DUREE_PASSAGE_HEURES`
> pour le champ `duree_passage_heures`. NE PAS confondre avec les durées de symptômes (« depuis 23h »,
> « depuis 15 jours »).
### Bloc canonique à injecter (exemple MOREL)
```
===== FAITS CALCULÉS =====
ADMISSION = 01/01/2025 03:12
SORTIE = 01/01/2025 06:49
DUREE_PASSAGE_HEURES = 3.62
AGE_AU_PASSAGE = 77 ans
SEXE = F
CCMU = 3
GEMSA = 2
DIAGNOSTIC_PRINCIPAL = J12.1 Pneumopathie VRS [CMA2]
DECISION_ADMINISTRATIVE = Consultation externe (UC CONSULT.URGENCES)
==========================
```
### Ranking risque de bug LLM
**Top 3 risqués** :
1. **25003284 MOREL** — « depuis 23h » × 3, bandeau Arrivée/Sortie hors zone OCR
2. **25003364 LEROY** — « depuis 15 jours » × 3, terrain lourd SLA+BPCO, durée passage 7h35 limite
3. **25012257 BRUNEL** — multiples « >48h », « >72h », « >24h », allergie « il y a 30 ans »
**Top 3 safe** :
1. **25003451 ROUX** — 2h00, plaie nette, récit linéaire
2. **25010621 FAURE** — 2h49, dates absolues
3. **25048485 NGUYEN** — 6h50, crise convulsive datée
### Corrections data.js recommandées
- Ligne 228 : "78 ans" → "77 ans" (MOREL)
- Ligne 1519 : PERRIN GEMSA 4 → 2, OU us_destination → UHCD (cohérence VT)
- Ligne 1184 : DA SILVA episode_heure `03:00``03:25`
- Ligne 2025 : commentaire ORDRE_DOSSIERS aligné avec VT actuelle
---
## Rapport 4 — Référentiel officiel PMSI 2026 (sources web)
### Verdict liminaire — règle officielle
**Le DIM senior avait raison sur la conjonction ET. La règle 2/3 est officiellement fausse.**
Source primaire (citation textuelle Instruction DGOS/R1/DSS/1A/2020/52 Annexe 3) :
> « Ces trois conditions présentent un caractère cumulatif et s'apprécient avant l'admission du patient
> en UHCD. Dès que l'une d'entre elles n'est pas remplie, la prise en charge ne donne pas lieu à
> facturation d'un GHS mais à celle d'un forfait "accueil et traitement des urgences" (ATU). »
Guide ATIH MCO 2025 et 2026 (chap. I §1.2, identique entre les deux versions) :
> « Lorsque l'une de ces conditions n'est pas remplie, il ne doit pas être produit de RUM. »
### Arbre de décision officiel
```
PASSAGE URGENCES
|
[Suivi d'une hospitalisation MCO de la même entité géographique ?]
/ \
OUI NON
| |
GHS du séjour MCO [Les 3 critères UHCD sont-ils CUMULATIVEMENT remplis,
(avec quote-part appréciés AVANT l'admission en UHCD ?]
UHCD si passage / \
UHCD initial) OUI (3/3) NON (0, 1 ou 2 sur 3)
| |
(1) Pathologie potentiellement FORFAIT URGENCES :
évolutive susceptible - FU (forfait âge) à l'AM
d'aggravation OU diagnostic + FPU (côté patient, 23€)
incertain + suppléments éventuels
(2) ET surveillance médicale (SU2/SU3, SUB/SB2/SB3,
+ environnement SIM/SIC, SUN/SUF, SUM,
paramédical délivrables SAS, PE1/PE2)
uniquement en hospitalisation + actes/consultations
(3) ET examens complémentaires en sus
ou actes thérapeutiques
|
GHS mono-RUM UHCD
(date entrée = date sortie ;
niveau de sévérité le plus
bas de la racine GHM ;
tarif unique quelle que soit
la durée)
```
### Précisions critiques
- **Pas de seuil 6h opposable** : aucune source primaire (ATIH MCO 2025/2026, DGOS 2020, arrêté 19/02/2015) n'impose une durée plancher. Le seuil 6h relève du folklore DIM ou Clinical Decision Units anglo-saxonnes.
- **Plafond pratique** : DMS < 24h recommandée (SFMU 2024), tolérance ≤ 36h ou 2 nuits consécutives.
- **Critère 3 ne renvoie pas exclusivement à des actes CCAM** : pas de critère répétitif (Annexe 3 DGOS 2020).
- **Mode de sortie domicile + 3 critères remplis = GHS UHCD légal**. Ce sont les 3 conditions cliniques d'admission qui priment, pas le mode de sortie. MAIS : mode_sortie codé "Consultation externe" (mode ≠ 8) = drapeau rouge contrôle CPAM.
### Verdict cas MOREL Catherine (selon règle officielle)
| Critère | État | Validé |
|---|---|---|
| (1) Pathologie évolutive ou diagnostic incertain | Pneumopathie VRS fébrile (39.4°C) chez 77 ans, asthme + insuf coronarienne, peakflow effondré | **OUI** |
| (2) Surveillance + environnement paramédical hospitalier | Aérosols ×3, monitoring tachycardie 117→91, antibio initiée, surveillance peakflow | **OUI** |
| (3) Examens complémentaires ou actes thérapeutiques | RT, NFS, CRP, PCR VRS, aérosols répétés, ATB | **OUI** |
**3/3 cumulatifs ⇒ GHS UHCD légalement justifiable (racine 04M Pneumopathies)**, indépendamment de la durée 3h37.
### Points de vigilance MOREL pour défense contrôle CPAM
1. Durée 3h37 anormalement courte — signal d'alerte habituel ; dossier doit documenter explicitement le caractère évolutif AVANT admission UHCD.
2. Mode de sortie "Consultation externe" = drapeau rouge. Code mode 8 (domicile) attendu pour UHCD.
3. CCMU 3 + GEMSA 2 = cohérent UHCD.
4. DP J12.1 (CMA2) = pertinent, supporte GHS.
### Conclusion MOREL
**Le passage relève d'une UHCD facturable en GHS** (et non d'un forfait pur), à condition que le dossier documente les 3 critères AVANT admission UHCD et que le mode de sortie soit harmonisé. La VT actuelle `server.py:32` (FORFAIT) est donc à réinterroger.
### Nomenclature officielle (post-réforme 2024-2026)
**Forfaits âge (un seul par passage)** :
| Code | Description | Conditions |
|------|-------------|-----------|
| FU0 | Nouveau-né | < 4 mois (créé arrêté 29/02/2024) |
| FU1 | Nourrisson/enfant | 4 mois à < 16 ans |
| FU2 | Adulte jeune | 16 à < 45 ans |
| FU3 | Adulte mûr | 45 à < 75 ans |
| FU4 | Personne âgée | ≥ 75 ans |
**FPU (côté patient)** :
- 23€ depuis 1er mars 2026 (vs 19,61€ avant)
- 9,96€ minoré (ALD, AT-MP)
**Suppléments cliniques** :
| Code | Description | Conditions |
|------|-------------|-----------|
| SU2 | CCMU 2+ | État CCMU 2 + acte CCAM de la liste annexe 18 |
| SU3 | CCMU 3,4,5 | État CCMU 3/4/5 sans hospitalisation |
| PE1 | Pédiatrique | Patient FU0/FU1 + diagnostic CIM-10 liste 1 annexe 19 (~800 codes) |
| PE2 | Pédiatrique + | Patient FU0/FU1 + diagnostic CIM-10 liste 2 annexe 19 (sous-liste lourde) |
**Autres suppléments** :
- SUB/SB2/SB3 (biologie), SIM/SIC (imagerie), SUN (nuit 22-8h), SUF (soirée/férié/dimanche)
- SUM (transport médicalisé), SAS (avis spécialisé), SSN/SSF (avis spé nuit/férié)
**Règles de cumul** :
- Aucun supplément sans FU
- Un seul FU par passage
- SU2 / SU3 non cumulables entre eux
- Un seul supplément biologie, un seul supplément imagerie
- SUN / SUF non cumulables
- PE1/PE2 cumulabilité : zone grise à clarifier avec DIM cible
### "FF1/FF2/FF3" — N'EXISTENT PAS
Codes absents de la nomenclature officielle 2024-2026. Probablement dénomination interne Easily ou GHT Sud 95. **À clarifier impérativement avant démo.**
### Texte 200 mots prêt à coller dans PROMPT 1 (Section F du rapport)
> Tu évalues si un passage aux urgences relève d'un GHS UHCD ou d'un Forfait Urgences (FU+FPU). Applique
> cette règle officielle ATIH/DGOS 2026 (Instruction DGOS/R1/DSS/1A/2020/52 Annexe 3 + Guide ATIH MCO
> 2025/2026 §1.2 + arrêté 19/02/2015 art.12) :
>
> **GHS UHCD facturable UNIQUEMENT si TOUTES les conditions suivantes sont cumulativement remplies,
> appréciées AVANT admission en UHCD :**
> 1. État pathologique potentiellement évolutif et susceptible d'aggravation, OU diagnostic incertain ;
> 2. ET surveillance médicale et environnement paramédical délivrables uniquement en hospitalisation ;
> 3. ET réalisation d'examens complémentaires ou d'actes thérapeutiques.
>
> **Si une seule condition manque** → facturation forfait urgences (FU0-FU4 selon âge + FPU patient
> + suppléments éventuels : SU2/SU3, PE1/PE2, SUB/SB2/SB3, SIM/SIC, SUN/SUF, SUM, SAS), pas de GHS.
>
> **NE PAS appliquer la règle "2 sur 3"** : elle est non conforme et expose à requalification CPAM.
>
> Exception : l'administration d'un produit de la réserve hospitalière déclenche un GHS UHCD même hors
> critères ci-dessus.
>
> Aucune durée minimum n'est réglementairement opposable, mais une DMS < 24h est attendue ; au-delà
> de 36h ou 2 nuits, escalader vers un humain. Le passage en UHCD suivi d'une mutation MCO de même
> entité géographique ne donne lieu qu'à UN SEUL GHS du séjour complet.
### Sources primaires (12 PDF archivés dans `docs/clients/ght_sud_95/referentiels_officiels/`)
| Fichier | Source | URL d'origine |
|---------|--------|---------------|
| `solimed_instruction.pdf` | Instruction DGOS/R1/DSS/1A/2020/52 du 10/09/2020 (intégrale) | solimed.fr |
| `apm_instruction.pdf` | Idem (copie alternative) | apm |
| `instruction_gradation_2020.pdf` | Idem | gouv |
| `instruction_gradation_bo.pdf` | Idem (BO) | gouv |
| `atih_mco_2025.pdf` | Guide méthodologique ATIH MCO 2025 | atih.sante.fr |
| `atih_mco_2026.pdf` | Guide méthodologique ATIH MCO 2026 | atih.sante.fr |
| `sfmu_uhcd_2024.pdf` | Guide bonnes pratiques UHCD 2024 — SFMU | sfmu.org |
| `sfmu_ref_uhcd.pdf` | Référentiel SFMU UHCD | sfmu.org |
| `dhumu_uhcd.pdf` | Critères admission UCSU 2024 | dhumu.fr |
| `oru_pdl_uhcd.pdf` | ORU Pays de Loire UHCD | oru-pdl.org |
| `fhpmco_2023.pdf` | FHP-MCO Financement médecine d'urgence n°17 (mai 2023) | fhpmco.fr |
URLs Légifrance non archivées en PDF (à consulter en ligne) :
- Arrêté du 19/02/2015 modifié, art. 12, Chap. 7 : `legifrance.gouv.fr/codes/section_lc/JORFTEXT000030280539/JORFSCTA000030280566/2024-03-02`
- Arrêté du 27/12/2021 financement structures urgences : `JORFTEXT000044592184`
- Arrêté du 29/02/2024 modifiant 19/02/2015 (FU0 + annexes 18/19) : `JORFTEXT000049219412`
- Annexe 19 Liste 1 PE1 (CIM-10) : `LEGIARTI000049222509/2024-03-02`
- Arrêté du 17/12/2021 montants FPU : `JORFTEXT000044592137`
### Confiance globale
- **ÉLEVÉ** sur règle cumulative ET, nomenclature FU0-FU4, verdict MOREL, absence de seuil 6h
- **MOYEN** sur listes exhaustives annexes 18/19, cumulabilité PE1+PE2
- **FAIBLE** sur "FF1/FF2/FF3" (probablement Easily) et sémantique PE2 (était incorrecte dans nos hypothèses initiales)
---
## Plan d'action P0 consolidé (à valider avec Dom + Amina)
### Préalable bloquant
**Trancher VT MOREL avec Amina/DIM Carvella** : selon règle officielle ATIH, MOREL = UHCD. Mais `server.py:32` dit FORFAIT (corrigé 2026-05-05). Soit :
- (a) Erreur dans server.py et VT à inverser ⇒ MOREL devient cas de démo "UHCD non détectée" avec ROI ~375€
- (b) VT volontaire pour illustrer un bug de codage administratif ⇒ système détecte l'incohérence "profil clinique UHCD + mode_sortie consultation externe" comme perte de recettes
Option (b) est plus puissante en démo. À confirmer.
### Actions P0 (~3h cumulé, validation 1 par 1)
| # | Action | Fichier | Effort | Risque |
|---|--------|---------|--------|--------|
| C | Params Ollama déterministes (`temp=0, seed=42, top_p=1, top_k=0`) | `core/llm/t2a_decision.py:109-113` | 5 min | nul |
| A | Préprocesseur Python `build_dpi_enriched` (calcul durée + âge depuis OCR) | `core/llm/t2a_decision.py` (nouvelle fonction) | 45 min | faible |
| B | PROMPT 1 réécrit avec **règle officielle ATIH** (Section F rapport 4) + 2 few-shot + listes mutuellement exclusives + ordre clés preuves AVANT décision | `core/llm/t2a_decision.py:31-72` | 45 min | moyen |
| D | Garde-fous serveur dans `_handle_t2a_decision_action` (coercion + recompute 3/3 + regex contradiction + retry) | `agent_v0/server_v1/replay_engine.py:1204+` | 45 min | faible |
| E | PROMPT 3 réécrit (balises XML + clause refus syntaxique exacte + structure conclusion verrouillée) | DB workflow step 17 (UPDATE SQL) | 30 min | faible |
### Actions P1 (~1h, qualité)
| # | Action | Fichier |
|---|--------|---------|
| F | PROMPT 2 ajustements (3 lignes : durée explicite, mode sortie, actes CCAM, gestion conflit OCR) | DB workflow step 14 |
| G | Corrections data.js (78→77 ans MOREL ; PERRIN GEMSA ; DA SILVA episode_heure ; commentaire ligne 2025) | `data.js` |
| H | Logs structurés JSON brut t2a_decision pour audit Q&A post-démo | `replay_engine.py` |
### Actions P2 (hors scope démo)
- Cas PE2/SU2 : exclure de la démo OU branche déterministe CCAM
- Geler `dpi_canonique` en variable de session
---
## Questions ouvertes pour reprise
1. **VT MOREL** : Amina valide quoi (FORFAIT inversée ou maintenue avec narratif "détection bug codage") ?
2. **Cas PE2/SU2 démo** : on les inclut ou on les écarte ?
3. **"FF1/FF2/FF3"** : c'est interne Easily ou erreur de saisie ?
4. **Cumulabilité PE1+PE2** : à clarifier avec DIM Carvella ?
5. **Ordre d'attaque** : on confirme C → A → B → D → E ?

View File

@@ -0,0 +1,852 @@
# [S1] Chantier build_dpi_enriched + garde-fous comparaison Python ↔ LLM
Brief V2 finalisé après revue [S2] — 12 mai 2026.
## CONTEXTE
Bug identifié sur cas MOREL (audit `docs/handoffs/2026-05-12_audit_complet_decision_t2a.md`) :
le LLM hallucine "23h" pour la durée du passage alors que la durée réelle est 3h37
(arrivée 01/01/2025 03:12 → sortie 01/01/2025 06:49). Hypothèse confirmée par capture
UI Easily Assure : confusion avec `depuis 23h` présent dans `Observ. IDE Urg` du DPI.
Source de données validée : "Extract texte scroll auto" capture la totalité de
l'écran y compris le bandeau ET la section "Synthèse Urgences" qui contient les
horaires structurés ligne par ligne (Date d'épisode, Date de prise en charge
médicale, Date de décision médicale, etc.) + CCMU + GEMSA + diagnostic principal
+ décision médicale terrain + mode de venue.
Solution validée avec Dom : sortir le calcul de durée + les classifications
cliniques structurées du LLM, les extraire en Python déterministe, injecter
le résultat en tête du DPI dans un bloc FAITS_CALCULÉS. Le LLM ne calcule plus,
il lit. La décision médicale terrain est extraite mais **NON injectée dans le
prompt** (sinon le LLM s'aligne sur le terrain au lieu de raisonner) : elle sert
uniquement au garde-fou serveur de comparaison a posteriori.
Branche : décision (séparée de l'optim vitesse en cours sur autre branche).
Pas de modif du PROMPT 3 dans ce chantier. La réécriture du prompt viendra
APRÈS le dry-run de validation.
## OBJECTIF
Livrer 2 commits indépendants :
- **Commit 1** : fonction `build_dpi_enriched` + test golden MOREL
- **Commit 2** : garde-fous comparaison durée + décision dans `_handle_t2a_decision_action`
**PAS DE COMMIT COMPOSITE.** Chaque commit doit être revertable indépendamment.
---
## ÉTAPE 0 — Vérification source de données (5 min, AVANT commit 1)
Lis le code qui construit le `dpi_raw` envoyé au LLM (concaténation des 5 variables
`t_motif_admission`, `t_examen_clinique`, `t_imagerie`, `t_notes_medicales`, `t_synthese_urgences`).
Ajoute un `logger.debug` temporaire qui dump le `dpi_raw` effectivement reçu par le
prompt T2A. Relance Demo_urgence_2 (ou un cas test équivalent) sur MOREL.
Vérifie dans le log :
- La section "Synthèse Urgences" est présente avec ses lignes structurées
(Date d'épisode, Date de décision médicale, CCMU, GEMSA, Diagnostics, Décision médicale)
- Le bandeau Easily Assure est présent (Arrivée, Sortie, Né(e) le)
Si OUI aux deux : feu vert chantier, retire le `logger.debug`, passe au commit 1.
Si NON (l'une OU l'autre des sources manque) : STOP, ping Dom avant de continuer.
Le chantier sera requalifié (élargir la capture OCR ou revoir la construction de `dpi_raw`).
---
## COMMIT 1 — build_dpi_enriched
### EMPLACEMENT
`core/llm/t2a_decision.py` (cf. audit ligne 704, nouvelle fonction)
### SIGNATURE
```python
def build_dpi_enriched(dpi_raw: str) -> tuple[str, dict]:
"""
Enrichit le DPI brut avec un bloc FAITS_CALCULÉS en tête.
Returns:
- dpi_enriched : str — DPI avec FAITS_CALCULÉS en tête + dpi_raw inchangé
- metadata : dict — valeurs calculées en Python, utilisées par le
serveur pour les garde-fous (PAS injectées dans le LLM
pour decision_medicale_terrain et orientation_terrain)
"""
```
Le tuple est volontairement utilisé pour permettre le garde-fou serveur du
commit 2 sans avoir à reparser. Si la convention du repo préfère deux fonctions
séparées (`extract_metadata` + `build_enriched_string`), justifie en commentaire et
adopte cette convention.
### LOGIQUE D'EXTRACTION
**PRIORITÉ DE PARSING** : section "Synthèse Urgences" d'abord (lignes structurées),
bandeau Easily Assure en fallback (regex sur `dpi_raw` global).
Synthèse Urgences est plus fiable que le bandeau parce que les champs sont
ligne par ligne, libellés explicites, format constant.
**CHAMPS À EXTRAIRE** :
Depuis Synthèse Urgences (priorité) :
- `date_admission` : ligne "Episode - Date" → "01/01/2025 à 03:12"
OU ligne "Date de la prise en charge médicale"
- `date_sortie` : ligne "Date de décision médicale" → "01/01/2025 à 06:49"
- `ccmu` : ligne "CCMU" → libellé COMPLET tel qu'affiché dans la
Synthèse Urgences (ex: `"3. Etat lésionnel et/ou pronostic
fonctionnel jugés susceptibles de s'aggraver aux urgences
ou durant l'intervention du SMUR, sans mettre en jeu le
pronostic vital"`). **Attention** : le libellé peut être
multi-ligne dans le texte OCR — le parser doit reconstituer
toute la valeur jusqu'à la prochaine ligne structurée
(libellé suivant ou ligne vide).
- `gemsa` : ligne "GEMSA" → libellé COMPLET (ex: `"2. Patient non
convoqué sortant après consultation ou soins"`)
- `priorite_iao` : ligne "Priorité" → "Priorité 3" (libellé complet tel quel)
- `mode_venue` : ligne "Mode de transport à l'arrivée" → "Véhicule personnel"
- `mode_medicalisation` : ligne "Médicalisation du transport" → "Aucune médicalisation"
- `mode_entree` : ligne "Mode d'entrée" → "Autres admissions urgentes"
- `diagnostic_principal`: ligne "Diagnostics" → "J12.1 Pneumopathie..."
- `decision_terrain` : ligne "Décision médicale" → "Consultation externe"
- `orientation_terrain` : ligne "US de destination" → "UC CONSULT.URGENCES"
Depuis bandeau (fallback uniquement si Synthèse Urgences absente) :
- `Né\(e\) le (\d{2}/\d{2}/\d{4})` → `date_naissance`
- `Arrivée\s*:\s*(\d{2}/\d{2}/\d{4}\s+\d{2}:\d{2})` → `date_admission`
- `Sortie\s*:\s*(\d{2}/\d{2}/\d{4}\s+\d{2}:\d{2})` → `date_sortie`
**IMPORTANT** : la date de naissance n'est PAS dans la Synthèse Urgences, elle est
uniquement dans le bandeau. Toujours parser le bandeau pour la naissance.
**IMPORTANT** : ignorer le pattern `IAO : <nom> (HH:MM)` dans le bandeau. Le `(HH:MM)`
après IAO est l'horaire de triage IAO, PAS une durée. Ne pas le confondre.
**IMPORTANT — Robustesse aux occurrences multiples du bandeau** (ÉTAPE 0, [S2]) :
Le bandeau Easily Assure est dans l'en-tête de page (fixe), donc capté par
**chaque** `extract_text(top_var)` des 5 `extract_text_scroll` (un par onglet).
Conséquence : le `dpi_text` reçu par `_handle_t2a_decision_action` contient
**probablement le bandeau répété ~5 fois**.
Le parser regex de `build_dpi_enriched` doit en tenir compte :
- **Utiliser `re.search` (première occurrence)**, pas `re.findall` ni boucle.
- **Vérification de cohérence (optionnelle mais recommandée)** : si plusieurs
occurrences du bandeau sont détectées via `re.findall` à des fins de check,
et qu'elles divergent (ex: arrivées différentes), logger :
```
[build_dpi_enriched] Bandeau détecté N fois avec divergences — symptôme
d'OCR instable, prendre la 1re occurrence par défaut
```
Sinon : pas de log (silence en cas de cohérence).
Confirmation runtime de la présence effective du bandeau dans le `dpi_text` sera
obtenue automatiquement lors du dry-run E2E MOREL post-commit 2. Pas de
`logger.debug` séparé nécessaire.
### CALCULS DÉRIVÉS
- `age_ans = dateutil.relativedelta(date_admission, date_naissance).years`
(**OBLIGATOIRE** — pas de `//365` qui peut générer un écart d'1 an sur cas limites)
- `duree_timedelta = date_sortie - date_admission`
- `duree_heures_decimales = round(duree_timedelta.total_seconds() / 3600, 2)`
- `duree_format_humain = "{H} heures et {M} minutes"` (ex: `"3 heures et 37 minutes"`)
Format choisi pour lever toute ambiguïté avec une notation décimale "3.37".
### CONSTRUCTION DU BLOC FAITS_CALCULÉS
À injecter EN TÊTE du `dpi_raw` retourné (avant tout autre contenu) :
```
FAITS_CALCULÉS (déterministes, ne pas recalculer) :
- Âge du patient : {age_ans} ans
- Date admission : {date_admission_str}
- Date sortie : {date_sortie_str}
- Durée totale du passage : {duree_format_humain} (soit {duree_heures_decimales} heures décimales)
- CCMU : {ccmu}
- GEMSA : {gemsa}
- Priorité IAO : {priorite_iao}
- Mode de venue : {mode_venue}, {mode_medicalisation}
- Mode d'entrée : {mode_entree}
- Diagnostic principal : {diagnostic_principal}
[dpi_raw inchangé en dessous]
```
**Exemple rendu pour MOREL** :
```
FAITS_CALCULÉS (déterministes, ne pas recalculer) :
- Âge du patient : 77 ans
- Date admission : 01/01/2025 à 03:12
- Date sortie : 01/01/2025 à 06:49
- Durée totale du passage : 3 heures et 37 minutes (soit 3.62 heures décimales)
- CCMU : 3. Etat lésionnel et/ou pronostic fonctionnel jugés susceptibles de s'aggraver aux urgences ou durant l'intervention du SMUR, sans mettre en jeu le pronostic vital
- GEMSA : 2. Patient non convoqué sortant après consultation ou soins
- Priorité IAO : Priorité 3
- Mode de venue : Véhicule personnel, Aucune médicalisation
- Mode d'entrée : Autres admissions urgentes
- Diagnostic principal : J12.1 Pneumopathie due au virus respiratoire syncytial [VRS] [CMA2] - actif
```
⚠️ **NE JAMAIS INCLURE `decision_terrain` NI `orientation_terrain` DANS LE BLOC FAITS_CALCULÉS.**
Ces deux valeurs vont uniquement dans le dict `metadata` retourné, pour usage serveur.
### METADATA RETOURNÉE (dict)
```python
{
"age_ans": int | None,
"date_admission": datetime | None,
"date_sortie": datetime | None,
"duree_heures_decimales": float | None,
"ccmu": str | None, # libellé COMPLET (ex: "3. Etat lésionnel et/ou pronostic fonctionnel jugés susceptibles...")
"gemsa": str | None, # libellé COMPLET (ex: "2. Patient non convoqué sortant après consultation ou soins")
"priorite_iao": str | None, # libellé tel quel (ex: "Priorité 3")
"mode_venue": str | None,
"mode_medicalisation": str | None,
"mode_entree": str | None,
"diagnostic_principal": str | None,
"decision_terrain": str | None, # ← NON injecté au LLM
"orientation_terrain": str | None, # ← NON injecté au LLM
"parsing_warnings": list[str] # ← champs n'ayant pas pu être parsés
}
```
### ROBUSTESSE
- Chaque champ est optionnel. Si un parsing échoue, mettre `None` dans metadata,
ne pas mentionner le champ dans FAITS_CALCULÉS (ne pas écrire `"Âge : None"`),
et ajouter un message dans `metadata["parsing_warnings"]`.
- Cas critique : si `date_admission` OU `date_sortie` est `None`, injecter dans
FAITS_CALCULÉS la ligne suivante au lieu de la durée :
`"- Durée totale du passage : NON CALCULABLE (horaires non détectés)"`
Logger WARNING avec préfixe `[build_dpi_enriched]`.
- **NE PAS crasher.** Retourner toujours un tuple valide, même si tout le parsing
échoue (FAITS_CALCULÉS vide + metadata avec tous les champs None).
### TEST GOLDEN MOREL
Créer un fichier de test (pytest ou unittest selon convention du repo) avec :
**Input** : un DPI brut contenant :
1. Une ligne bandeau :
```
IPP : 25003284 MOREL Catherine Né(e) le 14/03/1947 | 77 ans | Sexe : F |
Arrivée : 01/01/2025 03:12 | IAO : CARON Sandrine (03:25)
Médecin : BONNET Antoine | Sortie : 01/01/2025 06:49
```
2. Une section "Synthèse Urgences" reproduisant la structure de la capture
MOREL : Episode - Date 01/01/2025 03:12, CCMU 3, GEMSA 2, Diagnostics
J12.1 Pneumopathie..., Décision médicale Consultation externe, US de
destination UC CONSULT.URGENCES, etc.
**Assertions sur `dpi_enriched` (string)** :
- `"Durée totale du passage : 3 heures et 37 minutes"` présent
- `"(soit 3.62 heures décimales)"` présent
- `"Âge du patient : 77 ans"` présent
- `"CCMU : 3. Etat lésionnel"` présent (début libellé complet)
- `"sans mettre en jeu le pronostic vital"` présent (fin libellé CCMU — vérifie
que tout le libellé multi-ligne a été reconstitué)
- `"GEMSA : 2. Patient non convoqué sortant après consultation ou soins"` présent
- `"Priorité IAO : Priorité 3"` présent
- `"J12.1 Pneumopathie"` présent
- `"Consultation externe"` ABSENT du bloc FAITS_CALCULÉS
(assertion forte — vérifier que la string ne contient pas
`"Décision médicale terrain : ..."` dans FAITS_CALCULÉS)
- Le bloc FAITS_CALCULÉS apparaît AVANT le bandeau brut dans la chaîne
**Assertions sur `metadata` (dict)** :
- `metadata["duree_heures_decimales"] == 3.62` (tolérance ±0.01)
- `metadata["age_ans"] == 77`
- `metadata["ccmu"].startswith("3.")` ET `"Etat lésionnel" in metadata["ccmu"]`
ET `"pronostic vital" in metadata["ccmu"]` (vérifie début + reconstitution complète)
- `metadata["gemsa"].startswith("2.")` ET `"non convoqué" in metadata["gemsa"]`
- `metadata["priorite_iao"] == "Priorité 3"`
- `metadata["decision_terrain"] == "Consultation externe"`
- `metadata["orientation_terrain"] == "UC CONSULT.URGENCES"`
- `metadata["parsing_warnings"]` est une liste vide
### TEST NÉGATIF
Retirer la ligne "Date de décision médicale" du DPI input ET la ligne "Sortie"
du bandeau. Vérifier :
- La fonction ne crashe pas
- `dpi_enriched` contient `"Durée totale du passage : NON CALCULABLE"`
- `metadata["duree_heures_decimales"] is None`
- `metadata["parsing_warnings"]` contient une entrée explicite
### INTÉGRATION PIPELINE
Identifier l'endroit où le `dpi_raw` (les 5 t_* concaténées) est passé au LLM
pour la décision T2A. Insérer un appel à `build_dpi_enriched` JUSTE AVANT
l'envoi au LLM. Stocker la metadata pour réutilisation au commit 2.
**PAS de modification du PROMPT 3** (instruction 5 sur le calcul de durée reste
telle quelle pour permettre la comparaison en commit 2).
### VALIDATION COMMIT 1
- pytest passe sur le test golden + test négatif
- Lecture par Dom de la fonction et du test avant push
- Commit message : `feat(t2a): build_dpi_enriched - extraction déterministe horaires + classifications cliniques`
---
## PHASE INTERMÉDIAIRE — Mini-bench standalone sur 11 dossiers POC
Entre commit 1 (validé, mergé) et commit 2 (à coder), créer un script de bench
standalone qui exécute `build_dpi_enriched` + appel LLM sur les 11 dossiers POC
GHT Sud 95, **sans passer par Demo_urgence_2 ni Léa/Windows**.
**Objectif** : observer le comportement réel du LLM sur un panel représentatif
AVANT d'écrire le garde-fou décision (commit 2), ce qui permet :
- De découvrir les libellés `Décision médicale` réels → alimente le mapping
`TERRAIN_VERS_T2A` sans grep manuel
- De mesurer le taux de convergence durée/décision sur 11 cas, pas 1
- D'identifier d'autres patterns d'hallucination au-delà du "depuis 23h" MOREL
### EMPLACEMENT
`scripts/bench_t2a_dryrun.py`
### COMPORTEMENT
1. Lire les 11 dossiers depuis :
`/home/dom/ai/rpa_vision_v3/docs/clients/ght_sud_95/mockup_easily_assure/data.js`
⚠️ **Format réel** : ce n'est pas du JSON pur — c'est un **module JavaScript**
(`const DOSSIERS = { "<IPP>": { ... }, ... };`).
11 dossiers confirmés (un par IPP). Cas pilote = MOREL `"25003284"`.
Structure observée par dossier (champs pertinents — à compléter en lisant
directement le fichier avant de coder le parseur) :
```
"<IPP>": {
statut_attente: bool,
identite: { ipp, nom, prenom, ne_le, age, sexe },
passage: { arrivee, iao, iao_heure, medecin, sortie, motif_court, statut },
motif: {
obs_ide, // ← contient le piège "depuis 23h" (cas MOREL)
symptomes_orientation, symptomes_date, symptomes_par,
priorite, // ← "Priorité 2" ou "Priorité 3" (mappe au metadata python priorite_iao)
ccmu, // ← libellé complet "N. Description..."
gemsa, // ← libellé complet "N. Description..."
},
diagnostics: [ { code, ... }, ... ],
decision: "<libellé terrain>",
orientation: "<...>",
us_destination: "<...>",
}
```
⚠️ **Mapping JS → metadata python** :
- `motif.priorite` (JS) → `priorite_iao` (metadata python) — le nom python est
conservé pour clarté métier, ne pas renommer.
- `motif.ccmu`, `motif.gemsa` (JS) → `ccmu`, `gemsa` (metadata python)
**Options de parsing** (à trancher par [S1] selon ce qui est le plus simple) :
- Conversion ponctuelle JS → JSON via un petit script (`node -e "console.log(JSON.stringify(require('./data.js').DOSSIERS))"` ou équivalent)
- Parsing direct en Python via regex / `pyjsparser` / `esprima-python`
- Demande à Dom d'exporter un `data.json` parallèle
⚠️ **Note anonymisation — statut INCERTAIN** (Dom, 12 mai) :
Dom n'est pas sûr si la version actuelle de `data.js` est v1 brute, v1
corrigée, ou v2. Voir mémoire `feedback_anonymisation_stricte.md` —
`data.js` v1 avait des hallucinations à sens inversé (ankylose↔anhydrose,
avec/sans injection) liées à une anonymisation LLM trop libre.
**Action requise pour le bench — filet `data_quality_warning`** :
Pour chaque dossier, pendant l'exécution, ajouter dans le log un champ
`data_quality_warning` qui **flag sans corriger** toute incohérence
clinique flagrante détectable simplement :
- Motif incohérent avec diagnostic principal (ex: motif "Asthme" + diag
"Fracture du poignet")
- Sexe ou âge contradictoires entre champs (`identite.sexe` vs mentions
dans `obs_ide`, `identite.age` vs `identite.ne_le`)
- Observations qui contredisent les signes vitaux (ex: "patient stable"
+ tachycardie sévère mentionnée)
C'est un **filet, pas une analyse exhaustive**. Si quelque chose paraît
bizarre pendant l'exécution, le signaler à Dom en fin de bench (stats
finales) plutôt que d'interpréter une divergence LLM↔terrain comme un
défaut du LLM.
**Le bench doit logger fidèlement ce qu'il reçoit, jamais "corriger"
silencieusement.**
2. Pour chaque dossier :
a. **Construire `dpi_raw` en texte plat** en concaténant les champs
structurés sous une forme proche de ce que produit OCR scroll auto réel
(bandeau en tête, puis sections Motif/Examens/Imagerie/Notes/Synthèse).
⚠️ **NE PAS bypasser `build_dpi_enriched`** en lui donnant les données
déjà structurées sous forme de dict. La fonction doit recevoir un
**texte plat qu'elle parsera elle-même**. C'est le test de robustesse
du parser sur 11 cas. Sinon le bench n'a aucune valeur diagnostique.
Pour rester proche du format OCR scroll auto réel, [S1] s'inspire d'un
dump OCR existant si disponible (ex: capture MOREL ayant servi à
l'audit). À défaut, format raisonnable inspiré de l'UI Easily Assure.
b. Appeler `build_dpi_enriched(dpi_raw)` → récupère `(dpi_enriched, metadata)`
c. Appeler le LLM avec PROMPT 3 **actuel** (PAS réécrit) + `dpi_enriched`
d. Logger les 4 traces `[t2a_dryrun_*]` dans `logs/t2a_dryrun/<IPP>_<timestamp>.log`
(**instrumentation portée par le bench lui-même**, autonome — le serveur
n'a pas encore le commit 2 à ce stade)
3. **Mapping `TERRAIN_VERS_T2A` — VALIDÉ PAR DOM (12 mai 2026)** :
```python
TERRAIN_VERS_T2A = {
"Consultation externe": "FORFAIT_URGENCE",
"Hospitalisation": "REQUALIFICATION_HOSPITALISATION",
"Sortie après surveillance UHCD": "REQUALIFICATION_HOSPITALISATION",
"Transfert intra-hospitalier": "REQUALIFICATION_HOSPITALISATION",
}
```
Cas non mappés volontairement :
- `"Retour structure d'origine"` → à arbitrer cliniquement (email DIM
Pauline/Amina en cours de préparation). Pour aujourd'hui : non mappé,
loguera `Libellé terrain non mappé` sans erreur.
- `""` (chaîne vide) → dossier en attente, **SKIPPER** dans la boucle
(cf. `statut_attente: true` côté JS).
**Chiffres empiriques (parsés depuis `data.js` le 12 mai)** :
- 11 dossiers total
- **0 dossier `statut_attente: true`** → pas de skip (le check reste dans
le bench pour robustesse future)
- 11 dossiers utiles dans la boucle
- Décomposition `synthese.decision` :
- `Consultation externe` : 7 → mappés FORFAIT_URGENCE
- `Hospitalisation` : 1 → mappé REQUALIFICATION
- `Sortie après surveillance UHCD` : 1 → mappé REQUALIFICATION
- `Transfert intra-hospitalier` : 1 → mappé REQUALIFICATION
- `Retour structure d'origine` : 1 → non mappé (volontaire)
- **Total : 11 utiles, 10 mappés, 1 non mappé**.
✅ Incohérence précédente du brief (10 cas annotés vs 9 mappés annoncés)
levée : c'était 10 mappés sur 11 utiles, l'erreur venait de la prémisse
"1 dossier en attente" qui n'existe pas dans `data.js`.
**Implémentation recommandée par [S2]** : placer ce mapping dans un module
partagé (ex: `core/llm/t2a_mappings.py`), importé à la fois par le bench
et par `_handle_t2a_decision_action` au commit 2. Évite la duplication et
garantit la cohérence entre les 2 phases.
4. Afficher un tableau récap stdout (markdown) — 9 colonnes :
```
| IPP | Nom | duree_python | duree_llm | conv_duree | decision_llm | decision_terrain | mapping_attendu | conv_decision |
```
5. **Sortie CSV recommandée** : `scripts/bench_t2a_dryrun_<timestamp>.csv`
pour analyse downstream / archivage / comparaison exécution #1 vs #2.
### ACTIVATION
```bash
T2A_DRYRUN=1 python scripts/bench_t2a_dryrun.py
```
### USAGE EN 2 EXÉCUTIONS
- **Exécution #1** : APRÈS commit 1 (build_dpi_enriched + golden MOREL),
AVANT commit 2 (garde-fous).
Objectif : mesurer convergence durée + décision sur 11 cas avec le mapping
pré-validé. Identifier d'éventuels patterns d'hallucination au-delà de
"depuis 23h" MOREL. Le mapping étant déjà validé par Dom, **pas de second
grep** — le commit 2 peut partir avec cette table sans découverte
intermédiaire.
- **Exécution #2** : APRÈS commit 2.
Objectif : validation finale avant push. Vérifier que la logique garde-fou
logue proprement, que le pipeline complet ne régresse pas, et que les
résultats #1 vs #2 sont cohérents (si params LLM reproductibles).
### PARAMÈTRES LLM — TRANCHÉ : OPTION 2 (Dom, 12 mai)
Le bench appelle le LLM avec **`temperature=0`, `seed=42`**.
Le serveur de production reste inchangé pour aujourd'hui. La dette « passer le
serveur en `temp=0`/`seed=42` » est tracée en P0 [S3].
⚠️ **Point de vigilance — vérifier empiriquement que `gemma4:31b-cloud`
respecte effectivement `seed=42`**. Certains providers cloud ignorent ce
paramètre à cause du routage GPU non déterministe. Test simple : lancer 2 fois
le même prompt avec le même seed et comparer les sorties.
- Si sorties identiques → seed respecté, reproductibilité OK.
- Si sorties différentes → seed ignoré côté cloud. **Loguer explicitement ce
bruit résiduel dans les stats finales du bench** (mention type "seed non
respecté côté modèle cloud, divergences résiduelles attendues"). Ne pas
masquer le bruit ni l'interpréter comme un échec du chantier.
À documenter dans la sortie stdout du bench si le bruit est détecté.
### LIMITATION (notée par Dom)
Ce bench teste `build_dpi_enriched` + appel LLM **isolé**. Il NE remplace PAS un
dry-run end-to-end via Demo_urgence_2 qui reste utile pour valider l'intégration
pipeline complète. Faire AU MOINS un Demo_urgence_2 sur MOREL après commit 2 pour
valider la chaîne complète.
### VALIDATION PHASE INTERMÉDIAIRE
- Le bench tourne sans crash sur les 11 dossiers (1 skip pour vide + 10 traités)
- Le tableau récap stdout est lisible avec ses 9 colonnes
- Les 10 logs `logs/t2a_dryrun/<IPP>_*.log` sont écrits (1 skip)
- Le CSV de bench est sauvegardé
### STATS DE FIN DE BENCH (livrables)
À produire à la fin du bench, soit en stdout après le tableau, soit dans un
fichier dédié `scripts/bench_t2a_dryrun_<timestamp>_stats.md` :
- **Taux convergence durée Python ↔ LLM** sur 10 dossiers utiles (hors vide)
- **Taux convergence décision terrain ↔ LLM** sur 10 dossiers mappés
(11 utiles - 1 non mappé "Retour structure d'origine")
- **Liste des dossiers en divergence** avec extraits de log pertinents
(ex: snippet `"justification"` du LLM pour comprendre l'erreur)
- **Recommandation** : faut-il réécrire PROMPT 3 (finding #5) ou les
FAITS_CALCULÉS suffisent-ils ?
Commit message : `feat(t2a): scripts/bench_t2a_dryrun.py - mini-bench standalone 11 dossiers POC`
---
## COMMIT 2 — Garde-fous comparaison Python ↔ LLM
### EMPLACEMENT
`_handle_t2a_decision_action` (audit finding #1, fichier serveur).
Si tu ne trouves pas la fonction, grep `"_handle_t2a_decision_action"` dans le
repo et liste les fichiers concernés à Dom avant de modifier.
### LOGIQUE GARDE-FOU 1 — Durée
Après réception du JSON de réponse LLM, AVANT toute utilisation du champ
`duree_passage_heures` :
```python
duree_python = metadata["duree_heures_decimales"] # source de vérité
duree_llm = json_response.get("duree_passage_heures")
SEUIL_TOLERANCE_HEURES = 0.5 # ≈ 30 minutes, distingue hallucination grossière
if duree_python is None:
logger.info("[t2a_compare_duree] Pas de durée Python disponible (parsing échoué)")
elif duree_llm is None:
logger.info("[t2a_compare_duree] LLM a renvoyé null pour duree_passage_heures")
elif abs(duree_llm - duree_python) > SEUIL_TOLERANCE_HEURES:
logger.warning(
f"[t2a_compare_duree] DIVERGENCE : LLM={duree_llm}h vs Python={duree_python}h "
f"(écart={abs(duree_llm - duree_python):.2f}h)"
)
else:
logger.info(f"[t2a_compare_duree] Convergence : {duree_python}h")
# Dans tous les cas, UI et BDD utilisent duree_python (source de vérité = Python).
# La valeur LLM n'est conservée que dans le log pour analyse.
```
### LOGIQUE GARDE-FOU 2 — Décision
**MAPPING DÉJÀ VALIDÉ PAR DOM** (12 mai 2026, basé sur les 11 dossiers POC).
[S1] utilise directement le mapping ci-dessous (identique à celui du mini-bench
PHASE INTERMÉDIAIRE). **Aucune procédure de découverte/grep à effectuer** : le
mapping est définitif pour le périmètre démo GHT Sud 95.
**Implémentation recommandée** : module partagé `core/llm/t2a_mappings.py`
importé à la fois par `scripts/bench_t2a_dryrun.py` et par
`_handle_t2a_decision_action`. Garantit cohérence entre PHASE INTERMÉDIAIRE
et COMMIT 2, et évite toute divergence.
Si un nouveau libellé apparaît hors POC actuel (déploiement futur autre
établissement), le bench le logguera `Libellé terrain non mappé` et il sera
ajouté ponctuellement après validation Dom.
Toujours dans `_handle_t2a_decision_action`, après le garde-fou durée :
```python
decision_terrain = metadata["decision_terrain"] # ex: "Consultation externe"
decision_llm = json_response.get("decision") # ex: "FORFAIT_URGENCE" ou
# "REQUALIFICATION_HOSPITALISATION"
# Mapping terrain → catégorie T2A attendue
# (importé idéalement depuis core/llm/t2a_mappings.py — table validée Dom 12/05)
TERRAIN_VERS_T2A = {
"Consultation externe": "FORFAIT_URGENCE",
"Hospitalisation": "REQUALIFICATION_HOSPITALISATION",
"Sortie après surveillance UHCD": "REQUALIFICATION_HOSPITALISATION",
"Transfert intra-hospitalier": "REQUALIFICATION_HOSPITALISATION",
# "Retour structure d'origine" volontairement non mappé (à discuter cliniquement)
# "" (chaîne vide) → dossier en attente, ne devrait pas atteindre cette logique
}
attendu = TERRAIN_VERS_T2A.get(decision_terrain) if decision_terrain else None
if decision_terrain is None:
logger.info("[t2a_compare_decision] Pas de décision terrain disponible")
elif attendu is None:
logger.info(f"[t2a_compare_decision] Libellé terrain non mappé : '{decision_terrain}'")
elif decision_llm != attendu:
logger.warning(
f"[t2a_compare_decision] DIVERGENCE : LLM={decision_llm} vs "
f"terrain={decision_terrain} (attendu T2A : {attendu})"
)
else:
logger.info(f"[t2a_compare_decision] Convergence : LLM et terrain concordent ({attendu})")
# NE PAS overrider la décision LLM ici. Le log sert uniquement à mesurer la
# divergence. Si un override existait déjà avant ce commit, ne PAS le toucher
# (sujet finding #1 à traiter dans un chantier séparé).
```
### PAS DE BANDEAU UI POUR L'INSTANT
Log fichier uniquement. On ajoutera un bandeau "À valider" plus tard si les
divergences sont fréquentes. Pour l'instant, on instrumente pour mesurer.
### VÉRIFICATION AVAL (30 secondes)
Avant de pousser commit 2, `grep "duree_passage_heures"` dans tout le repo.
Vérifier qu'aucun consommateur aval (rapport, export, BDD) ne dépend de la
valeur LLM. Si un consommateur le fait, signaler à Dom avant de modifier
le comportement par défaut (UI/BDD = valeur Python).
### INSTRUMENTATION DRY-RUN — OBLIGATOIRE
Pour le dry-run MOREL (et tout cas T2A quand le flag est actif), le serveur
DOIT logger 4 traces structurées, dans cet ordre temporel :
**1.** `[t2a_dryrun_metadata]` — APRÈS l'appel `build_dpi_enriched()`, AVANT l'appel LLM
- Contenu : dict metadata complet retourné par `build_dpi_enriched`
(toutes les valeurs Python : durée, âge, CCMU, GEMSA, decision_terrain,
orientation_terrain, parsing_warnings)
**2.** `[t2a_dryrun_prompt]` — JUSTE AVANT l'appel API LLM
- Contenu in extenso :
- System prompt (PROMPT 3 intégral, le texte effectivement envoyé)
- User message complet (DPI enrichi = FAITS_CALCULÉS + dpi_raw)
- Tout autre paramètre transmis à l'API (température, seed si défini, modèle)
- Format : 3 backticks markdown pour préserver l'indentation et la lisibilité
**3.** `[t2a_dryrun_response]` — APRÈS réception réponse LLM (cas succès)
- Contenu :
- JSON brut complet reçu, AVANT tout parsing
- Stats Ollama : `eval_count`, `eval_duration`, `prompt_eval_count`,
`prompt_eval_duration` (utile pour débit tokens/s)
- Modèle utilisé, latence totale
**4.** `[t2a_dryrun_error]` — UNIQUEMENT si exception ou code HTTP non-2xx
- Contenu :
- Type d'exception ou code HTTP
- Payload partiel reçu s'il y en a
- Stack trace courte
- Ce log compense le fait que `[t2a_dryrun_response]` ne sera pas écrit en
cas d'échec API.
#### DESTINATION DES LOGS — FICHIER DÉDIÉ
**NE PAS** écrire ces logs dans le log serveur courant (4-6k tokens de prompt
le rendraient illisible).
Écrire dans un fichier dédié horodaté :
```
logs/t2a_dryrun/<cas_id_ou_IPP>_<timestamp>.log
```
Où :
- `<cas_id_ou_IPP>` : identifiant du dossier traité (ex: `"25003284"` pour MOREL)
- `<timestamp>` : format ISO compact, ex: `20260512_143022`
Créer le dossier `logs/t2a_dryrun/` automatiquement s'il n'existe pas.
⚠️ Vérifier que `logs/t2a_dryrun/` est dans `.gitignore`. Si non, l'ajouter
dans le même commit. Les logs contiennent des données patient (même si
fictives MOREL en POC, c'est l'hygiène par défaut).
#### ACTIVATION VIA FLAG D'ENVIRONNEMENT
Variable : `T2A_DRYRUN=1`
Deux modes d'activation à documenter dans le commit message (ou un petit
`docs/dryrun.md` court) :
**Mode dev local (ad-hoc)** :
```bash
T2A_DRYRUN=1 python -m visual_workflow_builder.backend ...
```
**Mode service systemd (réversible, sans modifier le unit file en dur)** :
Créer `/etc/systemd/system/<nom_du_service>.d/dryrun.conf` avec :
```ini
[Service]
Environment=T2A_DRYRUN=1
```
Puis :
```bash
sudo systemctl daemon-reload && sudo systemctl restart <service>
```
Pour désactiver : supprimer le drop-in et redémarrer.
⚠️ Ne JAMAIS modifier le unit file principal en dur pour activer le dryrun.
Toujours passer par drop-in ou ad-hoc.
#### POURQUOI CETTE INSTRUMENTATION EST CRITIQUE
Le log "convergence/divergence" du garde-fou indique uniquement qu'il y a
un problème. Pour diagnostiquer, on a besoin de voir :
- Si FAITS_CALCULÉS est bien injecté et bien positionné en tête du prompt
- Si le DPI brut contient effectivement `"depuis 23h"` (piège dans Observ. IDE Urg)
- Comment le LLM justifie son éventuelle hallucination (champ `"justification"`
de sa réponse JSON, lecture humaine)
- Si l'éventuelle divergence vient d'un bug d'intégration (FAITS_CALCULÉS non
injecté), du LLM (ignore les FAITS_CALCULÉS), ou d'une erreur API
Sans ces 4 traces, un échec du dry-run est inexploitable et on ne pourra
pas décider de la suite (réécrire PROMPT 3 vs autre diagnostic).
#### NOTE FORMAT POUR EXTENSION FUTURE
Format markdown backticks est OK pour analyse manuelle sur 1-2 cas.
Si on étend à N cas en batch (post-démo), basculer en JSONL — pas
nécessaire aujourd'hui, juste à garder en tête. À mentionner dans la
dette [S3].
### VALIDATION COMMIT 2
- Dry-run sur DPI MOREL : exécuter le workflow Demo_urgence_2 (ou un appel
ciblé à la fonction de décision si possible sans tout le workflow), vérifier
dans les logs :
- `"Durée totale du passage : 3 heures et 37 minutes"` injecté dans le prompt
- Un log `[t2a_compare_duree]` présent avec convergence OU divergence
- Un log `[t2a_compare_decision]` présent avec convergence OU divergence
- Les 4 traces `[t2a_dryrun_*]` présentes dans `logs/t2a_dryrun/25003284_*.log`
- Commit message : `feat(t2a): garde-fous comparaison durée + décision Python vs LLM + instrumentation dry-run`
---
## MÉTHODE DE TRAVAIL (rappel CLAUDE.md)
- **Chirurgie** : un changement, un test, validation avant le suivant
- Pas de rustines structurelles non discutées
- Lecture du code AVANT proposition de modif (lis `_handle_t2a_decision_action`
AVANT de proposer ton intégration commit 2)
- Court de préférence dans tes retours à Dom
- Backup avant toute modif BDD (ici pas de schéma BDD touché normalement,
mais vérifie)
- **Pas de modification PROMPT 3 dans ces commits, sous aucun prétexte**
---
## LIVRABLES ATTENDUS
1. Résultat étape 0 (Synthèse Urgences + bandeau présents dans `dpi_raw` ? OUI/NON)
2. Commit 1 + tests verts (golden MOREL + test négatif)
3. Phase intermédiaire — mini-bench exécution #1 (post-commit 1, pré-commit 2) :
- 10 logs `logs/t2a_dryrun/<IPP>_*.log` écrits (1 dossier vide skippé)
- Tableau récap stdout (9 colonnes) + CSV
- Stats de fin de bench (taux convergence durée X/11, décision X/10)
4. Commit 2 + 4 traces `[t2a_dryrun_*]` dans `_handle_t2a_decision_action`
5. Mini-bench exécution #2 (post-commit 2) — validation finale
6. **Dry-run E2E Demo_urgence_2 sur MOREL** — OBLIGATOIRE après commit 2.
Le mini-bench ne remplace pas cette validation pipeline complète.
7. Bref retour à Dom :
- `duree_python` observée sur MOREL (attendu : 3.62)
- `duree_llm` observée sur MOREL
- Convergence ou divergence durée ?
- `decision_llm` observée sur MOREL (FORFAIT_URGENCE ou REQUALIFICATION ?)
- Convergence ou divergence décision ?
- **Synthèse mini-bench 11 dossiers** :
- Taux de convergence durée (X/11 utiles)
- Taux de convergence décision (X/10 mappés)
- Liste des dossiers en divergence + extrait du log (champ `justification`
LLM) pour comprendre l'erreur
- Patterns d'hallucination identifiés au-delà du "depuis 23h" MOREL
- Recommandation : faut-il réécrire PROMPT 3 dans la foulée ou la
comparaison montre que le LLM s'aligne déjà spontanément sur les
FAITS_CALCULÉS ?
**PAS DE GO POUR LA RÉÉCRITURE DU PROMPT 3 SANS VALIDATION EXPRESSE DE DOM
APRÈS LECTURE DU DRY-RUN.**
---
## ARBITRAGES — soulevés en revue [S2], tous tranchés par Dom (12 mai 2026)
1. ~~**CCMU/GEMSA — code seul ou code + libellé ?**~~
**TRANCHÉ** : libellé COMPLET tel qu'affiché dans la Synthèse Urgences,
intégré dans `ccmu` et `gemsa` (string unique, code + libellé). Brief mis à
jour dans `LOGIQUE D'EXTRACTION`, `METADATA RETOURNÉE`, `CONSTRUCTION DU BLOC
FAITS_CALCULÉS` et `TEST GOLDEN MOREL`.
2. ~~**Priorité IAO non extraite**~~
**TRANCHÉ** : OUI, extraire `priorite_iao` depuis la ligne "Priorité" de la
Synthèse Urgences, injecter dans FAITS_CALCULÉS juste après GEMSA, avant
Mode de venue. Format : `"- Priorité IAO : Priorité 3"`. Brief mis à jour
dans les 4 mêmes sections que ci-dessus.
3. ~~**Mapping `TERRAIN_VERS_T2A` — à enrichir AVANT commit 2**~~
**TRANCHÉ + COMPLÉTÉ (Dom, 12 mai)** : table définitive validée par Dom
sur les 11 dossiers POC, sans étape de grep intermédiaire :
```python
TERRAIN_VERS_T2A = {
"Consultation externe": "FORFAIT_URGENCE",
"Hospitalisation": "REQUALIFICATION_HOSPITALISATION",
"Sortie après surveillance UHCD": "REQUALIFICATION_HOSPITALISATION",
"Transfert intra-hospitalier": "REQUALIFICATION_HOSPITALISATION",
}
```
Cas spéciaux : `"Retour structure d'origine"` non mappé (à discuter cliniquement),
`""` = dossier en attente skippé par le bench. Implémentation recommandée :
module partagé `core/llm/t2a_mappings.py`. Voir sections PHASE INTERMÉDIAIRE
et LOGIQUE GARDE-FOU 2 du brief.
4. ~~**Avertissement explicite "depuis 23h" dans FAITS_CALCULÉS ?**~~
**TRANCHÉ** : **OPTION A — expérience pure, pas de filet**. On injecte
uniquement les faits dans FAITS_CALCULÉS, sans avertissement préventif sur
"depuis Xh" ou similaire.
**Raison** : le dry-run MOREL a une valeur diagnostique forte. Si on ajoute
le filet maintenant, on ne saura jamais si FAITS_CALCULÉS seul suffit ou si
c'est le filet qui sauve la mise. Cette information est nécessaire pour les
futurs cas qu'on n'aura pas anticipés.
**Plan B si le dry-run échoue** : si le dry-run montre que le LLM replonge
sur "depuis 23h" malgré FAITS_CALCULÉS en tête, [S1] patche PROMPT 3 (finding
#5) avec un avertissement ciblé. Mais cette décision n'est prise qu'APRÈS
observation du log, pas en anticipation.
⇒ **Aucune modification du brief sur ce point** : FAITS_CALCULÉS reste tel
que défini ci-dessus, sans ligne d'avertissement.
---
## STATUT FINAL DU BRIEF
✅ **Tous les arbitrages tranchés** (CCMU/GEMSA libellé complet, Priorité IAO,
mapping `TERRAIN_VERS_T2A` validé sur 10/11 cas, "depuis 23h" Option A pure,
paramètres LLM Option 2 `temp=0/seed=42`, filet `data_quality_warning` pour
incertitude anonymisation v1/v2).
⚠️ **2 points à clarifier en début d'exécution par [S1], non bloquants pour
ÉTAPE 0** :
1. ~~Incohérence compte mapping (10 cas annotés vs 9 mappés annoncés)~~ —
**résolue le 12 mai** : parsing empirique de `data.js` confirme 11 utiles
(0 en attente), 10 mappés, 1 non mappé (`Retour structure d'origine`).
2. Vérification empirique seed `gemma4:31b-cloud` (cf. PARAMÈTRES LLM).
🚀 **GO ÉTAPE 0**.
---
## CONTEXTE [S2] — pour traçabilité
Brief produit par session [S1], revu par session [S2] (audit éclair 30-45 min,
12 mai 2026). Voir aussi :
- `docs/handoffs/2026-05-12_handoff_S2_vers_S1.md` — synthèse audit éclair [S2]
- `docs/handoffs/2026-05-12_audit_complet_decision_t2a.md` — rapport [S1] de référence
- Doc DIM/TIM source : `~/Téléchargements/RPU UHCD IA (2) (5)/RPU UHCD IA/RPU UHCD IA.pptx`

View File

@@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Codage — Easily Assure</title>
<link rel="stylesheet" href="easily.css">
</head>
<body>
<div class="app-header">
<div class="brand">Easily Assure <span class="sub">Urgences — Maquette POC</span></div>
<div class="user-zone">
<span>DR. Antoine BONNET</span>
<a href="index.html">Liste patients</a>
<a href="#" onclick="event.preventDefault(); window.resetCodage && window.resetCodage();">Reset</a>
<a href="#">Déconnexion</a>
</div>
</div>
<div class="menu-bar">
<a href="index.html">Patients</a>
<a href="#">Planning</a>
<a id="lien-dossier" href="dossier.html">Dossier en cours</a>
<a class="active" href="#">Codage</a>
<a href="#">Statistiques</a>
</div>
<div id="patient-banner" class="patient-banner">
<!-- Bandeau injecté par app.js -->
</div>
<div id="bandeau-attente" style="display:none; background:#fff8e0; border:2px solid #e0a800; padding:10px 16px; margin:0; color:#8a6d00; font-weight:bold;">
⚠ Dossier en attente — cas réel à transmettre par Pauline / Amina.
</div>
<div class="tabs">
<a class="tab" id="tab-vers-dossier" href="dossier.html">&lt; Retour dossier</a>
<a class="tab active">Arbre décisionnel UHCD / Forfait Urgences</a>
</div>
<div class="content aiva-page">
<div class="aiva-header">
<h1 class="aiva-title">aiva-vision — Aide à la décision de facturation urgences</h1>
<p class="aiva-subtitle">Forfait urgences vs requalification en hospitalisation MCO — décision T2A/PMSI assistée par LLM local</p>
</div>
<div class="aiva-layout">
<!-- COLONNE GAUCHE -->
<div class="aiva-col">
<h2><span class="icon">🔗</span> Dossier patient (DPI urgences)</h2>
<div class="aiva-col-label">Coller ou saisir le dossier patient</div>
<textarea id="dpi-input" class="aiva-dpi-textarea" placeholder="Coller ou saisir le dossier patient"></textarea>
<button id="btn-analyser" class="aiva-btn-primary">Analyser</button>
</div>
<!-- COLONNE DROITE -->
<div class="aiva-col">
<h2>Décision facturation</h2>
<!-- État loading (overlay pendant analyse) -->
<div id="aiva-result-loading" class="aiva-spinner aiva-result-hidden">
Analyse en cours…
</div>
<!-- Résultat (visible dès le chargement, état "neutre" en attente d'analyse) -->
<div id="aiva-result">
<div id="aiva-decision-banner" class="banner-empty">— En attente d'analyse —</div>
<div id="aiva-valorisation">&nbsp;</div>
<div id="aiva-verite-terrain">Vérité-terrain : <code></code></div>
<div class="aiva-metrics">
<div class="aiva-metric">
<div class="aiva-metric-label">Confiance</div>
<div class="aiva-metric-value" id="aiva-confiance"></div>
</div>
<div class="aiva-metric">
<div class="aiva-metric-label">Durée passage</div>
<div class="aiva-metric-value" id="aiva-duree"></div>
</div>
<div class="aiva-metric">
<div class="aiva-metric-label">Latence</div>
<div class="aiva-metric-value" id="aiva-latence"></div>
</div>
</div>
<div class="aiva-section-label">Justification</div>
<textarea id="aiva-justification" class="aiva-justif-textarea" placeholder="Justification de la décision (modifiable — Léa peut écrire avant l'analyse)"></textarea>
<div class="aiva-elements-grid">
<div>
<h3>Éléments pour hospitalisation</h3>
<ul id="aiva-elements-hospi"><li class="aiva-list-empty">Aucun élément pour le moment</li></ul>
</div>
<div>
<h3>Éléments pour forfait</h3>
<ul id="aiva-elements-forfait"><li class="aiva-list-empty">Aucun élément pour le moment</li></ul>
</div>
</div>
</div>
</div>
</div>
<div style="margin-top:20px; display:flex; gap:10px;">
<a class="btn large" id="btn-retour-dossier" href="dossier.html">&lt; Retour dossier</a>
</div>
</div>
<div class="app-footer">
Maquette pédagogique — données fictives — usage interne POC GHT Sud 95
</div>
<script src="data.js"></script>
<script src="app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,654 @@
/* Maquette Easily Assure — palette inspirée des 4 captures dossier 25003284
Objectifs :
- lisibilité OCR (Tahoma 13-14px, contrastes nets)
- éléments cliquables identifiables (cursor, hover, classes .clickable)
- rendu cohérent navigateur Windows / Linux (polices système)
*/
* { box-sizing: border-box; }
body {
margin: 0;
padding: 0;
font-family: Tahoma, Geneva, Verdana, "DejaVu Sans", Arial, sans-serif;
font-size: 13px;
color: #000;
background: #f5f5f5;
}
/* ===== Barre titre application ===== */
.app-header {
background: linear-gradient(to bottom, #4a7ba8, #2c5d8c);
color: #fff;
padding: 8px 16px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #1a3d5c;
height: 42px;
}
.app-header .brand {
font-size: 18px;
font-weight: bold;
letter-spacing: 0.5px;
}
.app-header .brand .sub {
font-size: 12px;
font-weight: normal;
margin-left: 12px;
opacity: 0.85;
}
.app-header .user-zone {
font-size: 12px;
display: flex;
gap: 16px;
align-items: center;
}
.app-header .user-zone a {
color: #cfe3f5;
text-decoration: none;
}
.app-header .user-zone a:hover { text-decoration: underline; }
/* ===== Barre menu principal ===== */
.menu-bar {
background: #d5e3f0;
border-bottom: 1px solid #8aa9c8;
padding: 0 16px;
display: flex;
height: 32px;
align-items: stretch;
}
.menu-bar a {
padding: 0 14px;
display: flex;
align-items: center;
color: #1a3d5c;
text-decoration: none;
font-size: 13px;
border-right: 1px solid #b8cee0;
cursor: pointer;
}
.menu-bar a:hover { background: #e8f0f8; }
.menu-bar a.active { background: #fff; font-weight: bold; }
/* ===== Bandeau patient ===== */
.patient-banner {
background: #fff;
border-bottom: 2px solid #4a7ba8;
padding: 10px 16px;
display: flex;
align-items: center;
gap: 24px;
}
.patient-banner .ipp {
font-size: 14px;
font-weight: bold;
color: #2c5d8c;
}
.patient-banner .nom {
font-size: 16px;
font-weight: bold;
}
.patient-banner .info-bloc {
color: #444;
font-size: 12px;
}
.patient-banner .info-bloc b { color: #000; }
/* ===== Onglets dossier ===== */
.tabs {
background: #e8eef5;
border-bottom: 2px solid #4a7ba8;
display: flex;
padding: 0 12px;
height: 36px;
align-items: stretch;
}
.tabs .tab {
padding: 0 18px;
display: flex;
align-items: center;
background: #c8d6e6;
border: 1px solid #8aa9c8;
border-bottom: none;
margin-right: 2px;
margin-top: 6px;
font-size: 13px;
cursor: pointer;
color: #1a3d5c;
text-decoration: none;
}
.tabs .tab:hover { background: #d5e3f0; }
.tabs .tab.active {
background: #fff;
font-weight: bold;
border-bottom: 1px solid #fff;
margin-bottom: -1px;
color: #000;
}
/* ===== Contenu principal ===== */
.content {
background: #fff;
padding: 12px 16px 32px 16px;
min-height: calc(100vh - 200px);
}
/* ===== Sections collapsibles (vues docx 1, 2, 3) ===== */
.section {
margin-bottom: 14px;
border: 1px solid #8aa9c8;
background: #fff;
}
.section-header {
background: linear-gradient(to bottom, #c8d8e8, #a8c4dc);
border-bottom: 1px solid #8aa9c8;
padding: 6px 10px;
font-weight: bold;
color: #1a3d5c;
font-size: 13px;
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
gap: 6px;
}
.section-header::before {
content: "▼";
font-size: 10px;
color: #2c5d8c;
}
.section.collapsed .section-header::before { content: "▶"; }
.section.collapsed .section-body { display: none; }
.section-body {
padding: 10px;
background: #fafdff;
}
/* ===== Tables génériques ===== */
table.data {
border-collapse: collapse;
width: 100%;
font-size: 13px;
}
table.data th, table.data td {
border: 1px solid #b8cee0;
padding: 5px 8px;
text-align: left;
vertical-align: top;
}
table.data th {
background: #d5e3f0;
color: #1a3d5c;
font-weight: bold;
}
table.data tr:nth-child(even) td { background: #f0f7fc; }
table.data .num { text-align: right; font-family: "Courier New", monospace; }
/* Liens cliquables avec contraste fort pour OCR */
a.link, .link {
color: #0033cc;
text-decoration: underline;
cursor: pointer;
}
a.link:hover, .link:hover { color: #cc0000; }
/* Valeurs surlignées (signes vitaux anormaux) */
.warn { color: #b00020; font-weight: bold; }
/* ===== Synthèse Urgences (capture 4) ===== */
.synthese-titre {
background: linear-gradient(to bottom, #6090c0, #2c5d8c);
color: #fff;
padding: 10px 14px;
font-size: 16px;
font-weight: bold;
margin-bottom: 0;
}
.synthese table {
width: 100%;
border-collapse: collapse;
margin-bottom: 14px;
}
.synthese .row-titre td {
background: linear-gradient(to bottom, #c8d8e8, #a8c4dc);
font-weight: bold;
color: #1a3d5c;
padding: 6px 10px;
border: 1px solid #8aa9c8;
}
.synthese td.label {
background: #f5f5f5;
width: 230px;
padding: 5px 10px;
border: 1px solid #d0d8e0;
font-weight: normal;
color: #333;
}
.synthese td.value {
padding: 0;
border: 1px solid #d0d8e0;
background: #fff;
}
.synthese td.value input,
.synthese td.value textarea,
.synthese td.value select {
width: 100%;
border: none;
background: #e8f5d8;
padding: 5px 8px;
font-family: inherit;
font-size: 13px;
color: #000;
}
.synthese td.value textarea {
min-height: 70px;
resize: vertical;
}
/* ===== Onglet Codage T2A — algo PPTX ===== */
.algo-card {
border: 1px solid #8aa9c8;
background: #fff;
padding: 16px 20px;
margin-bottom: 14px;
}
.algo-card h2 {
margin: 0 0 8px 0;
color: #1a3d5c;
font-size: 16px;
}
.algo-card .preuves {
font-size: 12px;
color: #444;
margin-top: 6px;
padding-left: 22px;
}
.criterion {
display: flex;
align-items: flex-start;
gap: 12px;
margin: 10px 0;
}
.criterion input[type=checkbox] {
width: 22px;
height: 22px;
margin-top: 2px;
cursor: pointer;
}
.criterion label {
font-size: 14px;
cursor: pointer;
user-select: none;
}
.preuve-zone {
margin-top: 12px;
padding-top: 10px;
border-top: 1px dashed #c8d6e6;
}
.preuve-zone-label {
display: block;
font-size: 12px;
color: #555;
margin-bottom: 4px;
font-style: italic;
}
.preuve-text {
width: 100%;
min-height: 140px;
padding: 10px 12px;
border: 1px solid #8aa9c8;
background: #fffef0;
font-family: inherit;
font-size: 14px;
line-height: 1.5;
color: #000;
resize: vertical;
box-sizing: border-box;
}
.preuve-text:focus {
outline: 2px solid #4a7ba8;
background: #fff;
}
.preuve-text::placeholder {
color: #999;
font-style: italic;
}
.verdict {
margin-top: 18px;
padding: 14px 18px;
border: 2px solid #999;
background: #f5f5f5;
font-size: 16px;
font-weight: bold;
}
.verdict.uhcd {
border-color: #2e7d32;
background: #e8f5e8;
color: #2e7d32;
}
.verdict.forfait {
border-color: #e65100;
background: #fff3e0;
color: #b25000;
}
.btn {
display: inline-block;
padding: 7px 16px;
background: linear-gradient(to bottom, #4a7ba8, #2c5d8c);
color: #fff;
border: 1px solid #1a3d5c;
cursor: pointer;
font-size: 13px;
font-family: inherit;
text-decoration: none;
border-radius: 2px;
}
.btn:hover { background: linear-gradient(to bottom, #5a8bb8, #3c6d9c); }
.btn.large { padding: 10px 22px; font-size: 14px; }
/* Pied de page */
.app-footer {
text-align: center;
color: #888;
font-size: 11px;
padding: 10px;
border-top: 1px solid #ddd;
background: #f5f5f5;
}
/* Liste patients (index.html) */
.search-bar {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 14px;
}
.search-bar input[type=text] {
padding: 5px 8px;
font-size: 13px;
border: 1px solid #8aa9c8;
width: 240px;
}
.patient-list-table tr.row-clickable { cursor: pointer; }
.patient-list-table tr.row-clickable:hover td { background: #fffbe0; }
/* ===========================================================
AIVA-VISION — Aide à la décision de facturation urgences
(palette claire alignée sur Easily Assure)
=========================================================== */
.aiva-page {
padding: 0 4px;
}
.aiva-header {
margin-bottom: 18px;
padding-bottom: 10px;
border-bottom: 1px solid #c8d6e6;
}
.aiva-title {
font-size: 22px;
font-weight: bold;
color: #1a3d5c;
margin: 0 0 4px 0;
}
.aiva-subtitle {
font-size: 12px;
color: #666;
margin: 0;
}
/* Layout 2 colonnes */
.aiva-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
align-items: start;
}
@media (max-width: 1100px) {
.aiva-layout { grid-template-columns: 1fr; }
}
.aiva-col {
border: 1px solid #8aa9c8;
background: #fff;
padding: 14px 16px;
box-sizing: border-box;
}
.aiva-col h2 {
margin: 0 0 10px 0;
font-size: 16px;
color: #1a3d5c;
display: flex;
align-items: center;
gap: 6px;
}
.aiva-col h2 .icon {
font-size: 14px;
color: #4a7ba8;
}
.aiva-col-label {
font-size: 12px;
color: #555;
margin-bottom: 6px;
}
/* Colonne gauche — DPI */
.aiva-dpi-textarea {
width: 100%;
min-height: 360px;
padding: 10px 12px;
border: 1px solid #8aa9c8;
background: #fffef0;
font-family: inherit;
font-size: 13px;
line-height: 1.5;
color: #000;
resize: vertical;
box-sizing: border-box;
margin-bottom: 12px;
}
.aiva-dpi-textarea:focus {
outline: 2px solid #4a7ba8;
background: #fff;
}
.aiva-dpi-textarea::placeholder {
color: #999;
font-style: italic;
}
.aiva-btn-primary {
width: 100%;
padding: 12px 16px;
background: linear-gradient(to bottom, #4a7ba8, #2c5d8c);
color: #fff;
border: 1px solid #1a3d5c;
cursor: pointer;
font-size: 14px;
font-weight: bold;
font-family: inherit;
border-radius: 2px;
letter-spacing: 0.5px;
}
.aiva-btn-primary:hover {
background: linear-gradient(to bottom, #5a8bb8, #3c6d9c);
}
.aiva-btn-primary:disabled {
background: #b8cee0;
cursor: wait;
color: #fff;
}
/* Colonne droite — Décision facturation */
#aiva-decision-banner {
padding: 16px 18px;
font-size: 18px;
font-weight: bold;
text-align: center;
color: #fff;
margin-bottom: 8px;
letter-spacing: 0.5px;
border: 2px solid transparent;
}
#aiva-decision-banner.banner-forfait {
background: linear-gradient(to bottom, #4caf50, #2e7d32);
border-color: #1b5e20;
}
#aiva-decision-banner.banner-uhcd {
background: linear-gradient(to bottom, #e57373, #c62828);
border-color: #8e0000;
}
#aiva-decision-banner.banner-empty {
background: #f5f5f5;
color: #888;
border: 1px dashed #b8cee0;
font-weight: normal;
font-size: 14px;
}
#aiva-valorisation {
text-align: center;
color: #444;
font-size: 13px;
margin-bottom: 14px;
}
#aiva-verite-terrain {
background: #f0f7fc;
border-left: 3px solid #4a7ba8;
padding: 6px 10px;
font-size: 12px;
color: #333;
margin-bottom: 14px;
}
#aiva-verite-terrain code {
background: #1a3d5c;
color: #ffeb3b;
padding: 1px 6px;
border-radius: 2px;
font-family: "Courier New", monospace;
font-size: 11px;
}
#aiva-verite-terrain .concordance-ok { color: #2e7d32; font-weight: bold; }
#aiva-verite-terrain .concordance-ko { color: #c62828; font-weight: bold; }
/* 3 indicateurs (Confiance / Durée / Latence) */
.aiva-metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 14px;
}
.aiva-metric {
background: #f0f7fc;
padding: 8px 10px;
border: 1px solid #d0e0ec;
}
.aiva-metric-label {
font-size: 11px;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 2px;
}
.aiva-metric-value {
font-size: 18px;
font-weight: bold;
color: #1a3d5c;
font-family: "Courier New", monospace;
}
/* Justification */
.aiva-section-label {
font-size: 12px;
font-weight: bold;
color: #1a3d5c;
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
#aiva-justification {
background: #e3f2fd;
border: 1px solid #90caf9;
border-left: 3px solid #1976d2;
padding: 10px 12px;
font-size: 13px;
line-height: 1.5;
color: #0d47a1;
margin-bottom: 16px;
}
/* Variante <textarea> de la justification (éditable par Léa). */
textarea#aiva-justification {
width: 100%;
min-height: 96px;
box-sizing: border-box;
font-family: inherit;
resize: vertical;
}
textarea#aiva-justification:focus {
outline: 2px solid #1976d2;
background: #fff;
}
textarea#aiva-justification::placeholder {
color: #6b8db5;
font-style: italic;
}
/* Listes éléments hospitalisation / forfait */
.aiva-elements-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.aiva-elements-grid h3 {
font-size: 13px;
color: #1a3d5c;
margin: 0 0 6px 0;
}
.aiva-elements-grid ul {
margin: 0;
padding-left: 18px;
font-size: 12px;
line-height: 1.5;
color: #333;
}
.aiva-elements-grid li {
margin-bottom: 4px;
}
.aiva-elements-grid li.aiva-list-empty {
color: #888;
font-style: italic;
list-style: none;
margin-left: -18px;
}
/* État caché par défaut (avant clic Analyser) */
.aiva-result-hidden { display: none; }
.aiva-result-empty {
text-align: center;
color: #888;
font-style: italic;
padding: 80px 20px;
border: 1px dashed #c8d6e6;
}
/* Spinner pendant analyse */
.aiva-spinner {
text-align: center;
padding: 60px 20px;
color: #4a7ba8;
font-size: 14px;
}
.aiva-spinner::before {
content: "⏳";
display: block;
font-size: 32px;
margin-bottom: 10px;
animation: aiva-rotate 1.2s linear infinite;
}
@keyframes aiva-rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}

View File

@@ -0,0 +1,88 @@
# Handoff [S2] → [S1] — Audit éclair prompts t2a (12 mai 2026)
## Mission [S2]
Audit qualité des 3 prompts (resume_patient, justification_t2a, t2a_decision)
sur cas MOREL réel + doc DIM/TIM officielle de l'établissement.
Time-box 30-45 min. Pas de code touché.
## Sources consultées
- 3 prompts collés par Dom (versions actuelles en prod)
- Doc DIM/TIM : `/home/dom/Téléchargements/RPU UHCD IA (2) (5)/RPU UHCD IA/RPU UHCD IA.pptx`
→ Slide 6 = règle officielle de l'établissement : **3 critères validés → UHCD ; 1 critère manquant → Forfait Urgences** (conjonction ET, 3/3)
- Capture UI aiva-vision sur dossier MOREL Catherine (77 ans, IPP 25003284,
passage 01/01/2025 03:12 → 06:49 = 3h37 réel)
## Bugs confirmés en condition réelle (cas MOREL)
### 🔴 #1 — Contradiction décision ↔ justification (NOUVEAU vs audit [S1])
- UI affiche `FORFAIT URGENCES (FFU/ATU)` en vert
- Le texte de justification conclut littéralement à `requalification en hospitalisation (UHCD)`
- Le PROMPT 2 dit « Tu ne dois PAS changer la décision » mais le résultat suggère
que la décision a été overridée APRÈS le LLM-justif (par la vérité-terrain ?),
sans recalculer la justif ni les listes `elements_pour_*`.
- À investiguer côté serveur, vraisemblablement dans `_handle_t2a_decision_action`
(ordre d'application : LLM-décision → override vérité-terrain → LLM-justif
appelée avec la décision corrigée OU pas du tout ?).
### 🔴 #2 — Hallucination durée (déjà connu, reconfirmé)
- Durée affichée : **23.0 h**
- Durée réelle calculable depuis bandeau Easily Assure : **3h37**
- LLM confond « symptômes depuis 23h » avec « durée du passage »
- C'est probablement la cause amont du bug #1 (23h pousse mécaniquement vers UHCD)
### 🟡 #3 — Listes elements_pour_* inversées
- `elements_pour_hospitalisation` contient « Sortie en consultation externe avec
point de contrôle à 48h » → c'est un argument POUR le forfait
- `elements_pour_forfait` : « Aucun élément cité par le LLM »
- Cohérent avec l'hypothèse override (le LLM avait raisonné UHCD)
### 🟡 #4 — Confiance "faible" ignorée par l'UI
- LLM marque sa propre confiance faible, l'UI affiche la décision sans warning
- Pour audience DIM, c'est rédhibitoire
### 🟡 #5 — Le PROMPT 3 contredit la doc DIM/TIM interne
- Prompt actuel : « au moins 2 sur 3 validés ⇒ REQUALIFICATION »
- Doc DIM slide 6 : « si oui aux 3 critères ⇒ UHCD ; 1 critère manquant ⇒ Forfait »
- Donc le prompt n'est pas seulement non-conforme à l'ATIH, il est aussi
non-conforme à la propre doc de l'établissement client.
- Idem pour le seuil « durée > 6h » dans le critère 2 : ABSENT de la PPTX,
confirme que c'est une erreur du DIM senior à retirer.
## Sur MOREL spécifiquement
Si on applique strictement la règle 3/3 de la doc :
- Critère 1 (pathologie évolutive) ✓ (pneumopathie + 77 ans + insuf coro + asthme)
- Critère 2 (surveillance prolongée) **✗** (3h37 réels, observations itératives non documentées)
- Critère 3 (examens/actes) ✓ (radio + biol + PCR VRS + aérosols + Augmentin)
⇒ 2/3 ⇒ **Forfait Urgences** (verdict correct)
La décision affichée actuelle est juste, mais **pour la mauvaise raison** :
le pipeline tombe sur le bon résultat via la vérité-terrain en override,
pas via un raisonnement LLM correct. Faux positif de concordance.
## Reco prioritaire — ordre des chantiers
1. **Préprocesseur Python `build_dpi_enriched`** (chantier déjà identifié en [S1])
→ calcul déterministe durée depuis horaires bandeau, injection
`FAITS_CALCULÉS: durée_passage = 3h37` dans le prompt.
→ C'est le levier #1. Sans ça, tout LLM continuera à halluciner.
2. **Réécrire PROMPT 3** avec règle 3/3 ET (texte Section F du rapport
`docs/handoffs/2026-05-12_audit_complet_decision_t2a.md`) + retirer seuil 6h.
3. **Garde-fou serveur** dans `_handle_t2a_decision_action` :
- Si `confiance == "faible"` → bandeau UI « À valider »
- Si override vérité-terrain appliqué → recalculer LLM-justif AVEC la décision
corrigée, ou afficher explicitement la divergence.
4. **Ajouter CCMU + GEMSA + mode_venue + occupation_lit** dans le contexte LLM
(slide 5 doc DIM, manquants dans le prompt actuel). Post-démo si time-box serré.
## Ce que [S2] N'A PAS fait (par choix de time-box)
- Pas lu le DOCX compagnon `RPU UHCD IA.docx` (PPTX déjà suffisamment claire)
- Pas grep des chemins fichiers ni vérif paramètres Ollama (temp/seed)
- Pas touché de code
## Synchro [S1]
Findings #1 et #5 (override vérité-terrain non recalculé + non-conformité doc
établissement) sont nouveaux vs ton audit du 12 mai. Le reste reconfirme.

View File

@@ -0,0 +1,173 @@
# Handoff fin de journée — 12 mai 2026
**Période couverte** : 09:16 → 19:30 (avec coupure déjeuner ~13:00-13:45 et vélo 18:00-19:27)
**Démo cible** : 15 mai 2026 (3 jours restants)
**Statut global** : très en avance sur planning matinal, qualité non-minimale visée maintenue.
---
## TL;DR — À lire en premier demain matin
1. **PRIORITÉ #1 demain** : bug d'orchestration skip ord 13 découvert en fin de journée. Criticité HAUTE pour démo. Voir fiche dédiée `_archives/handoff_evidence_20260512_1940/DETTE_S1_skip_ord13_orchestration.md` + trace JSON `replay_fb0c9882_state.json`.
2. **PRIORITÉ #2 demain** : reprise du mini-bench décision sur 11 dossiers POC dès que gemma4:31b-cloud répond (avorté aujourd'hui après 2-3 dossiers sur timeout Ollama Cloud 503).
3. **Acquis majeur du jour** : A.1 paste validé techniquement (65s → 2s sur step #25, 762 chars). Gain visible énorme sur la démo.
4. **2 commits décision en local non pushés** : à arbitrer demain matin avant de reprendre.
---
## État des chantiers à la coupure
### Chantier décision (`build_dpi_enriched` + garde-fous)
| Étape | Statut |
|---|---|
| Brief V3 finalisé (8 arbitrages tranchés) | ✅ `docs/handoffs/2026-05-12_brief_S1_build_dpi_enriched.md` |
| Étape 0 (vérif source données) | ✅ close sur inférence statique + capture MOREL |
| Commit 1 `build_dpi_enriched` (commit `9872f4510`) | ✅ local, **NON pushé** |
| Tests : golden MOREL + négatif + intégration | ✅ 41/41 verts, 0 régression sur 37 tests existants |
| Commit "bench" `bench_t2a_dryrun.py` + `t2a_mappings.py` (commit `f2212e77e`) | ✅ local, **NON pushé** |
| Mini-bench exécution sur 11 dossiers | 🔴 avorté après 2-3 dossiers (timeout gemma4:31b-cloud) |
| Commit 2 du brief (garde-fou serveur) | ❌ non démarré (dépend du bench) |
| Dry-run E2E Demo_urgence_2 sur MOREL | ❌ non démarré |
**Note vocabulaire** : "commit bench" ≠ "commit 2 du brief". Le commit 2 du brief = garde-fou Python ↔ LLM dans `_handle_t2a_decision_action`, pas encore commencé.
**État git branche décision** :
```
HEAD : f2212e77e (feat: bench + mappings) — local
9872f4510 (feat: build_dpi_enriched) — local
2 commits en avance sur origin/feature/qw-seuite-mai
```
### Chantier vitesse (A.1 paste + B parallélisation)
| Étape | Statut |
|---|---|
| Baseline run 4 instrumentée (timing) | ✅ 381s mesurés, goulots identifiés |
| Bascule γ → δ (A.1 d'abord, B après) | ✅ acté en cours de journée sur chiffres réels |
| A.1 paste implémenté (executor.py + DB) | ✅ |
| Test E2E run 6 — preuve gain | ✅ step #25 paste 762 chars en 2,02s vs 65s baseline |
| qwen3-next:80b-cloud testé qualité | ✅ run 8 dec + resume_patient cliniquement propres (1336 chars) — **switch ponctuel pour test, pas durable** |
| B parallélisation Ollama | ❌ annulé (A.1 plus rentable, β reporté [S3]) |
| Run 8 final | 🔴 cassé sur orchestration (bug skip ord 13 découvert) |
---
## Décisions et arbitrages tranchés aujourd'hui
### Chantier décision (8 arbitrages)
1. **CCMU/GEMSA dans FAITS_CALCULÉS** : libellé COMPLET, pas code seul (test Ollama a montré que gemma4 hallucine, gemma3 confabule pire — cf. constat empirique ci-dessous)
2. **Priorité IAO** : extraite et injectée dans FAITS_CALCULÉS (champ `motif.priorite` du data.js)
3. **Mapping TERRAIN_VERS_T2A** : 4 entrées validées sur grep data.js (Consultation externe, Hospitalisation, Sortie après surveillance UHCD, Transfert intra-hospitalier). "Retour structure d'origine" (dossier IPP 25012257, BRUNEL Henri) laissé non mappé → question à poser à Pauline/Amina.
4. **Mise en garde "depuis 23h"** : Option A pure (pas d'avertissement injecté), valeur diagnostique du dry-run préservée
5. **Décision médicale terrain** : extraite dans metadata mais PAS injectée dans FAITS_CALCULÉS (scénario C) → audit indépendant LLM + comparaison serveur a posteriori
6. **Paramètres LLM bench** : Option 2 (temperature=0, seed=42 côté bench uniquement) → vérifier empiriquement si gemma4:31b-cloud respecte le seed
7. **Filet `data_quality_warning`** : flagging incohérences cliniques détectables sans correction (anonymisation data.js statut incertain)
8. **Test négatif** = test de dégradation gracieuse (input dégradé, pas test qui doit échouer)
### Chantier vitesse
- **Bascule γ → δ** : A.1 paste d'abord (gain 94s, effort 30-45min, risque faible) avant B parallélisation (gain révisé 55-65s après mesure honnête). Bascule justifiée par recalcul ratio parallélisme (1,27× et non 2,7× après séparation cold-start)
- **Paste opt-in** : workflows non-Citrix explicitement marqués `paste: true`, fallback char-by-char par défaut
- **Source de vérité parallélisation Ollama** : mesure 2 appels CHAUDS en série vs parallèle, pas comparaison cold-start vs warm (piège initial corrigé)
---
## Constats empiriques importants
### Ollama Cloud sur nomenclatures PMSI françaises (test direct)
- **gemma4:31b-cloud** : connaissance partielle, fausse dans les détails (CCMU/GEMSA confondus avec logique GHM "ressources consommées"), mais reste dans le champ correct (classification individuelle). Le bloc *Thinking* montre une auto-conscience d'incertitude.
- **gemma3:27b-cloud** : **confabulation pure**. Invente l'acronyme GEMSA ("Gestion des Mouvements de Patients et des Stocks"), transforme CCMU en plan blanc niveau crise sanitaire, présente avec assurance maximale et lien officiel inventé. Beaucoup plus dangereux que gemma4 (pas d'auto-doute).
- **Conséquence design** : libellé complet PMSI obligatoire dans FAITS_CALCULÉS quel que soit le modèle.
- **Conséquence choix modèle** : NE PAS envisager gemma3:27b comme fallback gemma4 sur T2A. Pour fallback post-démo : viser modèle médical français spécialisé (DrBERT/edsnlp) ou fine-tuning.
- **Méthode validée** : test Ollama direct 30 sec avant tout pari sur la connaissance d'un modèle, y compris pour changement de modèle (pas seulement initialisation).
### Méthodes sanctuarisées
1. **"Sauvegarde + fork avant chantiers parallèles"** — appliqué ce matin spontanément, a payé toute la journée. À sanctuariser dans CLAUDE.md comme prérequis avant tout chantier multi-branches.
2. **"Instrumenter avant optimiser"** — appliqué 2 fois aujourd'hui, 2 corrections de cap décisives :
- Baseline run 4 a transformé "97% opaque" en goulots chiffrés
- Mesure parallélisme v2 a corrigé un ratio 2,7× → 1,27× et fait basculer l'arbitrage γ → δ
3. **"Test Ollama direct avant pari sur connaissance LLM"** — applicable à toute classification spécialisée (PMSI, CIM-10, CCAM, etc.)
4. **"Mesurer 2 conditions comparables, jamais cold-start vs warm"** — piège classique de mesure de parallélisme corrigé en cours de journée.
5. **`git status` systématique en début de session Claude Code** — incident commit composite (4 fichiers backend + 2 fichiers frontend stagés non liés) corrigé proprement par Option A reset+restore. À sanctuariser pour éviter récidive.
---
## Dette tracée [S3] (à traiter post-démo)
### Critique pour démo 15 mai
- **DETTE [S1] skip ord 13 orchestration** : bug d'orchestration découvert run 8, voir fiche dédiée `_archives/handoff_evidence_20260512_1940/DETTE_S1_skip_ord13_orchestration.md`. Criticité HAUTE. NOT REPRO 100% (runs 2-3 ce matin OK). Investigation : `replay_engine.py` + `api_stream.py`, mécanisme "server side action" et transition serveur → visuel → serveur.
- **DETTE [S2] faux positif click_anchor "Codage"** (run 1 matin) — peut-être lié à S1 ?
- **Robustesse Ollama Cloud en démo** : NON COUVERT. Panne 503 vécue aujourd'hui (gemma4:31b temporarily overloaded). Si panne pendant démo le 15 → fallback nécessaire. Options : modèle local sur RTX 5070 12GB (qualité dégradée Q3/Q2 inacceptable sur gemma4:31b), démo enregistrée en backup, ou autre modèle local équivalent.
### Optimisations post-démo
- **B parallélisation Ollama** : reporté, gain 55-65s sur 381s, à reprendre après stabilisation démo
- **Verify post-click** : trop coûteux (15-25s gaspillés sur 8 clics)
- **Warmup gemma4:31b-cloud** : pour neutraliser cold-start (~40s)
- **Re-test A.1 paste isolé sur gemma4:31b-cloud post-incident** Ollama
- **Crop OCR zone visible utile** : gain 10-20s
### Décision qualité
- **Mapping TERRAIN_VERS_T2A** : libellé "Retour structure d'origine" (dossier 25012257 BRUNEL Henri) à arbitrer cliniquement avec Pauline/Amina
- **Email Pauline/Amina** : à finaliser une fois résultats mini-bench disponibles. Questions candidates :
1. "Retour structure d'origine" → quelle catégorie T2A ?
2. Règle 3/3 ET (doc DIM établissement) vs règle ATIH 2/3 (PROMPT 3 actuel) — confirmation pratique ?
3. Critère 2 (surveillance prolongée) — signaux concrets attendus dans DPI ?
4. Critère 3 (examens/actes) — un acte CCAM suffit, ou acte ET examen ?
5. "Sortie après surveillance UHCD" vs "Hospitalisation" vs "Transfert intra-hospitalier" — tous REQUALIFICATION ?
6. Bandeau UI "à valider manuellement" si LLM confiance "faible" ?
- **CCMU + GEMSA + mode de venue + diagnostic principal** dans FAITS_CALCULÉS : intégré (élargissement Option Élargie pris en cours de journée)
- **Override vérité-terrain dans `_handle_t2a_decision_action`** (finding #1 audit) : à traiter en parallèle de commit 2
### Méthode
- Sanctuariser dans CLAUDE.md : `git status` initial, sauvegarde+fork avant chantiers parallèles, instrumenter avant optimiser, test Ollama direct, mesure comparable
- **Communication Dom ↔ Claude Code × 2 ↔ Claude session principale** : déperdition d'information observée (3 arbitrages décision retransmis 2 fois). Pattern à formaliser : Dom pointe Claude Code vers fichiers de référence rédigés en session principale (brief md) plutôt que paraphraser en chat.
### Asymétrie mémoire Claude session principale ↔ Dom
À traiter en session dédiée (pas en passant) : Dom maintient une mémoire active des patterns de Claude entre sessions, Claude n'a accès qu'aux notes uploadées ponctuellement. Asymétrie connue, signalée à plusieurs reprises aujourd'hui.
---
## Points ouverts à reprendre demain en priorité
### Matin (priorité par ordre)
1. **Investigation bug skip ord 13** (orchestration) — priorité HAUTE. Voir fiche dédiée + trace JSON. Investigation `replay_engine.py` et `api_stream.py`, mécanisme server-side action et transition serveur → visuel → serveur.
2. **Arbitrage push commits décision** (`9872f4510` + `f2212e77e`) — décider si on push avant ou après le bench complet.
3. **Reprise mini-bench décision** sur 11 dossiers POC dès que gemma4:31b-cloud répond (ou décider de basculer sur gemma4 local si panne persiste). Sortie attendue : tableau récap convergence durée + décision sur 10 dossiers utiles.
4. **Commit 2 du brief décision** (garde-fou serveur) après bench validé.
5. **Dry-run E2E Demo_urgence_2 sur MOREL** après commit 2.
### Aprem (si temps)
- Email Pauline/Amina (rédigé sur la base des résultats bench)
- Réécriture éventuelle PROMPT 3 selon résultats convergence
- Investigation faux positif click_anchor "Codage" (run 1 matin) — possiblement même cause que skip ord 13
- B parallélisation si tout le reste est stable
### Décision stratégique en suspens
- **Fallback Ollama Cloud pour démo 15 mai** : à arbitrer. Options à instruire : gemma4 local (qualité quantization à vérifier), qwen3-next:80b (testé qualité aujourd'hui sur run 8), démo enregistrée backup.
---
## Annexes
- Trace bug skip ord 13 : `_archives/handoff_evidence_20260512_1940/replay_fb0c9882_state.json` (129 Ko, 1167 lignes)
- Fiche dette dédiée : `_archives/handoff_evidence_20260512_1940/DETTE_S1_skip_ord13_orchestration.md`
- Synthèse Claude Code journée : `_archives/handoff_evidence_20260512_1940/SYNTHESE_journee_handoff.md`
- Sceau démo matin : tag git `demo-stable-2026-05-12` + tarball 23 Go + SHA256 vérifiés
- Backups DB : chaîne de 4 sur la journée (`backups_db_chain.txt`)
---
**Coupe nette ce soir. Pas d'autre tentative. Le vélo a coupé proprement, ne replonge pas dans le code après 20h.**

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,168 @@
"""Aide à la décision de facturation urgences T2A/PMSI via LLM local.
Décide si un passage aux urgences relève :
- du FORFAIT_URGENCE (passage simple, retour à domicile)
- de la REQUALIFICATION_HOSPITALISATION (séjour MCO, valorisation 1k-5k€+)
Le prompt impose une extraction littérale des faits du DPI (pas d'invention)
et une modulation honnête de la confiance. Validé sur 15 DPI synthétiques :
qwen2.5:7b atteint 100 % d'accuracy en ~5 s/cas avec 4,7 Go VRAM.
Voir docs/clients/ght_sud_95/ et demo/facturation_urgences/RESULTATS.md pour le
bench comparatif des 11 LLMs évalués.
"""
from __future__ import annotations
import json
import logging
import os
import time
import urllib.error
import urllib.request
from typing import Any, Dict
logger = logging.getLogger(__name__)
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434/api/generate")
DEFAULT_MODEL = os.environ.get("T2A_MODEL", "qwen2.5:7b")
DEFAULT_TIMEOUT = 60 # secondes
PROMPT_TEMPLATE = """Tu es médecin DIM (Département d'Information Médicale), expert en facturation T2A/PMSI aux urgences hospitalières en France.
Analyse le dossier patient ci-dessous pour déterminer si le passage relève :
- FORFAIT_URGENCE : passage simple, retour à domicile, sans surveillance prolongée ni soins continus
- REQUALIFICATION_HOSPITALISATION : séjour MCO requis selon les 3 critères PMSI/ATIH
LES 3 CRITÈRES UHCD (au moins 2 sur 3 validés ⇒ REQUALIFICATION) :
1. Pathologie potentiellement évolutive (instabilité hémodynamique, terrain à risque, traitement nécessitant adaptation)
2. Surveillance médicale et paramédicale prolongée (constantes itératives, observations IDE/médecin, durée > 6 h)
3. Examens complémentaires ou actes thérapeutiques (biologie, imagerie, sutures, gestes techniques)
INSTRUCTIONS STRICTES :
1. N'utilise QUE des éléments littéralement présents dans le dossier patient. N'invente AUCUN critère.
2. Pour CHAQUE critère (1, 2, 3), tu DOIS produire un texte de preuve qui contient AU MOINS UNE CITATION LITTÉRALE du dossier entre guillemets français « ... ». Exemple : « FC à 110 bpm, TA 92/60 ».
3. Si le critère est NON validé, ne renvoie JAMAIS un fallback creux : explique factuellement ce qui manque, en citant le dossier (ex: « Sortie à H+2 », « Aucun acte technique au compte-rendu »).
4. Le texte de chaque preuve fait 2-3 phrases : (i) la citation littérale, (ii) l'analyse PMSI, (iii) la conclusion validé/non validé.
5. Calcule la durée totale du passage en heures (admission → sortie/transfert) à partir des horaires du dossier.
6. Module ta confiance honnêtement :
- "elevee" uniquement si tous les indices convergent
- "moyenne" si éléments ambivalents
- "faible" si information manquante ou très atypique
Réponds STRICTEMENT en JSON valide, sans texte avant ni après :
{{
"duree_passage_heures": <nombre>,
"elements_pour_hospitalisation": [<phrases littéralement extraites du dossier>],
"elements_pour_forfait": [<phrases littéralement extraites du dossier>],
"decision": "FORFAIT_URGENCE" | "REQUALIFICATION_HOSPITALISATION",
"decision_court": "UHCD" | "Forfait Urgences",
"preuve_critere1": "<2-3 phrases incluant AU MOINS UNE citation littérale entre « » (motif, symptôme, terrain à risque, traitement). Si non validé : factualise ce qui manque en citant le dossier.>",
"critere1_valide": true | false,
"preuve_critere2": "<2-3 phrases incluant AU MOINS UNE citation littérale entre « » (constantes, observations IDE, durée surveillance). Si non validé : factualise.>",
"critere2_valide": true | false,
"preuve_critere3": "<2-3 phrases incluant AU MOINS UNE citation littérale entre « » (actes/examens : biologie, imagerie, suture, etc.). Si non validé : factualise.>",
"critere3_valide": true | false,
"justification": "<2-3 phrases synthétiques s'appuyant explicitement sur les preuves ci-dessus, avec au moins une citation>",
"confiance": "elevee" | "moyenne" | "faible"
}}
DOSSIER PATIENT :
{dpi}
"""
def analyze_dpi(
dpi_text: str,
model: str = DEFAULT_MODEL,
timeout: int = DEFAULT_TIMEOUT,
ollama_url: str = OLLAMA_URL,
) -> Dict[str, Any]:
"""Soumet un DPI urgences à un LLM Ollama et retourne la décision JSON.
Args:
dpi_text: Texte du dossier patient (concaténation des onglets ou DPI brut).
model: Modèle Ollama à utiliser (default qwen2.5:7b — 100% accuracy bench).
timeout: Timeout HTTP en secondes.
ollama_url: Endpoint Ollama (default localhost:11434/api/generate).
Returns:
Dict avec :
decision: "FORFAIT_URGENCE" | "REQUALIFICATION_HOSPITALISATION"
elements_pour_hospitalisation: List[str]
elements_pour_forfait: List[str]
duree_passage_heures: float
justification: str
confiance: "elevee" | "moyenne" | "faible"
_elapsed_s: float (latence)
_model: str
En cas d'erreur :
{"_error": str, "_elapsed_s": float} (réseau / Ollama indisponible)
{"_parse_error": True, "_raw": str, "_elapsed_s": float} (JSON invalide)
"""
payload = {
"model": model,
"prompt": PROMPT_TEMPLATE.format(dpi=dpi_text),
"stream": False,
"format": "json",
"keep_alive": "5m",
"options": {
"temperature": 0.1,
"num_predict": 1500,
"num_ctx": 16384,
},
}
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
ollama_url,
data=data,
headers={"Content-Type": "application/json"},
method="POST",
)
t0 = time.time()
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
body = json.loads(resp.read().decode("utf-8"))
except (urllib.error.URLError, TimeoutError, ConnectionError) as e:
elapsed = round(time.time() - t0, 1)
logger.warning("analyze_dpi: Ollama indisponible (%s) après %.1fs", e, elapsed)
return {"_error": str(e), "_elapsed_s": elapsed, "_model": model}
elapsed = time.time() - t0
raw_response = body.get("response", "").strip()
raw_thinking = body.get("thinking", "").strip()
candidates = [raw_response]
if not raw_response and raw_thinking:
last_close = raw_thinking.rfind("}")
last_open = raw_thinking.rfind("{", 0, last_close)
if last_open != -1 and last_close != -1:
candidates.append(raw_thinking[last_open:last_close + 1])
parsed = None
for cand in candidates:
cleaned = cand
if cleaned.startswith("```"):
cleaned = cleaned.split("\n", 1)[-1]
if cleaned.endswith("```"):
cleaned = cleaned.rsplit("```", 1)[0]
cleaned = cleaned.strip()
try:
parsed = json.loads(cleaned)
break
except json.JSONDecodeError:
continue
if parsed is None:
return {
"_parse_error": True,
"_raw": (raw_response or raw_thinking)[:500],
"_elapsed_s": round(elapsed, 1),
"_model": model,
}
parsed["_elapsed_s"] = round(elapsed, 1)
parsed["_model"] = model
parsed["_eval_count"] = body.get("eval_count")
return parsed

Binary file not shown.

View File

@@ -0,0 +1,164 @@
# Inventaire anchors — préparation interop Excel
**Date** : 2026-05-13
**Contexte** : Préparation du workflow `Demo_urgence_2_interop` (id `wf_56bf8fa2d332_1778666923`). Ce document inventorie les ancres VWB nécessaires pour remplacer les 5 steps « UI Codage Easily » supprimés par des étapes d'export Excel.
**Source** : table `visual_anchors` de `visual_workflow_builder/backend/instance/workflows.db` (150 ancres au total).
---
## 1. Structure d'un anchor (schéma `visual_anchors`)
```sql
CREATE TABLE visual_anchors (
id VARCHAR(64) NOT NULL PRIMARY KEY,
image_path VARCHAR(512),
thumbnail_path VARCHAR(512),
bbox_x FLOAT,
bbox_y FLOAT,
bbox_width FLOAT,
bbox_height FLOAT,
screen_width INTEGER,
screen_height INTEGER,
description TEXT,
confidence_threshold FLOAT,
created_at DATETIME,
capture_method VARCHAR(64),
target_text TEXT,
ocr_description TEXT
);
```
### Champs et rôle
| Champ | Obligatoire | Rôle |
|---|---|---|
| `id` | ✅ | Format observé : `anchor_<12 hex>_<timestamp Unix s>` (ex. `anchor_c07eeb32f46e_1778497407`) |
| `image_path` | ✅ (de facto) | PNG plein du crop d'ancre. 100 % des 150 ancres l'ont. Chemin absolu vers `visual_workflow_builder/backend/data/anchors/<id>_full.png` |
| `thumbnail_path` | ✅ (de facto) | PNG miniature, 100 % renseigné. Chemin `<id>_thumb.png` dans le même dossier |
| `bbox_x`, `bbox_y`, `bbox_width`, `bbox_height` | ✅ | Coordonnées de la bounding box dans la résolution de référence. Toutes les ancres ont une bbox |
| `screen_width`, `screen_height` | ✅ | Résolution de l'écran sur lequel l'ancre a été enregistrée. Permet la normalisation pour le matching à toute résolution |
| `description` | ⚠️ Optionnel | Description textuelle, souvent « Button labeled "..." ». Vide pour 45/150 ancres |
| `confidence_threshold` | ✅ | Seuil pour template matching. **Valeur unique observée : 0.8** sur les 150 ancres |
| `created_at` | ✅ | Timestamp ISO |
| `capture_method` | ✅ | **Valeur unique observée : `screen_capture`** sur les 150 ancres |
| `target_text` | ⚠️ Optionnel | Texte OCR vu **autour** de l'ancre au moment de la capture. Sert au matching `by_text` quand l'ancre est élargie. Vide pour 45/150 |
| `ocr_description` | ⚠️ Optionnel | Souvent doublon de `description`. Vide pour 45/150 |
### Liaison avec `steps`
```
steps.anchor_id ───▶ visual_anchors.id (FK, NULL-able)
```
Un step `click_anchor` typique réfère son ancre par `anchor_id`, ET embarque aussi une copie compactée dans `parameters_json.visual_anchor` (champs `anchor_id`, `bounding_box`). Le `by_text` est lui dans `parameters_json.by_text` au niveau step (pas dans l'ancre). Voir exemple ord 4 :
```json
{
"visual_anchor": {
"anchor_id": "anchor_c07eeb32f46e_1778497407",
"bounding_box": {"x": 264, "y": 421, "width": 199, "height": 51}
},
"by_text": "Examens cliniques"
}
```
### Distribution de la table
| Critère | Valeurs |
|---|---|
| Total anchors | 150 |
| Fichiers sur disque (`data/anchors/`) | 370 PNGs (150 × 2 = 300 attendus, ~70 orphelins) |
| `capture_method` | 100 % `screen_capture` |
| `confidence_threshold` | 100 % `0.8` |
| Résolution de référence | 2560×1600 : 98 ancres • 1920×1080 : 49 • 320×180 : 3 (atypique) |
| Sans `target_text` | 45 / 150 |
| Sans `description` | 45 / 150 |
---
## 2. Ancres référencées par le workflow source `Demo_urgence_2`
| Ord | action_type | anchor_id | bbox (x,y,w,h) | OCR `target_text` (court) |
|---:|---|---|---|---|
| 0 | extract_table | `anchor_90aab00906b5_1778493250` | (19, 505, 2529, 488) | Tableau liste patients (zone full liste IPP/NOM/Date…) |
| 2 | click_anchor | `anchor_79c26617976d_1778493882` | (30, 548, 101, 30) | `25003284` cellule IPP |
| 4 | click_anchor | `anchor_c07eeb32f46e_1778497407` | (264, 421, 199, 51) | onglet `Examens cliniques` |
| 6 | click_anchor | `anchor_e31b93822caa_1778497559` | (477, 429, 109, 35) | onglet `Imagerie` |
| 8 | click_anchor | `anchor_b8bf39376b8a_1778497633` | (612, 423, 183, 48) | onglet `Notes médicales` |
| 10 | click_anchor | `anchor_e757dbf3f22f_1778497837` | (802, 423, 214, 46) | onglet `Synthèse Urgences` |
| 13 | click_anchor | `anchor_8d4b9cf0207c_1778575835` | (833, 404, 230, 49) | onglet `Codage` |
| 15 | click_anchor | `anchor_8e72328ac1f1_1778575896` | (20, 381, 338, 131) | textarea `Coller ou saisir le dossier patient` |
| 18 | click_anchor | `anchor_de15b10a848b_1778498168` | (1350, 981, 619, 31) | textarea `Justification de la décision` |
**Toutes en référence 2560×1600** (résolution Léa Windows actuelle).
Pattern observé pour un click sur libellé simple (cas ord 4 — `Examens cliniques`) :
- Crop ~200×50 autour du libellé
- `target_text` ≈ contexte OCR voisin (libellés alentours) pour aider le matcher hybride
- `description` ≈ libellé propre (« Button labeled "Examens cliniques" »)
- `confidence_threshold = 0.8`
---
## 3. Ancres existantes pour le besoin interop Excel
Requête effectuée sur `target_text`, `description`, `ocr_description` avec les patterns : `excel`, `.xlsx`, `xls`, `codage_urgence`, `bureau`, `desktop`.
**Résultat : aucune ancre trouvée.**
| Besoin interop | Ancre existante ? | Action requise |
|---|---|---|
| Icône bureau Windows (générique, pour ouvrir l'Explorateur ou poser le fichier sur bureau) | ❌ | **À créer** en enregistrement VWB |
| Cellule Excel (entête ou première cellule de saisie) | ❌ | **À créer** — dépend du gabarit du `.xlsx` |
| Fichier `codage_urgence.xlsx` (icône bureau ou ligne Explorateur) | ❌ | **À créer** — le fichier doit d'abord exister sur le bureau Windows |
| Bouton « Enregistrer » Excel | ❌ | **À créer** si on passe par Ctrl+S, sinon pas nécessaire |
| Onglet/bouton Excel « Fichier » ou « Ouvrir » | ❌ | **À créer** seulement si scénario UI complet (vs server-side openpyxl) |
---
## 4. Procédure de création d'une ancre
### Option A — Côté serveur (recommandée si pure I/O fichier)
Si l'export Excel est réalisé via une action serveur (openpyxl côté Linux, fichier déposé sur partage SMB / synchronisé via OwnCloud), **aucune ancre Léa Windows n'est nécessaire**. Le workflow se termine après `llm_generate justification_t2a` par un step serveur de type à définir (ex. nouveau `export_excel_row` à câbler, ou via `_handle_*` existant).
Avantages :
- Pas d'ancres à enregistrer
- Pas de dépendance à la résolution écran
- Robuste, déterministe
### Option B — Côté UI Léa Windows (si la démo doit montrer Excel ouvert)
Si on veut **visuellement** voir Léa ouvrir Excel et coller les données, il faut enregistrer les ancres via le mode enregistrement VWB sur la machine cible (Windows 192.168.1.11, résolution 2560×1600 cohérente avec les 98 ancres existantes du workflow source).
Étapes type :
1. Préparer le fichier `codage_urgence.xlsx` sur le bureau Windows (gabarit prêt avec entêtes : IPP, Décision, Résumé, Justification, etc.)
2. Lancer VWB en mode enregistrement, viser machine 192.168.1.11
3. Ancres à capturer dans l'ordre du scénario (à valider avec Dom) :
- Ancre 1 : icône `codage_urgence.xlsx` sur le bureau (pour double-clic d'ouverture). **⚠️ rappel : `double_click` non implémenté côté Léa, fallback = 2 click rapprochés ou key_combo `["alt", "f4"]` après ouverture par défaut.**
- Ancre 2 : cellule cible de la première colonne (IPP) — varie selon le gabarit
- Ancre 3 : éventuel bouton « Enregistrer » ou ancre de validation
4. Pour chaque ancre : laisser VWB capturer le crop, vérifier que `target_text`, `description`, bbox sont cohérents avant validation
### Champs minima à fournir lors de la création
| Champ | Valeur cible |
|---|---|
| `id` | auto-généré (`anchor_<hex>_<ts>`) |
| `image_path` + `thumbnail_path` | auto-générés par VWB |
| `bbox_x/y/width/height` | définis par la sélection visuelle de l'utilisateur |
| `screen_width/height` | **2560 × 1600** (cohérence avec ancres existantes) |
| `confidence_threshold` | 0.8 (valeur unique observée dans la table) |
| `capture_method` | `screen_capture` |
| `target_text` | OCR autour de l'ancre — auto-rempli par VWB |
| `description` | optionnel, ex. « Icône fichier codage_urgence.xlsx sur le bureau » |
---
## 5. Récapitulatif
- **Structure d'ancre** : 15 champs, dont **9 obligatoires** (id, image_path, thumbnail_path, 4 bbox, 2 résolutions de réf, confidence, capture_method) et **4 fortement recommandés** (description, target_text, ocr_description, created_at). Référence dans `steps.anchor_id`.
- **Réutilisable directement** : les 8 ancres `click_anchor` du workflow source restent utilisées par `Demo_urgence_2_interop` (anchor_id partagé, FK).
- **À créer** : 0 si **option A** (export Excel server-side via openpyxl) ; **2 à 4 ancres** si **option B** (Léa ouvre et remplit Excel à l'écran).
**Décision attendue de Dom** : choisir entre option A (server-side) ou B (UI Léa) avant de poursuivre la conception des steps Excel.

View File

@@ -0,0 +1,283 @@
# Handoff session 15-16 mai 2026 — Workflow Demo_urgence_3_db + linux_db
**Auteur** : Claude (session du 15 mai 22h jusqu'au 16 mai 2h)
**Objectif** : permettre la reprise propre du travail (même session ou nouvelle session) jusqu'à la démo vidéo "demo 95" prévue **jeudi 21 mai 2026 (J-5)**.
---
## 1. Contexte démo
**Démo "95" / GHT Sud 95 — Paris 21 mai 2026** — format **vidéo enregistrée** (pas live).
Périmètre recentré : **requalification forfait / hospitalisation** uniquement (Demo_urgence_2 v2 Excel abandonnée — Excel inadapté pour textes longs).
**Différenciateurs vidéo** :
- 100 % vision (pas de DOM, pas d'API)
- Multi-OS Windows ↔ Linux (NoMachine)
- Bureau (Word) ET base (DBeaver/SQLite VM Ubuntu)
**Patient démo** : MOREL Catherine, IPP 25003284 (hardcodé partout)
**Date démo** : 15/05/2026 (hardcodée)
**Décision démo** : REQUALIFICATION_HOSPITALISATION
**Somme récupérée** : 1750 € (hardcodée)
---
## 2. Workflows en base
DB : `/home/dom/ai/rpa_vision_v3/visual_workflow_builder/backend/instance/workflows.db`
### Demo_urgence_3_db — `wf_483910cdd851_1778750587` (workflow complet)
40 steps (ord 0-39). Source = Demo_urgence_2_interop (wf_56bf8fa2d332_1778666923, intact).
| ord | action | rôle |
|---|---|---|
| 0-14 | DPI extraction + t2a_decision + 2× llm_generate | Génère `t_*`, `dec`, `resume_patient`, `justification_t2a` |
| 15 | Win+D | Basculer bureau Windows |
| 16-28 | Word | Ouvre template `rapport_T2A_template.docx`, saisit IPP/date/décision/somme via Tab |
| 29 | double_click LINUX_demo.nxs | Ouvre NoMachine vers VM Ubuntu (auto-login configuré par Dom) |
| 30 | type_text "loli" | Mot de passe Linux (skippé si auto-login actif) |
| 31 | Enter | Valider login |
| 32-39 | DBeaver dans VM | Ouvre demo_95 2, console SQL, INSERT, exécute |
**Note** : ord 35 a `by_text="Tables"`, ord 36 a `by_text="demo_95"` (ajoutés en cours de session).
### linux_db — `wf_0786343fb2b7_1778879244` (sous-workflow de test rapide)
**Créé spécifiquement** pour itérer rapidement sur la partie NoMachine + DBeaver sans rejouer le DPI+LLM (5-10 min). Initialement 11 steps = ord 29-39 du Demo_urgence_3_db.
État actuel après nombreuses modifs Dom : **10 ou 11 steps** (à vérifier en début de session). Dom a notamment :
- Supprimé certains steps
- Ajouté un click sur menu "Éditeur SQL" + sous-menu (passage à 3 clics au lieu de Ctrl+Alt+Enter, plus robuste)
- Réordonné via UPDATE SQL (les edges DAG visuelles VWB ne se sauvegardent pas)
**Commande pour voir l'état** :
```bash
sqlite3 /home/dom/ai/rpa_vision_v3/visual_workflow_builder/backend/instance/workflows.db \
"SELECT \"order\" AS ord, action_type, COALESCE(label,'') AS label, json_extract(parameters_json, '\$.visual_anchor.bounding_box') AS bbox, json_extract(parameters_json, '\$.by_text') AS by_text FROM steps WHERE workflow_id='wf_0786343fb2b7_1778879244' ORDER BY \"order\";"
```
---
## 3. Modifs code appliquées (NON COMMITÉES côté Linux)
### `agent_v0/agent_v1/ui/chat_window.py` (Léa Windows)
Trois modifs UX (validées Dom : Q1=oui minimiser sur Annuler, Q2=500ms, Q3=clear visuel uniquement, Q4=bulle accueil effaçable) :
1. **Nouvelle méthode `_clear_chat_history()`** (vers ligne 870) : détruit les widgets enfants de `_msg_frame` à chaque pause. Ne touche pas à `self._messages` (RAM conservée pour debug).
2. **Appel dans `_show_and_render`** : `self._clear_chat_history()` AVANT `_render_paused_bubble(payload)`.
3. **Minimisation 500 ms** sur `_on_paused_resume` ET `_on_paused_abort` : `self._root.after(500, self._do_hide)`.
### `agent_v0/agent_v1/core/executor.py` (Léa Windows)
1. **Délai paste** (ligne 2579) : `time.sleep(0.05)``time.sleep(0.5)` pour laisser NoMachine sync le clipboard Windows → VM Ubuntu. **NE FONCTIONNE TOUJOURS PAS** — clipboard probablement non propagé du tout par NoMachine.
### `agent_v0/server_v1/resolve_engine.py` (serveur Linux)
1. **Constante drift** (ligne 2110) : `_RESOLUTION_MAX_DRIFT: 0.20``0.95`. Permet à l'élément d'être trouvé n'importe où à l'écran (esprit "100 % vision"). Service `rpa-streaming` redémarré, nouveau PID au démarrage (vérifier `systemctl --user status rpa-streaming`).
### Déploiements Windows effectués via SCP
- `chat_window.py``dom@192.168.1.11:C:/rpa_vision/agent_v1/ui/chat_window.py`
- `executor.py``dom@192.168.1.11:C:/rpa_vision/agent_v1/core/executor.py`
- Léa Windows **a été redémarrée** après les SCP (à confirmer pour la session du 16 mai)
**Convention notée** : à chaque modif d'un fichier client Windows, Claude fait le SCP automatiquement (mot de passe SSH `loli` cf. `reference_credentials.md`).
---
## 4. Modifs DB appliquées (workflows.db)
Liste exhaustive des UPDATE de la session :
1. **ord 29 source** (`step_5c81e5bdd20f_1778879250` historique) : `by_text="LINUX_demo"` (ne plus retrouver via faux positifs)
2. **ord 35 source + ord 4 linux_db (anc.)** : `by_text="Tables"`
3. **ord 36 source + ord 5 linux_db (anc.)** : `by_text="demo_95"`
4. **ord 40 source — INSERT SQL** (`step_9cf99c5f2420_1778862183`) : ajout `"paste": true` au parameters_json
5. **ord 5 linux_db (Ctrl+Alt+Enter)** : ajout `delay_before: 1500` ms pour laisser le focus s'établir après le clic demo_95 2 (peut être inutile maintenant que Dom passe par le menu)
6. **Réordonnancement linux_db** : déplacement du clic demo_95 2 (ord 7) à ord 4 + décalage des suivants
7. **Réordonnancement linux_db après ajout menu** : 3 nouveaux clic_anchor menu (ord 8/9/10 → 5/6/7), type_text + Ctrl+Enter décalés à 8/9
8. **Retrait `paste`** sur le step type_text INSERT (paste:true ne fonctionnait pas → fallback frappe char-by-char)
9. **Retrait `by_text="demo_95"`** sur le nouveau click demo_95 2 (ord 4 linux_db) car OCR matchait l'onglet `<demo_95 2> Script-1` en haut au lieu du Navigator à gauche
**Backups disponibles** dans `visual_workflow_builder/backend/instance/workflows.db.backup_*` :
- `..._avant_dup_demo3` (création Demo_urgence_3_db)
- `..._avant_paste_true` (avant UPDATE paste:true)
- `..._avant_bytext_ord29` (avant by_text LINUX_demo)
- `..._avant_bytext_demo95` (avant by_text demo_95 + Tables)
- `..._avant_suppr_vwb31` (avant suppression Dom)
- `..._avant_linux_db` (avant création linux_db)
- `..._avant_reorder_linux_db` (avant 1er réordonnancement)
- `..._avant_reorder_menu` (avant 2e réordonnancement)
---
## 5. Bugs / dette technique identifiés (À TRAITER POST-DÉMO)
### Critiques pour la démo vidéo
1. **paste:true ne fonctionne pas** entre Léa Windows et VM Ubuntu via NoMachine
- Soit `win32clipboard.SetClipboardText()` plante silencieusement
- Soit NoMachine ne propage pas le clipboard Windows → Ubuntu (probable)
- **Investiguer** : vérifier NoMachine settings (Devices → Clipboard → cocher "Both directions")
- **Fallback** : char-by-char (lent mais peut-être suffisant pour 1859 chars)
2. **Clics Léa n'atteignent pas toujours la VM Ubuntu** via NoMachine
- Pattern : Léa clique côté Windows, l'agent reporte `success=True` mais `no_screen_change`
- Chaque clic prend **2 min** côté agent (retries internes silencieux)
- À ce rythme, le replay complet est inutilisable pour vidéo (25+ min)
- **Hypothèse** : focus de fenêtre NoMachine instable, ou mode input non passthrough
- **Workaround temporaire** : Dom intervient en supervision humaine (chaque step → bulle Léa → clic manuel + Continuer)
3. **Faux négatifs détecteur screen_change** : seuil `global=0.002%` considéré "aucun changement" sur les clics de menu qui pourtant ouvrent quelque chose visuellement. Déclenche retries inutiles.
4. **Bug `get_target_memory_store` import** dans `replay_memory.py` :
```
Learning: échec stockage target_memory: cannot import name 'get_target_memory_store'
```
Non bloquant mais empêche l'apprentissage des corrections humaines.
5. **VLM Quick Find exception** : `'int' object has no attribute 'get'` — fallback OK mais à corriger.
### Bugs UX VWB
6. **Bouton "Stop" disparaît** côté VWB UI quand le replay reste actif côté serveur (désynchro). Confondant.
7. **Bouton vert "Exécuter"** appelle un mini-runtime legacy (`real_demo.py`) qui ne supporte que click/type/wait. Pour le replay complet, **toujours cliquer le bouton bleu "→ Windows"**.
8. **DAG edges visuelles** dans VWB ne se sauvegardent pas → l'ordre visuel des flèches ne reflète pas l'ordre réel (basé sur `steps.order`). Confusing pour l'utilisateur.
9. **Bannière Windows 11 "Aucun périphérique audio disponible"** (grise) persistante en haut → VLM la décrit comme "popup, dialog box, confirmation, or erro..." (faux). Pollue les diagnostics.
### Bugs Léa
10. **Démarrage Léa très lent** : 3-6 minutes au lancement (chargement modèles ML probablement). À investiguer.
11. **Capture VWB** : si Léa Windows n'est pas lancée, le fallback `mss` sur Wayland échoue (`XGetImage failed`). Le bouton "Capturer" dans CapturePanel a un fallback local Linux qui ne marche plus sur Wayland natif (Chrome moderne).
---
## 6. État infra runtime (vérifications à faire en début de session)
- **Service `rpa-streaming`** (port 5005) : `systemctl --user status rpa-streaming` → doit être active. Si redémarré entre temps, PID différent du 308418 mentionné.
- **VWB backend** : port 5002, lancé via `app.py` (PID ≈ 140052 au moment du handoff).
- **VWB frontend** : port 3002, Vite/React.
- **Léa Windows** : sur `192.168.1.11`, tray icon visible si lancée. Pour confirmer connexion = polls `/api/v1/traces/stream/replay/next` depuis 192.168.1.11 dans les logs serveur.
- **NoMachine** : configuré avec auto-login Ubuntu activé (skip lock screen). Clipboard partage **probablement non bidirectionnel** — à vérifier.
---
## 7. Reprise demain — checklist proposée
### Étape 0 — Vérification état
```bash
# Vérifier services
systemctl --user status rpa-streaming
ps aux | grep -E "app.py|api_stream" | grep -v grep
# Vérifier état workflows
sqlite3 /home/dom/ai/rpa_vision_v3/visual_workflow_builder/backend/instance/workflows.db \
"SELECT id, name, updated_at FROM workflows WHERE name LIKE 'Demo_urgence_3%' OR name LIKE 'linux_db';"
# Vérifier modifs code non commitées
cd /home/dom/ai/rpa_vision_v3 && git status --short | grep -E "chat_window|executor|resolve_engine"
```
### Étape 1 — Résoudre le bug "clics ne traversent pas NoMachine"
C'est le bloqueur n°1. Sans ça, pas de démo vidéo possible.
**Diagnostic Dom (16 mai 2h)** : « C'est NoMachine côté Windows qui ne répond plus comme il faut. » À investiguer en priorité demain (mise à jour NoMachine récente ? Config qui a sauté ? Process zombi ? Réinstall ?).
**Pistes** :
- Activer le partage clipboard NoMachine bidirectionnel (Settings → Devices → Clipboard)
- Vérifier que la fenêtre NoMachine reste plein écran tout au long
- Tester un autre client display (RDP natif Windows ?)
- Configurer Léa pour focus la fenêtre NoMachine avant chaque action (mécanisme à ajouter dans executor.py ?)
### Étape 2 — Si NoMachine OK : tester linux_db de bout en bout
Cible : run linux_db en ≤ 2 min sans intervention manuelle.
### Étape 3 — Run complet Demo_urgence_3_db
Cible : 1 run propre du DPI au INSERT SQL. Enregistrer cette session pour la vidéo.
### Étape 4 — Post-démo
Traiter la dette listée section 5 dans l'ordre d'importance.
---
## 8. Annexes
### IDs critiques
- **Workflow Demo_urgence_3_db** : `wf_483910cdd851_1778750587`
- **Workflow linux_db** : `wf_0786343fb2b7_1778879244`
- **Workflow source Demo_urgence_2_interop** (intact) : `wf_56bf8fa2d332_1778666923`
- **IPP démo** : `25003284`
- **Step INSERT SQL Demo_urgence_3_db** : `step_9cf99c5f2420_1778862183` (ord 40, paste:true)
- **Service streaming** : `rpa-streaming` user systemd
### Variables runtime produites par le workflow
- `t_extraction_liste` (extract_table ord 0)
- `t_motif_admission`, `t_examen_clinique`, `t_imagerie`, `t_notes_medicales`, `t_synthese_urgences` (extract_text_scroll)
- `dec` (objet) : `dec.decision`, `dec.decision_court`, `dec.preuve_critere1/2/3` (t2a_decision ord 12, model `gemma4:31b-cloud`)
- `resume_patient` (llm_generate ord 13)
- `justification_t2a` (llm_generate ord 14)
### Fichiers source modifiés (non commités)
```
modified: agent_v0/agent_v1/ui/chat_window.py # clear+minimize Léa
modified: agent_v0/agent_v1/core/executor.py # paste delay 0.5s
modified: agent_v0/server_v1/resolve_engine.py # drift 0.95
```
### Schema DB rapide
- Table `steps` : `id, workflow_id, action_type, "order" (réservé), position_x, position_y, parameters_json (JSON), anchor_id (FK), label, created_at, updated_at`
- Table `workflows` : `id, name, description, tags_json, trigger_examples_json, created_at, updated_at, is_active, source, review_status, review_feedback, reviewed_at`
- VWB affiche `order` en 1-indexed (VWB N = DB N-1)
### Commandes utiles
```bash
# Logs streaming (filtrés)
journalctl --user-unit=rpa-streaming --since "10 minutes ago" --no-pager | \
grep -vE "stream/image|/replay/next|GET /health|EDS-NLP" | tail -30
# Backup DB
cp /home/dom/ai/rpa_vision_v3/visual_workflow_builder/backend/instance/workflows.db{,.backup_$(date +%Y-%m-%d_%H%M%S)_label}
# SCP fichier vers Léa Windows
SSHPASS='loli' sshpass -e scp -o StrictHostKeyChecking=no \
/home/dom/ai/rpa_vision_v3/agent_v0/agent_v1/ui/chat_window.py \
dom@192.168.1.11:C:/rpa_vision/agent_v1/ui/chat_window.py
# Forcer reprise replay côté serveur
curl -X POST "http://localhost:5005/api/v1/traces/stream/replay/<replay_id>/resume"
```
### Trois questions Q1/Q2/Q3 Léa chat (déjà tranchées)
- Q1 : minimiser aussi sur Annuler ? → **oui**
- Q2 : délai minimisation ? → **500 ms**
- Q3 : purger `self._messages` RAM ? → **non** (clear visuel uniquement, RAM gardée pour debug)
- Q4 (bulle accueil RGPD) : effaçable au 1er pause ? → **oui** OK
---
## 9. Posture pour la reprise
**Si nouvelle session Claude** : commencer par lire ce handoff EN ENTIER + `MEMORY.md` + `CLAUDE.md`. Ne pas relancer de modifs sans vérifier l'état actuel de la DB et du code.
**Si même session Claude** (ce qui est déjà perdu probablement) : reprendre directement.
**Priorité absolue** : résoudre le bug "clics NoMachine ne traversent pas". Sans ça, la démo vidéo est compromise.
**Démo J-5** : jeudi 21 mai. Marge de manœuvre étroite. Si NoMachine est un blocage durable, envisager un plan B (un seul OS = tout sous Linux ? ou tout sous Windows ?).

View File

@@ -0,0 +1,169 @@
# Handoff session 2026-05-16 matinée — Contournement Ctrl+V NoMachine via ydotool
**Auteur** : Claude (session 10:00 → 15:10 CEST)
**Suite de** : `2026-05-16_handoff_demo3_workflow.md` (handoff du 16 mai 02:04, session 15→16 mai)
**Objectif** : démo vidéo "demo 95" jeudi 21 mai 2026 (J-5)
---
## 1. Problème résolu
Bloqueur n°1 du handoff précédent (« clics ne traversent pas NoMachine ») : partiellement résolu.
- **Clics Léa via NoMachine** : OK (traversent normalement, propagation pynput → SendInput Windows → NoMachine → VM)
- **Key combos avec Ctrl (Ctrl+V, Ctrl+Enter)** : **bloqués par NoMachine en mode "passive grab" par défaut**
Cause root identifiée (agent de recherche web) : NoMachine Player Windows en passive grab intercepte/avale la touche Ctrl quand "Grab the keyboard input" n'est pas explicitement activé. Source : [KB NoMachine AR01P00959](https://kb.nomachine.com/AR01P00959), [TR08R09811](https://www.nomachine.com/TR08R09811).
Solution 1 testée (chercher l'option dans le menu NoMachine 9.5.7) : **option introuvable dans l'UI**.
Solution 2 retenue : **bypasser entièrement NoMachine pour le paste/execute** en injectant les touches *directement dans la VM* via `ydotool` (compatible Wayland natif Ubuntu 26.04).
---
## 2. Architecture finale
```
[Léa Windows] --pynput--> [clics Windows] --NoMachine--> [VM Ubuntu DBeaver]
(clics OK) (focus zone éditeur SQL)
[hôte Linux] --SSH--> [VM Ubuntu]
prepare_clipboard.sh wl-copy + xsel (gardien boucle) → clipboards Wayland + X11 remplis
paste_and_execute.sh ydotool key 29:1 47:1 47:0 29:0 → Ctrl+V dans VM (bypasse NoMachine)
ydotool key 29:1 28:1 28:0 29:0 → Ctrl+Enter dans VM
```
---
## 3. Livrables créés
### Scripts (`~/ai/rpa_vision_v3/scripts/`)
| Fichier | Rôle |
|---|---|
| `payload_insert_morel.sql` | Payload INSERT SQL (1632 bytes, source de vérité) extrait du backup DB |
| `prepare_clipboard_linuxdb.sh` | Pousse payload sur VM, lance gardien boucle qui re-pousse clipboard Wayland + X11 toutes les 0.5s (sinon écrasé par sélection terminal/etc.) |
| `paste_and_execute_linuxdb.sh` | Vérifie ydotoold + gardien, déclenche Ctrl+V puis Ctrl+Enter via ydotool dans la VM |
### Modifications workflows.db (3 backups successifs)
```
workflows.db.backup_2026-05-16_102755_avant_clipboard_relay (avant 1er UPDATE)
workflows.db.backup_2026-05-16_111541_avant_rawkeys (tentative raw_keys, abandonnée)
workflows.db.backup_2026-05-16_150853_avant_suppr_steps_8_9 (avant suppression key_combos)
```
État final workflow `linux_db` (`wf_0786343fb2b7_1778879244`) : **7 steps**, tous des clics
```
0 double_click_anchor LINUX_demo
1 click_anchor
2 click_anchor
3 wait_for_anchor
4 click_anchor
5 click_anchor
6 click_anchor
```
Les ex-steps 7 (Ctrl+V) et 8 (Ctrl+Enter) ont été **supprimés** car remplacés par `paste_and_execute_linuxdb.sh` lancé à la main.
### DB demo_95 (VM)
Nettoyée : 3 rows propres (DURAND, MARTIN, PETIT), AUTOINCREMENT reset à 3. Prochain INSERT MOREL aura id=4.
---
## 4. Dépendances installées dans la VM (Ubuntu 26.04 Wayland)
```
wl-clipboard (wl-copy/wl-paste — clipboard Wayland)
openssh-server (SSH depuis Linux hôte)
xsel (xsel — clipboard X11/XWayland)
x11-xserver-utils (xhost — autoriser connexions X11 locales)
ydotool (1.0.4-3 — injection clavier uinput, Wayland-compatible)
```
Démarrage `ydotoold` daemon (pas de service systemd fourni par le paquet Ubuntu) :
```bash
echo 'loli' | sudo -S sh -c 'nohup setsid ydotoold --socket-path=/tmp/.ydotool_socket --socket-perm=0666 >/var/log/ydotoold.log 2>&1 </dev/null &'
```
(Inclus dans `paste_and_execute_linuxdb.sh` qui le relance si absent.)
---
## 5. À refaire après chaque reboot VM
1. **xhost** (autorise SSH non-interactif à utiliser xsel sur XWayland) :
```bash
sshpass -p loli ssh dom@192.168.122.132 \
"XAUTH=\$(ls /run/user/1000/.mutter-Xwaylandauth.* 2>/dev/null | head -1); \
DISPLAY=:0 XAUTHORITY=\$XAUTH xhost +local:"
```
2. **prepare_clipboard_linuxdb.sh** (relance gardien clipboard + daemon ydotoold via paste_and_execute si nécessaire)
---
## 6. Procédure démo finale
```bash
# AVANT la démo (1 fois, à froid après reboot) :
~/ai/rpa_vision_v3/scripts/prepare_clipboard_linuxdb.sh
# Pour chaque take de la démo :
# 1. Lancer workflow linux_db dans VWB (bouton bleu "→ Windows")
# 2. Attendre que Léa termine les 7 steps (clics visibles dans la VM via NoMachine)
# 3. Dans un terminal Linux hôte :
~/ai/rpa_vision_v3/scripts/paste_and_execute_linuxdb.sh
# → INSERT collé + exécuté instantanément dans DBeaver (visible à l'écran)
```
Vérif post-démo (compter rows) :
```bash
sshpass -p loli ssh dom@192.168.122.132 \
'python3 -c "import sqlite3; c=sqlite3.connect(\"/home/dom/demo_95\"); print(list(c.execute(\"SELECT id,nom_patient,date_creation FROM requalifications_t2a ORDER BY id DESC LIMIT 2\")))"'
```
Reset pour nouveau take :
```bash
sshpass -p loli ssh dom@192.168.122.132 \
'python3 -c "
import sqlite3
c = sqlite3.connect(\"/home/dom/demo_95\")
c.execute(\"DELETE FROM requalifications_t2a WHERE id > 3\")
c.execute(\"UPDATE sqlite_sequence SET seq = 3 WHERE name = '\''requalifications_t2a'\''\")
c.commit()"'
```
---
## 7. Dette technique créée par cette session
1. **Couplage hôte ↔ VM dur** : password `loli` en clair dans les scripts (`SSH_PASS`, `SUDO_PASS`). À remplacer par clé SSH + sudoers NOPASSWD pour ydotoold. Hors scope J-5.
2. **Gardien clipboard boucle 0.5s** : consomme CPU négligeable mais pas propre. À remplacer post-démo par un mécanisme dbus signal ou systemd path unit qui re-pousse seulement sur événement "clipboard cleared".
3. **paste_and_execute lancé à la main** : pas intégré au workflow VWB. Pour intégration auto post-démo, deux options :
- **A** : ajouter à Léa un nouveau action_type `shell_remote` (modif executor.py côté Windows + déploiement SCP)
- **B** : modifier `replay_engine.py` côté serveur pour exécuter localement (Linux hôte) certains step types au lieu de dispatcher à Léa
4. **xhost +local:** : large. À remplacer par xhost SI:localuser:dom post-démo.
5. **AUTOINCREMENT id=9** déjà consommé par les tests : reset à 3 pour la démo, mais à vérifier après chaque take.
---
## 8. État infra runtime
- Serveur `rpa-streaming` : actif (PID 308418 mentionné dans handoff précédent)
- ydotoold daemon : actif côté VM, socket `/tmp/.ydotool_socket` (0666)
- Gardien clipboard : actif côté VM (PID dans `/tmp/clipboard_guard.pid`)
- 3 clipboards remplis :
- Wayland VM (1633 bytes via wl-copy)
- X11 VM (1632 bytes via xsel)
- Windows (1632 bytes via Set-Clipboard — peut nécessiter re-push manuel si vidé)
---
## 9. Sessions du jour (16 mai)
- 06:52 → 07:55 : session reprise courte (fermée par Dom par erreur), test workflow → blocage Ctrl+V
- 10:00 → 15:10 : session diagnostic+fix complet
- Diagnostic clipboard Wayland ≠ X11 (XWayland ne synchronise pas)
- Fix : gardien double-write wl-copy + xsel + xhost +local:
- Diagnostic NoMachine passive grab mange Ctrl
- Fix : bypass via ydotool en SSH dans la VM
- Workflow linux_db simplifié (suppression key_combos)

View File

@@ -0,0 +1,138 @@
# Handoff session 2026-05-17 — instabilité NoMachine + état général démo J-4
**Auteur** : Claude (session 09:30 → ~11:00 CEST)
**Suite de** : `2026-05-16_handoff_ydotool_clipboard.md` (handoff hier soir)
**Objectif** : démo vidéo "demo 95" Paris **jeudi 21 mai 2026 (J-4)**
---
## 1. Diagnostic principal du jour : **NoMachine freeze côté Windows**
**Symptôme** : Léa déplace la souris (mouse_move OK, visible côté VM via NoMachine viewer) mais les **clics ne s'enregistrent pas dans la VM**. Le serveur signale `vlm_and_template_all_failed` au step suivant car l'état visuel attendu (launcher GNOME ouvert, DBeaver visible, etc.) n'est jamais atteint.
**Cause root identifiée par Dom** : **NoMachine viewer Windows freeze** progressivement. Au démarrage frais ça marche, après quelques minutes les inputs synthétiques (mouse click, keystrokes) sont mangés silencieusement. Pattern intermittent.
**Mitigation immédiate démo** :
- Redémarrer NoMachine côté Windows avant chaque take
- Garder NoMachine en plein écran (la fenêtre ne doit pas déborder de l'écran, sinon le grounding Léa crop sur une zone hors-écran et tout casse — vu rect aberrant `(2553, 46) 1705×977` dans les logs)
**Investigation post-démo nécessaire** :
- Version NoMachine actuelle : 9.5.7. Vérifier si update fixe le freeze
- Logs NoMachine côté Windows (mémoire, codec, déconnexions)
- Tester alternatives : RDP natif Windows, TigerVNC, Sunshine/Moonlight
---
## 2. Bug secondaire : `RPA_VLM_MODEL=gemma4:e4b` dans le code Léa
**Localisation** : `C:\rpa_vision\agent_v1\core\executor.py` lignes ~1569, 1700, 2248 :
```python
_vlm_model_popup = os.environ.get("RPA_VLM_MODEL", "gemma4:e4b")
```
Le tag `gemma4:e4b` n'existe pas dans Ollama → `[POPUP-VLM] HTTP 404` à chaque appel popup → la cascade de récupération en cas de visual_resolve fail est cassée → Léa demande directement l'aide humaine au lieu de tenter le fallback popup-detect.
**Fix simple** : exporter `RPA_VLM_MODEL=qwen2.5vl:7b` (ou autre modèle existant) dans l'env Windows de Léa, OU patcher le code Léa pour avoir un default valide.
---
## 3. État infra (en fin de session)
### Côté Linux hôte
- Serveur `rpa-streaming` : restart à 10:48:58 (PID 319324) après reset state interne (workflow `paused_need_help` bloquant)
- VLM model configuré : `qwen2.5vl:7b` (via `.env.local` → override sur `vlm_config.py`)
- Note : qwen2.5vl runtime Ollama = 13 GB → débordement CPU à 100%. Pour `linux_db` simple, le VLM Ollama n'est appelé qu'en fallback (~1.6s avec réponse vide), pas critique. Le grounding principal est InfiGUI-G1-3B Transformers (3.9 GB VRAM, permanent depuis 7 jours).
### Côté VM Ubuntu 26.04 (192.168.122.132)
- `/home/dom/demo_95` : SQLite, **table renommée** `requalifications_t2a`**`requalification_urgence`** ✓
- État table : 3 rows propres (DURAND, MARTIN, PETIT), AUTOINCREMENT à 3 → prochain INSERT = id 4
- `ydotoold` : daemon actif depuis hier (socket `/tmp/.ydotool_socket` perm 0666)
- Gardien clipboard `prepare_clipboard_linuxdb.sh` : potentiellement à relancer après reboot VM
- `xhost +local:` : perdu au reboot, à refaire
### Côté Windows (Léa)
- Léa redémarrée plusieurs fois aujourd'hui (10:00, 10:32)
- **Léa peut crasher silencieusement** (déjà constaté ce matin). Risque pour la démo : prévoir un quick-restart Léa pré-démo et surveiller pendant le take.
---
## 4. Avancées concrètes vs hier
| Élément | Hier (handoff 2026-05-16) | Aujourd'hui |
|---|---|---|
| Table demo_95 | `requalifications_t2a` | `requalification_urgence` (renommée) |
| Payload INSERT | `INSERT INTO requalifications_t2a ...` | `INSERT INTO requalification_urgence ...` |
| Step 8 workflow `linux_db` | `paste_and_execute` server-side | identique ✓ |
| Step 8 workflow `Demo_urgence_3_db` (ord 40) | `INSERT INTO requalifications_t2a` | `INSERT INTO requalification_urgence` (mis à jour) |
| Scripts shell | `paste_and_execute_linuxdb.sh` avec bugs (verif stricte +1 byte, sudo TTY required) | **fixés** : tolérance ±1 byte newline, skip sudo si daemon présent |
| Server-side action `paste_and_execute` | intégrée en code (replay_engine + api_stream) | intégrée + testée avec succès hier |
---
## 5. Backups du jour (workflows.db)
```
workflows.db.backup_2026-05-17_093654_avant_rename_table (avant ALTER TABLE)
workflows.db.backup_2026-05-17_102048_avant_rerecord_linuxdb (jamais utilisé — le re-record n'a pas été fait)
```
---
## 6. Suite de session après reboots serveur + Windows
### Routine de redémarrage à respecter
1. **Linux hôte** : `systemctl --user start rpa-streaming` (vérif `journalctl --user-unit=rpa-streaming -f` pour VLM model log)
2. **VM Ubuntu** :
- Si reboot VM : `sudo ydotoold --socket-path=/tmp/.ydotool_socket --socket-perm=0666 &` (depuis terminal NoMachine, pas SSH)
- Si reboot VM : `xhost +local:` (depuis terminal NoMachine, pas SSH)
- Si reboot VM : `~/ai/rpa_vision_v3/scripts/prepare_clipboard_linuxdb.sh` (relance gardien clipboard)
3. **Windows** : Léa démarre via raccourci ou `C:\rpa_vision\.venv\Scripts\pythonw.exe run_agent_v1.py`
4. **NoMachine Windows** : ouvrir connexion vers VM, mettre en plein écran, NE PAS bouger pendant les replays
### Vérification avant démo
- DBeaver doit être déjà ouvert dans la VM avec la base `demo_95` connectée et la console SQL prête (la conn navigue dans `requalification_urgence`)
- Le workflow `linux_db` part du principe que NoMachine vient juste d'être ouvert mais que **DBeaver n'est PAS encore ouvert** (les steps ouvrent le launcher GNOME, cliquent DBeaver, etc.)
### Pour le take vidéo (J-4)
- Faire 1 warmup run avant le take (cold start VLM ~1m30 sur le 1er resolve)
- Reset la table après le warmup :
```bash
sshpass -p loli ssh dom@192.168.122.132 \
'python3 -c "import sqlite3; c=sqlite3.connect(\"/home/dom/demo_95\"); c.execute(\"DELETE FROM requalification_urgence WHERE id > 3\"); c.execute(\"UPDATE sqlite_sequence SET seq = 3 WHERE name = '\''requalification_urgence'\''\"); c.commit()"'
```
---
## 7. Dette technique (à traiter post-démo)
1. **NoMachine freeze sous Windows** — sujet #1 pour la fiabilité long terme
2. **`RPA_VLM_MODEL=gemma4:e4b` hardcoded dans Léa** (executor.py lignes 1569, 1700, 2248) → patch + SCP
3. **qwen2.5vl:7b runtime 13 GB sur RTX 5070 12 GB** — déborde CPU. Choix VLM stable manquant. Pistes : Holo1.5-7B, MiniCPM-V 2.6, ou désactiver carrément le VLM auxiliaire pour les workflows à anchor templates fiables
4. **Léa peut crasher silencieusement** sur Windows — investiguer logs Windows pour identifier (mémoire ? exception ?)
5. **Couplage hôte ↔ VM dur** : password `loli` en clair dans les scripts. À remplacer par clé SSH + sudoers NOPASSWD post-démo
---
## 8. Files clés à connaître
- `/home/dom/ai/rpa_vision_v3/scripts/prepare_clipboard_linuxdb.sh` (gardien clipboard, lance daemon ydotoold si absent — désormais en mode skip-sudo)
- `/home/dom/ai/rpa_vision_v3/scripts/paste_and_execute_linuxdb.sh` (Ctrl+V + Ctrl+Enter via ydotool, invocable depuis serveur ou à la main)
- `/home/dom/ai/rpa_vision_v3/scripts/payload_insert_morel.sql` (1635 bytes, table `requalification_urgence`)
- `/home/dom/ai/rpa_vision_v3/core/detection/vlm_config.py` (`DEFAULT_VLM_MODEL = "gemma4:latest"` mais env var de `.env.local` met `qwen2.5vl:7b`)
- `/home/dom/ai/rpa_vision_v3/agent_v0/server_v1/replay_engine.py` (`_handle_paste_and_execute_action` au handler)
- `/home/dom/ai/rpa_vision_v3/agent_v0/server_v1/api_stream.py` (`elif type_ == "paste_and_execute"` dans le dispatcher)
## 9. Workflow `linux_db` actuel (en DB)
`wf_0786343fb2b7_1778879244` — 9 steps :
```
0 double_click_anchor LINUX_demo (icône NoMachine sur bureau Windows)
1 click_anchor (bouton Activités GNOME en bas-gauche Ubuntu via NoMachine)
2 click_anchor (icône DBeaver dans le launcher GNOME ouvert)
3 wait_for_anchor attente
4 click_anchor (dans DBeaver)
5 click_anchor (dans DBeaver)
6 click_anchor (dans DBeaver — focus zone éditeur SQL)
7 keyboard_shortcut Alt+F11 (plein écran DBeaver)
8 paste_and_execute server-side (lance scripts/paste_and_execute_linuxdb.sh)
```

View File

@@ -0,0 +1,150 @@
# Handoff 2026-05-18 — Consolidation post-blocages démo J-3
**Session** : 08:00 → ~09:15 CEST
**Démo cible** : "demo 95" Paris **jeudi 21 mai 2026 (J-3)**
**Suite de** : `2026-05-17_handoff_session_nomachine.md`
---
## 1. Trois bugs distincts résolus aujourd'hui
### 1.1 Réseau — Windows → VM NoMachine sur port 4001
**Symptôme** : NoMachine Windows ne joignait pas la VM en 4001.
**Cascade de causes** (trois bloqueurs successifs, chacun nécessaire) :
1. **UFW politique `deny (routed)`** par défaut → DNAT marchait, FORWARD dropait. Fix posé :
```bash
sudo ufw route allow proto tcp from any to 192.168.122.132 port 4000
```
2. **`LIBVIRT_FWI` REJECT par défaut** pour tout NEW depuis l'extérieur vers virbr0 → la règle libvirt ACCEPT de fin de chaîne n'était jamais atteinte. Fix initial volatil :
```bash
sudo iptables -I LIBVIRT_FWI -d 192.168.122.132 -p tcp --dport 4000 -j ACCEPT
```
3. **Persistance LIBVIRT_FWI** : hook libvirt créé pour re-injecter la règle à chaque démarrage du réseau `default` :
- `/etc/libvirt/hooks/network` (chmod 755) — réagit à `default started begin`
- Déclenche : `iptables -I LIBVIRT_FWI 1 -d 192.168.122.132 -p tcp --dport 4000 -j ACCEPT` (idempotent)
**Statut** : 3-way handshake Windows↔VM confirmé par tcpdump live. **Non testé en condition reboot** (à valider au prochain `virsh net-destroy default && virsh net-start default`).
DNAT 4001→4000 lui-même reste géré par le service existant `/etc/systemd/system/nomachine-vm-forward.service` (script `/usr/local/sbin/nomachine-vm-forward.sh`).
### 1.2 Vision — score template_matching effondré (régression apparente)
**Symptôme** : steps `click_anchor` qui passaient à 0.97-0.98 hier donnaient 0.498 (icône Ubuntu) ou 0.757 (Editeur SQL) → cascade VLM/template échoue → `strict_vlm_template_failed`.
**Cause établie par mesure pixel-précise** :
- Le screenshot envoyé par Léa Windows au serveur fait **800×500 px** (downscale par défaut dans `agent_v0/agent_v1/core/executor.py:2895` — `_capture_screenshot_b64(max_width=800, quality=60)`).
- Les ancres sont enregistrées en pixels physiques (ex. 91×90 px sur capture record 2560×1600).
- Ratio capture runtime / capture record = 800/2560 = **0.3125** → ancre 91×90 devient 27×27 dans la capture serveur.
- La liste multi-scale `resolve_engine.py:130` ne descendait qu'à `0.5` → trou entre 0.5 et l'échelle réelle 0.3 → `cv2.matchTemplate` plafonnait à 0.498.
- Vérification chiffrée (script ad hoc sur la capture du fail) : à scale 0.30, score = **0.900** pile, centre détecté au pixel près de la cible attendue.
**Fix posé** : extension de la liste multi-scale dans `resolve_engine.py:130`.
Avant :
```python
for scale in [1.0, 0.9, 1.1, 0.8, 1.2, 0.75, 1.25, 0.6, 1.5, 0.5, 1.75, 2.0]:
```
Après :
```python
for scale in [1.0, 0.95, 0.93, 0.9, 1.05, 1.1, 0.85, 0.8, 1.15, 1.2, 0.75, 1.25, 0.6, 1.5, 0.5, 0.4, 0.35, 0.3, 0.25, 1.75, 2.0]:
```
Backup avant modif : `docs/handoffs/2026-05-18_resolve_engine_avant_scales_fix.py`.
**Validation runtime** : workflow `linux_db` exécuté de bout en bout, tous les steps visuels OK (LINUX_demo ✓, Activités GNOME ✓, DBeaver ✓, demo_95 ✓, Editeur SQL ✓ à 0.948, etc.). Modif **non commitée**.
**Pourquoi la régression est apparue "hier ça marchait"** : la valeur `max_width=800` est ancienne et inchangée. Hypothèse retenue (par élimination, audit serveur+client en agents parallèles) — un changement récent du flux serveur a fait que le screenshot consommé pour le matching est celui downscalé au lieu d'une capture full-res antérieure, OU les 20 tests d'hier étaient sur une configuration où NoMachine cropait différemment et l'écart ne touchait pas le seuil. À investiguer post-démo.
### 1.3 Replay — `paste_and_execute` step 8 (workflow linux_db)
**Symptôme** : `paste_and_execute échoué (rc=1) stderr=ERREUR: ydotoold (socket /tmp/.ydotool_socket) absent dans la VM`.
**Cause** : ydotoold est démarré à la main depuis hier soir (handoff 17 mai §4) — pas de persistance. Au prochain reboot VM (ou kill manuel), le daemon disparaît.
**Fix posé — service systemd persistant** sur la VM :
`/etc/systemd/system/ydotoold.service` (créé, enabled, active) :
```ini
[Unit]
Description=ydotool daemon (input injection pour replay RPA Vision)
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/ydotoold --socket-path=/tmp/.ydotool_socket --socket-perm=0666
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.target
```
Effet : démarre au boot VM, redémarre auto si crash, socket en `/tmp/.ydotool_socket` perm 0666.
**Clipboard gardien** : toujours géré par `scripts/prepare_clipboard_linuxdb.sh` (pas systématisé, à relancer manuellement après reboot VM — sujet ouvert, voir §3).
---
## 2. Dette technique nouvellement identifiée
### Dette A — Client Léa Windows envoie des captures 800×500 au serveur
**Localisation** : `agent_v0/agent_v1/core/executor.py:2895` — défaut `max_width=800` pour `_capture_screenshot_b64`.
**Sites appelants à corriger pour forcer full-res** (7 sites) :
- ligne 633, 801, 824, 894 (screenshot_before), 935, 989, 1055, 1303 (screenshot_after / result["screenshot"]) → tous sans override, donc tous downscalent à 800 px.
- Seules les lignes 1334 et 2147 (resolve_target) passent `max_width=0` (full-res).
**Fix recommandé post-démo** : ajouter `max_width=0, quality=75` sur ces 7 sites d'appel (préserver la flexibilité du défaut pour usages de monitoring live). Puis SCP vers `dom@192.168.1.11` + restart Léa.
**Workaround actuel** : l'élargissement multi-scale côté serveur (§1.2) compense fonctionnellement la dette. À conserver de toute façon.
### Dette B — Bug `'int' object has no attribute 'get'` dans VLM Quick Find
Trace dans les logs du 18 mai 08:35:46 : `VLM Quick Find : exception (20.4s) — 'int' object has no attribute 'get'`. Erreur Python dans le pipeline VLM (pas un crash Ollama). Non bloquant tant que template matching réussit, mais à investiguer.
### Dette C — Persistance du gardien clipboard sur la VM
Le script `prepare_clipboard_linuxdb.sh` doit être relancé à la main après chaque reboot VM. Aujourd'hui : oneshot. Possible amélioration : systemd-user service ou systemd timer. Hors scope J-3.
---
## 3. Routine reboot à jour
**Au boot hôte Linux** :
1. `systemctl --user start rpa-streaming` — manuel ou auto via session
2. `nomachine-vm-forward.service` — auto via systemd (DNAT 4001→4000)
3. `libvirtd` démarre `default` → hook `/etc/libvirt/hooks/network` injecte `LIBVIRT_FWI ACCEPT 4000` (auto)
4. UFW route allow — persistée par UFW
**Au boot VM Ubuntu** :
1. `ydotoold.service` — **auto** (systemd, installé aujourd'hui)
2. `xhost +local:` — manuel, perdu au reboot
3. `prepare_clipboard_linuxdb.sh` — manuel (charge le payload SQL + gardien)
**Côté Windows** :
1. Lancer Léa (`run_agent_v1.py`)
2. Ouvrir NoMachine via icône bureau "LINUX_demo" — pointe vers `192.168.1.40:4001` (DNAT vers VM)
---
## 4. État runtime à la clôture de session
- `rpa-streaming` actif (PID 1641479+), VLM `qwen2.5vl:7b` chargé
- ydotoold actif côté VM (systemd, PID 48926)
- Clipboard VM rempli (1635 bytes payload INSERT MOREL, table `requalification_urgence`)
- Workflow `linux_db` (9 steps) validé E2E aujourd'hui — temps total ~30s
- Workflow `Demo_urgence_3_db` (40 steps) en cours de re-record par Dom au moment du handoff
---
## 5. Action recommandée avant la démo J-3
1. **Commiter** le fix scales `resolve_engine.py:130` (1 ligne) — workflow validé E2E
2. **Tester** un reboot VM pour valider que `ydotoold.service` redémarre bien et que le hook libvirt repose la règle `LIBVIRT_FWI`
3. **Vérifier** que `prepare_clipboard_linuxdb.sh` est ajouté à la procédure pré-démo dans `docs/handoffs/2026-05-17_handoff_session_nomachine.md` §6 (à compléter)
4. **Optionnel** : traiter dette A (`max_width=0` client) si temps avant J-3

File diff suppressed because it is too large Load Diff