docs: add POC specs, handoffs, and research notes

This commit is contained in:
Dom
2026-06-02 16:28:34 +02:00
parent 18ed6cb751
commit f2e9aac6b7
86 changed files with 27615 additions and 25 deletions

View File

@@ -1,8 +1,8 @@
# Audit Complet — RPA Vision V3
**Date** : 4 avril 2026
**Auditeur** : Claude Sonnet 4.6 + 5 agents d'exploration spécialisés
**Périmètre** : Projet complet (code source, tests, sécurité, déploiement, qualité)
**Date** : 4 avril 2026
**Auditeur** : Claude Sonnet 4.6 + 5 agents d'exploration spécialisés
**Périmètre** : Projet complet (code source, tests, sécurité, déploiement, qualité)
**Environnement** : Ubuntu 24.04, Python 3.12.3, NVIDIA RTX 5070 (12 Go VRAM)
---
@@ -34,8 +34,8 @@
RPA Vision V3 est un système d'automatisation RPA 100% basé sur la vision (pas d'accessibilité, pas de sélecteurs DOM). Il utilise CLIP, FAISS, Ollama (VLM local), SomEngine (YOLO + docTR) et le template matching pour identifier et interagir avec les éléments d'interface.
**État** : Phase 0 complète, Phase 1 (streaming agent) en stabilisation.
**Maturité** : Prototype avancé / pré-production.
**État** : Phase 0 complète, Phase 1 (streaming agent) en stabilisation.
**Maturité** : Prototype avancé / pré-production.
**Risque principal** : Tokens de production hardcodés dans le code source.
Le projet est fonctionnel : le replay visuel fonctionne sur Windows, le VWB permet de construire des workflows, le dashboard de monitoring est opérationnel. Cependant, la dette technique s'accumule (fichiers monolithiques, 47 Go de venvs dupliqués, code mort) et des failles de sécurité critiques doivent être corrigées avant toute mise en production.
@@ -387,9 +387,9 @@ filterwarnings = ignore::DeprecationWarning
**Fichier** : `.env.local` (gitignored mais sur disque)
Le fichier contient en clair :
- `ANTHROPIC_API_KEY=sk-ant-api03-...` (clé Anthropic complète)
- `OPENAI_API_KEY=sk-proj-...` (clé OpenAI complète)
- `GOOGLE_API_KEY=AIzaSy...` (clé Google complète)
- `ANTHROPIC_API_KEY=REDACTED` (clé Anthropic complète)
- `OPENAI_API_KEY=REDACTED` (clé OpenAI complète)
- `GOOGLE_API_KEY=REDACTED` (clé Google complète)
- `DEEPSEEK_API_KEY=3d7b...` (clé Deepseek complète)
- `ENCRYPTION_PASSWORD`, `SECRET_KEY`, `RPA_TOKEN_ADMIN`, `AUTOHEAL_ADMIN_TOKEN`, `RPA_API_TOKEN`
@@ -406,8 +406,8 @@ Le fichier contient en clair :
```python
# Temporary fix: Add production tokens directly
prod_admin_token = "73cf0db73f9a5064e79afebba96c85338be65cc2060b9c1d42c3ea5dd7d4e490"
prod_readonly_token = "7eea1de415cc69c02381ce09ff63aeebf3e1d9b476d54aa6730ba9de849e3dc6"
prod_admin_token = "REDACTED"
prod_readonly_token = "REDACTED"
```
Ces tokens **admin** sont dans le code source, visibles dans git. Ils donnent un accès complet à l'API de streaming (port 5005) exposé sur Internet via `lea.labs.laurinebazin.design`.

View File

@@ -0,0 +1,100 @@
# Audit — Point d'intégration agent pour le contrat `finalize` étendu
**Date** : 2026-05-20
**Mission** : Claude 4 (lecture seule)
**Périmètre** : `agent_v0/agent_v1/network/streamer.py`, `agent_v0/agent_v1/main.py`, `agent_v0/agent_v1/ui/smart_tray.py`, `agent_v0/agent_v1/ui/chat_window.py`, `agent_v0/agent_v1/ui/shared_state.py`, `agent_v0/lea_ui/server_client.py`
**Contexte** : le serveur va enrichir la réponse `POST /api/v1/traces/stream/finalize` avec un nouveau contrat `{ replay_ready, replay_request, launch_replay }`. Objectif : identifier le **plus petit point d'intégration** côté agent pour consommer ce contrat proprement, **sans remettre le VWB au centre**.
## Constat
Aujourd'hui, `streamer.py:622-623` reçoit la réponse `/finalize` (`result = resp.json()`) et la jette dans un `logger.info(f"Session finalisée: {result}")`. **Aucun consommateur ne voit ce payload**. Tous les fils en aval (`main.AgentV1.stop_session`, `smart_tray._on_stop_session`, `chat_window._on_quick_stop`) terminent sans rien savoir du résultat serveur. La surface unique de capture du payload existe déjà — il suffit de l'exploiter.
Côté déclenchement replay, **une seule surface client** émet l'appel HTTP : `smart_tray._launch_replay(workflow_id, workflow_name)` (ligne 459-505) qui fait `POST /api/v1/traces/stream/replay/start` avec `{"workflow_id": ...}`. Tout le reste (chat_window, lea_ui/server_client) consomme un replay déjà initié (resume/abort, polling actions). Le nouveau contrat `replay_request` pourrait juste enrichir le payload passé à cet appel existant.
L'orchestration globale (`AgentV1` dans `main.py`) wire déjà session start/stop via `AgentState` (`shared_state.py`). C'est le seul endroit qui voit à la fois le moment précis de la fin d'enregistrement (`stop_session`), l'identité de la session, et a accès à toutes les UI (tray + chat) via `self._state`.
## Tableau — surfaces d'arrêt / relance et recommandations
### Arrêt d'un enregistrement (où voir la fin)
| Fichier | Point d'entrée | Rôle | Risque branchement | Recommandation |
|---|---|---|---|---|
| `streamer.py:602-627` | `_finalize_session()` | Site **unique** qui reçoit `resp.json()` de `/finalize` | Aucun — actuellement payload jeté | 🟢 **POINT D'INTÉGRATION RECOMMANDÉ** — ajouter callback `on_finalize_result: Callable[[dict], None]` invoqué après `resp.ok`. Découplé, non-bloquant, contenu. |
| `streamer.py:131-157` | `TraceStreamer.stop()` | Drain queue + appel `_finalize_session` | Modifier le retour casse l'API publique (appelée par `main.stop_session`) | 🟡 Acceptable mais moins propre — retourner le payload via `stop()` couple les appelants à un retour qui n'existait pas. Préférer le callback. |
| `main.py:395-430` | `AgentV1.stop_session()` | Orchestre captor.stop + sleep + streamer.stop | Lieu naturel pour router le payload reçu — mais doit l'obtenir d'amont | 🟢 Destinataire idéal du callback — a accès à `self._state` pour notifier UI, et `self.user_id` pour reconstituer `agent_<user_id>` session de polling. |
| `shared_state.AgentState.stop_recording()` | Méthode | Source de vérité de l'arrêt UI | Étendre la signature `_on_stop` casse rétrocompat | 🟡 À éviter en première intention — l'état partagé doit rester muet sur le résultat serveur. |
| `smart_tray._on_stop_session()` (ligne 396) | Bouton tray "C'est terminé" | Notif "Merci ! J'ai bien mémorisé..." | Faire du HTTP ici dupliquerait `_launch_replay` | 🔴 À éviter — la couche UI ne doit pas appeler `/finalize` ni interpréter sa réponse. |
| `smart_tray._on_emergency_stop()` (ligne 507) | Bouton ARRÊT D'URGENCE | Stop immédiat sans finalize | Hors scope (arrêt brutal, pas de proposition replay) | ⚫ Aucun branchement — un emergency stop n'attend pas le replay. |
| `chat_window._on_quick_stop()` (ligne 1458) | Bouton chat "Arrêter" | Appelle `shared_state.stop_recording()` | Idem ligne au-dessus | 🔴 À éviter — duplication. |
### Relance d'un replay (où injecter `replay_request` / `launch_replay`)
| Fichier | Point d'entrée | Rôle | Risque branchement | Recommandation |
|---|---|---|---|---|
| `smart_tray._launch_replay(workflow_id, workflow_name)` (ligne 459-505) | Appel HTTP `POST /replay/start` | **Seul site** qui déclenche un replay neuf côté client | Élargir la signature pour accepter un `replay_request: dict` (ou écraser `workflow_id` par tout le dict si fourni) | 🟢 Réutiliser tel quel — passer le `replay_request` du serveur comme JSON body. Surface existante, mature, notification Article 50 déjà cablée ligne 469-472. |
| `smart_tray._make_replay_callback()` (ligne 326) | Wrapping pour menu | Fabrique callbacks pour menu "Mes tâches" | Non touché par ce changement | ⚫ Aucun branchement |
| `main._replay_poll_loop()` (ligne 276-345) | Boucle permanente | **Récepteur** d'actions, pas déclencheur de replay | Auto-déclencher ici = invisible, non conforme Article 14 | 🔴 À éviter formellement — le poll loop reste polymorphique sur la session, ne doit pas démarrer de session. |
| `chat_window._on_paused_resume/_abort` (ligne 1016-1080) | Boutons bulle Pause | Resume/abort d'un replay **déjà en cours** | Hors scope (action sur replay existant) | ⚫ Aucun branchement |
| `lea_ui/server_client.LeaServerClient.start_polling()` (ligne 261) | API alternative | Polling parallèle non utilisé par main (qui a son propre loop) | Confusion : code dormant en partie | 🟡 Ne pas ajouter de logique ici — confirmer d'abord si ce client est encore actif au runtime. |
### Notification / proposition utilisateur post-enregistrement
| Surface | Mécanisme dispo | Utilisation recommandée |
|---|---|---|
| `smart_tray._notifier.notify(title, msg)` (NotificationManager) | Notification système non-bloquante | 🟢 Notif simple "Tâche prête, voulez-vous la tester ?" si `replay_ready=true` mais `launch_replay=false`. |
| `smart_tray._ask_consent(title, msg)` (ligne 74) | Dialog tkinter Yes/No | 🟢 Si le serveur signale `replay_ready=true`, dialog post-enregistrement (réutilise le même pattern que le consentement avant enregistrement). |
| `chat_window` (bulle paused) | Tkinter custom bubble | 🟡 Réutiliser le pattern paused_bubble (ligne 902-1014) pour proposer un "Tester maintenant" / "Plus tard" serait propre mais plus de code. À garder pour une v2 produit. |
| `shared_state` listeners | Notification générique | 🟡 Émettre un nouvel événement type `recording_finalized` peut servir si plusieurs UI doivent réagir — mais ajoute du contrat. |
## Trois options d'intégration
### Option A — Solution minimale (recommandée)
1. `TraceStreamer` reçoit un setter optionnel `set_on_finalize_result(callback: Callable[[dict], None])`. Stocké en attribut.
2. Dans `_finalize_session()` après `resp.ok`, appel non-bloquant `self.on_finalize_result(result)` si défini.
3. `AgentV1.__init__` wire le callback : `self.streamer.set_on_finalize_result(self._on_finalize_result)`.
4. `AgentV1._on_finalize_result(payload)` :
- si `payload.get("launch_replay") is True` ET `replay_request` présent → invoque `self.ui._launch_replay(replay_request)` (avec adaptation signature pour accepter un dict)
- sinon si `payload.get("replay_ready") is True``self.ui._notifier.notify("Léa", "J'ai compris la tâche. Voulez-vous la tester ?")` + ouvre `_ask_consent` (en thread)
- sinon : silencieux
**Volume estimé** : ~15-20 lignes de code dans 2-3 fichiers. Aucun changement de contrat dans `shared_state`, `chat_window`, `lea_ui`. Réutilise `_launch_replay` et `_ask_consent` existants.
**Conforme** : Article 14 (contrôle humain) si dialog `_ask_consent` ; Article 50 (transparence) si `_launch_replay` est utilisé (notification déjà émise ligne 469-472).
### Option B — Solution produit propre (à viser ensuite)
Tout de Option A, plus :
- Ajouter une bulle interactive dans `chat_window` (pattern paused_bubble), au lieu/en plus du dialog tkinter, pour proposer le test avec contexte (nom de la tâche, durée, nombre d'actions).
- Persister la proposition non répondue dans `AgentState` pour que l'utilisateur la retrouve même s'il a fermé la notif.
- Logger côté audit_trail l'événement "test post-enregistrement proposé / accepté / refusé" pour conformité Article 12.
**Volume estimé** : +50-80 lignes, principalement UI chat_window. Plus de couplage shared_state.
### Option C — À éviter
- **Auto-déclencher le replay sans confirmation** depuis le callback finalize, même si `launch_replay=true`. Sans dialog, non conforme Article 14 (l'utilisateur peut ne pas être devant l'écran à ce moment). Si le serveur impose vraiment `launch_replay=true`, l'agent doit quand même afficher un compte à rebours visible (3-5s) avec annulation possible — mais pas exécuter silencieusement.
- **Faire le HTTP `/finalize` depuis `chat_window` ou `smart_tray`** pour lire la réponse. Duplication, l'API serveur est déjà appelée par streamer.
- **Modifier `_replay_poll_loop` pour intercepter une transition "finalize→start replay"**. Couplage faux entre poll permanent et événement ponctuel ; race condition si l'utilisateur enchaîne plusieurs enregistrements.
- **Étendre la signature de `AgentState.stop_recording()`** pour propager le payload. La couche état partagé doit rester muette sur le contenu serveur.
- **Modifier `_finalize_session()` pour retourner la valeur via `streamer.stop()`**. Casse l'API publique de `stop()` (utilisée par `main.stop_session`), oblige à modifier deux endroits, et empêche un fan-out à plusieurs listeners éventuels.
## Conclusion
**Un seul point d'intégration recommandé** : ajouter un callback `on_finalize_result` à `TraceStreamer`, invoqué dans `_finalize_session` après réception de la réponse, wired par `AgentV1` qui dispatche selon `launch_replay` / `replay_ready` vers les surfaces UI déjà existantes (`smart_tray._launch_replay` ou `smart_tray._ask_consent` + `_notifier.notify`).
Ce point :
- **Capture la réponse là où elle existe déjà** (`streamer.py:622`) sans dupliquer d'appel HTTP.
- **Découpe la responsabilité** : streamer = transport, main = orchestration, smart_tray = UI/consentement.
- **Préserve le VWB hors-jeu** : aucun appel ne transite par VWB, le replay est lancé directement par l'agent via l'endpoint `/replay/start` déjà éprouvé.
- **Respecte Article 14** si l'auto-lancement passe par `_ask_consent` ou une notif validable.
- **Coût d'implémentation** : ~15-20 lignes de code, 2-3 fichiers.
Hors périmètre de cette mission : la définition exacte du contenu de `replay_request` (qui touche au workflow_id, à la machine cible, aux paramètres execution_mode). À spécifier côté serveur avant de figer la signature client.
## Méthode d'audit
- Lecture intégrale : `shared_state.py` (190 lignes), `smart_tray.py` (781 lignes), `lea_ui/server_client.py` (375 lignes).
- Lecture déjà réalisée à l'audit Léa-first (2026-05-19) : `streamer.py` (734 lignes), `main.py` (561 lignes).
- Grep ciblé `chat_window.py` (1619 lignes) : `stop|replay|workflow|finalize|launch|on_stop|on_replay` (60 résultats analysés).
- Aucune modification de code, aucune exploration VWB, aucune refonte UI proposée — conformément aux interdits.

View File

@@ -0,0 +1,320 @@
# Audit runtime Léa-first — `capture → replay direct → memory`
**Date** : 2026-05-19
**Auteur** : Claude (mission audit n°1, lecture seule)
**Périmètre** : `agent_v0/agent_v1/`, `agent_v0/server_v1/`, `core/learning/`
**Branche** : `backup/post-demo-2026-05-19` (HEAD `5ea4960e6`)
**Objectif** : identifier 5-10 blocages concrets qui empêchent la voie nominale `capture → replay direct → memory` d'être fiable. **Pas de VWB, pas de démo, pas de bench modèles, pas de refonte large.**
---
## Verdict global
La voie nominale **existe partiellement en code** mais comporte 3 ruptures fonctionnelles (P0) et 4 dégradations silencieuses (P1) qui la rendent **non fiable en pratique**. Le contournement actuel = passer par VWB pour fabriquer un workflow réutilisable, ou par le worker VLM offline. Pas de boucle "Léa capture → Léa rejoue" directe.
État composant par composant :
| Composant | État réel |
|---|---|
| Capture événements client (`captor.py`, `vision/capturer.py`) | mature, production-grade, bug coord critique |
| Streaming vers serveur (`streamer.py`) | mature, robuste (retry, buffer SQLite, backpressure) |
| Accumulation côté serveur (`live_session_manager.py`) | OK, `to_raw_session` câblé au worker VLM |
| Construction workflow depuis session Léa (`workflow_replay.py`) | **ORPHELIN** (0 caller runtime) |
| Replay direct sans VWB | **N'EXISTE PAS** (replay actuel consomme workflow VWB) |
| Memory lookup (resolve_engine + replay_memory) | branché, **gated silencieusement** sur `window_title` |
| Memory record_success / failure | branché, même gating |
| Memory record_human_correction (apprentissage supervisé) | **CASSÉ** (double bug) |
| `core/learning/*` (continuous_learner, feedback_processor, learning_manager) | **NON BRANCHÉ** au runtime serveur Léa-first |
| Observabilité mémoire | **AVEUGLE** (logs only, aucun endpoint) |
---
## P0 — Ruptures (la voie nominale ne marche pas)
### Blocage #1 — `record_human_correction` cassé, double bug
**Fichier** : `agent_v0/server_v1/replay_learner.py:210-219`
**Fonction** : `ReplayLearner.record_human_correction()`
**Bug A — import inexistant** :
```python
from .replay_memory import get_target_memory_store
store = get_target_memory_store()
```
La fonction `get_target_memory_store` **n'existe pas** dans `replay_memory.py`. La vraie s'appelle `get_memory_store` (`replay_memory.py:47`). Le `try/except` à la ligne 224 avale silencieusement l'`ImportError`. **Aucune trace dans les logs au niveau INFO.**
**Bug B — signature obsolète** :
```python
store.record_success(
screen_signature="human_correction",
target_spec=target_spec,
resolved_position={"x_pct": x_pct, "y_pct": y_pct},
method="human_supervised",
score=1.0,
)
```
La vraie signature (`core/learning/target_memory_store.py:212-219`) attend :
```python
def record_success(
self,
screen_signature: str,
target_spec,
fingerprint: TargetFingerprint,
strategy_used: str,
confidence: float,
)
```
Les paramètres `resolved_position`, `method`, `score` **n'existent pas**. `TypeError` garanti si bug A est fixé sans fixer B.
**Impact produit** : l'apprentissage par correction humaine — la boucle "Léa apprend en regardant l'humain corriger" — est **totalement inopérant**. La correction est juste loguée en JSONL local (`record()` ligne 206), jamais hoistée dans la mémoire persistante consultée au prochain run.
**Gravité** : P0
**Catégorie** : bug réel (double)
---
### Blocage #2 — `build_workflow_replay` orphelin (pas de pont capture → replay direct)
**Fichier** : `agent_v0/server_v1/workflow_replay.py:29-186`
**Fonction** : `build_workflow_replay()`
**Constat** :
```bash
$ grep -rn "build_workflow_replay" --include="*.py" | grep -v "workflow_replay.py:"
# (vide)
```
0 caller runtime. Le code décrit pourtant exactement le pont attendu (workflow enrichi → actions de replay avec vérification FAISS par node), mais il n'est appelé **nulle part**.
**Ce qui marche aujourd'hui à la place** :
- `api_stream.py:1479-1525` (`POST /finalize`) → enqueue session au worker VLM (process séparé)
- Le worker construit un workflow via `GraphBuilder` (cf. `stream_processor.py:2306-2335`)
- Mais **rien ne renvoie ces actions à un replay direct**. Le replay (`replay_engine.py`) consomme un workflow VWB (table `steps` DB), pas une séquence construite à partir d'une session Léa.
**Impact produit** : pas de chemin "Léa enregistre → on rejoue la session telle quelle". Toute session Léa doit transiter par VWB ou un commit DB manuel pour devenir rejouable.
**Gravité** : P0
**Catégorie** : branche non branchée (code mort)
---
### Blocage #3 — Memory gated sur `target_spec.window_title` silencieusement inopérante
**Fichiers** :
- `agent_v0/server_v1/resolve_engine.py:1541-1554` (lookup)
- `agent_v0/server_v1/api_stream.py:3634-3639, 3666-3672` (record)
**Bug structurel** : la signature d'écran V4 = `sha256(normalize(window_title))[:16]` (cf. `replay_memory.py:94-103`). Si `target_spec.window_title` est vide ou absent :
```python
def compute_screen_sig(window_title: str) -> str:
norm = _norm_text(window_title)
if not norm:
return "" # → memory_lookup/record skip silencieux
return hashlib.sha256(norm.encode("utf-8")).hexdigest()[:16]
```
**Conséquence runtime** : sur les workflows édités à la main dans VWB ou construits sans renseigner `window_title` (cas dominant aujourd'hui), `screen_sig=""`**ni lookup ni record déclenchés**. Pas de log d'erreur, pas de signal. La mémoire reste vide pendant des semaines sans alerter.
**Validation** : sur le workflow Demo_urgence_3_db, beaucoup de steps ont `target_spec` sans `window_title` (anchors ciblés par `by_text`). Vérifiable rapidement par :
```bash
sqlite3 visual_workflow_builder/backend/instance/workflows.db \
"SELECT json_extract(parameters_json, '$.target_spec.window_title')
FROM steps WHERE workflow_id='wf_483910cdd851_1778750587';"
```
**Impact produit** : la mémoire persistante peut paraître branchée (singleton init OK, JSONL/SQLite créés) et **ne stocker aucune entrée** sur les workflows réels.
**Gravité** : P0
**Catégorie** : dette (gating sur condition fragile)
---
### Blocage #4 — `mss.monitors[1]` aveugle aux dims aberrantes (corruption en amont)
**Fichier** : `agent_v0/agent_v1/vision/capturer.py`
**Sites** : `:107` (`capture_full_context`), `:150` (`capture_dual`), `:247` (`capture_active_window`)
**Code commun** :
```python
with mss.mss() as sct:
monitor = sct.monitors[1]
sct_img = sct.grab(monitor)
img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")
```
**Bug observé en démo (19 mai)** : `mss.monitors[1]` retourne intermittemment `{width: 2560, height: 60}` au lieu de `{width: 2560, height: 1600}` → coords `y_pct × 60 = 16 px` au lieu de `y_pct × 1600 = 424 px`. Aucune défense dans le code.
**Impact produit** : toute capture servant de référence à la mémoire peut être corrompue. Un fingerprint enregistré avec `y_pct = 0.0099` au lieu de `0.265` **empoisonne le store** : au prochain hit, Léa clique à 16 px du haut au lieu du bon endroit. Et le `fail_count` augmente sans que la cause soit visible.
**Fix attendu** (lecture seule, donc juste indiqué) : refuser la capture si `monitor.height < 200` (ou autre seuil sain), fallback sur autre monitor ou nouvelle tentative.
**Gravité** : P0
**Catégorie** : bug réel
---
## P1 — Dégradations silencieuses (la voie marche mais fausse)
### Blocage #5 — Captures Léa downscalées 800×500 envoyées au serveur
**Fichier** : `agent_v0/agent_v1/core/executor.py:2895`
**Défaut** : `_capture_screenshot_b64(max_width=800, quality=60)`
**7 sites d'appel sans override** : `:633, :801, :824, :894, :935, :989, :1055, :1303`. Seuls `:1334` et `:2147` (resolve_target) passent `max_width=0` (full-res).
**Impact produit** :
- Matching template au serveur reçoit du 800×500 → compensé par multi-scale étendu côté serveur (`resolve_engine.py:130`, fix du 18 mai)
- **Mais** les coords stockées dans la mémoire dépendent du chemin de projection (full-res vs downscale). Bruit imprécis sur le store.
**Gravité** : P1 (workaround serveur tient, base mémoire bruitée)
**Catégorie** : dette
---
### Blocage #6 — `_replay_active` flag mal géré pendant les pauses
**Fichier** : `agent_v0/agent_v1/main.py:319-345`
**Code problématique** :
```python
if had_action:
if not self._replay_active:
self._replay_active = True
...
else:
if self._replay_active:
print("[REPLAY] Replay termine — retour en mode capture")
self._replay_active = False
```
**Bug** : si le serveur renvoie `action=null + replay_paused=true` (attente humaine), `had_action=False` → Léa interprète "fin du replay" → cleanup UI + bulle paused n'apparaît plus. Comportement déjà observé en démo (cf. handoff 19 mai bug P1 "Léa client interprète action=null + replay_paused=true comme fin du replay").
**Impact produit** : tracking de replay corrompu côté client pendant les pauses. Désaligne aussi le `ChatWindow` (bulle paused invisible après plusieurs replays).
**Gravité** : P1
**Catégorie** : bug réel
---
### Blocage #7 — `core/learning/*` Phase 7 non branché au runtime serveur Léa
**Fichiers** :
- `core/learning/continuous_learner.py` (644 lignes)
- `core/learning/feedback_processor.py` (176 lignes)
- `core/learning/learning_manager.py` (180 lignes)
- `core/learning/versioned_store.py` (592 lignes)
**Consumers réels** (grep `from core.learning`) :
| Caller | Statut |
|---|---|
| `core/execution/execution_loop.py:49, 71` | runtime alternatif, pas le serveur Léa |
| `core/pipeline/workflow_pipeline.py:29` | pipeline batch GUI legacy |
| `gui/orchestrator.py:52` | GUI PyQt5 legacy |
| `visual_workflow_builder/backend/services/learning_integration.py:36` | service VWB |
| `examples/test_phase7_*.py` | exemples |
| `tests/unit/test_versioned_store.py` | tests |
| `tests/test_correction_pack_integration.py` | tests |
**Le runtime serveur Léa-first** (`api_stream.py` + `replay_engine.py` + `resolve_engine.py` + `stream_processor.py`) n'instancie **rien** de tout ça. Seul `TargetMemoryStore` est consommé via `replay_memory.py`.
**Impact produit** :
- Drift detection (`ContinuousLearner`) = mort en flux Léa-first
- Versioned prototypes (`VersionedStore`) = morts
- Retraitement feedback bus (`FeedbackProcessor`) = mort
- Stats globales (`LearningManager`) = mortes
**Gravité** : P1
**Catégorie** : branche non branchée
---
### Blocage #8 — Pas de signal "session enregistrée → workflow rejouable" exposé côté API
**Fichier** : `agent_v0/server_v1/api_stream.py:1479-1525` (`POST /api/v1/traces/stream/finalize`)
**Constat** : `finalize` marque la session, enqueue au worker VLM (`_enqueue_to_worker(session_id)`), rend la main. **Aucun endpoint** :
- Pour savoir quand le workflow construit est prêt
- Pour le déclencher en replay direct sur une cible (machine, agent)
- Pour récupérer la liste des "workflows construits par Léa" disponibles
La séquence "Léa fais ça → maintenant Léa, rejoue ça" n'a pas de surface API exposée — elle passe **implicitement** par VWB (qui lit les workflows en DB et orchestre).
**Impact produit** : la voie nominale "capture → replay direct" n'a pas de point d'entrée client. C'est cohérent avec le blocage #2 (`build_workflow_replay` orphelin) : le pont produit n'existe pas non plus côté API.
**Gravité** : P1
**Catégorie** : branche non branchée (au niveau orchestration)
---
## P2 — Bruit et observabilité
### Blocage #9 — Deux boucles heartbeat parallèles, persistance non scoped
**Fichier** : `agent_v0/agent_v1/main.py:131, 349-393`
**Constat** : 2 boucles tournent :
- `_heartbeat_loop` (ligne 434) — actif seulement si `self.session_id` (= recording actif)
- `_background_heartbeat_loop` (ligne 349) — actif **en permanence**, pousse sous `bg_<machine_id>` toutes les 5s même sans session
Le serveur persiste ces sessions `bg_*` dans `data/streaming_sessions/`. Pas de purge automatique scoped (la purge générale tourne sur les sessions finalisées > 24h, mais `bg_*` ne se finalise jamais).
**Impact produit** : pollution disque indépendante de l'usage. Croissance non maîtrisée. Bruit dans toute analyse a posteriori des sessions Léa réelles.
**Gravité** : P2
**Catégorie** : dette
---
### Blocage #10 — Aucune métrique runtime sur la mémoire
**Fichiers** :
- `agent_v0/server_v1/replay_memory.py` (`memory_lookup`, `memory_record_*`)
- `core/learning/target_memory_store.py` (`get_stats`)
**Constat** :
- Les hits/misses sont seulement `logger.info` (`replay_memory.py:191-196`).
- `TargetMemoryStore.get_stats()` (`target_memory_store.py:440-479`) renvoie `total_entries, total_successes, total_failures, overall_confidence, jsonl_files_count, jsonl_total_size_mb`**mais n'est branché à aucune route API**.
- Pas de compteur Prometheus, pas d'endpoint `/api/v1/memory/stats`, pas de surface dashboard.
**Impact produit** : impossible de répondre en runtime à "la mémoire travaille-t-elle aujourd'hui ?" ou "combien d'entrées sur ce workflow ?" sans grepper les logs ou ouvrir la DB SQLite à la main. Debugging et validation Léa-first **à l'aveugle**.
**Gravité** : P2
**Catégorie** : dette (observabilité)
---
## Tableau récapitulatif
| # | Sévérité | Catégorie | Fichier:fonction | 1-line |
|---|---|---|---|---|
| 1 | P0 | bug | `replay_learner.py:210` `record_human_correction` | Import inexistant + signature obsolète, apprentissage humain mort |
| 2 | P0 | branche non branchée | `workflow_replay.py:29` `build_workflow_replay` | Orphelin, pas de pont capture→replay direct |
| 3 | P0 | dette | `resolve_engine.py:1541` + `api_stream.py:3634` | Memory gated sur `window_title` souvent absent, silencieusement morte |
| 4 | P0 | bug | `vision/capturer.py:107,150,247` | `mss.monitors[1]` aveugle, base mémoire empoisonnée |
| 5 | P1 | dette | `executor.py:2895` (7 sites) | Captures 800×500 par défaut, store bruité |
| 6 | P1 | bug | `main.py:319-345` `_replay_poll_loop` | `_replay_active` mal géré pendant pause, état UI désynchro |
| 7 | P1 | branche non branchée | `core/learning/*` | Phase 7 non branchée au runtime serveur |
| 8 | P1 | branche non branchée | `api_stream.py:1479` `/finalize` | Pas d'API "rejoue ce que tu viens d'enregistrer" |
| 9 | P2 | dette | `main.py:131,349` | Heartbeat background pollue la persistance |
| 10 | P2 | dette | `replay_memory.py` + `target_memory_store.py:440` | Aucune métrique runtime mémoire exposée |
---
## Recommandation de séquencement (si on devait choisir 4 fixes)
Pour rendre la voie nominale `capture → replay direct → memory` opérationnelle avec un effort minimal :
1. **#4** d'abord — fixer `mss.monitors[1]` aveugle. Sinon tout ce qu'on stocke après est faux.
2. **#3** ensuite — exiger ou dériver `window_title` dans le `target_spec` à l'enregistrement Léa (la capture client a déjà cette info via `window_capture.title`, à propager). Sans ça, la mémoire reste vide.
3. **#1** — corriger `record_human_correction` (import + signature). Ouvre la boucle d'apprentissage supervisé.
4. **#2** + **#8** ensemble — soit rebrancher `build_workflow_replay` au worker VLM et exposer un endpoint client, soit assumer que VWB reste l'orchestrateur intermédiaire. Décision produit à arbitrer.
**Pas dans le périmètre de cette mission** : proposer le design des fixes (la mission demandait l'audit, pas la refonte).
---
## Méthode d'audit
- Lectures intégrales : `core/learning/__init__.py`, `core/learning/target_memory_store.py`, `replay_memory.py`, `replay_learner.py`, `live_session_manager.py`, `workflow_replay.py`, `core/captor.py`, `vision/capturer.py`, `network/streamer.py`, `main.py`
- Lectures ciblées : `api_stream.py:1479-1525, 3600-3690`, `stream_processor.py:1700-1745, 2285-2345`, `resolve_engine.py:1525-1565`
- Grep consumers : `build_workflow_replay`, `memory_lookup`, `memory_record_*`, `ReplayLearner`, `record_human_correction`, `to_raw_session`, `TargetMemoryStore(`, `ShadowLearningHook`, `from core.learning`
- Croisement avec : handoffs 12-19 mai, `DETTE_TECHNIQUE.md`, `AUDIT_CONTROLES_DEBRANCHES_2026-05-08.md`

View File

@@ -0,0 +1,109 @@
# Audit — Perte de `window_title` dans le pipeline mémoire
**Date** : 2026-05-19
**Mission** : Claude 3 (lecture seule)
**Périmètre** : `agent_v0/server_v1/stream_processor.py`, `api_stream.py`, `replay_engine.py`, `workflow_replay.py`, `replay_memory.py`
**Branche** : `backup/post-demo-2026-05-19` (HEAD `5ea4960e6`)
**Référence** : blocage P0 #3 de `docs/AUDIT_LEA_FIRST_RUNTIME_2026-05-19.md`
## Constat
La mémoire persistante (`TargetMemoryStore`) repose sur une signature d'écran `sha256(window_title)` (cf. `replay_memory.py:94-103`). Sans `window_title`, la signature est vide → ni `memory_lookup` ni `memory_record_*` ne se déclenchent. Le code utilise `try/except` permissif et un `if _window_title` silencieux — aucun signal n'est émis quand la mémoire est skip.
L'audit identifie **deux problèmes simultanés** :
1. **Asymétrie écriture/lecture** : plusieurs chemins de production écrivent `window_title` sur l'action *top-level* (`action["window_title"]` ou `action["expected_window_before"]`), mais la lecture mémoire (`api_stream.py:3634-3639`) cherche **uniquement dans `target_spec`**. Conséquence : la fallback `or _mem_target_spec.get("expected_window_before", "")` ne peut jamais réussir car ce champ n'est posé que sur l'action top-level.
2. **Producteurs incomplets** : plusieurs constructeurs de `target_spec` n'injectent pas `window_title` même quand l'information est disponible dans le contexte.
Résultat net : sur le **chemin Léa-first natif** (capture → workflow construit par `build_replay_from_raw_events`), la mémoire ne se déclenche jamais bien que `window_title` soit présent sur l'action.
## Tableau — cartographie des chemins
### Producteurs `target_spec` / actions click
| Fichier | Fonction / site | Chemin | `window_title` dans target_spec | Impact mémoire |
|---|---|---|---|---|
| `stream_processor.py:1532-1601` | `build_replay_from_raw_events` (raw events Léa → actions) | **Léa-first natif** | **JAMAIS posé** dans target_spec — écrit top-level ligne 1545 | 🔴 lookup + record **toujours skip** |
| `stream_processor.py:1590-1601` | branche enrich post-Léa (anchor + window_capture) | Léa-first natif | non — propage `enrichment` (by_text, anchor) + `window_capture.rect`, jamais `window_title` | 🔴 même chemin que 1545 |
| `stream_processor.py:4396-4443` | `_create_edge_action` (worker VLM offline, GraphBuilder edges) | **workflow construit hors session** | OK ligne 4402 : `if window_title: target_spec['window_title'] = window_title` | 🟢 mémoire active si node metadata contient `window_title` |
| `replay_engine.py:534-548` | `_generate_setup_env_actions` clic Démarrer | **replay-session bootstrap** | **AUCUN window_title** posé (légitime : fenêtre Bureau Windows) | 🟡 dette assumée — clic système |
| `replay_engine.py:563-578` | `_generate_setup_env_actions` clic Rechercher | replay-session bootstrap | **AUCUN window_title** posé (légitime : menu Démarrer) | 🟡 dette assumée |
| `replay_engine.py:611-625` | `_generate_setup_env_actions` clic résultat app | replay-session bootstrap | **AUCUN window_title** posé (résultats recherche volatils) | 🟡 dette assumée |
| `replay_engine.py:966-979` | `_normalize_action` (action depuis objet Target) | normalisation chemin Target API | by_role, by_text, context_hints posés — **PAS window_title** | 🔴 lookup + record skip |
| `replay_engine.py:1804-1807` | `_create_replay_state` slim copy | tous chemins (post-construction) | conserve si présent ; strip uniquement `anchor_image_base64` | 🟢 transparent |
| `workflow_replay.py:119-128` | `build_workflow_replay` (orphelin) | **branche non branchée** | OK ligne 123 : `"window_title": node_title` posé correctement | ⚫ code mort, 0 caller runtime |
### Lecteurs `window_title` (sites memory)
| Fichier | Fonction / site | Cherche dans | Statut |
|---|---|---|---|
| `replay_memory.py:142-206` | `memory_lookup` | `target_spec.get("window_title", "")` | 🔴 silencieusement skip si vide |
| `api_stream.py:3634-3639` | memory_record_success/failure source | `_mem_target_spec.get("window_title", "")` puis `_mem_target_spec.get("expected_window_before", "")` | 🔴 deuxième fallback **inopérante**`expected_window_before` n'est jamais dans `target_spec` |
| `resolve_engine.py:1541` | déclenche memory_lookup | `target_spec.get("window_title", "")` | 🔴 propagation du silence |
| `api_stream.py:3278-3281` | log REPLAY (observabilité) | `action.get("expected_window_before") or _tspec.get("window_title", "")` | 🟢 **correct** — preuve que les 2 endroits existent et qu'au moins ce code le sait |
| `api_stream.py:3599` | audit_trail `target_app` | `_target_spec.get("window_title", "")` | 🔴 audit incomplet sur chemins où window_title est top-level |
| `stream_processor.py:1149, 1176` | `_enrich_actions_with_intentions` (user-facing) | `action.get("target_spec", {}).get("window_title", "")` | 🟡 affiche `'?'` ou `'inconnue'` quand absent |
### Producteurs top-level `action["window_title"]` ou `expected_window_before`
| Fichier | Site | Champ posé | Présent dans target_spec ? |
|---|---|---|---|
| `stream_processor.py:1545` | `build_replay_from_raw_events` | `action["window_title"] = window["title"]` | **non** |
| `replay_engine.py:1797-1798` | `_create_replay_state` slim copy | `a_copy["expected_window_before"]`, `a_copy["expected_window_title"]` | **non** |
## Cas critiques (chemins où la mémoire skip silencieusement)
### Cas A — Session Léa fraîchement enregistrée, workflow direct
**Trigger** : utilisateur enregistre une session, `finalize` enqueue, `build_replay_from_raw_events` produit les actions.
**Chemin** : `stream_processor.py:1532-1601`.
**Symptôme** : `action["window_title"]` est posé au top-level (ligne 1545), mais le `target_spec` (lignes 1590-1601) ne contient que `enrichment + window_capture`. Au replay, `memory_record_success` lit `_mem_target_spec.get("window_title", "")` → vide → `compute_screen_sig``""` → skip silencieux.
**Conséquence produit** : la voie nominale Léa-first n'alimente jamais la mémoire. Aucune leçon stockée.
### Cas B — Action via API Target normalisé
**Trigger** : un client appelle une API qui passe par `_normalize_action` (replay_engine.py:966).
**Chemin** : `replay_engine.py:966-979`.
**Symptôme** : target_spec construit avec `by_role`, `by_text`, `context_hints` uniquement. `window_title` jamais posé même si disponible dans le contexte appelant.
**Conséquence produit** : tout client qui passe par cette API perd la mémoire.
### Cas C — Workflow `setup_env` autogénéré (ouvrir une app via Démarrer)
**Trigger** : un workflow démarre par ouvrir une app, génération automatique de 3 clics (Démarrer → Recherche → Résultat).
**Chemin** : `replay_engine.py:534-625`.
**Symptôme** : aucun des 3 clics n'a `window_title` dans target_spec. C'est intentionnel (fenêtres système volatiles), mais la mémoire ne s'active pas non plus sur ces clics.
**Conséquence produit** : ces 3 clics ne bénéficieront jamais de l'apprentissage par répétition, alors qu'ils sont parmi les plus stables visuellement (bouton Démarrer toujours en bas-gauche).
### Cas D — Fallback `expected_window_before` codée mais inopérante
**Trigger** : action a `expected_window_before` posé top-level (par `_create_replay_state` ou un workflow VWB qui le renseigne).
**Chemin** : `api_stream.py:3634-3639`.
**Symptôme** : le code tente le fallback `_mem_target_spec.get("expected_window_before", "")`. Mais `_mem_target_spec` est l'objet `target_spec` ; `expected_window_before` n'est jamais dans target_spec, il est sur action top-level (cf. `replay_engine.py:1797`). La fallback est **toujours vide**.
**Conséquence produit** : même quand l'information existe au top-level de l'action, le memory_record ne la voit pas.
**Preuve qu'il existait une intention de cohérence** : `api_stream.py:3278-3281` (log REPLAY) lit correctement `action.get("expected_window_before") or _tspec.get("window_title")`. Le code mémoire a copié la mauvaise moitié de l'expression.
### Cas E — Workflow VWB édité à la main sans `window_title`
**Trigger** : workflow construit/édité dans VWB (table `steps`), `target_spec.window_title` souvent omis.
**Chemin** : consommé tel quel par `_create_replay_state`.
**Symptôme** : aucun warning, aucune erreur, aucun signal. La mémoire reste vide pour ce workflow.
**Conséquence produit** : sur Demo_urgence_3_db (46 steps), à vérifier combien de steps ont effectivement `target_spec.window_title` non vide — probablement minoritaire.
## Conclusion
**Bug réel (P0)** :
- **Cas D** — `api_stream.py:3634-3639` : la fallback `or _mem_target_spec.get("expected_window_before", "")` cherche dans `target_spec` au lieu de l'action top-level. Code mort par erreur de copy-paste depuis `api_stream.py:3278-3281` qui faisait correctement `action.get(...) or _tspec.get(...)`. Une ligne à corriger une fois la décision produit prise.
- **Cas A** — `stream_processor.py:1545` vs `:1590-1601` : asymétrie de contrat dans le même fichier, sur le chemin Léa-first nominal. `window_title` est connu (posé top-level) mais non propagé dans `target_spec` où la mémoire le cherche.
**Dette de contrat** (P1) :
- **Cas B** — `replay_engine.py:966-979` : `_normalize_action` n'inclut pas `window_title` dans `target_spec`. Contrat implicite "tous les producteurs de `target_spec` doivent injecter `window_title` quand disponible" non documenté ni appliqué.
- **Cas E** — workflows VWB sans `window_title` : pas de validation côté serveur, pas de warning. La forme du contrat n'est jamais vérifiée à la création.
- **Cas C** — clics `setup_env` : exclusion légitime mais devrait être documentée. Une mémoire "setup_env" pourrait utiliser une signature d'écran différente (cf. `replay_learner.py:213` qui utilise `"human_correction"` comme signature constante — pattern réutilisable).
**Branche non branchée** :
- `workflow_replay.py:119-128` : code mort qui pose pourtant `window_title` correctement. Cohérent avec son orphelinat global (blocage P0 #2 de l'audit Léa-first). Pas de valeur tant qu'il n'est pas câblé.
**Synthèse 1-ligne** : la mémoire est branchée mais le contrat `window_title in target_spec` n'est respecté que par le chemin GraphBuilder (worker VLM offline) ; le chemin Léa-first nominal et la normalisation API perdent l'info, et la fallback prévue pour rattraper est inopérante par bug de lecture. Le résultat observable : `TargetMemoryStore` reste vide sur les sessions Léa réelles.
## Méthode d'audit
- Grep cibles : `target_spec.*=`, `"window_title"`, `window_title=`, `"type": "click"` sur les 5 fichiers du périmètre.
- Lectures ciblées : `stream_processor.py:1140-1180, 1530-1605, 4380-4445`, `replay_engine.py:525-625, 955-980, 1780-1810`, `api_stream.py:3270-3290, 3540-3670`, `workflow_replay.py` (intégral), `replay_memory.py` (intégral).
- Croisement écritures/lectures pour identifier les asymétries.
- **Pas de modification de code, pas d'exploration VWB, pas de proposition de refonte** — conformément aux interdits de la mission.

View File

@@ -0,0 +1,132 @@
# CR — Audit `paused_bubble: bus déconnecté, resume non émis` + fallback HTTP
**Date** : 2026-05-22
**Branche** : `backup/post-demo-2026-05-19`
**Périmètre** : agent-side uniquement (`agent_v0/agent_v1/**` + `agent_v0/lea_ui/**`). `agent_v0/server_v1/replay_engine.py` non touché.
**Statut** : patch + tests implémentés et verts (19 tests neufs + 1 test intégration trim).
---
## 1. Cause exacte la plus probable
Le bouton **Continuer** de la bulle paused suit un chemin **unique**, sans fallback :
1. `ChatWindow._on_paused_resume(replay_id)` (`agent_v0/agent_v1/ui/chat_window.py:1016`) teste `self._bus is not None and self._bus.connected`.
2. Si vrai → `self._bus.resume_replay(replay_id)``FeedbackBusClient._safe_emit("lea:replay_resume", …)` (`agent_v0/agent_v1/network/feedback_bus.py:135`).
3. `_safe_emit` re-vérifie `self._sio.connected`, sinon retourne `False` (`feedback_bus.py:141-149`).
4. Côté serveur, c'est `agent_chat` (port 5004, SocketIO) qui relaie en HTTP `POST /api/v1/traces/stream/replay/{id}/resume` vers le serveur streaming (port 5005).
**Le bug** : si le bus SocketIO est tombé (network blip, `agent_chat` redémarré, `LEA_FEEDBACK_BUS=0`, ou socket cassé entre `connect()` et le `emit`), le clic est *perdu* :
- log `paused_bubble: bus déconnecté, resume non émis` (`chat_window.py:1036`)
- boutons **disabled** (`_disable_paused_buttons`)
- fenêtre **minimisée** 500 ms plus tard (`self._root.after(500, self._do_hide)`)
- UX affiche « ⚠ Bus indisponible — réessayez dans 5 s » mais l'utilisateur **ne peut pas** réessayer (boutons figés + fenêtre cachée)
- côté serveur : le replay reste `paused_need_help` jusqu'à expiration / cancel manuel
L'endpoint HTTP qui ferait le job existe pourtant déjà côté serveur (`api_stream.py:4333` `POST /replay/{id}/resume` et `:4443` `/cancel`) — il n'est juste pas appelé directement par l'agent quand le bus est down.
**Confiance haute** : la chaîne du chemin nominal et le défaut de fallback ont été tracés ligne par ligne ; le log exact correspond bien à ce branchement.
## 2. Fichiers / fonctions concernés
| Fichier | Fonctions clés |
|---|---|
| `agent_v0/agent_v1/ui/chat_window.py` | `_on_paused_resume:1016`, `_on_paused_abort:1044`, `_disable_paused_buttons:1071` |
| `agent_v0/agent_v1/network/feedback_bus.py` | `FeedbackBusClient.resume_replay:130`, `abort_replay:137`, `_safe_emit:141`, `connected:122` |
| `agent_v0/agent_v1/main.py` | wiring `chat_window._bus` (start/stop dans `_start_chat`, fenêtre `start_session`) |
| `agent_v0/lea_ui/server_client.py` | `_auth_headers:114`, `_stream_url`, base requests existante (resume/abort absents avant ce patch) |
| `agent_v0/server_v1/api_stream.py` (référence, non modifié) | `/replay/{id}/resume:4333`, `/replay/{id}/cancel:4443` |
## 3. Patch minimal recommandé (implémenté)
**Choix** : ajouter un **fallback HTTP direct** côté agent vers `/replay/{id}/resume` et `/replay/{id}/cancel`, déclenché quand le bus SocketIO est down ou que l'emit échoue. En cas d'échec sur les deux canaux, ne PAS désactiver les boutons et ne PAS auto-hide la fenêtre → l'utilisateur peut réessayer.
Pas de queue persistante, pas de retry automatique : minimum viable, déterministe, traçable dans les logs (`channel=bus` vs `channel=http` vs aucun).
### Changements de code
**`agent_v0/lea_ui/server_client.py`** — ajout de deux méthodes HTTP symétriques au flux SocketIO :
- `resume_replay(replay_id) -> bool` : POST `/traces/stream/replay/{id}/resume`, retourne `resp.ok`.
- `abort_replay(replay_id) -> bool` : POST `/traces/stream/replay/{id}/cancel`, retourne `resp.ok`.
- Toutes deux : guard `replay_id` vide, lazy import `requests`, try/except → False sur exception, `_auth_headers()` pour le Bearer.
**`agent_v0/agent_v1/ui/chat_window.py`** — refactor de la décision d'envoi :
- Nouveau helper `_dispatch_paused_action(replay_id, bus_method, client_method) -> (emitted, channel)` qui essaie bus puis HTTP fallback. Retourne le canal utilisé pour le log (`"bus"` / `"http"` / `""`).
- `_on_paused_resume` et `_on_paused_abort` utilisent ce helper. En cas d'échec sur les deux canaux :
- feedback UI : « ⚠ Serveur injoignable — réessayez »
- `_enable_paused_buttons()` (nouveau) réactive les deux boutons
- **pas** de `_root.after(500, self._do_hide)` (pas d'auto-hide)
- log warning `paused_bubble: bus et HTTP indisponibles, resume non émis pour <id>`
- En cas de succès : feedback « → Reprise demandée… » avec mention du canal dans le log (`replay_resume émis pour <id> via bus|http`).
Aucun changement de signature publique ; aucun touchage côté `agent_v0/server_v1/`.
## 4. Tests ajoutés
| Fichier | Tests | Bilan |
|---|---|---|
| `tests/unit/test_server_client_replay_controls.py` | 10 tests (`resume_replay` × 5 + `abort_replay` × 5) : succès, échec serveur, replay_id vide, exception réseau, URL & header auth | ✅ 10/10 |
| `tests/unit/test_chat_window_paused_dispatch.py` | 9 tests sur `_dispatch_paused_action` en isolation Tkinter (bus OK, bus down, bus emit False, bus raise, no bus, all-fail, no-client, méthode absente, abort symétrique) | ✅ 9/9 |
| `tests/integration/test_replay_session_trim_neutral.py` | 1 test bout-en-bout `_extract_required_apps → _generate_setup_actions → _trim_redundant_setup_events → build_replay_from_raw_events` sur fixture reproduisant `sess_20260520T102916_066851` — vérifie que la première action utile post-setup est `type 'test'`, pas un click `by_text="Sans titre"` | ✅ 1/1 |
Total **20 tests neufs**, **79 tests verts** sur le périmètre (les 59 existants `test_env_setup.py` n'ont pas régressé).
### Commandes de validation
```bash
cd /home/dom/ai/rpa_vision_v3
source .venv/bin/activate
set -a && source .env.local && set +a
python -m pytest tests/unit/test_server_client_replay_controls.py -v
python -m pytest tests/unit/test_chat_window_paused_dispatch.py -v
python -m pytest tests/integration/test_replay_session_trim_neutral.py -v
python -m pytest tests/unit/test_env_setup.py tests/unit/test_server_client_replay_controls.py tests/unit/test_chat_window_paused_dispatch.py tests/integration/test_replay_session_trim_neutral.py
```
## 5. Fichiers modifiés
| Fichier | Nature | SCP Windows requis |
|---|---|---|
| `agent_v0/lea_ui/server_client.py` | Ajout `resume_replay` + `abort_replay` (~45 lignes) | Oui → `dom@192.168.1.11:C:/rpa_vision/lea_ui/server_client.py` |
| `agent_v0/agent_v1/ui/chat_window.py` | Refactor `_on_paused_resume`, `_on_paused_abort` ; ajout `_dispatch_paused_action`, `_enable_paused_buttons` (~110 lignes touchées) | Oui → `dom@192.168.1.11:C:/rpa_vision/agent_v1/ui/chat_window.py` |
| `tests/unit/test_server_client_replay_controls.py` | NEW (109 lignes) | Non |
| `tests/unit/test_chat_window_paused_dispatch.py` | NEW (115 lignes) | Non |
| `tests/integration/test_replay_session_trim_neutral.py` | NEW (130 lignes) | Non |
⚠️ Le miroir `agent_v0/deploy/windows_client/lea_ui/server_client.py` est obsolète (setup initial, pas l'incrémental — cf. handoff 2026-05-20). Le canal réel reste le SCP manuel direct vers `C:/rpa_vision/`.
## 6. Risques et limites
- **Pas de queue persistante** : si l'utilisateur clique Continuer pendant un blackout réseau total (bus + HTTP indisponibles), le clic n'est pas mis en attente. Le patch garantit juste qu'il pourra réessayer (boutons restent actifs, pas d'auto-hide). Une vraie queue serait une refacto, hors scope « minimal ».
- **Pas d'invalidation du bus** : si l'attribut `self._sio.connected` est `True` mais le socket est en fait mort (cas rare), le bus émettra et retournera `True` au niveau client — le serveur ne recevra rien et le replay restera figé. Mitigation indirecte : `_safe_emit` re-vérifie `connected` juste avant le `emit`, et le pattern try/except attrape les erreurs réelles. Pas de fix supplémentaire, hors scope.
- **Endpoint `/cancel` côté serveur** : utilisé par `abort_replay`. Hypothèse : il fonctionne comme attendu (idempotent, accepte un replay déjà annulé). Référence `api_stream.py:4443` — pas re-vérifié dans cet audit.
- **`LEA_FEEDBACK_BUS=0`** : si le flag d'env désactive le bus côté Windows, `self._bus` reste `None`. Le patch couvre ce cas : HTTP est appelé direct. À garder à l'esprit pour la doc de déploiement Windows.
- **`server_client` non câblé après instanciation de ChatWindow** : `update_server_client()` existe (`chat_window.py:255`), donc le wiring tardif est OK. Si `server_client` reste `None` pendant le clic, le patch tombe en `(False, "")` proprement.
## 7. Bonus — test d'intégration de non-régression pour le trim
Le test demandé en second choix est livré dans `tests/integration/test_replay_session_trim_neutral.py`. Il exécute la chaîne **complète** `replay-session` côté serveur sur une fixture synthétique reproduisant le pattern de `sess_20260520T102916_066851` :
- focus initial Notepad sur un titre non-neutre (`http…txt Bloc-notes`)
- clic intra-Notepad à rel_y ≈ 40 sur la barre d'onglets
- focus_change vers `Sans titre Bloc-notes` (titre neutre = état setup auto)
- saisie `test`
Le test vérifie trois invariants stricts :
1. `_generate_setup_actions` produit bien les actions Notepad (`act_setup_sess_click_start`, `click_search`, `click_result`).
2. Après `_trim_redundant_setup_events`, aucun event mouse_click ne porte un `window.title` contenant l'URL `http192.168.1.40` (le clic redondant a été coupé).
3. Après `build_replay_from_raw_events`, la première action utile est `type "test"` — pas un click `by_text="Sans titre"` que `_infer_tab_switch_target` aurait pu produire si le clic redondant avait survécu au trim.
Si la régression du bug du 20 mai revient (par exemple un revert silencieux du patch `_NEUTRAL_TITLE_TOKENS`), ce test échoue immédiatement avec un message clair.
## 8. Synthèse pour décision
- **Cause** : pas de fallback HTTP, UI bloque l'utilisateur dès qu'un emit SocketIO échoue → replay paused figé.
- **Patch** : `ServerClient.resume_replay/abort_replay` (HTTP direct) + `ChatWindow._dispatch_paused_action` (bus → HTTP) + ré-activation boutons + skip auto-hide sur échec total.
- **Scope** : 2 fichiers prod (≤ 160 lignes touchées), 3 fichiers test (354 lignes). Pas de refacto.
- **Validation** : 79/79 tests verts. À valider en condition réelle : kill agent_chat (port 5004) pendant un replay paused, cliquer Continuer → côté Léa log `replay_resume émis pour … via http` + replay redémarre.
- **SCP requis** : 2 fichiers vers Windows avant relance Léa (`lea_ui/server_client.py`, `agent_v1/ui/chat_window.py`).

View File

@@ -0,0 +1,154 @@
# CR — Durcissement du setup auto Windows (gardes visuelles + skip pixel-change)
**Date** : 2026-05-22
**Branche** : `backup/post-demo-2026-05-19`
**Périmètre** : `agent_v0/server_v1/replay_engine.py`, `agent_v0/agent_v1/core/executor.py`, `agent_v0/agent_v1/ui/chat_window.py`
**Statut** : patch + tests implémentés et verts (104/104 sur le périmètre).
---
## 1. Constat live (run du 22 mai 2026 — `replay_sess_76b7d067`)
Sur `sess_20260520T102916_066851`, le trim `neutral=True` passe correctement (patch 20 mai). En revanche, le setup auto Windows enchaîne ses étapes sans aucune garde visuelle intermédiaire :
- `act_setup_sess_click_start` finit en `position_fallback` (clic blind, sans résolution VLM).
- Succès jugé sur **simple changement d'écran** (`_wait_for_screen_change` dans `executor.py:1313`).
- `type_search` et `wait_results` partent sans garde de fenêtre.
- `click_app_result` découvre **trop tard** que la fenêtre attendue n'est pas `Rechercher` mais `Fenêtre de dépassement de capacité de la barre d'état système.`.
- Pendant ce temps, `bloc-notes` a déjà été tapé dans la mauvaise surface (le popup overflow), polluant l'état.
## 2. Cause racine
Deux trous combinés :
| # | Trou | Détail |
|---|---|---|
| A | **Pas de pré/post-conditions visuelles** entre les étapes du setup | `verify_screen` côté agent (`executor.py:1196`) ne faisait qu'un `time.sleep` et déléguait toute vérification au serveur — qui n'a pas de node CLIP pour ces étapes intermédiaires |
| B | **Validation par simple pixel-change** sur `click_start` | `executor.py:1313` considère un click `_setup_phase` valide dès qu'un seul pixel change. Or l'overflow popup change l'écran sans pour autant ouvrir le bon menu |
## 3. Patch minimal — nouveau contrat de contrôle visuel
### 3.1 Nouvelle chaîne setup (12 actions, 3 gardes intermédiaires + 1 finale)
```
1. click_start_menu clic visuel, fallback x/y
2. wait_start_menu 1000 ms
3. verify_start_menu_open GARDE 1 — titre ∈ {Rechercher, Search,
Cortana, Démarrer,
Start, SearchHost,
StartMenuExperienceHost}
4. click_search_box clic visuel (uniquement si search_mode = click_then_type)
5. wait_search_ready 500 ms
6. verify_search_box_active GARDE 2 — titre ∈ {<title_session_source>,
Rechercher, Search}
7. type_app_name frappe « Bloc-notes »
8. wait_search_results 1200 ms
9. verify_search_results_visible GARDE 3 — titre ∈ {Rechercher, Search,
Cortana, SearchHost,
StartMenuExperienceHost}
10. click_app_result clic visuel + expected_window_before
11. wait_app_launch 2000 ms (3000 pour Office)
12. verify_screen final CLIP node setup_initial (pré-existant)
```
### 3.2 Mécanisme des gardes (nouveau champ sur `verify_screen`)
Champ ajouté sur le contrat `verify_screen` : `expected_window_title_contains: List[str]`. Côté agent :
1. `time.sleep` d'attente (comportement legacy).
2. Si patterns présents : `get_active_window_info()` → comparaison substring case-insensitive avec les patterns.
3. Match positif → succès, on continue.
4. Match négatif → bascule en **mode apprentissage humain** (`_capture_human_correction`, 120 s).
- Si l'utilisateur agit : warning `setup_guard_window_mismatch`, success=True, la correction remonte au serveur.
- Si timeout : success=False, `needs_human=True`, pause supervisée.
Pas de changement côté serveur. Le node CLIP final (`setup_initial`) reste pour le verify post-launch.
### 3.3 Skip pixel-change pour `_setup_phase`
Dans `executor.py`, juste avant la branche `_wait_for_screen_change` :
```
is_setup_action = bool(action.get("_setup_phase"))
if needs_screen_check and hash_before and is_setup_action:
# Setup phase : pixel-change neutralisé, la garde verify_screen tranche
time.sleep(0.5)
elif needs_screen_check and hash_before:
# Comportement legacy pour les actions utilisateur
...
```
Conséquences :
- `click_start_menu` ne peut plus être validé sur la seule ouverture du systray overflow popup.
- Le verify_screen suivant détecte le mauvais titre fenêtre et déclenche immédiatement le mode apprentissage.
- Les actions utilisateur **hors setup** conservent strictement le comportement précédent (non-régression vérifiée).
### 3.4 Fix troncature bulle pause supervisée (livré au tour précédent)
Pour mémoire — déjà appliqué :
- `chat_window._compute_paused_bubble_height(reason_str)` : helper statique testable.
- Calcul : `max(wrapped_lines, explicit_lines)` avec cap à 12 lignes (vs 8 avant).
- Scrollbar activée dès que **cap atteint OU contenu ≥ 200 chars** (vs > 280 chars avant).
- Les longs `reason` serveur listant plusieurs candidats (avec `\n`) ne sont plus tronqués silencieusement.
## 4. Fichiers modifiés
| Fichier | Modification | SCP Windows |
|---|---|---|
| `agent_v0/server_v1/replay_engine.py` | `_generate_setup_actions` : insertion de 3 actions `verify_screen` (`verify_start_menu_open`, `verify_search_box_active`, `verify_search_results_visible`) | Non |
| `agent_v0/agent_v1/core/executor.py` | Helper statique `_window_title_matches_any` ; branche `verify_screen` étendue avec garde titre fenêtre + mode apprentissage ; skip `_wait_for_screen_change` pour `_setup_phase=True` | **Oui**`C:/rpa_vision/agent_v1/core/executor.py` |
| `agent_v0/agent_v1/ui/chat_window.py` | Helper statique `_compute_paused_bubble_height` ; cap relevé à 12 lignes, scrollbar dès cap atteint ou ≥ 200 chars (tour précédent) | **Oui**`C:/rpa_vision/agent_v1/ui/chat_window.py` |
⚠️ Le miroir `agent_v0/deploy/windows_client/` est obsolète (setup initial uniquement). Canal d'incrémental réel = SCP manuel direct vers `C:/rpa_vision/`.
## 5. Tests ajoutés ou adaptés
| Fichier | Nature | Tests |
|---|---|---|
| `tests/unit/test_env_setup.py` | NEW classe `TestSetupVisualGuards` | 6 tests : insertion `verify_start_menu_open`, `verify_search_box_active` (mode `click_then_type`), absence en `direct_typing`, `verify_search_results_visible` toujours présent (les 2 modes), timeout ≤ 2 s sur toutes les gardes |
| `tests/unit/test_env_setup.py` | Adaptation de 5 tests existants | `test_notepad_setup_visual` (12 actions), `test_skips_search_click_for_direct_typing`, `test_verify_screen_final_present_with_title`, `test_no_final_verify_without_title`, `test_full_pipeline_from_events` (séquence canonique mise à jour) |
| `tests/unit/test_executor_verify_window_guard.py` | NEW fichier | 13 tests : helper `_window_title_matches_any` (7 cas) + routage garde (4 cas : match, mismatch+correction, mismatch+timeout, neutre sans patterns) + skip pixel-change `_setup_phase` (2 cas : setup skippe, hors-setup garde le comportement) |
| `tests/unit/test_chat_window_paused_dispatch.py` | Ajout classe `TestPausedBubbleHeight` (tour précédent) | 6 tests : empty, court, long single line, multi-lignes `\n`, cap atteint, seuil 200 chars |
| `tests/integration/test_replay_session_trim_neutral.py` | Inchangé (tour précédent) | 1 test bout-en-bout — toujours vert avec le nouveau setup |
**Bilan tests sur le périmètre** : **104 / 104 verts**.
```bash
cd /home/dom/ai/rpa_vision_v3
source .venv/bin/activate
set -a && source .env.local && set +a
python -m pytest \
tests/unit/test_env_setup.py \
tests/unit/test_executor_verify_window_guard.py \
tests/unit/test_chat_window_paused_dispatch.py \
tests/unit/test_server_client_replay_controls.py \
tests/integration/test_replay_session_trim_neutral.py -v
```
## 6. Comportement attendu en live
Après SCP `executor.py` + `chat_window.py` et redémarrage Léa, sur un nouveau `/replay-session` de `sess_20260520T102916_066851` :
| Scénario | Log Léa attendu | Issue |
|---|---|---|
| `click_start` touche le vrai bouton Windows | `[LEA] verify_screen garde OK : 'Recherche' matche [...]` | Setup avance, frappe protégée |
| `click_start` ouvre systray overflow popup | Pixel-change observé MAIS log explicite `Setup action … : validation pixel-change skippée (garde verify_screen ultérieure)` puis `[LEA] verify_screen garde KO : attendu un titre contenant [...], actuel 'Fenêtre de dépassement…'` | Mode apprentissage humain immédiat, aucune frappe à l'aveugle |
| Focus perdu pendant `wait_search_results` (notification surgit) | `[LEA] verify_screen garde KO` sur `verify_search_results_visible` | Apprentissage humain avant `click_app_result` |
| Bulle de pause avec un long `reason` | Scrollbar visible | Plus de troncature |
## 7. Risques / limites
- **Patterns FR+EN uniquement** : couverture Windows 10/11 FR et EN. Sur OS exotique (DE, ES, ZH), il faudra étendre `expected_window_title_contains`. Localisé dans `_generate_setup_actions`, extension triviale.
- **Skip pixel-change conditionné à `_setup_phase`** : seules les actions marquées `_setup_phase=True` perdent la validation pixel-change. Si une future contribution ajoute une action setup sans garde verify_screen derrière, on perdrait le filet. À surveiller / documenter dans la convention de génération.
- **Mode `direct_typing`** : couverture par `verify_start_menu_open` (avant frappe) + `verify_search_results_visible` (avant clic résultat). Pas de `verify_search_box_active` car pas de `click_search_box` à valider — testé explicitement.
- **Helper `_compute_paused_bubble_height`** : prend en compte les `\n` explicites et la longueur ; cap 12 lignes. Compromis volontairement conservateur — afficher une scrollbar légèrement trop tôt vaut mieux que tronquer du contenu critique de pause.
## 8. Synthèse pour décision
- **Avant ce patch** : setup auto enchaînait click → wait → type → click_result sans contrôle entre, et un seul changement de pixel suffisait à valider la première étape. Constat live = `bloc` tapé dans `Fenêtre de dépassement…`, click_result en erreur tardive, `paused_need_help`.
- **Après ce patch** : 3 gardes verify_screen titre fenêtre + skip pixel-change setup → chaque transition critique est verrouillée. Mode apprentissage humain immédiat à la première dérive. Pixel-change ne décide plus de la validité d'une étape setup.
- **Scope** : 2 fichiers prod modifiés (≈ 90 lignes ajoutées dans `replay_engine.py`, ≈ 75 dans `executor.py`), 2 fichiers test (≈ 350 lignes neuves + adaptations). Aucun changement côté serveur ni protocole.
- **SCP** : `executor.py` et `chat_window.py` à pousser vers `C:/rpa_vision/agent_v1/…` avant relance Léa. `replay_engine.py` reste côté serveur Linux.
- **Validation live à faire** : lancer un `/replay-session` sur `sess_20260520T102916_066851`, vérifier la présence des 3 logs `verify_screen garde OK` (ou un mode apprentissage propre en cas de dérive).

View File

@@ -0,0 +1,76 @@
# Healthcheck Lea stack — preuve initiale
Date : 2026-05-25 12:53 Europe/Paris
Script : `tools/lea_healthcheck.py`
Mode : lecture seule, aucun restart, aucune restauration, aucune suppression.
## Commandes
Local Linux seul :
```bash
.venv/bin/python tools/lea_healthcheck.py
```
Linux + Windows via SSH, sans stocker le mot de passe dans le script :
```bash
SSHPASS='***' LEA_SSH_COMMAND='sshpass -e ssh' \
.venv/bin/python tools/lea_healthcheck.py --windows-host 192.168.1.11
```
Sortie JSON :
```bash
.venv/bin/python tools/lea_healthcheck.py --json
```
## Resultat initial
Statut global : **WARN**.
OK :
- `rpa-streaming.service` actif.
- Port `5005` ouvert.
- `/health` streaming healthy.
- Ollama API `11434` ouverte.
- Tags critiques presents :
- `qwen2.5vl:7b-rpa`
- `t2a-gemma3-27b:latest`
- `t2a-gemma3-27b-q4:latest`
- `thiagomoraes/medgemma-27b-it:Q4_K_S`
- `qwen2.5vl:7b-rpa` resident dans Ollama avec `context_length=2048`.
- Store Ollama : 38 manifests, 106 blobs.
- 3 blobs critiques 27B presents.
- Windows SSH joignable.
- Tache Windows `LeaInteractive` : `Running`.
- 2 processus `run_agent_v1.py` observes, conforme au wrapper venv + Python reel.
Etat initial avant C1 :
- `rpa-agent-chat.service` inactif.
- Port `5004` ferme.
- FeedbackBus non joignable.
- Variable utilisateur Windows `LEA_FEEDBACK_BUS='1'`, donc Lea tente le bus 5004 alors qu'il est down.
Etat apres C1 / restart controle du 2026-05-25 13:26 :
- `rpa-agent-chat.service` actif.
- Port `5004` ouvert.
- SocketIO polling OK avec origins `http://192.168.1.40:5004` et `http://192.168.1.11:5004`.
- `GET /api/status` FeedbackBus retourne `status=online`.
- Healthcheck Linux + Windows : **OK**.
Point restant : `agent_chat` tente encore de charger OWL-v2 sur CUDA au boot et garde environ 602 MiB VRAM apres OOM. Cela n'empeche pas 5004, mais doit etre traite dans le chantier performance/VRAM.
## Interpretation
Le chemin critique replay/pause/resume reste couvert par `rpa-streaming` port 5005 et par le fallback HTTP.
Le chantier propre avant le 1 juin est de choisir entre :
1. reparer et rallumer FeedbackBus 5004 pour la narration temps reel ;
2. ou desactiver explicitement `LEA_FEEDBACK_BUS` cote Windows si la narration n'est pas retenue.
Avec le report de la demo au 1 juin, l'option privilegiee est de reparer proprement 5004 au lieu de masquer le warning.

View File

@@ -0,0 +1,233 @@
# Lessons Learned — Sprint démo GHT 5→19 mai 2026
**Objectif du document** : inventaire factuel des 15 jours de bug-chasing pré-démo GHT. À relire AVANT d'attaquer ARES et Anouste pour ne pas refaire les mêmes diagnostics.
**Pas de prose, pas de plan d'action ici** — juste « ce qui marche » / « ce qui ne marche pas » + références.
## Périmètre
- **Période** : 2026-05-05 → 2026-05-19 (15 jours)
- **Branches** : `feature/qw-suite-mai` (travail) → `backup/post-demo-2026-05-19` (commit `5ea4960e6`)
- **Référence "ça marchait" antérieure** : tag `demo-stable-2026-05-12` (commit `2eeaa806b`), branche `demo/ght-2026-05-08` (commit `56e869c46`)
- **Démo livrée** : vidéo `Demo_urgence_3_db` (`wf_483910cdd851_1778750587`, 46 steps), patiente MOREL Catherine, décision UHCD, 1750 €
---
## ✅ Ce qui marche (validé empiriquement pendant le sprint)
### Pipeline visuel / résolution UI
| Élément | Statut | Référence |
|---|---|---|
| Template matching multi-scale étendu (0.25 → 2.0) | scores ≥ 0.9 retrouvés sur capture downscalée 800×500 | `resolve_engine.py:130`, handoff 18 mai §1.2 |
| `hybrid_text_direct` rebranché dans cascade strict (mode legacy) | actif depuis commit `1cbec2806` | audit F2.4.4 |
| Exemption drift > 0.20 si `template_matching ≥ 0.95` ou `hybrid_text_direct ≥ 0.80` | actif, évite faux rejets sur high-confidence | `resolve_engine.py:2367-2390`, audit F2.2.1, DETTE-002 |
| Fallback heartbeat full-screen si capture client tronquée < 1200×800 | actif, élargi le 7 mai par `7233df2bb` | `api_stream.py:4422`, audit F2.2.6 |
| Anchors `by_text` (LINUX_demo, Tables, demo_95) ciblage textuel anti-faux-positif | validé sur Demo_urgence_3_db ord 29/35/36 | handoff 16 mai §4 |
| LoopDetector composite (screen_static + action_repeat + retry) | actif par défaut | `loop_detector.py`, commit `2a51a844b` |
| SafetyChecksProvider hybride (déclaratif + LLM contextuel) | actif sur `safety_level == medical_critical` | commit `7c6945171`, audit F2.3.4 |
| MonitorRouter (résolution écran cible multi-monitor) | actif, enrichissement heartbeat `monitor_index + monitors_geometry` | commits `6582a69d3`, `b1a3aa16f`, `2d71e2a24` |
### Workflow `linux_db` (NoMachine + DBeaver VM Ubuntu)
| Élément | Statut |
|---|---|
| Clics simples traversent NoMachine vers la VM | OK (pynput → SendInput → NoMachine → VM) |
| Bypass Ctrl+V/Ctrl+Enter via `ydotool` directement dans la VM | fix retenu (NoMachine en passive grab mange Ctrl) |
| `ydotoold` daemon persistant via service systemd | installé 18 mai, redémarre au boot VM |
| Gardien clipboard `prepare_clipboard_linuxdb.sh` (wl-copy + xsel boucle 0.5s) | actif, recharge VM clipboard |
| Workflow `linux_db` (7-9 steps) E2E sur VM Ubuntu en ~30s | validé 18 mai |
| Hook libvirt `/etc/libvirt/hooks/network` injecte `LIBVIRT_FWI ACCEPT 4000` | installé 18 mai, à valider au reboot |
| DNAT 4001→4000 via `nomachine-vm-forward.service` | persistant |
| UFW `route allow 4000 → 192.168.122.132` | persistant |
### LLM / Modèles
| Élément | Statut |
|---|---|
| `gemma4:latest` retenu pour `safety_checks` (bench rigoureux) | commit `0a02a6ec9`, BENCH_SAFETY_CHECKS_2026-05-06.md |
| `gemma4:31b-cloud` pour `t2a_decision` MOREL | qualité clinique propre observée (run 8 du 12 mai) |
| `qwen3-next:80b-cloud` testé qualité OK | switch ponctuel, pas durable |
| `qwen2.5vl:7b` pour VLM | configuré via `.env.local`, déborde CPU sur RTX 5070 12GB (acceptable car fallback) |
| `InfiGUI-G1-3B` Transformers grounding | 3.9 GB VRAM, permanent depuis 7 jours, principal |
| Bypass LLM via `static_result` / `static_text` (`replay_engine.py`) | court-circuit Ollama pour MOREL UHCD 1750 € — utilisé en démo |
| Module `smart_resize` officiel Qwen3-VL (commit `0d7bcd18a`) | commité mais ⚠ calé sur mauvaise référence patch_size — voir DETTE-014 |
| Bench `bench_t2a_dryrun.py` + `t2a_mappings.py` (commit `f2212e77e`) | outillage standalone 11 dossiers POC |
| `build_dpi_enriched` extraction déterministe horaires/classifications (commit `9872f4510`) | 41/41 tests verts |
### Infra & déploiement
| Élément | Statut |
|---|---|
| Service systemd `rpa-streaming` | actif, restart propre |
| Backup tarball post-démo `_archives/rpa_vision_v3_post_demo_20260519_142940.tar.gz` (8.9 GB) | SHA256 vérifié |
| Backup git `backup/post-demo-2026-05-19` poussé sur gitea (627 fichiers, 468 anchors) | commit `5ea4960e6` |
| 12+ backups DB `workflows.db.bak.*` jalonnant la session | présents dans `visual_workflow_builder/backend/instance/` |
| Registre `docs/DETTE_TECHNIQUE.md` créé (14 entrées) | actif depuis 9 mai |
### Méthodes de travail (sanctuariser)
| Méthode | Origine |
|---|---|
| `git status --short` SYSTÉMATIQUE en début de session | incident commit composite 12 mai (4 backend + 2 frontend stagés non liés) |
| Sauvegarde + fork AVANT chantiers parallèles | appliqué 12 mai, a payé |
| Instrumenter AVANT optimiser | corrigé 2 fois le cap (baseline run 4 + mesure parallélisme v2) |
| Test Ollama direct 30s AVANT pari sur connaissance LLM | `gemma3:27b` aurait été retenu par erreur sinon |
| Mesurer 2 conditions COMPARABLES, jamais cold-start vs warm | parallélisme ratio 2.7× → 1.27× corrigé |
| Diff PNG anchor avant/après recapture | aurait économisé 15 jours de bug-chasing (cause racine bug P0 #1) |
---
## ❌ Ce qui ne marche pas (cause connue ou hypothèse, contournement noté)
### 🔴 Bugs P0 racines (NON résolus — démo a tourné grâce aux contournements)
| Bug | Cause connue | Contournement | Fix réel à faire |
|---|---|---|---|
| **VWB recapture anchor ne régénère pas le PNG** | inconnue — `capture.py` réutilise PNG existant ou écrit avant screenshot ; 2 anchors capturés à 8j d'intervalle bit-à-bit identiques | recapture inutile, accepter régressions mystérieuses | audit `visual_workflow_builder/backend/api_v3/capture.py` |
| **Stop VWB ne purge pas la queue serveur** | VWB n'appelle pas `POST /api/v1/traces/stream/replay/<id>/cancel` au clic Stop | script `./scripts/cancel-replays.sh` manuel | brancher Stop → cancel côté frontend |
| **Coord client Léa Y cassé (÷ ~27)** | `mss.monitors[1]` retourne intermittemment `2560×60` au lieu de `2560×1600``y_pct × 60 = 16 px` (clic en haut écran) | aucun — bug intermittent | `agent_v0/agent_v1/core/executor.py:606-617`, ajouter fallback `if height < 200: reject` |
| **Bug skip ord 13 orchestration** (intermittent, run 8 du 12 mai) | non identifiée — transition serveur → visuel → serveur (mécanisme server-side action) | aucun, NOT REPRO 100% | trace `replay_fb0c9882_state.json` ; investiguer `replay_engine.py` + `api_stream.py` |
| **Bug échelle pixel grounding Ollama** (smart_resize non-déterministe) | DETTE-006 + DETTE-010 + DETTE-014 — checkpoint Qwen3-VL utilise `Qwen2VLImageProcessorFast` avec `patch_size=16` (factor=32, non 28) ; module `smart_resize.py` calé sur mauvaise référence | non posé | réaligner après lecture `image_processing_qwen2_vl_fast` |
### ⚠ Bugs P1 (workaround dispo)
| Bug | Cause | Workaround |
|---|---|---|
| Léa état mémoire dégradé (bulle paused n'apparaît plus après plusieurs replays) | `_last_pause_msg_shown` + `_chat_window_ref` jamais reset | restart Léa Windows |
| `delay_before` / `delay_after` ignorés au runtime | non lus par executor.py | fix partiel `dag_execute.py` pour `duration_ms` ; généraliser à `delay_before/after` |
| Léa interprète `action=null + replay_paused=true` comme "fin du replay" | `main.py` désactive `_replay_active` à tort | fix proposé `executor.py:1875` retourner `True` (non appliqué — nécessite SCP + restart Léa) |
| VWB frontend cache après modif DB | pas d'invalidation cache React | Ctrl+Shift+R obligatoire |
| `paste:true` ne fonctionne pas Windows → VM Ubuntu via NoMachine | NoMachine ne propage pas clipboard (ou `win32clipboard.SetClipboardText` plante silencieusement) | bypass via `ydotool` dans la VM (voir ✅) |
| Léa client envoie captures **800×500** au serveur | défaut `max_width=800` dans `executor.py:2895`, 7 sites d'appel sans override | compensé côté serveur par multi-scale étendu (✅) — fix client à poser : `max_width=0` sur 7 sites + SCP |
| `RPA_VLM_MODEL=gemma4:e4b` hardcoded dans Léa Windows (tag inexistant) | `executor.py` lignes 1569, 1700, 2248 | exporter `RPA_VLM_MODEL=qwen2.5vl:7b` env Windows |
| NoMachine viewer Windows freeze (clics avalés après quelques minutes) | NoMachine 9.5.7, pattern intermittent | restart NoMachine + plein écran obligatoire |
| Bug `'int' object has no attribute 'get'` VLM Quick Find | exception Python, non bloquant | DETTE B handoff 18 mai |
| Bug `get_target_memory_store` import dans `replay_memory.py` | import cassé | non bloquant mais empêche apprentissage corrections humaines |
| Démarrage Léa très lent (3-6 min au lancement) | chargement modèles ML | investiguer |
| Léa peut crasher silencieusement sous Windows | non identifié (mémoire ? exception ?) | quick-restart avant démo |
| Bouton "Stop" disparaît côté VWB UI alors que replay actif serveur | désynchro UI/serveur | confondant, à fixer |
| DAG edges visuelles VWB ne se sauvegardent pas | seul `steps.order` fait foi | confondant, à fixer |
| Capture VWB fallback `mss` Linux échoue sur Wayland natif | `XGetImage failed` | dépendance Léa Windows OBLIGATOIRE pour capturer |
### 🚫 Code orphelin / débranché (audit 8 mai)
| Code | Statut | Référence |
|---|---|---|
| `_resolve_by_yolo` défini, importé, **jamais appelé** | cascade OmniParser/YOLO neutralisée | DETTE-004, F2.4.1 |
| `_fuzzy_match` importé `api_stream.py:4372` mais jamais appelé | import mort | F2.5.2 |
| `VisualEmbeddingManager` + `ScreenshotValidationManager` (`core/visual/*`) définis mais jamais instanciés | mémoire visuelle orpheline | DETTE-005 |
| `ShadowLearningHook` (`core/grounding/shadow_learning_hook.py`) défini mais jamais instancié | Phase 6 FAST→SMART→THINK non câblée à Shadow | DETTE-009 |
| `_handle_possible_popup` (client) défini, **0 site d'appel** | fonction morte côté Léa, remplacée par `_handle_popup_vlm` | F5.5.1 |
| Pre-check VLM par-clic désactivé par `if False:` (`observe_reason_act.py:1704-1713`) | actif depuis 25 avril, commentaire « pipeline FAST→SMART→THINK a déjà validé » | DETTE-008, F6.1.1 |
| Pre-check OCR sémantique gardé par flag `RPA_ENABLE_TEXT_PRECHECK=false` par défaut | extinction explicite 8 mai pour démo GHT | DETTE-001, F2.3.1, F2.6.2 |
| Self-healing Win+D au retry 1 reverté (`22c0a2ba6`) | cercle vicieux observé | DETTE-003, F2.2.3 |
| Trois implémentations `smart_resize` coexistent (server.py, infigui_worker.py, module officiel) | unification post-démo Kerella | DETTE-007 |
| `pause_for_human` ignorée silencieusement en mode autonome (sans `safety_checks`) | actif `api_stream.py:3011-3017` | F2.6.5 |
### Anti-méthode observée (autocritique)
| Erreur | Conséquence |
|---|---|
| Modifs locales empilées sans validation à chaque étape | 15 jours de dérive depuis `demo-stable-2026-05-12` |
| Pas de smoke-test reproductible entre démos | régressions silencieuses découvertes par hasard |
| Bug VWB recapture anchor non détecté pendant 15 jours | cause racine n°1 des régressions, restée invisible |
| Workarounds empilés (cancel-replays.sh, bypass LLM static, pause humaine NoMachine) | dette technique non remboursée |
| Recapture en aveugle (Dom a recapturé des anchors en pensant fixer, le bug était VWB) | effort gaspillé |
| Apprentissage workflow via VWB-recording au lieu de mode Shadow Léa | divergence VWB ↔ Léa, 5 bugs P0 |
---
## ⚠ Contournements ACTIFS à connaître absolument
À sortir avant chaque nouveau run / déploiement client. Ces hacks **ne survivront pas** à un environnement propre.
| Contournement | Localisation | Risque |
|---|---|---|
| Bypass auth NPM `/aiva-urgence/` | `proxy_host/10.conf` (hors git) | écrasé si UI NPM touchée |
| Bypass LLM `static_result/static_text` MOREL | `replay_engine.py` steps 12-14 Demo_urgence_3_db | démo seulement, pas réutilisable client |
| Script `scripts/cancel-replays.sh` | manuel après chaque Stop VWB | oublié → replay zombie |
| `prepare_clipboard_linuxdb.sh` à relancer après reboot VM | non auto | clipboard vide → paste vide |
| `xhost +local:` à refaire après reboot VM | non auto | xsel échoue |
| Bypass Ctrl+V via `ydotool` au lieu de NoMachine clipboard | architectural, OK | dépend de la VM, pas Windows pur |
| Mot de passe `loli` en clair dans les scripts SSH/sudo | DETTE 5/16 mai | à remplacer par clé SSH + sudoers NOPASSWD |
| `RPA_VLM_MODEL=gemma4:e4b` hardcoded Léa | env var Windows à exporter | popup VLM 404 sinon |
| Flag pré-check OCR off par défaut | `RPA_ENABLE_TEXT_PRECHECK=false` | clics au pif si VLM/template échouent |
| Drift exemption template ≥ 0.95 / hybrid ≥ 0.80 | `resolve_engine.py:2367-2390` | accepte position visuelle même hors zone enregistrée |
| Fallback heartbeat sur capture < 1200×800 | `api_stream.py:4422` | risque image stale si heartbeat ancien |
---
## 🧠 Constats produit (à intégrer dans le pivot post-démo)
1. **Erreur stratégique identifiée par Dom (19 mai)** : apprendre via VWB-recording au lieu de Shadow Léa = deux représentations divergentes (anchors capturés à un moment T, replay à T+N). Origine des 5 bugs P0.
2. **VWB et Léa non unifiés** : VWB édite un script explicite que Léa rejoue. Pas de réinterprétation au runtime. Unification réelle = 4-6 semaines (refonte paradigme, hors fenêtre 15j POC).
3. **gemma3:27b CONFABULE sur PMSI français** (invente acronymes GEMSA, présente avec assurance maximale) — ne JAMAIS l'envisager comme fallback `gemma4` sur T2A.
4. **gemma4:31b** a conscience d'incertitude (bloc *Thinking*) — mais confond CCMU/GEMSA avec logique GHM. Libellé complet PMSI obligatoire dans FAITS_CALCULÉS.
5. **Ollama Cloud 503** vécue 12 mai → robustesse non couverte pour démo. Pas de fallback local équivalent qualité testé.
6. **Communication Dom ↔ Claude Code × 2 ↔ Claude session principale** : déperdition observée (3 arbitrages décision retransmis 2 fois). Pointer Claude Code vers fichiers de référence rédigés en session principale, pas paraphraser en chat.
---
## Références
### Commits clés (5 → 19 mai)
- `5ea4960e6` (19 mai) — backup snapshot post-démo GHT
- `f2212e77e` (12 mai) — bench_t2a_dryrun.py + t2a_mappings.py
- `9872f4510` (12 mai) — build_dpi_enriched extraction déterministe
- `2eeaa806b` (9 mai) — **tag `demo-stable-2026-05-12` — référence "ça marchait"**
- `bfbf0f9c3` (9 mai) — refactor parser bbox_2d centralisé
- `0d7bcd18a` (9 mai) — module smart_resize officiel (⚠ DETTE-014)
- `88ed103de` (9 mai) — création registre DETTE_TECHNIQUE.md
- `731b5bcae` (8 mai) — réactivation pré-check OCR calibrage chirurgical
- `56e869c46` (8 mai) — flag pré-check OCR off par défaut **(branche `demo/ght-2026-05-08`)**
- `7847a0e82` (7 mai) — toast paused supervisée + threshold FIND-TEXT 0.75
- `40440f1ca` (7 mai) — cure régression b584bbabc fallback aveugle
- `f62fda575` / `7233df2bb` (7 mai) — fallback heartbeat image tronquée + execution_mode supervised
- `1cbec2806` (6 mai) — rebrancher hybrid_text_direct
- `22c0a2ba6` (6 mai) — **revert** self-healing Win+D auto (cercle vicieux)
- `c969f93a2` (6 mai) — self-healing Win+D auto retry 1 (reverté)
- `864530c85` (6 mai) — `_async_replay_lock` helper + 17 endpoints async non-bloquants
- `0a02a6ec9` (6 mai) — bench rigoureux LLM safety_checks → `gemma4:latest`
- `2a51a844b` (5 mai) — LoopDetector composite
- `7c6945171` (5 mai) — SafetyChecksProvider hybride
### Handoffs détaillés
- `docs/handoffs/2026-05-19_handoff_post_demo_GHT.md` — bilan démo + 5 bugs P0
- `docs/handoffs/2026-05-18_handoff_consolidation.md` — UFW/LIBVIRT, template multi-scale, ydotoold systemd
- `docs/handoffs/2026-05-17_handoff_session_nomachine.md` — NoMachine freeze + `gemma4:e4b`
- `docs/handoffs/2026-05-16_handoff_ydotool_clipboard.md` — bypass Ctrl+V via ydotool
- `docs/handoffs/2026-05-16_handoff_demo3_workflow.md` — création Demo_urgence_3_db + linux_db
- `docs/handoffs/2026-05-13_inventaire_anchors_interop.md` — inventaire anchors
- `docs/handoffs/2026-05-12_handoff_fin_journee.md` — bug skip ord 13, bench A.1 paste, 8 arbitrages décision
- `docs/handoffs/2026-05-12_brief_S1_build_dpi_enriched.md` — brief V3 décision
- `docs/handoffs/2026-05-12_audit_complet_decision_t2a.md` — audit t2a_decision
### Audits & rapports
- `docs/DETTE_TECHNIQUE.md` — 14 entrées
- `docs/AUDIT_CONTROLES_DEBRANCHES_2026-05-08.md` — audit serveur+client 50+ findings
- `docs/AUDIT_DIM_TIM_DEMO_GHT_2026-05-08.md` — audit médecin DIM + TIM
- `docs/AUDIT_MEMOIRE_CLAUDE_2026-05-08.md` — santé index mémoire
- `docs/AUDIT_BDD_WORKFLOW_2026-05-10.md` — audit BDD workflows
- `docs/BUG_PRECHECK_SPATIAL_BLINDNESS_2026-05-08.md` — DETTE-001
- `docs/BENCH_SAFETY_CHECKS_2026-05-06.md` — bench LLM safety_checks
- `docs/BENCH_T2A_DECISION_11DOSSIERS.md` — bench décision
- `docs/MIGRATION_VLM_PLAN_2026-05-09.md` — plan migration VLM (DETTE-006, DETTE-010, DETTE-014)
- `docs/INVESTIGATION_MEMOIRE_VISUELLE_ORPHELINE_2026-05-09.md` — DETTE-005, DETTE-009
### Backups
| Type | Localisation |
|---|---|
| Tarball post-démo | `/home/dom/ai/_archives/rpa_vision_v3_post_demo_20260519_142940.tar.gz` (8.9 GB, SHA256 `7ab84f22d5a4...`) |
| Branche git backup | `backup/post-demo-2026-05-19` sur gitea (commit `5ea4960e6`) |
| Tag stable référence | `demo-stable-2026-05-12` (commit `2eeaa806b`) |
| Branche démo référence | `demo/ght-2026-05-08` (commit `56e869c46`) |
| Backups DB workflows | `visual_workflow_builder/backend/instance/workflows.db.bak.*` (12+ jalons) |
---
*Document maintenu par Dom. Toute nouvelle leçon (succès ou échec) à ajouter dans la section appropriée. Pas de remplissage — uniquement faits sourcés.*

View File

@@ -0,0 +1,93 @@
# Incident Ollama model store — diagnostic lecture seule
Date : 2026-05-25 12:55 Europe/Paris
Auteur : Codex
Contexte : Dom observe que des modeles semblent avoir disparu de `ollama list`, notamment des 27B medicaux / T2A.
## Conclusion provisoire
Les deux modeles T2A 27B critiques ne sont pas perdus :
- `t2a-gemma3-27b:latest` est present dans Ollama et son blob est identique au GGUF local Q8.
- `t2a-gemma3-27b-q4:latest` est present dans Ollama et son blob est identique au GGUF local Q4.
- Les deux GGUF existent aussi dans `/mnt/backup/projects/t2a_v2/models/` avec les memes checksums.
Le modele `thiagomoraes/medgemma-27b-it:Q4_K_S` est aussi present dans le store actif Ollama, avec un blob de 16G.
Ce diagnostic ne prouve pas qu'aucun autre modele n'a disparu : il prouve que les 27B identifies pendant l'incident sont encore presents et reconstruisibles.
## Etat Ollama actif
- Un seul serveur `ollama serve` observe.
- Processus principal : `/usr/local/bin/ollama serve`, user `ollama`.
- Store actif : `/var/lib/ollama/.ollama/models`.
- `OLLAMA_MODELS` shell : vide.
- Service systemd : pas d'override explicite `OLLAMA_MODELS`, mais le journal indique le store actif ci-dessus.
- Store actif : 38 manifests, 106 blobs, 178G.
- `/usr/share/ollama` : absent.
- `/home/dom/.ollama/models` : absent.
Historique notable :
```text
/home/dom/.bash_history:33:sudo rm -rf /usr/share/ollama
/home/dom/.bash_history:34:sudo rm -rf ~/.ollama
```
Le fichier bash history date de 2025-10-23, donc ce n'est pas une preuve d'action recente. C'est compatible avec une ancienne migration/reinstallation Ollama ayant supprime d'anciens stores non actifs.
## Modeles critiques verifies
| Tag Ollama | Taille | Quant | Blob actif | Source locale | Backup | Statut |
|---|---:|---|---|---|---|---|
| `t2a-gemma3-27b:latest` | 28G | Q8_0 | `sha256-2f2509e30b0d07db517b82e62404194ef355846f08ac287775ff363693086818` | `/home/dom/ai/t2a_v2/models/t2a-gemma3-27b-q8_0.gguf` | `/mnt/backup/projects/t2a_v2/models/t2a-gemma3-27b-q8_0.gguf` | Present + reconstructible |
| `t2a-gemma3-27b-q4:latest` | 16G | Q4_K_M | `sha256-0139f42273d53348fa0d24daae016b7231e1310258bbbaa7e38a1af703217c1a` | `/home/dom/ai/t2a_v2/models/t2a-gemma3-27b-q4_k_m.gguf` | `/mnt/backup/projects/t2a_v2/models/t2a-gemma3-27b-q4_k_m.gguf` | Present + reconstructible |
| `thiagomoraes/medgemma-27b-it:Q4_K_S` | 16G | Q4_K_S | `sha256-7cb6ff10942c8ccf370e274daafaf56da3fff318f40a355df331d8783c6c11f3` | Store Ollama actif | non identifie hors Ollama | Present |
Checksums confirmes :
```text
2f2509e30b0d07db517b82e62404194ef355846f08ac287775ff363693086818 /var/lib/ollama/.ollama/models/blobs/sha256-2f2509e30b0d07db517b82e62404194ef355846f08ac287775ff363693086818
2f2509e30b0d07db517b82e62404194ef355846f08ac287775ff363693086818 /home/dom/ai/t2a_v2/models/t2a-gemma3-27b-q8_0.gguf
2f2509e30b0d07db517b82e62404194ef355846f08ac287775ff363693086818 /mnt/backup/projects/t2a_v2/models/t2a-gemma3-27b-q8_0.gguf
0139f42273d53348fa0d24daae016b7231e1310258bbbaa7e38a1af703217c1a /var/lib/ollama/.ollama/models/blobs/sha256-0139f42273d53348fa0d24daae016b7231e1310258bbbaa7e38a1af703217c1a
0139f42273d53348fa0d24daae016b7231e1310258bbbaa7e38a1af703217c1a /home/dom/ai/t2a_v2/models/t2a-gemma3-27b-q4_k_m.gguf
0139f42273d53348fa0d24daae016b7231e1310258bbbaa7e38a1af703217c1a /mnt/backup/projects/t2a_v2/models/t2a-gemma3-27b-q4_k_m.gguf
7cb6ff10942c8ccf370e274daafaf56da3fff318f40a355df331d8783c6c11f3 /var/lib/ollama/.ollama/models/blobs/sha256-7cb6ff10942c8ccf370e274daafaf56da3fff318f40a355df331d8783c6c11f3
```
## Modelfiles de reconstruction T2A
Les Modelfiles existent en local et backup :
```text
/home/dom/ai/t2a_v2/models/Modelfile-t2a-gemma3-27b
/home/dom/ai/t2a_v2/models/Modelfile-t2a-gemma3-27b-q4
/mnt/backup/projects/t2a_v2/models/Modelfile-t2a-gemma3-27b
/mnt/backup/projects/t2a_v2/models/Modelfile-t2a-gemma3-27b-q4
```
Reconstruction possible, si necessaire et apres accord :
```bash
ollama create t2a-gemma3-27b -f /home/dom/ai/t2a_v2/models/Modelfile-t2a-gemma3-27b
ollama create t2a-gemma3-27b-q4 -f /home/dom/ai/t2a_v2/models/Modelfile-t2a-gemma3-27b-q4
```
Ne pas executer tant que les tags sont deja presents.
## Elements a clarifier
1. La liste attendue par Dom avant incident : noms exacts des modeles absents.
2. Si des modeles etaient dans `/usr/share/ollama` ou `~/.ollama/models`, ils ne sont pas dans le store actif actuel.
3. Timeshift contient des repertoires `var/lib/ollama` vides : pas de restauration model store via Timeshift observee.
4. Aucun `/api/delete` recent observe apres le 28 avril dans les extraits consultes ; les logs complets peuvent etre re-parcourus si une date suspecte est fournie.
## Decision immediate
- Ne rien supprimer.
- Ne rien restaurer a chaud.
- Garder ce fichier comme inventaire initial.
- Demander a Gemini de completer la table et chercher une eventuelle liste historique des tags attendus.

View File

@@ -0,0 +1,268 @@
# Audit chaîne apprentissage modèle IA — 2026-06-01
> **DRAFT audit factuel — lecture seule, pas encore appliqué.**
>
> Date : 2026-06-01 22:00 Europe/Paris
> Auteurs : agent Explore Claude (audit primaire) + Claude (synthèse + matérialisation fichier)
> Statut : DRAFT — relecture Dom/Codex/Qwen attendue
> Origine demande : Dom 2026-06-01 ~21:40 — « tu pourrais lancer un agent explorateur pour nous remonter la chaîne exact d'apprentissage du modèle d'IA sur lequel j'ai travaillé. Le code existe, mais je pense qu'il a été débranché... »
## TL;DR — Constat fort
**L'intuition de Dom était JUSTE.** La chaîne d'apprentissage est **partiellement débranchée** depuis plusieurs semaines/mois. Les composants nécessaires pour implémenter ce que Dom a explicité dans ses 5 messages du 2026-06-01 20:46-21:27 (auto-évaluation par répétition, fusion/regroupement compétences immuables, versioning adaptateurs UI, portabilité du modèle appris) **existent déjà** dans le repo :
- `core/learning/continuous_learner.py`**644 lignes**
- `core/learning/feedback_processor.py`**176 lignes**
- `PrototypeVersionManager` (support de ContinuousLearner)
- `TargetMemoryStore`, `VersionedStore` (supports)
**Et ils sont tous orphelins** : ils ne sont pas importés par les points d'entrée actifs (`api_stream.py`, `run_worker.py`, `agent_chat/app.py`, `web_dashboard/app.py`, etc.).
**En plus** : le **worker VLM** (le composant qui retraite les sessions finalisées avec ScreenAnalyzer/CLIP/FAISS/GraphBuilder) **n'a traité aucune session depuis 5 jours** (queue vide). Sessions accumulées non passées par le pipeline d'enrichissement profond.
## Section meta — Constat de méthode (à dire franchement)
> Dom 2026-06-01 ~21:55 : « on vient de passer presque 7 jours à refaire ce que j'avais déjà fait. Il faut arrêter de réinventer la roue. »
C'est un constat factuel et juste. Cet audit (le seul à avoir cartographié l'existant) aurait dû être fait **avant** :
- de spécifier P1-SEMANTIQUE comme une nouvelle Phase 2.5 ;
- de proposer un `LoopDetector` proactif comme « bonus » alors que `ContinuousLearner` couvre ce besoin ;
- de discuter de « désapprentissage » (notion que Dom a explicitement rejetée 21:27) alors que `PrototypeVersionManager` gère déjà le versioning des prototypes ;
- de proposer de nouveaux mécanismes de fusion de compétences alors que `FeedbackProcessor` est conçu pour ça.
**Pourquoi cet oubli ?** Trois facteurs cumulés :
1. `docs/POC/` (5 docs Dom déposés du 28-29/05 et 01/06) a été lue **après** rédaction du plan POC Claude du jour (mémorisé comme erreur de méthode dans `feedback_lire_docs_poc_avant_depot.md`).
2. **Aucun agent n'a été missionné en début de journée pour cartographier l'existant** dans `core/learning/`, `core/healing/`, `core/cognition/`. Mon audit Explore de 17:00 a flagué `ContinuousLearner` et `RecoveryLogger` comme orphelins mais sans alarmer sur le fait que ces orphelins **étaient précisément ce qui était demandé**.
3. Codex a été en mode urgence patch (P0 régression, dashboard test, etc.) et Claude en mode livraison agressive (5 livraisons P1 dans la journée) **sans pause cartographie**.
**Décision opérationnelle à acter** : **avant tout nouveau module Léa learning, lancer un agent Explore qui vérifie ce qui existe dans `core/learning/`, `core/cognition/`, `core/healing/`, `core/training/`**. Ne pas spec/coder un module avant d'avoir confirmé qu'il n'a pas déjà été codé et débranché par Dom dans une session antérieure.
## §1 — Schéma de la chaîne attendue
```
[Phase 1 — Capture]
PC Windows agent_v1 → push frames + actions + events
data/training/live_sessions/<machine>/<session>/
├ shots/*.png
├ actions.jsonl
└ events.jsonl
finalize() côté api_stream.py
enqueue → data/training/_worker_queue.txt ← ⚠️ EXISTE, vide depuis 5 jours
[Phase 2 — Enrichissement post-session (worker VLM)]
run_worker.py poll _worker_queue.txt (10s interval)
StreamProcessor.reprocess_session()
├ ScreenAnalyzer (VLM lecture sémantique)
├ CLIPEmbedder (embeddings UI)
├ FAISS index update
├ _enrich_actions_with_intentions (Ollama gemma4 → intention/avant/après)
└ GraphBuilder (transitions états)
data/training/.../enriched_*.jsonl + index.faiss
[Phase 3 — Construction WorkflowIR]
build_replay_from_raw_events() → WorkflowIR
data/workflows/<session_id>.json OU data/competences/candidate/<slug>.yaml
selon le chemin (legacy workflow vs nouveau cycle Léa-first 01/06)
[Phase 4 — Apprentissage continu (CŒUR DU DÉBAT)]
┌──────────────────────────────────────────────────────┐
│ ContinuousLearner (644 lignes, ORPHELIN) │
│ ├ EMA online sur prototypes │
│ ├ Détection dérive UI (variance temporelle) │
│ ├ Variantes de prototypes (clustering) │
│ ├ TargetMemoryStore : mémoire des éléments cibles │
│ └ PrototypeVersionManager : versioning rollback │
│ │
│ FeedbackProcessor (176 lignes, ORPHELIN) │
│ ├ Boucle feedback humain → ajustement prototype │
│ └ Fusion observations multiples → compétence │
└──────────────────────────────────────────────────────┘
| Doit être déclenché par : nouvelle session retraitée
| Doit produire : score confiance, variantes, dérive
data/learning/prototypes_<state>.json (présent ? à vérifier disque)
data/learning/feedback_log.jsonl (présent ? à vérifier disque)
[Phase 5 — Boucle retour healing]
┌──────────────────────────────────────────────────────┐
│ RecoveryLogger (ORPHELIN runtime hors VWB) │
│ SelfHealingIntegration (VWB seulement) │
└──────────────────────────────────────────────────────┘
[Phase 6 — Utilisation au replay (HOT PATH)]
resolve_engine.py :
cascade OCR → template matching → VLM grounding
(PAS de consultation des prototypes ContinuousLearner) ← rupture
(PAS de consultation FAISS index appris) ← rupture
Si compétence avec .semantic.yaml (Phase 2.5 nouveau 01/06) :
Phase25Analyzer.match_screen() → annotations sémantiques
[Phase 7 — Fine-tuning VLM (HORS rpa_vision_v3)]
~/ai/t2a-finetune/, ~/ai/t2a/, ~/ai/t2a_v2/
Modèle custom Dom : qwen2.5vl:7b-rpa
Dataset alimenté manuellement (probablement) ← non-vérifié
```
## §2 — État par phase
| Phase | Code existe | Wired runtime | Dernière utilisation effective | Verdict |
|---|---|---|---|---|
| 1. Capture live | ✅ | ✅ | aujourd'hui (sessions live actives) | OK |
| 1bis. Enqueue worker | ✅ (`finalize()` `api_stream.py:2253+`) | ⚠️ **à diagnostiquer** | Probablement débranché — queue vide 5j alors que sessions live continuent | ⚠️ R6 critique |
| 2. Worker VLM post-session | ✅ (`run_worker.py`) | ✅ (réveillé aujourd'hui 18:54 PID 4054092) | **0 session traitée depuis 5 jours** | ⚠️ tourne à vide |
| 2bis. Enrichissement actions | ✅ (`stream_processor.py:1643`) | ✅ (au build) | continu, mais perd valeur sans ContinuousLearner | OK partiel |
| 3. Construction WorkflowIR | ✅ | ✅ (au moment du build) | aujourd'hui (P1-LEA-SHADOW livré) | OK nouveau cycle |
| **4. ContinuousLearner** | ✅ **644 lignes** | ❌ **ORPHELIN** | Jamais appelé en runtime | 🔴 **DÉBRANCHÉ** |
| **4bis. FeedbackProcessor** | ✅ **176 lignes** | ❌ **ORPHELIN** | Jamais appelé | 🔴 **DÉBRANCHÉ** |
| **4ter. PrototypeVersionManager** | ✅ | ❌ ORPHELIN (dép ContinuousLearner) | Jamais | 🔴 DÉBRANCHÉ |
| 5. RecoveryLogger | ✅ | ❌ ORPHELIN hors VWB | VWB seulement | 🔴 DÉBRANCHÉ runtime agent_v1 |
| 6. Replay hot path | ✅ | ✅ | actif | OK fonctionnel mais déconnecté de Phase 4 |
| 6bis. Phase 2.5 sémantique | ✅ (livré 20:15) | ✅ (endpoint dispo) | aujourd'hui | OK nouveau |
| 7. Fine-tuning VLM | ✅ hors repo (siblings) | n/a (asynchrone manuel) | inconnu | hors scope audit interne |
## §3 — Ruptures identifiées
| ID | Rupture | Sévérité | Détail | Conséquence POC |
|---|---|---|---|---|
| **R1** | ContinuousLearner orphelin | 🟡 MOYENNE | EMA online, dérive UI, variantes prototypes : **tous existent mais non câblés**. Couvre exactement le besoin "auto-évaluation par répétition" exprimé par Dom 2026-06-01 20:46. | Pas d'apprentissage incrémental cross-session. Léa ne s'améliore pas avec l'usage. |
| **R2** | PrototypeVersionManager orphelin | 🟡 MOYENNE | Dépend de R1. Versioning prototypes + rollback. Couvre "versioning des adaptateurs UI" demandé par Dom 21:27. | Pas de rollback prototype dégradé. Pas de gestion versions UI. |
| **R3** | FeedbackProcessor orphelin | 🟠 LOURDE | Boucle feedback humain → ajustement prototype. **Cœur de la fusion/regroupement vers compétence immuable** demandée par Dom 21:27. | Léa Phase 4 humaine (corrections) ne nourrit pas le modèle. Apprentissage repart de zéro à chaque session. |
| **R4** | RecoveryLogger orphelin runtime | 🟢 FAIBLE | Healing limité à VWB. Pas de retour boucle sur sessions agent_v1 ratées. | Workflows en échec récurrent ne génèrent pas d'insights actionnables. |
| **R5** | Phase 2.5 sémantique (livrée aujourd'hui) → utilisation au replay incertaine | 🟢 FAIBLE | `.semantic.yaml` produit mais utilisé seulement si Phase25Analyzer.match_screen() est consultée. Wiring à confirmer. | Annotations sémantiques apprises peut-être pas exploitées au replay. |
| **R6** | **Worker queue vide depuis 5 jours malgré sessions live actives** | 🔴 **CRITIQUE** | Le worker tourne (PID 4054092 actif) mais `data/training/_worker_queue.txt` est vide. **Soit `finalize()` n'enqueue plus, soit toutes les sessions échouent silencieusement à se finaliser, soit la queue est purgée ailleurs.** À diagnostiquer URGENT. | **0 enrichissement profond depuis 5 jours**. Toutes les sessions live actuelles sont stockées brutes sans ScreenAnalyzer/CLIP/FAISS/GraphBuilder. POC Wallerstein impossible en l'état. |
## §4 — Modules orphelins inventaire
### O1 — `core/learning/continuous_learner.py` (644 lignes)
- **Rôle** : adaptation incrémentale des prototypes UI par EMA online, détection de dérive temporelle, génération de variantes par clustering
- **Importé par** : 0 point d'entrée actif (vérifié par grep)
- **Dernière modification git** : à confirmer
- **Pourquoi débranché** : inconnu — pas de commit `disable` ou `remove` visible. Probablement n'a jamais été câblé en runtime depuis sa création (intention de wiring jamais finalisée).
- **Effort rebranchement** : **MOYEN** (2-3 j-h). Nécessite :
- Hook dans `run_worker.py` après `reprocess_session()` pour appeler `learner.update(prototypes, new_observations)`
- Chargement initial des prototypes au démarrage worker
- Persistance prototypes mis à jour : `data/learning/prototypes_<state>.json`
- Tests intégration : sessions répétées sur même UI → vérification EMA progresse
### O2 — `core/learning/feedback_processor.py` (176 lignes)
- **Rôle** : intègre les feedbacks humains (validate/correct/undo Phase 4 Léa-first) dans le modèle prototype
- **Importé par** : 0 point d'entrée actif
- **Effort rebranchement** : **LOURD** (3-5 j-h). Nécessite :
- Hook dans `agent_chat/handlers/learn_action.py` (livré aujourd'hui) à chaque `POST /shadow/feedback`
- Routage : feedback → FeedbackProcessor → ContinuousLearner
- Persistance log : `data/learning/feedback_log.jsonl`
- Tests : validation step → prototype renforcé ; correction → prototype variante créée
### O3 — `PrototypeVersionManager`
- **Rôle** : versionner les prototypes successifs, permettre rollback si nouvelle version dégrade
- **Dépendance** : ContinuousLearner (utilise pour stocker versions)
- **Effort rebranchement** : **FAIBLE** (1 j-h) une fois O1 rebranché
- **Couvre la décision Dom 21:27** : « Ce qu'il faut versionner/invalider, ce sont plutôt mappings UI propres à une application/version, sélecteurs/positions/labels OCR, hypothèses fragiles ou obsolètes, compétence mal validée »
### O4 — `TargetMemoryStore`, `VersionedStore`
- **Rôle** : supports de persistance pour les prototypes versionnés
- **Effort** : couvert par O1+O3
### O5 — `RecoveryLogger` / `SelfHealingIntegration`
- **Statut** : utilisé par VWB seulement, pas par agent_v1 runtime
- **Effort rebranchement runtime agent_v1** : MOYEN (2 j-h)
- **Priorité** : P2 (post-MVP), pas critique POC
## §5 — Worker queue R6 — diagnostic urgent
**Constat** : worker actif PID 4054092 depuis 18:54, log indique poll toutes les 10s sur `_worker_queue.txt`. Mais **0 traitement** depuis le réveil.
**Hypothèses à vérifier (par ordre de probabilité)** :
1. **`finalize()` côté `api_stream.py:2253+` n'enqueue plus** (commit qui a cassé le pipeline)
2. Les sessions live actuelles ne sont **jamais finalisées** (problème côté agent_v1 Windows qui ne push pas la fin de session)
3. La queue est **purgée par un autre processus** (cron ? cleanup ?)
4. Path resolution : worker poll un fichier inexistant ou un chemin différent de celui où `finalize()` écrit
**Action recommandée** : lancer un agent Explore ciblé sur :
- `git log -p agent_v0/server_v1/api_stream.py | grep -A 20 "finalize\|_worker_queue"` pour voir les commits récents touchant à la queue
- Tester manuellement : finaliser une session test, vérifier que le fichier `_worker_queue.txt` est touché, vérifier que le worker la dépile
- Identifier où la rupture est exacte
## §6 — Effort global de rebranchement
| Composant | Effort | Priorité POC Wallerstein |
|---|---|---|
| **R6 — Diagnostic worker queue** | Faible (1-2 j-h) | 🔴 **P0 ABSOLU** (sinon POC impossible) |
| O1+O3+O4 — ContinuousLearner + Versioning | Moyen (2-3 j-h) | 🟠 P1 (apprentissage incrémental) |
| O2 — FeedbackProcessor | Lourd (3-5 j-h) | 🟠 P1 (fusion compétences) |
| O5 — RecoveryLogger runtime | Moyen (2 j-h) | 🟢 P2 (post-MVP) |
| **Total rebranchement (P0+P1)** | **6-10 j-h** | À comparer aux ~15 j-h de spécifications/impl P1 d'aujourd'hui qui les reconstruisaient partiellement de zéro |
## §7 — Conséquences POC Wallerstein
### En l'état actuel (rien rebranché)
-**Aucune session traitée par le pipeline d'enrichissement profond depuis 5 jours** (worker tourne à vide). Cumulé des sessions live brutes accumulées : ScreenAnalyzer, CLIP, FAISS, GraphBuilder pas appliqués.
-**Pas d'apprentissage incrémental** : chaque démo Léa = repart de zéro. Pas d'auto-évaluation par répétition (alors que Dom le demande explicitement 20:46).
-**Pas de versioning prototypes** : si Easily/DPI change d'interface, pas de mécanisme de rollback. (Alors que Dom le demande 21:27.)
-**Pas de portabilité du modèle appris** : pas de paquet portable de réflexes/compétences/schémas/détecteurs/mappings, car ce paquet est produit par la chaîne d'apprentissage qui est débranchée. (Alors que Dom dit « point essentiel » 21:27.)
- ✅ Restitution Option C livrée aujourd'hui (P1-LEA-SHADOW) mais **trop longue** pour sessions 1-2h (recadrage Dom 20:46)
- ✅ Phase 2.5 sémantique livrée mais **ne produit pas encore les signaux de confiance/regroupement** demandés (recadrage Dom 20:46)
### Avec rebranchement P0+P1 (6-10 j-h)
- ✅ Worker pipeline actif → toutes les sessions live enrichies (ScreenAnalyzer/CLIP/FAISS/GraphBuilder)
- ✅ ContinuousLearner alimenté → apprentissage par répétition automatique
- ✅ FeedbackProcessor branché → fusion progressive vers compétences immuables
- ✅ PrototypeVersionManager actif → versioning mappings UI, rollback si dégradation
- ⚠️ Reste à ajouter : enrichir P1-LEA-SHADOW et P1-SEMANTIQUE avec champs `confidence`, `uncertainties[]`, `repetition_count`, distinctions `hypothesis`/`candidate`/`validated` (≈2-3 j-h additionnels)
- ⚠️ Reste à concevoir : paquet portable séparé de la mémoire patient (décision Dom 21:27 — pas encore couvert par aucune impl, ni avant aujourd'hui ni dans mes livraisons P1) (≈3-5 j-h)
## §8 — Plan d'action recommandé
### Étape A — IMMÉDIAT (avant tout nouveau dev)
1. **Diagnostic R6 worker queue** (1-2 j-h) : pourquoi vide depuis 5 jours malgré sessions live actives
2. **Audit factuel modules orphelins** : confirmer le bon état du code de `ContinuousLearner`, `FeedbackProcessor`, `PrototypeVersionManager` (pas de bug bloquant, signatures à jour vis-à-vis du reste du codebase)
3. **Lecture par Dom** des modules orphelins pour confirmer qu'ils correspondent bien à son intention historique
### Étape B — REBRANCHEMENT (P0+P1)
4. Rebrancher worker queue (R6) — code probablement minimal, action chirurgicale
5. Rebrancher ContinuousLearner + supports (O1+O3+O4) avec tests intégration
6. Rebrancher FeedbackProcessor (O2) + hook dans `agent_chat/handlers/learn_action.py` (livré aujourd'hui) à chaque `POST /shadow/feedback`
### Étape C — AJUSTEMENTS LIVRAISONS P1
7. Ajouter `confidence`, `uncertainties[]`, `repetition_count`, `hypothesis/candidate/validated` aux SessionState + payload persist
8. Phase 2.5 sémantique : enrichir pour produire signaux confiance + regroupement (actions stables vs parasites, invariants vs variables, blocs récurrents)
9. Option C restitution : raccourcir à « centré incertitudes uniquement », jamais relecture complète
### Étape D — PORTABILITÉ (objectif Dom essentiel)
10. Concevoir paquet portable : export réflexes/compétences/schémas/détecteurs/mappings/plans d'action/métriques, **sans** mémoire patient ni captures brutes
11. Mécanisme d'import sur poste tiers
12. Validation : aucune trace patient dans le paquet exporté
## §9 — Sources de l'audit
- Agent Explore Claude — 2026-06-01 21:50 (audit primaire)
- Audit Claude antérieur 17:00 (`feedback_lea_principes_techniques.md`) qui avait flagué `ContinuousLearner` et `RecoveryLogger` comme orphelins (mais sans alarme suffisante)
- Audit Explore worker VLM 17:30 (Claude) qui avait confirmé que worker traite sessions finalisées
- 5 messages Codex 2026-06-01 20:46-21:37 relayant 5 décisions/clarifications Dom
- Code source `core/learning/`, `core/healing/`, `agent_v0/server_v1/`, `agent_chat/`
---
*Fin DRAFT — relecture Dom/Codex/Qwen attendue avant action.*
**Décision opérationnelle proposée à Dom** : suspendre tout nouveau dev de modules d'apprentissage Léa tant que (a) R6 worker queue n'est pas diagnostiqué + corrigé et (b) Dom n'a pas confirmé que les modules orphelins identifiés correspondent à son intention historique.

View File

@@ -0,0 +1,729 @@
# AUDIT — Migration Token Global → Token Par-Poste
## rpa_vision_v3 — POC Clinique Wallerstein
**Status** : DRAFT lecture seule — audit factuel — pas encore appliqué
**Date** : 2026-06-01
**Scope** : Identification des modifications pour passer d'un token API global (partagé entre tous les postes) à un token unique par-poste permettant la révocation chirurgicale indépendante
**Contexte** : POC sur ≤5 TIM pendant plusieurs mois ; passage de 3 mois (doc initiale) à durée longue → risque amplifié de compromission d'un poste
---
## 1. Source du Token Actuel — Facteurs Identifiés
### 1.1 Génération et Stockage Côté Serveur
**Fichier** : `agent_v0/server_v1/api_stream.py` (lignes 285-363)
| Point | Facteur |
|-------|---------|
| **Variable env** | `RPA_API_TOKEN` (obligatoire en production) |
| **Mode génération** | Lecture depuis l'env OBLIGATOIRE (`os.getenv("RPA_API_TOKEN", "").strip()`) |
| **Tolérance dev** | Génération aléatoire via `secrets.token_hex(32)` si `RPA_AUTH_DISABLED=true` |
| **Fail-closed** | Production : token manquant → `sys.exit(1)` immédiat, pas de génération silencieuse |
| **Scope du token** | **GLOBAL** : un seul token pour tous les postes clients (tous les TIM partagent la même valeur) |
| **Type de valeur** | Hex string de 32 caractères (e.g. `a1b2c3d4e5f6...`) |
| **Lifecycle** | Aucune rotation built-in ; révocation → rotation manuelle de la var env + redémarrage du serveur |
### 1.2 Distinction Admin/ReadOnly
**Constat** : **N'existe pas actuellement.**
- Un seul niveau de droit : droits R/W complets
- Tous les postes ont capacité à : capturer screenshot, invoquer VLM, lire/exécuter workflows
- Aucune notion de scope ou permission granulaire par token
### 1.3 Lecture au Démarrage du Serveur
**Code** (api_stream.py:301-328) :
```python
_API_TOKEN_ENV = os.environ.get("RPA_API_TOKEN", "").strip()
if _AUTH_DISABLED:
API_TOKEN = _API_TOKEN_ENV or secrets.token_hex(32)
elif not _API_TOKEN_ENV:
logger.critical("[SÉCURITÉ] FATAL — RPA_API_TOKEN est absent ou vide. ...")
sys.exit(1)
else:
API_TOKEN = _API_TOKEN_ENV
```
**Impact** : Token chargé une seule fois au boot, inchangé pendant toute la session du serveur.
---
## 2. Fabrication du ZIP Installeur Léa — Flux Token
### 2.1 Pipeline de Téléchargement du ZIP
**Endpoint** : `web_dashboard/app.py``POST /api/fleet/download/<machine_id>` (ligne 2156)
**Flux** :
```
1. Dashboard Frontend (ui) demande au Dashboard Backend (5001)
2. Backend lit la liste des agents enregistrés (AgentRegistry SQLite)
3. Requête POST vers serveur streaming (5005) : /api/v1/agents/fleet
4. Streaming renvoie liste agents (enrolled_agents table)
5. Backend construit config.txt avec :
- RPA_SERVER_URL (toujours conforme, finit par /api/v1)
- RPA_API_TOKEN = valeur globale unique (tous les postes)
- RPA_MACHINE_ID = fourni par l'UI
- RPA_USER_LABEL = nom du collaborateur
```
### 2.2 Source du Token Injecté dans le ZIP
**Fichier** : `web_dashboard/app.py` (ligne 2183)
```python
api_token = os.environ.get('RPA_API_TOKEN', '') # <- MÊME VALEUR pour tous les postes
custom_config = _build_custom_config(machine_id, user_name, api_token)
```
**Constat** :
- Le token global de l'environnement du Dashboard (5001) est utilisé
- **Tous les ZIPs téléchargés contiennent le même token** (pas de diversification par poste)
- Aucune génération de token unique lors du POST /download
### 2.3 Machine ID — Source et Validation
**Provenance du machine_id** :
- Côté **client Windows** (Léa.bat / install.bat) : Inno Setup génère un UUID via la page custom d'enrollment
- Côté **serveur** : Enrôlement possible sans vérification du machine_id source (endpoint `/api/v1/agents/enroll` accepte n'importe quel machine_id du client)
**Validation** :
- Pas de vérification cryptographique du machine_id (e.g. pas de signature pour prouver l'authenticité)
- Le machine_id est traité comme **identifiant de confiance** après enrôlement (logging, last_seen_at)
### 2.4 Endpoint d'Enrôlement — État Actuel
**Endpoint** : `agent_v0/server_v1/api_stream.py``POST /api/v1/agents/enroll` (ligne 6638)
| Aspect | État Actuel |
|--------|-------------|
| **Qui génère le token ?** | Dashboard Backend (lit env `RPA_API_TOKEN`) |
| **Le client enrôlé reçoit quoi ?** | Le token global dans la réponse `/api/v1/agents/enroll` (ligne 6696) |
| **Existe-t-il un endpoint qui génère token unique ?** | Non. Phase 2 signalée dans le commentaire (ligne 6647) |
| **Stockage des tokens par-poste ?** | Non. Table `enrolled_agents` enregistre le poste mais pas le token |
**Code** (api_stream.py:6688-6698) :
```python
return {
"status": "enrolled",
"created": result["created"],
"reactivated": result["reactivated"],
"machine_id": machine_id,
# Phase 1 : on renvoie le token global pour que le client puisse
# verifier qu'il est bien aligne avec le serveur. Phase 2 pourra
# emettre un token par poste (issued_token != API_TOKEN global).
"api_token": API_TOKEN, # <- GLOBAL
"agent": agent,
}
```
---
## 3. Validation du Token Côté Serveur
### 3.1 Middleware de Vérification
**Fichier** : `agent_v0/server_v1/api_stream.py` (ligne 351)
```python
async def _verify_token(request: Request):
"""Middleware de vérification du token API Bearer."""
if _AUTH_DISABLED:
return
if request.url.path in _PUBLIC_PATHS:
return
auth = request.headers.get("Authorization", "")
if not auth.startswith("Bearer ") or auth[7:] != API_TOKEN:
raise HTTPException(status_code=401, detail="Token API invalide")
```
**Constat** :
- **Comparaison à une constante** unique (pas de lookup table)
- Tous les endpoints protégés (sauf 5 publics) nécessitent ce token exact
- Le token est envoyé dans chaque requête HTTP : `Authorization: Bearer <token>`
### 3.2 Granularité des Droits
**Constat** : **Un seul niveau : R/W complet**
- Pas de distinction admin/user/readonly
- Pas de scope granulaire (e.g. "capture seulement" vs "exécution workflow")
- Tous les agents ayant le bon token peuvent accéder à toutes les fonctions
### 3.3 Machine ID et Audit
**Machine_id dans les requêtes** :
Le client (Léa) envoie le machine_id dans les headers ou dans le body de certaines requêtes. Côté serveur :
- `audit_trail.record(AuditEntry(..., machine_id=client_provided_machine_id, ...))` (enregistré, pas vérifié)
- `agent_registry.touch_last_seen(machine_id)` (mise à jour timestamp)
**Risque** : Aucune vérification que le machine_id envoyé par le client correspond au token utilisé. Un client pourrait usurper l'identité d'une autre machine en envoyant un autre machine_id + le token global.
---
## 4. Points d'Appel Côté Agent Windows
### 4.1 Lecture de config.txt au Démarrage
**Fichier** : `agent_v0/agent_v1/config.py` (ligne 57)
```python
API_TOKEN = os.environ.get("RPA_API_TOKEN", "")
```
Le token vient de trois sources (ordre de priorité) :
1. Variable env `RPA_API_TOKEN` (posée par Lea.bat après parsing de config.txt)
2. Fichier `config.txt` dans le ZIP (contient le token global)
3. Défaut : chaîne vide (agent lancé sans token)
**Fichier** : `deploy/lea_package/Lea.bat` (ligne 40-43)
```batch
if exist "config.txt" (
for /f "usebackq eol=# tokens=1,* delims==" %%a in ("config.txt") do (
if not "%%a"=="" if not "%%b"=="" set "%%a=%%b"
)
)
```
Lea.bat parse config.txt ligne par ligne et pose les variables d'environnement (y compris `RPA_API_TOKEN`).
### 4.2 Envoi du Token dans les Requêtes
**Streaming WebSocket** : `agent_v0/agent_v1/network/streamer.py` (ligne ~80-90)
```python
from ..config import API_TOKEN, STREAMING_ENDPOINT
# Dans la fonction d'auth :
if API_TOKEN:
return {"Authorization": f"Bearer {API_TOKEN}"}
```
**Requêtes REST (executor)** : `agent_v0/agent_v1/core/executor.py`
```python
if API_TOKEN:
headers["Authorization"] = f"Bearer {API_TOKEN}"
```
**Constat** :
- Token présent dans **chaque** requête (REST + WebSocket streaming)
- Format standard Bearer : `Authorization: Bearer <token_value>`
- Aucune logique de refresh ou renouvellement
### 4.3 Renouvellement / Refresh du Token
**Constat** : **N'existe pas.**
- Token lu une seule fois au démarrage de l'agent
- Aucun mécanisme pour redemander un token au serveur sans redémarrer l'agent
- Révocation d'un token global = tous les agents existants deviennent invalides immédiatement (pas de transition gracieuse)
---
## 5. Architecture Cible : Token Par-Poste
### 5.1 Schéma de Stockage Côté Serveur
**Nouvelle table SQLite** dans `data/databases/rpa_data.db` :
```sql
CREATE TABLE IF NOT EXISTS postes_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
machine_id TEXT NOT NULL UNIQUE,
token_hash TEXT NOT NULL, -- SHA256(token)
token_readable TEXT NOT NULL, -- token lisible, jamais loggé
status TEXT DEFAULT 'active', -- 'active', 'revoked'
created_at TEXT NOT NULL, -- ISO 8601 UTC
revoked_at TEXT, -- ISO 8601 UTC si révoqué
rotated_at TEXT, -- ISO 8601 UTC si régénéré
last_seen_at TEXT, -- Dernière utilisation
rotation_reason TEXT -- 'manual_revoke', 'rotation_request', etc.
);
CREATE INDEX idx_postes_tokens_machine_id ON postes_tokens(machine_id);
CREATE INDEX idx_postes_tokens_status ON postes_tokens(status);
```
**Co-habitation** : Table dans la même DB que `enrolled_agents` (même `data/databases/rpa_data.db`).
### 5.2 Endpoint d'Enrôlement Amélioré
**Route modifiée** : `POST /api/v1/agents/register` (nouvelle, plutôt que d'écraser `/agents/enroll`)
```python
@app.post("/api/v1/agents/register", status_code=201)
async def agents_register(request: AgentRegisterRequest):
"""
Enrôlement du poste + génération du token unique.
Comportement :
- Enregistre le poste (ou le réactive) dans enrolled_agents
- Génère un token unique via secrets.token_urlsafe(32)
- Stocke le hash SHA256 du token dans postes_tokens
- Retourne le token UNE SEULE FOIS dans la réponse
Client doit sauvegarder ce token dans config.txt immédiatement.
Aucune autre route ne renvoie le token en clair.
"""
machine_id = (request.machine_id or "").strip()
if not machine_id:
raise HTTPException(status_code=400, detail="machine_id obligatoire")
# 1. Enregistrer le poste (ou le réactiver)
try:
result = agent_registry.enroll(machine_id=machine_id, ...)
except AgentAlreadyEnrolledError:
# Poste déjà actif : générer un nouveau token pour rotation
pass
# 2. Générer token unique
token_value = secrets.token_urlsafe(32) # e.g. "aB1_cD2-eF3...XyZ"
token_hash = hashlib.sha256(token_value.encode()).hexdigest()
# 3. Insérer ou mettre à jour dans postes_tokens
postes_registry.create_or_rotate(
machine_id=machine_id,
token_hash=token_hash,
token_readable=token_value, # Stocké temporairement
rotation_reason="enrollment"
)
# 4. Retourner le token UNE SEULE FOIS
return {
"status": "registered",
"machine_id": machine_id,
"token": token_value, # <- À SAUVEGARDER IMMÉDIATEMENT
"expires_in": "never", # Sans expiration, mais révocable à tout moment
"agent": {...}
}
```
### 5.3 Modification du Middleware d'Auth
**Fichier** : `agent_v0/server_v1/api_stream.py` (remplacer `_verify_token`)
```python
async def _verify_token(request: Request):
"""Middleware de vérification du token par-poste.
Lookup du token dans postes_tokens au lieu de comparaison constante.
Cache mémoire pour éviter hammer SQLite à chaque requête.
"""
if _AUTH_DISABLED:
return
if request.url.path in _PUBLIC_PATHS:
return
auth = request.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Token invalide")
token_value = auth[7:]
token_hash = hashlib.sha256(token_value.encode()).hexdigest()
# 1. Chercher dans le cache (dict en mémoire)
if token_hash in _token_cache:
cached = _token_cache[token_hash]
if cached["status"] != "active":
raise HTTPException(status_code=401, detail="Token révoqué")
cached["last_used"] = time.time()
request.scope["machine_id"] = cached["machine_id"] # Injecter pour l'audit
return
# 2. Chercher en DB
row = postes_registry.get_by_hash(token_hash)
if row is None or row["status"] != "active":
raise HTTPException(status_code=401, detail="Token invalide")
# 3. Mettre en cache
_token_cache[token_hash] = {
"machine_id": row["machine_id"],
"status": row["status"],
"last_used": time.time(),
}
# Limiter la taille du cache (LRU, max 1000 tokens)
if len(_token_cache) > 1000:
oldest = min(_token_cache.items(), key=lambda x: x[1]["last_used"])
del _token_cache[oldest[0]]
# Injecter machine_id pour l'audit
request.scope["machine_id"] = row["machine_id"]
return
```
### 5.4 Endpoint de Révocation
**Route** : `POST /api/v1/postes/<machine_id>/revoke` (sécurisé par auth Bearer)
```python
@app.post("/api/v1/postes/{machine_id}/revoke")
async def revoke_poste(machine_id: str):
"""
Révoque le token du poste (soft delete, audit conservé).
Dashboard ou administrateur : révoque un poste compromis.
"""
# Vérifier que le token Bearer utilisé a le droit de révoquer
# (optionnel : admin-only, ou même machine_id peut révoquer son propre token)
row = postes_registry.revoke(machine_id=machine_id, reason="admin_revoke")
if row is None:
raise HTTPException(status_code=404, detail="Machine not found")
# Invalider le cache
if row["token_hash"] in _token_cache:
del _token_cache[row["token_hash"]]
logger.info(f"[SECURITY] Poste révoqué : {machine_id}")
return {"status": "revoked", "machine_id": machine_id}
```
### 5.5 Endpoint de Régénération
**Route** : `POST /api/v1/postes/<machine_id>/rotate` (pour l'utilisateur du poste)
```python
@app.post("/api/v1/postes/{machine_id}/rotate")
async def rotate_poste_token(machine_id: str):
"""
L'utilisateur du poste demande un nouveau token (le vieil est invalidé).
Retourne le nouveau token (à injecter dans config.txt).
"""
# Vérifier que le token actuel appartient à ce machine_id
# (via injection dans request.scope par le middleware)
new_token = secrets.token_urlsafe(32)
new_hash = hashlib.sha256(new_token.encode()).hexdigest()
postes_registry.rotate(
machine_id=machine_id,
new_token_hash=new_hash,
rotation_reason="user_request"
)
# Invalider l'ancien token du cache
old_row = postes_registry.get_by_machine_id(machine_id)
if old_row and old_row["token_hash"] in _token_cache:
del _token_cache[old_row["token_hash"]]
return {
"status": "rotated",
"machine_id": machine_id,
"new_token": new_token,
"hint": "Mettez à jour config.txt avec ce nouveau token"
}
```
### 5.6 UI Dashboard — Onglet Postes
**Nouvelle page** : `/templates/fleet_management.html`
Fonctionnalités :
- Liste des postes actifs (machine_id, user_name, last_seen_at, enrolled_at)
- Liste des postes révoqués (avec date révocation + raison)
- Boutons par poste :
- **Révoquer** : POST `/api/v1/postes/{machine_id}/revoke` → impossibilité pour le poste de se connecter
- **Régénérer token** : POST `/api/v1/postes/{machine_id}/rotate` → nouveau token à faire entrer sur le poste
- **Afficher détails** : historique de révocation/rotation
- Filtres : statut (actif/révoqué), plage de dates, collaborateur
---
## 6. Chiffrage Qualitatif de la Migration
### 6.1 Fichiers à Modifier
| Fichier | Modification | Effort | Risque |
|---------|--------------|--------|--------|
| `agent_v0/server_v1/api_stream.py` | Remplacer `_verify_token`, ajouter `_token_cache`, endpoints `/api/v1/postes/*/revoke` et `/rotate` | **Moyen** (~200-250 lignes) | Moyen : tous les endpoints dépendent du middleware |
| `agent_v0/server_v1/agent_registry.py` | Ajouter classe `PostesTokenRegistry` (CRUD postes_tokens table) | **Moyen** (~150-200 lignes) | Faible : isolé, pas d'impact existant |
| `agent_v0/server_v1/` (nouveau) | `postes_registry.py` (classe PostesTokenRegistry) | **Moyen** (~150-200 lignes) | Faible : nouveau module |
| `web_dashboard/app.py` | Remplacer `_build_custom_config` + endpoint `/api/fleet/download` pour générer token unique | **Moyen** (~100-150 lignes) | Moyen : impact sur téléchargement ZIP |
| `web_dashboard/templates/` | Ajouter `fleet_management.html` (onglet postes) | **Moyen** (~300-400 lignes HTML/JS) | Faible : interface nouvelle, pas de régression |
| `agent_v0/agent_v1/config.py` | Pas de modification (lecture env inchangée) | **Aucun** | Aucun |
| `deploy/installer/Lea.iss` | Pas de modification (Inno Setup pose la var env) | **Aucun** | Aucun |
**Total effort** : ~1000-1300 lignes de code nouveau / modifié
### 6.2 Migration Progressive en 3 Étapes
#### Étape A : Fondations (1-2 jours)
**Objectif** : Ajouter la table + le registre, mais le serveur accepte les 2 modes.
1. Créer table `postes_tokens` + index dans `data/databases/rpa_data.db`
2. Implémenter classe `PostesTokenRegistry` (CRUD simple)
3. Modifier `_verify_token` pour :
- D'abord chercher le token en par-poste (lookup table)
- Si non trouvé, faire un fallback sur le mode global (API_TOKEN)
4. Logger chaque validation en clair : `[AUTH] Token trouvé : par-poste | global`
**Pre-requis** : Aucun
**Durée estimée** : 1-2 jours
**Point de non-retour** : Non (le fallback permet de revenir en arrière)
**Impact utilisateurs** : Nul (transparent, les agents existants continuent avec le global)
**Code exemple (middleware)**:
```python
async def _verify_token(request: Request):
auth = request.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
raise HTTPException(status_code=401)
token_value = auth[7:]
token_hash = hashlib.sha256(token_value.encode()).hexdigest()
# Mode 1 : Par-poste
row = postes_registry.get_by_hash(token_hash)
if row and row["status"] == "active":
logger.info(f"[AUTH] Token par-poste validé : {row['machine_id']}")
request.scope["machine_id"] = row["machine_id"]
return
# Mode 2 : Global (fallback, phase 1)
if token_value == API_TOKEN:
logger.info(f"[AUTH] Token global validé (fallback phase 1)")
return
raise HTTPException(status_code=401, detail="Token invalide")
```
#### Étape B : Redéploiement sur Postes Wallerstein (3-5 jours)
**Objectif** : Migrer les 5 postes du POC en par-poste (ZIPs avec tokens uniques).
1. Nouvelle version du Dashboard : endpoint `/api/v1/agents/register` qui génère tokens uniques
2. Modifier `/api/fleet/download/<machine_id>` pour injecter le token unique (pas le global)
3. Sur chaque poste Wallerstein :
- Télécharger le nouveau ZIP via le Dashboard
- Extraire et remplacer config.txt
- Redémarrer Léa
**Pre-requis** : Étape A complète et testée
**Durée estimée** : 3-5 jours (déploiement sur le terrain + tests)
**Point de non-retour** : OUI — une fois que tous les postes ont le par-poste, l'admin peut supprimer API_TOKEN global
**Impact utilisateurs** : Très faible (ZIPs pré-générés, transparents pour les utilisateurs)
**Checklist de déploiement** :
- [ ] Tester endpoint `/api/v1/agents/register` en lab
- [ ] Générer 5 ZIPs avec tokens uniques (une clé par poste)
- [ ] Vérifier que config.txt contient le bon token pour chaque machine_id
- [ ] Déployer sur poste 1, vérifier dernière utilisation dans Dashboard
- [ ] Idem postes 2-5
- [ ] Vérifier historique audit : chaque poste tracé séparément
#### Étape C : Durcissement — Suppression du Global (1 jour)
**Objectif** : Désactiver le mode global, garantir que seule l'authentification par-poste fonctionne.
1. Retirer le fallback global de `_verify_token`
2. Supprimer variable env `RPA_API_TOKEN` du bootstrap
3. Logger un FATAL si la variable est trouvée (debug)
4. Mettre à jour la doc : "Mode global dépréciéé depuis 2026-06-XX"
**Pre-requis** : Tous les postes en par-poste (Étape B complète)
**Durée estimée** : 1 jour
**Point de non-retour** : OUI — après ce point, les anciens agents (mode global) ne peuvent plus se connecter
**Impact utilisateurs** : ÉLEVÉ si Étape B incomplète (agents bloqués)
---
## 7. Risques Non Couverts Par la Migration
### 7.1 Vol Physique du PC TIM avec config.txt en Clair
**Risque** : Token stocké en clair dans le fichier `config.txt` (Windows, pas chiffré par défaut)
**Mitigations possibles** :
- DPAPI Windows : Chiffrer config.txt via `CryptProtectData` (nécessite clé utilisateur Windows)
- Stockage sécurisé : Léa peut demander token à la première utilisation (sans le persister)
- JWT court + refresh : Token courte durée de vie (~30 min) + refresh token distinct (mais complexe)
- Jeton hardware : Clé USB ou smartcard (hors scope POC)
**Recommandation** : Pour Wallerstein, utiliser DPAPI Windows (effort faible, sécurité augmentée).
### 7.2 Replay Attacks sur le Streaming WebSocket
**Risque** : Sans TLS strict (HTTPS/WSS), un attaquant en MITM peut rejouer les requêtes vidées du token Bearer.
**Mitigations actuelles** :
- Reverse proxy NPM (déjà en place) expose lea.labs.laurinebazin.design en HTTPS
- WSS (WebSocket Secure) utilisé en production (non en lab)
**Recommandation** : Vérifier que tous les endpoints streaming utilisent WSS en production (config NPM).
### 7.3 Token Enregistré dans les Logs
**Risque** : Si Flask ou Uvicorn log les en-têtes HTTP en DEBUG, le token Bearer s'affiche en clair.
**Prévention** :
- Mode production (défaut) : `docs_url=None, redoc_url=None` (api_stream.py:469-471)
- Vérifier que les logs ne contiennent jamais `Authorization: Bearer ...`
**Commande de vérification** :
```bash
grep -r "Authorization.*Bearer" /path/to/logs/ # Ne doit retourner rien
grep -r "RPA_API_TOKEN" /path/to/logs/ # Ne doit retourner rien
```
### 7.4 Usurpation d'Identité : Machine_ID
**Risque** : Actuellement, aucune vérification que le machine_id envoyé par le client correspond au token utilisé.
**Scénario** : Client A possède token de poste A. Poste A envoie requête avec `machine_id=poste_B` → audit imputé à poste B.
**Mitigation dans l'architecture cible** :
- Dès que le token est validé, extraire automatiquement le machine_id depuis la table postes_tokens
- Rejecter toute tentative du client d'envoyer un machine_id différent
```python
async def _verify_token(request: Request):
# ... validation token ...
row = postes_registry.get_by_hash(token_hash)
# Injecter l'identité vraie dans la requête
request.scope["machine_id"] = row["machine_id"] # <- Vrai machine_id
request.scope["user_id"] = row["user_id"] # <- Vrai user_id
# Ne pas faire confiance au client pour l'identité
```
### 7.5 Absence de Rotation Forcée
**Risque** : Aucune rotation obligatoire (ex. tous les 90 jours). Un token compromis et non détecté reste valide indéfiniment.
**Mitigation pour Wallerstein** :
- Ajouter `last_rotation_at` et `rotation_period_days` à la table postes_tokens
- Endpoint `/api/v1/postes/{machine_id}/status` retourne un warning si rotation > 90 jours
- Dashboard affiche visuel "Token à renouveler" (jaune : 60j, rouge : >90j)
**Effort** : Moyen (~50-100 lignes)
---
## 8. Verdict et Recommandations
### Effort Global
**Catégorie** : **MOYEN** (4-8 jours-homme, 1000-1300 lignes, pas d'architecte externe requis)
- Fondations (Étape A) : 1-2 jours
- Déploiement terrain (Étape B) : 3-5 jours (nécessite accès aux postes Wallerstein)
- Durcissement (Étape C) : 1 jour
### Durée Estimée
| Phase | Durée | Bloqué Par |
|-------|-------|-----------|
| A (Fondations) | 1-2 j | Aucun |
| B (Déploiement) | 3-5 j | A complète |
| C (Durcissement) | 1 j | B complète |
| **TOTAL** | **5-8 j** | — |
### Dépendances Connues
**Aucune dépendance blocage avec autres chantiers actuels** :
- Worktree guard Qwen : Isolé, pas d'impact (module core/)
- Gap propagation verdict : Isolé (migration replay)
- Mismatch captures persistées : Isolé (API /traces/upload)
**Dépendance suggérée** : Implémenter mitigations § 7.1-7.5 après la migration (durcissement sécurité, effort marginal).
### Recommandation
**GO pour le POC Wallerstein** (approche par étapes) :
1. **Étape A maintenant** : Ajouter table + registre + middleware 2 modes (1-2 jours)
2. **Étape B début Wallerstein** : Redéployer les 5 postes avec tokens uniques (3-5 jours, pendant POC)
3. **Étape C post-POC** : Durcir si compromise d'un poste détectée, ou en fin de POC
Cette approche offre la **révocation chirurgicale** dès l'Étape B (capacité à révoquer un poste sans affecter les autres), tout en gardant une marche arrière jusqu'à la fin de l'Étape B.
---
## Annexe A : Schéma de Synthèse
```
┌─────────────────────────────────────────────────────────────┐
│ Système Actuel (Phase 1 — Token Global) │
├─────────────────────────────────────────────────────────────┤
│ • RPA_API_TOKEN = "a1b2c3d4...xyz" (1 seul, dans .env) │
│ • Tous les postes envoient le MÊME token │
│ • Aucune révocation fine (révoque tout ou rien) │
│ • Audit : machine_id non vérifiable (peut être usurpé) │
└─────────────────────────────────────────────────────────────┘
(Étape A : 1-2j)
┌─────────────────────────────────────────────────────────────┐
│ État Intermédiaire (Phase 1.5 — Dual Mode) │
├─────────────────────────────────────────────────────────────┤
│ • Table postes_tokens ajoutée │
│ • Middleware accepte tokens par-poste OU global │
│ • Anciens agents (global) : continuent à fonctionner │
│ • Nouveaux agents (par-poste) : peuvent être enrôlés │
└─────────────────────────────────────────────────────────────┘
(Étape B : 3-5j, terrain)
┌─────────────────────────────────────────────────────────────┐
│ Cible (Phase 2 — Token Par-Poste) │
├─────────────────────────────────────────────────────────────┤
│ • Poste A : token_A (unique) → machine_id_A │
│ • Poste B : token_B (unique) → machine_id_B │
│ • Poste C : token_C (unique) → machine_id_C │
│ • ... │
│ • Révocation poste A : ne touche pas B, C, D, E │
│ • Audit : machine_id garanti (extrait du token validé) │
└─────────────────────────────────────────────────────────────┘
(Étape C : 1j)
┌─────────────────────────────────────────────────────────────┐
│ Durcissement (Phase 3 — Suppression Global) │
├─────────────────────────────────────────────────────────────┤
│ • RPA_API_TOKEN supprimé du bootstrap │
│ • Middleware accepte UNIQUEMENT tokens par-poste │
│ • Anciens agents (global) : rejetés (404) │
└─────────────────────────────────────────────────────────────┘
```
---
## Annexe B : Checklist de Vérification Pré-Déploiement
- [ ] **DB** : Table postes_tokens créée et indexée, migration de la DB testée
- [ ] **Code** : Middleware dual-mode (par-poste + fallback global) compilé
- [ ] **Cache** : Vérifier que le cache mémoire ne fuit pas (taille bornée à 1000 tokens)
- [ ] **Audit** : Chaque validation loggée avec indication du mode (par-poste ou global)
- [ ] **Endpoint register** : Génère token unique, hash stocké en DB, token retourné UNE FOIS
- [ ] **Endpoint revoke** : Invalidation du token en DB + vidage du cache
- [ ] **Endpoint rotate** : Génère nouveau token, ancien invalidé
- [ ] **UI Dashboard** : Affichage liste postes (actifs + révoqués), boutons revoke/rotate
- [ ] **Tests unitaires** : Vérifier que token global + par-poste fonctionnent (Étape A)
- [ ] **Tests intégration** : Workflow complet (enroll → register → streaming → audit)
- [ ] **Logs de sécurité** : Aucun token en clair, aucune variable d'env loggée
- [ ] **Documentation** : Mise à jour du PLAYBOOK_DSI_RSSI avec procédure par-poste
---
**Document généré** : 2026-06-01
**Auteur** : Audit factuel (sans modifications de code)
**Validation requise avant implémentation** : Dom (chef projet)

View File

@@ -0,0 +1,313 @@
# requirements-dgx-aarch64.txt — DRAFT pour relecture Codex/Dom
**Statut** : DRAFT — pas encore appliqué. Aucun changement au `requirements.txt` réel.
**Date** : 2026-06-01
**Cible** : DGX Spark, Ubuntu 24.04 ARM64, CUDA 13 système, GB10 (sm_121), container Docker headless serveur.
**Source de vérité** : `docs/POC/PORTAGE_DGX_SPARK_2026-05-28.md` §5.
**Base** : `requirements.txt` actuel = 180 paquets — bench x86 `venv_v3/`.
---
## Règles appliquées
- PyTorch : `torch`, `torchvision` désépinglés, installés via `--index-url https://download.pytorch.org/whl/cu128` (binaire sm_120 compat sm_121).
- Wheels GPU NVIDIA (`nvidia-*-cu12`, 15 paquets) : **supprimés**, tirés transitivement par torch cu128 ARM.
- Capture / GUI / X11 : **supprimés** — container serveur headless ; l'agent capture tourne sur poste Linux client séparé.
- `onnx==1.20.1` : **supprimé** (audit dépendances : fantôme transitif).
- Reste : wheels neutres ARM/CPU **gardés tels quels** ; wheels torch-dependent **désépinglés** pour laisser pip résoudre.
---
## 1. GARDÉ TEL QUEL (paquets neutres ARM/CPU)
Wheels pur Python ou wheels manylinux2014_aarch64 / musllinux_aarch64 publiés sur PyPI. §5 RAS pour les binaires explicitement listés.
- **Web/API** : `fastapi`, `Flask`, `Flask-Caching`, `Flask-Cors`, `Flask-Migrate`, `Flask-SocketIO`, `Flask-SQLAlchemy`, `starlette`, `uvicorn`, `uvloop` (§5), `httptools`, `httpcore`, `httpx`, `h11`, `websockets`, `wsproto`, `simple-websocket`, `python-engineio`, `python-socketio`, `python-multipart`, `Werkzeug`, `Jinja2`, `itsdangerous`, `blinker`, `click`, `MarkupSafe`, `cachelib`, `watchfiles`, `anyio`, `bidict`
- **DB/migrations** : `SQLAlchemy`, `alembic`, `Mako`, `greenlet`, `redis`
- **ML/vision CPU** : `faiss-cpu` (§5), `opencv-python` (§5), `pillow`, `numpy`, `scipy`, `scikit-learn`, `matplotlib`, `contourpy`, `fonttools`, `kiwisolver`, `cycler`, `pyparsing`, `ml_dtypes`, `networkx`, `sympy`, `mpmath`, `joblib`, `threadpoolctl`, `RapidFuzz`, `shapely` (§5), `pyclipper` (§5), `h5py` (§5)
- **PDF/docs** : `pypdfium2` (§5), `lxml` (§5), `python-docx`, `openpyxl`, `et_xmlfile`, `defusedxml`, `anyascii`, `langdetect`, `ftfy`, `wcwidth`, `regex`
- **Ollama** : `ollama` (client HTTP pur Python)
- **Validation/config** : `pydantic`, `pydantic_core`, `annotated-doc`, `annotated-types`, `typing-inspection`, `typing_extensions`, `validators`, `marshmallow`, `jsonschema`, `jsonschema-specifications`, `referencing`, `rpds-py`, `attrs`, `PyYAML`, `python-dotenv`
- **HTTP/réseau** : `aiohttp`, `aiohappyeyeballs`, `aiosignal`, `frozenlist`, `multidict`, `propcache`, `yarl`, `requests`, `urllib3`, `charset-normalizer`, `idna`, `certifi`
- **Crypto/utils** : `cryptography`, `cffi`, `pycparser`, `filelock`, `fsspec`, `packaging`, `platformdirs`, `pathspec`, `six`, `python-dateutil`, `sortedcontainers`, `tqdm`, `Pygments`
- **Monitoring** : `prometheus_client`, `psutil`, `pynvml`, `nvidia-ml-py` (bindings NVML pur Python, pas un wheel CUDA toolkit)
- **Protobuf** : `protobuf` (§5)
- **Dev/tests** : `pytest`, `pytest-asyncio`, `pytest-cov`, `pytest-flask`, `pytest-mock`, `hypothesis`, `coverage`, `iniconfig`, `pluggy`, `black`, `flake8`, `mypy`, `mypy_extensions`, `mccabe`, `pycodestyle`, `pyflakes`
## 2. GARDÉ MAIS DÉSÉPINGLÉ (laisser pip résoudre avec torch cu128 ARM)
| Paquet | Version actuelle x86 | Raison |
|--------|----------------------|--------|
| `torch` | `2.9.1` | installé via index PyTorch cu128 ARM (étape 1 ci-dessous) |
| `torchvision` | `0.24.1` | idem, doit matcher torch cu128 ARM |
| `transformers` | `4.57.3` | dépend de torch — laisser résoudre |
| `accelerate` | `1.13.0` | dépend de torch |
| `timm` | `1.0.24` | dépend de torch |
| `open_clip_torch` | `3.2.0` | dépend de torch |
| `python-doctr` | `1.0.1` | dépend de torch + torchvision (§5 RAS) |
| `safetensors` | `0.7.0` | binding Rust, couplé à transformers/torch |
| `tokenizers` | `0.22.2` | binding Rust, couplé à transformers |
| `huggingface-hub` | `0.36.0` | couplé à transformers |
| `hf-xet` | `1.2.0` | binding Rust, wheel aarch64 à vérifier (cf. §4) |
## 3. SUPPRIMÉ
### 3a. Capture / GUI / X11 (serveur DGX headless)
`PyQt5`, `PyQt5-Qt5`, `PyQt5_sip`, `mss`, `PyAutoGUI`, `pynput`, `evdev`, `python-xlib`, `python3-xlib`, `pystray`, `PyGetWindow`, `PyMsgBox`, `PyScreeze`, `MouseInfo`, `pyperclip`, `pytweening`, `PyRect` — tous hors scope container serveur (§5 ligne 81 : non bloquant si non chargés). Restent sur le poste Linux client de l'agent capture.
### 3b. Wheels GPU NVIDIA épinglées x86 (15 paquets)
`nvidia-cublas-cu12`, `nvidia-cuda-cupti-cu12`, `nvidia-cuda-nvrtc-cu12`, `nvidia-cuda-runtime-cu12`, `nvidia-cudnn-cu12`, `nvidia-cufft-cu12`, `nvidia-cufile-cu12`, `nvidia-curand-cu12`, `nvidia-cusolver-cu12`, `nvidia-cusparse-cu12`, `nvidia-cusparselt-cu12`, `nvidia-nccl-cu12`, `nvidia-nvjitlink-cu12`, `nvidia-nvshmem-cu12`, `nvidia-nvtx-cu12` — §5 ligne 78 : désépinglés, retirés du requirements, tirés par torch cu128 ARM.
### 3c. Fantôme
`onnx==1.20.1` — audit runtime : non importé par le code server. `onnxruntime` déjà confirmé non utilisé (cf. brief Dom). Suppression nette.
## 4. À VÉRIFIER (validation Codex/Dom requise)
| Paquet | Question | Action proposée |
|--------|----------|-----------------|
| `triton` | §5 risque ligne 79 : PTXAS embarqué peut crasher avec CUDA 13 | Non listé (tiré par torch cu128) ; si KO au runtime : `export TRITON_PTXAS_PATH=/usr/local/cuda/bin/ptxas` |
| `hf-xet` | Wheel aarch64 publié au moment du build ? | Désépingler ; si non dispo, retirer (dep optionnelle de `huggingface-hub`) |
| `accelerate` | Version compatible torch cu128 ARM ? | Désépinglé (cat. 2) — flag pour test post-install |
| `onnx` (suppression) | Confirmé sur scope server/ + core/grounding/ + core/detection/ ? | `grep -r "^import onnx\|^from onnx" --include='*.py' server/ core/` avant build |
| `nvidia-ml-py` vs `pynvml` | Doublon (officiel vs fork tiers) | Probable retrait de `pynvml`, à arbitrer |
---
## 5. Bloc `requirements-dgx-aarch64.txt` proposé
Installation en 2 étapes (cu128 ARM **avant** le reste, sinon pip tire les wheels x86 du cache) :
```bash
# Étape 1 : PyTorch ARM cu128
pip install --no-cache-dir torch torchvision \
--index-url https://download.pytorch.org/whl/cu128
# Étape 2 : reste
pip install --no-cache-dir -r requirements-dgx-aarch64.txt
```
```txt
# requirements-dgx-aarch64.txt
# Cible : DGX Spark / Ubuntu 24.04 ARM64 / CUDA 13 / GB10 sm_121
# Pré-requis : torch+torchvision installés via index cu128 ARM AVANT ce fichier.
# --- Stack torch-dependent (désépinglé) ---
transformers
accelerate
timm
open_clip_torch
python-doctr
safetensors
tokenizers
huggingface-hub
hf-xet
# --- Web / API ---
fastapi==0.128.0
Flask==3.0.0
Flask-Caching==2.1.0
Flask-Cors==4.0.0
Flask-Migrate==4.1.0
Flask-SocketIO==5.3.5
Flask-SQLAlchemy==3.1.1
starlette==0.50.0
uvicorn==0.40.0
uvloop==0.22.1
httptools==0.7.1
httpcore==1.0.9
httpx==0.28.1
h11==0.16.0
websockets==16.0
wsproto==1.3.2
simple-websocket==1.1.0
python-engineio==4.8.0
python-socketio==5.10.0
python-multipart==0.0.21
Werkzeug==3.1.5
Jinja2==3.1.6
itsdangerous==2.2.0
blinker==1.9.0
click==8.3.1
MarkupSafe==3.0.3
cachelib==0.9.0
watchfiles==1.1.1
anyio==4.12.1
bidict==0.23.1
# --- DB / migrations ---
SQLAlchemy==2.0.23
alembic==1.18.4
Mako==1.3.10
greenlet==3.3.0
redis==5.0.1
# --- ML / vision CPU ---
faiss-cpu==1.13.2
opencv-python==4.12.0.88
pillow==12.1.0
numpy==2.2.6
scipy==1.17.0
scikit-learn==1.8.0
matplotlib==3.10.8
contourpy==1.3.3
fonttools==4.62.1
kiwisolver==1.5.0
cycler==0.12.1
pyparsing==3.3.2
ml_dtypes==0.5.4
networkx==3.6.1
sympy==1.14.0
mpmath==1.3.0
joblib==1.5.3
threadpoolctl==3.6.0
RapidFuzz==3.14.3
shapely==2.1.2
pyclipper==1.4.0
h5py==3.16.0
# --- PDF / docs ---
pypdfium2==5.6.0
lxml==6.0.2
python-docx==1.2.0
openpyxl==3.1.5
et_xmlfile==2.0.0
defusedxml==0.7.1
anyascii==0.3.3
langdetect==1.0.9
ftfy==6.3.1
wcwidth==0.2.14
regex==2025.11.3
# --- Ollama ---
ollama==0.6.1
# --- Validation / config ---
pydantic==2.12.5
pydantic_core==2.41.5
annotated-doc==0.0.4
annotated-types==0.7.0
typing-inspection==0.4.2
typing_extensions==4.15.0
validators==0.35.0
marshmallow==3.20.1
jsonschema==4.20.0
jsonschema-specifications==2025.9.1
referencing==0.37.0
rpds-py==0.30.0
attrs==25.4.0
PyYAML==6.0.1
python-dotenv==1.0.0
# --- HTTP / réseau ---
aiohttp==3.13.3
aiohappyeyeballs==2.6.1
aiosignal==1.4.0
frozenlist==1.8.0
multidict==6.7.0
propcache==0.4.1
yarl==1.22.0
requests==2.32.5
urllib3==2.6.3
charset-normalizer==3.4.4
idna==3.11
certifi==2026.1.4
# --- Crypto / utils ---
cryptography==46.0.3
cffi==2.0.0
pycparser==2.23
filelock==3.20.3
fsspec==2026.1.0
packaging==25.0
platformdirs==4.5.1
pathspec==1.0.3
six==1.17.0
python-dateutil==2.8.2
sortedcontainers==2.4.0
tqdm==4.67.1
Pygments==2.19.2
# --- Monitoring ---
prometheus_client==0.23.1
psutil==7.2.1
pynvml==13.0.1
nvidia-ml-py==13.590.48
# --- Protobuf ---
protobuf==7.34.0
# --- Dev / tests (à isoler dans requirements-dev.txt à terme) ---
pytest==9.0.2
pytest-asyncio==1.3.0
pytest-cov==4.1.0
pytest-flask==1.3.0
pytest-mock==3.12.0
hypothesis==6.92.1
coverage==7.13.1
iniconfig==2.3.0
pluggy==1.6.0
black==23.12.1
flake8==6.1.0
mypy==1.7.1
mypy_extensions==1.1.0
mccabe==0.7.0
pycodestyle==2.11.1
pyflakes==3.1.0
```
**Bilan** : 180 → ~125 lignes. Retraits = 17 capture/GUI + 15 nvidia-cu12 + 1 onnx + 11 remontés cat. 2 désépinglée (torch/torchvision/transformers/accelerate/timm/open_clip_torch/python-doctr/safetensors/tokenizers/huggingface-hub/hf-xet) = 44 lignes en moins, 9 désépinglées remontées en tête.
---
## 6. Vérifications post-install sur DGX
```bash
# 1. PyTorch + GPU sm_121
python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_capability(0))"
# attendu : (True, (12, 1))
python -c "import torch; print(torch.__version__, torch.version.cuda)"
# attendu : 2.9.x+ / 12.8
# 2. FAISS
python -c "import faiss; print(faiss.__version__)"
# attendu : 1.13.x
# 3. Transformers stack
python -c "import transformers; print(transformers.__version__)"
python -c "import accelerate, timm, open_clip, doctr; print(accelerate.__version__, timm.__version__, open_clip.__version__, doctr.__version__)"
# 4. Bindings Rust aarch64
python -c "import tokenizers, safetensors; print(tokenizers.__version__, safetensors.__version__)"
python -c "import hf_xet; print(hf_xet.__version__)" # point de vérif cat. 4
# 5. Confirmation onnx absent et runtime OK
python -c "import onnx" 2>&1 | grep -q ModuleNotFoundError && echo "OK : onnx absent"
# 6. Points d'entrée applicatifs
python -c "from web_dashboard.app import app; print('dashboard OK')"
python -c "from visual_workflow_builder.backend.app import app; print('VWB backend OK')"
# adapter le module server selon le wiring réel
# 7. Sanity GPU
python -c "import torch; x=torch.randn(8,8,device='cuda'); print('matmul GPU OK', (x@x).device)"
# 8. Ollama client → daemon
python -c "import ollama; print(ollama.list())"
```
**Garde-fous §5** :
- Si kernels Triton crashent : `export TRITON_PTXAS_PATH=/usr/local/cuda/bin/ptxas`.
- Si pip tire encore des `nvidia-*-cu12` x86 : confirmer que l'étape 1 (torch cu128 ARM) a été exécutée **avant**, et que `--no-cache-dir` est bien passé.
---
## 7. Points ouverts pour Codex/Dom
1. Confirmer suppression `onnx` : `grep -r "^import onnx\|^from onnx" --include='*.py' server/ core/` avant build image Docker.
2. Splitter dès le POC `requirements-dev.txt` (pytest/black/mypy/flake8/coverage/hypothesis) pour ne pas polluer l'image server ? À trancher.
3. `hf-xet` : si wheel aarch64 indispo au build, retirer (dep optionnelle `huggingface-hub`).
4. `nvidia-ml-py` vs `pynvml` : doublon. Garder uniquement `nvidia-ml-py` (officiel) ?

View File

@@ -0,0 +1,240 @@
# SPECS_AGENT_CHAT_LEARN_ACTION — 2026-06-01
> **DRAFT specs pour implémentation — pas encore appliqué.**
> Nouveau module `agent_chat/handlers/learn_action.py` orchestrant le dialogue Léa-first conversationnel et le raccordement au cycle Shadow existant (streaming server port 5005).
> Référence d'archi : Claude 2026-06-01 16:20 + addendum 17:45. Audit 17:00 : pas de nouveau code dashboard pour l'apprentissage — tout passe par agent-chat (port 5004).
## Sommaire
1. Architecture du module
2. Intent recognition (3 approches, reco hybride)
3. Mapping intents → appels Shadow
4. Formateur Option C (restitution texte + libellés OCR)
5. Itération correction (phase 4)
6. Phase 5 — nomination + scope (paramètres vs constantes)
7. Hooks mode proactif (LoopDetector)
8. Persistance d'état (session_id.json)
9. Tests à prévoir
10. Estimation effort implémentation
---
## 1. Architecture du module
**Fichier cible** : `agent_chat/handlers/learn_action.py` (nouveau, dossier `handlers/` à créer).
**Classe principale** : `LearnActionOrchestrator`
- Instanciée une fois par `agent_chat/app.py` au démarrage (singleton via `get_learn_action_orchestrator()` cohérent avec `intent_parser`, `confirmation`, `conversation_manager`).
- Injection : `StreamingClient` (wrapper httpx sync vers `RPA_STREAMING_URL` port 5005), `IntentParser` (réutilisé), `ResponseGenerator` (formuler phrases Léa), callback `emit_dual` (réutilise `_emit_dual` de `app.py` pour socket.io).
**Machine d'état finie** (Enum `LearnState`) :
```
IDLE
→ LISTENING (déclencheur : bouton 🎓 ou phrase magique → POST /shadow/start)
→ WAITING_USER_STOP (Léa observe en silence ; ping périodique notifications)
→ ANALYZING (utilisateur a dit "stop" → POST /shadow/stop)
→ PRESENTING (Léa restitue Option C dans le chat)
→ ITERATING_FEEDBACK (boucle correction step-par-step)
→ NAMING (Léa demande nom + scope paramètres)
→ PERSISTING (POST /api/v1/lea/competences/candidate/persist)
→ DONE | ABORTED
```
Transitions illégales rejetées + log. Chaque transition réussie : (a) sauvegarde d'état §8, (b) émission socket.io `lea:learn_state_changed`.
**Persistance d'état par session** : `agent_chat/state/<session_id>.json` (dossier créé au démarrage). Format §8.
**Interface streaming server (5005)** :
- Client httpx sync (cohérence avec `requests` déjà utilisé dans `app.py:31`).
- Wrapper `StreamingClient` (~50 lignes) : `shadow_start`, `shadow_stop`, `shadow_understanding`, `shadow_feedback`, `shadow_build`, `competence_persist`.
- Timeout 5s par appel, retry x2 sur `ConnectionError`, logging structuré.
- Auth via `RPA_API_TOKEN` env var (réutilise `_streaming_headers()`).
**Découplage** : le module ne touche **jamais** directement à `app.py` ni aux templates HTML. Il expose API Python (`start_session`, `handle_chat_message`, `handle_proactive_signal`, `resume_session`). C'est `app.py` qui route les messages du chat vers ce handler quand `LearnState != IDLE`.
## 2. Intent recognition
Pendant une session, messages utilisateur en français libre. Léa doit comprendre : start, stop, validate_step, correct_step, name_competence, mark_parameter, cancel.
**3 approches** :
- **(a) Phrases magiques regex** — déterministe, 0 latence, fragile sur variantes.
- **(b) NLU léger `qwen2.5:0.5b` Ollama** — robuste paraphrases, latence ~200-500ms, format=json mode.
- **(c) Hybride a → b — RECOMMANDÉ** : table regex sur ~80% des cas évidents, fallback `qwen2.5:0.5b` si aucun match avec confidence ≥ 0.9. Si LLM `confidence < 0.7` → Léa demande clarification.
**Implémentation** : sous-classe `LearnIntentParser` qui *étend* `IntentParser` existant en ajoutant enum `LearnIntent` (distinct de `IntentType`).
## 3. Mapping intents → appels Shadow
| Intent (`LearnIntent`) | Phase | Appel HTTP (5005) | Payload clé | Effet |
|---|---|---|---|---|
| `START_OBSERVE` | IDLE → LISTENING | `POST /api/v1/shadow/start` | `{session_id}` | démarre observation Windows |
| `USER_STOP_OBSERVE` | WAITING_USER_STOP → ANALYZING | `POST /shadow/stop` + `GET /shadow/<id>/understanding` | `{session_id}` | récupère steps compris |
| `VALIDATE_STEP` | ITERATING_FEEDBACK | `POST /shadow/feedback` | `{session_id, action:"validate", step_index}` | marque validé |
| `CORRECT_STEP` | ITERATING_FEEDBACK | `POST /shadow/feedback` | `{session_id, action:"correct", step_index, new_intent}` | corrige intent |
| `UNDO_STEP` | ITERATING_FEEDBACK | `POST /shadow/feedback` | `{session_id, action:"undo", step_index}` | retire step |
| `MERGE_NEXT` | ITERATING_FEEDBACK | `POST /shadow/feedback` | `{session_id, action:"merge_next", step_index}` | fusionne |
| `SPLIT_STEP` | ITERATING_FEEDBACK | `POST /shadow/feedback` | `{session_id, action:"split", step_index, at_event_index}` | coupe |
| `NAME_COMPETENCE` | NAMING (interne) | aucun | — | prépare payload persist |
| `MARK_PARAMETER` | NAMING (interne) | aucun | — | annote step (`is_parameter: true`) |
| `PERSIST` | NAMING → PERSISTING | `POST /api/v1/lea/competences/candidate/persist` | `{session_id, name, slug, parameters[]}` | enregistre |
| `CANCEL` | n'importe → ABORTED | `POST /shadow/stop` + journaling sortie d'urgence si partial | `{session_id, reason}` | nettoyage |
**Note** : payload final de `/persist` non figé tant que les specs parallèles ne sont pas mergées. Couche `PersistPayloadBuilder` isolée pour réagir aux évolutions.
**Préalable shadow_build** : avant `/persist`, appeler `POST /shadow/build` pour matérialiser WorkflowIR final (incluant feedbacks rejoués).
## 4. Formateur Option C
**But** : transformer `understanding[]` retourné par `/shadow/<id>/understanding` en texte naturel français + libellés OCR entre « ».
**Format attendu** :
```
1. Fenêtre « Patient » → ouverte
2. Bouton « Nouvelle facture » → cliqué
3. Champ « IPP » → saisi : « 25003284 »
4. Bouton « Valider » → cliqué (à confirmer)
```
**Règles** :
- 1 ligne par step.
- Libellés OCR entre « » (U+00AB / U+00BB).
- Verbe passé composé (`cliqué`, `saisi`, `ouverte`, `validé`).
- Si `confidence_ocr < 0.6` → suffixe `(à confirmer)`.
- Numérotation à partir de 1.
**Implémentation** : classe `OptionCFormatter` (~80 lignes), méthode `format(understanding: list) -> str`. Table mapping `action_type → verbe`. Fallback générique « action sur "X" → effectuée » si type inconnu.
**Inclure aussi** : question fermée Léa après la liste : « C'est bien ça ou je me suis trompée quelque part ? ». Transition vers ITERATING_FEEDBACK.
## 5. Itération correction (phase 4)
**Parser** doit détecter :
- Numéro de step : regex `\b(?:étape|numéro|step|ligne|le|la)?\s*([1-9]\d?)\b` + fallback contextuel.
- Action correction : type de feedback shadow (validate / correct / undo / merge_next / split).
- Contenu correction : si `correct`, extraire `new_intent` (reste de phrase après verbe correctif). Mini appel `qwen2.5:0.5b` pour nettoyer.
**Boucle** :
1. Appel `/shadow/feedback`.
2. Récupération nouveau `understanding[]` via `/shadow/<id>/understanding`.
3. Reformulation Léa avec **recap complet** (Option C re-formatée). Pas de diff partiel.
4. Question fermée : « C'est bon ou il reste à corriger ? ».
**Garde-fous** :
- **Limite 3 corrections sur même step** (compteur par `step_index`). Au 4ᵉ → sortie d'urgence : « Je n'arrive pas à comprendre l'étape N°X. Je préfère qu'on reprenne plus tard. Je garde tout. » → ABORTED avec `partial=true`.
- **Timeout silencieux** : 5 min sans message → Léa relance « Tu es toujours là ? Si tu veux on s'arrête, je garde tout. »
- **Détection boucle de doute** : alternance 2 fois `correct`/`undo` sur même step → Léa propose de relancer l'enregistrement de cette étape seulement.
## 6. Phase 5 — nomination + scope
**5.1 Nomination** :
- Léa : « Comment on appelle cette tâche ? Tu peux la nommer simplement, en français. »
- Validation : non vide, ≤ 80 chars, pas de doublon strict (lookup `GET /api/v1/lea/competences?name=…`).
- **Slugification côté serveur** : client envoie `name` brut, serveur dérive `slug`.
**5.2 Scope paramètres** :
- Pour chaque step avec valeur saisie, Léa pose la question : « La valeur "25003284" pour le champ "IPP" — c'est l'exemple du jour ou ça doit toujours être ça ? »
- Choix :
- « toujours » / « constante » → `is_parameter=False`
- « ça change » / « exemple » → `is_parameter=True`, nom proposé (default = nom champ OCR slugifié), confirmation
- Résumé final : « Donc je retiens : tâche `<nom>` avec les paramètres `ipp`, `montant`. C'est OK ? »
## 7. Hooks mode proactif (LoopDetector)
**Contexte** : `agent_v0/server_v1/loop_detector.py` détecte 3 signaux : `screen_static`, `action_repeat`, `retry_threshold`.
**Mécanisme** :
- Souscription WebSocket (à exposer côté streaming) ou polling `GET /api/v1/shadow/loop_signals?since_ts=…` toutes les 30s.
- Sur `action_repeat` → message proactif : « J'ai remarqué que tu fais souvent la même séquence. Tu veux m'apprendre à la faire pour toi ? » (bouton « Apprenez-moi » mis en évidence).
- Sur `retry_threshold` (pendant replay) → « Je n'arrive pas à reproduire l'étape N°X. Tu peux me re-montrer ? » → mini-apprentissage ciblé.
**Cooldown / mémoire** :
- Cooldown global 5 min entre 2 messages proactifs.
- Mémoire courte des **refus** (« non merci » → blocage 24h sur signal précis, persisté `agent_chat/state/proactive_memory.json`).
- **Report unique** : « pas maintenant » → re-proposition une fois 30 min plus tard.
**Pas d'auto-déclenchement** : un signal proactif n'appelle **jamais** `/shadow/start` sans validation utilisateur explicite.
## 8. Persistance d'état
**Fichier** : `agent_chat/state/<session_id>.json` (un par session active).
**Contenu** :
```json
{
"session_id": "...",
"user_id": "...",
"trigger_source": "button" | "magic_phrase" | "proactive",
"state": "ITERATING_FEEDBACK",
"created_at": "...",
"last_transition_at": "...",
"shadow_understanding": [...],
"pending_feedbacks": [...],
"correction_counters": {"3": 2, ...},
"competence_name": null | "...",
"parameters_marked": [...],
"abort_reason": null | "..."
}
```
**Écriture** : à chaque transition d'état (atomique `tmp` + `os.replace`), à chaque feedback appliqué, à chaque marquage paramètre.
**Reprise** : au démarrage `app.py`, scanner `state/*.json`. Pour chaque non-DONE/ABORTED :
- Vérifier session shadow côté streaming.
- Si trouvée → restaurer + notifier user : « On était en train d'apprendre "<nom>". Tu veux qu'on reprenne ou qu'on jette ? »
- Si shadow plus en mémoire → `state=ABORTED, abort_reason="streaming_lost"`.
**Nettoyage** : DONE archivées dans `state/archive/` après 7 jours ; ABORTED conservées 30 jours.
## 9. Tests à prévoir
**Unit (`tests/agent_chat/test_learn_action.py`)** :
- `LearnIntentParser` : 30+ cas regex + 5 cas LLM mocké.
- `OptionCFormatter` : 10 cas understanding → texte attendu, dont confidence basse.
- Gestionnaire corrections : 3 corrections → sortie d'urgence, boucle correct/undo → propose re-record.
- `StateStore` : write atomique, reprise après crash simulé.
**Integration (`tests/agent_chat/test_learn_action_integration.py`)** :
- Flux complet mocké bout-en-bout.
- Cas redémarrage agent-chat pendant `ITERATING_FEEDBACK` → reprise propre.
- Cas streaming down sur `/shadow/stop` → retry x2 puis message d'erreur explicite.
- Cas signal proactif `action_repeat` → message + cooldown.
**Robustesse** :
- Timeout streaming sur `/persist` → état `PERSISTING`, retry manuel possible.
- Agent Windows down pendant LISTENING → détection > 60s → « Je n'arrive pas à observer, on réessaie ? ».
- Concurrence 2 sessions parallèles → pas d'interférence.
**Couverture cible** : ≥ 80% sur `learn_action.py`, ≥ 70% global.
## 10. Estimation effort implémentation
| Composant | Lignes Python | Lignes tests | Effort |
|---|---:|---:|---|
| `LearnActionOrchestrator` + machine d'état | ~180 | ~80 | moyen |
| `LearnIntentParser` (hybride) | ~120 | ~60 | moyen |
| `OptionCFormatter` | ~80 | ~40 | léger |
| `StreamingClient` (httpx wrapper) | ~70 | ~30 | léger |
| `StateStore` (persistance + reprise) | ~80 | ~30 | léger |
| `ProactiveHook` (LoopDetector) | ~70 | ~10 | moyen (dépend WS server) |
| **Total** | **~600** | **~250** | **moyen-lourd** |
**Dépendances bloquantes** :
- Specs `/persist` parallèles à figer avant `PersistPayloadBuilder`.
- Endpoint WebSocket / polling LoopDetector côté streaming.
**Hors scope** :
- Bouton 🎓 Windows : déjà fonctionnel, on s'aligne sur trigger existant.
- UI dashboard 5001 : aucune intervention.
- Modifs `app.py` : injection singleton + routage messages quand `state != IDLE` (~40 lignes additionnelles).
**Risques** :
- Latence cumulée NLU + HTTP : viser ≤ 1s par tour utilisateur.
- Cohérence `correction_counters` local et historique `validator` côté shadow : source de vérité = observer (relecture via `/understanding`).
- Reprise après crash : éviter `/shadow/feedback` déjà appliqué (idempotence à valider).
---
*Fin DRAFT — relecture Codex/Dom attendue avant implémentation. Aucune modification de fichier de prod.*

View File

@@ -0,0 +1,210 @@
# SPECS — POST /api/v1/lea/competences/candidate/persist
> **DRAFT specs pour implémentation — pas encore appliqué — relecture Codex/Dom attendue**
>
> Date : 2026-06-01
> Auteur : Claude (agent Plan)
> Statut : DRAFT — aucun code écrit, aucun fichier de prod modifié
> Cible : `agent_v0/server_v1/api_stream.py`
## Contexte
L'audit Claude du 2026-06-01 17h00 confirme l'absence de ce endpoint. Le cycle Shadow apprentissage actuel s'arrête à `/api/v1/shadow/build` (renvoie le `workflow_ir` en mémoire, ne le persiste pas).
Ce endpoint clôt le cycle Léa-first (6 phases) :
`start → stop → understanding → feedback (N×) → build → **persist**`
Sources :
- `docs/coordination/inbox_codex/2026-06-01_1620_claude-to-codex_ARCHI-apprentissage-Lea-first-validee-Dom.md`
- `docs/coordination/inbox_codex/2026-06-01_1745_claude-to-codex_ADDENDUM-archi-Lea-lecture-semantique-agent-externe.md`
- `docs/coordination/inbox_codex/2026-06-01_1800_claude-to-codex_PRECISIONS-desapprentissage-regle-or-HDS.md`
Schéma YAML de référence : `data/competences/candidate/key_win_r_wait_explorer_exe.yaml`
## Sommaire
1. Contrat de l'endpoint
2. Validation pré-écriture
3. Génération YAML
4. Journalisation
5. Cas particuliers
6. Sécurité
7. Tests à prévoir
8. Estimation effort implémentation
---
## 1. Contrat de l'endpoint
**Méthode / URL** : `POST /api/v1/lea/competences/candidate/persist`
**Headers requis** :
- `Authorization: Bearer <token-poste>` (cohérent avec patch P0 révocation token Codex)
- `Content-Type: application/json`
**Payload entrée (JSON)** :
```json
{
"name": "Saisir texte Word",
"machine_id": "DESKTOP-58D5CAC_windows",
"session_id": "sess_20260601T180000_abc123",
"workflow_ir": { "steps": [...], "preconditions": [...], "success_marker": {...} },
"parameters": [ { "name": "texte", "type": "string", "required": true } ],
"external_decision": {
"agent_id": "t2a_v2_codeur",
"decision_contract": "..."
},
"annotations_semantiques": {
"intent_fr": "saisir le texte transmis dans Word"
},
"learning_metadata": {
"persist_id": "uuid-v4-client",
"partial": false,
"dropout_reason": null,
"source_phase": "shadow_build"
}
}
```
**Codes HTTP** :
- `201 Created` — YAML écrit, audit journalisé
- `400 Bad Request` — payload invalide (schema, slug, agent externe inconnu)
- `401 Unauthorized` — token absent / invalide / révoqué
- `403 Forbidden``machine_id` du payload ≠ machine du token
- `409 Conflict` — slug existe déjà dans `data/competences/candidate/`
- `429 Too Many Requests` — rate limit dépassé
- `500 Internal Server Error` — échec atomic write ou roundtrip
**Payload sortie (201)** :
```json
{
"competence_id": "saisir_texte_word",
"yaml_path": "data/competences/candidate/saisir_texte_word.yaml",
"learning_state": "candidate",
"persist_id": "uuid-v4-serveur",
"audit_entry_id": 4271
}
```
## 2. Validation pré-écriture
**Slugification** (`name``competence_id`) :
- Conversion kebab-case ASCII (translittération accents)
- Lowercase, espaces → `_`, chars non `[a-z0-9_]` retirés
- Longueur ≤ 80 caractères, ≥ 3 caractères
- Pattern regex final : `^[a-z][a-z0-9_]{2,79}$`
- Si collision avec un fichier existant en `candidate/` : **409 sans suffixage automatique**
**Schema YAML cohérent** — champs obligatoires : `schema_version, id, name, version, learning_state, intent, parameters, preconditions, methods, success_marker, failure_message_template, promotion, generalisation, failure_log, created_at, last_updated_at, methods_execution`
**WorkflowIR** :
- `workflow_ir.steps` non vide **sauf si** `learning_metadata.partial == true`
- Chaque step possède `kind` ∈ primitives connues (réutiliser registry primitives existante)
**Idempotence** :
- `persist_id` (UUID v4 client) déduit la clé d'idempotence
- Si `persist_id` déjà présent dans `persist_audit.jsonl` → renvoyer `200 OK` avec le payload de l'entrée précédente (pas 409)
## 3. Génération YAML
**Chemin cible** : `data/competences/candidate/<slug>.yaml`
**Atomic write** :
1. Écrire dans `data/competences/candidate/.<slug>.yaml.tmp.<persist_id>`
2. `os.rename()` vers `<slug>.yaml` (atomique POSIX)
3. Si collision détectée entre validation et rename → 409, supprimer le `.tmp`
**Post-write roundtrip** :
- Recharger via `core.competences.catalog.load_competence(slug)`
- Vérifier que `competence.to_dict()` est cohérent
- Si KO → 500, supprimer le fichier, log critique
## 4. Journalisation
**Audit principal** — append-only `data/competences/persist_audit.jsonl` :
```json
{
"persist_id": "uuid-v4",
"timestamp": "2026-06-01T18:05:32+02:00",
"machine_id": "...",
"session_id": "...",
"competence_name": "Saisir texte Word",
"competence_id": "saisir_texte_word",
"yaml_path": "data/competences/candidate/saisir_texte_word.yaml",
"learning_state": "candidate",
"partial": false,
"dropout_reason": null,
"external_agent_id": null,
"audit_entry_id": 4271
}
```
**Apprentissages incomplets** — si `partial: true`, entrée *en plus* dans `data/competences/incomplete_learnings.jsonl` avec `dropout_reason` obligatoire.
**Append-only strict** : ouvrir en mode `a`, jamais réécrire. Verrou `fcntl.flock` par fichier.
## 5. Cas particuliers
| Cas | Comportement |
|---|---|
| `learning_metadata.partial == true` | accepté, `learning_state` forcé `incomplete`, entrée double dans `incomplete_learnings.jsonl`, `dropout_reason` obligatoire (sinon 400) |
| Payload demande `learning_state: stable` | **rejet silencieux** — forcer `candidate` (jamais stable par persist direct — règle d'or HDS) |
| Aucun `learning_state` fourni | défaut `candidate` |
| `external_decision.agent_id` inconnu de `ExternalDecisionClient` registry | **400** avec liste agents valides |
| `workflow_ir.steps` vide et `partial == false` | **400** |
| YAML déjà présent en `data/competences/stable/` ou `supervised/` avec même slug | **409** (collision cross-states) |
| Désapprentissage en cours sur le slug | **409** `detail: "désapprentissage actif"` |
## 6. Sécurité
- **Token Bearer obligatoire** — réutiliser middleware d'auth du patch P0 révocation token. Pas de fallback "agent local".
- **Couplage machine_id ↔ token** : token référence `machine_id` ; si `payload.machine_id != token.machine_id`**403**.
- **Rate limit** : 10 persists/min/`machine_id` (in-memory token-bucket). Au-delà → **429** avec `Retry-After`.
- **Pas d'écriture hors `data/competences/candidate/`** : path traversal interdit (slug strict, jamais lu d'un champ user).
- **Logs** : ne jamais journaliser le token. `persist_id` et `machine_id` only.
- **Règle d'or HDS** : aucune donnée patient ne doit transiter — refuser le payload si `workflow_ir` ou `annotations_semantiques` contient pattern PII détecté. Détection MVP via regex paramétrable.
## 7. Tests à prévoir
**Unit (`tests/unit/test_competence_persist.py`)** :
- `test_slug_generation_normal` / `_with_accents` / `_too_short` / `_collision_409`
- `test_yaml_schema_required_fields_present`
- `test_atomic_write_then_rename`
- `test_post_write_roundtrip_ok` / `_corrupt_yaml_500`
- `test_idempotence_same_persist_id_returns_previous`
- `test_partial_true_forces_incomplete_state`
- `test_payload_stable_forced_to_candidate`
- `test_external_agent_unknown_400`
- `test_pii_detected_rejected`
**Integration (`tests/integration/test_shadow_full_cycle.py`)** :
- Cycle complet : `shadow/start → POST événements → shadow/stop → shadow/build → persist`
- Vérifier YAML présent, `persist_audit.jsonl` enrichi
- Cas partial : double entrée `incomplete_learnings.jsonl`
**Sécurité (`tests/security/test_persist_auth.py`)** :
- `test_no_token_401`
- `test_token_revoked_401`
- `test_machine_id_mismatch_403`
- `test_rate_limit_11th_call_429`
- `test_path_traversal_in_name_blocked`
## 8. Estimation effort implémentation
| Item | Volume |
|---|---|
| Handler FastAPI + Pydantic models | ~80-120 lignes (`api_stream.py`) |
| Helpers (slugify, atomic_write, audit_append) | ~40 lignes (nouveau module `core/competences/persist.py`) |
| Tests unit + integration + sécu | ~150 lignes |
**Effort total** : faible-moyen — **0.5 à 1 jour**. Pas de dépendance externe nouvelle, réutilise `core.competences.catalog` et le pattern atomic-write de C-γ.
**Points d'attention pour la review** :
- Confirmer comportement collision (409 strict vs suffixage `_v2`)
- Valider mapping `workflow_ir.steps[*].kind``yaml.methods[*].kind` avec registry primitives à jour
- Décider si rate limit par `machine_id` ou par token
- Statuer sur format `audit_entry_id` (auto-incrément vs hash)
---
*Fin DRAFT — relecture Codex/Dom attendue avant kick-off implémentation.*

View File

@@ -0,0 +1,169 @@
# SPECS Phase 2.5 Sémantique — POC RPA Vision v3
**DRAFT specs pour implémentation — pas encore appliqué — alignement avec arbitrage Plato 2026-06-01 18:18**
Date : 2026-06-01
Auteur : Claude (agent Plan), arbitrage Plato/Codex
Statut : DRAFT — à valider par Dom avant implémentation
Référence arbitrage : `docs/coordination/inbox_claude/2026-06-01_1818_codex-to-claude_ADDENDUM-agent-plato-archi-semantique.md`
Référence addendum initial Claude : 17:45 (OmniParser systématique dans hot path replay) — **révoqué par Plato**
## Principes d'arbitrage retenus
1. Garder replay/click/OCR existant comme chemin principal (hot path inchangé)
2. Phase 2.5 = post-apprentissage uniquement, jamais en hot path replay
3. Demander à l'humain seulement les ambiguïtés utiles
4. Sémantique = contexte, pas prérequis de chaque clic
5. `ExternalDecisionClient` autour de `t2a_decision`, puis adaptation AIVA
6. OCR critique renforcé par régions annotées + escalade humaine
## Sommaire
1. Périmètre Phase 2.5 MVP
2. Endpoint à créer
3. Identification des écrans distincts
4. Stockage des annotations sémantiques
5. Réutilisation au replay (autopilote / autonome)
6. Compatibilité compétences existantes
7. Garde-fous anti-fragilité OmniParser
8. Coûts perf attendus
9. Tests à prévoir
10. Estimation effort implémentation
11. Hors scope MVP Phase 2.5
---
## 1. Périmètre Phase 2.5 MVP
- **Quand** : déclenchement à la fin de la phase d'observation, après `POST /api/v1/shadow/stop`, **avant** restitution Option C à l'humain.
- **Sur quoi** : screenshots clés capturés (1 par écran distinct détecté), **pas tous les frames**.
- **Avec quoi** : OmniParser réutilisé (déjà présent, adaptateur fragile mais isolé), encapsulé derrière garde-fou anti-fragilité §7.
- **Pour quoi** : produire un payload structuré `{tables[], forms[], buttons[], text_blocks[]}` par écran clé, stocké dans un YAML sémantique séparé du YAML compétence principal.
- **Non-objectif** : Phase 2.5 ne touche pas le chemin replay. Elle enrichit uniquement la compétence apprise.
## 2. Endpoint à créer
`POST /api/v1/lea/screen/analyze` côté streaming server (port 5005).
- **Entrée** : `{session_id: str, screenshot_indexes: int[]}` — index des frames capturées en mode léger pendant l'observation.
- **Sortie** : `{screens: [{index, hash, structure: {tables, forms, buttons, text_blocks}}]}`.
- **Idempotence** : cache disque sous `data/cache/omniparser/<session>/<index>.json`.
- **Timeout** : 30s par screenshot. Dépassement → fallback OCR-seul (docTR text_blocks seuls), flag `degraded: true`.
- **Erreur OmniParser** (exception, modèle KO) : fallback OCR-seul + log `logs/omniparser_errors.log`. L'endpoint ne renvoie **jamais** 500 — toujours 200 avec flag dégradé.
## 3. Identification des écrans distincts
Heuristique de regroupement, exécutée avant l'appel `/api/v1/lea/screen/analyze` :
- Calcul `imagehash.phash` pour chaque frame capturée.
- Grouping par similarité : Hamming distance ≤ 8 ⇒ même écran logique.
- Sélection d'un frame représentatif par groupe (premier dans l'ordre temporel, ou celui avec le plus de détections OCR).
- **Limite POC** : maximum **10 écrans distincts par session**. Au-delà, session marquée `too_complex` et l'humain est invité à scinder la compétence en sous-compétences (message explicite dans restitution Option C).
## 4. Stockage des annotations sémantiques
Fichier séparé du YAML compétence principal, suffixe `.semantic.yaml`.
Chemin : `data/competences/candidate/<slug>.semantic.yaml`
Structure :
```yaml
competence_id: facturation_urgence_simple
semantic_version: 1
generated_at: 2026-06-01T18:30:00Z
omniparser_version: <hash adaptateur>
degraded: false
screens:
- screen_id: screen_001
phash: "abc123..."
representative_frame_index: 42
annotations:
- region_id: region_3
bbox: [120, 80, 400, 150]
semantic_label: motif_arrivee
confidence: human_verified
structure:
tables: [...]
forms: [...]
buttons: [...]
text_blocks: [...]
```
- Séparation stricte : YAML compétence principal reste lisible et minimal. `.semantic.yaml` est optionnel.
- YAML compétence référence le fichier sémantique par clé `semantic_ref: <slug>.semantic.yaml` si présent.
## 5. Réutilisation au replay (autopilote / autonome)
OmniParser au replay **uniquement** si la compétence référence un `.semantic.yaml` (présence de `semantic_ref`).
Algorithme :
1. Au démarrage du replay d'un écran, calcul `phash` du screen courant.
2. Si Hamming distance ≤ 8 avec un `screen_id` connu OU même nombre de tables/buttons/forms structurellement → match, application des annotations.
3. Sinon → divergence :
- **Autopilote** : escalade humaine via Option C runtime (« écran inattendu, valider ou annoter »).
- **Autonome** : log incident `logs/replay_incidents.log`, **stop** avec code `semantic_mismatch`.
Compétences sans `semantic_ref` → on saute totalement cette étape (rétrocompat §6).
## 6. Compatibilité compétences existantes
- Compétences sans `.semantic.yaml` ⇒ pas de Phase 2.5 au replay, pas d'OmniParser, **chemin OCR + template + click inchangé**.
- Phase 2.5 = **opt-in par compétence** : seule une compétence apprise après l'activation de Phase 2.5 possédera le fichier sémantique.
- Aucune migration auto des compétences anciennes. Rétrocompat garantie par l'absence de `semantic_ref`.
- Pas de breaking change sur le format YAML compétence principal — seulement une clé optionnelle.
## 7. Garde-fous anti-fragilité OmniParser
Adaptateur OmniParser fragile (chemin absolu, dépendances lourdes). Mesures :
- **Wrapper try/except global** autour de chaque appel OmniParser, fallback OCR-seul (docTR) systématique en cas d'exception.
- **Log dédié** : toute erreur → `logs/omniparser_errors.log` avec stack trace, session_id, frame_index.
- **Healthcheck au démarrage Phase 2.5** : appel test sur image bidon. Si échec → bascule auto en mode dégradé OCR-seul, log warning, restitution `degraded: true`.
- **Test unitaire obligatoire** : mock OmniParser qui lève exception, vérifier que la chaîne aboutit à un `.semantic.yaml` dégradé valide.
- **Isolation chemin absolu** : config OmniParser (chemin modèle, version) centralisée dans `config/omniparser.yaml`, jamais en dur.
## 8. Coûts perf attendus
- **Dev (RTX 5070, OmniParser CPU)** : 2 à 5s par screen. Acceptable (principe 5 Dom, hors hot path).
- **DGX Spark (cible prod)** : < 500ms par screen.
- **Coût session typique** : 10 screens × 2-5s = 20-50s ajoutés à la fin d'une session d'apprentissage sur dev. Tolérable, asynchrone vis-à-vis humain (restitution Option C peut afficher spinner).
- Pas d'impact sur replay (hot path) tant que pas de `semantic_ref`. Avec `semantic_ref`, surcoût = `phash` (négligeable) + OmniParser uniquement en cas de mismatch structure.
## 9. Tests à prévoir
**Unitaires** :
- Hash perceptuel + grouping : frames similaires/différentes, vérif Hamming threshold.
- Fallback OCR-seul si OmniParser KO (mock exception, timeout, healthcheck KO).
- Génération `.semantic.yaml` : structure valide, `degraded` correctement positionné.
- Cap 10 écrans : session avec 15 → marquage `too_complex` propre.
**Intégration** :
- Flux complet : session shadow → stop → identification écrans → analyse → `.semantic.yaml` écrit → Option C.
- Replay compétence avec `.semantic.yaml` : match nominal, mismatch déclenche escalade autopilote / stop autonome.
- Replay compétence sans `.semantic.yaml` : chemin legacy intact, aucun appel OmniParser.
**Sécurité** :
- Injection chemins relatifs/absolus dans `session_id` ou `slug` refusée (regex strict `^[a-z0-9_]+$`).
- Pas d'écriture hors `data/competences/candidate/` et `data/cache/omniparser/`.
## 10. Estimation effort implémentation
- ~200-300 lignes Python (endpoint, grouping phash, wrapper OmniParser, écriture YAML, lecture replay).
- ~150 lignes tests.
- Effort **moyen**, étalable sur 1-2 jours.
- **Risque régression faible** : ne touche pas hot path replay, opt-in par compétence, rétrocompat totale.
## 11. Hors scope MVP Phase 2.5
- Détection visuelle de changements d'écran en temps réel pendant le replay (= hot path, révoqué par Plato).
- Annotations sémantiques automatiques par VLM (prérequis trop ambitieux POC).
- Versioning sémantique multi-versions Easily (post-POC).
- Recalcul auto annotations si interface change (post-POC, lié mode rééducation).
- Annotations multi-langues (POC français uniquement).
- Synchronisation `.semantic.yaml` entre agents distribués (post-POC).
---
**Fin DRAFT — à valider par Dom avant implémentation. Aucune modification de fichier de prod. Aucun commit.**

View File

@@ -0,0 +1,158 @@
# Préparation validation réelle — flux `finalize → proposition utilisateur → replay-session`
**Date** : 2026-05-20
**Mission** : Claude 5 (lecture seule)
**Périmètre** : `agent_v0/deploy/windows_client/**`, `agent_v0/agent_v1/**`, scripts SCP, docs déploiement
**Branche** : `backup/post-demo-2026-05-19` (HEAD `5ea4960e6`, 5 fichiers uncommitted + 1 untracked)
## Constat
Le câblage agent-side du contrat enrichi `/finalize` est **déjà fait et présent dans la branche locale** :
- `streamer.py:96-103, 627-635` — attribut `_on_finalize_result`, setter `set_on_finalize_result`, invocation après `resp.ok` (catch d'erreur du callback en log warning).
- `main.py:31` — import `dispatch_finalize_result`.
- `main.py:222` — wire `self.streamer.set_on_finalize_result(self._on_finalize_result)` dans `start_session`.
- `main.py:445-448``_on_finalize_result(payload)` dispatche via `dispatch_finalize_result(self.ui, payload, replay_name)` avec `replay_name = self._last_recording_name`.
- `finalize_contract.py` (untracked) — module pur, `dispatch_finalize_result(ui, payload, replay_name)` route selon `replay_launch.status` (`started`/`failed`) et `replay_ready + replay_request`.
- `smart_tray.py:577-599``offer_finalize_replay(replay_request, replay_name)` : notification toast + dialog `_ask_consent` Article 14, puis `_launch_replay_request(replay_request, replay_name)` si accepté.
Le contrat attendu côté serveur s'appelle **`replay_launch`** (status `started`/`failed`), pas `launch_replay` comme supposé en mission 4. Les 3 clés consommées : `replay_ready: bool`, `replay_request: dict` (doit contenir `session_id`), `replay_launch: dict {status}`.
**Synchro vers Léa Windows** : aucun script SCP automatique présent dans le repo. Le canal réel est un `sshpass scp` manuel fichier par fichier vers `dom@192.168.1.11:C:/rpa_vision/agent_v1/<chemin>` (cf. `memory/feedback_scp_auto_modif_client_windows.md` + handoff 2026-05-16). Le dossier `agent_v0/deploy/windows_client/` existe mais sert au **setup initial** (setup.bat), pas au déploiement incrémental — ses fichiers miroirs sont **obsolètes de 2 à 7 semaines** par rapport à la source.
Le `BUILD_DEPLOY_GUIDE.md` (24 novembre 2025) parle de l'ancien `agent_v0/` mode upload chiffré — **obsolète** pour le flux agent_v1 streaming actuel.
## Fichiers à déployer
### Tableau fichier / déploiement / motif
| Fichier source | Doit être déployé ? | Pourquoi |
|---|---|---|
| `agent_v0/agent_v1/main.py` | ✅ **OUI** | Lignes 31 (import), 222 (wire), 445-448 (`_on_finalize_result`) ajoutées — sans ce fichier, le callback n'est pas wired |
| `agent_v0/agent_v1/network/streamer.py` | ✅ **OUI** | Lignes 96-103 + 627-635 ajoutées — sans ce fichier, le payload `/finalize` est jeté dans un log comme avant |
| `agent_v0/agent_v1/ui/smart_tray.py` | ✅ **OUI** | Méthode `offer_finalize_replay` (577-599) ajoutée — sans elle, `finalize_contract.py` log "UI indisponible" et abandonne |
| `agent_v0/agent_v1/finalize_contract.py` | ✅ **OUI (NOUVEAU FICHIER)** | Untracked en git, n'existe nulle part sur Léa Windows → **`ImportError` au démarrage de l'agent** si oublié (import ligne `main.py:31`) |
| `agent_v0/agent_v1/vision/capturer.py` | ✅ **OUI** | Fix monitor missions 2/2b (garde dims aberrantes + fallback) — pertinent pour la fiabilité de la session enregistrée qui alimente le `replay_request` |
| `agent_v0/agent_v1/core/executor.py` | 🟡 Si modifié séparément | Apparaît dans `git status` (modifié dans session antérieure), à vérifier avec `git diff` avant SCP pour ne pas pousser un changement non validé |
| `agent_v0/agent_v1/ui/smart_tray.py` (méthode `_launch_replay_request`) | ✅ Inclus dans smart_tray.py | Référencée par `offer_finalize_replay`, ligne 507 |
| `agent_v0/agent_v1/config.py` | ⚫ Non | Pas modifié par les missions récentes |
| `agent_v0/agent_v1/ui/chat_window.py` | ⚫ Non | Pas modifié par mission 4 |
| `agent_v0/agent_v1/ui/shared_state.py` | ⚫ Non | Pas modifié |
| `agent_v0/agent_v1/network/persistent_buffer.py` | ⚫ Non | Pas modifié |
| `agent_v0/agent_v1/network/feedback_bus.py` | ⚫ Non | Pas modifié |
| `agent_v0/lea_ui/server_client.py` | ⚫ Non | Pas modifié, et probablement non utilisé au runtime actuel (main a son propre poll loop) |
| `agent_v0/deploy/windows_client/**` | ⚫ Non | Miroir obsolète, sert au setup initial, ne fait pas partie du canal de déploiement incrémental |
### La synchro actuelle couvre-t-elle ?
| Fichier | Couvert par synchro auto ? | Statut |
|---|---|---|
| `agent_v0/agent_v1/network/streamer.py` | ❌ NON | Aucun script auto — SCP manuel obligatoire |
| `agent_v0/agent_v1/ui/smart_tray.py` | ❌ NON | Aucun script auto — SCP manuel obligatoire |
| `agent_v0/agent_v1/main.py` | ❌ NON | Aucun script auto — SCP manuel obligatoire |
| `agent_v0/agent_v1/finalize_contract.py` | ❌ NON (cas critique) | Fichier NEUF, untracked git, **absent partout sur Léa** → ImportError garanti si oublié |
**Conclusion synchro** : il faut SCP les 4 fichiers manuellement. Le risque principal est d'oublier `finalize_contract.py` (nouveau) car il n'apparaîtra pas dans un diff incrémental classique — le réflexe "je SCP les fichiers modifiés" ne le couvre pas. **Doit être SCP en premier.**
### Commandes SCP de référence
```bash
SSHPASS='loli' sshpass -e scp -o StrictHostKeyChecking=no \
/home/dom/ai/rpa_vision_v3/agent_v0/agent_v1/finalize_contract.py \
dom@192.168.1.11:C:/rpa_vision/agent_v1/finalize_contract.py
SSHPASS='loli' sshpass -e scp -o StrictHostKeyChecking=no \
/home/dom/ai/rpa_vision_v3/agent_v0/agent_v1/network/streamer.py \
dom@192.168.1.11:C:/rpa_vision/agent_v1/network/streamer.py
SSHPASS='loli' sshpass -e scp -o StrictHostKeyChecking=no \
/home/dom/ai/rpa_vision_v3/agent_v0/agent_v1/ui/smart_tray.py \
dom@192.168.1.11:C:/rpa_vision/agent_v1/ui/smart_tray.py
SSHPASS='loli' sshpass -e scp -o StrictHostKeyChecking=no \
/home/dom/ai/rpa_vision_v3/agent_v0/agent_v1/main.py \
dom@192.168.1.11:C:/rpa_vision/agent_v1/main.py
SSHPASS='loli' sshpass -e scp -o StrictHostKeyChecking=no \
/home/dom/ai/rpa_vision_v3/agent_v0/agent_v1/vision/capturer.py \
dom@192.168.1.11:C:/rpa_vision/agent_v1/vision/capturer.py
```
## Checklist smoke
### Pré-requis machine (à valider une fois)
| Item | Vérification |
|---|---|
| Léa Windows installée à `C:\rpa_vision\` | `dir C:\rpa_vision\agent_v1\main.py` doit répondre |
| Venv `.venv` ou `.venv_v1_win` présent avec dépendances | `C:\rpa_vision\.venv\Scripts\python.exe -c "import pystray, pynput, mss, plyer; print('ok')"` |
| Token API exporté côté Windows | `echo %RPA_API_TOKEN%` (ou variable session) — sinon `set RPA_API_TOKEN=<token>` |
| Serveur Linux accessible | depuis Windows : `curl -H "Authorization: Bearer %RPA_API_TOKEN%" http://192.168.1.40:5005/health` |
| `rpa-streaming` actif côté Linux | `systemctl --user status rpa-streaming` |
| VWB backend actif (port 5002) | optionnel — la mission 5 ne le requiert pas |
| Léa Windows actuellement quittée | tray icône absente |
### Déploiement (≈ 1 min)
1. **SCP `finalize_contract.py` EN PREMIER** (fichier neuf, sinon ImportError au prochain `python run_agent_v1.py`).
2. SCP `streamer.py`, `smart_tray.py`, `main.py`, `capturer.py` (ordre indifférent).
3. Côté Léa Windows : ouvrir le dossier `C:\rpa_vision\agent_v1\` et confirmer les 5 fichiers ont la date du jour.
4. Lancer Léa : double-clic raccourci ou `C:\rpa_vision\.venv\Scripts\pythonw.exe run_agent_v1.py`.
5. Vérifier qu'aucune fenêtre console n'apparaît avec une stack trace `ImportError: finalize_contract`. Si oui → relancer avec `python.exe` (pas `pythonw.exe`) pour voir l'erreur, ou re-SCP `finalize_contract.py`.
6. Vérifier l'icône systray apparaît (cercle gris/orange selon connexion serveur).
### Test nominal (≈ 2 min)
| # | Action utilisateur | Logs Léa Windows attendus | Logs serveur Linux attendus | Comportement attendu |
|---|---|---|---|---|
| 1 | Clic droit tray → "Apprenez-moi une tâche" | `agent_v0.agent_v1.ui.shared_state Enregistrement demarre : ...` + `agent_v1.network.streamer Streamer pour sess_... démarré` | `[REGISTER] session sess_... machine=desktop-58d5cac` | Notif "C'est parti !" |
| 2 | Saisir nom "test_finalize_replay", confirmer | idem | idem | Tray icône passe rouge |
| 3 | Faire 3-5 clics dans une app simple (calculatrice, notepad) | `Action capturée : mouse_click` × N | événements + screenshots reçus dans `/event` `/image` | Compteur "X étapes mémorisées" augmente dans le menu tray |
| 4 | Clic droit tray → "C'est terminé" | `Enregistrement arrete : test_finalize_replay (N actions)` puis `Streamer ... arrêté` puis **`Session finalisée: {...replay_ready: true, replay_request: {session_id: sess_..., ...}, replay_launch: {status: ...}}`** | `[FINALIZE] sess_... → workflow construit, replay_request retourné` | Notif "Merci ! J'ai bien mémorisé vos N actions." |
| 5 | (auto) | `agent_v1.ui.smart_tray.offer_finalize_replay : proposition test 'test_finalize_replay'` | rien | Notif toast "J'ai compris la tâche 'test_finalize_replay'. Voulez-vous la tester ?" |
| 6 | Dialog tkinter Yes/No s'ouvre, cliquer **Oui** | `agent_v1.ui.smart_tray Replay demarre pour workflow ...` | `[REPLAY_START] replay_id=replay_..., workflow=...` puis actions enqueues | Tray icône passe bleu (replay actif), notification Article 50 "Le système d'intelligence artificielle exécute la tâche..." |
| 7 | Observer Léa rejouer les clics | `Replay action received : click ...` × N puis `Replay terminé — retour en mode capture` | actions dispatchées via `/replay/next`, résultats via `/replay/result` | Les clics sont rejoués à l'écran. Tray icône revient vert. Notif "C'est fait !" |
**Critère succès global** : 7 étapes en ≤ 5 min, sans intervention manuelle entre 4 et 7 autre que le dialog `_ask_consent`.
### Test d'échec (variantes à essayer après le nominal)
| Variante | Action | Comportement attendu |
|---|---|---|
| Refus utilisateur | Étape 6 : cliquer **Non** | `_launch_replay_request` non appelé, aucun POST `/replay/start`, log Léa silencieux après `offer_finalize_replay`, retour mode capture sans replay |
| Serveur ne renvoie pas `replay_ready` | Couper le câblage côté serveur pour qu'il renvoie juste `{}` | Léa log `Session finalisée: {}`, **aucune notif**, pas de dialog. Comportement = backward-compat parfaite |
| Serveur renvoie `replay_launch.status=started` | Test serveur déjà lance le replay | Log Léa `Replay direct déjà lancé par le serveur après finalize`, dialog **NON affiché** (logique `dispatch_finalize_result:20-22`) |
| Serveur renvoie `replay_launch.status=failed` | Idem | Log warning `Auto-replay serveur échoué après finalize, proposition manuelle`, dialog **affiché** (fallback) |
| `finalize_contract.py` absent | Ne pas SCP ce fichier, relancer Léa | `ImportError: cannot import name 'dispatch_finalize_result'` au démarrage, agent ne démarre pas |
| `offer_finalize_replay` absent (smart_tray ancien) | Ne pas SCP smart_tray.py, relancer | Log `UI indisponible pour proposer un test immédiat`, pas de dialog, replay non lancé |
| Network coupé pendant finalize | Débrancher Wi-Fi pile au "C'est terminé" | Log Léa `Finalisation échouée: <exception>`, callback non invoqué, aucune notif. Session reste localement, retry implicite au prochain run (buffer SQLite) |
| Replay impossible (workflow vide ou bug) | Faire 0 clic pendant l'enregistrement avant Stop | Selon serveur : si `replay_ready=false`, dispatch silencieux ligne 24. Pas de dialog, comportement attendu = ne pas proposer un test inutile |
### Signes d'échec à surveiller
- **Stack trace au démarrage Léa** → `finalize_contract.py` manquant ou syntax error
- **Aucun log `Session finalisée:` après "C'est terminé"** → streamer n'arrive pas à appeler `/finalize` (réseau ? auth ? port ?)
- **Log `Session finalisée: {...}` MAIS pas de notif Léa** → soit le payload ne contient pas `replay_ready/replay_request`, soit `offer_finalize_replay` n'est pas sur smart_tray (ancien fichier)
- **Notif affichée mais pas de dialog Yes/No** → bug tkinter (rare) ou thread bloqué (vérifier `_ask_consent` qui crée son propre Tk())
- **Dialog accepté mais aucun POST `/replay/start` côté serveur** → `_launch_replay_request` mal câblé ou `replay_request` invalide (session_id absent)
## Risques / points ouverts
1. **`finalize_contract.py` jamais committé** — `git status` le marque untracked. Risque : disparition silencieuse au prochain `git stash` ou `git checkout`. À committer avant la session de test.
2. **Le SCP des 5 fichiers n'est pas atomique** — si Léa est relancée entre le SCP de 2 fichiers, état incohérent possible. Mitigation : SCP les 5 fichiers d'abord, **puis** relancer Léa, jamais l'inverse.
3. **Le miroir `agent_v0/deploy/windows_client/agent_v1/` est obsolète de 7 semaines** — risque de confusion si quelqu'un essaie de redéployer un poste neuf via `setup.bat`. Hors scope mission 5 mais à signaler pour le prochain client.
4. **`_last_recording_name` peut être vide si `start_session` est appelé avec une chaîne vide** — `_on_finalize_result` fallback sur "la tâche que vous venez d'enregistrer", testé dans `finalize_contract.py:38`. OK mais l'UX dégrade.
5. **Le contrat serveur n'est pas encore validé** — la mission concerne le côté agent. Vérifier côté serveur que `/finalize` retourne bien `{replay_ready, replay_request, replay_launch}` AVANT de smoke-tester. Si le serveur retourne encore l'ancien payload, le test nominal s'arrêtera après étape 4 sans crash mais sans proposition (variante 2 du test d'échec).
6. **Token API `RPA_API_TOKEN` côté Windows** — si la variable n'est pas définie dans la session Windows actuelle, le `register` et le `finalize` retourneront 401 silencieusement (cf. `streamer.py:573-600`). Sympôme : log Léa `Enregistrement session échoué: 401`.
7. **Pas de test d'idempotence** — si l'utilisateur enchaîne 2 enregistrements rapidement et accepte le test du premier, le `_replay_active` pourrait masquer un second `offer_finalize_replay`. À surveiller mais hors scope ici.
8. **Article 14 / Article 50** — le dialog `_ask_consent` couvre l'Article 14 (contrôle humain avant lancement auto). La notification Article 50 (transparence "le système IA exécute...") est déjà câblée dans `_launch_replay_request` réutilisé. Conforme.
## Méthode d'audit
- Lecture intégrale : `finalize_contract.py` (40 lignes), `LISEZMOI.txt` deploy, `setup.bat`.
- Lecture déjà réalisée (audits précédents 2026-05-19/20) : `streamer.py`, `main.py`, `smart_tray.py` complets, `vision/capturer.py`.
- Lecture ciblée : `BUILD_DEPLOY_GUIDE.md` (constate obsolescence), `feedback_scp_auto_modif_client_windows.md`, handoff 2026-05-16 (commandes SCP de référence).
- Grep : consumers `dispatch_finalize_result`, `offer_finalize_replay`, `_launch_replay_request`, `_last_recording_name`.
- Diff dates source vs miroir deploy/windows_client pour valider obsolescence.
- `git diff agent_v0/agent_v1/network/streamer.py` pour confirmer la nature exacte des modifs.
- **Aucune modification de code, aucune action VWB, aucune modif de script** — conformément aux interdits.

View File

@@ -0,0 +1,393 @@
# Synthèse croisée — Études techno RPA vision & méthodes de replay
**Date :** 2026-05-23
**Auteur :** Claude (session principale) à partir des `docs/` du projet
**Périmètre :** consolider, en un seul document, tout ce que `rpa_vision_v3/docs/` contient sur (a) les techno externes étudiées pour faire avancer le RPA visuel et (b) les méthodes de replay testées/diagnostiquées. Destiné à être lu par un agent comme brief de mise à niveau.
**Statut :** lecture seule, pas d'action. Pas de proposition d'implémentation : on consolide ce qui est déjà documenté et on signale les liens.
---
## 0. Comment lire ce document
Le projet a accumulé sur marsmai 2026 :
- Des **explorations externes** (5+4 frameworks computer-use / RPA visuel).
- Des **benchmarks internes** (VLM grounding, LLM décision T2A, LLM safety checks).
- Des **diagnostics replay post-démo GHT** très précis, qui révèlent que la cause des échecs récents n'est PAS la cascade de résolution UI, mais la couche transport et un bug OCR latent.
Les sections 1 à 4 sont organisées par thème (frameworks, VLM/grounding, LLM décisionnels, replay). La section 5 croise tout et signale les liens forts. La section 6 est la carte des documents de référence.
---
## 1. Frameworks externes étudiés (RPA visuel, GUI agents, computer-use)
Deux vagues d'exploration distinctes, à 5 jours d'écart, par 2 canaux différents — leur convergence est elle-même un signal.
### 1.1. Vague QW Suite Mai — 5 frameworks computer-use (2026-05-05)
Source : `docs/superpowers/specs/2026-05-05-qw-suite-mai-design.md` §1 + `docs/QW_SUITE_MAI.md` en-tête.
| Framework | Apport repéré | Quick win extrait |
|---|---|---|
| **Simular Agent-S** | architecture computer-use mature | (pas de QW direct) |
| **browser-use** | détection de boucle de stagnation | **QW2 LoopDetector composite** (CLIP screen_static + action_repeat + retry_threshold) |
| **OpenAI CUA (sample)** | pattern de validations humaines avant action | **QW4 Safety checks hybrides** (declaratif + LLM contextuel local) |
| **Coasty (open-cu)** | computer-use générique | (pas de QW direct) |
| **Showlab OOTB** | capture/grounding propre par moniteur | **QW1 Multi-écrans** (`monitor_index` + fallback focus actif puis composite) |
**Livrables réels (commit en branche `feature/qw-suite-mai`)** :
- QW1 multi-écrans + `MonitorRouter` (`screeninfo>=0.8` ajouté) — `loop_detector.py`, commits `6582a69d3`, `b1a3aa16f`, `2d71e2a24`, fix `fc01afa59`.
- QW2 LoopDetector — commit `2a51a844b`.
- QW4 SafetyChecksProvider + endpoint `/api/v3/replay/resume` + `<ChecklistPanel>` VWB — commit `7c6945171`, `0a02a6ec9` (sélection du LLM).
- 24 tests QW + 89 baseline = 113 verts.
- Kill-switches env vars : `RPA_LOOP_DETECTOR_ENABLED`, `RPA_SAFETY_CHECKS_LLM_ENABLED`, `RPA_LOOP_SCREEN_STATIC_THRESHOLD`, etc.
### 1.2. Vague Inspiration Frameworks — 4 projets RPA visuel (2026-05-10)
Source : `docs/INSPIRATION_FRAMEWORKS_2026-05-10.md`.
| Framework | Stars (mai 2026) | Apport conceptuel |
|---|---|---|
| **OpenAdapt** (OpenAdaptAI) | ~7k | RPA générative VLM, "success traces become new training data" (Evaluation-Driven Feedback) |
| **Skyvern** (Skyvern-AI) | ~12k | browser RPA vision, **Planner-Actor-Validator loop**, **Visual Workflow Builder**, prompt caching, WebVoyager 85.85% |
| **OmniParser** (Microsoft) | ~22k | tokenisation d'écran (interactable elements + captions sémantiques) AVANT le VLM principal, OmniTool VM Windows |
| **TagUI** (AI Singapore) | — | RPA cross-OS multilangue, moins LLM-first |
**Convergences fortes mises en évidence dans le doc** :
1. **Policy / Grounding Separation** — chez nous VWB = Policy, cascade `_resolve_target` (OCR → template → VLM) = Grounding. Existe en pratique, à formaliser dans la doc.
2. **Safety Gate** — Léa vérifie l'ancre visuelle avant de cliquer. Vocabulaire reconnu → atout pitch healthtech.
3. **Abstraction Ladder** — replay déclaratif VWB (low) ↔ ORA `observe_reason_act` (high). À ne PAS opposer.
4. **Planner-Actor-Validator** (Skyvern) — VWB ≈ Planner statique, Léa ≈ Actor + Validator partiel. Le bug du step 10 démontre que **notre Validator est laxiste** (pHash global au lieu de vérification sémantique).
5. **Tokenizing UI screenshots** (OmniParser) — pas en place chez nous. Suggestion modeste : logger la liste des candidats à chaque appel `_resolve_target` (vue parsée implicite).
6. **OmniTool VM Windows + agent côté serveur** — convergence indépendante avec notre archi Léa Windows + agent Linux.
**Repères benchmarks externes mentionnés** : WindowsAgentArena, ScreenSpot, ScreenSpot-Pro, WebVoyager.
### 1.3. Convergences et différentiels nets
| Ce qu'ils ont en plus | Ce qu'on a en plus |
|---|---|
| Formalisation explicite des composants | Spécialisation healthtech (moat) |
| Communication mainstream | Validation visuelle (Safety Gate) côté agent Windows |
| Pre-built templates / use cases | VWB visuel = différenciateur UX |
| | Stack on-premise complète (RGPD / souveraineté) |
---
## 2. VLM et grounding — historique implémentations + benchmarks
Source maître : `docs/HISTORIQUE_VLM_IMPLEMENTATIONS_2026-05-08.md` (audit branche `feature/qw-suite-mai` HEAD `731b5bcae`).
### 2.1. Modèles VLM passés ou en place
| Modèle | Période | Backend(s) | Statut |
|---|---|---|---|
| `qwen2.5-vl:7b` | 2026-03 → encore présent | Ollama + vLLM | actif (mais déborde VRAM, voir 2.3) |
| `qwen3-vl:8b` | 2026-04 → | Ollama | fallback dans `vlm_config.FALLBACK_VLM_MODELS` |
| `UI-TARS-1.5-7B` | 2026-04-25 (commit `9da589c8c`) | Transformers Flask `server.py` 4-bit NF4 | **remplacé** par InfiGUI (commit `77faa03ec`) |
| **`InfiGUI-G1-3B`** | 2026-04-26 → | Transformers (Flask `server.py` + worker subprocess + daemon Unix socket) | **principal grounding aujourd'hui** (3.9 GB VRAM, `_smart_resize` complet) |
| `SeeClick` (Qwen-VL) | 2026-01 → ? | HuggingFace direct | exporté mais "cassé" — commit `d1b556b6c` retire de l'executor |
| `OWL-v2` (Google) | — | Transformers direct (`OwlDetector`) | détecteur open-vocabulary, câblé dans `ui_detector.py` mais aucun bench récent |
| `medgemma:4b` | — | Ollama | retenu QW4 par défaut puis **évincé** au bench (cf. 2.4) |
| `gemma4:e4b` / `gemma4:latest` | — | Ollama | usage VLM et safety_checks |
| Cloud opt-in OpenAI/Gemini/Anthropic | — | HTTP | `VLM_ALLOW_CLOUD=true` dans `visual_workflow_builder/backend/vlm_provider.py` |
3 entry-points distincts coexistent pour la même logique Transformers : `core/grounding/server.py` (Flask :8200), `core/grounding/infigui_server.py` (Unix socket, service systemd `rpa-grounding.service`), `core/grounding/infigui_worker.py` (subprocess one-shot). Le pipeline FAST→SMART→THINK + ThinkArbiter + ShadowLearningHook a été câblé en avril (commits Phase 1→6 entre `ea36bba5c` et `73cea2385`).
### 2.2. Backends testés
`Ollama HTTP`, `vLLM HTTP OpenAI-compat`, `Transformers in-process` (3 entry-points), `HuggingFace direct` (SeeClick, OWL-v2), `Cloud opt-in`.
vLLM a été ajouté en mars (commit `394342be7` du 2026-03-31), positionné comme essai principal AVANT fallback Ollama dans `resolve_engine.py:785-816`. Modèle hardcodé par défaut `Qwen/Qwen2.5-VL-7B-Instruct-AWQ`, switchable via env `VLLM_MODEL`.
### 2.3. Bench grounding bbox_2d (2026-05-08)
Source : `docs/MIGRATION_VLM_PLAN_2026-05-09.md` §2.
Screenshot fixture : `data/training/live_sessions/bg_DESKTOP-58D5CAC_windows/shots/heartbeat_1773792436.png` (2560×1600, dialog OK/Cancel).
| Modèle / config | Latence | bbox_2d | Parse regex prod | Coords cohérentes |
|---|---|---|---|---|
| `qwen2.5vl:7b` Ollama (prod) | 11.0 s | `{"bbox_2d":[422,604,462,624]}` | oui | **non** (cx ≈ 0.17, top-left) |
| `qwen3-vl:8b` Ollama (prod strict) | 8.0 s | vide (thinking absorbe les 50 tokens) | non | n/a |
| `qwen3-vl:8b` Ollama (`think:false`, num_predict=256) | 1.7 s | liste nue `[332,487,362,507]` | non (regex attend objet) | n/a |
| `qwen3-vl:8b` Ollama (prompt JSON explicite) | 1.8 s | `{"bbox_2d":[...]}` | oui | **non** (même bug d'échelle) |
**Deux problèmes structurels relevés** :
1. **VRAM saturée** : qwen2.5vl:7b = 14 GB en mémoire totale, RTX 5070 = 12 GB. Ollama bascule split CPU/GPU 42/58 → 11s par appel. qwen3-vl:8b (6 GB) tient full GPU à 1.7 s.
2. **Bug d'échelle bbox_2d** : Qwen2.5-VL retourne les coords dans la résolution **post-smart_resize**, pas l'orig. Ollama applique son propre smart_resize sans exposer la taille → prod divise par `orig_w` au lieu de `resized_w` → coords toutes shiftées top-left. Présent dans 4 occurrences de `resolve_engine.py` + `_locate_popup_button` (L:2576). Source : HF discussion #13 Qwen2.5-VL-7B-Instruct. Cité mainteneur : « *bbox_2d coordinates will be relative to your resized image size* » + « *resized dimensions parameter is not supported in OLLAMA* ».
**Conclusion plan migration** : tant qu'on passe par Ollama le fix n'est qu'une rustine. Cible = vLLM ou Transformers direct avec passage explicite de `resized_width`/`resized_height`. Modèle pressenti `qwen3-vl:8b`. Module `smart_resize` officiel commité (`0d7bcd18a`) mais **DETTE-014** : calé sur mauvaise référence (`patch_size=16` Qwen3-VL → factor 32, pas 28).
### 2.4. Bench safety_checks (2026-05-06)
Source : `docs/BENCH_SAFETY_CHECKS_2026-05-06.md`. Méthodo : 5 scénarios anomalies × 5 modèles, cold + 3 runs warm, métriques (JSON valide, détection, latence).
| Modèle | Cold (s) | Warm avg (s) | JSON | Détection |
|---|---:|---:|---:|---:|
| **`gemma4:latest`** | 10.6 | **2.9** | 92% | **46%** ✅ retenu |
| `qwen3-vl:8b` | 5.6 | — | **0%** | 0% (ignore `format=json` Ollama) |
| `qwen2.5vl:7b` | 9.4 | 6.6 | 100% | 23% |
| `qwen2.5vl:3b` | 6.0 | 2.0 | 100% | 8% (vérifie pour vérifier) |
| `medgemma:4b` | 2.0 | 0.5 | 100% | **0%** (renvoie systématiquement `[]`) |
Enseignement : `medgemma:4b` malgré son nom est **mauvais choix par défaut** (trop obéissant au "rien à signaler"). `gemma4:latest` gagne sur cohérence motif/diagnostic. `qwen3-vl:8b` à écarter tant qu'il ignore `format=json` Ollama.
Aucun modèle ne détecte les 5 scénarios — IPP corrompu et forfait incohérent ratés par tous.
### 2.5. Code potentiellement utilisable pour la migration
Repris de `HISTORIQUE_VLM_IMPLEMENTATIONS_2026-05-08.md` §6 :
- **Transformers clé en main** : `core/grounding/server.py` (loader + `_smart_resize` complet MIN/MAX_PIXELS) et `core/grounding/infigui_worker.py` (`load_model`, `infer`). Il suffit de changer `MODEL_ID` (env `GROUNDING_MODEL` déjà supporté).
- **vLLM clé en main** : `resolve_engine.py:785-816` contient déjà l'appel HTTP OpenAI-compat avec `image_url: data:image/jpeg;base64`. Il manque le passage de `resized_width/resized_height` (extension OpenAI vLLM).
- **Socket persistant + fallback subprocess** : `infigui_server.py` + `ui_tars_grounder.py` réutilisables tels quels.
- **Prompt UI-TARS officiel** récupérable via `git show 9da589c8c:core/grounding/server.py`.
---
## 3. LLM décisionnels (T2A) — bench 18 modèles
Source : `docs/BENCH_T2A_DECISION_11DOSSIERS.md` (2026-05-05). 18 modèles × 11 DPI GHT Sud 95 (5 UHCD / 6 Forfait, vérité-terrain corrigée le 2026-05-05).
Top 5 :
| # | Modèle | Acc | p50 | Verdict |
|---|---|---:|---:|---|
| 1 | `gemma3:27b-cloud` | 8/11 (73%) | 10.6s | 🟢 retenu démo |
| 2 | `qwen3:8b` | 7/11 (64%) | 7.6s | 🟡 backup local (5 GB) |
| 3 | `qwen2.5:7b` | 7/11 (64%) | 10.0s | 🟡 |
| 4 | `qwen3-vl:235b-instruct-cloud` | 7/11 (64%) | 20.3s | 🟡 |
| 5 | `qwen3.5:9b` | 7/11 (64%) | 25.8s | 🟡 |
Échecs notables :
- `gemma4:latest` 6/11 (55%) — **insuffisant** sur décision T2A (mais OK sur safety_checks, cf. §2.4).
- `medgemma:4b` 4/11 (36%) — JSON cassé via Ollama, **PAS le LLM médical magique** que son nom suggère.
- `gpt-oss:20b-cloud` 0% — format JSON cassé.
- 3 cas universellement ratés (Pneumo VRS, Aura migr., Salpingite) → soit DPI à enrichir, soit vérité-terrain à rediscuter avec Pauline.
**Pendant la démo réelle** (cf. `LESSONS_LEARNED_GHT_2026-05.md`) : `gemma4:31b-cloud` retenu pour T2A MOREL (qualité clinique propre, run 8 du 12 mai). Test ponctuel de `qwen3-next:80b-cloud` OK aussi mais pas durable. Ollama Cloud 503 vécue le 12 mai → robustesse non couverte. **`gemma3:27b` confabule sur PMSI français** (invente GEMSA), à NE PAS utiliser en fallback.
---
## 4. Replay — méthodes, blocages observés, options
### 4.1. Architecture replay actuelle (rappel)
VWB Flask + SQLite édite un workflow déclaratif. Le serveur (`api_stream.py`) dispatche step par step à un client Léa V1 sur Windows distant, qui exécute via la cascade de résolution UI (OCR → template → VLM/grounding). Transport = **HTTP pull / long-poll** `GET /replay/next`.
Trois engines pertinents :
- `agent_v0/server_v1/replay_engine.py` — pilotage côté serveur
- `agent_v0/server_v1/replay_verifier.py` — vérification post-action
- `agent_v0/server_v1/replay_learner.py` + `replay_memory.py` — apprentissage (import cassé sur `get_target_memory_store`)
### 4.2. Diagnostic phare — Replay bloqué 8 mai 2026
Source : `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md`. Replay `replay_free_68ca51ab` sur `Urgence_aiva_demo` cancellé à 10:34.
**Deux causes simultanées :**
1. **Cause primaire — désync HTTP** :
Client Léa V1 utilise `read_timeout=5 s` sur `GET /replay/next` (`agent_v1/core/executor.py:1786`). Le serveur exécute parfois `extract_text` (57 s) PUIS dispatch un `click` dans le même appel HTTP. Le client coupe avant la réponse. L'action `click` est déjà *poppée* de la queue serveur et stockée dans `_retry_pending` mais **jamais re-dispatchée** (pas de watchdog). 9 actions sautées en 33 s sur les steps 10/12/14/17.
2. **Cause aggravante — OCR-DIRECT center-of-line** :
`_resolve_by_ocr_text` (`resolve_engine.py:1447-1527`) retourne le centre de la **ligne docTR entière** quand le `target_text` est un sous-fragment (score 0.8). La barre de tabs Easily est détectée comme UNE ligne → `Imagerie` / `Notes médicales` / `Synthèse Urgences` renvoient tous (0.23, 0.28). Confirmé par e2e_singleshot du même jour.
### 4.3. Hypothèses systématiquement testées (≠ rustinées)
Cf. §2 du diagnostic. Sur 6 hypothèses :
- Hyp #1 (cascade serveur foire) : **infirmée** — la cascade n'est même jamais invoquée pour ces tabs.
- Hyp #2 (cascade locale Léa) : **infirmée** — client n'a rien reçu.
- Hyp #3 (coords brutes obsolètes) : **infirmée** — strict mode bypass la bbox. Anomalie cosmétique restante : ancres `anchor_0438bd2d9bdd_1778161174` et `anchor_6a2591e7c51c_1778229076` ont des bboxes décalées d'un cran à gauche.
- Hyp #4 (offset écran live vs record) : **partiellement vraie** — offset ±10-30 px, gérable, dégradé par le bug OCR-DIRECT.
- Hyp #5 (event onclick JS de la maquette) : **infirmée**`addEventListener('click')` propre sur `<a class="tab">`, aucun overlay.
- Hyp #6 (cache client/serveur) : **infirmée** — aucun `from_memory=True`, TargetMemoryStore pas hit.
### 4.4. Quatre correctifs proposés (gradués)
Repris du diagnostic §5 :
| Correctif | Effort | Risque | Effet |
|---|---|---|---|
| **Quick fix 1**`timeout=5 → 30` côté client | 510 min | très bas | Plus aucun click perdu sur extract_text/t2a_decision lents |
| **Quick fix 2** — OCR-DIRECT center-of-span | 1015 min | moyen | Chaque tab résolu à son propre centre. À NE PAS appliquer à chaud démo |
| **Fix moyen terme** — watchdog `_retry_pending` côté serveur | 3060 min | moyen | Toute action dispatchée sans REPORT depuis > 30s repush en tête de queue, `lea:dispatch_orphan_resent` |
| **Fix structurel** — SSE ou WebSocket | 12 j | — | Push ack-based, suppression du bug timeout pour de bon, détection immédiate de déconnexion |
### 4.5. État post-démo (19 mai) — 5 bugs racines P0 non résolus
Source : `docs/LESSONS_LEARNED_GHT_2026-05.md` §🔴.
| Bug | Cause | Contournement actuel |
|---|---|---|
| VWB recapture anchor ne régénère pas le PNG | `capture.py` réutilise PNG existant ou écrit avant screenshot | recapture inutile |
| Stop VWB ne purge pas la queue serveur | pas d'appel `POST /api/v1/traces/stream/replay/<id>/cancel` au clic Stop | `./scripts/cancel-replays.sh` manuel |
| Coord client Léa Y cassé (÷ ~27) | `mss.monitors[1]` retourne intermittemment `2560×60` | aucun |
| Bug skip ord 13 orchestration | non identifiée, transition serveur→visuel→serveur | aucun, NOT REPRO 100% |
| Bug échelle pixel grounding Ollama | DETTE-006/010/014, `Qwen2VLImageProcessorFast` `patch_size=16` factor 32 ≠ 28 | non posé |
Démo livrée grâce à `Demo_urgence_3_db` (46 steps, MOREL Catherine, UHCD 1750 €) et **bypass LLM static_result/static_text** (`replay_engine.py` steps 12-14). Pas réutilisable client.
### 4.6. Contournements actifs côté replay (à survoler avant tout déploiement)
Repris de `LESSONS_LEARNED_GHT_2026-05.md` (§⚠ Contournements actifs) :
- Drift exemption template ≥ 0.95 / hybrid ≥ 0.80 (`resolve_engine.py:2367-2390`)
- Fallback heartbeat sur capture < 1200×800 (`api_stream.py:4422`)
- Flag pré-check OCR off par défaut (`RPA_ENABLE_TEXT_PRECHECK=false`)
- `RPA_VLM_MODEL=gemma4:e4b` hardcodé Léa (tag inexistant) — exporter `qwen2.5vl:7b`
- Mot de passe `loli` en clair dans scripts SSH/sudo
- Bypass Ctrl+V via `ydotool` au lieu de NoMachine clipboard
### 4.7. Code orphelin/débranché côté replay (audit 8 mai)
- `_resolve_by_yolo` défini, importé, jamais appelé (DETTE-004)
- `_fuzzy_match` import mort `api_stream.py:4372`
- `VisualEmbeddingManager` + `ScreenshotValidationManager` définis non instanciés (DETTE-005)
- `ShadowLearningHook` défini non instancié (DETTE-009)
- `_handle_possible_popup` côté client, 0 site d'appel
- Pre-check VLM par-clic désactivé par `if False:` dans `observe_reason_act.py:1704-1713` (DETTE-008)
- Trois implémentations `smart_resize` coexistent (DETTE-007)
- `pause_for_human` ignorée silencieusement en mode autonome (`api_stream.py:3011-3017`)
### 4.8. Smoke test finalize → replay (2026-05-20)
Source : `docs/SMOKE_TEST_FINALIZE_REPLAY_2026-05-20.md`.
Le flux `finalize → proposition utilisateur → replay-session` est **déjà câblé** côté agent :
- `streamer.py:96-103, 627-635``set_on_finalize_result` + invocation
- `main.py:31, 222, 445-448` — wire callback dans `start_session`
- `finalize_contract.py` (untracked) — `dispatch_finalize_result(ui, payload, replay_name)`
- `smart_tray.py:577-599``offer_finalize_replay` + dialog consent Article 14
Contrat serveur attendu : `replay_launch.status` (`started`/`failed`), `replay_ready`, `replay_request`.
**4 fichiers à SCP manuellement** vers Léa Windows (`finalize_contract.py` en premier, c'est un nouveau fichier, `ImportError` garanti sinon).
---
## 5. Croisements et liens forts
### 5.1. Le replay actuel échoue avant même que le grounding entre en scène
Le diagnostic 8 mai est explicite : le bug primaire est **transport** (HTTP pull/long-poll, timeout client trop court, pas de watchdog), pas **vision**. Tous les efforts d'amélioration VLM (vLLM, InfiGUI, Qwen3-VL, smart_resize) sont nécessaires mais ne corrigeront RIEN tant que les actions sont perdues entre serveur et client.
**Lien fort** : QW2 LoopDetector + QW4 SafetyChecksProvider (sprint mai) traitent les symptômes (UI bloquée, validation humaine) mais pas la cause racine (HTTP fragile). Une refonte SSE/WebSocket est citée comme "fix structurel" par le diagnostic 8 mai et n'apparaît dans aucun plan post-démo.
### 5.2. Le pattern Planner-Actor-Validator de Skyvern décrit littéralement notre dette
- VWB = Planner statique → mais **inflexible**, pas de re-planification au runtime.
- Léa = Actor → fonctionnel.
- Validator = absent ou laxiste → le bug step 10 (Imagerie cliqué dans bandeau Edge, REPORT success=True) en est l'illustration.
Le pHash global utilisé pour VERIFY post-action est connu insuffisant (`feedback_phash_vs_dialog_in_vm.md`). Un Validator-as-component avec vérification sémantique (texte attendu présent dans la zone visée, par exemple) éliminerait toute la classe de bugs "j'ai cliqué quelque part mais pas où je voulais".
### 5.3. OmniParser ↔ notre cascade
OmniParser tokenise l'écran en éléments structurés AVANT le VLM principal. UI-DETR-1 fait quelque chose d'analogue mais SEULEMENT côté VWB recording, pas en replay runtime — asymétrie connue (cf. CLAUDE.md). Logger systématiquement la liste des candidats à chaque appel `_resolve_target` (suggestion §4.1 du doc inspiration) donnerait une "vue parsée" implicite sans refonte.
### 5.4. medgemma:4b — l'illusion du modèle médical
Apparaît avec un nom prometteur dans 2 benchs distincts (`BENCH_SAFETY_CHECKS_2026-05-06.md` §résultats, `BENCH_T2A_DECISION_11DOSSIERS.md` #12). **Mauvais aux deux** : `[]` systématique en safety_checks, 4/11 (36%) en T2A avec JSON cassé. Le nom suggère plus que la performance.
### 5.5. Le bug d'échelle bbox_2d est la racine documentée d'un problème encore actif
`MIGRATION_VLM_PLAN_2026-05-09.md` documente précisément la cause (smart_resize Ollama opaque). Le module officiel a été commité (`0d7bcd18a`) mais DETTE-014 signale qu'il est mal calé. Trois implémentations coexistent (DETTE-007). Le bench bbox cible (§5 du plan) n'a **pas été refait** post-migration — checkpoint manquant pour valider la trajectoire.
### 5.6. Les frameworks externes valident notre direction
OpenAdapt + Skyvern + OmniParser + TagUI = 4 projets matures qui formalisent ce qu'on fait déjà en pratique. Convergence indépendante = signal fort.
Mais ce qu'ils ont en plus n'est pas trivial à rattraper :
- Vocabulaire normé (Policy/Grounding/Safety Gate/Validator) → adoptable sans refonte, gain pitch.
- Pre-built templates / use cases → maturité de marché, on n'a que la démo GHT.
- Evaluation-Driven Feedback (OpenAdapt) — on a `TargetMemoryStore` (Phase 1 apprentissage Léa) mais sans pipeline d'entraînement.
---
## 6. Carte des documents de référence (à charger dans le contexte selon le sujet)
### Étude techno externe
- **`docs/INSPIRATION_FRAMEWORKS_2026-05-10.md`** — OpenAdapt / Skyvern / OmniParser / TagUI, patterns à adopter
- **`docs/QW_SUITE_MAI.md`** — synthèse livraison QW1+QW2+QW4 (5 frameworks computer-use)
- **`docs/superpowers/specs/2026-05-05-qw-suite-mai-design.md`** — spec design détaillée, décisions architecturales
- **`docs/superpowers/plans/2026-05-05-qw-suite-mai.md`** — plan d'exécution
### VLM, grounding, modèles
- **`docs/HISTORIQUE_VLM_IMPLEMENTATIONS_2026-05-08.md`** — audit complet implémentations, commits, code potentiellement perdu (rien d'irrécupérable)
- **`docs/MIGRATION_VLM_PLAN_2026-05-09.md`** — plan migration `qwen2.5vl Ollama → vLLM/Transformers Qwen3-VL`, bench bbox_2d
- `docs/VLM_DETECTION_IMPLEMENTATION.md` — implémentation initiale (22 nov 2025)
- `docs/OLLAMA_INTEGRATION.md` — intégration Ollama
- `docs/reference/QWEN3_VL_CONFIGURATION.md` — config Qwen3-VL
### Benchmarks
- **`docs/BENCH_T2A_DECISION_11DOSSIERS.md`** — 18 modèles × 11 DPI, gemma3:27b-cloud retenu
- **`docs/BENCH_SAFETY_CHECKS_2026-05-06.md`** — 5 modèles × 5 scénarios, gemma4:latest retenu
- `docs/BENCH_MEDGEMMA.md` + `.json` — medgemma:4b CIM-10 (écarté pour safety/T2A)
- `docs/BENCH_MINI_LLM_NLP.md` — 14 commandes NLP fr pour Léa
- `docs/QW_SMOKE_TESTS_2026-05-06.md` — smoke tests QW Suite
### Replay — diagnostic et méthodes
- **`docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md`** — diagnostic complet replay_free_68ca51ab, 4 correctifs proposés, le doc le plus instructif sur les méthodes
- **`docs/LESSONS_LEARNED_GHT_2026-05.md`** — 15 jours de bug-chasing, 5 bugs P0, contournements actifs
- `docs/SMOKE_TEST_FINALIZE_REPLAY_2026-05-20.md` — flux finalize → replay câblé, fichiers à SCP
- `docs/AUDIT_FINALIZE_CONTRACT_INTEGRATION_2026-05-20.md` — audit contrat finalize
- `docs/CR_AUDIT_PAUSED_RESUME_BUS_2026-05-22.md` — audit pause/resume bus de replay
- `docs/CR_AUDIT_SETUP_VISUAL_GUARDS_2026-05-22.md` — audit guards visuels au replay
- `docs/AUDIT_LEA_FIRST_RUNTIME_2026-05-19.md` — audit runtime Léa-first post-démo
- `docs/EXECUTION_LOOP_FLAGS.md` — flags boucle d'exécution
- `docs/DETTE_TECHNIQUE.md` — 14 entrées (DETTE-001 à DETTE-014)
### Audits transversaux 8-10 mai
- `docs/AUDIT_CONTROLES_DEBRANCHES_2026-05-08.md` — 50+ findings serveur+client
- `docs/AUDIT_DIM_TIM_DEMO_GHT_2026-05-08.md` — audit médecin DIM + TIM
- `docs/AUDIT_BDD_WORKFLOW_2026-05-10.md` — audit BDD workflows
- `docs/BUG_PRECHECK_SPATIAL_BLINDNESS_2026-05-08.md` — DETTE-001
- `docs/INVESTIGATION_MEMOIRE_VISUELLE_ORPHELINE_2026-05-09.md` — DETTE-005, DETTE-009
- `docs/CARTE_FONCTIONNELLE_2026-05-08.md` — carte fonctionnelle système
### Vision/architecture de fond
- `docs/VISION_RPA_INTELLIGENT.md` — vision produit (CLAUDE.md `feedback_follow_spec` : à lire AVANT toute proposition)
- `docs/ROADMAP_RPA_100_VISION.md` — roadmap 100% vision
- `docs/REFERENCE_VISION_RPA.md` — référence VWB Vision RPA (jan 2026)
- `docs/CARTOGRAPHY.md` — cartographie d'exécution
- `docs/PLAN_ACTEUR_V1.md` / `docs/PLAN_APPRENTISSAGE_LEA.md` / `docs/PLAN_AGENT_RUST.md` — plans cibles
---
## 7. Projets et benchmarks externes nommément cités
À reprendre dans un brief ou un mail (sourcing déjà fait dans `INSPIRATION_FRAMEWORKS_2026-05-10.md` §8) :
- **OpenAdapt v1.0** — https://github.com/OpenAdaptAI/OpenAdapt
- **OmniParser V2** — https://github.com/microsoft/OmniParser
- **Skyvern** — https://github.com/Skyvern-AI/skyvern
- **TagUI** — AI Singapore
- **Simular Agent-S** — Simular (5 framework QW Suite)
- **browser-use** — open source, source LoopDetector
- **OpenAI CUA sample** — pattern Safety Checks
- **Coasty open-cu** — open source
- **Showlab OOTB** — source multi-écrans
- Benchmarks externes : **WindowsAgentArena**, **ScreenSpot**, **ScreenSpot-Pro**, **WebVoyager** (Skyvern 85.85%)
---
## 8. Ce que cette synthèse n'aborde pas (à demander à Dom si besoin)
- Estimation chiffrée du coût d'une refonte SSE/WebSocket dans `api_stream.py`.
- Comparaison vLLM vs Transformers direct pour le grounding (le plan migration laisse la question ouverte).
- État d'OpenAdapt Evaluation-Driven Feedback vis-à-vis de notre `TargetMemoryStore` actuel.
- Tests des modèles candidats à un fine-tuning GUI (GUI-R1, OS-Atlas, ShowUI, SE-GUI) cités dans `memory/project_finetuning_vlm_plan.md` mais hors périmètre des bench mai.
- Bench de UI-DETR-1 standalone (utilisé au recording VWB, pas évalué isolément).
---
*Document destiné à être consommé en lecture seule par un agent ou un humain qui doit se mettre à niveau sur l'état des études techno et du replay au 23 mai 2026. Toute action à prendre est hors-périmètre de ce doc et nécessite une décision explicite de Dom.*

View File

@@ -0,0 +1,474 @@
# Cartographie des briques existantes — modèle Mandat / Protocoles
Date : 2026-05-25
Statut : lecture structurelle, pas une spec d'implémentation
## Synthèse
Le projet contient déjà la plupart des briques nécessaires au modèle :
```text
intention / mandat -> agent_chat + working_memory + ORA
protocoles / gestes -> gesture_catalog + autonomous_planner + workflows VWB
observation visuelle -> capturers + grounding + SomEngine + OCR/VLM
scènes / dialogues -> DialogResolver + window guards + UI patterns
action -> agent_v1 executor + replay engine
vérification retour -> ReplayVerifier + Validator V2 + shadow validator
apprentissage -> replay_learner + target_memory + correction packs + coaching
évaluation -> LeaBench + tests replay + démo DPI
supervision -> pause supervisée + UI Léa + confirmation loop
```
La faiblesse principale n'est pas l'absence de composants. C'est leur absence de **contrat unificateur** autour de :
```text
mandat -> intention -> scène -> affordance -> geste -> retour -> preuve apprenable
```
## Briques runtime live
### Agent Windows live
Fichiers :
```text
agent_v0/agent_v1/core/executor.py
agent_v0/agent_v1/core/grounding.py
agent_v0/agent_v1/core/policy.py
agent_v0/agent_v1/core/recovery.py
agent_v0/agent_v1/core/system_dialog_guard.py
agent_v0/agent_v1/ui/*
```
Rôle actuel :
```text
exécuter les actions ;
résoudre les cibles visuelles ;
faire les fallbacks ;
pauser/demander de l'aide ;
gérer certains dialogues ;
remonter les résultats au serveur.
```
Lien avec le modèle :
```text
geste
observation locale
doute de localisation
doute de scène
retour action
pause supervisée
```
Manque conceptuel :
```text
pas encore de trace causale complète ;
pas de mandat/intention actif porté explicitement ;
fallbacks encore trop locaux si non encadrés par le contrat ;
pas de typage explicite du doute dans l'API résultat.
```
### Serveur replay / coordination
Fichiers :
```text
agent_v0/server_v1/api_stream.py
agent_v0/server_v1/stream_processor.py
agent_v0/server_v1/replay_engine.py
agent_v0/server_v1/replay_watchdog.py
agent_v0/server_v1/replay_verifier.py
agent_v0/server_v1/replay_learner.py
agent_v0/server_v1/replay_memory.py
agent_v0/server_v1/resolve_engine.py
```
Rôle actuel :
```text
construire/rejouer les actions ;
distribuer les actions à l'agent ;
résoudre les cibles côté serveur ;
vérifier les retours ;
planifier retries ou pauses ;
enregistrer résultats et apprentissages.
```
Lien avec le modèle :
```text
protocole candidat sous forme workflow/replay ;
résolution scène/affordance via resolve_engine ;
retour via ReplayVerifier ;
preuve potentielle via replay_learner/replay_memory ;
pause supervisée ;
temporalité via watchdog/retry.
```
Manque conceptuel :
```text
le workflow reste la structure dominante ;
les expected_result existent mais ne forment pas encore une trace causale ;
la vérification ne sait pas toujours quelle intention elle valide ;
les retries ne sont pas encore arbitrés par doute typé.
```
## Briques cognitives déjà présentes
### Mémoire de travail
Fichier :
```text
core/cognition/working_memory.py
```
Points forts :
```text
objective ;
current_step ;
observation courante ;
historique d'actions ;
faits appris ;
expected_screen ;
confidence ;
needs_help ;
timing.
```
Correspondance modèle :
```text
mandat/intention embryonnaire ;
trace courte ;
précondition attendue via expected_screen ;
temporalité ;
doute via confidence/needs_help.
```
Manque :
```text
pas encore de scène d'intention typée ;
pas de trace causale formelle geste/scène/intention/hypothèse/retour ;
pas de distinction correction/démonstration/validation.
```
### Boucle ORA
Fichier :
```text
core/execution/observe_reason_act.py
```
Points forts :
```text
observe -> reason -> act -> verify ;
Decision avec expected_after ;
VerificationResult ;
mode instruction via VLM ;
erreurs typées de base.
```
Correspondance modèle :
```text
base naturelle pour le contrat d'action ;
hypothèse de retour via expected_after ;
vérification post-geste.
```
Manque :
```text
encore orienté step/workflow ;
pas de protocole ouvert ;
pas de preuve apprenable structurée ;
pas d'opérateur d'extension de grammaire.
```
## Briques intention / gestes / risque
### Agent chat et planification autonome
Fichiers :
```text
agent_chat/intent_parser.py
agent_chat/autonomous_planner.py
agent_chat/gesture_catalog.py
agent_chat/confirmation.py
agent_chat/urgences_orchestrator.py
```
Points forts :
```text
parseur d'intentions utilisateur ;
planner libre sans workflow pré-enregistré ;
catalogue de gestes universels Windows/navigateur/édition ;
évaluation de risque ;
confirmation utilisateur ;
orchestrateur urgence avec intentions nommées.
```
Correspondance modèle :
```text
mandat ;
protocoles universels ;
gestes types ;
niveau de risque ;
protocole métier urgence.
```
Manque :
```text
cette couche semble parallèle au runtime live, pas intégrée au replay agent_v1 ;
le risque est lexical et statique, pas encore tutoré par niveau de délégation ;
les gestes ne sont pas encore reliés à scènes/affordances/retours qualifiés.
```
## Briques scènes / dialogues
### DialogResolver
Fichiers :
```text
agent_v0/server_v1/core/dialog/catalog.py
agent_v0/server_v1/core/dialog/resolver.py
tests/unit/test_dialog_resolver.py
tests/integration/test_dialog_resolver_endpoint.py
```
Points forts :
```text
catalogue de dialogues connus ;
policy auto/pause/skip ;
protection modaux système ;
pause conservative sans match ;
evidence title + OCR/UIA.
```
Correspondance modèle :
```text
scènes d'intention simples ;
affordances compatibles par dialogue ;
affordances anti-intention potentielles ;
rupture de scène.
```
Manque :
```text
pas encore branché sur une intention active ;
un dialogue est connu en absolu, pas encore "compatible avec ce mandat" ;
les scènes composées métier restent hors catalogue.
```
## Briques vérification / preuve
### ReplayVerifier et Validator V2
Fichiers :
```text
agent_v0/server_v1/replay_verifier.py
core/validation/orchestrator.py
core/workflow/shadow_validator.py
tests/unit/test_replay_critic.py
tests/integration/test_validator_step10.py
```
Points forts :
```text
vérification pixel ;
critic sémantique avec expected_result ;
verdicts agrégés ;
shadow validation.
```
Correspondance modèle :
```text
qualification du retour ;
preuve apprenable potentielle ;
doute d'effet ;
temporalité post-action.
```
Manque :
```text
le retour n'est pas toujours rattaché à intention/scène/affordance ;
le "rien ne change" n'est pas encore qualifié par protocole ;
la preuve apprenable n'est pas encore un objet explicite.
```
## Briques apprentissage / correction
### Mémoire et corrections
Fichiers :
```text
core/learning/target_memory_store.py
agent_v0/server_v1/replay_learner.py
agent_v0/server_v1/replay_memory.py
core/corrections/*
core/coaching/*
```
Points forts :
```text
mémoire de cible persistante JSONL + SQLite ;
résultats replay enregistrés ;
correction packs versionnés ;
statistiques succès/échec ;
sessions de coaching avec accept/reject/correct/manual/skip.
```
Correspondance modèle :
```text
preuve apprenable ;
correction ;
démonstration ;
validation ;
désapprentissage par baisse confiance / failure_count ;
niveau de délégation tutoré.
```
Manque :
```text
les preuves actuelles sont très orientées cible UI ;
il manque intention/scène/affordance/retour dans la clé d'apprentissage ;
correction et démonstration sont proches mais pas encore distinguées au niveau modèle ;
le niveau de délégation par protocole/environnement n'est pas centralisé.
```
## Briques évaluation / terrains d'essai
### LeaBench
Fichiers :
```text
core/evaluation/computer_use_bench.py
core/evaluation/ollama_lea_bench_adapter.py
tools/lea_bench.py
tools/lea_bench_ollama.py
benchmarks/computer_use/cases/*.jsonl
```
Rôle :
```text
mesurer décisions computer-use ;
cas Bloc-notes/replay ;
adapter modèles locaux ;
scorer abstention/danger.
```
Usage modèle :
```text
tester scène/intention/affordance ;
mesurer action fiable vs abstention ;
convertir échecs réels en cas reproductibles.
```
### Maquette DPI / urgence
Fichiers :
```text
demo/facturation_urgences/*
tests/e2e/test_urgence_aiva_demo.py
tests/e2e/fixtures/urgence_aiva_demo/*
```
Rôle :
```text
terrain métier ;
cas urgences synthétiques ;
workflow démo existant ;
stress test scènes composées et protocole métier.
```
Usage modèle :
```text
protocole métier générique ;
adaptateur applicatif ;
apprentissage sans contenu sensible réel ;
validation de la généralisation hors Bloc-notes.
```
## Diagnostic structurel
Le projet a déjà presque toutes les pièces, mais elles ne sont pas au même étage :
```text
agent_chat sait raisonner intention/risque mais n'est pas le runtime live principal ;
agent_v1 sait exécuter live mais raisonne encore action/cible ;
server_v1 sait vérifier/apprendre mais pas sous trace causale complète ;
core/cognition a la mémoire de travail mais elle n'est pas encore le fil commun ;
core/learning sait mémoriser mais pas encore apprendre une grammaire d'action ;
LeaBench sait mesurer mais doit recevoir des cas "mandat/protocole/scène".
```
## Assemblage conceptuel recommandé
Sans parler encore de code, la bonne direction est d'utiliser :
```text
core/cognition/working_memory.py
comme fil de mandat/intention/trace causale ;
agent_chat/gesture_catalog.py
comme base des protocoles universels ;
agent_v0/server_v1/core/dialog/*
comme catalogue initial de scènes/dialogues ;
agent_v0/server_v1/replay_verifier.py + core/validation/*
comme qualification du retour ;
core/learning + core/corrections + core/coaching
comme apprentissage tutoré et désapprentissage ;
LeaBench + Bloc-notes + maquette DPI
comme banc de validation progressif.
```
## Point d'attention immédiat
Une correction technique est déjà en attente dans :
```text
agent_v0/agent_v1/core/grounding.py
```
Elle bloque le fallback texte local après rejet sémantique serveur. Elle correspond directement à la règle :
```text
un rejet sémantique doit dominer les fallbacks opportunistes.
```
Elle devra être testée et intégrée quand on repassera en exécution.

View File

@@ -0,0 +1,99 @@
# Cartographie micro-apprentissage Lea - 2026-05-27
## But
Passer du replay metier fragile a une boucle courte ou Lea observe des gestes simples, les interprete, verifie leur effet, puis memorise ce qui marche.
On reutilise l'existant. On ne fabrique pas une boite a clic.
Premiere competence retenue: `ouvrir le menu Demarrer`. C'est plus atomique que "ouvrir Chrome" ou "ouvrir Word" et sert de base aux competences composees suivantes.
## Briques a reutiliser maintenant
| Besoin | Brique existante | Chemin |
|---|---|---|
| Demarrer une demonstration humaine | Bouton `Apprenez-moi` + `SharedState.start_recording()` | `agent_v0/agent_v1/ui/chat_window.py`, `agent_v0/agent_v1/ui/shared_state.py` |
| Capturer clics/frappes/ecran | `AgentV1.start_session()` + `TraceStreamer` | `agent_v0/agent_v1/main.py`, `agent_v0/agent_v1/network/streamer.py` |
| Recevoir les evenements | endpoint `/event` + `StreamProcessor.process_event()` | `agent_v0/server_v1/api_stream.py`, `agent_v0/server_v1/stream_processor.py` |
| Construire une trace exploitable | `StreamProcessor.finalize_session()` + `GraphBuilder` | `agent_v0/server_v1/stream_processor.py`, `core/graph/graph_builder.py` |
| Apprendre des corrections humaines | `ReplayLearner.record_human_correction()` | `agent_v0/server_v1/replay_learner.py` |
| Memoriser les cibles fiables | `memory_record_success()` / `memory_lookup()` | `agent_v0/server_v1/replay_memory.py`, `core/learning/target_memory_store.py` |
| Verifier qu'une action a eu un effet | `ReplayVerifier.verify_action()` | `agent_v0/server_v1/replay_verifier.py` |
| Parler clairement a l'humain | contrat 4 champs | `agent_v0/agent_v1/ui/message_contract.py` |
| Verifier le socle technique | preflight read-only | `tools/lea_micro_preflight.py` |
## Briques a eviter pour la V1 micro-learning
- Replay VWB/DAG comme moteur principal.
- Coordonnees brutes comme verite finale.
- Clipboard global Linux/Windows/NoMachine comme transport de donnees.
- Scenarios metier longs.
- Corrections injectees a la main dans un workflow pour faire passer une demo.
## Flux minimal
1. Observation
Dom lance `Apprenez-moi`, nomme une micro-tache, puis montre 1 geste court.
2. Interpretation
Le serveur conserve les evenements, les screenshots, les titres de fenetre, les indices UIA/OCR/SoM disponibles.
3. Generalisation prudente
Lea derive une competence courte: intention, contexte d'ecran attendu, signaux visuels, postcondition observable.
4. Tentative encadree
Lea ne tente en autonomie que si la cible est suffisamment decrite et verifiable.
5. Verification
Pixel diff seul ne suffit pas. Il faut verifier un effet observable: fenetre ouverte, champ rempli, titre change, application fermee, texte present.
6. Memoire
Les succes repetes alimentent `TargetMemoryStore`. Les echecs et corrections humaines alimentent `ReplayLearner`.
7. Demande humaine
Si Lea ne sait pas, elle doit afficher:
```text
J'essaie de : <INTENTION>
J'attendais : <ATTENDU>
Je vois : <VU>
Peux-tu : <DEMANDE>
```
## Etats d'une competence
| Etat | Sens |
|---|---|
| OBSERVATION | Lea a seulement vu le geste humain. |
| COACHING | Lea peut proposer une tentative, mais demande confirmation/correction. |
| AUTO_CANDIDATE | Lea a reussi plusieurs fois avec verification. |
| AUTO | Lea peut executer seule avec garde-fous. |
## Fichiers a lire/modifier ensuite
Priorite 1:
- `agent_v0/agent_v1/ui/chat_window.py` : texte de lancement/fin d'apprentissage.
- `agent_v0/agent_v1/ui/shared_state.py` : metadata de session micro-learning.
- `agent_v0/agent_v1/main.py` : nommage session + mode capture.
- `agent_v0/server_v1/stream_processor.py` : sortie de finalisation orientee competence courte.
- `agent_v0/server_v1/replay_learner.py` : persistance des corrections et resultats.
Priorite 2:
- `agent_v0/server_v1/replay_memory.py` : fiabilite et collisions sur boutons generiques.
- `agent_v0/server_v1/replay_verifier.py` : verifier des postconditions simples.
- `agent_v0/agent_v1/core/executor.py` : mode correction humaine et messages visibles.
## Risques prioritaires
1. Confondre trace de demonstration et competence generalisable.
2. Valider une action seulement parce que les pixels bougent.
3. Apprendre une coordonnee dependante DPI/ecran.
4. Produire un message conforme au contrat mais pauvre en contexte.
5. Recharger le VLM au mauvais moment et ajouter une latence cold start.
6. Melanger session de capture et replay actif.
## Regle de travail
Chaque micro-tache doit tenir en moins de deux minutes, avec une postcondition observable. Si on ne peut pas formuler la postcondition, on ne l'apprend pas encore.

View File

@@ -0,0 +1,609 @@
# Modèle Mandat / Protocoles / Scènes pour Léa
Version : 0.1 brainstorming
Date : 2026-05-25
Statut : modèle conceptuel, pas une spec technique
## Thèse
Léa n'est pas une boîte à clics et ne doit pas rejouer un workflow. Léa est une collaboratrice visuelle mandatée.
Elle reçoit une fin à atteindre, choisit un chemin connu ou apprend un chemin nouveau, observe le retour de ses actions, qualifie réussite/échec/doute, puis adapte son comportement.
Formule centrale :
```text
Un protocole est une grammaire d'action autour d'une intention.
```
Un logiciel, un OS ou un DPI peut changer les pixels, les titres, les boutons et les DPI. Le processus métier reste souvent stable : chercher, ouvrir, saisir, valider, enregistrer, corriger, transmettre, facturer, archiver. Léa doit apprendre ces grammaires d'action, pas mémoriser des coordonnées.
## Vocabulaire
### Mandat
Un mandat est une fin déléguée à Léa.
Exemples :
```text
Ecris ce texte et enregistre-le.
Trouve une vidéo de jazz et lance-la.
Ouvre le dossier patient de Mme X.
Saisis cette information dans le logiciel métier.
```
Le mandat n'est pas une suite de clics. Il donne le but, le contexte et éventuellement les limites.
### Intention active
L'intention active est le sous-but courant qui sert le mandat.
Exemple pour `Ecris ce texte et enregistre-le` :
```text
ouvrir un outil de saisie
écrire le texte
déclencher la sauvegarde
choisir ou confirmer le nom
vérifier que le fichier existe
```
### Protocole d'usage
Un protocole d'usage est un chemin connu, adaptable, pour accomplir une intention.
Exemples universels :
```text
ouvrir une application
chercher sur le web
saisir du texte
sauvegarder un fichier
confirmer une boîte de dialogue
fermer une fenêtre
copier-coller
```
Exemples métier :
```text
ouvrir une fiche patient
créer une ligne de facturation
valider une commande de stock
rapprocher une écriture comptable
```
Un protocole n'est pas un workflow figé. Il contient des scènes attendues, des affordances compatibles, des variantes autorisées et des conditions d'arrêt.
### Scène
Une scène est la situation visuelle pertinente pour l'intention courante.
La scène active pertinente n'est pas forcément la fenêtre au focus OS. C'est la zone ou la boîte qui sert le mandat maintenant.
Exemples :
```text
Bloc-notes prêt à recevoir du texte.
Fenêtre Enregistrer sous.
Dialogue "Voulez-vous enregistrer les modifications ?".
Page de résultats Google.
Page vidéo YouTube.
Fiche patient ouverte.
```
### Affordance
Une affordance est une action proposée par la scène.
Exemples :
```text
bouton Enregistrer
bouton Annuler
champ Nom du fichier
barre d'adresse
champ de recherche
bouton Lecture
onglet Patient
bouton Valider
```
Léa ne doit pas seulement reconnaître une affordance. Elle doit comprendre son rôle dans la scène et sa compatibilité avec l'intention.
### Geste
Un geste est l'action concrète décidée dans une scène.
Exemples :
```text
cliquer sur Enregistrer
taper le nom du fichier
appuyer sur Ctrl+S
sélectionner un résultat de recherche
cliquer sur Lecture
```
### Retour
Un retour est tout changement ou non-changement observé après un geste.
```text
Résultat attendu obtenu -> réussite.
Résultat contraire -> échec.
Rien ne change -> attente, latence ou échec à qualifier.
Nouvelle fenêtre -> événement à interpréter.
Erreur -> rupture ou branche prévue selon le protocole.
```
### Doute
Le doute est un signal utile, pas une faiblesse.
Léa doute quand :
```text
les sources visuelles divergent ;
la scène observée ne correspond pas à l'intention ;
aucun protocole connu ne s'applique ;
un retour attendu n'arrive pas ;
une action répétée ne produit aucun effet ;
la scène est sensible ou irréversible sans mandat explicite.
```
Le doute peut mener à une variante, une demande d'aide, ou une pause.
## Boucle cognitive minimale
```text
1. Recevoir le mandat.
2. Déduire l'intention active.
3. Choisir le protocole connu le plus simple.
4. Observer la scène active pertinente.
5. Identifier les affordances disponibles.
6. Choisir le geste compatible avec l'intention.
7. Agir.
8. Observer le retour.
9. Qualifier : réussite, échec, attente, rupture, doute.
10. Continuer, essayer une variante, demander de l'aide, ou apprendre.
```
Le point essentiel : une action n'est pas justifiée par le fait qu'un bouton existe. Elle est justifiée parce que ce bouton, dans cette scène, sert l'intention active.
## Contrat d'action
Avant d'agir, Léa doit pouvoir répondre implicitement à cinq questions :
```text
Quelle intention est-ce que je sers ?
Dans quelle scène suis-je ?
Quelle affordance est-ce que j'utilise ?
Pourquoi cette affordance est-elle compatible avec mon intention ?
Quel retour est-ce que j'attends ?
```
Si Léa ne peut pas produire cette justification, elle ne doit pas transformer l'action en clic opportuniste.
Ce contrat n'impose pas de demander à l'humain à chaque doute. Il impose que toute tentative ait une hypothèse vérifiable.
## Autonomie
L'autonomie de Léa est une autonomie d'initiative, pas une autonomie d'entêtement.
Léa peut :
```text
choisir le chemin le plus simple ;
changer de chemin si le premier échoue ;
essayer une variante cohérente ;
interpréter un retour ;
demander de l'aide ;
apprendre après aide ou résultat qualifié.
```
Léa ne doit pas :
```text
agir coûte que coûte ;
inventer une réussite ;
apprendre un échec comme une réussite ;
continuer un fallback après rejet sémantique ;
sortir d'un protocole sans raison explicable.
```
Le risque n'est pas interdit. Il doit être exploitable :
```text
observable
attribuable à une intention
réversible si possible
évaluable après coup
transformable en apprentissage
```
## Structure conceptuelle d'un protocole
Un protocole peut se décrire sur un coin de papier avec les éléments suivants :
```text
Nom
Intention servie
Préconditions plausibles
Scènes attendues
Affordances compatibles par scène
Gestes possibles
Variantes autorisées
Retours attendus
Branches normales
Ruptures connues
Conditions de réussite
Conditions d'abstention ou demande d'aide
Preuves apprenables
```
Ce n'est pas une séquence figée. C'est une grammaire : elle autorise plusieurs phrases correctes pour atteindre la même intention.
## Exemple 1 : ouvrir un logiciel
Mandat :
```text
Ouvre Bloc-notes.
```
Intention :
```text
rendre disponible un outil de saisie texte simple
```
Protocoles possibles :
```text
menu Démarrer / recherche Windows
raccourci existant
commande Exécuter
barre de recherche système
```
Scènes attendues :
```text
bureau ou environnement de départ
menu/recherche d'application
résultat "Bloc-notes"
fenêtre Bloc-notes ouverte
```
Affordances compatibles :
```text
champ de recherche
résultat d'application Bloc-notes
zone de texte vide
```
Retours attendus :
```text
une fenêtre de saisie texte apparaît
elle accepte le focus
elle permet de taper du texte
```
Variantes :
```text
si la recherche Windows échoue, essayer Exécuter/notepad
si un autre éditeur texte est disponible et accepté par le mandat, l'utiliser
si aucune scène d'édition texte n'apparaît, demander ou apprendre
```
## Exemple 2 : saisir un texte
Mandat :
```text
Saisis "testtesttest".
```
Intention :
```text
placer le contenu texte dans une zone éditable
```
Scènes attendues :
```text
éditeur texte ouvert
champ texte actif
curseur visible ou zone éditable détectée
```
Affordances compatibles :
```text
zone de saisie
document vide ou modifiable
```
Gestes :
```text
focus zone éditable si nécessaire
taper le texte
```
Retours attendus :
```text
le texte apparaît
le contenu correspond au mandat
```
Ruptures :
```text
zone non éditable
fenêtre inattendue
texte non apparu
application fermée
```
## Exemple 3 : enregistrer un fichier
Mandat :
```text
Enregistre le texte saisi.
```
Intention :
```text
persister le contenu courant dans un fichier
```
Protocoles possibles :
```text
Ctrl+S
menu Fichier > Enregistrer
bouton Enregistrer si visible
Enregistrer sous si le fichier n'a pas encore de nom
```
Scènes attendues :
```text
éditeur texte avec contenu non sauvegardé
dialogue Enregistrer sous
dialogue "voulez-vous enregistrer les modifications ?"
dialogue "le fichier existe déjà, voulez-vous le remplacer ?"
retour à l'éditeur avec état sauvegardé
```
Affordances compatibles :
```text
Enregistrer
Oui
Remplacer, si le mandat autorise l'écrasement
champ Nom du fichier
```
Affordances contraires :
```text
Annuler
Ne pas enregistrer
Non, si la question porte sur la sauvegarde souhaitée
```
Retours attendus :
```text
la boîte de sauvegarde se ferme
le fichier existe à l'emplacement choisi
le contenu est présent
le document n'est plus en état non sauvegardé
```
Règle clé :
```text
Voir un bouton "Enregistrer" ne suffit pas.
Il faut que ce bouton soit dans une scène compatible avec l'intention de sauvegarde.
```
## Exemple 4 : regarder une vidéo de jazz
Mandat :
```text
Trouve et lance une vidéo de jazz.
```
Intention :
```text
obtenir une vidéo en lecture correspondant au thème jazz
```
Protocoles possibles :
```text
ouvrir navigateur -> YouTube -> chercher jazz -> lancer une vidéo
ouvrir navigateur -> moteur de recherche -> "video jazz" -> ouvrir un résultat vidéo
utiliser une application ou un favori connu si disponible
```
Scènes attendues :
```text
navigateur ouvert
barre d'adresse ou champ de recherche
page de résultats
page vidéo
lecteur avec bouton Lecture ou vidéo déjà en lecture
```
Affordances compatibles :
```text
barre d'adresse
champ de recherche
résultat vidéo pertinent
bouton Lecture
```
Retours attendus :
```text
une vidéo démarre
le contenu semble lié au jazz
le son ou la lecture est active si observable
```
Variantes :
```text
si YouTube est inaccessible, utiliser le moteur de recherche
si le premier résultat n'est pas pertinent, revenir et choisir un autre résultat
si un consentement cookie bloque la scène, traiter le dialogue seulement s'il est compatible avec la navigation
```
## Généralisation multi-environnements
Léa doit apprendre à plusieurs niveaux.
### Niveau 1 : mémoire locale
```text
Dans cet écran précis, ce bouton sauvegarde.
Dans ce DPI, cette scène ressemble à Save As.
Dans ce logiciel, ce champ est le champ de recherche patient.
```
### Niveau 2 : protocole applicatif
```text
Dans ce logiciel DPI, ouvrir un dossier patient passe par recherche -> liste -> fiche.
Dans ce logiciel comptable, valider une écriture passe par saisie -> contrôle -> validation.
```
### Niveau 3 : protocole métier
```text
Tous les DPI ont une façon de chercher un patient, ouvrir sa fiche, saisir une information, valider et tracer.
Tous les logiciels de stock ont une façon de rechercher un article, ajuster une quantité, valider un mouvement.
Tous les logiciels comptables ont une façon de saisir, contrôler, rapprocher, valider.
```
### Niveau 4 : protocole universel
```text
chercher
ouvrir
saisir
valider
annuler
enregistrer
confirmer
revenir
fermer
```
La généralisation consiste à relier les preuves locales à ces niveaux supérieurs.
## Apprentissage
Léa apprend seulement à partir d'un résultat qualifié.
Apprentissage valide :
```text
L'action a produit le retour attendu.
L'humain a confirmé ou corrigé.
La scène et l'intention sont connues.
Le geste est attribuable au résultat.
```
Apprentissage interdit :
```text
clic opportuniste sans justification
effet non vérifié
échec enregistré comme succès
correction humaine confondue avec autonomie
retour ambigu non qualifié
```
La correction humaine ne doit pas seulement enregistrer "où cliquer". Elle doit enrichir :
```text
quelle scène était visible
quelle intention était active
quelle affordance était correcte
quel geste a été fait
quel retour a prouvé la réussite
```
## Ce que ce modèle aurait changé dans nos tests
Dans les tests humains réalisés ces derniers jours, beaucoup d'échecs venaient d'une confusion entre :
```text
résoudre une cible visuelle
et accomplir une intention
```
Avec ce modèle :
```text
ouvrir un logiciel
trouver une zone de saisie
taper
déclencher une sauvegarde
interpréter Enregistrer sous
confirmer Enregistrer
traiter un remplacement ou une demande de sauvegarde
vérifier le résultat
```
ne sont plus des actions isolées. Ce sont des scènes normales dans un protocole connu.
Si une fenêtre inattendue apparaît, Léa ne demande pas "où cliquer ?". Elle se demande :
```text
Cette scène est-elle une continuation normale de mon mandat ?
Quelles affordances propose-t-elle ?
Laquelle sert l'intention ?
Quel retour dois-je attendre ?
```
## Questions ouvertes
1. Quel vocabulaire final garder : protocole d'usage, geste type, routine intentionnelle ?
2. Comment exprimer à l'utilisateur le mandat courant sans jargon ?
3. Quelles familles de protocoles universels faut-il inscrire en premier ?
4. Comment distinguer visuellement une scène pertinente d'une fenêtre simplement au focus ?
5. Comment demander de l'aide sans transformer l'humain en téléopérateur ?
6. Comment capturer l'apprentissage métier sans mémoriser des informations sensibles ?
## Synthèse courte
```text
Léa reçoit un mandat.
Elle choisit un protocole.
Elle observe une scène.
Elle interprète les affordances.
Elle agit avec une hypothèse.
Elle qualifie le retour.
Elle apprend uniquement d'un résultat qualifié.
```
Cette structure permet de viser la généralisation : mêmes intentions, scènes différentes, logiciels différents, DPI différents, OS différents.

View File

@@ -0,0 +1,953 @@
# Modèle Mandat / Protocoles / Scènes pour Léa
Version : 0.2 brainstorming
Date : 2026-05-25
Statut : modèle conceptuel, pas une spec technique
## Thèse
Léa n'est pas une boîte à clics et ne doit pas rejouer un workflow. Léa est une collaboratrice visuelle mandatée.
Elle reçoit une fin à atteindre, choisit un protocole connu ou apprend un chemin nouveau, observe le retour de ses actions, qualifie réussite/échec/doute, puis adapte son comportement.
Formule centrale :
```text
Un protocole est une grammaire d'action autour d'une intention.
```
La différence essentielle :
```text
Workflow = séquence fermée de gestes prévus.
Protocole = grammaire ouverte qui autorise des phrases nouvelles si elles servent l'intention et sont vérifiables.
```
Un logiciel, un OS ou un DPI peut changer les pixels, les titres, les boutons et les DPI. Le processus métier reste souvent stable : chercher, ouvrir, saisir, valider, enregistrer, corriger, transmettre, facturer, archiver. Léa doit apprendre ces grammaires d'action, pas mémoriser des coordonnées.
## Vocabulaire
### Mandat
Un mandat est une fin déléguée à Léa.
Exemples :
```text
Ecris ce texte et enregistre-le.
Trouve une vidéo de jazz et lance-la.
Ouvre le dossier patient de Mme X.
Saisis cette information dans le logiciel métier.
```
Le mandat n'est pas une suite de clics. Il donne le but, le contexte et éventuellement les limites.
Un mandat peut être :
```text
ponctuel : fais cette action précise maintenant ;
de session : traite cette série de dossiers ;
permanent : applique ce protocole dans ce périmètre habituel ;
amendé : l'humain ajoute ou précise une intention en cours d'exécution.
```
### Intention active
L'intention active est le sous-but courant qui sert le mandat.
Exemple pour `Ecris ce texte et enregistre-le` :
```text
ouvrir un outil de saisie
écrire le texte
déclencher la sauvegarde
choisir ou confirmer le nom
vérifier que le fichier existe
```
### Protocole d'usage
Un protocole d'usage est un chemin connu, adaptable, pour accomplir une intention.
Exemples universels :
```text
ouvrir une application
chercher sur le web
saisir du texte
sauvegarder un fichier
confirmer une boîte de dialogue
fermer une fenêtre
copier-coller
```
Exemples métier :
```text
ouvrir une fiche patient
créer une ligne de facturation
valider une commande de stock
rapprocher une écriture comptable
```
Un protocole contient des scènes attendues, des affordances compatibles, des variantes autorisées, des conditions d'arrêt et un opérateur d'extension.
### Scène d'intention
Une scène d'intention est la situation visuelle pertinente pour l'intention courante.
La scène d'intention n'est pas forcément la fenêtre au focus OS. C'est la zone, fenêtre, boîte ou sous-zone qui sert le mandat maintenant.
Exemples :
```text
Bloc-notes prêt à recevoir du texte.
Fenêtre Enregistrer sous.
Dialogue "Voulez-vous enregistrer les modifications ?".
Page de résultats Google.
Page vidéo YouTube.
Fiche patient ouverte.
Onglet "Facturation" dans une fiche patient.
Table de mouvements dans un logiciel de stock.
```
### Scène composée
Une scène peut contenir des sous-scènes.
Exemple DPI :
```text
Fiche patient
-> bandeau identité
-> onglets séjour / facturation / documents
-> liste des passages
-> panneau de détail
-> popup de validation
```
Chaque sous-scène a ses affordances propres. Léa doit pouvoir focaliser sur la sous-scène qui sert l'intention active, au lieu de traiter toute la fenêtre comme un bloc plat.
### Affordance
Une affordance est une action proposée par la scène.
Exemples :
```text
bouton Enregistrer
bouton Annuler
champ Nom du fichier
barre d'adresse
champ de recherche
bouton Lecture
onglet Patient
bouton Valider
```
Léa ne doit pas seulement reconnaître une affordance. Elle doit comprendre son rôle dans la scène et sa compatibilité avec l'intention.
### Affordance anti-intention
Une affordance anti-intention est visible et actionnable, mais elle contredit l'intention active.
Exemples :
```text
Annuler pendant une sauvegarde voulue.
Ne pas enregistrer quand l'intention est de sauvegarder.
Supprimer quand l'intention est de consulter.
Fermer une fiche non sauvegardée quand l'intention est de valider.
```
Identifier les affordances anti-intention est aussi important qu'identifier l'affordance correcte.
### Geste
Un geste est l'action concrète décidée dans une scène.
Exemples :
```text
cliquer sur Enregistrer
taper le nom du fichier
appuyer sur Ctrl+S
sélectionner un résultat de recherche
cliquer sur Lecture
```
### Retour
Un retour est tout changement ou non-changement observé après un geste.
```text
Résultat attendu obtenu -> réussite.
Résultat contraire -> échec.
Rien ne change -> attente, latence ou échec à qualifier.
Nouvelle fenêtre -> événement à interpréter.
Erreur -> rupture ou branche prévue selon le protocole.
```
### Rupture
Une rupture est un changement de scène imprévu ou hors mandat.
Un échec se produit dans la scène attendue. Une rupture déplace Léa hors de la scène d'intention.
Exemples :
```text
UAC ou alerte sécurité pendant une sauvegarde.
Popup de mise à jour navigateur pendant une saisie DPI.
Léa Assistante prend le focus alors que l'intention porte sur Bloc-notes.
```
## Doute typé
Le doute est un signal utile. Il doit être typé, car chaque doute appelle une réponse différente.
### Doute de localisation
```text
Je sais quoi chercher, mais je ne sais pas où c'est.
```
Réponse possible :
```text
élargir la zone, changer de méthode visuelle, utiliser une ancre, explorer la sous-scène.
```
### Doute d'identification
```text
Je vois quelque chose, mais je ne sais pas si c'est le bon élément.
```
Réponse possible :
```text
vérifier le rôle dans la scène, comparer avec affordances voisines, demander si risque élevé.
```
### Doute de scène
```text
Je ne suis pas sûr d'être dans la bonne scène d'intention.
```
Réponse possible :
```text
requalifier la scène, revenir à une scène connue, demander.
```
### Doute d'effet
```text
Je sais quoi faire, mais je ne sais pas si ce geste produira l'effet attendu.
```
Réponse possible :
```text
agir si réversible et vérifiable, demander si irréversible ou sensible.
```
### Doute d'intention
```text
Je ne suis plus sûr du sous-but que je dois servir.
```
Réponse possible :
```text
arrêter la chaîne, demander à l'humain, ne pas improviser.
```
## Boucle cognitive minimale
```text
1. Recevoir le mandat.
2. Déduire l'intention active.
3. Choisir le protocole connu le plus simple.
4. Observer la scène d'intention.
5. Identifier les affordances et affordances anti-intention.
6. Choisir le geste compatible avec l'intention.
7. Formuler une hypothèse d'effet attendu.
8. Agir.
9. Observer le retour dans une fenêtre temporelle attendue.
10. Qualifier : réussite, échec, attente, rupture, doute.
11. Continuer, essayer une variante, demander de l'aide, oublier ou apprendre.
```
Le point essentiel : une action n'est pas justifiée par le fait qu'un bouton existe. Elle est justifiée parce que ce bouton, dans cette scène, sert l'intention active.
## Contrat d'action
Avant d'agir, Léa doit pouvoir répondre implicitement à cinq questions :
```text
Quelle intention est-ce que je sers ?
Dans quelle scène suis-je ?
Quelle affordance est-ce que j'utilise ?
Pourquoi cette affordance est-elle compatible avec mon intention ?
Quel retour est-ce que j'attends ?
```
Si Léa ne peut pas produire cette justification, elle ne doit pas transformer l'action en clic opportuniste.
Ce contrat n'impose pas de demander à l'humain à chaque doute. Il impose que toute tentative ait une hypothèse vérifiable.
## Trace causale
Chaque geste devrait laisser une trace causale active :
```text
J'ai fait G
dans la scène S
pour servir l'intention I
avec l'hypothèse H
et j'attendais le retour R
```
Cette trace sert à :
```text
qualifier le retour ;
expliquer l'action à l'humain ;
apprendre une preuve correcte ;
éviter d'apprendre une corrélation fausse ;
désapprendre une mauvaise leçon plus tard.
```
Sans trace causale, Léa apprend des coïncidences. Avec trace causale, elle apprend des gestes situés.
## Temporalité attendue
Un protocole doit porter une attente temporelle approximative.
Exemples :
```text
un clic bouton local doit produire un changement en 100 ms à 2 s ;
une ouverture d'application peut prendre plusieurs secondes ;
une recherche web peut prendre plus longtemps selon le réseau ;
une sauvegarde locale doit généralement fermer la boîte rapidement ;
un DPI legacy peut avoir des latences longues et variables.
```
`Rien ne change` n'est pas automatiquement un échec. C'est un retour à qualifier selon :
```text
le protocole ;
la scène ;
le délai habituel ;
les indicateurs de chargement ;
les répétitions ;
le risque du geste suivant.
```
## Autonomie
L'autonomie de Léa est une autonomie d'initiative, pas une autonomie d'entêtement.
Léa peut :
```text
choisir le chemin le plus simple ;
changer de chemin si le premier échoue ;
essayer une variante cohérente ;
interpréter un retour ;
demander de l'aide ;
apprendre après aide ou résultat qualifié.
```
Léa ne doit pas :
```text
agir coûte que coûte ;
inventer une réussite ;
apprendre un échec comme une réussite ;
continuer un fallback après rejet sémantique ;
sortir d'un protocole sans raison explicable.
```
Le risque n'est pas interdit. Il doit être exploitable :
```text
observable
attribuable à une intention
réversible si possible
évaluable après coup
transformable en apprentissage
```
## Extension de grammaire
Cette section distingue un protocole d'un workflow.
Si Léa trouve une affordance compatible avec l'intention mais absente du protocole connu, elle peut former une hypothèse :
```text
Cette affordance A, dans la scène S, semble servir l'intention I.
Si j'effectue le geste G, j'attends le retour R.
```
Elle peut essayer si :
```text
le geste est réversible ou faiblement risqué ;
le retour attendu est observable ;
la scène est compatible avec le mandat ;
le doute n'est pas un doute d'intention ;
le mandat n'exige pas confirmation explicite.
```
Après essai :
```text
si R arrive -> affordance candidate à apprentissage ;
si R n'arrive pas -> affordance marquée incompatible ou incertaine ;
si effet négatif -> correction / rollback / demande humaine ;
si doute persiste -> ne pas apprendre comme réussite.
```
Cette capacité d'extension empêche le protocole de devenir un workflow figé.
## Structure conceptuelle d'un protocole
Un protocole peut se décrire sur un coin de papier avec les éléments suivants :
```text
Nom
Intention servie
Préconditions plausibles
Scènes attendues
Sous-scènes possibles
Affordances compatibles par scène
Affordances anti-intention
Gestes possibles
Variantes autorisées
Retours attendus
Temporalité attendue
Branches normales
Ruptures connues
Doutes possibles et réponses associées
Conditions de réussite
Conditions d'abstention ou demande d'aide
Opérateur d'extension de grammaire
Preuves apprenables
Conditions de désapprentissage
```
## Preuve apprenable
Une preuve apprenable est un retour qui peut être conservé au-delà de la session parce qu'il est fiable et généralisable.
Une preuve apprenable doit être :
```text
attribuable causalement à un geste ;
observée dans une scène typée ;
liée à une intention active ;
qualifiée comme réussite, échec ou incompatibilité ;
reproductible ou vérifiable dans une scène similaire ;
non contredite par une correction humaine ou un rollback.
```
Exemples :
```text
Dans cette boîte "Enregistrer sous", le bouton "Enregistrer" valide la sauvegarde.
Dans ce DPI, le champ "IPP" sert à chercher un patient.
Dans cette page YouTube, cliquer sur cette vignette ouvre une vidéo.
Dans cette popup, "Ne pas enregistrer" contredit l'intention de sauvegarde.
```
Une observation n'est pas forcément une preuve apprenable. Un effet peut être accidentel, ambigu ou causé par l'humain.
## Correction, démonstration, apprentissage
L'aide humaine peut avoir plusieurs statuts.
### Correction
L'humain corrige une erreur dans une situation précise.
```text
Tu t'es trompée de bouton. Ici il fallait cliquer sur Enregistrer.
```
Effet :
```text
réparer ou invalider une preuve locale ;
ne pas créer automatiquement un protocole générique.
```
### Démonstration
L'humain enseigne une nouvelle compétence ou un nouveau chemin.
```text
Regarde, dans ce DPI, pour ouvrir un patient, je fais recherche -> IPP -> entrée -> fiche.
```
Effet :
```text
créer ou enrichir un protocole applicatif ;
capturer scènes, affordances, gestes, retours.
```
### Validation
L'humain confirme qu'une hypothèse de Léa est correcte.
```text
Oui, ce bouton Valider correspond bien à sauvegarder la fiche.
```
Effet :
```text
promouvoir une affordance candidate en preuve apprenable.
```
## Désapprentissage
Léa doit pouvoir oublier ou dégrader une leçon.
Une preuve doit être invalidée si :
```text
elle provoque un échec répété ;
elle est contredite par l'humain ;
elle ne marche que dans une scène plus étroite que prévu ;
elle a été apprise après un retour ambigu ;
elle a été associée à la mauvaise intention ;
elle dépendait d'une correction humaine non identifiée.
```
Désapprendre ne veut pas forcément supprimer. Cela peut vouloir dire :
```text
réduire la confiance ;
restreindre le périmètre ;
marquer comme spécifique à une scène ;
mettre en quarantaine jusqu'à validation ;
supprimer si dangereux.
```
## Mode exploration
Dans un environnement inconnu, Léa peut observer sans agir pour cartographier les scènes et affordances.
Objectif :
```text
identifier les zones principales ;
nommer les sous-scènes ;
repérer les affordances ;
relier les affordances à des intentions possibles ;
détecter les actions sensibles ;
préparer une démonstration ou un essai vérifiable.
```
Le mode exploration évite deux excès :
```text
agir sans cartographie ;
se bloquer parce que rien n'est connu.
```
## Adaptateurs métier
La généralisation métier ne supprime pas les spécificités éditeur.
Un DPI, un ERP, une comptabilité ou une gestion de stock exposent des processus semblables, mais chaque éditeur a ses scènes, libellés, latences, contraintes et pièges.
Le bon modèle est :
```text
protocole métier générique
-> adaptateur applicatif par éditeur/client
-> preuves locales par écran/version/DPI
```
Exemple :
```text
Chercher un patient
-> protocole métier DPI
-> adaptateur Easily / Maincare / Cerner / autre
-> preuves locales du site client
```
## Carnet de bord narratif
Léa devrait conserver un fil narratif par mandat.
Ce fil n'est pas un log technique. C'est l'explication utilisateur :
```text
J'ai ouvert Bloc-notes.
J'ai saisi le texte demandé.
J'ai déclenché la sauvegarde avec Ctrl+S.
J'ai vu "Enregistrer sous".
J'ai confirmé le nom du fichier.
La sauvegarde a réussi car le fichier existe et le contenu est présent.
```
En cas de blocage :
```text
Je voulais enregistrer, mais une fenêtre inconnue est apparue.
Je ne sais pas si elle appartient à la sauvegarde.
J'ai besoin d'aide pour qualifier cette scène.
```
Ce carnet sert à la confiance, au debug, à la formation et à l'amélioration continue.
## Exemple 1 : ouvrir un logiciel
Mandat :
```text
Ouvre Bloc-notes.
```
Intention :
```text
rendre disponible un outil de saisie texte simple
```
Protocoles possibles :
```text
menu Démarrer / recherche Windows
raccourci existant
commande Exécuter
barre de recherche système
```
Scènes attendues :
```text
bureau ou environnement de départ
menu/recherche d'application
résultat "Bloc-notes"
fenêtre Bloc-notes ouverte
```
Affordances compatibles :
```text
champ de recherche
résultat d'application Bloc-notes
zone de texte vide
```
Retours attendus :
```text
une fenêtre de saisie texte apparaît
elle accepte le focus
elle permet de taper du texte
```
Variantes :
```text
si la recherche Windows échoue, essayer Exécuter/notepad
si un autre éditeur texte est disponible et accepté par le mandat, l'utiliser
si aucune scène d'édition texte n'apparaît, demander ou apprendre
```
## Exemple 2 : saisir un texte
Mandat :
```text
Saisis "testtesttest".
```
Intention :
```text
placer le contenu texte dans une zone éditable
```
Scènes attendues :
```text
éditeur texte ouvert
champ texte actif
curseur visible ou zone éditable détectée
```
Affordances compatibles :
```text
zone de saisie
document vide ou modifiable
```
Gestes :
```text
focus zone éditable si nécessaire
taper le texte
```
Retours attendus :
```text
le texte apparaît
le contenu correspond au mandat
```
Ruptures :
```text
zone non éditable
fenêtre inattendue
texte non apparu
application fermée
```
## Exemple 3 : enregistrer un fichier
Mandat :
```text
Enregistre le texte saisi.
```
Intention :
```text
persister le contenu courant dans un fichier
```
Protocoles possibles :
```text
Ctrl+S
menu Fichier > Enregistrer
bouton Enregistrer si visible
Enregistrer sous si le fichier n'a pas encore de nom
```
Scènes attendues :
```text
éditeur texte avec contenu non sauvegardé
dialogue Enregistrer sous
dialogue "voulez-vous enregistrer les modifications ?"
dialogue "le fichier existe déjà, voulez-vous le remplacer ?"
retour à l'éditeur avec état sauvegardé
```
Affordances compatibles :
```text
Enregistrer
Oui
Remplacer, si le mandat autorise l'écrasement
champ Nom du fichier
```
Affordances anti-intention :
```text
Annuler
Ne pas enregistrer
Non, si la question porte sur la sauvegarde souhaitée
```
Retours attendus :
```text
la boîte de sauvegarde se ferme
le fichier existe à l'emplacement choisi
le contenu est présent
le document n'est plus en état non sauvegardé
```
Règle clé :
```text
Voir un bouton "Enregistrer" ne suffit pas.
Il faut que ce bouton soit dans une scène compatible avec l'intention de sauvegarde.
```
## Exemple 4 : regarder une vidéo de jazz
Mandat :
```text
Trouve et lance une vidéo de jazz.
```
Intention :
```text
obtenir une vidéo en lecture correspondant au thème jazz
```
Protocoles possibles :
```text
ouvrir navigateur -> YouTube -> chercher jazz -> lancer une vidéo
ouvrir navigateur -> moteur de recherche -> "video jazz" -> ouvrir un résultat vidéo
utiliser une application ou un favori connu si disponible
```
Scènes attendues :
```text
navigateur ouvert
barre d'adresse ou champ de recherche
page de résultats
page vidéo
lecteur avec bouton Lecture ou vidéo déjà en lecture
```
Affordances compatibles :
```text
barre d'adresse
champ de recherche
résultat vidéo pertinent
bouton Lecture
```
Retours attendus :
```text
une vidéo démarre
le contenu semble lié au jazz
le son ou la lecture est active si observable
```
Variantes :
```text
si YouTube est inaccessible, utiliser le moteur de recherche
si le premier résultat n'est pas pertinent, revenir et choisir un autre résultat
si un consentement cookie bloque la scène, traiter le dialogue seulement s'il est compatible avec la navigation
```
## Exemple 5 : facturer un passage aux urgences
Mandat :
```text
Facture le passage de Mme MOREL du 12/05 aux urgences.
```
Intentions enchaînées :
```text
ouvrir le logiciel métier
chercher la patiente
ouvrir la bonne fiche
identifier le passage du 12/05
ouvrir la scène de facturation
contrôler les informations nécessaires
valider la facturation
vérifier que le statut est facturé ou prêt à transmettre
```
Scènes composées :
```text
écran d'accueil DPI
recherche patient
liste de résultats
fiche patient
liste des passages
onglet facturation
dialogue de validation
```
Exemples de doutes :
```text
doute d'identification patient si plusieurs homonymes ;
doute de scène si l'onglet ouvert n'est pas la facturation ;
doute d'effet si le bouton Valider ne précise pas ce qu'il valide ;
doute d'intention si le mandat ne précise pas quoi faire en cas de données manquantes.
```
## Généralisation multi-environnements
Léa doit apprendre à plusieurs niveaux.
### Niveau 1 : mémoire locale
```text
Dans cet écran précis, ce bouton sauvegarde.
Dans ce DPI, cette scène ressemble à Save As.
Dans ce logiciel, ce champ est le champ de recherche patient.
```
### Niveau 2 : protocole applicatif
```text
Dans ce logiciel DPI, ouvrir un dossier patient passe par recherche -> liste -> fiche.
Dans ce logiciel comptable, valider une écriture passe par saisie -> contrôle -> validation.
```
### Niveau 3 : protocole métier
```text
Tous les DPI ont une façon de chercher un patient, ouvrir sa fiche, saisir une information, valider et tracer.
Tous les logiciels de stock ont une façon de rechercher un article, ajuster une quantité, valider un mouvement.
Tous les logiciels comptables ont une façon de saisir, contrôler, rapprocher, valider.
```
### Niveau 4 : protocole universel
```text
chercher
ouvrir
saisir
valider
annuler
enregistrer
confirmer
revenir
fermer
```
La généralisation consiste à relier les preuves locales à ces niveaux supérieurs.
## Questions ouvertes restantes
1. Quel vocabulaire final garder : protocole d'usage, geste type, routine intentionnelle ?
2. Comment exprimer à l'utilisateur le mandat courant sans jargon ?
3. Quelles familles de protocoles universels faut-il inscrire en premier ?
4. Comment demander de l'aide sans transformer l'humain en téléopérateur ?
5. Comment capturer l'apprentissage métier sans mémoriser des informations sensibles ?
6. Comment arbitrer une affordance nouvelle compatible mais située dans une action sensible ?
7. Comment représenter un mandat amendé sans perdre la trace causale initiale ?
## Synthèse courte
```text
Léa reçoit un mandat.
Elle choisit un protocole.
Elle observe une scène d'intention.
Elle interprète les affordances et les anti-affordances.
Elle agit avec une hypothèse.
Elle qualifie le retour dans le temps attendu.
Elle apprend uniquement d'une preuve qualifiée.
Elle peut étendre sa grammaire, corriger ou désapprendre.
```
Cette structure permet de viser la généralisation : mêmes intentions, scènes différentes, logiciels différents, DPI différents, OS différents.

View File

@@ -0,0 +1,146 @@
# Modèle Mandat / Protocoles / Scènes — v0.3 arbitrages Dom
Date : 2026-05-25
Statut : cadrage conceptuel avec arbitrages utilisateur
Base : `MODELE_MANDAT_PROTOCOLS_LEA_2026-05-25_v0.2.md`
## Arbitrage 1 — priorité entre protocoles
Quand plusieurs protocoles peuvent servir la même intention, Léa choisit le chemin le plus simple selon une priorité pondérée :
```text
mieux connu
-> moins risqué
-> plus court
```
Interprétation :
```text
mieux connu = le protocole possède les preuves qualifiées les plus solides ;
moins risqué = le geste est réversible, observable, et dans le niveau de délégation accordé ;
plus court = moins de gestes ou moins de latence attendue.
```
Exemple :
```text
Pour sauvegarder :
Ctrl+S est prioritaire si l'éditeur est connu et l'état attendu vérifié.
Menu Fichier > Enregistrer reste une variante si Ctrl+S échoue ou n'est pas disponible.
```
## Arbitrage 2 — préconditions plausibles
Dom reformule simplement :
```text
Léa doit vérifier l'état qu'elle attend.
```
Une précondition plausible est donc un état attendu vérifiable avant de tenter un protocole ou un geste.
Exemples :
```text
Pour saisir du texte :
je dois voir ou déduire une zone éditable focusable.
Pour enregistrer :
je dois être dans une application qui contient un contenu à sauvegarder,
ou dans une boîte de dialogue qui appartient à la sauvegarde.
Pour cliquer "Enregistrer" :
je dois être dans une scène d'intention compatible avec la sauvegarde,
pas simplement voir le mot "Enregistrer" n'importe où.
Pour ouvrir une fiche patient :
je dois être dans une scène de recherche/liste/fiche compatible avec le DPI.
```
La précondition évite l'essai voué à l'échec. Elle ne rend pas Léa prudente par défaut ; elle lui donne un état de référence.
## Arbitrage 3 — niveau de risque et autonomie tutorée
Dom précise que le niveau de risque ne doit pas être uniquement une catégorie abstraite codée à l'avance. Il dépend aussi du niveau réel de Léa évalué par son collaborateur/tuteur.
Principe :
```text
Léa devient autonome sur un périmètre quand le collaborateur estime qu'elle est au bon niveau.
```
Cela rapproche Léa d'un humain en apprentissage :
```text
au début, le tuteur laisse faire certaines actions simples ;
puis il élargit le périmètre quand les preuves s'accumulent ;
sur un protocole maîtrisé, le risque pratique devient très faible ;
sur un nouveau logiciel métier, l'apprentissage recommence sur ce périmètre.
```
Le risque doit donc être modélisé comme :
```text
risque intrinsèque du geste
+ sensibilité métier
+ maturité de Léa sur ce protocole
+ maturité de Léa dans cet environnement
+ mandat explicitement donné par l'humain
```
Exemples :
```text
Cliquer dans une zone de texte : risque intrinsèque mineur.
Valider une facture : risque métier majeur si Léa n'est pas tutorée sur ce protocole.
Valider une facture dans un protocole déjà démontré, dans un mandat de session explicite : risque pratique réduit.
Supprimer ou transmettre à l'extérieur : reste sensible, même avec maturité élevée.
```
## Niveau de délégation
On remplace donc une vision statique `low/medium/high` par une notion de délégation tutorée :
```text
Niveau 0 — observation : Léa regarde, cartographie, ne fait pas.
Niveau 1 — proposition : Léa propose le geste, l'humain valide.
Niveau 2 — exécution supervisée : Léa agit sur gestes réversibles, demande sur doute.
Niveau 3 — autonomie de session : Léa agit dans un mandat explicite borné.
Niveau 4 — autonomie habituelle : Léa agit seule sur protocole connu et environnement maîtrisé.
```
Ce niveau peut être différent selon :
```text
le protocole ;
le logiciel ;
le client ;
le type de donnée ;
le collaborateur/tuteur ;
la période d'apprentissage.
```
## Effet sur le modèle v0.2
La boucle devient :
```text
Mandat
-> intention active
-> protocoles candidats
-> choix : mieux connu / moins risqué / plus court
-> vérification des préconditions attendues
-> observation scène d'intention
-> affordance compatible
-> geste avec hypothèse de retour
-> qualification du retour
-> apprentissage ou désapprentissage
-> ajustement du niveau de délégation
```
## Point clé
L'autonomie n'est pas l'absence de risque. L'autonomie est la capacité de prendre des initiatives proportionnées à un mandat, un niveau de preuve et un niveau de délégation.
Le risque utile est celui qui produit de l'apprentissage qualifié.

View File

@@ -32,9 +32,9 @@ headers = {
}
resp = requests.post(
SERVER_URL,
files=files,
data=data,
SERVER_URL,
files=files,
data=data,
headers=headers,
timeout=timeout
)
@@ -75,8 +75,8 @@ if os.getenv("RPA_TOKEN_READONLY"):
Added environment variables directly to service:
```ini
Environment="RPA_TOKEN_ADMIN=73cf0db73f9a5064e79afebba96c85338be65cc2060b9c1d42c3ea5dd7d4e490"
Environment="RPA_TOKEN_READONLY=7eea1de415cc69c02381ce09ff63aeebf3e1d9b476d54aa6730ba9de849e3dc6"
Environment="RPA_TOKEN_ADMIN=REDACTED"
Environment="RPA_TOKEN_READONLY=REDACTED"
```
## Current Status
@@ -105,7 +105,7 @@ Expected result: HTTP 200 or 400 (not 401 Unauthorized)
## Production Tokens
- **Admin Token**: `73cf0db73f9a5064e79afebba96c85338be65cc2060b9c1d42c3ea5dd7d4e490`
- **Read-Only Token**: `7eea1de415cc69c02381ce09ff63aeebf3e1d9b476d54aa6730ba9de849e3dc6`
- **Admin Token**: `REDACTED`
- **Read-Only Token**: `REDACTED`
These tokens are configured in `/etc/rpa_vision_v3/rpa_vision_v3.env` and the systemd service.

View File

@@ -15,7 +15,7 @@ The issue was an **encryption key mismatch** between the agent and server:
## Key Issues Identified
### 1. Agent Configuration
- Agent config had `encryption_password: None`
- Agent config had `encryption_password: None`
- When `None`, it defaulted to session-based password instead of environment password
- Each session used a different encryption key
@@ -55,7 +55,7 @@ def load_env_file(env_path):
"""Charge un fichier .env dans les variables d'environnement"""
if not env_path.exists():
return False
with open(env_path, 'r') as f:
for line in f:
line = line.strip()
@@ -121,7 +121,7 @@ Server status: "encryption_enabled": true
Both agent and server now use the shared password from `.env.local`:
```bash
ENCRYPTION_PASSWORD=2c8129fa522ae8b6bbea1dbf1cadbddd46d760121a49c1ded076dfd6da756805
ENCRYPTION_PASSWORD=REDACTED
```
## Testing

View File

@@ -11,7 +11,7 @@ The Agent V0 authentication issue has been **partially fixed**. Here's the curre
- Dashboard: `http://localhost:5001`
- Worker, healthcheck, and retention services ✅
2. **Agent V0 Configuration**:
2. **Agent V0 Configuration**:
- Agent uploader now includes authentication headers ✅
- Run script exports environment variables to agent ✅
- Agent can access production tokens ✅
@@ -75,8 +75,8 @@ The agent can now be tested with:
## 🔑 Production Tokens
- **Admin**: `73cf0db73f9a5064e79afebba96c85338be65cc2060b9c1d42c3ea5dd7d4e490`
- **Read-Only**: `7eea1de415cc69c02381ce09ff63aeebf3e1d9b476d54aa6730ba9de849e3dc6`
- **Admin**: `REDACTED`
- **Read-Only**: `REDACTED`
## 📊 Progress Summary

View File

@@ -0,0 +1,164 @@
# Guide de test humain — Compétences batch 1
**Date** : 2026-06-01
**Auteur** : Qwen
**Objectif** : tester en direct que Léa peut rejouer une compétence apprise, et que l'humain peut valider le résultat.
---
## Prérequis généraux
| Élément | Vérification |
|---------|-------------|
| Dashboard web actif | `http://<ip>:3000` → page Knowledge Base visible |
| Streaming server actif | `http://<ip>:5005` (le dashboard s'y connecte) |
| Agent Windows connecté | La machine `DESKTOP-58D5CAC_windows` apparaît dans le dashboard |
| Compétences visibles | 3 compétences `candidate` dans la Knowledge Base |
---
## Test 1 — `key_win_r_wait_explorer_exe` (Win+R → Exécuter)
### Ce que Léa va faire
Appuyer sur `Win+R` et attendre que la fenêtre **Exécuter** apparaisse.
### État initial requis
- ✅ Bureau Windows visible
-**Aucun** dialogue Exécuter ouvert (ferme-le s'il est ouvert)
-`explorer.exe` tourne (toujours le cas sur Windows)
### Procédure
1. Ouvre le dashboard → onglet **Knowledge Base**
2. Trouve la ligne `key_win_r_wait_explorer_exe`
3. Clique **Tester**
4. Une modale s'ouvre, le replay se lance
5. **Pause before** : la modale dit "Prépare le test supervisé..." → clique **Continuer le test**
6. Léa envoie `Win+R` automatiquement
7. La fenêtre Exécuter doit apparaître en ~1-2 secondes
8. **Pause after** : la modale dit "Valide le résultat..." → 3 boutons apparaissent
### Ce que tu dois voir
- La fenêtre Exécuter s'ouvre et passe au premier plan
- La modale affiche "Valide le résultat de la compétence..."
### Comment juger
| Verdict | Quand |
|---------|-------|
| **Valide** ✅ | La fenêtre Exécuter apparaît et est au premier plan |
| **Invalide** ❌ | Rien ne se passe, ou une autre fenêtre s'ouvre |
| **Incertain** ⚠️ | La fenêtre Exécuter apparaît mais n'est pas au premier plan (une autre app la cache) |
### Gap connu
> Si le dialogue Exécuter était **déjà ouvert** avant le test, le succès est un faux positif. Le YAML a un `t2_known_gap` documenté : `run_dialog_preexisting_false_positive`. **Vérifie l'absence du dialogue avant de cliquer Continuer.**
---
## Test 2 — `key_ctrl_s_wait_notepad_exe` (Ctrl+S → Enregistrer sous)
### Ce que Léa va faire
Appuyer sur `Ctrl+S` et attendre que le dialogue **Enregistrer sous** apparaisse.
### État initial requis
-**Bloc-notes (Notepad) ouvert** avec un document
- ✅ Document **non enregistré** et **modifié** (le titre doit contenir un astérisque `*`, ex: `Sans titre * Bloc-notes`)
- ✅ Aucun dialogue "Enregistrer sous" déjà ouvert
### Comment préparer
1. Ouvre Bloc-notes (`notepad.exe`)
2. Tape du texte au hasard
3. **Ne sauvegarde pas** → le titre doit montrer `*` (document modifié non sauvegardé)
### Procédure
1. Dashboard → Knowledge Base → `key_ctrl_s_wait_notepad_exe`
2. Clique **Tester**
3. Modale : "Prépare le test supervisé..." → clique **Continuer le test**
4. Léa envoie `Ctrl+S` automatiquement
5. Le dialogue "Enregistrer sous" doit apparaître en ~1-2 secondes
6. **Pause after** : clique Valide / Invalide / Incertain
### Ce que tu dois voir
- Le dialogue "Enregistrer sous" de Notepad apparaît au premier plan
- Bloc-notes reste ouvert en arrière-plan
### Comment juger
| Verdict | Quand |
|---------|-------|
| **Valide** ✅ | Le dialogue "Enregistrer sous" apparaît |
| **Invalide** ❌ | Rien ne se passe (document déjà enregistré = sauvegarde silencieuse), ou Notepad se ferme |
| **Incertain** ⚠️ | Le dialogue apparaît mais Notepad n'est pas en arrière-plan |
### Gap connu
> `save_as_requires_unsaved_notepad_document` : si le document a déjà un chemin de sauvegarde, `Ctrl+S` sauvegarde silencieusement sans ouvrir le dialogue. **Le document doit être non enregistré.**
---
## Test 3 — `key_alt_f4_wait_windowsterminal_exe` (Alt+F4 → fermer fenêtre)
### Ce que Léa va faire
Appuyer sur `Alt+F4` pour fermer la fenêtre Bloc-notes courante.
### État initial requis
-**Bloc-notes ouvert** avec un fichier (peut être enregistré ou non)
- ✅ Bloc-notes au **premier plan**
- ⚠️ Si le document est modifié et non sauvegardé, un **dialogue de confirmation** peut apparaître ("Voulez-vous enregistrer ?")
### Procédure
1. Ouvre Bloc-notes avec un fichier
2. Mets Bloc-notes au premier plan
3. Dashboard → Knowledge Base → `key_alt_f4_wait_windowsterminal_exe`
4. Clique **Tester**
5. Modale : "Prépare le test supervisé..." → clique **Continuer le test**
6. Léa envoie `Alt+F4` automatiquement
7. Bloc-notes doit se fermer (ou afficher le dialogue de confirmation)
8. **Pause after** : clique Valide / Invalide / Incertain
### Ce que tu dois voir
- Bloc-notes se ferme
- La fenêtre derrière (ex: explorateur, bureau, terminal) passe au premier plan
### Comment juger
| Verdict | Quand |
|---------|-------|
| **Valide** ✅ | Bloc-notes se ferme, une autre fenêtre prend le focus |
| **Invalide** ❌ | Bloc-notes ne se ferme pas, ou une erreur système apparaît |
| **Incertain** ⚠️ | Le dialogue "Enregistrer les modifications ?" apparaît — la fermeture n'est pas complète |
### Gap connu — ⚠️ IMPORTANT
> `alt_f4_confirmation_dialog_not_covered` : le `success_marker` actuel attend `C:\Windows\system32\cmd.exe` / `WindowsTerminal.exe` après fermeture. C'est un artefact de la session d'observation (c'était le Terminal qui était derrière). **Ce n'est pas le bon marqueur de succès général.** Si une autre fenêtre que le Terminal est derrière, le wait_state peut échouer même si la fermeture a réussi.
**Conséquence** : ce test est le moins fiable des 3. Si Alt+F4 ferme bien Bloc-notes mais que le wait_state timeout (parce que la fenêtre derrière n'est pas Terminal), clique **Incertain** — ce n'est pas un bug de Léa, c'est le marqueur de succès qui est trop spécifique.
---
## Résumé des verdicts
Après chaque test, le dashboard enregistre le verdict dans `data/competence_verdicts/verdicts.jsonl`. Tu ne verras pas de changement immédiat dans les fichiers YAML — la promotion en `stable` nécessite 3 succès indépendants.
| Compétence | Verdict | Observations |
|-----------|---------|-------------|
| `key_win_r_wait_explorer_exe` | | |
| `key_ctrl_s_wait_notepad_exe` | | |
| `key_alt_f4_wait_windowsterminal_exe` | | |
---
## Questions fréquentes
**Q : Et si la modale reste bloquée sur "Lancement du replay..." ?**
R : Vérifie que l'agent Windows est connecté et que le streaming server (`:5005`) tourne. Ferme la modale et recommence.
**Q : Et si le replay envoie les touches mais que rien ne se passe ?**
R : Vérifie que la fenêtre cible est au premier plan. Si tu testes Ctrl+S mais que Notepad n'est pas actif, le Ctrl+S ira à une autre application.
**Q : Puis-je tester plusieurs fois la même compétence ?**
R : Oui. Chaque test génère un nouveau verdict. Pour la promotion `candidate → stable`, il faut 3 succès avec 3 contextes distincts.
**Q : Que se passe-t-il si je clique "Invalide" ?**
R : Le verdict est enregistré comme `invalid`. Le replay se termine normalement. Le YAML n'est pas modifié. Si 3 invalid consécutifs surviennent, un flag `regression_suspected` sera activé.
---
*Auteur : Qwen*

View File

@@ -0,0 +1,554 @@
# Protocole de test humain E2E — POC Aiva-vision
**Date** : 2026-06-01
**Version** : 1.0
**Objet** : valider le POC en conditions réelles — apprentissage → restitution → correction → persistance → replay supervisé → verdict
**Durée estimée** : 45-60 minutes
---
## 1. Checklist pré-test
### 1.1 Services à vérifier
| Service | Port | Comment vérifier | GO/NO-GO |
|---------|------|------------------|----------|
| Ollama | 11434 | `curl http://localhost:11434/api/tags` — doit retourner la liste des modèles | ☐ |
| Streaming server | 5005 | `curl http://localhost:5005/health` ou `./svc.sh status` | ☐ |
| Web Dashboard | 5001 | `curl http://localhost:5001/health` ou ouvrir `http://localhost:5001` | ☐ |
| Agent Chat | 5004 | `curl http://localhost:5004/health` ou ouvrir `http://localhost:5004` | ☐ |
| VWB Backend | 5002 | `./svc.sh status` (optionnel si on ne teste pas via VWB) | ☐ |
| VWB Frontend | 3002 | `curl http://localhost:3002` (optionnel) | ☐ |
Commande rapide pour tout vérifier d'un coup :
```bash
./svc.sh status
```
### 1.2 Modèles Ollama requis
```bash
ollama list
```
Vérifier la présence de :
- `qwen2.5vl:7b` (ou `qwen2.5vl:7b-rpa`) — grounding visuel
- `qwen2.5:7b` — raisonnement texte
- `qwen2.5:0.5b` — intent recognition léger (fallback)
**Si un modèle manque** : `ollama pull <nom>` avant de continuer.
### 1.3 Compétences existantes dans le catalogue
Ouvrir le dashboard → onglet **Knowledge Base**. Vérifier la présence de :
| Compétence | État attendu |
|------------|-------------|
| `key_win_r_wait_explorer_exe` | `candidate` |
| `key_ctrl_s_wait_notepad_exe` | `candidate` |
| `key_alt_f4_wait_windowsterminal_exe` | `candidate` |
**Si le catalogue est vide** : les fichiers YAML doivent exister dans `data/competences/candidate/`. Vérifier :
```bash
ls -la data/competences/candidate/
```
Si les fichiers existent mais n'apparaissent pas dans le dashboard, redémarrer le streaming server (`./svc.sh restart streaming`).
### 1.4 Agent Windows connecté
- Dans le dashboard, vérifier que la machine cible apparaît (ex: `DESKTOP-XXXXX_windows`).
- Si l'agent n'est pas connecté : le lancer depuis le poste Windows (`Lea_v*.zip``lea_agent.exe`).
- **Machine cible** : noter le `machine_id` exact (affiché dans le dashboard ou dans `data/agents/`).
### 1.5 État initial de la machine Windows
Avant toute manipulation :
- [ ] **Fermer tous les dialogues parasites** : boîtes de dialogue "Enregistrer", "Exécuter", confirmations, pop-up Windows Update
- [ ] **Fermer les applications non nécessaires** : tout garder minimal (bureau + applications de test uniquement)
- [ ] **Vérifier que le bureau est visible** : pas d'application plein écran
- [ ] **Couper les notifications Windows** : mode "Ne pas déranger" activé (Win+A → activer)
- [ ] **Noter la résolution écran** : `Win+I → Système → Écran` (utile si un problème de positionnement survient)
### 1.6 Dossier de collecte des preuves
Créer un dossier de travail pour cette session de test :
```bash
mkdir -p /home/dom/ai/rpa_vision_v3/docs/demo/e2e-poc-$(date +%Y%m%d)
```
Ce dossier recevra les screenshots, logs, et verdicts collectés pendant le test.
---
## 2. Protocole pas-à-pas
### Phase 1 — Démarrer Léa / agent Windows
**Ce que l'opérateur fait** :
1. Vérifier que l'agent Windows est connecté (dashboard → page principale)
2. Noter le `machine_id` affiché
3. Ouvrir l'agent chat (`http://localhost:5004`) dans un navigateur
**Ce que Léa fait** : Rien (état IDLE)
**Ce qu'on doit voir** :
- Dashboard : machine connectée, statut "online"
- Agent chat : interface de conversation vide, bouton "Apprendre" visible
**Critère GO/NO-GO** :
- GO : agent chat accessible, machine connectée
- NO-GO : relancer l'agent Windows, vérifier le réseau, vérifier que le streaming server tourne
**Preuve à collecter** :
- Screenshot du dashboard montrant la machine connectée
- Copier le `machine_id` dans un fichier `preuves/machine_id.txt`
---
### Phase 2 — Déclencher l'apprentissage depuis agent-chat
**Ce que l'opérateur fait** :
1. Dans l'agent chat, taper : `lea apprends-moi` ou cliquer le bouton "Apprendre" si disponible
2. Observer la réponse de Léa
**Ce que Léa fait** :
- Transition IDLE → LISTENING → WAITING_USER_STOP
- Répond : *"Je te regarde. Fais ce que tu veux m'apprendre, et dis-moi « stop » ou « j'ai fini » quand c'est terminé."*
- Appelle `shadow_start` côté streaming server
**Ce qu'on doit voir** :
- Message de Léa confirmant qu'elle observe
- Side shadow actif côté streaming server (vérifiable via `curl http://localhost:5005/api/v1/shadow/sessions` si endpoint existe)
**Critère GO/NO-GO** :
- GO : Léa a répondu avec le message d'observation
- NO-GO : si `shadow_start` échoue → vérifier streaming server, token API (`RPA_API_TOKEN`), relancer
**Preuve à collecter** :
- Screenshot de la conversation agent-chat
- Copier le `session_id` (affiché dans la réponse ou dans les logs `agent_chat/state/`)
---
### Phase 3 — Observer une tâche courte
**Tâche cible** : `Win+R` → ouverture de la boîte "Exécuter"
**Ce que l'opérateur fait** (sur le poste Windows) :
1. **Basculer sur la session Windows** (via RDP ou directement)
2. Appuyer sur `Win+R`
3. Vérifier que la boîte "Exécuter" s'ouvre
4. **Ne pas fermer** la boîte "Exécuter" (elle servira de preuve visuelle)
5. Revenir à l'agent chat
**Ce que Léa fait** :
- Capture les événements clavier (`key_press` pour `win` + `r`)
- Capture les screenshots avant/après
- Enregistre la session brute
**Ce qu'on doit voir** :
- La boîte "Exécuter" apparaît à l'écran Windows
- Dans les logs du streaming server, des événements `key_press` et des screenshots sont reçus
**Critère GO/NO-GO** :
- GO : la boîte "Exécuter" est visible à l'écran
- NO-GO : si `Win+R` ne fonctionne pas → vérifier que le clavier Windows n'est pas bloqué, que RDP ne capture pas les touches
**Preuve à collecter** :
- Screenshot de la boîte "Exécuter" ouverte (depuis le poste Windows)
- Noter l'heure exacte de l'action
---
### Phase 4 — Arrêter l'observation
**Ce que l'opérateur fait** :
1. Dans l'agent chat, taper : `stop` ou `j'ai fini`
**Ce que Léa fait** :
- Reconnaît l'intent `USER_STOP_OBSERVE` (regex, confiance > 0.9)
- Transition WAITING_USER_STOP → ANALYZING → PRESENTING
- Appelle `shadow_stop` + `shadow_understanding` côté streaming server
- Formate la restitution Option C
**Ce qu'on doit voir** :
- Léa répond avec un résumé de ce qu'elle a compris, par exemple :
```
Voilà ce que j'ai compris :
1. Touche → raccourci utilisé : « win + r »
2. Fenêtre « Exécuter » → ouverte
C'est bien ça ou je me suis trompée quelque part ?
```
**Critère GO/NO-GO** :
- GO : Léa a restitué au moins 1 étape correcte
- NO-GO : si Léa dit "aucune étape comprise" → vérifier que les screenshots ont bien été capturés, que le shadow était actif
**Preuve à collecter** :
- Screenshot de la restitution Léa dans l'agent chat
- Copier le contenu JSON de `understanding` depuis `curl http://localhost:5005/api/v1/shadow/<session_id>/understanding`
---
### Phase 5 — Restitution et correction (Option C — formateur)
**Ce que l'opérateur fait** :
1. **Lire attentivement** la restitution de Léa
2. Si c'est correct : taper `oui` ou `c'est bon` ou `parfait`
3. Si c'est incorrect : taper `corrige l'étape 1 : [description correcte]`
**Ce que Léa fait** :
- Si validation : transition ITERATING_FEEDBACK → NAMING
- Si correction : envoie le feedback au streaming server (`shadow_feedback`), reçoit la nouvelle compréhension, reformule
- Compte les corrections par étape (max 3 → sortie d'urgence)
**Ce qu'on doit voir** :
- Si corrigée : Léa reformule avec la correction appliquée
- Si validée : Léa demande le nom de la compétence
**Critère GO/NO-GO** :
- GO : Léa a soit validé soit corrigé avec reformulation
- NO-GO : si Léa ne comprend pas la correction → vérifier l'intent parser (logs), le regex, le fallback Ollama
**Preuve à collecter** :
- Screenshot de l'échange de correction (si applicable)
- Noter le nombre de corrections apportées
---
### Phase 6 — Nommer la compétence et marquer les paramètres
**Ce que l'opérateur fait** :
1. Donner un nom : `ouverture executer` ou `lancer executer`
2. Si Léa demande si une valeur est un paramètre : répondre "toujours" ou "c'est l'exemple du jour"
3. Pour `Win+R`, il n'y a pas de valeur saisie → aucun paramètre à marquer
**Ce que Léa fait** :
- Enregistre le nom de la compétence
- Cherche les steps avec des valeurs saisies → demande si ce sont des paramètres
- Transition NAMING → PERSISTING quand tout est marqué
**Ce qu'on doit voir** :
- Léa confirme : *"C'est enregistré sous « ouverture executer » (slug `ouverture_executer`). Paramètres : aucun."*
**Critère GO/NO-GO** :
- GO : Léa a confirmé la persistance
- NO-GO : si `shadow_build` ou `competence_persist` échoue → vérifier logs streaming server, endpoint `/api/v1/lea/competences/candidate/persist`
**Preuve à collecter** :
- Screenshot du message de confirmation
- Vérifier que le YAML a été créé : `ls -la data/competences/candidate/`
- Copier le contenu du YAML dans `preuves/competence_<slug>.yaml`
---
### Phase 7 — Tester via le dashboard (replay supervisé)
**Ce que l'opérateur fait** :
1. Ouvrir le dashboard → onglet **Knowledge Base**
2. Trouver la compétence fraîchement créée
3. Cliquer **Tester**
4. Une modale s'ouvre : "Prépare le test supervisé..."
5. **Avant de cliquer "Continuer le test"** :
- Fermer la boîte "Exécuter" si elle est encore ouverte (étape critique — voir section 5 sur les faux succès)
- Vérifier que le bureau Windows est visible, aucune application parasite
6. Cliquer **Continuer le test**
**Ce que Léa fait** :
- Envoie `Win+R` automatiquement sur le poste Windows via l'agent
- Attend que la boîte "Exécuter" apparaisse (wait_state)
- Se met en pause après l'action
**Ce qu'on doit voir** :
- La boîte "Exécuter" s'ouvre sur le poste Windows (~1-2 secondes après le clic "Continuer")
- La modale affiche "Valide le résultat de la compétence..." avec 3 boutons : **Valide**, **Invalide**, **Incertain**
**Critère GO/NO-GO** :
- GO : la boîte "Exécuter" apparaît et la modale de verdict est affichée
- NO-GO : si rien ne se passe → vérifier que l'agent Windows est connecté, que le replay a bien été lancé (logs streaming server)
**Preuve à collecter** :
- Screenshot de la boîte "Exécuter" ouverte (depuis Windows)
- Screenshot de la modale de verdict dans le dashboard
---
### Phase 8 — Enregistrer le verdict
**Ce que l'opérateur fait** :
1. Évaluer le résultat selon les critères :
- **Valide** ✅ : la boîte "Exécuter" apparaît, au premier plan, correcte
- **Invalide** ❌ : rien ne se passe, ou une autre fenêtre s'ouvre
- **Incertain** ⚠️ : la boîte apparaît mais n'est pas au premier plan
2. Cliquer le bouton correspondant
3. La modale se ferme, le dashboard se rafraîchit
**Ce que Léa fait** :
- Persiste le verdict dans `data/competence_verdicts/verdicts.jsonl`
- Le replay reprend puis se termine
**Ce qu'on doit voir** :
- Le dashboard revient à la liste Knowledge Base
- Un nouveau verdict est enregistré (vérifiable dans le fichier JSONL)
**Critère GO/NO-GO** :
- GO : le verdict est enregistré, le fichier JSONL contient une nouvelle entrée
- NO-GO : si le verdict n'est pas persisté → vérifier logs dashboard, endpoint `/api/v1/lea/competences/<id>/verdict`
**Preuve à collecter** :
- Copier la dernière ligne du fichier `data/competence_verdicts/verdicts.jsonl` dans `preuves/verdict.jsonl`
- Screenshot du dashboard après verdict
---
### Phase 9 — Nettoyer et recommencer (itération)
Pour un test robuste, **répéter les phases 3-8 au moins 3 fois** avec des contextes différents :
| Itération | Contexte | Tâche |
|-----------|----------|-------|
| 1 | Bureau propre | `Win+R` → Exécuter |
| 2 | Avec Notepad ouvert + texte non sauvegardé | `Ctrl+S` → Enregistrer sous |
| 3 | Avec Notepad ouvert au premier plan | `Alt+F4` → Fermer |
**Ou** répéter 3 fois la même tâche avec des contextes légèrement différents (fenêtres ouvertes différentes, résolution modifiée, etc.) pour valider la robustesse.
---
## 3. Critères GO/NO-GO globaux
### 3.1 Qu'est-ce qui valide le POC ?
| Critère | Seuil |
|---------|-------|
| **Apprentissage par observation** | ✅ Léa doit restituer correctement au moins **1 tâche sur 3** tentées |
| **Restitution compréhensible** | ✅ Le texte Option C doit être lisible par un humain non-technique (phrases françaises, pas JSON) |
| **Correction acceptée** | ✅ Léa doit accepter et appliquer au moins **1 correction** |
| **Persistance** | ✅ Le YAML doit être créé dans `data/competences/candidate/` |
| **Replay supervisé** | ✅ Au moins **2 replays réussis sur 3** (la tâche est rejouée automatiquement) |
| **Verdict humain** | ✅ Les verdicts doivent être persistés dans le JSONL |
**Le POC est validé si** : au moins **2 des 3 compétences** du batch 1 passent le cycle complet (apprentissage → restitution → persistance → replay → verdict) avec un verdict **Valide**.
### 3.2 Échecs acceptables
| Échec | Acceptable ? | Condition |
|-------|-------------|-----------|
| `Alt+F4` → verdict Incertain | ✅ Oui | Le gap `alt_f4_confirmation_dialog_not_covered` est documenté |
| Ollama fallback timeout | ✅ Oui | Si les regex passent quand même (confiance > 0.9) |
| 1 correction sur 3 nécessaires | ✅ Oui | C'est dans le design (max 3 corrections/step) |
| Wait_state timeout sur Alt+F4 | ✅ Oui | Le success_marker est trop spécifique (artefact de session) |
### 3.3 Échecs bloquants
| Échec | Bloquant ? | Action |
|-------|-----------|--------|
| `shadow_start` échoue systématiquement | 🔴 BLOQUANT | L'agent Windows ne communique pas avec le streaming server |
| Aucune restitution (understanding vide) | 🔴 BLOQUANT | Le pipeline shadow ne reconstruit pas les étapes |
| `competence_persist` échoue | 🔴 BLOQUANT | La persistance ne fonctionne pas |
| Replay ne déclenche aucune action | 🔴 BLOQUANT | L'agent Windows ne reçoit pas les ordres |
| Verdict non persisté | 🔴 BLOQUANT | Le contrat de verdict n'est pas implémenté |
---
## 4. Preuves à collecter
### 4.1 Fichiers à vérifier après le test
| Fichier | Contenu | Comment vérifier |
|---------|---------|------------------|
| `data/competences/candidate/*.yaml` | Compétences persistées | `ls -la` + `cat` pour vérifier le contenu |
| `data/competence_verdicts/verdicts.jsonl` | Verdicts humains | `tail -n 5` pour voir les derniers verdicts |
| `agent_chat/state/learn_*.json` | États des sessions d'apprentissage | `ls -la agent_chat/state/` |
| `data/raw_sessions/<session_id>/` | Sessions brutes (screenshots + événements) | Vérifier la présence de screenshots |
| Logs streaming server | `journalctl -u rpa-streaming` ou stdout du process | Chercher les erreurs HTTP 5xx |
| Logs agent chat | stdout du process ou `logs/agent_chat.log` | Vérifier les transitions d'état |
| Logs dashboard | stdout du process ou `logs/dashboard.log` | Vérifier les appels replay/verdict |
### 4.2 Comment documenter un succès
Créer un fichier `preuves/succes_<competence_slug>.md` avec :
```markdown
# Succès — <nom de la compétence>
**Date** : 2026-06-01
**Session ID** : learn_xxx
**Machine** : DESKTOP-XXXXX_windows
## Preuves
- Screenshot restitution : `preuves/screenshot_restitution_<slug>.png`
- Screenshot replay : `preuves/screenshot_replay_<slug>.png`
- YAML : `data/competences/candidate/<slug>.yaml`
- Verdict : dernière ligne de `data/competence_verdicts/verdicts.jsonl`
## Observations
- Nombre de corrections : X
- Temps d'apprentissage : ~X minutes
- Temps de replay : ~X secondes
```
### 4.3 Comment documenter un échec
Créer un fichier `preuces/echec_<competence_slug>.md` avec :
```markdown
# Échec — <nom de la compétence>
**Date** : 2026-06-01
**Session ID** : learn_xxx
**Phase d'échec** : apprentissage / restitution / persistance / replay / verdict
## Symptôme
<description de ce qui s'est passé>
## Logs pertinents
<extraits de logs>
## Cause probable
<hypothèse>
## Reproduire
<étapes pour reproduire>
```
---
## 5. Risques de faux succès
### 5.1 Comment distinguer un vrai succès d'un faux positif
| Risque | Comment le détecter | Mitigation |
|--------|-------------------|------------|
| **Dialogue déjà ouvert** avant le replay | Vérifier visuellement **avant** de cliquer "Continuer le test" | Fermer tous les dialogues parasites (checklist phase 1.5) |
| **machine_id spoofé** ou wrong target | Vérifier que le `machine_id` dans le verdict correspond à la machine de test | Noter le `machine_id` en phase 1, le comparer dans le verdict JSONL |
| **Léa envoie les touches mais une autre app les reçoit** | Vérifier que la fenêtre cible est **au premier plan** avant le replay | Mettre la fenêtre cible au premier plan avant de cliquer "Continuer" |
| **Wait_state passe par chance** (la fenêtre était déjà là) | Le wait_state doit détecter un **changement d'état**, pas un état statique | Vérifier que le wait_state est bien un "appear" et pas un "already present" |
| **Verdict enregistré mais replay non exécuté** | Vérifier que `step_results[]` dans le verdict contient des steps avec `status: success` | Lire le JSONL et vérifier le contenu de `step_results` |
### 5.2 Pièges spécifiques à éviter
1. **RDP et les touches Windows** : en session RDP, `Win+R` peut être capturé par la machine locale au lieu de la machine distante.
- **Solution** : dans le client RDP, options → Clavier local → "Appliquer les combinaisons de touches Windows : Sur le serveur distant"
2. **Notifications Windows** : une notification pop-up peut voler le focus pendant le replay.
- **Solution** : activer le mode "Ne pas distrub" avant le test
3. **Windows Update** : un redémarrage intempestif pendant le test.
- **Solution** : suspendre Windows Update temporairement
4. **Agent Windows déconnecté** : le replay est envoyé mais l'agent ne le reçoit pas.
- **Solution** : vérifier la connexion avant chaque replay (dashboard → statut machine)
5. **Session shadow non fermée** : si `shadow_stop` n'a pas été appelé correctement, la session reste ouverte et peut interférer.
- **Solution** : vérifier `agent_chat/state/` — aucune session ne doit être dans un état non-terminal avant de recommencer
---
## 6. Bonus — Learning pack portable
### 6.1 Structurer les artefacts POC
Le risque : se retrouver avec des milliers de fichiers éparpillés entre `data/raw_sessions/`, `data/competences/`, `data/competence_verdicts/`, `agent_chat/state/`, et les screenshots.
**Structure recommandée** pour un POC exportable :
```
learning-poc-<date>/
├── manifest.json # Métadonnées : date, machine, opérateur, version du code
├── competences/
│ ├── <slug_1>.yaml # Compétences persistées
│ └── <slug_2>.yaml
├── verdicts/
│ └── verdicts.jsonl # Verdicts humains (copie du JSONL)
├── sessions/
│ ├── <session_id>/
│ │ ├── events.jsonl # Événements bruts
│ │ └── screenshots/ # Screenshots (compressés)
│ └── <session_id>/
├── audit/
│ ├── promotions.jsonl # Audit de promotion (si applicable)
│ └── test-log.md # Journal du test humain
└── proofs/
├── screenshot_restitution_*.png
├── screenshot_replay_*.png
└── succes_*.md / echec_*.md
```
### 6.2 Script d'export rapide
```bash
#!/bin/bash
# export_learning_poc.sh — créer un zip portable du POC
DATE=$(date +%Y%m%d)
DEST="learning-poc-${DATE}"
mkdir -p "$DEST"/{competences,verdicts,sessions,audit,proofs}
# Compétences candidate
cp data/competences/candidate/*.yaml "$DEST/competences/" 2>/dev/null
# Verdicts
cp data/competence_verdicts/verdicts.jsonl "$DEST/verdicts/" 2>/dev/null
# Audit
cp data/competences/promotions.jsonl "$DEST/audit/" 2>/dev/null
# Sessions récentes (dernières 24h)
find data/raw_sessions/ -maxdepth 1 -mtime -1 -type d -exec cp -r {} "$DEST/sessions/" \; 2>/dev/null
# States agent chat
cp agent_chat/state/learn_*.json "$DEST/audit/" 2>/dev/null
# Manifest
cat > "$DEST/manifest.json" <<EOF
{
"created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"machine": "$(hostname)",
"operator": "${OPERATOR:-dom}",
"code_version": "$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')",
"competences_count": $(ls "$DEST/competences/"*.yaml 2>/dev/null | wc -l),
"verdicts_count": $(wc -l < "$DEST/verdicts/verdicts.jsonl" 2>/dev/null || echo 0)
}
EOF
# Zip
zip -r "${DEST}.zip" "$DEST/"
echo "Pack créé : ${DEST}.zip"
```
### 6.3 Export vers DGX Spark
Le learning pack ainsi structuré est **autonome** :
1. Transférer le zip vers le DGX Spark : `scp learning-poc-*.zip user@dgx-spark:/path/to/imports/`
2. Importer côté DGX : le script d'import lit le `manifest.json`, place les YAML dans `data/competences/candidate/`, les verdicts dans `data/competence_verdicts/`, et les sessions dans `data/raw_sessions/`
3. Vérifier l'import : `ls data/competences/candidate/` + `tail data/competence_verdicts/verdicts.jsonl`
**Invariant** : le learning pack ne contient **pas** de modèles Ollama, pas de dépendances Python, pas de fichiers système. Uniquement des artefacts métier (YAML, JSONL, screenshots, events).
---
## Annexe — Résumé visuel du flux
```
[IDLE] ──(apprends-moi)──> [LISTENING] ──> [WAITING_USER_STOP]
(stop / j'ai fini)
v
[DONE] <── [PERSISTING] <── [NAMING] <── [ITERATING_FEEDBACK] <── [ANALYZING]
│ │
│ (oui / corrige étape N)
│ │
+──> YAML + verdict JSONL <── Dashboard: Tester ──> Replay supervisé
Valide/Invalide/Incertain
```
---
*Document créé le 2026-06-01. À suivre pas-à-pas pendant la démo.*

View File

@@ -171,7 +171,7 @@ Une fois la correction appliquée et validée :
---
**Tokens de Production (pour référence)** :
- Admin: `73cf0db73f9a5064e79afebba96c85338be65cc2060b9c1d42c3ea5dd7d4e490`
- ReadOnly: `7eea1de415cc69c02381ce09ff63aeebf3e1d9b476d54aa6730ba9de849e3dc6`
- Admin: `REDACTED`
- ReadOnly: `REDACTED`
⚠️ **IMPORTANT** : Ces tokens doivent rester confidentiels et seront à changer avant la production réelle.

View File

@@ -0,0 +1,120 @@
# Handoff — Fix capture monitor aberrante + sécurisation fallback
**Date** : 2026-05-19
**Branche** : `backup/post-demo-2026-05-19` (HEAD `5ea4960e6`, non commité)
**Missions** : Claude 2 (fix initial) + Claude 2b (sécurisation fallback)
**Bug source** : audit Léa-first runtime, blocage P0 #4`agent_v0/agent_v1/vision/capturer.py` accède aveuglément à `mss.monitors[1]` sans valider ses dimensions. Cas observé en démo GHT 19 mai : `mss` renvoie intermittemment `2560×60` au lieu de `2560×1600`, coords Y divisées ~27×, mémoire persistante empoisonnée.
## Contexte produit
La voie nominale `capture → replay direct → memory` repose sur des coordonnées normalisées via les dimensions de l'écran principal. Une capture acceptant des dimensions aberrantes propage des `(x_pct, y_pct)` faux jusqu'au `TargetMemoryStore` (`core/learning/target_memory_store.py`), qui stocke des fingerprints inexploitables et incrémente `fail_count` sans signal sur la cause. Le bug originel s'auto-amplifie : chaque réutilisation de la mémoire empoisonnée déclenche d'autres échecs silencieux.
## Fichier modifié
- `agent_v0/agent_v1/vision/capturer.py` (309 → 401 lignes)
## Fichier créé
- `tests/unit/test_capturer_monitor_guard.py` (10 tests)
## Périmètre strict
**Non touchés** : `executor.py`, `main.py`, `api_stream.py`, replay engine, `resolve_engine.py`, `replay_memory.py`. Le fix est intégralement contenu côté capture client. Pas de SCP vers Léa Windows réalisé.
## Changements code
### Constantes module-level
```python
MIN_MONITOR_WIDTH = 200
MIN_MONITOR_HEIGHT = 200
MONITOR_MAX_ATTEMPTS = 2
MONITOR_RETRY_DELAY_S = 0.05
```
Seuil 200 px : largement au-dessus du cas observé (60 px) et largement sous toute résolution physique légitime. Marge confortable pour ne pas rejeter une fenêtre RDP/NoMachine ré-dimensionnée. Retry 2× avec 50 ms : couvre le pattern "mss cache stale au premier appel" sans pénaliser le chemin nominal (worst-case 100 ms avant abandon).
### Helpers privés
- `_is_monitor_sane(monitor) -> bool` — prédicat plausibilité dims (None → False, dict avec width/height ≥ seuils → True)
- `_dim_str(monitor) -> str` — formatage compact `WxH` pour les logs (gère None)
- `_acquire_safe_grab(max_attempts, retry_delay_s, allow_secondary_fallback) -> (monitor_dict | None, PIL.Image | None)` — ouvre `mss.mss()`, valide les dims, retry, fallback contrôlé, retour `(None, None)` en cas d'abandon. Logs WARNING par tentative aberrante, ERROR explicite à l'abandon avec distinction des causes.
### Méthodes publiques refactorées
| Méthode | Comportement | Justification |
|---|---|---|
| `capture_full_context` | `_acquire_safe_grab()` → fallback secondaire **autorisé** | Heartbeat ne porte pas de coords client, un écran sain quelconque suffit |
| `capture_dual` | `_acquire_safe_grab(allow_secondary_fallback=False)` → fail-closed | Reçoit `(x, y)` en coords composite, cropper depuis monitor secondaire produirait une image saine mais décalée |
| `capture_active_window` (path standalone) | `_acquire_safe_grab(allow_secondary_fallback=False)` → fail-closed | `win_rect` en coords globales, même risque |
Signatures publiques inchangées : `""`, `{}`, `None` selon le contrat existant en cas d'erreur.
## Décision clé — fail-closed vs recalcul d'offsets
**Choix : fail-closed** sur fallback secondaire pour les méthodes coord-bearing. Pas de translation `(x - monitor.left, y - monitor.top)` car :
1. Translation impose ensuite de vérifier inclusion du clic et de la fenêtre dans le monitor choisi, gérer les fenêtres à cheval, valider les coords positives — surface de bug supérieure à celle qu'on essaie d'éliminer.
2. Le bug est intermittent ; refuser une capture pendant ~100 ms est acceptable, Léa reprend au prochain événement.
3. Aligne avec `feedback_failure_is_learning.md` — un échec explicite vaut mieux qu'une réussite fausse.
## Tests
```bash
source /home/dom/ai/rpa_vision_v3/.venv/bin/activate && \
python -m pytest tests/unit/test_capturer_monitor_guard.py -v
```
**Résultat** : `10 passed in 0.25s`
| # | Test | Mission | Couverture |
|---|---|---|---|
| 1 | `test_capture_full_context_returns_empty_when_monitor_height_aberrant` | 2 | refus dim aberrante height=60 |
| 2 | `test_aberrant_monitor_logs_warning_with_observed_dimensions` | 2 | log WARN contient "2560" et "60" |
| 3 | `test_capture_retries_when_first_monitor_query_is_aberrant` | 2 | retry réussi après 1er appel aberrant |
| 4 | `test_capture_falls_back_to_secondary_monitor_when_primary_aberrant` | 2 | multi-écrans : `capture_full_context` utilise monitors[2] |
| 5 | `test_capture_dual_returns_empty_dict_when_monitor_aberrant` | 2 | `capture_dual` retourne `{}` sur aberration |
| 6 | `test_capture_active_window_returns_none_when_monitor_aberrant` | 2 | `capture_active_window` retourne `None` sur aberration |
| 7 | `test_capture_full_context_succeeds_on_normal_dimensions` | 2 | non-régression chemin nominal |
| 8 | `test_capture_dual_fails_closed_when_only_secondary_monitor_sane` | 2b | `capture_dual` refuse fallback secondaire |
| 9 | `test_capture_active_window_fails_closed_when_only_secondary_monitor_sane` | 2b | `capture_active_window` refuse fallback secondaire |
| 10 | `test_capture_full_context_still_uses_secondary_fallback` | 2b | non-régression : heartbeat accepte toujours le fallback |
Cycle TDD respecté pour tous les RED (1, 3, 5, 8) : test rouge vu avant implémentation. Tests 2, 4, 6, 7, 9, 10 ont validé une propriété exercée par l'implémentation déjà en place mais distincte du test précédent.
## Logs runtime attendus
**Cas nominal** : aucun log de cette garde (silencieux).
**Cas dim aberrante avec retry réussi** :
```
WARNING agent_v0.agent_v1.vision.capturer Monitor[1] dims aberrantes (2560x60, seuil 200x200) — attempt 1/2
WARNING agent_v0.agent_v1.vision.capturer Capture fallback : monitor[1] dim=2560x1600, attempt=2
```
**Cas fallback secondaire (heartbeat)** :
```
WARNING ... Monitor[1] dims aberrantes (2560x60, seuil 200x200) — attempt 1/2
WARNING ... Capture fallback : monitor[2] dim=1920x1080, attempt=1
```
**Cas fail-closed (capture_dual / capture_active_window avec secondaire dispo)** :
```
WARNING ... Monitor[1] dims aberrantes (2560x60, seuil 200x200) — attempt 1/2
WARNING ... Monitor[2] sain (1920x1080) mais fallback secondaire refusé (allow_secondary_fallback=False) — capture cohérente des coords impossible
ERROR ... Capture abandonnée : monitor[1] aberrant après 2 tentatives (dernier vu 2560x60) et fallback secondaire désactivé pour préserver la cohérence des coordonnées
```
**Cas abandon total** :
```
ERROR ... Aucun monitor avec dims plausibles trouvé après 2 tentatives (dernier vu : 2560x60, seuil 200x200) — capture abandonnée
```
## Points ouverts (à tracer post-déploiement)
1. **Seuil 200 px choisi par jugement** — pas par mesure empirique sur l'éventail des dims légitimes (RDP, NoMachine windowed, multi-écran portrait). À relever si un faux rejet apparaît en prod.
2. **Reproduction du bug réel non testée** — les 10 tests utilisent des mocks `mss`. La cause exacte côté mss reste non identifiée. Cette garde **protège** sans **expliquer**. À confirmer en redéployant sur la machine Windows Léa.
3. **SCP vers Léa Windows non fait** — conformément à `feedback_scp_auto_modif_client_windows.md`, le SCP vers `dom@192.168.1.11` est attendu après toute modif `agent_v0/agent_v1/**`. À faire avant le prochain run réel.
4. **Suivi côté serveur potentiellement nécessaire** — si `capture_full_context` (heartbeat) commence à retourner une image issue d'un monitor secondaire en cas de panne intermittente du primary, et si le serveur réutilise ce heartbeat pour pré-check sur des coords (`/resolve_target` fallback heartbeat), la même incohérence coord/image pourrait apparaître côté serveur. **Hors périmètre** missions 2/2b. À tracer comme follow-up éventuel.
5. **Pas de translation x/y dans `capture_dual`/`capture_active_window`** — si un jour on voulait supporter le fallback secondaire proprement, ce serait un chantier séparé (calcul `(x - monitor.left, y - monitor.top)`, validation inclusion, gestion fenêtres à cheval).
6. **Couverture `capture_active_window` quand `full_img` est fourni par l'appelant** — la garde s'applique uniquement au path standalone (sans `full_img`). Le cas le plus fréquent (appel depuis `capture_dual` avec `full_img=img`) est implicitement gardé via la garde de `capture_dual`. Mais un autre appelant qui fournirait un `full_img` issu d'une capture non gardée court-circuiterait la défense. Pas d'appelant identifié actuellement.
## Référence audit
Ce fix résout le blocage **P0 #4** de `docs/AUDIT_LEA_FIRST_RUNTIME_2026-05-19.md`. Les blocages P0 #1 (`record_human_correction` cassé), #2 (`build_workflow_replay` orphelin), #3 (memory gated sur `window_title`) restent ouverts.

View File

@@ -0,0 +1,113 @@
# Handoff — Post-démo GHT 2026-05-19
## TL;DR
**Démo vidéo enregistrée en bout en bout** après une session de 14h (18-19 mai) et **15 jours de bug-chasing** depuis le 5 mai.
⚠️ **Retour d'expérience amer** : on est partis d'un système moins mature mais qui **fonctionnait** (état tag `demo-stable-2026-05-12`, branche `demo/ght-2026-05-08`) et on a dérivé via une cascade de modifs locales jusqu'à un système instable. La démo enregistrée a fonctionné **grâce à des contournements**, pas grâce à une vraie résolution des bugs.
🎯 **Objectif post-démo** : consolider, identifier les vrais bugs racines, restaurer un état stable utilisable pour les prochaines démos sans devoir refaire 14h de bricolage.
## État final livré
- **Vidéo démo** : enregistrée et utilisable
- **Workflow** : `Demo_urgence_3_db` (`wf_483910cdd851_1778750587`) — 46 steps
- **Branche backup git** : `backup/post-demo-2026-05-19` → commit `5ea4960e6` (poussée sur gitea)
- **Tarball complet** : `/home/dom/ai/_archives/rpa_vision_v3_post_demo_20260519_142940.tar.gz` (8.9 Go, sha256 `7ab84f22d5a45b7880cad4efb4466f9e320f3e1e33218ceee267fc93fe7631af`)
## Chronologie résumée de la session (18-19 mai)
### 18 mai
1. Création **page AIVA-URGENCE** autonome (`docs/clients/ght_sud_95/aiva_urgence/`) — interface médicale pour la démo, design dark
2. Intégration AIVA dans le workflow : 6 nouveaux steps insérés (ord 15-20) entre `llm_generate` et `Win+D`
3. Bug **NPM Basic Auth** sur `urgence.labs.laurinebazin.design` → bypass via `location ^~ /aiva-urgence/ auth_basic off;` directement dans `proxy_host/10.conf` (modif hors git, sera écrasée si UI NPM touchée)
4. Bug **`pause_for_human` skip silencieux en mode autonome** → fix `safety_checks` avec `required: false` sur step 1
5. Bug **frontend cache** : Ctrl+Shift+R obligatoire après chaque modif DB pour que VWB envoie le workflow à jour
6. Fix **`api_stream.py:3013`** — enrichir le payload `replay_paused/pause_message/replay_id` dès le premier polling `/replay/next` (sinon gap 1-2s)
7. Création `scripts/cancel-replays.sh` — workaround pour purger les queues serveur (bug Stop VWB)
### 19 mai
8. **Merge workflow `linux_db`** dans Demo_urgence_3_db (suppression ord 36-46, insertion 9 steps de linux_db) via agent
9. **Bypass LLM** : `_handle_t2a_decision_action` + `_handle_llm_generate_action` acceptent maintenant `static_result` / `static_text` → décision déterministe UHCD pour MOREL Catherine (court-circuit Ollama)
10. **Bug `delay_before/delay_after` jamais lus** au runtime → ajout step `wait` explicite + lift `duration_ms` dans `dag_execute.py`
11. **Bug coord côté Léa** : `actual_position Y` divisé par ~27 sur certains clics (mapping client utilise dim écran tronquée `2560×60` au lieu de `2560×1600`)
12. **Bug VWB recapture anchor** : "Recapture" via UI ne régénère PAS le PNG (les 2 anchors `anchor_d467a5411722` et `anchor_bfbffbb47be7` sont **bit-à-bit identiques** alors que capturés à 8 jours d'intervalle)
13. **Bug Léa état mémoire** : la bulle paused n'apparaît plus dans le chat après plusieurs replays consécutifs → résolu par restart Léa Windows
14. Solution pour NoMachine : remplacé le double-clic auto par `pause_for_human` → Dom clique manuellement pendant l'enregistrement
## Bugs racines identifiés (à traiter post-démo, par ordre de gravité)
### 🔴 P0 — Bug "recapture anchor ne régénère pas le PNG" (VWB)
**Symptôme** : à chaque modif, "régression mystérieuse". L'anchor recapturé pointe sur la mauvaise icône car le PNG reste l'ancien.
**Audit** : `visual_workflow_builder/backend/api_v3/capture.py` — chercher si `image_path` est défini AVANT le screenshot, ou si la fonction réutilise un fichier existant.
**Impact** : explique 80% des "ça marchait hier" et des régressions silencieuses entre démos.
### 🔴 P0 — Bug "Stop VWB ne purge pas la queue serveur"
**Symptôme** : relance d'un replay = celui-ci hérite des actions en attente du précédent → Léa reprend au milieu.
**Workaround actuel** : `./scripts/cancel-replays.sh` manuel.
**Vrai fix** : VWB doit appeler `POST /api/v1/traces/stream/replay/<id>/cancel` quand on clique Stop.
### 🔴 P0 — Bug coord côté Léa client (mapping Y cassé sur capture tronquée)
**Symptôme** : `actual_position Y = 0.0099` au lieu de `0.265` → clic en haut de l'écran au lieu de la cible.
**Cause** : `mss.monitors[1]` retourne intermittemment `2560×60` au lieu de `2560×1600` → Léa map `y_pct * 60 = 16 px`.
**Audit** : `agent_v0/agent_v1/core/executor.py:606-617` — ajouter fallback dim minimale (rejeter si `height < 200`).
### ⚠️ P1 — Léa client accumule un état mémoire dégradé
**Symptôme** : après plusieurs pauses consécutives, la bulle paused n'apparaît plus (chat vide alors que le serveur envoie bien `replay_paused: true`).
**Workaround** : restart Léa Windows.
**Vrai fix** : reset propre de `_last_pause_msg_shown`, `_chat_window_ref`, état Tkinter à chaque fin de replay (côté `main.py`).
### ⚠️ P1 — `delay_before` / `delay_after` ignorés au runtime
**Symptôme** : on peut configurer ces champs en DB mais Léa ne les lit pas.
**Fix partiel appliqué** : `dag_execute.py` lift `duration_ms` pour les actions `wait`/`wait_for_anchor`.
**Vrai fix** : faire de même pour `delay_before` et `delay_after` côté executor.py (généraliser).
### ⚠️ P1 — Léa client interprète `action=null + replay_paused=true` comme "fin du replay"
**Symptôme** : main.py désactive `_replay_active` à tort quand le replay est en pause → cleanup UI + bulle invisible.
**Fix proposé** : `executor.py:1875` retourner `True` (au lieu de `False`) quand `replay_paused` est traité.
**Status** : non appliqué (nécessite SCP + restart Léa Windows).
### ⚠️ P2 — Bug VWB frontend cache
**Symptôme** : après modif DB, le frontend continue à envoyer l'ancienne version. Ctrl+Shift+R obligatoire.
**Vrai fix** : invalidation cache automatique côté React quand workflow modifié serveur-side.
## Leçons de méthode (autocritique honnête)
1. **15 jours de dérive** : on a accumulé des modifs locales sans valider à chaque étape qu'on ne dégradait pas l'existant. CLAUDE.md disait "chirurgie itérative supervisée", on a fait l'inverse à plusieurs reprises.
2. **Pas de tests de non-régression entre démos** : à chaque "ça marchait hier", on découvrait par hasard qu'un truc avait changé. Manque de smoke-test reproductible.
3. **Bug VWB recapture anchor non détecté pendant 15 jours** : la cause racine n°1 des régressions silencieuses était invisible. Aurait dû être trouvée plus tôt par un audit "diff PNG anchor avant/après recapture".
4. **Workarounds empilés** : chaque bug a généré un workaround (script cancel-replays.sh, bypass LLM static, pause humaine NoMachine, etc.) au lieu de fixer la cause. Dette technique accumulée.
5. **Recapture en aveugle** : Dom a recapturé plusieurs anchors en pensant fixer le bug, alors que le bug était VWB côté capture (pas Dom).
## État technique en sortie de session
- **Branche actuelle** : `feature/qw-suite-mai` (avec toutes les modifs uncommitted de la session)
- **Branche backup** : `backup/post-demo-2026-05-19` (commit groupé, poussé sur gitea)
- **Référence "ça marchait"** : tag `demo-stable-2026-05-12` (commit `2eeaa806b`), branche `demo/ght-2026-05-08` (commit `56e869c46`)
- **Services actifs** : streaming (5005), VWB backend (5002), VWB frontend (3002), dashboard (5001), worker (5099), agent-chat (5004)
- **Bypass LLM actif** dans `replay_engine.py` : les steps 12/13/14 du workflow utilisent `static_result`/`static_text` → décision UHCD MOREL hardcodée
- **NPM bypass auth** actif dans `proxy_host/10.conf` (sera écrasé si UI NPM touchée)
## Sauvegardes
| Type | Localisation | Détails |
|------|--------------|---------|
| Tarball | `/home/dom/ai/_archives/rpa_vision_v3_post_demo_20260519_142940.tar.gz` | 8.9 Go, sha256 vérifié |
| Branche git | `backup/post-demo-2026-05-19` sur gitea | commit `5ea4960e6`, 627 fichiers incluant 468 anchors |
| Backups DB | `visual_workflow_builder/backend/instance/workflows.db.bak.*` | ~12 .bak DB de la session (chaque modif majeure) |
## Prochaine session — priorités proposées
1. **Comparer avec `demo/ght-2026-05-08`** : identifier ce qui a vraiment régressé depuis l'état "ça marchait" (mais qui était moins mature)
2. **Fixer le bug VWB recapture anchor** (P0) — c'est le boss final, il explique la majorité des régressions silencieuses
3. **Fixer le bug Stop VWB ne purge pas la queue** (P0) — supprime un workaround manuel
4. **Réintégrer le fix `executor.py:1875` (return True sur replay_paused) côté Léa** + SCP + restart Léa Windows
5. **Smoke-test reproductible** : script qui rejoue Demo_urgence_3_db de bout en bout et compare l'état attendu à chaque step (au lieu de tester manuellement)
## Pointeurs utiles
- Mémoire projet : `/home/dom/.claude/projects/-home-dom-ai-rpa-vision-v3/memory/MEMORY.md`
- CLAUDE.md projet : `/home/dom/ai/rpa_vision_v3/CLAUDE.md` (règles de méthode)
- Reset replays : `./scripts/cancel-replays.sh`
- Mockup AIVA-URGENCE : `docs/clients/ght_sud_95/aiva_urgence/` (port 8765, exposé via `https://urgence.labs.laurinebazin.design/aiva-urgence/`)
- Workflow ID démo : `wf_483910cdd851_1778750587`

View File

@@ -0,0 +1,138 @@
# Handoff Claude — Horizon 1 Léa-first
**Date** : 2026-05-20
**Branche** : `backup/post-demo-2026-05-19` (HEAD `5ea4960e6`)
**Session** : sessions du 19-20 mai post-démo GHT
## Contexte
Horizon 1 = pivot Léa-first post-démo GHT. Mission globale comprise : auditer l'état réel du runtime `capture → replay direct → memory`, identifier et corriger ce qui empêche cette voie d'être nominale, préparer la validation client. Cadre : pas de refonte large, pas de retour sur VWB comme outil central, chirurgie supervisée.
Le travail a été découpé par Dom en missions numérotées (1, 2, 2b, 3, 4, 5) avec périmètres serrés et interdits explicites. Ce handoff couvre uniquement ma contribution sur ces missions.
## Travaux réalisés
### Audits (lecture seule, sans modification de code)
| Mission | Objectif | Livrable |
|---|---|---|
| Préliminaire | Inventaire factuel des 15 jours pré-démo GHT (ce qui marche / ce qui casse) | `docs/LESSONS_LEARNED_GHT_2026-05.md` |
| 1 | 10 blocages runtime Léa-first (capture / replay direct / memory) | `docs/AUDIT_LEA_FIRST_RUNTIME_2026-05-19.md` |
| 3 | Cartographie de la perte de `window_title` dans le pipeline mémoire | `docs/AUDIT_WINDOW_TITLE_MEMORY_PATH_2026-05-19.md` |
| 4 | Point d'intégration agent le plus petit pour le contrat `/finalize` enrichi | `docs/AUDIT_FINALIZE_CONTRACT_INTEGRATION_2026-05-20.md` |
| 5 | Préparation validation réelle Windows (fichiers à déployer + smoke test) | `docs/SMOKE_TEST_FINALIZE_REPLAY_2026-05-20.md` |
### Modifications de code
**Faites par moi (missions 2 + 2b)** :
- `agent_v0/agent_v1/vision/capturer.py` — ajout garde dimensions monitor :
- constantes module-level (`MIN_MONITOR_WIDTH=200`, `MIN_MONITOR_HEIGHT=200`, `MONITOR_MAX_ATTEMPTS=2`, `MONITOR_RETRY_DELAY_S=0.05`)
- helpers `_is_monitor_sane`, `_dim_str`, `_acquire_safe_grab(allow_secondary_fallback)`
- refactor `capture_full_context` (fallback secondaire autorisé)
- refactor `capture_dual` (fail-closed sur fallback secondaire)
- refactor `capture_active_window` path standalone (fail-closed)
- `tests/unit/test_capturer_monitor_guard.py` — CRÉÉ, 10 tests TDD
Handoff de ce fix : `docs/handoffs/2026-05-19_handoff_fix_capture_monitor_guard.md`.
**Constatées dans la branche, NON faites par moi** (apparues entre mes missions, probablement faites dans une session parallèle) :
- `agent_v0/agent_v1/main.py` — câblage `set_on_finalize_result` + `_on_finalize_result` + import `dispatch_finalize_result`
- `agent_v0/agent_v1/network/streamer.py` — attribut + setter + invocation `_on_finalize_result` dans `_finalize_session`
- `agent_v0/agent_v1/ui/smart_tray.py` — méthode `offer_finalize_replay` + `_launch_replay_request`
- `agent_v0/agent_v1/finalize_contract.py` — NOUVEAU fichier (untracked)
- `agent_v0/agent_v1/core/executor.py` — modifié (origine antérieure, hors scope mes missions)
### Validations / tests
| Test | Commande | Résultat |
|---|---|---|
| Garde monitor (10 tests TDD) | `source .venv/bin/activate && python -m pytest tests/unit/test_capturer_monitor_guard.py -v` | `10 passed in 0.25s` |
| Import sanity du module modifié | `python -c "from agent_v0.agent_v1.vision.capturer import VisionCapturer, _acquire_safe_grab, _is_monitor_sane, MIN_MONITOR_WIDTH, MIN_MONITOR_HEIGHT, MONITOR_MAX_ATTEMPTS, MONITOR_RETRY_DELAY_S"` | `Import OK ; seuils=200x200, retries=2, delay=0.05s` |
Aucun autre test exécuté (suite complète projet, tests intégration, tests Windows réel).
## Fichiers touchés (effectivement modifiés par moi)
```
agent_v0/agent_v1/vision/capturer.py (modifié — garde monitor)
tests/unit/test_capturer_monitor_guard.py (créé — 10 tests)
```
Plus les 6 documents listés en section Audits ci-dessus.
**Aucune autre modification de code de ma part.** Les autres modifs visibles dans `git status` (main.py, streamer.py, smart_tray.py, finalize_contract.py, executor.py) ne sont pas de mon fait.
## Tests exécutés
```bash
source /home/dom/ai/rpa_vision_v3/.venv/bin/activate && \
python -m pytest tests/unit/test_capturer_monitor_guard.py -v
```
Résultat : **10 passed in 0.25s** (rejet dims aberrantes, log WARNING avec dims observées, retry après aberration, fallback monitor secondaire pour `capture_full_context`, fail-closed sur fallback secondaire pour `capture_dual` et `capture_active_window`, non-régression dim normale).
Cycle TDD respecté : RED observé avant chaque GREEN sur les 4 tests qui ont fait l'objet d'une implémentation neuve (tests 1, 3, 5, 8).
## Points ouverts
### Bugs P0 identifiés mais NON fixés
| # | Origine | Statut |
|---|---|---|
| P0 #1 audit Léa-first | `record_human_correction` double bug (import inexistant + signature obsolète) | **non fixé** |
| P0 #2 audit Léa-first | `build_workflow_replay` orphelin (0 caller runtime) | **non fixé** — note : pertinence à reconsidérer maintenant que le contrat `/finalize` enrichi semble couvrir le besoin |
| P0 #3 audit Léa-first / Mission 3 | `window_title` perdu : asymétrie écriture top-level vs lecture target_spec (`stream_processor.py:1545` vs `:1590-1601`) | **non fixé** |
| P0 #3 audit Léa-first / Mission 3 | Fallback `expected_window_before` morte dans `api_stream.py:3636` (cherche dans target_spec, n'est jamais là) | **non fixé** |
| P0 #4 audit Léa-first | `mss.monitors[1]` aveugle aux dims aberrantes | ✅ **FIXÉ** missions 2 + 2b |
### Validations Windows en attente
| Item | Statut |
|---|---|
| SCP des 5 fichiers vers `dom@192.168.1.11:C:/rpa_vision/agent_v1/` (capturer.py, finalize_contract.py, streamer.py, smart_tray.py, main.py) | **non fait** |
| Reproduction réelle du bug mss monitors aberrant | **non vérifié** (la garde protège sans expliquer la cause exacte) |
| Smoke test 5 minutes `enregistrement → finalize → proposition → replay-session` sur Léa Windows | **non fait** |
| Contrat serveur `/finalize` enrichi renvoie bien `{replay_ready, replay_request, replay_launch}` | **non vérifié** (audité côté agent uniquement, jamais déclenché en condition réelle) |
### État git
- 5 fichiers modifiés non committés (`capturer.py` par moi ; `main.py`, `streamer.py`, `smart_tray.py`, `executor.py` non par moi)
- 1 fichier untracked (`finalize_contract.py`, non par moi)
- 6 documents créés dans `docs/` et `docs/handoffs/` (par moi, non committés)
## Risques / hypothèses
### Sur le déploiement et la synchro client Windows
- **Hypothèse** : le canal de déploiement Windows reste le `sshpass scp` manuel fichier par fichier vers `C:/rpa_vision/agent_v1/`, conforme à `memory/feedback_scp_auto_modif_client_windows.md`. **Non vérifié** : aucun script SCP automatique n'est présent dans le repo, et le dossier `agent_v0/deploy/windows_client/` est obsolète de 2 à 7 semaines selon les fichiers (sert au setup initial, pas à l'incrémental).
- **Risque critique** : `finalize_contract.py` est un fichier neuf untracked. Le réflexe "je SCP les fichiers modifiés" ne le couvre pas. **Si oublié → ImportError au démarrage Léa, agent ne démarre pas**.
- **Risque** : SCP non atomique. Si Léa est relancée entre 2 SCP, état incohérent possible. Mitigation : SCP les 5 fichiers d'abord, **puis** relancer Léa.
- **Hypothèse** : token `RPA_API_TOKEN` est exporté côté session Windows. **Non vérifié** depuis cette session. Sans token → `register` et `finalize` retournent 401 silencieusement.
### Sur le contrat `/finalize` enrichi
- **Observé** : le câblage agent-side consomme un contrat `{replay_ready, replay_request, replay_launch}` (avec `replay_launch.status` ∈ {`started`, `failed`}). **Non vérifié** : le serveur Linux retourne effectivement ce contrat. Si le serveur renvoie encore l'ancien payload, le smoke test s'arrêtera silencieusement après l'étape "Session finalisée" sans crash ni proposition.
### Sur le fix monitor
- **Fait** : la garde protège contre dim aberrante via mocks. **Non vérifié** sur Léa Windows réelle (pas de SCP). La cause exacte côté `mss` reste non identifiée — cette garde **protège sans expliquer**.
- **Hypothèse** : seuil 200 px choisi par jugement, pas par mesure empirique sur l'éventail des dims légitimes. À relever si un faux rejet apparaît en prod.
## Ce que je recommande pour la suite
1. **Committer `finalize_contract.py` et les 5 fichiers modifiés** avant tout SCP. Un fichier untracked qui disparaît dans un `git stash` ou `git checkout` casserait silencieusement Léa au prochain démarrage. Un seul commit groupé `feat(agent): câblage contrat /finalize enrichi + garde dims monitor` aligne aussi les deux chantiers Horizon 1.
2. **Smoke test 5 min sur Léa Windows en suivant `docs/SMOKE_TEST_FINALIZE_REPLAY_2026-05-20.md`** avant tout autre développement. Risque P0 : tant qu'on n'a pas vu le flux complet `enregistrement → finalize → proposition → replay-session` tourner sur Léa réelle, on ne sait pas si le contrat serveur est bien aligné avec le câblage agent.
3. **Fixer les bugs P0 #1 et #3 (Mission 3) dans une mission courte dédiée** :
- `record_human_correction` (2 lignes : import + signature)
- `expected_window_before` fallback (1 ligne dans `api_stream.py:3636` : `action.get(...) or _tspec.get(...)` au lieu de `_tspec.get(...) or _tspec.get(...)`)
Ces 3 lignes débloquent l'apprentissage supervisé et la fallback mémoire, sans dépendre du run Windows.
4. **Reconsidérer le statut de P0 #2 (`build_workflow_replay` orphelin)** maintenant que le contrat `/finalize` enrichi expose `replay_request`. Hypothèse : le besoin du pont capture→replay-direct est résolu par le nouveau contrat. Si vrai → supprimer `workflow_replay.py` (code mort) ou le marquer explicitement obsolète. À trancher avant prochaine session sur le sujet.
5. **Ne pas attaquer le bug P0 #3 racine (window_title posé top-level au lieu de target_spec dans `stream_processor.py:1545`) sans confirmation produit du contrat mémoire**. La correction "rapide" (propager dans target_spec) marche mais n'élimine pas le doublon top-level / target_spec présent dans tout le code. Mieux : décider d'abord si la signature d'écran reste `sha256(window_title)` ou évolue (référence `last_window_info` au niveau session, par exemple).
---
**Conformité aux contraintes** : pas de réécriture historique projet, VWB mentionné une seule fois (recommandation 4, comparaison de pont), pas de proposition de refonte, factuel, court. Aucune modification de code dans cette mission handoff.

View File

@@ -0,0 +1,142 @@
# Handoff Horizon 1 — Lea-first
Date : 2026-05-20
Owner : Codex
Contexte : remise en ordre du coeur produit `Lea-first` en excluant `VWB` du chemin nominal court terme.
## 1. Ligne produit retenue
- Coeur produit court terme : `capture -> replay direct -> verification -> memory`
- `VWB` n'est pas la reference produit
- `TargetMemoryStore` est la boucle d'apprentissage utile pour Horizon 1
- La voie `shadow -> WorkflowIR -> ExecutionPlan` reste une direction moyen terme, pas la voie nominale actuelle
## 2. Tickets traites
### T1 — correction humaine -> mémoire persistante
Fichiers :
- `agent_v0/server_v1/replay_learner.py`
- `tests/unit/test_policy_grounding_recovery_learning.py`
Effet :
- `record_human_correction()` repersiste bien dans la mémoire cible
- fallback `window_title` amélioré
### T2 — pause/reprise replay côté agent
Fichiers :
- `agent_v0/agent_v1/core/executor.py`
- `agent_v0/agent_v1/main.py`
- `tests/unit/test_agent_v1_replay_pause_state.py`
Effet :
- `replay_paused=true` ne coupe plus prématurément le mode replay
### T3 — contrat `window_title` pour la mémoire
Fichiers :
- `agent_v0/server_v1/stream_processor.py`
- `agent_v0/server_v1/api_stream.py`
- `tests/unit/test_window_title_memory_path.py`
Effet :
- le flux Lea-first propage `window_title` jusque dans les chemins mémoire importants
### T4 — chaînage produit `finalize -> replay-session`
Fichiers :
- `agent_v0/server_v1/api_stream.py`
- `tests/integration/test_finalize_replay_chain.py`
Effet :
- `POST /api/v1/traces/stream/finalize` reste compatible
- expose maintenant `replay_ready` et `replay_request`
- peut lancer un replay direct avec `launch_replay=true`
- si le lancement échoue, la finalisation reste un succès exploitable
### T5 — intégration agent du contrat `finalize`
Fichiers :
- `agent_v0/agent_v1/network/streamer.py`
- `agent_v0/agent_v1/ui/smart_tray.py`
- `agent_v0/agent_v1/main.py`
- `agent_v0/agent_v1/finalize_contract.py`
- `tests/unit/test_agent_finalize_replay_contract.py`
- `tests/integration/test_client_server_compat.py`
Effet :
- `TraceStreamer` remonte le payload JSON de `/finalize`
- `AgentV1` route ce payload vers une logique légère dédiée
- le systray propose un test immédiat avec consentement humain
- le lancement de test passe par `POST /api/v1/traces/stream/replay-session`
- on ne réutilise pas `_launch_replay()` car cette méthode cible `/replay/start` avec `workflow_id`
## 3. Tests verts
- `pytest -q tests/unit/test_agent_v1_replay_pause_state.py`
- `pytest -q tests/unit/test_window_title_memory_path.py`
- `pytest -q tests/unit/test_agent_finalize_replay_contract.py`
- `pytest -q tests/integration/test_client_server_compat.py -k finalize`
- `pytest -q tests/integration/test_finalize_replay_chain.py`
- `pytest -q tests/unit/test_capturer_monitor_guard.py`
- `pytest -q tests/unit/test_target_memory_store.py`
- `pytest -q tests/unit/test_policy_grounding_recovery_learning.py -k 'ReplayLearner and human_correction'`
Note :
- le fichier complet `tests/unit/test_policy_grounding_recovery_learning.py` reste non portable localement à cause de `pynput`
## 4. Etat du worktree
Modifs runtime principales en cours :
- `agent_v0/agent_v1/core/executor.py`
- `agent_v0/agent_v1/main.py`
- `agent_v0/agent_v1/network/streamer.py`
- `agent_v0/agent_v1/ui/smart_tray.py`
- `agent_v0/agent_v1/vision/capturer.py`
- `agent_v0/server_v1/api_stream.py`
- `agent_v0/server_v1/replay_learner.py`
- `agent_v0/server_v1/stream_processor.py`
- `tests/integration/test_client_server_compat.py`
- `tests/unit/test_policy_grounding_recovery_learning.py`
Nouveaux fichiers runtime/tests :
- `agent_v0/agent_v1/finalize_contract.py`
- `tests/integration/test_finalize_replay_chain.py`
- `tests/unit/test_agent_finalize_replay_contract.py`
- `tests/unit/test_agent_v1_replay_pause_state.py`
- `tests/unit/test_capturer_monitor_guard.py`
- `tests/unit/test_window_title_memory_path.py`
Fichiers hors-scope a ne pas embarquer par reflexe :
- `visual_workflow_builder/backend/instance/workflows.db`
- docs d'audit/handoff/plan si on veut un commit runtime pur
- `.remember/*`
## 5. Recommandation de commit
Point de commit pertinent :
- un commit runtime/tests cohérent `Horizon 1 Lea-first`
Message suggéré :
- `lea-first: stabilize direct replay and finalize contract`
## 6. Prochain objectif
Ne pas ouvrir un nouveau chantier code tout de suite.
Priorité suivante :
- validation réelle du flux :
`fin enregistrement -> finalize -> proposition utilisateur -> replay-session`
Mission de lecture utile à déléguer :
- vérifier le déploiement Windows effectif des fichiers modifiés
- produire une checklist de smoke test réel de 5 minutes
## 7. Décision de reprise
Si nouvelle session :
- repartir à partir de ce handoff
- considérer le bloc `serveur + agent + tests finalize` comme base de référence
- ne pas revenir sur `VWB`
- passer en mode `validation réelle / déploiement / smoke test`

View File

@@ -0,0 +1,100 @@
Contexte
- Projet : `rpa_vision_v3`
- Poste Windows de démo : `dom@192.168.1.11`
- Léa Windows poll le serveur Linux `192.168.1.40:5005`
- Coordination Claude/Codex via `docs/coordination/`
Direction produit
- Ne pas dériver vers une simple “boîte à clic”.
- Cible produit : `capture brute -> post-traitement -> workflow différé compilé -> exécution supervisable`.
- Le `replay-session` direct reste utile pour smoke/debug, mais nest pas la voie produit finale.
- Les “réflexes” (`Enregistrer`, `Ouvrir`, `Copier`, etc.) doivent idéalement devenir des primitives robustes, pas des chorégraphies visuelles fragiles.
État code important déjà en place
- `finalize -> replay-session` validé.
- setup Windows durci par `verify_screen`.
- propagation `expected_window_before` côté serveur.
- relaxation ciblée du seuil pour `switch_tab`.
- garde drift `anchor-template`.
- handler runtime popup `Confirmer l'enregistrement -> Oui` ajouté côté agent dans `executor.py`.
- nouveau patch grounding côté agent :
- rejet des rects système / taskbar
- validation visuelle du crop fenêtre avant usage
- fallback plein écran si le crop ne matche pas visuellement
Derniers fichiers touchés
- `agent_v0/agent_v1/core/executor.py`
- `agent_v0/agent_v1/core/grounding.py`
- `tests/unit/test_executor_verify_window_guard.py`
- `tests/unit/test_policy_grounding_recovery_learning.py`
Validation locale récente
- `./.venv/bin/pytest -q tests/unit/test_executor_verify_window_guard.py`
- `./.venv/bin/pytest -q tests/unit/test_policy_grounding_recovery_learning.py -k GroundingEngine`
- `python3 -m py_compile agent_v0/agent_v1/core/executor.py`
- `python3 -m py_compile agent_v0/agent_v1/core/grounding.py tests/unit/test_policy_grounding_recovery_learning.py`
Déploiement Windows récent
- `C:\\rpa_vision\\agent_v1\\core\\executor.py` mis à jour
- `C:\\rpa_vision\\agent_v1\\core\\grounding.py` mis à jour
- `py_compile` Windows OK pour ces fichiers
Problème infrastructure découvert
- Le serveur Linux nutilise pas réellement le GPU actuellement.
- Symptômes :
- `nvidia-smi` échoue avec `Driver/library version mismatch`
- module noyau NVIDIA `580.126.09`
- libs userspace `580.159.03`
- PyTorch `.venv` : `cuda_available=False`
- Conséquence :
- une partie des résolutions vision reste CPU-bound
- les `Server resolve timeout` à 30s sont fortement amplifiés
- Correctif probable infra :
- reboot Linux pour réaligner driver chargé / libs
- sinon réparation driver NVIDIA
Replays live récents
- `replay_sess_ebb4d998`
- a servi à valider le besoin du handler popup overwrite
- `replay_sess_3c56b8f2`
- lancé avec mauvais `machine_id=bg_DESKTOP-58D5CAC_windows`
- non consommé par Léa
- `replay_sess_595c4947`
- lancé avec le bon `machine_id=DESKTOP-58D5CAC_windows`
- bien consommé par Léa
État exact du replay courant `replay_sess_595c4947`
- `status=running`
- `completed_actions=11/17`
- setup passé
- saisie `test` passée
- nouveau grounding visuel actif, preuve log :
- `Grounding fenêtre validé visuellement via 'test'`
- point de casse courant :
- action `act_raw_74e4e5ec` = clic onglet `Enregistrer sous`
- retry puis `som_anchor_match score=0.74`
- clic exécuté
- `POST-VÉRIF TIMEOUT : 'rpa_vision : Explorateur de fichiers' ≠ '*test - Bloc-notes'`
- action suivante `act_raw_022cb97c` se bloque immédiatement car fenêtre actuelle = `rpa_vision : Explorateur de fichiers`
- Léa repasse en apprentissage supervisé
Important
- Le handler popup overwrite `Oui/Non` na pas encore été réellement exercé sur ce run.
- Le blocage actuel est plus tôt : dérive `Enregistrer sous -> Explorateur`.
- Je nai pas de trace dun `Ctrl+C` dans ce replay courant.
Coordination Claude
- Brief ouvert :
- `docs/coordination/inbox_claude/2026-05-23_1024_codex-to-claude_notepad-saveas-explorer-drift.md`
- Réponse attendue dans :
- `docs/coordination/inbox_codex/`
Actions recommandées pour reprise
1. Lire le brief Claude ci-dessus et sa réponse si elle existe.
2. Vérifier si la machine Windows a bien redémarré Léa proprement.
3. Vérifier létat du replay courant `replay_sess_595c4947`.
4. Si le run est abandonné après reboot, relancer un nouveau `POST /replay-session` avec :
- `session_id=sess_20260520T102916_066851`
- `machine_id=DESKTOP-58D5CAC_windows`
5. Continuer le diagnostic uniquement sur la dérive `Enregistrer sous -> Explorateur`.
6. Garder en tête le problème infra GPU, mais ne pas le mélanger avec le bug fonctionnel de ce replay.

View File

@@ -0,0 +1,142 @@
# Handoff Codex — LeaBench / Grounding / Supervision equipe
Date : 2026-05-24 22:06 Europe/Paris
Auteur : Codex
Statut memoire Codex : `OK`, mais handoff ecrit pour reprise propre ou nouvelle session.
## Position projet
Lea ne doit pas etre une boite a clics. Direction maintenue :
`observe -> verifier preconditions -> agir -> stabiliser -> juger -> apprendre uniquement sur preuve saine`
Decisions non negociables :
- cloud models = analyse offline uniquement, apres anonymisation explicite ;
- runtime Lea reste controle par notre code ;
- aucun modele externe ne prend le controle direct de Windows ;
- corrections humaines et memory ne sont apprises que si coordonnees et contexte sont sains ;
- pas de branchement runtime experimental sans tests et rollback.
## Commits recents importants
- `debd7b423 feat(evaluation): add local Ollama LeaBench adapter`
- `6544ebe3f feat(evaluation): add 16 LeaBench cases from replay failures`
- `10136f0ee feat(agent): add standalone anchor-relative resolver`
- `054279feb feat(evaluation): add LeaBench model prompt packs`
- `ea1f57afb feat(evaluation): add LeaBench computer-use scorer`
- `345762330 fix(agent): respect server visual reject before text fallback`
- `b1b32187b fix(agent): P0.6 guard human corrections`
- `ad24d16d8 fix(executor): P0.9 double-check stabilité post-transition fenêtre`
## Etat code
LeaBench :
- `core/evaluation/computer_use_bench.py`
- `tools/lea_bench.py`
- `benchmarks/computer_use/cases/notepad_replay_failures_2026-05-24.jsonl`
- `benchmarks/computer_use/cases/leabench_extended_2026-05-24.jsonl`
- 4 cas Notepad initiaux + 16 cas etendus = 20 cas total.
- Les 16 cas etendus ont `screenshot_path` reels et sont valides.
Prompt packs :
- `--write-prompt-pack` ajoute a LeaBench.
- Le prompt pack ne fuite pas `expectation` ni `click_region`.
Adaptateur Ollama local :
- `core/evaluation/ollama_lea_bench_adapter.py`
- `tools/lea_bench_ollama.py`
- tests mockes dans `tests/unit/test_ollama_lea_bench_adapter.py`
- aucun run live Ollama n'a ete lance apres implementation, pour eviter contention GPU/Ollama pendant Lea.
Anchor relative :
- `agent_v0/agent_v1/core/anchor_relative.py`
- `agent_v0/agent_v1/core/anchor_catalog.py`
- `tests/unit/test_anchor_relative.py`
- Phase 1 standalone acceptee et committee.
- Pas de branchement `executor.py`.
- Garde Codex ajoute : `target_out_of_bounds`.
## Verifications passees
Commandes importantes deja passees :
```bash
python3 -m pytest -q tests/unit/test_computer_use_bench.py tests/unit/test_ollama_lea_bench_adapter.py tests/unit/test_anchor_relative.py
python3 -m py_compile core/evaluation/ollama_lea_bench_adapter.py tools/lea_bench_ollama.py
python3 tools/lea_bench.py --cases benchmarks/computer_use/cases/notepad_replay_failures_2026-05-24.jsonl --repo-root . --json
python3 tools/lea_bench.py --cases benchmarks/computer_use/cases/leabench_extended_2026-05-24.jsonl --repo-root . --json
```
Resultats :
- 25 tests verts sur les 3 modules.
- 4 cas Notepad valides.
- 16 cas etendus valides.
- baseline all-abstain sur 16 cas : 10/16 correct, 0 dangereux.
## Coordination lue
Claude :
- `2026-05-24_2230_claude-to-codex_qwen-leabench-prompt-spec.md`
- `2026-05-24_2230_claude-to-codex_anchor-relative-phase1-result.md`
- `2026-05-24_2230_claude-to-codex_leabench-cases-enrichis.md`
Gemini :
- `2026-05-24_2315_gemini-to-codex_leabench-adapters-and-cases-v1.md`
- `2026-05-24_2345_gemini-to-codex_cloud-adapters-privacy-spec.md`
- `2026-05-24_2355_gemini-to-codex_continual-learning-alignment.md`
Messages envoyes :
- `docs/coordination/inbox_claude/2026-05-24_2206_codex-to-claude_memory-health-check-and-handoff.md`
- `docs/coordination/outbox_gemini/2026-05-24_2206_codex-to-gemini_memory-health-check-and-handoff.md`
- `docs/coordination/outbox_gemini/2026-05-24_2208_codex-to-gemini_quality-frame-source-discipline.md`
## Points de vigilance
Deploiement Léa Windows :
- Verifie le 2026-05-24 vers 22:18 : `C:\rpa_vision\agent_v1\core\executor.py` date du 24/05 20:24, taille 176068 bytes.
- Marqueurs absents cote Windows : `skip_text_fallback_after_server_reject`, `drain_guard_s`, `below_threshold`.
- Conclusion : `b1b32187b` P0.6 et `345762330` R1 ne sont pas deployes cote client Windows.
- Ne pas relancer de replay live avant backup + SCP du `agent_v0/agent_v1/core/executor.py` local actuel vers Windows, puis relance Lea.
Gemini cloud/privacy :
- Ne pas accepter comme verite les IDs API `gpt-5.5`, `claude-opus-4.7`, etc. sans verification officielle.
- Distinguer vision offline vs computer-use natif vs runtime Lea.
- Ne pas coder d'adaptateur cloud maintenant.
- Regle imposee a Gemini : chaque claim doit etre classe `SOURCE_OFFICIELLE_VERIFIEE`, `SOURCE_PRIMAIRE_NON_OFFICIELLE`, `HYPOTHESE`, `PROPOSITION_PROJET` ou `A_VERIFIER`.
- Toute mention de modele/API/benchmark sans source officielle reste non decision-ready.
GPU/Ollama :
- Ne pas lancer `tools/lea_bench_ollama.py` si Lea est active.
- Lancer uniquement quand Dom confirme que Lea est idle.
Git :
- Beaucoup de docs non suivis dans `docs/`, `docs/coordination/`, `docs/recherche/`.
- Ne pas nettoyer/revert.
- Les commits techniques recents sont propres ; les docs coordination restent souvent untracked volontairement.
## Prochaine action recommandee
Si Dom confirme que Lea est idle :
```bash
python3 tools/lea_bench_ollama.py \
--cases benchmarks/computer_use/cases/leabench_extended_2026-05-24.jsonl \
--repo-root . \
--model qwen2.5vl:7b-rpa \
--output benchmarks/computer_use/predictions/qwen25vl_extended_2026-05-24.jsonl
python3 tools/lea_bench.py \
--cases benchmarks/computer_use/cases/leabench_extended_2026-05-24.jsonl \
--repo-root . \
--predictions benchmarks/computer_use/predictions/qwen25vl_extended_2026-05-24.jsonl \
--json
```
Sinon :
- attendre les reponses memory-health de Claude et Gemini ;
- preparer Phase 2 `GroundingGuard` sur design/test plan uniquement ;
- ne pas brancher runtime avant arbitrage.
## Resume court pour reprise
On a stabilise l'axe evaluation avant de continuer a corriger au feeling. LeaBench existe, il a 20 cas reels, un prompt pack neutre, et un adaptateur Ollama local teste sans GPU. `anchor_relative` existe en Phase 1 standalone. Prochaine decision : lancer Qwen local quand Lea est idle, puis utiliser le score pour decider quoi brancher dans `GroundingGuard`.

View File

@@ -0,0 +1,127 @@
# Handoff Claude — Phase 2 Notepad live réussi, passage session fraîche
- **Date** : 2026-05-25 09:10
- **Auteur** : Claude (session 18h→09h, ~15h continue)
- **Contexte** : live Bloc-notes `replay_sess_e96e5822` réussi 18/18, dialog handler runtime validé en prod. Dom demande session fraîche avant Phase 2 wiring.
## État mémoire honnête (au moment du handoff)
**Solide** :
- Modèle Mandat/Protocoles/Scènes v0.3 (vocabulaire complet, boucle 8 étapes, contrat d'action)
- 4 découvertes structurantes (D1 source/deploy, D2 expected_state mort, D3 single in-flight, D4 LearningManager dormant)
- 3 dataclasses créées hier (Trace, SceneExpected, Precondition+Recovery) — commit `7bb8d543a`
- Discipline rollback (tags, .bak Windows, sshpass `'loli'`)
- Contraintes Codex (4 fichiers en lecture seule)
- Pipeline de coordination (inbox_claude/inbox_codex)
**Friction qui commence** :
- Précision sur les `file:line` exacts (re-grep nécessaire)
- Détail diff par diff des 14 commits
- Détails fins des 7 tests manquants WP4
**Recommandation Claude** : session fraîche maintenant = la bonne décision. Confiance baisse sur les détails fins après ~15h continue.
## Décisions conceptuelles à conserver
1. **Léa est mandatée, pas commandée** : reçoit une fin, choisit le chemin, qualifie le retour.
2. **Un protocole est une grammaire d'action autour d'une intention** (≠ workflow scripté).
3. **Autonomie d'initiative, pas d'entêtement** + délégation tutorée 5 niveaux (N0 observation → N4 autonomie habituelle, contextuel selon protocole/app/client/tuteur/période).
4. **Précondition = état attendu vérifiable avant l'action** (formulation Dom).
5. **Priorité protocoles** : mieux connu → moins risqué → plus court.
6. **Apprentissage uniquement sur résultat qualifié** ; désapprentissage gradué (réduire confiance / restreindre périmètre / quarantaine / supprimer).
7. **Méta-cognition minimale** : 4 critères agrégés (score / ancre / scène / prédiction) → AGIR / DEMANDER / ABSTENTION.
8. **Rejet sémantique domine fallbacks opportunistes** (règle d'or — déjà gagnée pour close_tab via commit `345762330`).
## Workpacks produits ce matin (à intégrer)
| ID | Sujet | Livrable inbox_codex | Conclusion clé |
|---|---|---|---|
| WP1 (0905) | Inventaire source vs deploy | `2026-05-25_0905_WP1-inventaire-source-deploy.md` | **Risque CRITIQUE** : 9 fichiers divergents, 50% codebase deploy obsolète de 20j. NE PAS RESYNC avant démo. Porter d'abord `monitor_resolution` deploy→source. |
| WP2 (0905) | SceneExpected wiring | `2026-05-25_0905_WP2-scene-expected-wiring.md` | Point injection : `_attach_scene_expected()` dans stream_processor.py l.1368. Plan B (hors zone interdite Codex) recommandé passe 1. Flag `RPA_SCENE_EXPECTED` OFF. |
| WP3 (0905) | expected_state → Precondition | `2026-05-25_0905_WP3-expected-state-precondition.md` | Mapping Notepad : `Precondition(window_title_must_contain=["Sans titre","Untitled"])` + `Recovery(Ctrl+N, wait 400ms)`. Nouveau module isolé `precondition_inference.py`. Flag `RPA_PRECONDITION_INFER`. |
| WP4 (0905) | Single in-flight tests + factorisation | `2026-05-25_0905_WP4-single-inflight-tests-factorisation.md` | Helper `_find_in_flight_action()` remplace blocs api_stream.py:3074-3095 et 3138-3159. 3 tests critiques (1 vert, 1 xfail attendu, 1 limitation Q7). |
## Documents stratégiques à relire au redémarrage
Dans cet ordre :
1. `docs/architecture/MODELE_MANDAT_PROTOCOLS_LEA_2026-05-25_v0.3_ARBITRAGES_DOM.md` — modèle de base
2. `docs/architecture/CARTOGRAPHIE_BRIQUES_MANDAT_PROTOCOLS_2026-05-25.md` — carto initiale Codex
3. `docs/plans/PLAN_PHASE2_TRACE_MANDAT_PROTOCOLS_2026-05-25.md` — plan d'exécution Phase 2
4. `docs/coordination/inbox_codex/2026-05-25_0626_claude-to-codex_rapport-complet-phase-attente.md` — vue d'ensemble pour reprise
5. `docs/coordination/inbox_codex/2026-05-25_0905_claude-to-codex_WP{1,2,3,4}*.md` — les 4 derniers WP
6. `docs/coordination/inbox_claude/2026-05-25_0855_codex-to-claude_live-notepad-success-speed-followup.md` — preuve live + 3 axes follow-up
## Commits Phase 2 / cognition (état branche `backup/post-demo-2026-05-19`)
```
7bb8d543a feat(cognition): dataclasses Trace + SceneExpected + Precondition (Phase 2.1) ← Claude
[commits Codex Phase 2.0 grounding + précondition Notepad + single in-flight]
debd7b423 feat(evaluation): add local Ollama LeaBench adapter ← Codex
6544ebe3f feat(evaluation): add 16 LeaBench cases from replay failures ← Claude
10136f0ee feat(agent): add standalone anchor-relative resolver ← Claude
054279feb feat(evaluation): add LeaBench model prompt packs ← Codex
ea1f57afb feat(evaluation): add LeaBench computer-use scorer ← Codex
345762330 fix(agent): respect server visual reject before text fallback ← Codex
b1b32187b fix(agent): P0.6 guard human corrections ← Codex
ad24d16d8 fix(executor): P0.9 double-check stabilité post-transition fenêtre ← Claude
a76f3db68 feat(executor): P1 DialogResolver serveur en fallback ← Claude
9a029a221 fix(executor): timeout 120→30s ← Claude
5ed1810ef fix(memory): rejeter coords (0,0) et hors [0,1] ← Claude
c9878f0a7 fix(validator-v2): override success=False uniquement sur TERMINATE
```
Tags rollback récents : `rollback/pre-cognition-dataclasses-2026-05-25_0610`, et tous les tags P0.x/P1 d'hier.
## Risques restants
1. **Désynchronisation source/deploy** (D1) : critique, ne pas resync avant démo. Porter `monitor_resolution` deploy→source en priorité quand Codex libère executor.py.
2. **Race lock 3349/3470** (D3) : commentaire "race bénigne" accepte le double dispatch. Faible risque démo (Léa unique), à traiter post-démo.
3. **`expected_state` mort** (D2) : produit par gemma4 mais jamais lu. WP3 propose le wiring sans gros refactor.
4. **autonomous_planner orphelin** (D4) : 1019 LOC, endpoints HTTP 410. À archiver post-démo.
5. **Fixtures `replay_failures/` non versionnées** : si purgé, LeaBench casse. Décision Dom : versionner post-démo.
## Ce que Claude doit faire dans la prochaine session
1. **Relire les 6 documents stratégiques ci-dessus** (15 min)
2. **Vérifier l'état d'avancement Codex** sur le test live `replay_sess_e96e5822` + 3 axes follow-up (perf, trimming, test offline)
3. **Attendre arbitrage Codex** sur l'ordre d'intégration des 4 WP avant tout patch
4. **Respecter les 4 fichiers en lecture seule** : `executor.py`, `api_stream.py`, `replay_engine.py`, `grounding.py` (tant que Codex n'a pas explicitement libéré)
5. **Continuer la discipline rollback** : tag avant chaque modif, commit atomique, tests verts avant
6. Si patch validé par Codex : utiliser sshpass `SSHPASS='loli' sshpass -e scp/ssh dom@192.168.1.11` pour SCP Windows
## Ce que Claude ne doit PAS faire
- ❌ Modifier runtime sans accord explicite Codex
- ❌ Toucher aux 4 fichiers interdits tant que Codex pilote
- ❌ Réveiller `autonomous_planner`, `ORALoop`, `LearningManager` (modules dormants identifiés)
- ❌ Refactor `report_action_result` (700 lignes, 4 écritures, fragile)
- ❌ Refactor `resolve_engine._resolve_target_sync` (cascade VLM)
- ❌ Resync brutale source/deploy avant démo
- ❌ Activer `DialogResolver` serveur en live alors qu'il est déjà OK côté Léa
- ❌ Modifier schéma `target_memory.db` (5 backups en prod)
- ❌ Spawner d'agents pour du brainstorming si question est ciblée (overkill — réservé aux explorations larges et workpacks parallèles)
## Infos opérationnelles
- **Démo DPI** : `http://192.168.1.40:8765/` (Easily, pour tests futurs post-Bloc-notes)
- **Léa Windows** : 192.168.1.11, SSH password `loli`, exec via sshpass
- **rpa-streaming** : systemd user sur Linux, port 5005
- **Branche** : `backup/post-demo-2026-05-19`
- **DB target_memory** : `/home/dom/ai/rpa_vision_v3/data/learning/target_memory.db` (25 entrées valides, 0 poison (0,0))
## Inventaire complet des messages coordination 2026-05-24 → 2026-05-25
**inbox_claude/** (de Codex à Claude) : 25 messages depuis 2026-05-23 (le vieux `notepad-saveas-explorer-drift` reste open en option B, classé non-actionable).
**inbox_codex/** (de Claude à Codex) : ~30 messages depuis 2026-05-23.
Trop pour les lister, mais les plus récents (24h) :
- 13 messages de 2026-05-24 18h → 22h (P0.6 à P1, R1, anchor_relative, LeaBench cases, Qwen spec, GroundingGuard, observations Dom)
- 12 messages de 2026-05-25 02h → 09h (brainstorming Mandat/Protocoles v0.2/v0.3, cartographies A1-A4 + carto complémentaire, dataclasses cognitives, 4 WP, rapport phase attente)
---
**Statut handoff** : prêt pour redémarrage session fraîche. Bonne reprise.
Auteur : Claude

View File

@@ -0,0 +1,261 @@
# Handoff Codex — Notepad success / Phase 2 cognitive
Date : 2026-05-25 09:04 Europe/Paris
Auteur : Codex
Statut memoire Codex : OK pour supervision, mais session fraiche recommandee avant nouvelle phase de programmation lourde.
## TL;DR
Le test live Bloc-notes est passe de bout en bout.
- Replay : `replay_sess_e96e5822`
- Source : `sess_20260520T102916_066851`
- Machine : `DESKTOP-58D5CAC_windows`
- Resultat live : `18/18`, `0` echec, `0` retry, `0` non-verifie
- Point critique valide : dialogue Windows `Confirmer l'enregistrement` absorbe sans aide humaine
- Signal cle : `warning='runtime_dialog_handled_post_verify'`
Conclusion produit : le scenario n'est plus seulement une boite a clic. Lea a reussi a poursuivre son intention d'enregistrement malgre un dialogue systeme connu.
## Position projet a conserver
Lea doit etre pilotee comme un collaborateur visuel :
`mandat -> intention -> scene attendue -> precondition -> action -> retour observe -> apprentissage sain`
Principes non negociables :
- Ne pas traiter Lea comme un replay de pixels.
- Verifier l'etat reel avant les gestes sensibles.
- Ne pas apprendre depuis un succes douteux ou une correction humaine parasite.
- Les dialogues connus doivent etre absorbes dans la fenetre active, avec verification que le dialogue disparait.
- Les dialogues inconnus doivent produire du doute type et une demande d'aide, pas un clic opportuniste.
- Claude doit etre delegue largement avant toute phase longue ; Codex garde supervision, arbitrage, integration et live.
Directive persistante ajoutee :
- `docs/coordination/CODEX_MEMO_STRATEGIE_SUPERVISION_2026-05-24.md`
## Travail realise depuis le precedent handoff
### Correctifs runtime / tests locaux
Fichiers modifies cote Codex :
- `agent_v0/agent_v1/core/executor.py`
- `agent_v0/agent_v1/core/grounding.py`
- `agent_v0/server_v1/api_stream.py`
- `agent_v0/server_v1/replay_engine.py`
- `tests/integration/test_replay_session_trim_neutral.py`
- `tests/unit/test_env_setup.py`
- `tests/unit/test_executor_verify_window_guard.py`
- `tests/unit/test_grounding_engine.py`
Points fonctionnels :
- Grounding : respecte les rejets serveur explicites, notamment les rejets `close_tab_*`, `drift_*`, `below_threshold`.
- Replay setup Notepad : ouverture d'un document vierge avec `Ctrl+N` et verification de scene Bloc-notes.
- API replay : garde single in-flight partielle pour eviter plusieurs actions concurrentes.
- Executor Windows : handler de dialogue runtime connu revu pour travailler dans la fenetre active, cliquer le bouton attendu, puis verifier que la fenetre a change.
Tests passes :
```bash
.venv/bin/python -m py_compile agent_v0/agent_v1/core/executor.py tests/unit/test_executor_verify_window_guard.py
.venv/bin/python -m pytest -q tests/unit/test_executor_verify_window_guard.py
```
Resultat : tests `test_executor_verify_window_guard.py` verts.
Note : `python3 -m pytest` systeme echoue car `pynput` absent ; utiliser `.venv/bin/python`.
### Deploiement Windows
Deploiements effectues vers Lea Windows :
- `C:\rpa_vision\agent_v1\core\grounding.py`
- `C:\rpa_vision\agent_v1\core\executor.py`
Verifications :
- `python -m py_compile C:\rpa_vision\agent_v1\core\executor.py` OK cote Windows.
- `grounding.py` egalement compile OK apres SCP precedent.
- Lea a ete relancee par Dom avant le live test.
Attention : SSH a ensuite echoue avec `Too many authentication failures`; ne pas supposer que SSH est disponible sans corriger l'auth.
## Resultat live important
Ancien replay annule proprement :
- `replay_sess_37dd7cdf` -> `cancelled`
- verification : `active=0`
Nouveau replay propre :
- `replay_sess_e96e5822`
- lance apres redemarrage Lea et focus ecran 1
- termine avec succes selon logs :
- `Replay replay_sess_e96e5822 termine avec succes : 18/18 actions`
- metriques : `4 resolves [anchor_template=1, grounding_vlm=1, semantic_close_tab_hotkey=1, som_anchor_match=1] score_moy=0.94 temps_moy=10755ms`
Point critique :
- action : `act_raw_a8dbaaac`
- intention : enregistrer le document dans `Enregistrer sous`
- dialogue apparu : `Confirmer l'enregistrement`
- serveur OCR n'a pas trouve `Oui` via OCR-only
- agent/runtime a quand meme gere le dialogue
- report :
- `success=True`
- `warning='runtime_dialog_handled_post_verify'`
- `resolution_method='anchor_template'`
Interprétation :
- La brique "dialogue connu dans fenetre active + verification post-clic" fonctionne sur le cas Notepad.
- Le comportement doit maintenant devenir un test offline, pas rester une anecdote live.
## Problemes observes pendant le live
### Performance
Le demarrage et l'execution sont trop lents.
Observations :
- Environ 60 s entre preparation et demarrage effectif.
- Premier dispatch double :
- deux `DISPATCH act_setup_sess_open_run` quasiment simultanes.
- puis `dispatch_orphan_resent` a 50 s.
- Resolution moyenne loggee : environ `10755ms` par resolve.
Priorite prochaine : mesurer separement build replay, dispatch, attente report agent, resolve VLM/OCR/template.
### Single in-flight incomplet
Le live confirme les objections Claude :
- double dispatch initial encore visible ;
- la garde single in-flight ne couvre pas toute la fenetre de race ;
- ajouter tests avant refactor.
WP4 Claude recommande :
- tests d'abord ;
- helper `_find_in_flight_action(session_id, machine_id, replay_id)`;
- test concurrent probablement `xfail` tant que la race n'est pas corrigee proprement.
### Trimming hors objectif
Le replay a termine une derniere action `ouvrir le lien vers le dossier specifie`.
Ce n'est pas un echec du live, mais c'est hors mandat "saisir et enregistrer un texte".
Conclusion : le trimming doit etre pilote par objectif/intention, pas seulement par fin brute du recording.
### Source/deploy a clarifier
Claude WP1 signale une divergence massive entre :
- `agent_v0/agent_v1/`
- `agent_v0/deploy/windows_client/agent_v1/`
Mais le live valide semble utiliser `C:\rpa_vision\agent_v1\...`.
Ne pas lancer de resync massif avant verification du chemin reel d'execution Windows.
Action prochaine :
- confirmer le `CommandLine` du process Windows ;
- exposer ou logger un `AGENT_VERSION` avec hash/timestamp ;
- documenter une politique unique de deploiement.
## Coordination lue / envoyee
Retours Claude lus :
- `docs/coordination/inbox_codex/2026-05-25_0626_claude-to-codex_rapport-complet-phase-attente.md`
- `docs/coordination/inbox_codex/2026-05-25_0640_claude-to-codex_carto-complementaire-agent_chat-WM-ORA.md`
- `docs/coordination/inbox_codex/2026-05-25_0610_claude-to-codex_synthese-workpacks-A-B-C.md`
- `docs/coordination/inbox_codex/2026-05-25_0905_claude-to-codex_WP1-inventaire-source-deploy.md`
- `docs/coordination/inbox_codex/2026-05-25_0905_claude-to-codex_WP2-scene-expected-wiring.md`
- `docs/coordination/inbox_codex/2026-05-25_0905_claude-to-codex_WP3-expected-state-precondition.md`
- `docs/coordination/inbox_codex/2026-05-25_0905_claude-to-codex_WP4-single-inflight-tests-factorisation.md`
Messages Codex envoyes a Claude :
- `docs/coordination/inbox_claude/2026-05-25_0845_codex-to-claude_delegation-max-phase2-supervision.md`
- `docs/coordination/inbox_claude/2026-05-25_0855_codex-to-claude_live-notepad-success-speed-followup.md`
- `docs/coordination/inbox_claude/2026-05-25_0904_codex-to-claude_handoff-session-fraiche.md`
Claude doit produire son handoff miroir dans `docs/handoffs/`.
## Arbitrage courant
Ordre recommande pour prochaine session :
1. Lire ce handoff puis lire le handoff Claude miroir.
2. Verifier `git status` et ne rien revert.
3. Figer le succes live par un test offline minimal sur `runtime_dialog_handled_post_verify`.
4. Traiter performance/dispatch :
- ajouter les tests WP4 critiques ;
- factoriser seulement apres tests ;
- mesurer build/dispatch/resolve.
5. Ensuite seulement brancher transport inerte `SceneExpected` / `Precondition` cote serveur, flag OFF par defaut.
6. Ne pas activer de blocage runtime `scene_expected.required=true` tant que les tests et le chemin Windows reel ne sont pas clarifies.
Ce qu'il ne faut pas faire au redemarrage :
- Pas de resync massif `deploy/windows_client` avant verification.
- Pas de refactor large `api_stream.report_action_result`.
- Pas de reveil `autonomous_planner`, `ORALoop`, `LearningManager`.
- Pas de nouveau replay live avant d'avoir decide si on mesure performance ou si on teste une correction.
- Pas de commit composite melangeant runtime, docs et experiences.
## Git / etat worktree
Worktree sale attendu. Ne pas nettoyer automatiquement.
Modifies techniques connus :
- `agent_v0/agent_v1/core/executor.py`
- `agent_v0/agent_v1/core/grounding.py`
- `agent_v0/server_v1/api_stream.py`
- `agent_v0/server_v1/replay_engine.py`
- `tests/integration/test_replay_session_trim_neutral.py`
- `tests/unit/test_env_setup.py`
- `tests/unit/test_executor_verify_window_guard.py`
- `tests/unit/test_grounding_engine.py`
Nombreux docs non suivis dans `docs/`, `docs/coordination/`, `docs/recherche/`, `docs/plans/`, `docs/architecture/`.
Ne pas les supprimer ; ce sont des artefacts de coordination et recherche.
## Commandes utiles
Lister replays actifs :
```bash
TOKEN=$(grep '^RPA_API_TOKEN=' .env.local | cut -d= -f2- | tr -d "'\"")
curl -sS -H "Authorization: Bearer $TOKEN" \
http://localhost:5005/api/v1/traces/stream/replays |
python3 -c "import sys,json; d=json.load(sys.stdin); active=[r for r in d.get('replays', []) if r.get('status') in ('running','paused_need_help','busy')]; print('active=', len(active)); [print(r.get('replay_id'), r.get('status'), r.get('completed_actions'), '/', r.get('total_actions')) for r in active]"
```
Logs live Notepad :
```bash
journalctl --user -u rpa-streaming --since '2026-05-25 08:50:00' --no-pager |
rg "e96e5822|runtime_dialog|Confirmer|REPORT|VERIFY|dispatch_orphan|termine"
```
Tests unitaires executor :
```bash
.venv/bin/python -m pytest -q tests/unit/test_executor_verify_window_guard.py
```
## Resume court pour reprise
Le live Bloc-notes a reussi. Le dialogue `Confirmer l'enregistrement` est absorbe sans humain, ce qui valide la correction fenetre active/dialogue connu. Les deux vrais chantiers suivants sont maintenant la fiabilite du dispatch et la vitesse. Le modele cognitif reste : objectif explicite, scene attendue, precondition, action, retour. Ne pas repartir dans une rustine de clic ; transformer le succes en test, puis stabiliser la chaine.

View File

@@ -0,0 +1,262 @@
# Handoff Codex — Demo Aiva-urgence v2 / repetition humaine
- `Auteur`: Codex
- `Date`: 2026-05-26, fin de journee
- `Reprise visee`: 2026-05-27
- `Contexte`: preparation demo client Aiva-vision / Aiva-urgence, repetition avec Dom comme humain challenge
## 1. Decision produit du jour
Produit clarifie :
- `Aiva-vision` est le socle universel qui apprend les interfaces.
- `Léa` est le collaborateur agent qui observe, lit, agit, demande et apprend.
- `Aiva-urgence` est le plugin metier de la demo sante.
Principe Dom acte :
- qualite d'abord ;
- le client sait que nous sommes en POC ;
- en contexte hospitalier, un arret propre vaut mieux qu'une action fausse ;
- Léa doit savoir dire "je ne sais pas" ou "montrez-moi".
## 2. Scenario demo retenu
Scenario cible :
1. Léa lit la liste des passages aux urgences.
2. Elle decrit le tableau sans enumerer fragilement tous les IPP.
3. Elle propose : traiter tous les dossiers ou un dossier precis.
4. Dom choisit `MOREL Catherine / IPP 25003284`.
5. Léa ouvre le dossier et verifie le bandeau : `25003284`, `MOREL`, `Catherine`.
6. Léa collecte les 5 onglets :
- `Motif d'admission`
- `Examens cliniques`
- `Imagerie`
- `Notes medicales`
- `Synthese Urgences`
7. `Synthese Urgences` reste dans le perimetre live : lecture haut + bas, avec validation par marqueurs.
8. Léa demande ou consigner les informations.
9. Sortie prioritaire : tableur OnlyOffice visible.
10. Validation finale humaine obligatoire.
## 3. Scroll : arbitrage corrige
Dom a rappele que VWB n'avait pas eu de probleme de scroll.
Verification code :
- VWB et replay utilisent le contrat `extract_text_scroll` :
- OCR haut ;
- `ctrl+end` ;
- wait ;
- OCR bas ;
- concat ;
- retour `ctrl+home`.
Correction d'arbitrage :
- ne plus retirer `Synthese Urgences` par prudence abstraite ;
- cible live = 5 onglets ;
- degrade 4 onglets uniquement si echec concret non recupere.
Marqueurs obligatoires bas synthese :
- `CCMU`
- `GEMSA`
- `J12.1`
- `Consultation externe`
Principe qualite :
> Scroll reussi = geste envoye + changement visuel constate + donnees attendues relues.
Docs :
- `docs/coordination/active/2026-05-26_arbitrage-scroll-vwb-reference.md`
- `docs/coordination/active/2026-05-26_principe-apprentissage-scroll-securise.md`
## 4. Repetition humaine du 2026-05-27
Dom sera "l'humain challenge".
Il peut :
- interrompre ;
- refuser une lecture ;
- demander une preuve ;
- demander une reprise depuis un onglet ;
- demander OnlyOffice ;
- verifier que Léa ne conclut pas sans validation.
Regle orale Claude a retenir :
> Une reponse Léa doit toujours contenir une preuve, une question ou un arret. Jamais une affirmation seule.
Docs a relire avant repetition :
- `docs/coordination/active/2026-05-26_runbook-repetition-humain-challenge-demo-v2.md`
- `docs/coordination/inbox_codex/2026-05-26_2230_claude-to-codex_SCRIPT-oral-lea-humain-challenge.md`
- `docs/coordination/active/2026-05-26_etat-preparation-repetition-2026-05-27.md`
## 5. OCR IPP/chiffres
Benchmark local :
- Tesseract lit les 11 IPP exacts sur `landing_wide.png`.
- EasyOCR reste bon pour le texte continu, mais moins fiable sur les IPP secondaires.
- Preprocessing OpenCV global rejete : regressions marqueurs et latence.
- docTR utile pour structure/bboxes, pas meilleur que Tesseract pour les chiffres.
Patch implemente :
- `core/llm/ocr_extractor.py`
- ajout `extract_digits_tesseract_from_image(...)` ;
- ajout `engine` sur `extract_table_from_image(...)`.
- `core/llm/__init__.py`
- export de la nouvelle fonction.
- `agent_v0/server_v1/replay_engine.py`
- `extract_table` transmet maintenant `engine` ;
- le normaliseur accepte `variable_name` pour `extract_table`.
- `tests/unit/test_ocr_extractor_tesseract.py`
- tests unitaires nouveaux.
- `tests/integration/test_t2a_extract.py`
- test integration `variable_name` + `engine="tesseract"`.
Workflow raccorde :
- BDD : `visual_workflow_builder/backend/instance/workflows.db`
- Workflow : `Demo_urgence_3_db`
- ID : `wf_483910cdd851_1778750587`
- Step : `step_79c40f5a8342_1778750587`
- Modification : `parameters.engine = "tesseract"` sur `extract_table`.
Backup BDD :
- `visual_workflow_builder/backend/instance/workflows.db.backup_2026-05-26_ocr_tesseract_demo3`
Doc :
- `docs/coordination/active/2026-05-26_patch-ocr-tesseract-ipp.md`
## 6. Verifications passees
Tests :
```bash
pytest -q tests/unit/test_ocr_extractor_tesseract.py tests/integration/test_t2a_extract.py
```
Resultat :
- OK
- 40 tests passes
Compile :
```bash
python3 -m compileall -q core/llm/ocr_extractor.py core/llm/__init__.py agent_v0/server_v1/replay_engine.py tests/unit/test_ocr_extractor_tesseract.py tests/integration/test_t2a_extract.py
```
Resultat : OK.
Verification capture :
```bash
python3 -c "from core.llm.ocr_extractor import extract_digits_tesseract_from_image; print(extract_digits_tesseract_from_image('output/playwright/easily_dryrun_2026-05-26/landing_wide.png', pattern=r'^25\\d{6}$'))"
```
Resultat : 11/11 IPP exacts.
Etat services au dernier check :
- `http://127.0.0.1:8765/` accessible.
- `http://127.0.0.1:5005/health` OK.
- `http://127.0.0.1:5004/api/status` OK.
- OnlyOffice : `/snap/bin/onlyoffice-desktopeditors`.
- Tesseract : `/usr/bin/tesseract`, langues `eng`, `fra`, `osd`.
## 7. Coordination
Qwen :
- a valide le principe scroll securise ;
- a valide l'architecture OCR multi-moteur par zone ;
- a recu les infos de patch et raccord workflow.
Claude :
- a retracte la recommandation 4 onglets ;
- cible 5 onglets confirmee ;
- a livre le script oral "humain challenge".
Derniers messages lus :
- `docs/coordination/inbox_codex/2026-05-26_2149_qwen-to-codex_ACK-apprentissage-scroll-securise.md`
- `docs/coordination/inbox_codex/2026-05-26_2215_claude-to-codex_ACK-scroll-vwb-reformulation-discours.md`
- `docs/coordination/inbox_codex/2026-05-26_2230_claude-to-codex_SCRIPT-oral-lea-humain-challenge.md`
## 8. Points d'attention demain
1. Commencer par lire les nouveaux retours dans `docs/coordination/inbox_codex/`.
2. Verifier les services avant repetition :
- maquette `8765` ;
- streaming `5005` ;
- agent chat `5004` ;
- OnlyOffice ;
- Tesseract.
3. Rejouer le scenario avec Dom comme challengeur, pas comme assistant de demo.
4. Logger les moments ou Léa :
- cite une preuve ;
- demande confirmation ;
- s'arrete ;
- reprend apres correction.
5. Verifier que `Synthese Urgences` est tentee en 5e onglet.
6. Si scroll incomplet :
- ne pas exploiter l'onglet ;
- annoncer la limite ;
- demander confirmation humaine.
7. Si divergence OCR sur IPP :
- ne pas trancher ;
- demander confirmation.
8. OnlyOffice doit etre visible avant validation finale.
## 9. Worktree
Le worktree est deja tres modifie par plusieurs chantiers/collaborateurs.
Ne pas revert les changements non lies.
Changements Codex de cette fin de journee a surveiller :
- `core/llm/ocr_extractor.py`
- `core/llm/__init__.py`
- `agent_v0/server_v1/replay_engine.py`
- `tests/unit/test_ocr_extractor_tesseract.py`
- `tests/integration/test_t2a_extract.py`
- `visual_workflow_builder/backend/instance/workflows.db`
- `visual_workflow_builder/backend/instance/workflows.db.backup_2026-05-26_ocr_tesseract_demo3`
- docs sous `docs/coordination/active/` et inbox Claude/Qwen crees le 2026-05-26 soir.
Attention : `agent_v0/server_v1/replay_engine.py` contenait deja d'autres modifications hors patch OCR dans le diff global. Ne pas les attribuer automatiquement au patch OCR.
## 10. Premiere action conseillee demain
Lire :
```bash
find docs/coordination/inbox_codex -maxdepth 1 -type f -printf '%TY-%Tm-%Td %TH:%TM %f\n' | sort | tail -20
```
Puis lancer un smoke :
```bash
curl -fsS http://127.0.0.1:8765/ >/tmp/aiva_easily_health.html
curl -fsS http://127.0.0.1:5005/health
curl -fsS http://127.0.0.1:5004/api/status
pytest -q tests/unit/test_ocr_extractor_tesseract.py tests/integration/test_t2a_extract.py
```
Ensuite repetition humaine avec Dom.
Auteur : Codex

View File

@@ -0,0 +1,141 @@
# Handoff Codex - Reprise micro-apprentissage Lea P0
Date: 2026-05-27 21:35
Reprise prevue: 2026-05-28 matin
Pilote: Codex
Participants: Dom, Qwen, Claude
## Etat de fin de soiree
La demo metier / VWB n'est plus l'axe principal immediat. Le travail est recentre sur le coeur d'apprentissage de Lea.
Decision centrale:
- Unite de travail: **competence courte verifiee**.
- Cette competence est une couche de classification / promotion au-dessus de la chaine existante Graph / FAISS / Shadow / Replay / Memoire.
- On ne cree pas une nouvelle chaine d'apprentissage.
Chaine existante a reutiliser:
- `core/pipeline/workflow_pipeline.py`
- `core/graph/graph_builder.py`
- `core/graph/node_matcher.py`
- `core/embedding/faiss_manager.py`
- `core/embedding/state_embedding_builder.py`
- `core/workflow/shadow_observer.py`
- `core/workflow/shadow_validator.py`
- `core/learning/target_memory_store.py`
- `agent_v0/server_v1/replay_memory.py`
- `agent_v0/server_v1/replay_learner.py`
- `agent_v0/server_v1/replay_verifier.py`
- `tools/session_cleaner.py`
- `agent_chat/gesture_catalog.py`
- `core/knowledge/ui_patterns.py`
## Fait aujourd'hui
- Capture clavier systeme corrigee pour `Win+S` / `key_combo`.
- Consolidation preserve maintenant `key_combo`.
- Session cleaner corrige: plus d'affichage `<built-in method keys...>`.
- Health technique OK: RAM/VRAM/swap/Ollama acceptables; seul risque residuel = VLM cold start.
- Dashboard Base de connaissances identifie comme point d'inventaire:
- 13 666 vecteurs FAISS,
- 63 sessions,
- 29 workflows,
- 28 reflexes natifs,
- 3 machines.
- Qwen et Claude ont ete remis dans la boucle via leurs inbox fichiers.
## Retours agents lus
Claude:
- `docs/coordination/inbox_codex/2026-05-27_1959_claude-to-codex_CONTRAT-competence-courte-verifiee-P0.md`
- `docs/coordination/inbox_codex/2026-05-27_2040_claude-to-codex_PLAN-P1-contrat-p0-message-warning.md`
- `docs/coordination/inbox_codex/2026-05-27_2123_claude-to-codex_ACK-correction-session-wins-existe.md`
Qwen:
- `docs/coordination/inbox_codex/2026-05-27_2010_qwen-to-codex_AVIS-corrige-reuse-lea-core-complete.md`
- `docs/coordination/inbox_codex/2026-05-27_2044_qwen-to-codex_INVENTAIRE-offline-competences-existantes.md`
- `docs/coordination/inbox_codex/2026-05-27_2055_qwen-to-codex_INVENTAIRE-offline-competences-existantes.md`
Correction importante:
- Le retour Qwen 20:55 disait que `sess_20260527T185155_98ad9a` n'existait pas.
- Verification Codex: elle existe bien.
- Corrections envoyees:
- `docs/coordination/inbox_qwen/2026-05-27_2122_codex-to-qwen_CORRECTION-session-wins-existe.md`
- `docs/coordination/inbox_claude/2026-05-27_2122_codex-to-claude_CORRECTION-session-wins-existe.md`
## Session P0 confirmee
Ne pas recapturer Win+S pour P0.
Chemins confirmes:
- `data/training/live_sessions/streaming_sessions/sess_20260527T185155_98ad9a.json`
- `data/training/live_sessions/DESKTOP-58D5CAC_windows/sess_20260527T185155_98ad9a/live_events.jsonl`
- `data/training/live_sessions/DESKTOP-58D5CAC_windows/sess_20260527T185155_98ad9a/shots/`
Objectif P0:
- extraire le segment propre: etat initial -> `Win+S` -> `Rechercher/SearchHost.exe`,
- couper les evenements apres postcondition,
- ignorer les clics systray/pythonw,
- creer `data/competences/observed/open_windows_search.yaml`,
- verifier sans promouvoir `stable`.
## Objectifs demain matin
1. Figer le segment propre P0.
2. Creer le premier YAML `open_windows_search.yaml` en `observed` ou `candidate`.
3. Ajouter un validateur leger du schema competence, sans runtime lourd.
4. Brancher `message_contract.py` en mode warning sur le premier chemin de pause.
5. Lancer les tests de non-regression:
- capture clavier / key_combo,
- session cleaner,
- message contract,
- validateur YAML si ajoute.
6. Rejouer supervise seulement si Dom donne GO runtime.
## Regles a ne pas casser
- Pas de coordonnees comme savoir durable.
- Pas de nouvelle chaine parallele.
- Pas de promotion stable depuis dashboard / FAISS seul.
- Pas d'inference methode depuis postcondition seule.
- `SearchHost.exe` prouve l'etat, pas la methode.
- Messages Lea visibles: toujours dire intention, attendu, vu, demande.
- VLM en fallback seulement si fenetre/process/OCR simple ne suffisent pas.
## Repartition demain
Codex:
- implementation minimale,
- tests,
- verification disque/runtime,
- integration des retours Qwen/Claude.
Qwen:
- finaliser l'inventaire offline exploitable,
- corriger son inventaire avec la session P0 confirmee,
- identifier les prochains candidats sans nouvelle capture.
Claude:
- verrouiller le contrat YAML minimal,
- guider l'integration `message_contract.py` en warning,
- verifier les invariants de promotion.
## Premiere action recommande demain
Commencer par une lecture rapide de ce handoff, puis ouvrir:
- `docs/coordination/inbox_codex/2026-05-27_2040_claude-to-codex_PLAN-P1-contrat-p0-message-warning.md`
- `docs/coordination/inbox_codex/2026-05-27_2044_qwen-to-codex_INVENTAIRE-offline-competences-existantes.md`
- `data/training/live_sessions/streaming_sessions/sess_20260527T185155_98ad9a.json`
Ensuite seulement: patch minimal.

View File

@@ -0,0 +1,468 @@
# Handoff Codex - fin de soiree 2026-06-01 - reprise POC bi-turbo
Date: 2026-06-01 soir, Europe/Paris.
Auteur: Codex.
Statut: handoff de reprise. Aucun commit.
Ce handoff complete et remplace pour la reprise le handoff plus ancien :
`docs/handoffs/2026-06-01_handoff_codex_p0-p1-lea-session-propre.md`.
## Message court pour la prochaine session
Dom arrete ce soir pour se reposer. Demain, reprise "bi-turbo", mais l'objectif reste court terme :
- POC presentable/testable rapidement.
- Consolider ce qui existe.
- Arreter de recoder des briques deja implementees.
- Utiliser Claude et Qwen activement.
- Codex ne code pas seul.
- Qwen reste quality gate anti-doublon.
- Claude execute les correctifs courts.
- Codex orchestre et valide.
## Decisions Dom a ne pas perdre
1. Ne pas repartir sur VWB comme produit.
2. POC court terme d'abord : capture humaine fiable, replay precis, apprentissage consolide, test humain.
3. L'audit global ne doit pas devenir une phase longue. Il sert a aller plus vite.
4. Regle anti-reimplementation globale projet, pas seulement apprentissage :
- chercher l'existant,
- raccorder/consolider si possible,
- remplacer seulement si l'existant est clairement insuffisant.
5. Apprentissage Lea :
- pas de validation exhaustive de longues sessions,
- auto-evaluation par repetition,
- convergence sur repetitions,
- questions uniquement en mode copilote / prise de main / apprentissage supervise,
- aucune question pendant observation passive.
6. Les humains sont imparfaits :
- hesitations,
- erreurs de clic,
- retours arriere,
- corrections,
- pauses,
- bruit ecran.
Lea doit distinguer action utile, parasite, correction, hesitation.
7. Confidentialite DPI :
- donnees DPI exploitables localement sur DGX / hopital car necessaires pour trancher,
- aucune information ne sort du milieu hospitalier,
- artefacts portables sans memoire patient, captures DPI brutes, identifiants patients,
- attention au risque de memorisation par modele/adapters/embeddings si export naif.
8. Portabilite importante :
- exporter/importer les reflexes, competences, schemas, detecteurs, mappings, plans d'action, metriques,
- reduire duree d'apprentissage sur nouvelles machines,
- ne pas exporter la memoire patient.
9. Ne pas laisser de "a surveiller plus tard" flou :
- si un risque peut couter 10x plus tard, mettre maintenant un garde-fou minimal,
- acceptable POC seulement si le risque est contenu et documente.
## Etat coordination au moment du handoff
Derniers messages importants a lire dans cet ordre demain :
1. `docs/coordination/inbox_codex/2026-06-01_qwen-to-codex-claude_LEVEE-GO-P1-SEMANTIQUE.md`
- Qwen confirme GO P1-SEMANTIQUE, conditionnel leve.
2. `docs/coordination/inbox_codex/2026-06-01_2240_claude-to-codex_LEVEE-GO-conditionnel-Qwen-P1-SEMANTIQUE.md`
- Claude livre executor + timeout OmniParser + tests.
3. `docs/coordination/inbox_codex/2026-06-01_qwen-to-codex_REVUE-GLOBALE-ANTI-DOUBLON-REBRANCHEMENT.md`
- Qwen confirme R6 worker queue, FAISS, graph, learning orphelins, plan P0/P1/P2.
4. `docs/coordination/inbox_codex/2026-06-01_2200_claude-to-codex_ACK-5-decisions-Dom-+-audit-chaine-apprentissage-debranchee.md`
- Claude confirme chaine apprentissage partiellement debranchee.
5. `docs/POC/AUDIT_CHAINE_APPRENTISSAGE_2026-06-01.md`
- Audit detaille de la chaine apprentissage.
6. `docs/coordination/inbox_claude/2026-06-01_2222_codex-to-claude_GO-EXECUTION-POC-court-terme-R6-semantique-learning.md`
- GO Claude sur P0/P1 court terme.
7. `docs/coordination/inbox_qwen/2026-06-01_2222_codex-to-qwen_ACK-verdict-global-et-role-quality-gate.md`
- Qwen confirme comme quality gate attendu.
Important : apres le dernier recadrage Dom sur "a surveiller plus tard", Qwen a confirme que P1-SEMANTIQUE contient maintenant les 5 garde-fous demandes.
## Statut par lot
### P0 revocation / agent registry
Statut: probablement OK POC, non commite.
Travail applique dans le worktree :
- `agent_v0/server_v1/agent_registry.py`
- `agent_v0/server_v1/api_stream.py`
- tests P0 autour revocation/replay/enroll.
Qwen a livre et confirme :
- `/register`, `/stream/event`, `/replay/next`, `/replay/result`, `/finalize`, `/replay-session` gardes/testes.
- Strict default/unknown quand registry non vide.
Tests annonces/constates plus tot :
- `30 passed, 1 xfailed`, puis Qwen a annonce `32 passed, 1 xfailed`.
Attention :
- `api_stream.py` avait deja un tres gros diff avant Codex/Qwen/Claude.
- Toute modification dans ce fichier doit etre chirurgicale.
### P1-PERSIST
Statut: GO conditionnel POC, pas prod.
Claude a livre :
- `core/competences/persist.py`
- endpoint `/api/v1/lea/competences/candidate/persist` en fin de `api_stream.py`
- tests unit/integration/security.
Qwen a classe :
- acceptable POC,
- prod a durcir.
Risques/restes :
- couplage token <-> machine_id pas implemente,
- rate limiter in-memory,
- idempotence 201 vs 200,
- audit failure rollback YAML recommande.
### P1-LEA-SHADOW
Statut: GO Qwen.
Claude a livre :
- `agent_chat/handlers/learn_action.py`
- `agent_chat/handlers/__init__.py`
- `agent_chat/state/`
- modifications `agent_chat/app.py`
- route `POST /api/learn/start`
- rebranchement bouton Windows vers `/api/learn/start`
- `agent_v0/agent_v1/network/lea_orchestrator_client.py`
- modifications UI `chat_window.py`, `smart_tray.py`.
Qwen avait mis NO-GO puis a leve :
- `machine_id` ajoute dans `SessionState` et payload persist,
- `datetime.now(timezone.utc)`,
- guard anti-CONFIRM sans nom,
- route `/api/learn/start`.
Point important Dom :
- questions seulement en prise de main / supervision,
- pas pendant observation passive.
### P1-SEMANTIQUE
Statut: GO Qwen confirme, conditionnel leve.
Claude a livre puis corrige :
- `core/semantic/phase25_analyzer.py`
- `core/semantic/__init__.py`
- endpoint `/api/v1/lea/screen/analyze` dans `api_stream.py`
- tests `tests/unit/test_phase25_semantic.py`
- tests `tests/integration/test_phase25_semantic_integration.py`
Qwen a d'abord mis GO conditionnel :
- `analyze_frames` synchrone dans endpoint async,
- timeout OmniParser declare mais pas applique.
Claude a corrige :
- `run_in_executor` dans endpoint,
- timeout effectif OmniParser via `ThreadPoolExecutor(max_workers=2)`,
- fallback docTR,
- logs `logs/omniparser_errors.log`,
- tests annonces `35 passed / 0 failed`.
Qwen a confirme les garde-fous Dom :
- concurrence bornee,
- comportement si pool sature,
- fallback degrade,
- log explicite,
- tests/preuves.
Risque residuel accepte POC :
- un thread timeoutte ne peut pas etre tue en Python,
- contenu par pool borne + timeout + fallback + logs.
Codex n'a pas encore rerun localement les tests apres le dernier patch Claude. A faire demain si besoin :
```bash
.venv/bin/python -m py_compile core/semantic/phase25_analyzer.py agent_v0/server_v1/api_stream.py
.venv/bin/python -m pytest tests/unit/test_phase25_semantic.py tests/integration/test_phase25_semantic_integration.py -q
```
Note : les tests timeout peuvent durer environ 30s.
### R6 worker queue / enrichissement profond
Statut: P0 court terme, bloqueur POC si non resolu.
Qwen confirme :
- `data/training/_worker_queue.txt` existe, 0 octets, cree le 29 mai.
- Worker actif mais tourne a vide.
- Dernier traitement worker il y a plusieurs jours.
- Artefacts enriched/FAISS/graph stales.
- Sessions live recentes presentes :
- `sess_20260529T144652_5a2e96`
- `sess_20260529T154427_f95956`
Codex a confirme que ces dossiers existent :
- `data/training/live_sessions/DESKTOP-58D5CAC_windows/sess_20260529T144652_5a2e96`
- `data/training/live_sessions/DESKTOP-58D5CAC_windows/sess_20260529T154427_f95956`
- `data/training/_worker_queue.txt` = 0 octets.
Cause probable Qwen :
- `_enqueue_to_worker()` echoue silencieusement ou n'ecrit pas vraiment,
- logs serveur non accessibles,
- queue creee mais write absent.
GO donne a Claude a 22:22 :
1. Ajouter log critique avec `exc_info=True` dans `_enqueue_to_worker`.
2. Re-enqueue les deux sessions du 29 mai si elles existent.
3. Prouver queue -> worker -> artefacts ou logs exploitables.
Pas encore de livraison Claude R6 au moment du handoff.
Demain, premiere action :
- lire nouveaux messages Claude/Qwen,
- si pas de livraison R6, relancer Claude sur R6,
- ne pas partir sur autre chantier avant d'avoir statue R6.
### Existing learning modules
Statut: a rebrancher apres R6, pas a recoder.
Modules existants cites par Claude/Qwen :
- `core/learning/continuous_learner.py`
- `core/learning/feedback_processor.py`
- `core/learning/target_memory_store.py`
- `core/learning/versioned_store.py`
- `core/learning/learning_manager.py`
- `core/training/training_data_collector.py`
- `core/training/offline_trainer.py`
- `core/training/model_validator.py`
- `core/training/session_analyzer.py`
- `core/healing/learning_repository.py`
Verdict Qwen :
- `ContinuousLearner`: existant a rebrancher, couvre auto-evaluation par repetition.
- `FeedbackProcessor`: existant a rebrancher, couvre fusion/regroupement feedback.
- `target_memory_store` / `versioned_store`: existants a rebrancher.
- certains modules comme `learning_manager` / `quality_validator` sont deja branches ailleurs.
Ordre conseille apres R6 :
1. verifier signatures reelles,
2. hook minimal apres session enrichie,
3. hook feedback shadow -> FeedbackProcessor,
4. seulement ensuite enrichir payloads `confidence`, `uncertainties[]`, `repetition_count`, statuts `hypothesis/candidate/validated`.
### FAISS
Statut: actif partiellement, a consolider court terme uniquement si impact POC clair.
Qwen :
- FAISS actif localement dans plusieurs branches runtime.
- FAISS alimente par `reindex()`, `add_embedding()`, import pack.
- GlobalFAISS jamais consulte.
- save/load existent mais jamais appeles : index perdus au redemarrage.
- `replay_memory.py` n'utilise pas FAISS.
Decision court terme :
- FAISS persistence minimale = P1 demain si faible risque.
- GlobalFAISS consultation = P2 apres tests humains.
- Ne pas refondre FAISS maintenant.
Composants a garder en tete :
- `core/embedding/faiss_manager.py`
- `core/federation/faiss_global.py`
- `core/embedding/clip_embedder.py`
- `core/embedding/state_embedding_builder.py`
- `core/visual/visual_embedding_manager.py`
- `agent_v0/server_v1/replay_memory.py`
- tests `tests/unit/test_faiss_*`, `tests/unit/test_learning_pack.py`.
### Graph
Statut: actif, mais a reconnecter mieux avec Phase25 plus tard.
Qwen :
- graphe produit apres sessions via `finalize_session()` -> `GraphBuilder` -> `workflows/*.json`.
- utilise par WorkflowRunner, NodeMatcher, ExecutionLoop.
- compat apprentissage par repetition via DBSCAN / LearningState.
- NO-GO partiel sur distinction semantique action / hesitation / correction.
- P1-SEMANTIQUE ne doit pas devenir un format parallele long terme : a reconnecter au graphe.
Decision court terme :
- pas de refonte graphe maintenant.
- R6 d'abord pour que sessions repassent par enrichissement.
- Phase25 -> graphe = P1/P2 apres R6 si faible risque.
## Worktree / hygiene
Le worktree est tres sale. Ne pas revert ce que l'on n'a pas fait.
Commande observee :
```bash
git status --short
```
Points a retenir :
- `docs/POC/PREREQUIS_DSI_DGX_SPARK_2026-06-01.docx` est modifie par Dom : ne pas toucher.
- `api_stream.py` a un tres gros diff avec zones Qwen + Claude + modifications anterieures.
- Beaucoup de fichiers modifies/untracked predatent la session.
- Pas de commit sans GO explicite Dom.
- Si commit demain, probablement split logique :
1. P0 revocation Qwen,
2. P1 persist Claude,
3. P1 shadow + learn/start + bouton Windows,
4. P1 semantic,
5. R6 worker queue,
mais seulement apres revue et validation.
## Commandes utiles demain
Lire les derniers messages :
```bash
find docs/coordination/inbox_codex -maxdepth 1 -type f -newermt '2026-06-01 22:20:00' -printf '%T@ %TY-%Tm-%Td %TH:%TM %p\n' | sort -nr | head -80
find docs/coordination/inbox_codex -maxdepth 1 -type f -iname '*claude*' -printf '%T@ %TY-%Tm-%Td %TH:%TM %p\n' | sort -nr | head -30
find docs/coordination/inbox_codex -maxdepth 1 -type f -iname '*qwen*' -printf '%T@ %TY-%Tm-%Td %TH:%TM %p\n' | sort -nr | head -30
```
Verifier R6 fichiers/queue :
```bash
ls -l data/training/_worker_queue.txt
find data/training/live_sessions -maxdepth 3 -type d \( -name 'sess_20260529T144652_5a2e96' -o -name 'sess_20260529T154427_f95956' \) -print
rg -n "def _enqueue_to_worker|_worker_queue|enqueue" agent_v0/server_v1/api_stream.py agent_v0/server_v1/run_worker.py agent_v0/server_v1/stream_processor.py
```
Verifier P1-SEMANTIQUE localement :
```bash
.venv/bin/python -m py_compile core/semantic/phase25_analyzer.py agent_v0/server_v1/api_stream.py
.venv/bin/python -m pytest tests/unit/test_phase25_semantic.py tests/integration/test_phase25_semantic_integration.py -q
```
Tests P0 revocation a rerun si besoin :
```bash
.venv/bin/python -m py_compile agent_v0/server_v1/api_stream.py agent_v0/server_v1/agent_registry.py
.venv/bin/python -m pytest tests/unit/test_api_stream_revocation_gaps.py tests/integration/test_replay_single_inflight.py tests/integration/test_agents_enroll_api.py -q
```
## Plan de reprise recommande demain
### Etape 0 - Lire coordination
1. Lire ce handoff.
2. Lire les derniers `inbox_codex` apres 22:20.
3. Si Claude a livre R6, lire sa livraison.
4. Si Qwen a donne un nouveau verdict, le prioriser.
### Etape 1 - Clore P1-SEMANTIQUE
Statut deja GO Qwen.
Action Codex :
- rerun tests cibles si temps,
- si tests OK, marquer P1-SEMANTIQUE OK POC.
Ne pas ajouter de nouveau correctif sauf echec test reel.
### Etape 2 - R6 worker queue
Priorite P0.
Si Claude a livre :
1. Lire livraison.
2. Demander ou lire revue Qwen.
3. Verifier localement :
- queue ecrite,
- worker depile,
- artefacts/logs,
- pas d'effet de bord.
Si Claude n'a pas livre :
- relancer Claude avec le GO 22:22.
- garder Qwen en copie/review.
Definition of done R6 :
- une session test finalisee passe par worker,
- preuve disque ou logs exploitables,
- cause exacte documentee,
- pas de silencieux.
### Etape 3 - Rebrancher l'existant learning
Uniquement apres R6 prouve.
Ordre :
1. `ContinuousLearner` hook minimal apres session enrichie.
2. `FeedbackProcessor` hook minimal depuis feedback shadow.
3. `VersionedStore` / `TargetMemoryStore` si deja requis par les hooks.
4. Tests integration courts.
Ne pas creer nouveau module learning.
### Etape 4 - FAISS persistence minimale
Seulement si R6 OK et impact POC clair.
Objectif :
- save/load appele au bon moment,
- index pas perdu au redemarrage,
- tests existants adaptes si necessaire.
Ne pas traiter GlobalFAISS multi-postes maintenant.
### Etape 5 - Test humain E2E
Quand R6 + P1 green :
- utiliser `docs/demo/test-humain-e2e-poc.md` si present,
- verifier capture -> hypotheses -> persist -> replay -> verdict,
- ne pas chercher la quasi-prod avant d'avoir un cycle humain reel.
## Points a ne pas oublier
- Dom veut aller vite, mais solide.
- Dom est inquiet que l'on recode deja fait : toujours chercher l'existant.
- Dom veut Claude et Qwen utilises.
- Ne pas coder seul sans coordination.
- Les YAML sont checkpoints transitoires, pas memoire finale.
- Portabilite reste objectif majeur mais ne doit pas bloquer le POC sauf risque DPI.
- Donnees DPI utiles localement doivent rester exploitables ; ne pas anonymiser au point de perdre la capacite de trancher.
- Artefacts exportables doivent etre nettoyes/pseudonymises/sans memoire patient.
- Les tests humains manquent cruellement : ne pas repousser indefiniment.
## Dernier statut en une ligne
P1-LEA-SHADOW et P1-SEMANTIQUE sont GO Qwen pour POC ; le prochain vrai P0 est R6 worker queue/enrichissement profond, puis rebrancher ContinuousLearner/FeedbackProcessor existants sans recoder.

View File

@@ -0,0 +1,128 @@
# Handoff Codex — P0/P1 Léa, session propre
- `Date`: 2026-06-01 18:15 Europe/Paris
- `Contexte`: Dom demande qualité top, pas de régression, travail coordonné avec Claude/Qwen et agents.
- `Contrat produit`: Léa apprend par démonstration depuis Léa/agent-chat. Dashboard = admin/supervision/QA/promotion. VWB = outil admin/récupération uniquement.
## Décisions actives
| Sujet | Décision |
|---|---|
| Apprentissage | Départ depuis Léa (`agent-chat`, bouton/tray existant), pas bouton dashboard |
| Artefact durable | YAML `candidate/`, pas `stable` sans promotion admin |
| Runtime | Shadow existe mais est orphelin ; à raccorder |
| Lecture sémantique | Remarques Claude retenues : OmniParser runtime, Phase 2.5, agents externes, OCR qualité |
| Démo/POC | Pas de CLI opérateur ; CLI seulement dev/test |
| VLM | Pas de mock VLM en démo/POC |
## Messages lus
| Auteur | Fichier | Résumé |
|---|---|---|
| Qwen | `docs/coordination/inbox_codex/2026-06-01_qwen-to-codex_DIAGNOSTIC-P0-SINGLE-INFLIGHT.md` | Root cause P0 confirmé : early return `paused_need_help` renvoyait `status: ok` |
| Claude | `docs/coordination/inbox_codex/2026-06-01_1745_claude-to-codex_ADDENDUM-archi-Lea-lecture-semantique-agent-externe.md` | Ajout essentiel : lecture sémantique, OmniParser runtime, `ExternalDecisionClient`, OCR qualité |
| Qwen | `docs/coordination/inbox_codex/2026-06-01_qwen-to-codex_SYNTHESE-Q1-Q4-AGENTS-PARALLELES.md` | Shadow orphelin, `persist` absent, révocation non effective, micro-warnings |
## Dispatchs déposés
| Destinataire | Fichier |
|---|---|
| Claude | `docs/coordination/inbox_claude/2026-06-01_1812_codex-to-claude_GO-MAX-AGENTS-P0-P1-lea-quality-no-regression.md` |
| Qwen | `docs/coordination/inbox_qwen/2026-06-01_1812_codex-to-qwen_GO-MAX-AGENTS-P0-P1-lea-quality-no-regression.md` |
## Changements locaux effectués
### P0 replay single-inflight
Fichier touché : `agent_v0/server_v1/api_stream.py`
Changement Codex : dans la branche `paused_need_help` quand la queue est vide avant fin, l'early return renvoie maintenant :
```python
{
"status": "recorded",
"replay_status": replay_state["status"],
"pause_reason": "paused_need_help",
}
```
Important : `api_stream.py` avait déjà un énorme diff local avant ce changement. Ne pas revert. Le changement Codex est uniquement ce retour P0.
### Warnings dashboard Tester
Fichiers touchés par agent interne `Huygens` :
- `web_dashboard/templates/knowledge_base.html`
- `tests/unit/test_dashboard_routes.py`
Changements :
- Confirmation avant lancement d'une compétence qui ressemble à Win+R / Exécuter.
- Blocage/alerte du verdict `Valide` si aucune `step_results` ni evidence exploitable.
- Test HTML ciblé ajouté.
## Tests exécutés
| Commande | Résultat |
|---|---|
| `.venv/bin/python -m py_compile agent_v0/server_v1/api_stream.py` | OK |
| `.venv/bin/python -m pytest tests/integration/test_replay_single_inflight.py::test_concurrent_dispatch_and_result_no_double_increment -q` | OK |
| `.venv/bin/python -m pytest tests/integration/test_replay_single_inflight.py -q` | `10 passed, 1 xfailed` |
| `.venv/bin/python -m pytest tests/unit/test_dashboard_routes.py -q` | `30 passed` |
| `.venv/bin/python -m pytest tests/integration/test_replay_watchdog.py tests/integration/test_replay_resume_preserves_original_action.py::TestReplayResumePreservesOriginalAction::test_resume_dispatch_backfills_retry_pending_for_watchdog -q` | `11 passed` |
| `.venv/bin/python -m pytest tests/unit/test_dashboard_routes.py tests/unit/test_competence_verdicts.py tests/unit/test_competence_promotions.py tests/unit/test_competence_to_vwb_preview.py tests/unit/test_competence_catalog_loader.py tests/unit/test_vwb_supervised_pause_runtime.py tests/unit/test_lea_competence_verdict_api.py tests/integration/test_replay_single_inflight.py -q` | `75 passed, 1 xfailed` |
| `git diff --check -- agent_v0/server_v1/api_stream.py web_dashboard/templates/knowledge_base.html tests/unit/test_dashboard_routes.py ...` | OK |
Warnings attendus : `RequestsDependencyWarning` et `pynvml FutureWarning`.
## Agents internes Codex
| Agent | ID | Statut | Résumé |
|---|---|---|---|
| Lorentz | `019e83f4-6f94-77f2-aab4-82395ca62562` | Terminé | Confirme P0 Qwen ; risque faible ; follow-up : harmoniser payload complet de l'early return |
| Huygens | `019e83f4-b4b4-76b1-a9bc-49a3ec7b93fc` | Terminé | Patch warnings dashboard appliqué ; tests ciblés OK |
| Descartes | `019e83f4-85db-7a73-ba37-6b68938dd725` | Terminé | Révocation runtime non effective ; `/replay/next` public ; `touch_last_seen()` mort ; ré-enrollment admin_revoke à bloquer |
| Plato | `019e83f4-9b9f-7b63-904e-25befda0354a` | Terminé | Confirme faisabilité, mais recommande MVP prudent : Phase 2.5 post-apprentissage, pas OmniParser partout dans le hot path replay |
## Résultat agent Plato — architecture sémantique
Constats :
- OmniParser existe (`core/detection/omniparser_adapter.py`) mais reste fragile : chemin absolu, fallback souvent silencieux.
- ScreenAnalyzer existe et est conceptuellement propre, mais le streaming court-circuite l'initialisation lourde en mode léger.
- `t2a_decision` est réel et utilisable comme premier agent métier interne.
- `ExternalDecisionClient` n'existe pas encore.
- Confiance OCR actuelle insuffisante pour autonomie métier : certaines confiances sont approximatives.
- Des bypass `static_result` / `static_text` existent et doivent être interdits en démo/POC hors tests.
MVP recommandé :
1. Garder le P0 replay/click/OCR existant comme chemin principal.
2. Ajouter une Phase 2.5 post-apprentissage uniquement : snapshots sémantiques `{screen_id, window_title, screenshot_path, elements[]}`.
3. Demander à l'humain seulement les ambiguïtés utiles.
4. Brancher les résultats comme contexte, pas comme prérequis de chaque clic replay.
5. Démarrer `ExternalDecisionClient` autour de `t2a_decision`, puis adapter HTTP AIVA.
6. Prioriser OCR critique par régions annotées + escalade humaine si confiance basse.
## Prochains P0/P1
| Priorité | Sujet | Action recommandée |
|---|---|---|
| P0 | Revue/commit P0 replay + warnings | Relire diff final, décider commit |
| P0 POC | Révocation effective minimale | Ajouter garde registre côté serveur, retirer `/replay/next` des publics, appeler `touch_last_seen`, empêcher ré-enroll `admin_revoke` |
| P1 | `candidate/persist` | Créer endpoint `POST /api/v1/lea/competences/candidate/persist` + tests YAML |
| P1 | `agent-chat` -> Shadow | Raccorder bouton/chat Léa au cycle `start/stop/understanding/feedback/build/persist` |
| P1 | Lecture sémantique | D'abord Phase 2.5 post-apprentissage + snapshots, puis adapter OmniParser runtime, puis `ExternalDecisionClient` |
| P1 | Worker VLM | Vérifier/remettre green avant test humain sérieux |
## Points de vigilance
- Worktree très sale : ne pas revert les changements non identifiés.
- `docs/POC/PREREQUIS_DSI_DGX_SPARK_2026-06-01.docx` a été modifié par Dom : ne pas toucher.
- `api_stream.py` contient des changements préexistants massifs ; isoler les futurs patches.
- Token global : limitation encore forte. Le patch révocation minimale ne protège pas contre spoof d'un autre `machine_id` actif avec token global.
- Ne pas mettre OmniParser/VLM dans le hot path replay sans mesure perf/VRAM.
- Interdire les bypass `static_result` / `static_text` dans les workflows démo/POC.
- Session propre recommandée maintenant : oui, après lecture de ce handoff.
— Codex

View File

@@ -0,0 +1,160 @@
# HANDOFF Qwen — Fin de session 2026-06-01
- `Auteur`: Qwen
- `Date`: 2026-06-01 ~23:00 Europe/Paris
- `Prochaine reprise`: 2026-06-02 matin (bi-turbo)
- `Dom`: malade, en repos
---
## Bilan de la journée
**19 messages Codex lus, 12 ACK/revues déposés, 5 agents parallèles lancés, 1 revue globale 5 missions.**
| Lot | Statut | Verdict Qwen |
|-----|--------|-------------|
| **P0 révocation** | ✅ Livré + testé (32 passed, 1 xfail) | GO |
| **P0 single-inflight** | ✅ Patch `paused_need_help` `"ok"``"recorded"` | GO |
| **P1-PERSIST** | ✅ Livré par Claude (39 tests) | GO conditionnel (token↔machine_id reporté) |
| **P1-LEA-SHADOW** | ✅ Livré + corrigé NO-GO (62 tests) | GO — NO-GO levé |
| **P1-SEMANTIQUE** | ✅ Livré + corrigé GO conditionnel (35 tests) | GO — conditionnel levé |
| **Rebranchement bouton Windows** | ✅ Livré (9 tests) | GO |
| **Dashboard test competence** | ✅ Livré | GO |
| **C-alpha/B/gamma** | ✅ Livrés (chaîne complète) | GO |
| **Audit anti-doublon global** | ✅ 5 missions, 4 agents | Verdict déposé |
| **Plan tests humains E2E** | ✅ `docs/demo/test-humain-e2e-poc.md` | Prêt |
| **Guide test humain batch 1** | ✅ `docs/demo/test-humain-batch1.md` | Prêt |
---
## État des lieux — ce qui est fait
### Chaîne complète (de bout en bout)
```
Bouton Windows 🎓 → /api/learn/start → Shadow start → observation humaine
→ Shadow stop → understanding (Option C) → feedback → build → persist
→ YAML candidate → Dashboard "Tester" → replay supervisé → verdict
→ promotion dry-run → write-back YAML → audit trail
```
### P0 sécurité
- 6 endpoints runtime protégés par `_guard_agent_registry_access`
- Révocation effective (agent uninstalled → 403)
- `touch_last_seen()` actif
- `/replay/result` garde inconditionnel
- `/replay-session` garde ajouté
### Audit anti-doublon
**9 modules orphelins identifiés**, tous fonctionnels, aucun doublon significatif avec P1 du jour :
| Module | Rôle | Statut |
|--------|------|--------|
| `ContinuousLearner` (644 lignes) | Auto-évaluation par répétition, drift | À rebrancher P1 |
| `FeedbackProcessor` (176 lignes) | Feedback humain → ajustement | À rebrancher P1 |
| `VersionedStore` (520 lignes) | Snapshot/rollback learning | À rebrancher P1 |
| `TargetMemoryStore` (460 lignes) | Mémoire cibles UI | À rebrancher P1 |
| `learning_manager.py` (180 lignes) | Transitions états workflow | **Déjà rebranché** |
| `quality_validator.py` (460 lignes) | Validation qualité | **Déjà rebranché** |
| + 5 modules `core/training/` et `core/healing/` | Training, analyse, recovery | À rebrancher P1/P2 |
### R6 worker queue — CONFIRMÉ
- `_worker_queue.txt` vide depuis le 29 mai
- Worker actif (PID 4054092) mais ne traite rien depuis 5 jours
- 2 sessions finalisées non traitées
- Correctif : logger.critical dans `_enqueue_to_worker` + ré-enqueue manuel
### FAISS / Graphe
- FAISS : actif dans 4 branches runtime mais **persistance cassée** (save/load jamais appelés)
- Graphe : GO actif, produit après sessions, utilisé par replay, compatible répétition
- **Pas de doublon** entre graphe et P1-SEMANTIQUE, mais **pas de connexion** non plus
---
## État des lieux — ce qui reste à faire
### P0 — Immédiat (demain matin)
| Action | Fichier | Effort |
|--------|---------|--------|
| R6: logger `_enqueue_to_worker` + ré-enqueue sessions | `api_stream.py` | ~10 lignes |
| P1-SEMANTIQUE: le conditionnel est levé ✅ | — | Fait |
### P1 — Après P0
| Action | Effort | Impact |
|--------|--------|--------|
| Rebrancher ContinuousLearner | ~30-50 lignes | Auto-évaluation par répétition (décision Dom) |
| Rebrancher FeedbackProcessor | ~20 lignes | Hook dans learn_action.py |
| FAISS persistance (save/load) | ~20 lignes | Index perdus au redémarrage |
| Connecter Phase25 au graphe | ~30-50 lignes | Enrichir nodes, pas format parallèle |
### P2 — Après tests humains
| Action | Impact |
|--------|--------|
| Classification edges graphe (intention) | Distinguer action/hésitation/correction |
| GlobalFAISS consultation | Multi-postes |
| Confidence + repetition_count payload | Décision Dom auto-évaluation |
| Portabilité learning pack | Export DGX |
### Test humain E2E — PRÊT
- `docs/demo/test-humain-e2e-poc.md` — protocole 9 phases
- `docs/demo/test-humain-batch1.md` — guide 3 compétences
- Critère GO : ≥ 2/3 compétences batch 1 passent le cycle complet
---
## Décisions Dom actées (à respecter)
1. **Observation passive sans question** — Léa n'interrompt pas l'humain pendant l'observation
2. **Questions faible confiance seulement en prise de main** — copilote/supervisé uniquement
3. **Auto-évaluation par répétition** — Léa apprend à force d'observer/rejouer, pas par restitution lourde
4. **Données DPI exploitables localement** — DGX local, pas de sortie hors site
5. **Artefacts portables sans mémoire patient** — export réflexes/compétences, pas de captures brutes
6. **Noyau compétence stable + adaptateurs UI versionnés** — pas de "désapprentissage"
7. **Anti-doublon global** — chercher l'existant avant de coder, rebrancher avant de remplacer
8. **Pas de "surveiller plus tard" sans garde-fou** — contenir les risques maintenant
---
## Règles coordination
- **Qwen** = quality gate anti-doublon + revue bloquante
- **Claude** = exécution terrain des correctifs
- **Codex** = coordination + validation locale
- Inbox pattern : `docs/coordination/inbox_X/` pour messages, `syntheses/` pour rapports
- Réponses courtes et actionnables
---
## Points de vigilance pour demain
1. **Dom est malade** — il revient en bi-turbo mais peut être fatigué. Privilégier les tests humains courts.
2. **R6 est le seul bloquant technique** — sans worker, pas d'enrichissement FAISS/graph. Le correctif est trivial (~10 lignes).
3. **Pas de nouveau dev avant rebranchement** — Dom a été clair : on ne code pas de nouvelles features avant d'avoir rebranché l'existant.
4. **Test humain avant tout** — le protocole est prêt. Le plus important est de lancer un test réel avec Dom opérateur.
5. **P1-SEMANTIQUE GO** — le conditionnel est levé, mais c'est post-R6 en priorité.
---
## Fichiers clés à connaître
| Fichier | Rôle |
|---------|------|
| `docs/demo/test-humain-e2e-poc.md` | Protocole test humain E2E |
| `docs/demo/test-humain-batch1.md` | Guide 3 compétences batch 1 |
| `docs/POC/AUDIT_CHAINE_APPRENTISSAGE_2026-06-01.md` | Audit chaîne apprentissage (Claude) |
| `inbox_codex/...REVUE-GLOBALE-ANTI-DOUBLON-REBRANCHEMENT.md` | Revue globale Qwen 5 missions |
| `data/competences/candidate/*.yaml` | 6 compétences candidates |
| `data/competence_verdicts/verdicts.jsonl` | Verdicts supervisés |
| `data/training/_worker_queue.txt` | Queue worker (vide — à ré-enqueue) |
---
*Auteur : Qwen — Bonne nuit Dom, repose-toi bien !*

View File

@@ -0,0 +1,16 @@
Lis `docs/coordination/README.md` puis traite tous les fichiers avec `Statut: open` dans `docs/coordination/inbox_claude/`.
Priorité actuelle :
- `docs/coordination/inbox_claude/2026-05-23_1024_codex-to-claude_notepad-saveas-explorer-drift.md`
Contraintes :
- répondre dans `docs/coordination/inbox_codex/`
- respecter la convention de nommage
- ne pas modifier le fichier source
- patch minimal et tests si tu implémentes
- ne pas mélanger ce bug avec le handler popup overwrite déjà traité
Point de contexte utile :
- le grounding visuel agent a déjà été durci et validé
- le blocage courant nest plus la popup `Oui/Non`
- la cause à analyser est la dérive après clic `Enregistrer sous` vers `rpa_vision : Explorateur de fichiers`

View File

@@ -0,0 +1,56 @@
# Handoff Claude — 2026-05-25 soir
## État validé
- Smoke live Bloc-notes `replay_sess_1c0bfb42` : 16/16, 0 pause, score 0.94, temps moyen 4793 ms.
- Lot tests : `88 passed, 1 xfailed` (race concurrent polls documentée).
- VRAM agent_chat : 0 (libéré par C1+C1b+C1c+C1d).
- Build skip : 271 ms mesuré Codex (vs 91s historique).
- Profil démo actif sur Linux : `RPA_SKIP_INTENTION_ENRICHMENT=true`, `RPA_SKIP_BUILD_VISION=true`, `RPA_EASYOCR_GPU=0`, `AGENT_CHAT_ENABLE_OWL=0`, `AGENT_CHAT_ENABLE_UI_DETECTION=0`.
## Décisions actées
- Démo cible **Easily** (pas Bloc-notes) — Dom referra la capture à la main.
- D5-v2 = API préparatoire (qwen3.5 JSON), non consommée runtime.
- D5-v3a mini-fix : `num_ctx=4096` posé sur 3 sites bbox `resolve_engine.py`.
- D5-v3b helper bbox : reporté post-démo.
- D5-v3c Windows `num_ctx=8192` : NOGO avant freeze (fallbacks non triggered en smoke).
- C-P1 OCR tolérance préfixe : patch posé (`_text_match_fuzzy` + 14 tests).
- C-P3 bulle Léa : OK pour démo sans patch.
## Travaux en attente
- Capture réelle Easily par Dom selon protocole `inbox_codex/2026-05-25_1735_claude-to-codex_protocole-capture-easily-lea.md` (6 arbitrages à trancher avec Codex).
- Codex doit ACK : rapport unifié C-P1/C-P2/C-P3 (1720) + protocole Easily (1735).
- Commit/freeze du lot stabilisation par Codex (pas par Claude).
- Smoke équivalent Easily à faire (R7 ouvert).
## Risques bloquants seulement
- **R1** `RPA_GROUNDING_MODEL` ambigu (D5-v2 qwen3.5 vs legacy qwen2.5vl bbox) — NE PAS set globalement. D5-v3b résout post-démo.
- **R7** pas de smoke Easily équivalent (démo cible Easily). À faire avant freeze.
- Worktree large/sale — Codex groupera commits avant freeze.
## Fichiers importants
- `docs/coordination/active/2026-05-25_runbook-profil-demo-smoke.md` — runbook démo
- `docs/coordination/active/2026-05-25_etat-courant.md` — file active
- `docs/coordination/syntheses/2026-05-25_synthese-direction.md` — synthèse jour
- `docs/coordination/inbox_codex/2026-05-25_1720_claude-to-codex_REPONSE-taches-projet-ocr-d5v3c-lea.md` — C-P1/C-P2/C-P3
- `docs/coordination/inbox_codex/2026-05-25_1735_claude-to-codex_protocole-capture-easily-lea.md` — C-P4 Easily
## Prochaine action recommandée
Attendre ACK Codex sur C-P1+C-P2+C-P3 (rapport 1720) et sur le protocole Easily (1735). Ensuite : assister Dom pour la capture Easily selon protocole.
## Ce qu'il ne faut pas faire demain
- NE PAS set `RPA_GROUNDING_MODEL=qwen3.5:9b` globalement (casse les 3 sites bbox legacy).
- NE PAS toucher `agent_v0/agent_v1/core/executor.py` (Windows, reporté D5-v3c post-démo).
- NE PAS redéployer Windows sans GO Codex.
- NE PAS lancer de live replay sans GO Dom explicite.
- NE PAS commit (Codex groupe).
- NE PAS rouvrir D5-v3b helper bbox tant que lot perf/stabilité pas figé.
- NE PAS mettre de secret en clair dans `docs/coordination/` (placeholder).
- NE PAS copier mécaniquement le YAML `urgence_aiva_demo_expected.yaml` comme scénario (Dom recapture à la main).
- NE PAS spawner d'agent pour brainstorming ciblé (overkill, déjà cadré matin).

View File

@@ -0,0 +1,105 @@
# Handoff Claude — 2026-05-26 soir
## Rôle réassigné aujourd'hui
Dom m'a réassigné le rôle de **chef de projet** (priorisation + pilotage Codex), avec arbitrages stratégiques validés par Dom. Pas exécutant supervisé.
Feedback durable sauvé en mémoire : `feedback_depose_par_defaut.md` — ACK/reformulation/addendum vont directement dans `inbox_codex/` sans demander confirmation préalable.
## Pivot majeur de la journée
Démo Paris **du 21 mai → annulée/reportée**. Nouvelle démo cible **2026-06-01** (J-5 au matin du 27/05).
Le scénario démo a pivoté plusieurs fois aujourd'hui :
- Bloc-notes → Easily
- Ancien workflow `Urgence_aiva_demo`**scénario v2 collecte multi-onglets + transposition**
- Sortie : **OnlyOffice** (`/snap/bin/onlyoffice-desktopeditors`), pas LibreOffice (absent)
## État validé au 26/05 22:30
**Code & patch** :
- Patch OCR Tesseract IPP livré (Codex) : `extract_digits_tesseract_from_image()` + `extract_table(engine="tesseract")` dans `core/llm/ocr_extractor.py`. 40 tests OK. **11/11 IPP exacts** sur `landing_wide.png` (vs 8/11 EasyOCR brut).
- Workflow `Demo_urgence_3_db` (`wf_483910cdd851_1778750587`) branché : step `step_79c40f5a8342_1778750587` `extract_table` ajout `parameters.engine = "tesseract"`. Sauvegarde DB : `workflows.db.backup_2026-05-26_ocr_tesseract_demo3`.
- Healthcheck Linux OK (Codex matin) : `rpa-streaming` + `rpa-agent-chat` actives, profil démo systemd OK, ollama ps vide, baseline `api_stream` ~1.1 Go VRAM noté.
- Benchmark OCR fait : Tesseract champion chiffres/IPP, EasyOCR conserve texte continu. docTR utile pour bboxes/zonage si besoin futur. Preprocessing OpenCV global = régression, **pas activé par défaut**.
- Dry-run Easily v2 fait (Codex) : MOREL Catherine / IPP 25003284 lisible. Synthèse Urgences nécessite scroll obligatoire (CCMU/GEMSA/J12.1 en bas).
**Infra démo prête** :
- Maquette Easily : http://127.0.0.1:8765/
- Streaming :5005 OK, agent chat :5004 OK
- OnlyOffice + Tesseract (eng/fra/osd) dispo
## Décisions stratégiques actées
- **Périmètre live = 5 onglets** par défaut (Motif, Examens, Imagerie, Notes, Synthèse). Bascule 4 onglets = exception sur échec concret pendant répé, pas pré-décidée.
- **Vocabulaire produit** : Aiva-vision = plateforme universelle apprenante / Léa = agent d'interaction / Aiva-urgence = plugin métier santé.
- **Fail-safe valorisé** : Léa qui dit "je ne sais pas / montre-moi" = **succès produit**, pas échec. NOGO = actions dangereuses, pas demandes de confirmation.
- **Scroll = compétence apprise** : geste + changement visuel + marqueurs. Pas un raccourci fixe.
- **Démo réelle, pas de trucage** : pas de hardcode, pas de bidouille, pas de validation auto, pas de saut silencieux.
- **PII levée** : c'est une maquette, tout est fictif. MOREL Catherine / 25003284 OK.
## Demain — répétition humain challenge (2026-05-27)
Dom joue l'humain réel qui challenge Léa : interrompt, refuse, demande preuves, demande reprise.
Runbook : `docs/coordination/active/2026-05-26_runbook-repetition-humain-challenge-demo-v2.md`
Mon dernier livrable (22:30) couvre la dimension orale : 17 phrases-réponses Léa par catégorie (sélection / scroll / OnlyOffice / refus) + 4 phrases transitions + 8 NOGO comportementaux. Pattern minimum à tenir : **preuve / question / arrêt — jamais affirmation seule**.
## Mes 7 livrables déposés aujourd'hui (inbox_codex/)
1. `0920_claude-to-codex_reprise-session-plan-j6-demo.md` — reprise session
2. `1030_claude-to-codex_CHECKLIST-easily-capture-trace.md` — checklist capture
3. `2130_claude-to-codex_DEMO-v2-script-failsafe-onlyoffice.md` — script démo v2
4. `2145_claude-to-codex_ADDENDUM-demo-v2-dryrun-integration.md` — addendum dry-run
5. `2155_claude-to-codex_ACK-arbitrage-onglets-bascule-discours.md` — ACK 5/4/3 onglets
6. `2215_claude-to-codex_ACK-scroll-vwb-reformulation-discours.md` — ACK scroll VWB
7. `2230_claude-to-codex_SCRIPT-oral-lea-humain-challenge.md` — script oral challenge
## Travaux en attente
- **Répétition humain challenge 2026-05-27** (Dom + Codex), avec mes scripts oraux comme support
- **Capture réelle Easily** par Dom à finaliser après répé si la maquette demande des ajustements
- **Commits propres lot stabilisation** (Codex) — bloqués jusqu'à après capture+répé
- **Healthcheck Windows** — bloqué tant que Dom n'a pas fourni secret SSH non persistant
- **Smoke équivalent Easily** (R7) — post-capture, avant freeze 2026-06-01
## Risques bloquants
- **Synthèse Urgences scroll fragile** : à valider concrètement en répé. Si marqueurs CCMU/GEMSA/J12.1/Consultation externe absents après retry → bascule 4 onglets effective.
- **Divergence OCR EasyOCR/Tesseract** sur IPP : Léa doit demander confirmation, jamais trancher seule.
- **Pattern freeze NoMachine Windows** : toujours non résolu, mais hors scope démo Linux/OnlyOffice.
## Sources de vérité actives (ordre de lecture Codex)
1. `active/2026-05-26_cadrage-produit-aiva-vision.md`
2. `active/2026-05-26_arbitrage-dom-demo-reelle-poc.md`
3. `active/2026-05-26_principe-dom-apprentissage-fail-safe.md`
4. `active/2026-05-26_scenario-operatoire-demo-lea-v2-collecte-transposition.md`
5. `active/2026-05-26_arbitrage-scroll-vwb-reference.md`
6. `active/2026-05-26_principe-apprentissage-scroll-securise.md`
7. `active/2026-05-26_arbitrage-sortie-transposition-onlyoffice.md`
8. `active/2026-05-26_benchmark-ocr-local-captures-easily.md`
9. `active/2026-05-26_patch-ocr-tesseract-ipp.md`
10. `active/2026-05-26_runbook-repetition-humain-challenge-demo-v2.md`
11. `active/2026-05-26_etat-preparation-repetition-2026-05-27.md`
## Prochaine action recommandée à la reprise
À l'ouverture de session demain :
1. Lire `inbox_claude/` pour voir si Codex a déposé un ACK sur le script oral 22:30, ou des INFO de fin de soirée.
2. Lire `active/` pour voir si un nouveau doc a été produit pendant la nuit (post-répé éventuelle).
3. Reprendre le rôle de pilotage projet, attendre instruction Dom sur priorité du jour (préparation finale démo / ajustements post-répé / autre).
## Ce qu'il ne faut pas faire demain
- Ne pas proposer de "retirer Synthèse Urgences" du périmètre par prudence abstraite — rétracté à deux reprises aujourd'hui (addendum 21:45 puis ACK 22:15). Bascule 4 onglets = exception sur échec concret.
- Ne pas mentionner LibreOffice (absent côté Linux).
- Ne pas re-proposer "patch avant benchmark OCR" — l'ordre est verrouillé : benchmark d'abord, patch ensuite.
- Ne pas commiter (Codex groupe les commits).
- Ne pas lancer de live replay sans GO Dom explicite.
- Ne pas mettre de secret en clair dans `docs/coordination/`.
- Ne pas empiéter sur le périmètre Qwen/Anscombe (OCR benchmark, audit pipeline read-only).
- Ne pas attendre validation Dom pour déposer un ACK/reformulation côté Codex — feedback "dépose par défaut" actif (cf. `feedback_depose_par_defaut.md`).
- Ne pas refaire un parcours linéaire dans les scripts oraux — c'est mode humain challenge, le pattern "preuve / question / arrêt" est non négociable.

View File

@@ -0,0 +1,70 @@
# Prompt de reprise Claude — 2026-05-28 matin
> A coller (ou referencer) dans une nouvelle session Claude Code pour repartir propre.
## Contexte
Tu es chef de projet sur `rpa_vision_v3` (alias commercial `aiva-vision`), pilote Codex sur le pivot micro-apprentissage Lea. Le P0 du jour est `ouvrir_recherche_windows` a partir de la session `sess_20260527T185155_98ad9a`.
Hier (2026-05-27) :
- Cadrage `MicroEpisode` comme **annotation/promotion au-dessus** de la chaine existante Graph/FAISS/WorkflowPipeline/TargetMemoryStore (pas de nouvelle chaine).
- Brique `message_contract.py` ecrite par Codex (35 tests verts).
- Plan P1 livre cote Claude (YAML schema, 7 sites warning, predicate `window_title_in`, 4 tests, risques).
## A lire d'abord (ordre important)
1. `docs/handoffs/2026-05-27_handoff_codex_micro_apprentissage_lea_p0_reprise_2026-05-28.md` — handoff Codex
2. `docs/coordination/active/2026-05-28_plan-matin-micro-apprentissage-lea-p0.md` — plan matin actif
3. `docs/coordination/inbox_codex/2026-05-27_1959_claude-to-codex_CONTRAT-competence-courte-verifiee-P0.md` — contrat `MicroEpisode` (14 sections, transitions observed→candidate→supervised→stable)
4. `docs/coordination/inbox_codex/2026-05-27_2039_claude-to-codex_PLAN-P1-contrat-p0-message-warning.md` — plan P1 (YAML schema final, 7 sites warning avec lignes precises, predicate, 4 tests)
5. `docs/coordination/syntheses/2026-05-27_1956_codex_ADDENDUM-chaine-apprentissage-graph-faiss.md` — recadrage Dom : ne pas reinventer la chaine
## Priorites du jour (cf. PREP Codex 21:35)
1. Garder `MicroEpisode` = annotation/promotion au-dessus de Graph/FAISS, jamais en remplacement.
2. Finaliser `data/competences/observed/open_windows_search.yaml` (schema dans le PLAN P1 §1).
3. Guider le branchement `message_contract.py` en mode **warning** (helper `emit_or_warn`, 7 sites lignes precises dans PLAN P1 §2).
4. Verifier que les messages visibles respectent le format 4 champs : INTENTION / ATTENDU / VU / DEMANDE.
5. Refuser toute derive boite a clic ou chaine parallele.
## 5 decisions §13 a acter avec Dom
1. Validateur YAML chez Codex ou Claude ?
2. Helper `emit_or_warn` dans `message_contract.py` ou nouveau module ?
3. Branchement warning : 1 site d'abord ou 6 en bloc ?
4. Demotion stable→supervised : N=2 ou N=3 echecs ?
5. Promotion AUTO stable : automatique apres T3 ou validation Dom obligatoire ?
## Invariants
- Pas de patch sans GO Dom matin.
- Pas de recapture Win+S (session P0 existe : 3 chemins confirmes).
- Pas de bypass Graph / FAISS / TargetMemoryStore.
- Pas de coordonnees codees en dur ; jamais de boite a clic.
- Pas de message generique visible (`un element`, `cette action`, `Validation requise`).
- Ne pas modifier `agent_v0/agent_v1/core/executor.py` ni `core/grounding.py`.
- Pas de live replay / restart service / redeploiement Windows sans GO.
- Pas de commit (Codex groupe les commits).
- Secrets : pas en clair dans `docs/coordination/` (placeholder OK).
## Etat session P0
Session : `sess_20260527T185155_98ad9a` (23 events, machine `DESKTOP-58D5CAC_windows`).
Chemins :
- `data/training/live_sessions/streaming_sessions/sess_20260527T185155_98ad9a.json`
- `data/training/live_sessions/DESKTOP-58D5CAC_windows/sess_20260527T185155_98ad9a/live_events.jsonl`
- `data/training/live_sessions/DESKTOP-58D5CAC_windows/sess_20260527T185155_98ad9a/shots/`
Sequence clef : event #02 focus `Acces vocal → Rechercher (SearchHost.exe)` precede event #03 `key_combo ["win","s"]` (effet release-only). Heartbeats #07/#11/#13 confirment `active='Rechercher'`. Parasites a filtrer : #00 (Acces vocal), #15-22 (systray + pythonw).
Marqueur succes retenu : `window_title in {"Rechercher"} OR app_name == "SearchHost.exe"`.
## Canal coordination
Lecture Codex : `docs/coordination/inbox_claude/`
Depot Claude → Codex : `docs/coordination/inbox_codex/`
Polling : par defaut, depose direct (ACK/reformulation/addendum) sans demander confirmation.
## Premier reflexe attendu
Lire les 5 fichiers ci-dessus, faire un `ls -lt docs/coordination/inbox_claude/ | head -5` pour les eventuels messages nuit, puis demander GO Dom sur les 5 decisions §13 avant tout patch ou ecriture YAML.

View File

@@ -0,0 +1,23 @@
Lis dabord `docs/handoffs/2026-05-23_handoff_codex_lea_replay_resume.md`, puis vérifie `docs/coordination/inbox_codex/` pour toute réponse Claude récente.
Contexte à reprendre :
- projet `rpa_vision_v3`
- replay live courant : `replay_sess_595c4947`
- blocage actuel : dérive après clic sur longlet `Enregistrer sous` vers `rpa_vision : Explorateur de fichiers`
- le patch grounding visuel est déjà déployé sur Windows et actif
- le handler popup overwrite `Confirmer l'enregistrement -> Oui` est déjà codé mais pas encore exercé sur ce run
- problème infra séparé : GPU Linux non exploitable (`driver/library mismatch` NVIDIA)
Objectif immédiat :
1. vérifier si la machine a redémarré et si Léa est revenue
2. si besoin, relancer un `replay-session` avec :
- `session_id=sess_20260520T102916_066851`
- `machine_id=DESKTOP-58D5CAC_windows`
3. continuer uniquement le diagnostic/fix sur la dérive `Enregistrer sous -> Explorateur`
4. lire et intégrer la réponse Claude si elle existe
5. ne pas repartir de zéro ni rouvrir les sujets déjà clos
Rappels importants :
- utiliser `docs/coordination/` pour les briefs Claude
- ne pas mélanger le bug fonctionnel courant avec le problème infra GPU
- ne pas retirer le nouveau grounding visuel sans preuve contraire

View File

@@ -0,0 +1,19 @@
Lis d'abord :
1. `docs/handoffs/2026-05-25_handoff_codex_notepad_success_phase2.md`
2. le handoff Claude miroir du 2026-05-25 s'il existe dans `docs/handoffs/`
3. `docs/coordination/inbox_codex/` pour les messages Claude plus recents
Contexte court :
- Replay Bloc-notes live reussi : `replay_sess_e96e5822`, `18/18`, `0` echec.
- Point critique valide : `Confirmer l'enregistrement` traite sans aide humaine via `runtime_dialog_handled_post_verify`.
- Prochains chantiers : figer le succes par test offline, traiter vitesse/dispatch, clarifier chemin Windows reel, puis transport inerte `SceneExpected` / `Precondition`.
Regles de reprise :
- Ne pas faire de gros code sans deleguer a Claude.
- Ne pas resync `deploy/windows_client` avant verification du chemin reel execute par Lea.
- Ne pas relancer de replay live tant que l'objectif de test n'est pas clair.
- Utiliser `.venv/bin/python` pour les tests Python.
- Ne rien revert dans le worktree sale sans demande explicite Dom.

View File

@@ -0,0 +1,140 @@
# Handoff Codex — reprise soir 2026-05-25
- `Auteur`: Codex
- `Date`: 2026-05-25 soir Europe/Paris
- `Contexte`: stabilisation Lea / demo cible 2026-06-01
- `Role Codex`: direction technique, arbitrage, integration, tests finaux
## Etat valide
1. Smoke Notepad post-recablage valide :
- replay `replay_sess_1c0bfb42`
- resultat `completed`, `16/16`, `0 failed`, `0 retries`, `0 pause Lea`
- gardes memoire valides :
- `window_transition_requires_visual_confirmation`
- `generic_button_missing_context`
- dialogue remplacement absorbe via `runtime_dialog_handled_post_verify`
2. Profil demo Linux actif et coherent :
- `RPA_SKIP_INTENTION_ENRICHMENT=true`
- `RPA_SKIP_BUILD_VISION=true`
- `RPA_EASYOCR_GPU=0`
- `AGENT_CHAT_ENABLE_OWL=0`
- `AGENT_CHAT_ENABLE_UI_DETECTION=0`
3. R6 EasyOCR leve par Gemini :
- conserver la modification `easyocr_gpu_enabled(default=False)`
- objectif : pas d'allocation VRAM EasyOCR cachee
4. Bulle Lea :
- correctif scrollable/compact present
- Claude juge OK demo
- pas de patch supplementaire ce soir
## Decisions actees
- Notepad `16/16` valide le recablage, mais ne prouve pas encore la robustesse. Il faudra planifier une serie 10-15 runs avant de parler de stabilite forte.
- Pas de migration globale `qwen3.5` avant D5-v3b/D5-v3c.
- Runtime demo reste `qwen2.5vl` bbox legacy avec `num_ctx=4096`.
- `qwen3.5` = benchmark isole / API preparatoire JSON, pas runtime global.
- D5-v3c Windows `num_ctx=8192` = report post-demo.
- Easily avec Lea reel = chantier reporte apres-demain, pas demain.
- Secrets/docs = dette secondaire pour l'instant, a traiter avant commit public.
## Travail collegues a lire a la reprise
Lire les derniers messages dans `docs/coordination/inbox_codex/`, en particulier :
- `2026-05-25_1720_claude-to-codex_REPONSE-taches-projet-ocr-d5v3c-lea.md`
- `2026-05-25_1735_claude-to-codex_protocole-capture-easily-lea.md`
- `2026-05-25_2030_gemini-to-codex_REPONSE-taches-projet-perf-qwen-risques.md`
- `2026-05-25_2045_gemini-to-codex_GRILLE-demo-intelligence-facilite.md`
- `2026-05-25_2100_gemini-to-codex_PROPOSITION-demo-metier-avancee.md`
Messages de preparation de fin de soiree envoyes :
- `docs/coordination/inbox_claude/2026-05-25_2018_codex-to-claude_TACHES-preparation-sans-runtime.md`
- `docs/coordination/inbox_gemini/2026-05-25_2018_codex-to-gemini_TACHES-preparation-sans-runtime.md`
Dom demande aussi a Claude et Gemini de produire leurs handoffs soir.
## Patch OCR Claude
Claude annonce avoir pose :
- `agent_v0/server_v1/resolve_engine.py`
- `tests/unit/test_text_match_fuzzy_prefix.py`
Objet : accepter un OCR prefixe partiel, ex. `Enregi` pour `Enregistrer`, avec garde-fous :
- `len(observed) >= 4`
- `len(observed) >= 50% len(expected)`
- `expected.startswith(observed)`
A faire a la reprise :
1. verifier le diff soi-meme ;
2. lancer le test cible ;
3. arbitrer `ACK/NACK` a Claude ;
4. decider si seuil 50% ou 60%.
Commande cible :
```bash
.venv/bin/python -m pytest tests/unit/test_text_match_fuzzy_prefix.py -q
```
## Prochaines actions recommandees
Priorite courte :
1. `git status --short`
2. Lire nouveaux handoffs Claude/Gemini s'ils existent.
3. Verifier patch OCR Claude + test cible.
4. Repondre ACK/NACK a Claude.
5. Planifier protocole smoke Notepad 10-15 runs, sans le lancer sans GO Dom.
Priorite moyenne :
1. Decoupage worktree en commits propres.
2. Ne pas committer docs avant scan/sanitisation minimal.
3. Preparer, plus tard, capture Easily avec Lea reelle.
## A ne pas faire demain
- Ne pas lancer chantier Easily.
- Ne pas migrer `qwen3.5` en global.
- Ne pas set `RPA_GROUNDING_MODEL=qwen3.5:9b`.
- Ne pas redeployer Windows pour D5-v3c.
- Ne pas lancer 10-15 smokes sans fenetre de controle et GO Dom.
- Ne pas faire de grand nettoyage worktree non supervise.
- Ne pas archiver/supprimer messages coordination sans synthese.
## Commandes utiles lecture seule
```bash
git status --short
find docs/coordination/inbox_codex -type f -printf '%TY-%Tm-%Td %TH:%TM %p\n' | sort | tail -20
systemctl --user is-active rpa-streaming.service
curl -fsS http://127.0.0.1:5005/health
ollama ps
```
Pour les endpoints proteges, charger `.env.local` sans afficher le token :
```bash
set -a; . ./.env.local; set +a
curl -fsS -H "Authorization: Bearer $RPA_API_TOKEN" http://127.0.0.1:5005/api/v1/traces/stream/replays
```
## Risques restants
- Robustesse replay non prouvee statistiquement : un seul Notepad 16/16.
- Worktree tres sale, nombreux fichiers modifiés/non suivis.
- Docs historiques avec secrets : secondaire ce soir, bloquant avant publication/commit docs.
- Scenario demo client encore a concevoir : le client veut voir intelligence/facilite, pas seulement boutons.
- Capture Easily reelle devra utiliser patient fictif et eviter PII.
## Dernier mot
Le socle a enfin passe un vrai smoke propre. La bonne discipline maintenant : petits lots, delegation, verification, pas de migration globale ni nouveau chantier lourd avant reprise controlee.

View File

@@ -0,0 +1,114 @@
# Prompt reprise Gemini — 2026-05-25
Tu es Gemini, collègue de revue indépendante sur le projet Léa/RPA Vision.
Contexte local :
- Repo : `/home/dom/ai/rpa_vision_v3`
- Canal d'échange : fichiers Markdown dans `docs/coordination/`
- Tu réponds à Codex dans `docs/coordination/inbox_codex/`
- Format attendu : court, factuel, avec `ACK/NACK`, fichiers lus, risques, recommandations.
## Rôle attendu
Tu n'es pas le pilote principal. Ton rôle est :
- revue indépendante ;
- contrepoint technique ;
- détection d'angles morts ;
- validation ou NACK des décisions Codex/Claude ;
- pas de patch sauf anomalie critique explicitement justifiée.
Codex garde l'arbitrage. Claude exécute/analyse. Dom tranche les choix produit/demo.
## À lire en priorité
Lis dans cet ordre :
1. `docs/coordination/README.md`
2. `docs/coordination/active/2026-05-25_etat-courant.md`
3. `docs/coordination/syntheses/2026-05-25_synthese-direction.md`
4. `docs/coordination/registre/2026-05-25_decisions.md`
5. `docs/coordination/registre/2026-05-25_arbitrages-runbook-demo.md`
6. `docs/coordination/active/2026-05-25_execution-profil-demo-linux.md`
7. `docs/coordination/active/2026-05-25_runbook-profil-demo-smoke.md`
8. `docs/coordination/index/2026-05-25_messages-cles.md`
Ensuite seulement, lis les derniers messages si besoin :
- `docs/coordination/inbox_codex/2026-05-25_1900_claude-to-codex_D5v3a-mini-fix-resultat.md`
- `docs/coordination/inbox_codex/2026-05-25_1920_gemini-to-codex_ACK-revue-D5v3a-minifix.md`
- `docs/coordination/inbox_codex/2026-05-25_1645_claude-to-codex_runbook-demo-livre.md`
- `docs/coordination/inbox_claude/2026-05-25_1640_codex-to-claude_INFO-profil-demo-linux-active.md`
## État courant résumé
La démo client est reportée au 2026-06-01. On stabilise proprement.
Ce qui est validé :
- C2d-bis : `RPA_SKIP_BUILD_VISION=true` + short-circuit `vision_info.text`.
- Gain perf build confirmé par Codex après restart : `skip_ms=253`, `speedup=209.7x`.
- D5-v3a mini-fix : `num_ctx=4096` explicite sur les 3 appels bbox legacy serveur.
- Agent_chat ne charge plus OWL/UI detection et n'occupe plus la VRAM.
- Profil démo Linux actif via drop-ins systemd.
- Smoke offline OK.
Ce qui n'est PAS fait :
- Pas de smoke live.
- Pas de healthcheck Windows avec secret.
- Pas de commit.
- Pas de D5-v3b.
- Pas de modification Windows executor.
## Flags actifs côté Linux
`rpa-streaming.service` :
- `RPA_SKIP_INTENTION_ENRICHMENT=true`
- `RPA_SKIP_BUILD_VISION=true`
- `RPA_EASYOCR_GPU=0`
- `RPA_VALIDATOR_V2_ENABLED=true`
- `RPA_DIALOG_RESOLVER_ENABLED=true`
`rpa-agent-chat.service` :
- `AGENT_CHAT_ENABLE_OWL=0`
- `AGENT_CHAT_ENABLE_UI_DETECTION=0`
Important : ne PAS recommander d'activer globalement `RPA_GROUNDING_MODEL=qwen3.5:9b`.
Raison : conflit avec les callers bbox legacy qui attendent qwen2.5vl/bbox_2d.
## Points critiques à garder en tête
- `generate_grounding()` D5-v2 est une API préparatoire JSON `{x_pct,y_pct,confidence}`. Elle n'est pas encore consommée par les callers runtime.
- Les chemins bbox legacy restent qwen2.5vl, avec `num_ctx=4096` explicite depuis D5-v3a.
- Le healthcheck Linux peut afficher WARN si `qwen2.5vl` n'est pas résident à froid ; ce WARN est acceptable avant smoke live.
- Le test perf peut réchauffer qwen2.5vl en `CONTEXT=8192` via chemin full historique ; Codex l'a ensuite stoppé avec `ollama stop`.
- Windows executor contient encore des `num_ctx=8192`, report D5-v3c.
- Le worktree est large et sale : pas de commit/freeze sans tri Codex.
- Ne jamais stocker de secret réel dans les docs ; utiliser placeholders.
## Ce que Codex attend de toi maintenant
Mission de reprise :
1. ACK que tu as repris le contexte.
2. Vérifie que la structure de coordination est compréhensible.
3. Vérifie que le profil démo Linux actif ne contient pas de contradiction évidente.
4. Propose une checklist GO/NOGO courte pour la prochaine étape :
- healthcheck Windows ;
- smoke live court Bloc-notes.
5. Signale tout NACK bloquant avant smoke live.
Contraintes :
- Pas de patch code.
- Pas de restart service.
- Pas de live replay.
- Pas de D5-v3b.
- Répondre dans `docs/coordination/inbox_codex/` avec un fichier nommé :
`2026-05-25_HHMM_gemini-to-codex_ACK-reprise-contexte.md`
Auteur du prompt : Codex

View File

@@ -0,0 +1,33 @@
# Handoff Gemini — 2026-05-25 SOIR
## 1. État validé
- **Performance** : C2d-bis validé (build vision skip ~270ms, speedup ~210x).
- **Stabilisation VRAM** : D5-v3a (num_ctx=4096) colmaté sur les appels bbox legacy.
- **Profil Démo Linux** : Actif (drop-ins systemd), validé par smoke live Bloc-notes (`16/16`).
## 2. Décisions actées
- **Scénario Démo** : Passage d'un "waouh visuel" à une démo "métier" (lecture tableaux, extraction champs, saisie).
- **Architecture** : Maintien strict de `qwen2.5vl` pour le runtime démo ; `qwen3.5` réservé au benchmark/audit.
- **R6 EasyOCR** : Validé et conservé (respect de `RPA_EASYOCR_GPU=0`).
## 3. Travaux en attente
- **Configuration Easily** : Définir les `ExtractionSchema` (champs patients) pour le nouveau scénario métier.
- **Sanitisation Docs** : Nettoyage des secrets identifiés (G-P3).
- **Benchmark VLM** : Exécution du protocole isolé qwen2.5 vs qwen3.5.
## 4. Risques bloquants seulement
- **Secrets Git** : `docs/AUDIT_20260404.md` contient des clés API réelles. **Interdiction de push/publier sans purge.**
## 5. Fichiers importants
- `core/extraction/field_extractor.py` (Cœur de la démo métier demain).
- `agent_v0/server_v1/resolve_engine.py` (Contient le mini-fix VRAM).
- `docs/coordination/registre/2026-05-25_decisions.md` (Source de vérité).
## 6. Prochaine action recommandée
- **Lancer le Healthcheck Windows** (avec secret non persistant) pour valider l'agent avant le smoke métier Easily.
## 7. Ce qu'il ne faut pas faire demain
- **Ne pas activer `RPA_GROUNDING_MODEL=qwen3.5:9b` globalement.**
- **Ne pas modifier le code de `agent_v1` (Windows)** sans plan de déploiement validé.
Auteur : Gemini

View File

@@ -0,0 +1,85 @@
# Prompt reprise Gemini — 2026-05-26 matin
- `Auteur`: Codex
- `Date`: 2026-05-26 10:09 Europe/Paris
- `Contexte`: reprise J-6 démo Easily 2026-06-01
- `But`: permettre à Gemini de redémarrer vite sans relire tout l'historique
## État court
Le socle technique a été stabilisé hier soir :
- Smoke Bloc-notes `replay_sess_1c0bfb42` validé `16/16`, `0 failed`, `0 retries`, `0 pause Léa`.
- Profil démo Linux actif :
- `RPA_SKIP_INTENTION_ENRICHMENT=true`
- `RPA_SKIP_BUILD_VISION=true`
- `RPA_EASYOCR_GPU=0`
- `AGENT_CHAT_ENABLE_OWL=0`
- `AGENT_CHAT_ENABLE_UI_DETECTION=0`
- Runtime démo maintenu sur `qwen2.5vl` bbox legacy.
- `qwen3.5` reste benchmark/API préparatoire, pas runtime global.
- Patch OCR Claude `_text_match_fuzzy` ACK par Codex : tolérance préfixe `Enregi` / `Enregistrer`, seuil conservé à 50%.
- D5-v3c Windows `num_ctx=8192` reporté post-démo.
## État ce matin
Codex a distribué les tâches :
- Claude : `docs/coordination/inbox_claude/2026-05-26_0925_codex-to-claude_TACHES-reprise-easily-healthcheck-trace.md`
- Gemini : `docs/coordination/inbox_gemini/2026-05-26_0925_codex-to-gemini_TACHES-reprise-demo-metier-risques.md`
Healthcheck local Codex :
- `rpa-streaming.service`: actif
- `rpa-agent-chat.service`: actif
- `5005/health`: healthy
- `5004/api/status`: online
- `ollama ps`: aucun modèle résident
- `tools/lea_healthcheck.py`: WARN uniquement car `qwen2.5vl:7b-rpa` non résident
- Windows non testé : pas de secret SSH disponible dans la session Codex
## Ce que Gemini doit faire maintenant
Répondre uniquement à cette tâche :
`docs/coordination/inbox_gemini/2026-05-26_0925_codex-to-gemini_TACHES-reprise-demo-metier-risques.md`
Réponse attendue dans :
`docs/coordination/inbox_codex/2026-05-26_HHMM_gemini-to-codex_REPONSE-demo-metier-risques.md`
Format attendu : court, ACK/NACK explicite, directement utilisable par Dom/Codex.
## Contenu attendu
1. **Scénario métier minimal Easily**
- 6 à 10 actions maximum.
- Montrer intelligence + facilité, pas performance technique.
- Inclure au moins une lecture métier et une saisie contrôlée.
- Ne pas reprendre mécaniquement un workflow historique VWB.
2. **Critères client de réussite**
- 5 critères maximum, visibles par un client non technique.
3. **Risques bloquants démo**
- Seulement les risques qui justifient NOGO avant capture/replay.
- Distinguer technique, métier, conformité.
4. **Phrase qwen**
- Une formulation courte expliquant pourquoi on ne migre pas `qwen3.5` globalement avant la démo.
## Contraintes non négociables
- Pas de runtime.
- Pas de benchmark.
- Pas de migration modèle.
- Pas de replay live.
- Pas de redéploiement Windows.
- Pas de données patient réelles.
- Ne pas bloquer sur secrets/docs : dette à traiter avant publication, pas avant préparation locale.
## Décision si contexte incomplet
Si Gemini manque d'information, il doit répondre avec les hypothèses minimales plutôt que relancer une enquête longue.
Auteur : Codex

View File

@@ -0,0 +1,161 @@
# Handoff Qwen — Session 2026-05-26 soir → 2026-05-27
- `De`: Qwen
- `Date`: 2026-05-26 22:30 Europe/Paris
- `Contexte`: fin de session soir, reprise demain
- `Équipe`: Dom (arbitre), Codex (coordinateur), Claude/Claudettes (collègues), Qwen (moi)
---
## État du projet — ce qui a été fait ce soir
### Session de reprise (20h44 → 22h30)
1. **Lecture du prompt de reprise** (`PROMPT_REPRISE_QWEN_2026-05-26_SOIR.md`)
- Cadrage Aiva-vision / Léa / Aiva-urgence
- Scénario démo v2 : collecte multi-onglets → transposition OnlyOffice
- Limites non négociables Dom
2. **Première analyse**`2026-05-26_2044_qwen-to-codex_REPRISE-analyse-scenario-v2.md`
- ACK/NACK, 8-lignes résumé, 5 risques, 5 critères, 3 vérifications
- Recommandation : dry-run contrôlé
3. **Deuxième passe après lecture 5 sources actives**`2026-05-26_2050_qwen-to-codex_DELTA-apres-lecture-sources-actives.md`
- Corrections : `/api/analyse` n'est pas un endpoint vision, orthographe `Easily`
- 3 risques bloquants : extract_text_scroll, grounding maquette, sortie transposition
- Proposition transposition : .xlsx via openpyxl, fallback .txt
4. **Audit technique dry-run + OnlyOffice**`2026-05-26_2101_qwen-to-codex_AUDIT-technique-dryrun-onlyoffice.md`
- 8 ancres critiques à valider
- Seuils GO/NOGO par onglet
- Fallbacks F1-F4
5. **Seuils et fallbacks après dry-run**`2026-05-26_2113_qwen-to-codex_SEUILS-fallbacks-apres-dryrun.md`
- Seuils affinés sur données réelles du dry-run
- 4 fallbacks techniques documentés
6. **Rapport P0 OCR écran**`2026-05-26_2117_qwen-to-codex_RAPPORT-P0-ocr-ecran.md`
- Diagnostic pipeline OCR (EasyOCR, docTR, Tesseract)
- Architecture multi-moteur par zone
- Cold start vs interface apprise
- **Mis à jour** : docTR CPU repositionné comme moteur de zonage P0
7. **Retour benchmark OCR**`2026-05-26_2148_qwen-to-codex_RETOUR-benchmark-ocr-capitalisation.md`
- Tesseract 11/11 IPP en 0,47s ✅
- EasyOCR 8/11 IPP, bon sur texte continu
- Preprocessing OpenCV : régression, pas d'amélioration
- Architecture multi-moteur : chiffres→Tesseract, texte→EasyOCR, structure→docTR
- 5 règles de capitalisation
8. **ACK apprentissage scroll sécurisé**`2026-05-26_2149_qwen-to-codex_ACK-apprentissage-scroll-securise.md`
- GO/NOGO sur marqueurs après scroll (CCMU, GEMSA, J12.1, Consultation externe)
- Scroll réussi = geste + changement visuel + données relues
### Exploration web (solutions similaires)
- Agent-S : réflexion in-context, Best-of-N sampling, grounding dédié
- UI-TARS : grounding GUI par coordonnées, reinforcement learning
- Claude Computer Use : 22% OSWorld, scroll/drag difficiles
- OpenAI Operator : **abandonné** (août 2025)
- Différentiateur Aiva-vision : "l'agent qui sait s'arrêter" — défendable en domaine réglementé
### Exploration codebase
- Agent explorateur a scanné 880 fichiers Python, 39 sous-modules core/
- Pipeline complet compris : capture → streaming → analyse → grounding → execution → replay
---
## État technique connu — décisions actives
### OCR
- **EasyOCR brut** : moteur par défaut pour texte continu (inchangé)
- **Tesseract** : patch appliqué pour IPP/chiffres (`extract_digits_tesseract_from_image()`, `extract_table(engine="tesseract")`)
- **docTR CPU** : moteur de zonage pour band patient, synthèse, bboxes
- **Preprocessing OpenCV** : ❌ reporté (régression mesurée)
- **PaddleOCR** : ❌ post-démo
- **VLM OCR texte** : ❌ exclu J-6
### Workflow
- `Demo_urgence_3_db` / `wf_483910cdd851_1778750587` : step `extract_table` → Tesseract
- BDD backupée : `workflows.db.backup_2026-05-26_ocr_tesseract_demo3`
- 5 onglets préparés, live prudent possible en 4 si scroll échoue
### Démo
- Cible : 2026-06-01
- Répétition humaine : demain (Dom challengeur)
- Dossier cible : `MOREL Catherine / IPP 25003284`
- Sortie : `.xlsx` ouvert dans OnlyOffice (`/snap/bin/onlyoffice-desktopeditors`)
- Profil démo Linux actif (flags skip vision, EasyOCR CPU)
---
## Documents actifs à connaître
### Sources actives prioritaires
1. `docs/coordination/active/2026-05-26_cadrage-produit-aiva-vision.md`
2. `docs/coordination/active/2026-05-26_arbitrage-dom-demo-reelle-poc.md`
3. `docs/coordination/active/2026-05-26_principe-dom-apprentissage-fail-safe.md`
4. `docs/coordination/active/2026-05-26_scenario-operatoire-demo-lea-v2-collecte-transposition.md`
5. `docs/coordination/active/2026-05-26_audit-ancien-workflow-urgence-aiva.md`
### Documents ajoutés ce soir
- `docs/coordination/active/2026-05-26_benchmark-ocr-local-captures-easily.md`
- `docs/coordination/active/2026-05-26_arbitrage-scroll-vwb-reference.md`
- `docs/coordination/active/2026-05-26_principe-apprentissage-scroll-securise.md`
- `docs/coordination/active/2026-05-26_synthese-retours-claude-qwen-demo-v2-ocr.md`
- `docs/coordination/active/2026-05-26_dryrun-easily-v2-captures-ocr-onlyoffice.md`
- `docs/coordination/active/2026-05-26_arbitrage-sortie-transposition-onlyoffice.md`
- `docs/coordination/active/2026-05-26_mission-p0-ocr-ecran-lea.md`
- `docs/coordination/active/2026-05-26_mission-P0-ocr-ecran-qwen.md`
### Runbook
- `docs/coordination/active/2026-05-26_runbook-repetition-humain-challenge-demo-v2.md`
---
## Fichiers produits ce soir (inbox_codex)
| Fichier | Type |
|---------|------|
| `2026-05-26_2044_qwen-to-codex_REPRISE-analyse-scenario-v2.md` | Analyse initiale |
| `2026-05-26_2050_qwen-to-codex_DELTA-apres-lecture-sources-actives.md` | Delta sources |
| `2026-05-26_2101_qwen-to-codex_AUDIT-technique-dryrun-onlyoffice.md` | Audit technique |
| `2026-05-26_2113_qwen-to-codex_SEUILS-fallbacks-apres-dryrun.md` | Seuils/fallbacks |
| `2026-05-26_2117_qwen-to-codex_RAPPORT-P0-ocr-ecran.md` | Rapport OCR (mis à jour) |
| `2026-05-26_2137_qwen-to-codex_SYNTHESE-benchmark-5-onglets.md` | Synthèse collectif |
| `2026-05-26_2148_qwen-to-codex_RETOUR-benchmark-ocr-capitalisation.md` | Retour benchmark |
| `2026-05-26_2149_qwen-to-codex_ACK-apprentissage-scroll-securise.md` | ACK scroll |
---
## Ce qui reste à faire / à surveiller demain
1. **Répétition humaine** — Dom challengeur, critères GO/NOGO stricts
2. **Résultats de la répétition** — ajuster si nécessaire
3. **Patchs potentiels** — selon résultats répétition (scroll, grounding)
4. **Préparation démo 2026-06-01** — J-5 après demain
---
## Mémoire construite ce soir
- `user/dom_constraints.md` — Limites non négociables Dom
- `project/aiva_vision_demo.md` — Contexte démo Aiva-vision
- `feedback/qwen_avoidances.md` — Ce que Qwen doit éviter
- `feedback/dom_doctr_preference.md` — DocTR puissant pour zonage
- `feedback/qwen_proactive_improvements.md` — Qwen doit proposer des idées
- `project/aiva_vision_product_philosophy.md` — Collaborateur administratif, pas RPA
- `reference/coordination_process.md` — Coordination par fichiers Markdown
---
## Notes personnelles Qwen
- Le positionnement produit est **collaborateur administratif supervisé**, pas RPA "boîte à clic"
- Notre avantage : "l'agent qui sait s'arrêter" — pas un bug, une feature en domaine réglementé
- Architecture : Aiva-vision (socle universel) + plugins métier (accélérateurs d'apprentissage)
- Cycle Léa : apprendre → essayer → se planter → humain rattrape → consolide → indépendant
- L'exploration web montre que personne n'a résolu le computer use fiable (Claude 22%, OpenAI a abandonné)
---
*Auteur : Qwen*

View File

@@ -0,0 +1,208 @@
# Prompt reprise Qwen — Aiva-vision / Léa / Aiva-urgence
- `Auteur`: Codex
- `Date`: 2026-05-26 20:41 Europe/Paris
- `Contexte`: remplacement de Gemini comme collègue d'analyse
- `Objectif`: permettre à Qwen de contribuer efficacement à la préparation démo 2026-06-01
## Ton rôle
Tu rejoins une équipe de coordination autour du projet **Aiva-vision**.
Tu ne pilotes pas le runtime. Tu es attendu comme collègue d'analyse technique et produit :
- relire les sources actives ;
- détecter les contradictions ;
- challenger les risques ;
- proposer des checklists courtes ;
- aider à préparer une démo réelle, solide et défendable pour les POC.
Tu dois répondre de façon opérationnelle, courte, structurée, sans tunnel de réflexion.
## Produit
### Aiva-vision
Aiva-vision est la plateforme générique.
Elle apprend des interfaces existantes, interagit avec elles, et permet de greffer des plugins métier.
Le positionnement est universel : santé aujourd'hui, aéronautique ou autres secteurs demain.
### Léa
Léa est l'agent d'interaction d'Aiva-vision.
Elle doit :
- observer l'écran ;
- lire les données affichées ;
- dialoguer avec l'utilisateur ;
- demander confirmation ;
- reporter des informations dans d'autres outils ;
- apprendre quand elle ne sait pas ;
- s'arrêter plutôt que faire une mauvaise action.
### Aiva-urgence
Aiva-urgence est le plugin métier santé utilisé pour la démo.
Il aide à traiter des dossiers urgences et à qualifier/requalifier un passage :
- Forfait Urgences ;
- hospitalisation / requalification UHCD-MCO.
## Cadrage démo
Le client a déjà vu un vrai scénario filmé. Le 2026-06-01, il veut voir en vrai.
Dom impose :
- pas de trucage ;
- pas de bidouillage ;
- pas de succès simulé ;
- pas de hardcode pour faire illusion ;
- si Léa ne sait pas, elle s'arrête et demande de l'aide.
Dans le monde hospitalier, un arrêt sûr est préférable à une action dangereuse.
La démo est courte, mais elle prépare les POC.
## Scénario cible actuel
La base opérationnelle est :
`docs/coordination/active/2026-05-26_scenario-operatoire-demo-lea-v2-collecte-transposition.md`
Résumé :
1. Léa observe la liste des passages aux urgences.
2. Léa décrit le tableau : colonnes, dossiers, statuts.
3. Léa propose : traiter tous les dossiers ou un dossier en particulier.
4. Dom choisit un dossier, probablement `MOREL Catherine / 25003284`.
5. Léa ouvre le dossier.
6. Léa parcourt les onglets :
- `Motif d'admission`
- `Examens cliniques`
- `Imagerie`
- `Notes médicales`
- `Synthèse Urgences`
7. Léa collecte les informations, y compris avec scroll/ascenseur si nécessaire.
8. Léa demande où consigner les informations :
- Excel ;
- Word ;
- base de données ;
- autre plateforme/environnement.
9. Léa reporte les informations collectées.
10. Léa s'arrête pour validation humaine.
Le coeur de la démonstration : **lecture écran précise -> collecte multi-onglets -> choix utilisateur -> transposition dans un autre outil**.
## État technique connu
Validé :
- Smoke Bloc-notes `replay_sess_1c0bfb42` : `16/16`, `0 failed`, `0 retries`, `0 pause Léa`.
- Profil démo Linux actif :
- `RPA_SKIP_INTENTION_ENRICHMENT=true`
- `RPA_SKIP_BUILD_VISION=true`
- `RPA_EASYOCR_GPU=0`
- `AGENT_CHAT_ENABLE_OWL=0`
- `AGENT_CHAT_ENABLE_UI_DETECTION=0`
- Runtime démo visuel : `qwen2.5vl` bbox legacy.
- Ne pas migrer globalement vers `qwen3.5`.
- `qwen3.5` reste benchmark/API préparatoire.
- Patch OCR préfixe `Enregi` / `Enregistrer` ACK, seuil 50%.
- EasyOCR GPU désactivé par défaut.
- D5-v3c Windows `num_ctx=8192` reporté post-démo.
Maquette :
- service `rpa-mockup-easily.service` actif ;
- endpoint `http://127.0.0.1:8765/healthz` OK ;
- maquette Windows attendue : `http://192.168.1.40:8765/index.html`.
Important :
- `/api/analyse` utilise le modèle texte `qwen2.5:7b`, pas `qwen2.5vl`.
- `qwen2.5vl:7b-rpa` sert au grounding visuel/replay.
- l'ancien workflow `Urgence_aiva_demo` contient des briques utiles mais ne doit pas être rejoué tel quel.
## Documents à lire dans cet ordre
1. `docs/coordination/active/2026-05-26_cadrage-produit-aiva-vision.md`
2. `docs/coordination/active/2026-05-26_arbitrage-dom-demo-reelle-poc.md`
3. `docs/coordination/active/2026-05-26_principe-dom-apprentissage-fail-safe.md`
4. `docs/coordination/active/2026-05-26_scenario-operatoire-demo-lea-v2-collecte-transposition.md`
5. `docs/coordination/active/2026-05-26_audit-ancien-workflow-urgence-aiva.md`
Compléments utiles :
- `docs/coordination/inbox_codex/2026-05-26_1030_claude-to-codex_CHECKLIST-easily-capture-trace.md`
- `docs/coordination/inbox_codex/2026-05-26_1145_gemini-to-codex_REPONSE-demo-metier-risques.md`
- `docs/handoffs/PROMPT_REPRISE_CODEX_2026-05-25_SOIR.md`
## Méthode de travail
Le repo utilise une coordination par fichiers Markdown.
Pour écrire à Codex :
`docs/coordination/inbox_codex/YYYY-MM-DD_HHMM_qwen-to-codex_SUJET.md`
Format attendu :
- `De`: Qwen
- `A`: Codex
- `Date`: horodatage Europe/Paris
- `Statut`: ACK / NACK / proposition
- réponse courte et actionnable.
Ne pas écrire de rapport long sauf demande explicite.
Toujours distinguer :
- faits vérifiés ;
- hypothèses ;
- recommandations ;
- risques bloquants.
## Limites non négociables
- Pas de replay live sans GO explicite Dom.
- Pas de redéploiement Windows.
- Pas de restart service sans demande Codex/Dom.
- Pas de migration globale modèle.
- Pas de `RPA_GROUNDING_MODEL=qwen3.5:9b`.
- Pas de patch runtime sans demande.
- Pas de secret dans les docs.
- Pas de patient réel : la maquette est fictive.
- Pas de proposition cosmétique ou truquée.
## Ce que tu dois éviter
- Ne pas vendre une autonomie totale immédiate.
- Ne pas réduire Aiva-vision à la maquette Easily.
- Ne pas présenter Aiva-urgence comme tout le produit.
- Ne pas proposer l'ancien `Urgence_aiva_demo` comme scénario tel quel.
- Ne pas recommander une action risquée pour "faire passer la démo".
- Ne pas inventer des capacités absentes du code.
## Première tâche attendue
Après lecture des documents prioritaires, produis un fichier :
`docs/coordination/inbox_codex/YYYY-MM-DD_HHMM_qwen-to-codex_REPRISE-analyse-scenario-v2.md`
Contenu demandé :
1. ACK/NACK du cadrage produit.
2. Résumé du scénario v2 en 8 lignes maximum.
3. 5 risques NOGO maximum.
4. 5 critères de réussite client maximum.
5. Les 3 points techniques à vérifier avant capture.
6. Une seule recommandation prioritaire pour rendre Léa "super solide et agile" sur cette démo.
Ne propose pas encore de patch.
Auteur : Codex

View File

@@ -0,0 +1,257 @@
# Backlog Horizon 1 Lea-first -- 2026-05-19
**Objet**
- Transformer `Horizon 1` en backlog d'execution concret.
- Se limiter au coeur produit `Lea-first`.
- Ne pas reintroduire `VWB` ou la demo comme reference produit.
## 1. Definition de done
Horizon 1 est termine quand les conditions suivantes sont vraies :
- un workflow Lea-first canonique peut etre capture puis rejoue sans passer par `VWB`
- `pause -> intervention humaine -> resume` est fiable et comprehensible
- les succes, echecs et corrections humaines alimentent vraiment la memoire persistante
- on dispose d'un gate minimal reproductible avant toute session terrain
- on dispose d'un runbook operatoire court pour lancer, observer et reprendre un replay
## 2. Baseline technique au 2026-05-19
### Pipeline reel retenu
Le chemin nominal reste :
`capture Lea -> stream serveur -> replay direct -> verification -> memory`
Fichiers pivots :
- `agent_v0/agent_v1/main.py`
- `agent_v0/agent_v1/core/executor.py`
- `agent_v0/server_v1/api_stream.py`
- `agent_v0/server_v1/stream_processor.py`
- `agent_v0/server_v1/resolve_engine.py`
- `agent_v0/server_v1/replay_learner.py`
- `agent_v0/server_v1/replay_memory.py`
- `core/learning/target_memory_store.py`
### Gate minimal deja vert
Tests executes localement le 2026-05-19 :
- `pytest -q tests/integration/test_replay_resume_acknowledgments.py tests/integration/test_loop_detector_replay.py tests/unit/test_target_memory_store.py`
- `pytest -q tests/integration/test_stream_processor.py tests/smoke/test_smoke_e2e_minimal.py`
- `pytest -q tests/integration/test_pause_for_human.py tests/unit/test_replay_critic.py`
Conclusion :
- les briques `resume`, `loop detector`, `target memory`, `stream processor` et le smoke critique offline sont deja exploitables comme premier garde-fou
- `pause_for_human` et `replay_critic` peuvent rejoindre ce garde-fou immediatement
- il reste un warning `pytest-asyncio` de configuration de boucle, non bloquant pour Horizon 1
- `tests/unit/test_policy_grounding_recovery_learning.py` n'est pas encore un gate portable : 4 tests `RecoveryEngine` tombent localement sur `ModuleNotFoundError: pynput`
- ce gate ne prouve pas encore le replay Windows reel de bout en bout
## 3. Ordre d'attaque
### Chantier H1-A -- Stabiliser le replay nominal
**But**
- faire du replay direct le chemin assume et observable
**Fichiers cibles**
- `agent_v0/server_v1/stream_processor.py`
- `agent_v0/server_v1/api_stream.py`
- `agent_v0/agent_v1/main.py`
- `agent_v0/agent_v1/core/executor.py`
**Taches**
- figer un workflow Lea-first canonique de reference
- verifier que `build_replay_from_raw_events(...)` produit des actions propres sur ce workflow
- verifier le couplage `session_id / machine_id / replay queue` pour eviter les confusions inter-machines
- tracer explicitement le passage `capture finalisee -> replay injecte -> action poll -> resultat recu`
**Acceptation**
- a partir d'une capture Lea, le serveur injecte une queue de replay exploitable sans bridge VWB
- l'agent recupere les actions avec le bon `machine_id`
- l'index courant du replay et le nombre d'actions restantes restent coherents jusqu'a completion ou pause
**Gate associe**
- `tests/integration/test_stream_processor.py`
- `tests/smoke/test_smoke_e2e_minimal.py`
### Chantier H1-B -- Fiabiliser la pause supervisee
**But**
- rendre deterministe et lisible la boucle `pause / aide humaine / reprise`
**Fichiers cibles**
- `agent_v0/server_v1/api_stream.py`
- `agent_v0/server_v1/loop_detector.py`
- `agent_v0/agent_v1/network/feedback_bus.py`
- `agent_v0/agent_v1/ui/chat_window.py`
**Taches**
- formaliser la matrice des causes de `paused_need_help`
- distinguer les pauses qui doivent reinjecter l'action de celles qui ne doivent pas la reinjecter
- verifier l'alignement entre payload serveur, bulle Lea et endpoint `/replay/{id}/resume`
- verifier que les acquittements de `safety_checks` restent obligatoires quand requis
**Causes a couvrir explicitement**
- `pause_for_human`
- `target_not_found`
- `wrong_window`
- `no_screen_change_strict`
- `system_dialog`
- `loop_detected`
**Acceptation**
- chaque pause expose une raison, un message et un comportement de reprise clairs
- `resume` ne relance pas une action qui etait une pause intentionnelle
- `cancel` vide proprement la queue
- aucun cas de pause ne laisse le replay dans un etat ambigu
**Gate associe**
- `tests/integration/test_replay_resume_acknowledgments.py`
- `tests/integration/test_loop_detector_replay.py`
- `tests/integration/test_pause_for_human.py`
### Chantier H1-C -- Faire apprendre la memoire de replay
**But**
- s'assurer que la boucle d'apprentissage est branchee sur le runtime reel, pas seulement documentee
**Fichiers cibles**
- `agent_v0/server_v1/api_stream.py`
- `agent_v0/server_v1/replay_learner.py`
- `agent_v0/server_v1/replay_memory.py`
- `agent_v0/server_v1/resolve_engine.py`
- `core/learning/target_memory_store.py`
**Taches**
- verifier le contrat `actual_position` entre agent et serveur
- verifier que `memory_record_success()` ne se declenche qu'apres succes verifie
- verifier que `memory_record_failure()` decremente bien la fiabilite utile
- verifier que `record_human_correction()` nourrit bien `target_memory.db`
- tracer un cas de `memory_lookup HIT` jusqu'a la resolution suivante
**Acceptation**
- au moins un cas reel montre la sequence :
`target_not_found ou no_screen_change_strict -> correction humaine -> stockage -> hit memoire sur replay suivant`
- les fichiers `data/learning/replay_results/*.jsonl` et `data/learning/events/*/resolution_events.jsonl` sont utiles et lisibles
- `target_memory.db` contient des entrees correspondant a des cibles reelles du workflow canonique
**Gate associe**
- `tests/unit/test_target_memory_store.py`
- `tests/unit/test_policy_grounding_recovery_learning.py`
- `tests/unit/test_replay_critic.py`
### Chantier H1-D -- Runbook et gate terrain
**But**
- disposer d'un protocole d'execution simple avant toute session reelle
**Fichiers cibles**
- `agent_v0/deploy/test_replay_diag.py`
- `tests/smoke/test_smoke_e2e_minimal.py`
- nouvelle doc de runbook Lea-first
**Taches**
- definir un gate local minimum avant session terrain
- definir un workflow canonique a jouer a blanc avant toute session utilisateur
- ecrire une checklist courte `avant capture / avant replay / pendant pause / apres correction`
- rendre explicites les artefacts a conserver apres un incident
**Acceptation**
- on sait dire en moins de 5 minutes si la machine est "prete a rejouer"
- on sait quoi collecter en cas d'echec sans improviser
- on sait faire une reprise propre apres une pause supervisee
**Gate associe**
- `python agent_v0/deploy/test_replay_diag.py`
- `pytest -q tests/smoke/test_smoke_e2e_minimal.py`
## 4. Sequence immediate
### Etape 1 -- Geler le gate Horizon 1
- conserver les 5 fichiers de test deja verts comme premier noyau
- ajouter maintenant `tests/integration/test_pause_for_human.py`
- ajouter maintenant `tests/unit/test_replay_critic.py`
- garder `tests/unit/test_policy_grounding_recovery_learning.py` en seconde couronne tant que la dependance `pynput` n'est pas geree proprement pour le contexte de test
### Etape 2 -- Choisir le workflow canonique
- un seul workflow Lea-first
- court
- peu de fenetres
- au moins un clic, une saisie, une verification, une pause possible
### Etape 3 -- Prioriser les corrections
Ordre de traitement :
1. ambiguite de replay `session_id / machine_id / queue`
2. cas de pause/reprise incoherents
3. apprentissage non persiste ou non reutilise
4. runbook et collecte d'artefacts
## 5. Integration de l'audit Claude
Reference :
- [AUDIT_LEA_FIRST_RUNTIME_2026-05-19.md](/home/dom/ai/rpa_vision_v3/docs/AUDIT_LEA_FIRST_RUNTIME_2026-05-19.md)
Important :
- dans ce repo, `agent_v0/` reste le **conteneur de chemin** du code courant
- l'audit qui parle de `agent_v0/agent_v1` et `agent_v0/server_v1` parle donc bien du runtime actuel
### Points valides
- `record_human_correction()` est reellement casse
- la capture via `mss.monitors[1]` n'a pas de garde contre des dimensions aberrantes
- `_replay_active` est mal gere cote client pendant `replay_paused=true`
### Points a corriger dans la formulation
- le replay direct **existe bien** via `POST /api/v1/traces/stream/replay-session`
- ce endpoint charge `live_events.jsonl`, appelle `build_replay_from_raw_events(...)`, puis injecte la queue de replay
- en revanche, `finalize` n'enchaine pas ce chemin automatiquement et la surface produit reste morcelee
- `build_workflow_replay()` est bien orphelin, mais cela ne prouve pas que le replay direct n'existe pas
- le gating memoire sur `window_title` est reel
- mais sur le flux Lea-first natif, `build_replay_from_raw_events(...)` propage deja `window_title` dans les clics enrichis
- le risque est donc surtout : actions non enrichies, cas degradés, et workflows non Lea-first ou incompletés
- `core/learning/*` non branche au runtime serveur est un constat juste
- mais pour Horizon 1, ce n'est pas un blocage du coeur produit si on assume que `TargetMemoryStore` est la seule boucle d'apprentissage necessaire court terme
### Reclassement pratique pour Horizon 1
- `P0 reel` : correction humaine cassee
- `P0 reel` : corruption possible des captures moniteur
- `P1 reel` : pause/reprise desynchronisee cote client
- `P1 structurant` : absence de chainage produit propre `finalize -> replay-session`
- `P1 risque` : memoire trop dependante de `window_title`
- `P2 assume` : branches `core/learning/*` hors chemin nominal court terme
## 6. Ce que je retiens pour la suite immediate
Ordre de traitement mis a jour :
1. corriger `record_human_correction()` pour rouvrir l'apprentissage supervise
2. blinder la capture moniteur contre les dimensions aberrantes
3. corriger `_replay_active` sur `action=null + replay_paused=true`
4. definir si `finalize` doit proposer ou lancer explicitement `replay-session`
5. renforcer le fallback `window_title` pour le record / lookup memoire
## 7. Premier ticket d'execution
Le premier ticket que je lancerais sans attendre est :
**"Corriger `record_human_correction()` et valider un vrai cycle correction -> target_memory.db -> hit memoire."**
Pourquoi :
- c'est le bug le plus net et le plus rentable de l'audit
- il touche directement la promesse produit Lea-first
- il fournit ensuite un cas de validation concret pour Horizon 1

View File

@@ -0,0 +1,313 @@
# Cadrage coeur produit Lea -- 2026-05-19
**But**
- Repartir du vrai produit.
- Ne pas confondre le coeur `Lea-first` avec la demo recente ni avec le VWB.
- Distinguer ce qui est **branche au runtime**, ce qui est **partiellement branche**, et ce qui releve surtout de `VWB/demo/veille`.
## 1. Definition du coeur produit
Le coeur produit n'est pas :
- un canvas de creation manuelle de workflows
- un assembleur de demo
- un bench de modeles
Le coeur produit est :
- **un agent local Lea** qui observe un humain,
- **un serveur de streaming/replay** qui reconstruit une procedure exploitable,
- **une boucle d'apprentissage** qui s'ameliore avec les replays, les verifications et les corrections humaines.
En une ligne :
`capture Lea -> streaming serveur -> reconstruction procedure -> replay supervise -> apprentissage persistant`
## 2. Ce qui est reellement branche aujourd'hui
### A. Capture Lea -> serveur : OUI, c'est le vrai chemin de base
Cote client :
- `agent_v0/agent_v1/main.py`
- `agent_v0/agent_v1/core/captor.py`
- `agent_v0/agent_v1/network/streamer.py`
Ce que fait Lea aujourd'hui :
- capture evenements clavier/souris
- capture screenshots/crops
- ajoute du contexte UIA et fenetre
- streame vers le serveur via :
- `/register`
- `/event`
- `/image`
- `/finalize`
Conclusion :
- **la capture et le streaming Lea sont bien le socle produit deja branche**
### B. Reconstruction immediate d'un replay depuis la capture : OUI
Cote serveur :
- `agent_v0/server_v1/stream_processor.py`
- fonction `build_replay_from_raw_events(...)`
Ce chemin prend `live_events.jsonl` et produit directement :
- des actions rejouables
- des clics enrichis visuellement
- des intentions et screenshots attendus
- une consolidation avec l'historique d'apprentissage
Conclusion :
- **il existe deja un chemin capture -> replay exploitable sans passer par le VWB**
- c'est un candidat fort pour le vrai noyau produit court terme
### C. Replay d'execution Lea : OUI
Cote serveur :
- `agent_v0/server_v1/api_stream.py`
- `agent_v0/server_v1/replay_engine.py`
- `agent_v0/server_v1/resolve_engine.py`
Cote client :
- `agent_v0/agent_v1/main.py`
- `agent_v0/agent_v1/core/executor.py`
Ce qui est branche :
- le serveur alimente une queue de replay
- Lea poll `/replay/next`
- Lea execute
- le serveur resout les cibles, verifie, enregistre les resultats
Conclusion :
- **le replay Lea pilote par serveur est bien un axe central du produit**
### D. Apprentissage persistant sur le replay : OUI, mais partiel
Modules reels :
- `agent_v0/server_v1/replay_learner.py`
- `agent_v0/server_v1/replay_memory.py`
- `core/learning/target_memory_store.py`
Ce qui est reellement branche :
- historisation des resultats d'actions de replay
- stockage des corrections humaines
- lookup memoire avant cascade de resolution couteuse
- enregistrement des succes verifies apres action
Conclusion :
- **la boucle d'apprentissage "apres replay" existe vraiment**
- **c'est aujourd'hui le morceau le plus concret de l'apprentissage Lea**
## 3. Ce qui existe mais n'est pas encore le coeur runtime
### A. ShadowObserver / ShadowValidator : presents, mais pas au centre
Modules :
- `core/workflow/shadow_observer.py`
- `core/workflow/shadow_validator.py`
- endpoints `api_stream.py` : `/api/v1/shadow/*`
Constat important :
- le client Lea ne les appelle pas nativement
- aucun appelant clair n'apparait dans `agent_v0/agent_v1/`
- aucun appelant clair n'apparait non plus dans `visual_workflow_builder/` ou `agent_chat/`
Conclusion :
- **le mode shadow est une brique prometteuse, mais pas encore un flux produit dominant**
### B. WorkflowIR -> ExecutionPlan : presents, mais pas encore la voie nominale
Modules :
- `core/workflow/ir_builder.py`
- `core/workflow/execution_compiler.py`
- `agent_v0/server_v1/execution_plan_runner.py`
- endpoints `api_stream.py` :
- `/api/v1/traces/stream/workflow/compile`
- `/api/v1/traces/stream/replay/plan`
Constat :
- ce pipeline est bien code
- il compile une session en artefacts plus propres et plus deterministes
- mais il n'est pas pilote naturellement par le client Lea
- il ne semble pas etre le chemin principal utilise au quotidien
Conclusion :
- **c'est probablement la meilleure direction architecturale**
- **ce n'est pas encore le coeur produit effectivement adopte**
### C. WorkflowPipeline / GraphBuilder / LearningManager : reels, mais plutot couche moteur
Modules :
- `core/pipeline/workflow_pipeline.py`
- `core/graph/graph_builder.py`
- `core/learning/learning_manager.py`
Constat :
- ce socle exprime tres bien l'ambition du produit
- il construit des workflows depuis les sessions
- il gere des etats d'apprentissage
Mais :
- `LearningManager` est en memoire, pas le centre d'un flux de promotion produit visible
- cette couche n'est pas encore le protocole operatoire dominant Lea
Conclusion :
- **c'est le moteur conceptuel**
- **pas encore la boucle produit la plus nette pour les 15 prochains jours**
## 4. Ce qui releve du VWB, pas du coeur produit
Le VWB sert aujourd'hui surtout a :
- creer ou editer manuellement des workflows
- importer des workflows appris
- exporter un workflow vers Lea
- lancer une execution via proxy vers le streaming server
Indices forts dans le repo :
- `visual_workflow_builder/backend/api_v3/learned_workflows.py`
- `visual_workflow_builder/backend/api_v3/execute.py`
- `visual_workflow_builder/frontend_v4/src/services/api.ts`
Le VWB n'est donc pas :
- la source unique de l'apprentissage Lea
- le protocole naturel de capture du savoir-faire
- la definition du produit coeur
Conclusion :
- **le VWB est un outil satellite d'authoring, d'inspection et de bridge**
- **il ne doit pas definir la direction du produit Lea-first**
## 5. Ce qui releve surtout de la demo ou de la veille
Hors coeur produit immediat :
- workflows reconstruits manuellement pour la demo
- corrections specifiques a la maquette de demo
- benches de modeles et comparatifs
- experiments UX ou UX debug non relies a la capture/replay Lea
- documentation strategique ou speculative non branchee
Cela peut avoir de la valeur :
- comme preuve
- comme R&D
- comme materiau commercial
Mais ce n'est pas le socle a partir duquel il faut juger l'etat du produit.
## 6. Matrice simple
### A. Coeur produit branche aujourd'hui
- capture client Lea
- streaming temps reel
- replay serveur -> client Lea
- resolution visuelle au runtime
- apprentissage persistant post-replay
### B. Coeur produit partiellement branche
- observation shadow pendant l'enregistrement
- validation/correction structurée du savoir-faire
- compilation systematique en WorkflowIR/ExecutionPlan
- promotion explicite observation -> coaching -> autonomie
### C. Satellite produit
- VWB
- dashboard de supervision
- agent_chat
### D. Hors coeur / exploration
- benches modeles
- comparatifs frameworks
- documents de vision non encore materialises dans le chemin runtime
## 7. Ce qui manque pour que Lea "apprenne vraiment"
### 1. Un chemin unique assume
Aujourd'hui, plusieurs chemins coexistent :
- capture -> replay direct
- capture -> GraphBuilder/workflow
- capture -> shadow -> WorkflowIR
- WorkflowIR -> ExecutionPlan -> replay
Probleme :
- tant qu'un chemin nominal n'est pas assume, l'energie se disperse
Ce qu'il faut :
- choisir un pipeline principal
- releguer les autres en support, migration ou R&D
### 2. Une validation utilisateur native sur le chemin Lea
Le mode shadow et la validation structurée existent, mais ne semblent pas au centre de l'usage client.
Ce qu'il faut :
- que Lea sache naturellement :
- observer
- montrer ce qu'elle a compris
- recevoir correction/validation
- cristalliser un artefact rejouable
### 3. Une vraie politique de promotion de l'apprentissage
Le repo contient des concepts d'etat d'apprentissage, mais pas encore un protocole operatoire simple du style :
- observe N fois
- replaye sous supervision
- apprend des corrections
- passe en autonomie sur critere
Ce qu'il faut :
- des seuils, des traces, et un etat visible par workflow
### 4. Une alimentation effective de la memoire
La memoire persistante est branchee, mais sa valeur depend de :
- replays repetes
- validations reelles
- corrections humaines capturees proprement
Ce qu'il faut :
- 1 ou 2 cas terrain qui nourrissent vraiment la memoire
- une verification que la base d'apprentissage grossit utilement
## 8. Consequence pratique pour la remise en ordre
Si on parle du vrai produit, il faut travailler dans cet ordre :
1. **Cartographier et assumer le pipeline nominal Lea-first**
2. **Verifier le flux capture -> replay -> apprentissage**
3. **Decider ce qui, dans shadow/IR/ExecutionPlan, doit devenir le prochain cran officiel**
4. **Sortir le VWB du role de reference produit**
5. **Ranger la veille et la demo comme materiaux satellites**
## 9. Point de depart recommande
Le bon point de depart n'est pas :
- la demo recente
- le VWB
- la doc speculative
Le bon point de depart est :
- `agent_v0/agent_v1/`
- `agent_v0/server_v1/`
- `core/learning/`
- `core/workflow/` uniquement pour distinguer le branche du promis
## 10. Decision simple
**Formulation recommandee**
"Le produit coeur est Lea qui observe, rejoue et apprend.
Le VWB est un outil satellite.
Le pipeline nominal doit etre pense depuis Lea, pas depuis le canvas."
## 11. Sources principales
- [agent_v0/agent_v1/main.py](/home/dom/ai/rpa_vision_v3/agent_v0/agent_v1/main.py)
- [agent_v0/agent_v1/network/streamer.py](/home/dom/ai/rpa_vision_v3/agent_v0/agent_v1/network/streamer.py)
- [agent_v0/server_v1/stream_processor.py](/home/dom/ai/rpa_vision_v3/agent_v0/server_v1/stream_processor.py)
- [agent_v0/server_v1/api_stream.py](/home/dom/ai/rpa_vision_v3/agent_v0/server_v1/api_stream.py)
- [agent_v0/server_v1/replay_learner.py](/home/dom/ai/rpa_vision_v3/agent_v0/server_v1/replay_learner.py)
- [agent_v0/server_v1/replay_memory.py](/home/dom/ai/rpa_vision_v3/agent_v0/server_v1/replay_memory.py)
- [core/learning/target_memory_store.py](/home/dom/ai/rpa_vision_v3/core/learning/target_memory_store.py)
- [core/workflow/shadow_observer.py](/home/dom/ai/rpa_vision_v3/core/workflow/shadow_observer.py)
- [core/workflow/ir_builder.py](/home/dom/ai/rpa_vision_v3/core/workflow/ir_builder.py)
- [core/workflow/execution_compiler.py](/home/dom/ai/rpa_vision_v3/core/workflow/execution_compiler.py)
- [docs/VISION_RPA_INTELLIGENT.md](/home/dom/ai/rpa_vision_v3/docs/VISION_RPA_INTELLIGENT.md)
- [docs/CARTE_FONCTIONNELLE_2026-05-08.md](/home/dom/ai/rpa_vision_v3/docs/CARTE_FONCTIONNELLE_2026-05-08.md)

View File

@@ -0,0 +1,403 @@
# Cartographie executable du pipeline Lea-first -- 2026-05-19
**Objet**
- Decrire le pipeline reel du produit a partir du code branche.
- Montrer les artefacts, endpoints, workers et bifurcations.
- Distinguer clairement :
- le **nominal actuel**
- les **branches optionnelles**
- les **outils satellites** (`VWB`)
## 1. Vue d'ensemble
Le pipeline reel se lit aujourd'hui comme ceci :
```text
Lea (capture locale)
-> stream register/event/image/finalize
-> live session serveur
-> deux suites possibles
Suite A: replay direct
live_events.jsonl
-> build_replay_from_raw_events
-> queue de replay
-> Lea execute
-> verify + learning
Suite B: apprentissage structure
session finalisee
-> worker VLM
-> ScreenStates + embeddings
-> GraphBuilder
-> workflow JSON appris
Suite C: compilation V4 (optionnelle)
live_events.jsonl
-> IRBuilder
-> WorkflowIR
-> ExecutionCompiler
-> ExecutionPlan
-> replay/plan
```
## 2. Pipeline nominal actuel
### Etape 1 -- Capture locale par Lea
Fichiers principaux :
- `agent_v0/agent_v1/main.py`
- `agent_v0/agent_v1/core/captor.py`
- `agent_v0/agent_v1/network/streamer.py`
Flux :
1. `start_session()` cree `session_id`, `VisionCapturer`, `TraceStreamer`, `EventCaptorV1`.
2. `_on_event_bridge()` enrichit chaque action avec :
- `machine_id`
- `window`
- `screenshot_id`
- `vision_info`
- `window_capture`
3. Lea pousse :
- les evenements via `push_event`
- les screenshots via `push_image`
4. un `action_result` est capture 1s apres l'action
5. des heartbeats sont envoyes pendant l'enregistrement et hors enregistrement
Sorties locales utiles :
- session de capture locale Lea
- crops et screenshots associes
- stream HTTP vers le serveur
### Etape 2 -- Reception et persistance serveur
Fichiers principaux :
- `agent_v0/server_v1/api_stream.py`
- `agent_v0/server_v1/live_session_manager.py`
Endpoints principaux :
- `POST /api/v1/traces/stream/register`
- `POST /api/v1/traces/stream/event`
- `POST /api/v1/traces/stream/image`
- `POST /api/v1/traces/stream/finalize`
Ce qui est persiste :
- `data/training/live_sessions/{machine_id}/{session_id}/live_events.jsonl`
- `data/training/live_sessions/{machine_id}/{session_id}/shots/*.png`
- etat session en memoire via `LiveSessionManager`
Remarques importantes :
- les evenements sont journalises en JSONL
- les screenshots `shot_*_full` sont stockes sans analyse GPU immediate
- les heartbeats et screenshots de resultat sont gardes, mais pas tous analyses
### Etape 3 -- Generation immediate d'un replay direct
Fichiers principaux :
- `agent_v0/server_v1/stream_processor.py`
- `agent_v0/server_v1/api_stream.py`
Fonction centrale :
- `build_replay_from_raw_events(...)`
Ce qu'elle produit a partir de `live_events.jsonl` :
- actions normalisees
- fusion de texte saisi
- sanitization clavier
- `visual_mode` pour les clics
- `target_spec` enrichi
- `expected_screenshot_b64`
- `expected_window_title`
- `intention`
- consolidation avec l'historique du `ReplayLearner`
Conclusion :
- **c'est le chemin le plus direct entre observation Lea et replay exploitable**
### Etape 4 -- Replay serveur vers Lea
Fichiers principaux :
- `agent_v0/server_v1/api_stream.py`
- `agent_v0/server_v1/replay_engine.py`
- `agent_v0/server_v1/resolve_engine.py`
- `agent_v0/agent_v1/core/executor.py`
- `agent_v0/agent_v1/main.py`
Endpoints :
- `GET /api/v1/traces/stream/replay/next`
- `POST /api/v1/traces/stream/replay/result`
- `POST /api/v1/traces/stream/replay/{replay_id}/resume`
- `POST /api/v1/traces/stream/replay/{replay_id}/cancel`
Flux :
1. le serveur alimente une queue de replay
2. Lea poll `replay/next`
3. le serveur execute au besoin les actions serveur (`extract_text`, `t2a_decision`, etc.) avant de renvoyer une action visuelle
4. Lea execute l'action
5. Lea renvoie le resultat
6. le serveur verifie et met a jour l'etat du replay
Conclusion :
- **c'est le runtime produit actif**
### Etape 5 -- Verification et apprentissage post-replay
Fichiers principaux :
- `agent_v0/server_v1/replay_learner.py`
- `agent_v0/server_v1/replay_memory.py`
- `core/learning/target_memory_store.py`
Ce qui se passe apres chaque action :
- enregistrement du resultat d'action
- stockage des corrections humaines
- `memory_record_success()` apres succes verifie
- `memory_record_failure()` sur echec
- `memory_lookup()` avant la cascade de resolution au prochain replay
Artefacts d'apprentissage :
- `data/learning/replay_results/*.jsonl`
- `data/learning/events/YYYY-MM-DD/resolution_events.jsonl`
- `data/learning/target_memory.db`
Conclusion :
- **la memoire de replay est le bloc d'apprentissage le plus concret aujourd'hui**
## 3. Branche d'apprentissage structurel via worker
### Objectif
Construire un workflow appris a partir d'une session finalisee.
### Fichiers principaux
- `agent_v0/server_v1/run_worker.py`
- `agent_v0/server_v1/stream_processor.py`
- `core/pipeline/workflow_pipeline.py`
- `core/graph/graph_builder.py`
### Flux
1. `finalize` marque la session et l'ajoute a `data/training/_worker_queue.txt`
2. `run_worker.py` lit la queue
3. `reprocess_session()` recharge les screenshots `shot_*_full.png`
4. `process_screenshot()` produit :
- `ScreenState`
- embedding
- indexation FAISS
5. `finalize_session()` convertit vers `RawSession`
6. `GraphBuilder.build_from_session(...)` construit le workflow
7. `_persist_workflow()` sauvegarde le workflow appris
Artefacts :
- `data/training/_worker_queue.txt`
- `data/training/workflows/{machine_id}/wf_*.json`
Conclusion :
- **ce chemin construit du savoir-faire structure, mais en arriere-plan**
- **il n'est pas le replay nominal court terme**
## 4. Branche V4 compilee
### Objectif
Transformer une session en artefacts plus propres :
- `WorkflowIR`
- `ExecutionPlan`
### Fichiers principaux
- `core/workflow/ir_builder.py`
- `core/workflow/execution_compiler.py`
- `agent_v0/server_v1/execution_plan_runner.py`
- `agent_v0/server_v1/api_stream.py`
### Endpoints
- `POST /api/v1/traces/stream/workflow/compile`
- `POST /api/v1/traces/stream/replay/plan`
### Flux
1. lecture de `live_events.jsonl`
2. `IRBuilder.build(...)`
3. sauvegarde en `data/workflows_ir/*.json`
4. `ExecutionCompiler.compile(...)`
5. sauvegarde en `data/plans/*.json`
6. `execution_plan_to_actions(...)`
7. injection dans la queue de replay
Artefacts :
- `data/workflows_ir/*.json`
- `data/plans/*.json`
Conclusion :
- **c'est la branche architecturale la plus propre**
- **mais elle n'est pas encore pilotee naturellement par Lea**
## 5. Branche shadow
### Fichiers principaux
- `core/workflow/shadow_observer.py`
- `core/workflow/shadow_validator.py`
- `agent_v0/server_v1/api_stream.py`
### Endpoints
- `POST /api/v1/shadow/start`
- `POST /api/v1/shadow/stop`
- `POST /api/v1/shadow/feedback`
- `GET /api/v1/shadow/{session_id}/understanding`
- `POST /api/v1/shadow/build`
### Role
- observer la capture en temps reel
- construire une comprehension incrementale
- laisser l'utilisateur valider/corriger
- produire un `WorkflowIR`
### Constat
- le code est present
- le serveur l'accepte
- mais le client Lea ne semble pas le piloter nativement
Conclusion :
- **shadow est branche cote serveur**
- **shadow n'est pas encore la voie nominale Lea-first**
## 6. Role reel du VWB
### Ce qu'il fait
Fichiers principaux :
- `visual_workflow_builder/backend/api_v3/learned_workflows.py`
- `visual_workflow_builder/backend/api_v3/execute.py`
- `visual_workflow_builder/frontend_v4/src/services/api.ts`
Role :
- lister les workflows appris
- les importer dans le VWB
- les exporter vers Lea
- lancer une execution via proxy
Conclusion :
- **le VWB est un bridge et un outil d'authoring**
- **il ne doit pas etre lu comme le pipeline coeur du produit**
## 7. Artefacts de reference
### Capture live
- `data/training/live_sessions/{machine_id}/{session_id}/live_events.jsonl`
- `data/training/live_sessions/{machine_id}/{session_id}/shots/*.png`
### Sessions serveur en memoire / persistance
- `data/training/live_sessions/`
- `data/training/live_sessions/...`
### Queue worker
- `data/training/_worker_queue.txt`
- `data/training/_replay_active.lock`
### Workflows appris
- `data/training/workflows/{machine_id}/wf_*.json`
### Pipeline V4
- `data/workflows_ir/*.json`
- `data/plans/*.json`
### Apprentissage replay
- `data/learning/replay_results/*.jsonl`
- `data/learning/events/YYYY-MM-DD/resolution_events.jsonl`
- `data/learning/target_memory.db`
## 8. Ce qui est nominal, ce qui ne l'est pas
### Nominal actuel
- capture Lea
- streaming serveur
- replay direct
- verification
- memoire post-replay
### Non nominal mais reel
- worker qui fabrique un workflow appris
- compile V4 vers `ExecutionPlan`
- shadow avec validation
### Satellite
- import/export VWB
- edition manuelle VWB
- lancement VWB via proxy
## 9. Point de decision architectural
Le repo contient aujourd'hui **trois chemins serieux** :
### Option A -- assumer le direct replay comme v1 produit
Chemin :
- capture Lea
- `build_replay_from_raw_events`
- replay
- apprentissage memoire
Avantage :
- c'est le plus branche
- c'est le plus immediat
Limite :
- moins propre conceptuellement
- moins explicite sur la validation du savoir-faire
### Option B -- promouvoir `shadow -> WorkflowIR -> ExecutionPlan`
Chemin :
- capture Lea
- observation shadow
- correction/validation
- compilation
- replay de plan
Avantage :
- pipeline plus propre
- meilleure explicitation du savoir-faire
Limite :
- pas encore le flux naturel pilote par le client
### Option C -- continuer a passer par VWB
Ce n'est pas recommande comme coeur produit, car cela deplace la reference
du produit vers un outil satellite.
## 10. Recommandation de lecture
Si l'objectif est de remettre le produit en ordre, lire dans cet ordre :
1. `agent_v0/agent_v1/main.py`
2. `agent_v0/agent_v1/network/streamer.py`
3. `agent_v0/server_v1/api_stream.py`
4. `agent_v0/server_v1/stream_processor.py`
5. `agent_v0/server_v1/replay_learner.py`
6. `agent_v0/server_v1/replay_memory.py`
7. `core/workflow/ir_builder.py`
8. `core/workflow/execution_compiler.py`
9. `visual_workflow_builder/backend/api_v3/learned_workflows.py`
## 11. Conclusion courte
Le produit reel aujourd'hui n'est pas :
- `VWB -> workflow manuel -> replay`
Le produit reel aujourd'hui est plutot :
- `Lea observe -> le serveur reconstruit -> Lea rejoue -> le systeme apprend`
La vraie question n'est donc pas "faut-il garder le VWB au centre ?"
La vraie question est :
**quel pipeline Lea-first on officialise comme voie nominale :**
- le replay direct deja branche,
- ou la voie `shadow/IR/ExecutionPlan` qu'il faut maintenant faire monter en puissance ?

View File

@@ -0,0 +1,307 @@
# Feuille de route produit Lea-first -- 2026-05-19
**But**
- Transformer le recadrage technique en arbitrages produit.
- Dire clairement ce qui reste au centre, ce qui sort du centre, et ce qu'on fait monter en puissance.
- Donner une sequence d'execution lisible pour les prochaines semaines.
## 1. These produit
Le produit n'est pas un editeur de workflows.
Le produit est **Lea**, un agent local qui :
- observe un humain,
- rejoue une procedure,
- demande de l'aide quand elle ne sait pas,
- memorise les corrections,
- devient progressivement plus autonome.
La phrase de reference devrait etre :
**"Lea apprend par observation et se fiabilise par replay supervise."**
## 2. Decision structurante
### Ce qu'on garde au centre
- `agent_v0/agent_v1/`
- `agent_v0/server_v1/`
- `replay direct` depuis la capture
- `verification post-action`
- `memoire persistante` de replay
- `pause_for_human` et correction humaine
### Ce qu'on sort du centre
- `visual_workflow_builder/` comme chemin principal de creation
- la demo GHT comme reference produit
- les benches modeles comme moteur de roadmap
- `agent_chat/` comme facade prioritaire
- les analytics non necessaires a la boucle observation -> replay -> apprentissage
### Ce qu'on garde mais en satellite
- `VWB` pour inspection, import/export, correction manuelle ponctuelle
- dashboard pour supervision
- worker `GraphBuilder`
- pipeline `WorkflowIR -> ExecutionPlan`
## 3. Regle d'arbitrage
Une fonctionnalite est prioritaire si elle renforce au moins un de ces 4 axes :
1. Lea observe mieux
2. Lea rejoue plus surement
3. Lea apprend vraiment des corrections
4. Lea sait mieux quand s'arreter et demander de l'aide
Si une fonctionnalite ne renforce aucun de ces 4 axes, elle ne doit pas etre sur le chemin critique.
## 4. Ce qu'on garde
### A. Socle produit a conserver
- capture Lea enrichie (`captor`, `vision`, `streamer`)
- streaming serveur
- replay pilote par serveur
- `resolve_engine`
- `ReplayLearner`
- `replay_memory`
- `TargetMemoryStore`
- worker de retraitement session -> workflow appris
### B. Briques d'avenir a conserver
- `ShadowObserver`
- `ShadowValidator`
- `IRBuilder`
- `ExecutionCompiler`
- `ExecutionPlanRunner`
Pourquoi les garder :
- elles portent la meilleure cible architecturale moyen terme
- mais ne doivent pas dicter le court terme tant qu'elles ne sont pas sur le chemin nominal
## 5. Ce qu'on coupe du centre
### A. Couper du centre ne veut pas dire supprimer
On parle ici de **deprioriser**, pas de detruire.
### B. A sortir du role de reference produit
- `VWB` comme voie principale de creation de procedures
- workflows de demo recables a la main
- chasse aux bugs VWB sans impact sur Lea
- ajout de nouveaux blocs VWB non necessaires au produit
- travaux de design/UI autour d'un canvas qui n'est pas le coeur
### C. A stopper temporairement
- benchmarker de nouveaux modeles sans lien avec un echec produit concret
- etendre `agent_chat` tant que Lea n'est pas stabilisee
- pousser des analytics riches avant d'avoir une boucle d'apprentissage fiable
- melanger preuves demo, veille et architecture produit dans la meme priorite
## 6. Ce qu'on gele
### Gel court terme
- nouvelles features VWB
- nouvelles experiences demo-centriques
- nouvelles couches d'orchestration conversationales
- nouvelle dette de docs "vision" sans branchement runtime
### Autorise pendant le gel
- correctifs minimaux si un satellite bloque le coeur Lea-first
- documentation de clarification
- import/export necessaire pour debloquer un usage reel
## 7. Ce qu'on promeut
### Promotion immediate
**Voie nominale v1 produit**
`capture Lea -> replay direct -> verify -> memory`
C'est le pipeline a assumer officiellement maintenant.
### Promotion a court terme
- correction humaine explicite comme mecanisme produit central
- memoire persistante comme actif principal
- supervision humaine native
- runbook operatoire autour de Lea
### Promotion a moyen terme
**Voie nominale v2 cible**
`capture Lea -> shadow/validation -> WorkflowIR -> ExecutionPlan -> replay`
Cette voie doit etre promue seulement quand elle devient :
- plus simple a operer que le direct replay,
- et vraiment pilotee par Lea, pas par un outillage annexe.
## 8. Produit cible en 3 etages
### Etage 1 -- Lea Rejoue
Promesse :
- Lea peut rejouer de facon supervisee une procedure observee
Definition of done :
- 1 a 2 workflows reels reproductibles
- pause propre sur incertitude
- reprise fiable
- apprentissage des corrections
### Etage 2 -- Lea Apprend
Promesse :
- Lea rejoue mieux la 2e fois que la 1ere
Definition of done :
- `target_memory.db` utile et alimentee
- hits memoire visibles
- baisse des resolutions couteuses sur cas repetes
- historique de corrections exploitable
### Etage 3 -- Lea Comprend
Promesse :
- Lea sait expliciter ce qu'elle a compris pendant l'observation
Definition of done :
- flux shadow/validation utilisable sans bricolage externe
- generation d'un artefact rejouable propre
- promotion vers `ExecutionPlan`
## 9. Roadmap pratique
## Horizon 1 -- Stabiliser le produit reel
### Objectif
Faire de `capture -> replay direct -> apprentissage` un vrai produit POC.
### Priorites
1. Stabiliser le replay Lea sur 1 ou 2 cas reels
2. Rendre la pause supervisee propre et fiable
3. Aligner la verification et la memorisation
4. S'assurer que les corrections humaines nourrissent vraiment la memoire
5. Ecrire le runbook produit Lea
### Livrables
- pipeline nominal documente
- checklist de pre-replay
- 1 workflow canonique Lea-first
- 1 base de memoire non vide et utile
## Horizon 2 -- Native learning loop
### Objectif
Faire de la correction humaine un chemin naturel, pas un mecanisme cache.
### Priorites
1. rendre visible l'etat "je sais / je ne sais pas"
2. exposer clairement les corrections memorisees
3. definir les criteres de promotion d'un workflow
4. connecter proprement observation, replay et apprentissage
### Livrables
- etat d'apprentissage lisible par workflow
- journal des corrections humaines
- mesure simple du gain de memoire
## Horizon 3 -- Promotion de la voie compilee
### Objectif
Faire monter `shadow -> WorkflowIR -> ExecutionPlan` jusqu'a pouvoir remplacer la voie directe sur certains cas.
### Priorites
1. brancher un vrai appelant Lea vers `shadow`
2. rendre la validation utilisateur exploitable
3. compiler automatiquement une session validee
4. comparer replay direct vs replay compile sur un meme cas
### Livrables
- 1 flux shadow complet de bout en bout
- 1 `ExecutionPlan` reel lance sans bricolage annexe
- decision go/no-go sur la promotion en voie nominale
## 10. Non-objectifs explicites
Ce qu'on ne cherche pas a faire maintenant :
- faire du VWB le centre du produit
- multiplier les demos au prix du coeur technique
- construire un editeur BPMN generaliste
- benchmarker tout l'ecosysteme VLM
- transformer `agent_chat` en facade principale avant stabilisation Lea
## 11. Backlog dirigeant
### Maintenant
- replay direct fiable
- correction humaine exploitable
- memoire persistante alimentee
- runbook Lea-first
### Ensuite
- etat d'apprentissage visible
- shadow utile
- compile vers `ExecutionPlan`
### Plus tard
- VWB comme outil de correction avancée
- agent_chat comme surcouche
- analytics et reporting riches
## 12. Formulation de pilotage
Si tu dois recadrer une session de travail, la phrase a utiliser est :
**"Est-ce que ce qu'on fait aide Lea a mieux observer, rejouer, apprendre ou demander de l'aide ?"**
Si la reponse est non :
- ce n'est pas sur le chemin critique produit
## 13. Decision finale recommandee
### Voie nominale officielle a assumer maintenant
`Lea capture -> serveur construit un replay direct -> Lea execute -> le systeme apprend`
### Voie nominale cible a construire
`Lea capture -> Lea explicite ce qu'elle a compris -> validation -> ExecutionPlan -> replay`
### Statut du VWB
`outil satellite`
Pas :
- coeur produit
- reference d'architecture
- source de verite sur ce qu'est Lea
## 14. Documents lies
- [CADRAGE_COEUR_PRODUIT_LEA_2026-05-19.md](./CADRAGE_COEUR_PRODUIT_LEA_2026-05-19.md)
- [CARTOGRAPHIE_EXECUTABLE_PIPELINE_LEA_FIRST_2026-05-19.md](./CARTOGRAPHIE_EXECUTABLE_PIPELINE_LEA_FIRST_2026-05-19.md)
- [PLAN_REMISE_EN_ORDRE_POCS_2026-05-19.md](./PLAN_REMISE_EN_ORDRE_POCS_2026-05-19.md)
- [STATUS.md](/home/dom/ai/rpa_vision_v3/docs/STATUS.md)

View File

@@ -0,0 +1,111 @@
# Plan d'action collegues — apres C1/G2
Date : 2026-05-25 13:41 Europe/Paris
Pilotage : Codex
Cible demo : lundi 1 juin 2026
## Etat factuel
- `rpa-streaming` 5005 : actif.
- `rpa-agent-chat` 5004 : actif apres C1, SocketIO/CORS OK.
- Windows `LeaInteractive` : running, `LEA_FEEDBACK_BUS=1`.
- Inventaire Ollama v2 : 38 tags, reference actuelle.
- `qwen3.5:9b` : calibre avec prefill, resident en `CONTEXT=2048`, `100% GPU`, ~8.6 GB.
- `qwen2.5vl:7b-rpa` : point chaud precedent, peut revenir en `CONTEXT=8192` via appels hardcodes.
- C2 build replay : baseline mesuree 88-94s -> 22-24s avec skip enrichment, speedup ~4x.
## Probleme restant prioritaire
C1b a montre que le flag `AutonomousPlanner` ne suffit pas : `agent_chat` charge encore OWL-v2 via `WorkflowPipeline -> UIDetector` au boot.
Log observe :
```text
core.detection.owl_detector:Chargement OWL-v2 sur cuda...
core.detection.owl_detector:OWL-v2 chargé
core.detection.ui_detector:✓ OWL-v2 initialized
agent_chat.autonomous_planner:OWL-v2 visual detector skipped at boot
```
VRAM observee apres C1b restart :
```text
agent_chat python3 : 1478 MiB
ollama qwen3.5:9b : 7794-8566 MiB selon releve
```
Donc le chantier VRAM n'est pas termine.
## Repartition
### Claude — C1c runtime 5004 sans OWL GPU au boot
Objectif : `rpa-agent-chat` doit servir SocketIO 5004 sans charger OWL-v2/CUDA au boot.
Actions :
- Identifier le meilleur point de coupure : `agent_chat/app.py` instancie `WorkflowPipeline()` ; `WorkflowPipeline` instancie `UIDetector` avec `use_owl_detection=True`.
- Proposer patch minimal :
- soit `WorkflowPipeline(enable_ui_detection=False, enable_vlm=False)` dans `agent_chat` si ces fonctions sont hors chemin narration ;
- soit `DetectionConfig.use_owl_detection` pilote par env ;
- soit `AGENT_CHAT_ENABLE_OWL=0` applique aussi a `UIDetector`.
- Tests unitaires.
- Redemarrage seulement apres GO Codex.
- Critere : healthcheck OK, SocketIO OK, `agent_chat` VRAM nettement reduite, pas de log `Chargement OWL-v2 sur cuda`.
### Claude — C2b instrumentation des 22s restantes
Objectif : savoir ou part le build skip ~22-24s.
Actions :
- Ajouter des spans `[PERF]` autour de crops d'ancrage, cleaning, waits, ReplayLearner.
- Garder harnais `performance` manuel.
- Critere : tableau temps par segment, pas de replay live.
### Gemini — G2b benchmark qwen3.5 comparatif
Objectif : dire si `qwen3.5:9b` remplace `qwen2.5vl:7b-rpa`, devient fallback, ou est rejete.
Actions :
- Attendre que C1c stabilise VRAM si possible.
- Benchmark court puis complet :
- qwen3.5 avec prefill obligatoire ;
- `num_ctx=2048`, puis 4096 seulement avec GO ;
- 5 screenshots reels ;
- JSON validity, p50/p95, precision coordonnées, VRAM, offload.
- Comparer a `qwen2.5vl:7b-rpa` et `qwen2.5vl:3b` si cela ne perturbe pas un service actif.
- Critere : matrice de decision claire.
### Gemini — G3c patch proposal politique contexte
Objectif : fermer les retours accidentels en `CONTEXT=8192`.
Actions :
- Proposer patch minimal couvrant :
- executor Windows hardcodes 8192 ;
- `resolve_engine.py` appels sans `num_ctx` ;
- constante/env unique `RPA_VLM_DEFAULT_CTX=2048`.
- Ne pas appliquer.
- Inclure tests attendus et risques.
### Codex — arbitrage/integration
Actions :
- Maintenir healthcheck comme source de verite.
- Autoriser ou refuser les restarts.
- Integrer uniquement les patchs testables.
- Garder le store Ollama en lecture seule.
- Composer les decisions modele/runtime dans un plan demo.
## Ordre d'execution
1. C1c Claude : enlever le dernier chargement OWL GPU du 5004.
2. Healthcheck + VRAM.
3. G2b Gemini : benchmark qwen3.5 dans etat VRAM propre.
4. G3c Gemini + implementation Codex/Claude : politique contexte 2048.
5. C2b Claude : instrumentation 22s restantes.
6. Smoke replay controle, seulement apres stabilisation.

View File

@@ -0,0 +1,274 @@
# Plan Phase 2 — Trace mandat / protocoles
Date : 2026-05-25
Statut : plan d'exécution chirurgical, issu du modèle v0.3
Principe : additif, testable, sans refactor des gros blocs fragiles
## Objectif
Faire circuler un contrat commun dans Léa :
```text
mandat -> intention -> scène -> affordance -> geste -> retour -> preuve
```
Le but n'est pas de remplacer le replay actuel. Le but est de l'enrichir pour que les décisions, vérifications et apprentissages parlent la même langue.
## Règles de conduite
```text
Pas de replay live tant que les correctifs locaux ne sont pas testés.
Pas de refactor de api_stream.report_action_result.
Pas de refactor de resolve_engine._resolve_target_sync.
Pas de réveil brutal de LearningManager / ContinuousLearner.
Champs additifs uniquement.
Fallback comportement actuel si trace absente.
LeaBench et tests unitaires avant runtime live.
```
## Phase 2.0 — sécuriser le bug live Bloc-notes
But :
```text
empêcher qu'un rejet sémantique serveur soit contourné par hybrid_text_direct local
```
Actions :
```text
tester GroundingEngine après rejet serveur explicite ;
vérifier qu'un simple non-trouvé serveur autorise encore le fallback ;
déployer sur Windows seulement après tests ;
ne retenter le replay Bloc-notes qu'après relance Léa.
```
Critère de réussite :
```text
le cas close_tab_out_of_recorded_zone ne produit plus de clic local ;
le report remonte un échec honnête ou une pause, pas success=True.
```
## Phase 2.1 — objet trace additif
But :
```text
créer un objet commun transportable sans changer le comportement runtime
```
Trace minimale :
```text
mandate_id
intention_id
scene_id
affordance_signature
expected_retour
level_of_delegation
```
Emplacement recommandé :
```text
core/cognition/
```
Critère de réussite :
```text
tests unitaires de sérialisation ;
aucun appel runtime obligatoire ;
aucune régression si trace absente.
```
## Phase 2.2 — enrichissement build-time
But :
```text
produire la trace au moment où le serveur compile/enrichit les actions
```
Point d'entrée :
```text
agent_v0/server_v1/stream_processor.py
```
Principe :
```text
étendre les champs déjà produits par l'enrichissement intention/expected_result ;
ne pas rendre gemma4 obligatoire ;
si enrichissement indisponible, garder l'action actuelle.
```
Critère de réussite :
```text
actions compilées contiennent trace quand disponible ;
les anciens workflows restent valides.
```
## Phase 2.3 — transport agent
But :
```text
faire traverser la trace jusqu'à agent_v1 sans logique métier côté agent
```
Principe :
```text
l'agent transporte et rapporte ;
le serveur reste responsable du protocole ;
l'agent peut utiliser scene_expected pour pré-vérif simple, mais pas choisir un mandat.
```
Critère de réussite :
```text
report contient causal_trace ;
comportement identique si trace absente ;
pause_message peut inclure intention courante.
```
## Phase 2.4 — qualification retour
But :
```text
rattacher la vérification à l'intention et à la scène
```
Briques :
```text
ReplayVerifier
Validator V2
AuditTrail
LoopDetector
```
Sorties attendues :
```text
réussite
échec
attente
rupture
doute_localisation
doute_identification
doute_scène
doute_effet
doute_intention
```
Critère de réussite :
```text
un "rien ne change" n'est plus seulement un retry mécanique ;
il est qualifié selon l'intention, la scène et la temporalité attendue.
```
## Phase 2.5 — preuve apprenable
But :
```text
promouvoir uniquement les retours qualifiés en preuves
```
Briques à enrichir :
```text
replay_learner.ActionOutcome
replay_memory
target_memory_store via shim additif
audit_trail
```
Règle :
```text
pas de changement destructeur de hash mémoire existant sans migration ;
pas de succès appris si correction humaine non qualifiée ;
pas de promotion si semantic_verified est faux ou absent sur action sensible.
```
Critère de réussite :
```text
la mémoire sait distinguer "Enregistrer pour sauvegarder" de "Enregistrer dans une autre intention/scène".
```
## Phase 2.6 — délégation tutorée
But :
```text
modéliser le niveau d'autonomie par protocole, environnement et tuteur
```
Niveaux :
```text
N0 observation
N1 proposition
N2 exécution supervisée
N3 autonomie de session
N4 autonomie habituelle
```
Principe MVP :
```text
ne pas réveiller LearningManager ;
utiliser l'étage live ;
ajouter un DelegationResolver simple et non bloquant plus tard ;
fallback restrictif si aucune délégation connue.
```
## Bancs de validation
Ordre :
```text
LeaBench Bloc-notes enrichi
-> tests unitaires Grounding/Dialog/Verifier
-> replay Bloc-notes live
-> LeaBench Easily dérivés des fixtures
-> adaptateur T2A urgences
-> smoke E2E Easily live
```
## Décisions déjà prises
```text
priorité protocoles : mieux connu -> moins risqué -> plus court
précondition plausible : vérifier l'état attendu
risque : dépend du geste, du métier, du mandat et du niveau de Léa validé par tuteur
trace : générée au build, amendable au runtime par intervention humaine explicite
apprentissage : seulement sur résultat qualifié
```
## Décisions à ne pas prendre maintenant
```text
archiver workflow_replay.py
activer DialogResolver serveur en live
réveiller LearningManager / ContinuousLearner
modifier le schéma target_memory.db
refactorer api_stream.py
```
## Prochaine action concrète
```text
Terminer Phase 2.0 :
tester et déployer le verrou GroundingEngine anti-fallback opportuniste,
puis retenter le replay Bloc-notes.
```

View File

@@ -0,0 +1,217 @@
# Plan de remise en ordre POCs -- 2026-05-19
> Note : ce document a ete ecrit avant le recadrage explicite `Lea-first`.
> Il doit etre relu a la lumiere de
> [CADRAGE_COEUR_PRODUIT_LEA_2026-05-19.md](./CADRAGE_COEUR_PRODUIT_LEA_2026-05-19.md),
> qui distingue proprement coeur produit, VWB/demo et veille.
**Contexte**
- Les 15 derniers jours ont servi a produire une demo GHT sous forte pression.
- La demo a ete enregistree, mais avec des contournements empiles et une derive de perimetre documentee dans [docs/handoffs/2026-05-19_handoff_post_demo_GHT.md](../handoffs/2026-05-19_handoff_post_demo_GHT.md).
- Les premiers POCs demarrent dans 15 jours. L'objectif n'est pas de "tout reparer", mais de remettre le projet en ordre autour du chemin qui a le plus de valeur terrain.
## 1. Decision de pilotage
**Chemin nominal POC**
- `agent_v0/agent_v1/` (Lea) pour la capture et l'execution
- `agent_v0/server_v1/` pour le streaming, le replay et l'orchestration runtime
- `core/` pour la detection, l'extraction, le raisonnement et la supervision
**Chemin non critique a court terme**
- `visual_workflow_builder/` reste utile pour inspecter, exporter ou bricoler une demo
- `visual_workflow_builder/` ne doit plus etre la source principale de creation des workflows POC tant que ses bugs structurants ne sont pas fermes
**Raison**
- L'etat reel du projet decrit deja `agent_v1` + streaming + replay comme le socle le plus mature, alors que le VWB reste "en cours" avec bugs runtime connus.
- Le retour d'experience post-demo confirme que la derive VWB a coute du temps et masque des causes racines.
## 2. Objectif dans 15 jours
Arriver au debut des POCs avec :
- un chemin `Lea-first` assume et documente
- un socle de replay reproductible sur 1 a 2 cas terrain
- un smoke test canonique de pre-demo / pre-POC
- un registre clair de ce qui est stable, de ce qui est experimental, et des workarounds encore actifs
- une capitalisation utile de la veille, des benches et des lecons apprises
## 3. Ce qu'on garde, ce qu'on gele, ce qu'on sort du chemin critique
### A. A garder comme socle produit
- `agent_v0/agent_v1/`
- `agent_v0/server_v1/`
- `core/detection/`, `core/execution/`, `core/llm/`, `core/graph/`, `core/security/`
- la maquette et les assets de demo quand ils servent de terrain d'essai reproductible
- les docs de bench et d'audit qui documentent des decisions reversibles ou des limites prouvees
### B. A geler temporairement
- creation de workflows complexes par VWB pour les POCs
- nouvelles features UI non necessaires a l'execution terrain
- experimentation libre sur des variantes de modeles ou des sous-systemes non relies au replay
### C. A sortir du chemin critique
- corrections cosmetiques du VWB
- generalisation de `agent_chat/` tant qu'il n'est pas requis pour le protocole POC
- analytics avancées, reporting, experimentation produit non necessaire au go-live POC
## 4. Priorites reelles
### P0 -- cette semaine
1. **Fixer la ligne produit**
- Ecrire noir sur blanc que le chemin POC est `Lea -> streaming -> core`.
- Interdire les nouveaux workflows critiques crees d'abord dans le VWB.
2. **Geler une baseline de reference**
- Comparer `demo-stable-2026-05-12`, `demo/ght-2026-05-08` et `backup/post-demo-2026-05-19`.
- Sortie attendue : une baseline "reference POC" et une liste des ecarts volontaires ou accidentels.
3. **Lister les contournements actifs**
- `static_result` / `static_text` dans le replay
- bypass NPM auth hors git
- `cancel-replays.sh`
- pauses humaines remplaçant des automatisations defectueuses
- tout workaround client Lea ou VM encore necessaire
4. **Construire un smoke test canonique**
- 1 workflow de reference
- 1 environnement de reference
- 1 checklist avant execution
- 1 resultat attendu par etape critique
5. **Traiter les bugs qui cassent Lea en execution reelle**
- mapping coordonnees Y cote client
- gestion correcte de `replay_paused`
- reset d'etat propre apres replay
- captures runtime utiles au matching
### P1 -- avant les POCs
1. **Durcir le replay Lea-first**
- harmoniser les comportements strict / legacy la ou ils degradent la supervision
- verifier les delais runtime effectivement appliques
- s'assurer que les annulations et reprises sont propres
2. **Mettre au propre le protocole d'enregistrement**
- comment on capture un workflow de reference avec Lea
- ce qui est interdit pendant l'enregistrement
- quelles preuves garder quand un replay echoue
3. **Remettre a niveau la documentation de pilotage**
- `STATUS`
- un runbook POC
- un registre de decisions
- un registre de limites connues
### P2 -- apres stabilisation du socle
1. **Reprendre le VWB comme chantier separe**
- bug recapture d'anchor
- bug Stop qui ne purge pas la queue
- invalidation cache frontend
- coherence BDD / export / execution
2. **Reprendre les chantiers exploratoires**
- benchmarks modeles
- agent_chat
- analytics
- autres pistes produit
## 5. Sprint 15 jours
### Jours 1 a 3 -- Cadrage et gel
- Choisir la baseline de reference POC.
- Produire une matrice `stable / workaround / casse / hors-scope`.
- Lister les workflows de demo existants et en designer un seul comme workflow canonique.
- Geler les developpements hors chemin critique.
**Livrable**
- un dossier de reference POC
- une baseline git clairement nommee
- une checklist de reprise
### Jours 4 a 7 -- Stabilisation du runtime Lea
- Corriger les bugs client / replay qui cassent l'execution reelle.
- Retester sur l'environnement de demo le plus proche du terrain.
- Documenter le protocole de capture et de replay.
**Livrable**
- 1 smoke test manuel reproductible
- 1 replay de reference stable
- 1 liste limitee de workarounds encore toleres
### Jours 8 a 10 -- Tesorisation utile
- Trier la doc recente en trois familles :
- preuve exploitable
- decision durable
- archive de session
- Extraire la substance des benches, audits et handoffs dans des documents courts.
- Nettoyer la memoire projet pour que les vraies lecons remontent.
**Livrable**
- un registre de decisions
- un registre de preuves / benches
- un registre des limites et des dettes actives
### Jours 11 a 13 -- Preparation POC
- Rejouer le workflow canonique plusieurs fois.
- Tester les preconditions machine, VM, reseau, clipboard, captures, modele VLM.
- Verifier ce qui doit etre industrialise ou ritualise en procedure.
**Livrable**
- runbook operatoire POC
- checklist pre-demo / pre-POC
- journal d'incidents restant
### Jours 14 a 15 -- Verrouillage
- Dernier passage sur la doc critique.
- Validation humaine du chemin nominal.
- Gel des modifs non indispensables.
**Livrable**
- branche de stabilisation POC
- etat de confiance clair
- liste "ne pas toucher" avant demarrage
## 6. Tesorisation : structure cible
L'objectif n'est pas de conserver toutes les notes telles quelles. Il faut les condenser.
### Documents a avoir
- `docs/STATUS.md`
- etat reel par sous-systeme
- `docs/POC_RUNBOOK.md`
- comment lancer, verifier, relancer, diagnostiquer
- `docs/DECISIONS_LOG.md`
- decisions structurantes, datees, avec raison
- `docs/KNOWN_LIMITS.md`
- limites connues, contournements toleres, date de revision
- `docs/BENCHMARKS_INDEX.md`
- index court vers les benches qui comptent vraiment
### Regle de tri
- un handoff brut ne doit pas rester la seule source d'une decision importante
- une conclusion durable doit etre recopier dans un document stable
- une veille sans impact concret doit sortir du chemin critique documentaire
## 7. Ce que je ferais en premier, des demain
1. Choisir une baseline POC de reference.
2. Ecrire un court document "chemin nominal POC".
3. Construire le smoke test canonique.
4. Ouvrir la liste des workarounds actifs et decider lesquels sont acceptables 15 jours, lesquels doivent disparaitre.
5. Ne traiter le VWB que s'il bloque encore le chemin Lea-first.
## 8. Risques si on ne fait pas ce pivot
- Repartir dans des corrections VWB qui ne servent pas le demarrage POC
- Continuer a melanger demo, produit, veille et bricolage runtime
- Garder une documentation abondante mais peu actionnable
- Rejouer les memes erreurs de session, faute d'avoir remonte les vraies lecons au bon niveau
## 9. Sources de reference
- [docs/handoffs/2026-05-19_handoff_post_demo_GHT.md](../handoffs/2026-05-19_handoff_post_demo_GHT.md)
- [docs/handoffs/2026-05-18_handoff_consolidation.md](../handoffs/2026-05-18_handoff_consolidation.md)
- [docs/STATUS.md](../STATUS.md)
- [docs/CARTE_FONCTIONNELLE_2026-05-08.md](../CARTE_FONCTIONNELLE_2026-05-08.md)
- [docs/AUDIT_BDD_WORKFLOW_2026-05-10.md](../AUDIT_BDD_WORKFLOW_2026-05-10.md)
- [docs/QW_SUITE_MAI.md](../QW_SUITE_MAI.md)
- [docs/BENCH_T2A_DECISION_11DOSSIERS.md](../BENCH_T2A_DECISION_11DOSSIERS.md)
- [docs/AUDIT_MEMOIRE_CLAUDE_2026-05-08.md](../AUDIT_MEMOIRE_CLAUDE_2026-05-08.md)

View File

@@ -0,0 +1,67 @@
# Plan stabilisation demo Lea — cible lundi 1 juin 2026
Date de cadrage : 2026-05-25 12:44 Europe/Paris
Pilotage : Codex
Contexte : la demo client est reportee au lundi 1 juin 2026. On sort du mode rustine J-4 et on vise un systeme propre, mesurable, restaurable.
## Principes
1. Pas de restauration destructive ni suppression de modele sans inventaire et accord explicite.
2. Pas de replay live tant que les chemins pause/resume, FeedbackBus et perf ne sont pas instrumentes.
3. Les changements doivent avoir une preuve : test, commande de healthcheck, log cible, ou bench.
4. `C:\rpa_vision` reste le runtime Windows reel ; ne pas resynchroniser `agent_v0/deploy/windows_client`.
5. Les collegues repondent dans `docs/coordination/inbox_codex/` avec ACK/NACK explicite.
## P0 — Inventaire et protection Ollama
Objectif : garantir que les modeles critiques existent, sont referencables, et peuvent etre reconstruits si un tag disparait.
- Figer `ollama list`, manifests, gros blobs, `ollama show --modelfile` des modeles critiques.
- Verifier les artefacts locaux et backup : `t2a-gemma3-27b-q8_0.gguf`, `t2a-gemma3-27b-q4_k_m.gguf`, merged safetensors.
- Produire une table : tag Ollama, digest/blob, source GGUF/HF, backup, statut de reconstruction.
- Identifier les vrais manquants avec Dom si sa liste attendue depasse les 38 tags actuels.
## P0 — FeedbackBus 5004 propre
Objectif : garder la narration temps reel si elle est utile, mais sans bruit log ni service fragile.
- Corriger la cause `rpa-agent-chat.service inactive`.
- Corriger ou isoler le warning CLIP/torch au boot.
- Corriger CORS/SocketIO pour la ChatWindow Windows.
- Conserver le fallback HTTP 5005 pour `resume` / `abort`.
- Decider apres test si `LEA_FEEDBACK_BUS=1` reste actif cote Windows.
## P0 — Performance mesurable
Objectif : remplacer les intuitions par des mesures reproductibles.
- Harness build replay sans live replay : mesure avec/sans `RPA_SKIP_INTENTION_ENRICHMENT`.
- Mesure des appels VLM : modele, `num_ctx`, layers CPU/GPU, p50/p95, taux JSON valide.
- Politique de residence Ollama : `MAX_LOADED_MODELS=1`, modele VLM prechauffe, eviter les swaps texte/VLM.
- Decision documentee : `qwen2.5vl:7b-rpa` vs `qwen2.5vl:7b` vs `qwen2.5vl:3b` vs autre backend.
## P0 — Replay pause/resume robuste
Objectif : zero confusion visible dans Lea.
- La bulle supervisee ne doit plus tronquer le message.
- La bulle doit se fermer a la reprise serveur (`server_cleared`).
- Le compteur et le statut doivent refleter l'etape reelle.
- Smoke Windows obligatoire apres patch deploye.
## P1 — Hygiene runtime/deploiement
Objectif : rendre le systeme re-demarrable sans memoire orale.
- Runbook Linux : `rpa-streaming`, `ollama`, `rpa-agent-chat`.
- Runbook Windows : tache `LeaInteractive`, lock, logs, hash des fichiers deployes.
- Separateur clair : source repo vs runtime `C:\rpa_vision`.
## P1 — Pack de preuve demo
Objectif : arriver lundi avec une preuve concrete, pas seulement du code.
- Healthcheck global : Linux 5005/5004/Ollama + Windows agent.
- Bench perf avant/apres.
- Smoke replay controle, sans improvisation.
- Notes de risques restantes avec mitigation.

View File

@@ -0,0 +1,200 @@
# Protocole seance 1 micro-apprentissage Lea - 2026-05-27
## Competence cible
Premiere competence atomique:
```text
ouvrir le menu Demarrer
```
Raison: ouvrir Chrome, Word ou une recherche depend souvent de cette brique. Si Lea apprend d'abord ce geste simple, observable et verifiable, on pourra composer ensuite:
```text
ouvrir le menu Demarrer -> rechercher Chrome -> ouvrir Chrome
ouvrir le menu Demarrer -> rechercher Word -> ouvrir Word
```
On ne demarre donc pas par un scenario "ouvrir navigateur" complet. On commence plus petit.
## Duree
25 minutes maximum:
- 9 min: 3 demonstrations humaines,
- 5 min: 1 essai supervise de Lea,
- 5 min: 1 variante,
- 6 min: debrief et decision.
## Avant de commencer
1. Verifier le socle:
```bash
python3 tools/lea_micro_preflight.py
```
Verdict acceptable: tout OK, warning VLM non resident accepte.
2. Mettre Windows dans un etat simple:
- bureau visible,
- pas d'application plein ecran,
- NoMachine stable,
- presse-papiers non utilise,
- Lea connectee.
3. Ne pas ouvrir VWB, Aiva ou un workflow metier.
## Demonstrations humaines
Chaque demonstration doit etre lancee avec `Apprenez-moi`, puis arretee avec `Arreter`.
Nom de session:
```text
ouvrir le menu Demarrer
```
### Demo A - clic souris
Dom dit ce qu'il fait:
```text
J'ouvre le menu Demarrer en cliquant sur le logo Windows.
```
Actions:
1. cliquer le logo Windows,
2. attendre que le menu apparaisse,
3. verifier visuellement le champ `Rechercher`,
4. fermer avec `Echap`,
5. arreter l'enregistrement.
Postcondition:
- le champ `Rechercher` est visible.
### Demo B - touche Windows
Dom dit:
```text
J'ouvre le meme menu avec la touche Windows.
```
Actions:
1. appuyer sur `Win`,
2. attendre le menu,
3. verifier `Rechercher`,
4. fermer avec `Echap`,
5. arreter l'enregistrement.
Postcondition:
- le champ `Rechercher` est visible.
### Demo C - Win+S
Dom dit:
```text
J'ouvre directement la recherche avec Win+S.
```
Actions:
1. appuyer sur `Win+S`,
2. verifier que la zone de recherche est visible et active,
3. fermer avec `Echap`,
4. arreter l'enregistrement.
Postcondition:
- la zone de recherche est visible, idealement avec le focus.
## Essai supervise de Lea
Lea tente une methode simple, de preference `Win`.
Message attendu avant action:
```text
J'ouvre le menu Demarrer avec la touche Windows.
```
Verification attendue:
```text
Je vois le champ Rechercher, comme attendu.
```
Si elle echoue, elle s'arrete et affiche:
```text
J'essaie de : ouvrir le menu Demarrer
J'attendais : voir le champ Rechercher
Je vois : <description de l'ecran actuel>
Peux-tu : ouvrir le menu Demarrer une fois pour me montrer le geste
```
## Variante
Une seule variante pendant cette seance:
- Notepad ou une autre fenetre simple au premier plan,
- puis Lea tente d'ouvrir le menu Demarrer.
Critere de succes:
- le champ `Rechercher` devient visible malgre la fenetre au premier plan.
On ne teste pas encore DPI ou ecran secondaire pendant cette seance.
## Donnees a relever
Pour chaque demonstration et essai:
- methode: clic logo, Win, Win+S,
- fenetre active avant action,
- marqueur observe: `Rechercher`,
- resultat: succes, correction, echec,
- duree approximative,
- message affiche si blocage.
## Criteres de succes
- Lea formule la competence comme une intention, pas comme une coordonnee.
- Lea connait au moins deux methodes pour ouvrir le menu.
- Lea verifie le marqueur `Rechercher`.
- Aucun message generique ne sort: pas `un element`, pas `cette action`, pas `Validation requise`.
- Aucun replay VWB/metier n'est lance.
## Criteres d'echec
- Lea clique une coordonnee sans regarder le resultat.
- Lea dit OK alors que le menu n'est pas ouvert.
- Lea demande une aide incomprehensible.
- Lea confond menu Demarrer, bureau, navigateur ou Word.
- La capture/finalisation de session ne produit aucun artefact exploitable.
## Promotion
| Etat | Condition |
|---|---|
| OBSERVATION | au moins une demonstration capturee |
| COACHING | Lea peut tenter avec aide humaine disponible |
| AUTO_CANDIDATE | 3 succes verifies sur au moins 2 methodes |
| AUTO | pas pour cette seance; attendre DPI/ecran secondaire |
## Suite directe
Si cette seance est propre:
1. seance 2: saisir une recherche dans le menu Demarrer,
2. seance 3: ouvrir Chrome depuis le menu,
3. seance 4: ouvrir Word depuis le menu,
4. seance 5: fermer Word proprement,
5. seulement ensuite: DPI et ecran secondaire.

View File

@@ -0,0 +1,402 @@
# Axe A1 — État de l'art VLM Grounding UI (2025-2026)
**Date :** 2026-05-23
**Auteur :** Agent recherche dispatché (Claude Opus 4.7 1M)
**Périmètre :** modèles VLM de grounding d'éléments UI graphiques, focus 2025-2026, candidats déployables sur RTX 5070 12 GB VRAM, healthtech (licence permissive).
**Source maître interne :** [`SYNTHESE_TECHNOS_REPLAY_2026-05-23.md`](../SYNTHESE_TECHNOS_REPLAY_2026-05-23.md), [`MIGRATION_VLM_PLAN_2026-05-09.md`](../MIGRATION_VLM_PLAN_2026-05-09.md), [`HISTORIQUE_VLM_IMPLEMENTATIONS_2026-05-08.md`](../HISTORIQUE_VLM_IMPLEMENTATIONS_2026-05-08.md)
> Recherche documentaire — aucun test runtime. Chaque chiffre vient d'un papier, fiche HF ou leaderboard linké. Les scores ScreenSpot-Pro varient parfois de ±3 points entre sources (papier vs leaderboard tiers vs reproduction utilisateur). On affiche systématiquement le chiffre déclaré par les auteurs ou la fiche HF officielle.
---
## 1. TL;DR
1. **Le SOTA open-source 7B sur ScreenSpot-Pro a doublé en 12 mois** : 18.9 % (OS-Atlas-7B, oct 2024) → 61.6 % (UI-TARS-1.5-7B, avr 2025) → 51.9 % (InfiGUI-G1-7B, aoû 2025, AAAI 2026 Oral). Les fermés (GPT-5.2 à 86 %) creusent encore l'écart mais inutilisables on-premise.
2. **Notre InfiGUI-G1-3B actuel (45.2 % SSPro / 91.1 % SSv2) reste compétitif** pour 3 GB VRAM 4-bit. Le ratio perf/VRAM est excellent. Migration vers le 7B (51.9 % SSPro) faisable sans changer d'architecture (même `Qwen2_5_VLForConditionalGeneration`).
3. **Qwen3-VL-8B-Instruct (oct 2025, Apache 2.0) ne résout PAS le bug d'échelle bbox seul** : même convention post-resize que Qwen2.5-VL. Le fix est dans le **backend** (vLLM/Transformers in-process expose `resized_height/resized_width`), pas dans le modèle.
4. **Approche coordinate-free montante** (GUI-Actor, MolmoPoint-GUI, InfiGUI-G1) : la cible n'est plus du texte JSON mais un token de patch visuel ou des grounding-tokens. Élimine structurellement le bug d'échelle. Mais demande un fork Transformers ou un head custom.
**Recommandation top 3 pour notre cas (12 GB VRAM, healthtech, licence commerciale OK) :**
| # | Modèle | Pourquoi |
|---|---|---|
| 1 | **`InfiX-ai/InfiGUI-G1-7B`** ([HF](https://huggingface.co/InfiX-ai/InfiGUI-G1-7B), Apache 2.0) | Continuité totale avec notre stack `core/grounding/`, +5 pts SSPro vs G1-3B, tient en 4-bit NF4 (~6 GB), même format point post-resize que G1-3B donc le bug d'échelle est déjà géré côté `_smart_resize` |
| 2 | **`Hcompany/Holo1.5-7B`** ([HF](https://huggingface.co/Hcompany/Holo1.5-7B), Apache 2.0) | Qwen2.5-VL-7B-Instruct base, **57.9 % SSPro / 93.3 % SSv2**, natif 3840×2160 (utile fenêtres 2560×1600 Easily), entraîné GRPO sur UI réelles |
| 3 | **`ByteDance-Seed/UI-TARS-1.5-7B`** ([HF](https://huggingface.co/ByteDance-Seed/UI-TARS-1.5-7B), Apache 2.0) | **61.6 % SSPro / 94.2 % SSv2** déclarés (mais reproduction utilisateur à ~40-48 % selon [issue #215](https://github.com/bytedance/UI-TARS/issues/215)) — fallback si InfiGUI déçoit en réel |
---
## 2. Table comparative complète
Légende :
- VRAM : approximation pour inférence single-batch, base BF16 sans optim. `(4-bit ≈ /3)` pour quantif NF4 type bitsandbytes.
- SS = ScreenSpot v1 (1200+ instructions, multi-OS), SSv2 = re-annoté par OS-Atlas (11% corrections), SSPro = professional high-res (1581 instructions, 23 apps, 5 industries, 3 OS, papier ICLR 2025).
- Conv. coord : `0-1000` = normalisé 0-1000 indép. taille image (Qwen2-VL natif). `post-resize` = bbox dans la résolution **après smart_resize** côté modèle (Qwen2.5-VL). `point-token` = grounding via attention sur tokens visuels, pas de texte coord. `abs` = pixel image originale.
- "non trouvé" = pas de chiffre publié dans les sources consultées.
| Modèle | Params | VRAM BF16 | SS | SSv2 | SSPro | Sortie | Conv. coord | vLLM | Transformers | Licence | Release | HF |
|---|---:|---:|---:|---:|---:|---|---|:-:|:-:|---|---|---|
| **InfiGUI-G1-3B***(actuel)* | 3B | ~6 GB (~3 GB 4-bit) | 90.3 | 91.1 | 45.2 | point JSON | post-resize | ✅ | ✅ | Apache 2.0 | 2025-08-11 | [InfiX-ai/InfiGUI-G1-3B](https://huggingface.co/InfiX-ai/InfiGUI-G1-3B) |
| **InfiGUI-G1-7B** | 7B | ~14 GB (~6 GB 4-bit) | non trouvé | 93.5 | 51.9 | point JSON | post-resize | ✅ | ✅ | Apache 2.0 | 2025-08-11 | [InfiX-ai/InfiGUI-G1-7B](https://huggingface.co/InfiX-ai/InfiGUI-G1-7B) |
| **InfiGUI-R1-3B** *(prédécesseur)* | 3B | ~6 GB | 87.5 | non trouvé | 35.7 | point JSON | post-resize | ✅ | ✅ | Apache 2.0 | 2025-04-20 | [InfiX-ai/InfiGUI-R1-3B](https://huggingface.co/InfiX-ai/InfiGUI-R1-3B) |
| **UI-TARS-1.5-7B** | 7B | ~14 GB | non trouvé | 94.2 | 61.6 *(48 reprod.)* | action DSL `click(x,y)` | abs px | ✅ | ✅ | Apache 2.0 | 2025-04-16 | [ByteDance-Seed/UI-TARS-1.5-7B](https://huggingface.co/ByteDance-Seed/UI-TARS-1.5-7B) |
| **Qwen3-VL-8B-Instruct** | 9B | ~18 GB (~6 GB 4-bit) | ~94 (déclaré) | non trouvé | 54.6 *(leaderboard llm-stats)* / 61.8 *(papier)* | bbox_2d ou point JSON | post-resize (multiples 32) | ✅ (vllm≥0.11) | ✅ | Apache 2.0 | 2025-10-15 | [Qwen/Qwen3-VL-8B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-8B-Instruct) |
| **Qwen3-VL-4B-Instruct** | 4B | ~8 GB | non trouvé | non trouvé | 59.5 *(leaderboard)* | bbox_2d ou point JSON | post-resize | ✅ | ✅ | Apache 2.0 | 2025-10-15 | [Qwen/Qwen3-VL-4B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct) |
| **Qwen2.5-VL-7B-Instruct** *(legacy)* | 7B | ~14 GB | 88.8 | 88.8 | 26.8 | bbox_2d JSON | post-resize (multiples 28) | ✅ | ✅ | Apache 2.0 | 2025-01 | [Qwen/Qwen2.5-VL-7B-Instruct](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct) |
| **Holo1.5-7B** ⭐ | 7B | ~14 GB | non trouvé | 93.31 | 57.94 | non documenté (probable point) | non documenté | ✅ | ✅ | Apache 2.0 | 2025-09 | [Hcompany/Holo1.5-7B](https://huggingface.co/Hcompany/Holo1.5-7B) |
| **Holo1.5-3B** | 3B | ~6 GB | non trouvé | non trouvé | non trouvé | idem | idem | ✅ | ✅ | Apache 2.0 | 2025-09 | [Hcompany/Holo1.5-3B](https://huggingface.co/Hcompany/Holo1.5-3B) |
| **Holo1-7B** *(v1)* | 7B | ~14 GB | non trouvé (avg UI 76.2) | non trouvé | non trouvé | non documenté | non documenté | ✅ | ✅ | Apache 2.0 | 2025-06 | [Hcompany/Holo1-7B](https://huggingface.co/Hcompany/Holo1-7B) |
| **OS-Atlas-Base-7B** | 8B | ~16 GB | 82.5 *(papier)* | 85.1 *(InfiGUI eval)* | 18.9 | bbox + point JSON | 0-1000 normalisé | ✅ | ✅ | Apache 2.0 | 2024-10-30 | [OS-Copilot/OS-Atlas-Base-7B](https://huggingface.co/OS-Copilot/OS-Atlas-Base-7B) |
| **OS-Atlas-Base-4B** | 4B | ~8 GB | non trouvé | non trouvé | non trouvé | bbox + point JSON | 0-1000 normalisé | ✅ | ✅ | Apache 2.0 | 2024-10-30 | [OS-Copilot/OS-Atlas-Base-4B](https://huggingface.co/OS-Copilot/OS-Atlas-Base-4B) |
| **UGround-V1-7B** ⭐ | 7B | ~14 GB | 86.3 | non trouvé | non trouvé (probable ~36 papier) | point `(x,y)` | 0-1000 normalisé | ✅ | ✅ | Apache 2.0 | 2024-10-07 / révisé 2025-01 | [osunlp/UGround-V1-7B](https://huggingface.co/osunlp/UGround-V1-7B) |
| **UGround-V1-2B** | 2B | ~4 GB | non trouvé | non trouvé | non trouvé | point `(x,y)` | 0-1000 | ✅ | ✅ | Apache 2.0 | 2025-01 | [osunlp/UGround-V1-2B](https://huggingface.co/osunlp/UGround-V1-2B) |
| **UGround-V1-72B** | 72B | ~144 GB | non trouvé | non trouvé | 34.5 *(papier SSPro orig.)* | point `(x,y)` | 0-1000 | ✅ | ✅ | Apache 2.0 | 2025-01 | [osunlp/UGround-V1-72B](https://huggingface.co/osunlp/UGround-V1-72B) |
| **Magma-8B** | 9B | ~18 GB | mobile 59.5 / desktop 64.1 / web 60.6 | non trouvé | non trouvé | Set-of-Mark + bbox | non documenté | ⚠️ fork transfo | ✅ (fork) | **MIT** | 2025-02-18 | [microsoft/Magma-8B](https://huggingface.co/microsoft/Magma-8B) |
| **GUI-Actor-7B-Qwen2.5-VL** | 8B | ~16 GB | non trouvé | 92.1 | 44.6 | **special token attention head**`topk_points` normalisés | normalisé 0-1 (sans texte coord) | ⚠️ pas mentionné | ✅ (fork) | **MIT** | 2025-06-03 | [microsoft/GUI-Actor-7B-Qwen2.5-VL](https://huggingface.co/microsoft/GUI-Actor-7B-Qwen2.5-VL) |
| **GUI-Actor-7B-Qwen2-VL** | 8B | ~16 GB | non trouvé | 89.5 | 40.7 | idem | idem | ⚠️ | ✅ (fork) | MIT | 2025-06 | [microsoft/GUI-Actor-7B-Qwen2-VL](https://huggingface.co/microsoft/GUI-Actor-7B-Qwen2-VL) |
| **MolmoPoint-GUI-8B** ⭐ | 9B | ~18 GB | non trouvé | non trouvé | **61.1** (open SOTA) | grounding-tokens `[id,img,x,y]` | abs px | ❌ (logits processor custom) | ✅ | Apache 2.0 | 2026-03-18 | [allenai/MolmoPoint-GUI-8B](https://huggingface.co/allenai/MolmoPoint-GUI-8B) |
| **AGUVIS-7B-720P** | 8B | ~16 GB | 84.4 *(papier)* | non trouvé | 22.9 | bbox + action plan | non documenté (probable post-resize Qwen2-VL) | ✅ | ✅ | non trouvé (probable Apache via base) | 2024-12 | [xlangai/Aguvis-7B-720P](https://huggingface.co/xlangai/Aguvis-7B-720P) |
| **ShowUI-2B** | 2B | ~4 GB | 75.1 | non trouvé | 7.7 | point + action dict | normalisé 0-1 | ✅ | ✅ | **MIT** | 2024-11-26 | [showlab/ShowUI-2B](https://huggingface.co/showlab/ShowUI-2B) |
| **CogAgent-9B-20241220** | 14B (9B lang + 5B vision) | ~28 GB | leader cité, score précis non publié | non trouvé | non trouvé | `CLICK(box=[x1,y1,x2,y2])` action DSL | non documenté (probable abs sur 1120×1120) | ⚠️ partiel | ✅ | **Other** (custom, non-Apache) | 2024-12-20 | [zai-org/cogagent-9b-20241220](https://huggingface.co/zai-org/cogagent-9b-20241220) |
| **SeeClick** *(historique)* | 9.6B | ~19 GB | 53.4 | non trouvé | <10 (papier ScreenSpot-Pro) | bbox via Qwen-VL | non documenté | ❌ | ✅ | Apache 2.0 | 2024-04 (ACL 2024) | [cckevinn/SeeClick](https://huggingface.co/cckevinn/SeeClick) |
| **GUI-G2-7B** | 7B | ~14 GB | SOTA déclaré | SOTA déclaré | SOTA déclaré (Gaussian reward GRPO) | non documenté | non documenté | ✅ | ✅ | non trouvé | 2026-01 (AAAI 2026) | [inclusionAI/GUI-G2-7B](https://huggingface.co/inclusionAI/GUI-G2-7B) |
| **GPT-5.2** *(fermé)* | n/a | n/a (cloud) | — | — | **86.3** | n/a | n/a | n/a | n/a | OpenAI propriétaire | 2026 | n/a |
| **Gemini 3 Pro** *(fermé)* | n/a | n/a (cloud) | — | — | 72.7 | n/a | n/a | n/a | n/a | Google propriétaire | 2026 | n/a |
**Sources des scores principaux :** [ScreenSpot-Pro leaderboard llm-stats](https://llm-stats.com/benchmarks/screenspot-pro), [papier ScreenSpot-Pro arXiv:2504.07981](https://arxiv.org/abs/2504.07981), [InfiGUI-G1 paper arXiv:2508.05731](https://arxiv.org/abs/2508.05731), fiches HF citées colonne droite.
---
## 3. Fiches détaillées par modèle
### 3.1. InfiGUI-G1-3B / 7B (notre modèle actuel + upgrade direct)
- **Repo HF :** [InfiX-ai/InfiGUI-G1-3B](https://huggingface.co/InfiX-ai/InfiGUI-G1-3B), [InfiX-ai/InfiGUI-G1-7B](https://huggingface.co/InfiX-ai/InfiGUI-G1-7B)
- **Papier :** [arXiv:2508.05731](https://arxiv.org/abs/2508.05731) — *InfiGUI-G1: Advancing GUI Grounding with Adaptive Exploration Policy Optimization*, AAAI 2026 Oral
- **GitHub :** [InfiXAI/InfiGUI-G1](https://github.com/InfiXAI/InfiGUI-G1)
- **Release :** 2025-08-11
- **Licence :** Apache 2.0
- **Base :** Qwen2.5-VL-3B (resp. 7B), GRPO via AEPO (Adaptive Exploration Policy Optimization)
- **Bench :** ScreenSpot 90.3 (3B) / 92.5+ (7B), SSv2 91.1 / 93.5, **SSPro 45.2 (3B) / 51.9 (7B)**, MMBench-GUI L2 73.4 (3B) / 80.8 (7B)
- **Sortie :** point JSON `[{"point_2d": [x, y]}, …]`, coordonnées **post-resize** (le prompt expose `{new_width}x{new_height}` au modèle, mapping à faire client side)
- **Code grounding minimal :**
```python
# Sortie typique
# [{"point_2d": [421, 612], "label": "OK button"}]
# Mapping coords:
original_x = int(coords[0] / new_width * original_width)
original_y = int(coords[1] / new_height * original_height)
```
- **Pourquoi pertinent chez nous :** déjà câblé (`core/grounding/server.py` + `infigui_worker.py` + `infigui_server.py`), `_smart_resize` factor 28 calibré. Passage 3B → 7B = changement de `MODEL_ID` (env `GROUNDING_MODEL`). VRAM 4-bit ≈ 6 GB, tient sur RTX 5070.
### 3.2. UI-TARS-1.5-7B (ByteDance)
- **Repo HF :** [ByteDance-Seed/UI-TARS-1.5-7B](https://huggingface.co/ByteDance-Seed/UI-TARS-1.5-7B)
- **Papier :** [arXiv:2501.12326](https://arxiv.org/abs/2501.12326), UI-TARS-2 technical report [arXiv:2509.02544](https://arxiv.org/html/2509.02544v1)
- **GitHub :** [bytedance/UI-TARS](https://github.com/bytedance/ui-tars), desktop [bytedance/UI-TARS-desktop](https://github.com/bytedance/UI-TARS-desktop)
- **Release :** 2025-04-16
- **Licence :** Apache 2.0
- **Bench déclarés :** SSv2 94.2, **SSPro 61.6** — MAIS [issue #215](https://github.com/bytedance/UI-TARS/issues/215) signale reproduction à ~40-48 % selon prompt
- **Sortie :** action DSL natif `click(start_box='[x1,y1,x2,y2]')`, coordonnées **pixels absolues** sur image originale
- **Note :** UI-TARS-2 (sept 2025) existe mais pas open-source à date des sources consultées (technical report only). Continuer sur 1.5.
- **Risque :** asymétrie déclaré/reproduit. Tester localement avant migration.
### 3.3. Qwen3-VL-8B-Instruct (cible migration plan 9 mai)
- **Repo HF :** [Qwen/Qwen3-VL-8B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-8B-Instruct), quantif [cpatonn/Qwen3-VL-8B-Instruct-AWQ-8bit](https://huggingface.co/cpatonn/Qwen3-VL-8B-Instruct-AWQ-8bit), [cyankiwi/Qwen3-VL-8B-Instruct-AWQ-4bit](https://huggingface.co/cyankiwi/Qwen3-VL-8B-Instruct-AWQ-4bit)
- **GitHub :** [QwenLM/Qwen3-VL](https://github.com/QwenLM/Qwen3-VL)
- **Release :** 2025-10-15
- **Licence :** Apache 2.0
- **Bench :** [llm-stats SSPro 54.6](https://llm-stats.com/benchmarks/screenspot-pro) (rank 16), mais codersera blog cite ~94 % ScreenSpot et 61.8 % SSPro pour la variante computer-use de Qwen3-VL
- **Sortie :** flexible, supporte `bbox_2d` ET `point` selon prompt. Conv. **post-resize multiples de 32** (≠ Qwen2.5-VL qui était multiples de 28 — **DETTE-014 du repo s'aligne désormais sur cette nouvelle factor 32**)
- **Resize :** le modèle expose `resized_width` et `resized_height` en paramètres directs (cf. GitHub Qwen3-VL "Directly set resized_height and resized_width. These values will be rounded to the nearest multiple of 32")
- **Support vLLM :** `vllm>=0.11.0` requis
- **Pourquoi vigilant :** le bench llm-stats positionne Qwen3-VL-8B-Instruct à 54.6 % SSPro, **moins bon que InfiGUI-G1-7B (51.9 %)... attendez non, 54.6 > 51.9**. À 3 pts d'écart, dans la marge d'erreur protocole. Le 4B (59.5 % rank 12) est curieusement meilleur que le 8B (54.6 %), à investiguer.
### 3.4. Holo1.5-7B (H Company)
- **Repo HF :** [Hcompany/Holo1.5-7B](https://huggingface.co/Hcompany/Holo1.5-7B), variantes [3B](https://huggingface.co/Hcompany/Holo1.5-3B) et [72B](https://huggingface.co/Hcompany/Holo1.5-72B)
- **Blog :** [HF blog Holo1](https://huggingface.co/blog/Hcompany/holo1), [GRPO for GUI Grounding](https://huggingface.co/blog/HelloKKMe/grounding-r1)
- **Papier (Holo1) :** [arXiv:2506.02865](https://arxiv.org/pdf/2506.02865) *Surfer-H Meets Holo1*
- **Release :** v1 juin 2025, v1.5 septembre 2025
- **Licence :** Apache 2.0
- **Base :** Qwen2.5-VL-7B-Instruct
- **Bench :** **SSv2 93.31 %, SSPro 57.94 %**, WebClick 90.24 % — natif 3840×2160
- **Sortie :** non documenté dans la fiche HF directement (probable point format Qwen2.5-VL-like)
- **Pourquoi pertinent :** entraîné spécifiquement multi-environnements (web + desktop + mobile) avec GRPO, score SSPro très solide pour Apache 2.0. Probable swap drop-in dans `core/grounding/server.py` (même architecture Qwen2_5_VLForConditionalGeneration).
### 3.5. UGround-V1 (OSU NLP, ICLR'25 Oral)
- **Repo HF :** [osunlp/UGround-V1-7B](https://huggingface.co/osunlp/UGround-V1-7B), [2B](https://huggingface.co/osunlp/UGround-V1-2B), [72B](https://huggingface.co/osunlp/UGround-V1-72B)
- **Papier :** [arXiv:2410.05243](https://arxiv.org/abs/2410.05243), ICLR 2025 Oral
- **GitHub :** [OSU-NLP-Group/UGround](https://github.com/OSU-NLP-Group/UGround)
- **Licence :** Apache 2.0
- **Base :** Qwen2-VL-7B-Instruct
- **Bench :** ScreenSpot 86.3 % moyenne (texte/icône desktop/mobile/web 76-93 %), UGround-V1-72B cité 34.5 % sur SSPro (papier original SSPro)
- **Sortie :** point unique `(x, y)` en string, **convention normalisée [0, 1000)** indépendante de l'image (héritage Qwen2-VL)
- **Avantage :** convention 0-1000 = pas de bug d'échelle post-resize. Le modèle a appris à raisonner dans un espace canonique.
- **Inconvénient :** absence d'évaluation SSPro publique pour le 7B (à part le 72B). Compatible bbox = non (point only).
### 3.6. OS-Atlas (UCSD / Shanghai AI Lab)
- **Repo HF :** [OS-Copilot/OS-Atlas-Base-7B](https://huggingface.co/OS-Copilot/OS-Atlas-Base-7B), [Base-4B](https://huggingface.co/OS-Copilot/OS-Atlas-Base-4B), [Pro-7B](https://huggingface.co/OS-Copilot/OS-Atlas-Pro-7B), [Pro-4B](https://huggingface.co/OS-Copilot/OS-Atlas-Pro-4B)
- **Papier :** [arXiv:2410.23218](https://arxiv.org/abs/2410.23218), NeurIPS 2024
- **GitHub :** [OS-Copilot/OS-Atlas](https://github.com/OS-Copilot/OS-Atlas)
- **Release :** 2024-10-30
- **Licence :** Apache 2.0
- **Base Base-7B :** Qwen2-VL-7B-Instruct, dataset 13M éléments GUI cross-platform
- **Bench :** ScreenSpot 82.5 % avg, SSv2 85.1 %, **SSPro 18.9 %** *(plus bas que tous les modèles 2025)*
- **Sortie :** bbox + point JSON, **normalisé 0-1000** (Qwen2-VL natif)
- **Statut :** point de référence historique. Surclassé par tous les modèles 2025 sur SSPro. À retenir uniquement pour SSv2 / desktop low-res "tabs simples".
### 3.7. AGUVIS-7B (Salesforce + HKU)
- **Repo HF :** [xlangai/Aguvis-7B-720P](https://huggingface.co/xlangai/Aguvis-7B-720P)
- **Papier :** voir [paper list OSU](https://github.com/OSU-NLP-Group/GUI-Agents-Paper-List/blob/main/paper_by_key/paper_visual_grounding.md)
- **Release :** 2024-12
- **Licence :** non explicite sur fiche HF (probable Apache 2.0 via base)
- **Base :** Qwen2-VL
- **Bench :** ScreenSpot 84.4 % papier, **SSPro 22.9 %**
- **Sortie :** bbox + action plan (training en 2 étapes : grounding puis action)
- **Statut :** intéressant historiquement (pure-vision unified framework Salesforce). Score SSPro faible vs cohorte 2025. Pas prioritaire.
### 3.8. Magma-8B (Microsoft, CVPR 2025)
- **Repo HF :** [microsoft/Magma-8B](https://huggingface.co/microsoft/Magma-8B)
- **Papier :** [arXiv:2502.13130](https://arxiv.org/abs/2502.13130), CVPR 2025
- **GitHub :** [microsoft/Magma](https://github.com/microsoft/Magma)
- **Release :** 2025-02-18
- **Licence :** **MIT** (très permissive)
- **Bench :** ScreenSpot mobile 59.5 / desktop 64.1 / web 60.6 — **pas d'éval SSPro publiée**
- **Sortie :** Set-of-Mark (marques numérotées sur image) + Trace-of-Mark (vidéo). Hybride GUI + robotique
- **Inconvénient majeur :** nécessite **fork custom de Transformers** (`git+https://github.com/jwyang/transformers.git@dev/jwyang-v4.48.2`), pas de support vLLM standard
- **Pertinent si :** intérêt cross-domaine (GUI + robotique). Pour pure GUI, autres modèles font mieux.
### 3.9. GUI-Actor-7B-Qwen2.5-VL (Microsoft, NeurIPS 2025)
- **Repo HF :** [microsoft/GUI-Actor-7B-Qwen2.5-VL](https://huggingface.co/microsoft/GUI-Actor-7B-Qwen2.5-VL), variante [Qwen2-VL](https://huggingface.co/microsoft/GUI-Actor-7B-Qwen2-VL)
- **Papier :** [arXiv:2506.03143](https://arxiv.org/abs/2506.03143), NeurIPS 2025 — *GUI-Actor: Coordinate-Free Visual Grounding for GUI Agents*
- **GitHub :** [microsoft/GUI-Actor](https://github.com/microsoft/GUI-Actor)
- **Release :** 2025-06-03
- **Licence :** MIT
- **Bench :** SSv2 92.1, **SSPro 44.6** (sans verifier), avec verifier monte
- **Sortie :** **coordinate-free** — attention-based action head qui pointe directement vers les patches visuels. Output décodé en `topk_points` (coordonnées normalisées 0-1, sans génération texte)
- **Avantage théorique majeur :** élimine structurellement le bug d'échelle. Le modèle aligne directement un token spécial avec les patches visuels pertinents.
- **Inconvénient :** demande fork custom (`Qwen2_5_VLForConditionalGenerationWithPointer`), pas de support vLLM standard mentionné.
- **Intérêt R&D :** valider la direction "coordinate-free" comme architecturale pour la v2 grounding.
### 3.10. MolmoPoint-GUI-8B (Allen AI, mars 2026)
- **Repo HF :** [allenai/MolmoPoint-GUI-8B](https://huggingface.co/allenai/MolmoPoint-GUI-8B)
- **Blog :** [MolmoPoint blog Ai2](https://allenai.org/blog/molmopoint)
- **Papier :** voir blog (référence papier non explicite dans nos sources)
- **GitHub :** [allenai/molmo2](https://github.com/allenai/molmo2)
- **Release :** mars 2026
- **Licence :** Apache 2.0 (recherche/éducation, Responsible Use Guidelines Ai2)
- **Base :** Qwen3-8B + MolmoPoint-8B finetuning
- **Bench :** **SSPro 61.1 (SOTA open)**, OSWorldG 70.0
- **Sortie :** grounding-tokens `[object_id, image_num, x, y]`, **coords pixels absolues** (pas post-resize !)
- **Données training :** MolmoPoint-GUISyn = 36k screenshots synthétiques HR (desktop + web + mobile)
- **Inconvénient :** **pas de support vLLM** (logits processor custom requis), single-image only, pas de support training prod
- **Note pour nous :** **score SSPro le plus élevé parmi les open-source**, et conv. coord absolue = AUCUN bug d'échelle. Mais intégration plus lourde (custom logits processor).
### 3.11. CogAgent-9B-20241220 (Zhipu / THUDM)
- **Repo HF :** [zai-org/cogagent-9b-20241220](https://huggingface.co/zai-org/cogagent-9b-20241220)
- **Papier :** [arXiv:2312.08914](https://arxiv.org/abs/2312.08914) (v1), v2 dec 2024 sans papier dédié
- **GitHub :** [zai-org/CogAgent](https://github.com/zai-org/CogAgent)
- **Release :** 2024-12-20 (v2)
- **Licence :** **Other** (Custom Zhipu License, non Apache — vérifier compat commerciale healthtech !)
- **Base :** GLM-4V-9B (14B total : 9B language + 5B vision)
- **Bench :** "leader cité" sur ScreenSpot vs GPT-4o/Claude/SeeClick mais **chiffre SSPro précis non publié dans les sources consultées**
- **Sortie :** action DSL `CLICK(box=[[x1,y1,x2,y2]], element_info='...')`, conv. probablement absolue sur 1120×1120
- **Risque licence :** "Other" custom, à valider ligne par ligne avant production commerciale.
### 3.12. ShowUI-2B (Show Lab, CVPR 2025)
- **Repo HF :** [showlab/ShowUI-2B](https://huggingface.co/showlab/ShowUI-2B)
- **Papier :** [arXiv:2411.17465](https://arxiv.org/abs/2411.17465), CVPR 2025
- **GitHub :** [showlab/ShowUI](https://github.com/showlab/ShowUI)
- **Release :** 2024-11-26
- **Licence :** MIT
- **Base :** Qwen2-VL-2B-Instruct
- **Bench :** ScreenSpot 75.1 % (zéro-shot), **SSPro 7.7 %** *(très faible — modèle 2B léger)*
- **Sortie :** point normalisé 0-1 + action dict structuré
- **Pertinent si :** contrainte VRAM extrême (4 GB), workflow simple, fenêtres low-res. Pas pour Easily 2560×1600.
- **Successeur FocusUI** ([CVPR 2026](https://github.com/showlab/FocusUI)) : framework token pruning sur Qwen2.5-VL / Qwen3-VL multi-sizes, outperforme SOTA précédents.
### 3.13. SeeClick (référence historique, ACL 2024)
- **Repo HF :** [cckevinn/SeeClick](https://huggingface.co/cckevinn/SeeClick)
- **Papier :** [arXiv:2401.10935](https://arxiv.org/abs/2401.10935), ACL 2024
- **GitHub :** [njucckevin/SeeClick](https://github.com/njucckevin/SeeClick)
- **Release :** 2024-04
- **Licence :** Apache 2.0
- **Base :** Qwen-VL ≈9.6B + LoRA finetune
- **Bench :** ScreenSpot 53.4 % moyenne (Windows text 55.7, Windows icon/widget 32.5), **SSPro <10 % (papier SSPro orig.)**
- **Statut chez nous :** déjà testé, retiré de `intelligent_executor.py` au commit `d1b556b6c` (avril 2026, "cassé"). À NE PAS réutiliser.
### 3.14. GUI-G2-7B (Zhejiang Univ / inclusionAI, AAAI 2026)
- **Repo HF :** [inclusionAI/GUI-G2-7B](https://huggingface.co/inclusionAI/GUI-G2-7B)
- **GitHub :** [ZJU-REAL/GUI-G2](https://github.com/ZJU-REAL/GUI-G2)
- **Papier :** AAAI 2026 *GUI-G²: Gaussian Reward Modeling for GUI Grounding*
- **Innovation :** Gaussian reward modeling pour RL — récompense continue scalée selon la taille de l'élément cible (≠ binaire). Pertinent pour icônes petites en haute-res (cas Easily Assure).
- **Bench :** SOTA sur ScreenSpot/SSv2/SSPro déclaré (papier InfiGUI cite GUI-G2-7B à 47.5 % SSPro)
- **Statut :** récent (jan 2026), à surveiller mais pas encore largement reproduit publiquement.
### 3.15. UI-Venus (inclusionAI, 2025-2026)
- **GitHub :** [inclusionAI/UI-Venus](https://github.com/inclusionAI/UI-Venus)
- **Statut :** signalé dans recherche comme native UI agent screenshot-only. Pas d'évaluation détaillée trouvée dans nos sources.
### 3.16. Florence-2 (Microsoft) — hors scope GUI
- **Note :** modèle 0.27B encodant les coords comme tokens, **non entraîné sur UI** (object/phrase grounding général). Cité pour complétude — **PAS adapté** au cas GUI, à éliminer.
---
## 4. Analyse Qwen3-VL vs InfiGUI-G1 vs OS-Atlas vs Magma sur notre cas usage
Périmètre concret : Windows desktop Easily Assure, fenêtre 2560×1600 souvent croppée par mss à 2560×60 (bug DETTE séparé), 22+ steps mixant tabs, dropdowns, dialogues modaux, boutons de toolbar, champs de saisie.
| Critère | InfiGUI-G1-7B (upgrade direct) | Qwen3-VL-8B-Instruct (plan migration) | OS-Atlas-Base-7B (référence 2024) | Magma-8B (Microsoft hybrid) |
|---|---|---|---|---|
| **VRAM 4-bit RTX 5070 12 GB** | ~6 GB ✅ | ~6 GB ✅ | ~6 GB ✅ | ~8 GB ⚠️ (fork transfo) |
| **ScreenSpot-Pro (SSPro)** | 51.9 ✅ | 54.6 ✅ | 18.9 ❌ | non publié SSPro |
| **Convention coords** | post-resize (factor 28) — `_smart_resize` déjà en place | post-resize (factor 32) — DETTE-014 à recaler | 0-1000 normalisé — **pas de bug d'échelle** | SoM/marks, complexe |
| **Bug d'échelle bbox_2d évité ?** | non par construction, mais `_smart_resize` côté serveur OK si bien calibré | non par construction, idem (factor 32 ≠ 28 → recalibration) | **OUI** (0-1000 indépendant) | non documenté |
| **Format sortie** | point JSON `point_2d` | bbox_2d OU point JSON | bbox + point JSON | SoM (numéros sur image) |
| **vLLM support** | ✅ natif | ✅ (vllm≥0.11) | ✅ | ❌ fork custom |
| **Continuité code existant** | **maximale** — même architecture `Qwen2_5_VLForConditionalGeneration`, mêmes prompts, juste `MODEL_ID` à changer | moyenne — Qwen3-VL = nouvelle architecture, factor 32 ≠ 28, prompts à adapter (think:false, num_predict≥128) | bonne (Qwen2-VL base) — mais format coord 0-1000 → tout le parsing à refaire | faible — fork transfo, head SoM, parser custom |
| **Healthtech licence commerciale** | ✅ Apache 2.0 | ✅ Apache 2.0 | ✅ Apache 2.0 | ✅ MIT (encore plus permissive) |
| **Risque démo (Easily 2560×1600)** | bas | moyen (recalage factor 32 + DETTE-014 + nouveaux prompts) | élevé (SSPro 18.9 = grosses erreurs sur dialogues complexes) | élevé (intégration custom) |
| **Effort migration** | ~1 jour | ~3-5 jours | ~2-3 jours (réécrire parser 0-1000) | ~1 semaine + intégration spéciale |
**Conclusion comparative :** **InfiGUI-G1-7B est l'upgrade le plus rapide et le moins risqué**. Qwen3-VL-8B est techniquement aussi bon mais demande de recalibrer `_smart_resize` (DETTE-014 documente déjà le piège factor 28 vs 32). OS-Atlas perd 30+ pts SSPro vs cohorte 2025 mais offre la convention 0-1000 qui élimine le bug d'échelle. Magma intéressant en R&D, pas en production court terme.
---
## 5. Bug d'échelle bbox_2d : quels modèles l'évitent
Rappel du bug (cf. [`MIGRATION_VLM_PLAN_2026-05-09.md`](../MIGRATION_VLM_PLAN_2026-05-09.md) §1.2) : les coordonnées renvoyées sont dans la résolution **post-`smart_resize`** appliquée par le modèle, mais le code prod divise par `orig_w` au lieu de `resized_w` → toutes les coords shiftées top-left. Ollama n'expose pas `resized_dimensions`, d'où impossibilité de fixer côté client.
### Modèles SANS bug d'échelle (par construction)
1. **UGround-V1 (toutes tailles)** — sortie en `[0, 1000)` normalisé, parser officiel `actual_x = (x / 1000) * image_width`. Le modèle a appris à raisonner dans un espace canonique indépendant de la résolution réelle.
2. **OS-Atlas-Base-7B / Base-4B** — sortie normalisée 0-1000 (héritage Qwen2-VL). Pas d'aller-retour resize → coord.
3. **MolmoPoint-GUI-8B** — sortie en pixels absolus (le grounding-token est décodé en (x, y) image originale). Aucune transformation à faire côté client.
4. **GUI-Actor-7B-Qwen2.5-VL** — sortie en `topk_points` normalisés 0-1, sans texte coordonnées (attention head sur patches visuels). **Architecturalement coordinate-free** = élimination radicale du bug.
5. **UI-TARS-1.5-7B** — sortie en pixels absolus dans le DSL `click(start_box='[x1,y1,x2,y2]')`. Documenté ainsi, mais le modèle a un smart_resize interne dont la cohérence avec son DSL est à vérifier en réel (issue #215 GitHub suggère reproduction inconstante).
### Modèles AVEC bug d'échelle latent (à gérer côté client)
6. **InfiGUI-G1-3B / 7B** — `point_2d` post-resize, mais la fiche HF expose **explicitement** `{new_width}x{new_height}` dans le prompt et fournit le mapping. Si on lit la doc, pas de surprise. Notre `core/grounding/server.py` a déjà `_smart_resize` calibré.
7. **Qwen3-VL-8B-Instruct** — `bbox_2d` post-resize (factor **32**, pas 28 !). Avec backend in-process (vLLM ou Transformers), on peut passer `resized_width/resized_height` au modèle. Avec Ollama → impossible (cf. plan migration).
8. **Qwen2.5-VL-7B-Instruct** *(legacy)* — racine du bug actuel chez nous via Ollama. À abandonner.
9. **AGUVIS-7B-720P** — `720P` dans le nom suggère resize fixe vers 720p, mais convention coord non documentée.
### Recommandation
Pour éliminer **définitivement** le bug d'échelle :
- **Court terme (continuité code)** : passer en backend Transformers in-process avec exposition explicite de `resized_width/resized_height` (déjà en place dans `core/grounding/server.py` pour InfiGUI). Migrer 3B → 7B.
- **Moyen terme (architecture)** : évaluer GUI-Actor ou MolmoPoint-GUI en R&D pour l'approche coordinate-free / absolue.
---
## 6. Recommandation actionnable
Si on devait migrer maintenant pour la démo cliente suivante (post-GHT) :
### Option A — Continuité chirurgicale (recommandée)
**Modèle :** `InfiX-ai/InfiGUI-G1-7B`
**Backend :** Transformers in-process via `core/grounding/server.py` (déjà en place), changer `MODEL_ID` (env `GROUNDING_MODEL`)
**Effort :** ~1 jour
**Gain attendu :** +6.7 pts SSPro vs InfiGUI-G1-3B actuel (45.2 → 51.9), même format sortie, `_smart_resize` factor 28 inchangé
**Risque :** bas — même architecture, mêmes prompts, juste +3 GB VRAM (~6 GB en 4-bit NF4, tient)
### Option B — Saut SOTA open (R&D parallèle)
**Modèle :** `allenai/MolmoPoint-GUI-8B` (SSPro 61.1, open SOTA)
**Backend :** Transformers in-process avec logits processor custom (pas vLLM)
**Effort :** ~3-5 jours (intégration spéciale, training/eval pipelines)
**Gain attendu :** +15 pts SSPro vs actuel, **convention coord absolue → ZÉRO bug d'échelle**
**Risque :** moyen — pas de vLLM, single-image, intégration non standard
### Option C — Aligner sur le plan migration existant (Qwen3-VL)
**Modèle :** `Qwen/Qwen3-VL-8B-Instruct` (cible documentée dans [`MIGRATION_VLM_PLAN_2026-05-09.md`](../MIGRATION_VLM_PLAN_2026-05-09.md))
**Backend :** vLLM ≥0.11 (déjà câblé `resolve_engine.py:785-816`) ou Transformers
**Effort :** ~3-5 jours
**Gain attendu :** +9.4 pts SSPro (54.6 vs 45.2), `resized_width/resized_height` passable explicitement
**Risque :** moyen — factor 32 ≠ 28 (DETTE-014), nouveaux prompts (`think:false`, num_predict≥128), nouvelle architecture Qwen3-VL
### Choix recommandé : **A maintenant, B en R&D parallèle, C reporté tant que A fonctionne**
Raisons :
- A minimise le risque court terme et capitalise sur l'infra `core/grounding/` déjà investie depuis 2026-04-26.
- B teste l'hypothèse "coordinate-free / absolue" qui pourrait être le pattern d'avenir.
- C demande de recalibrer le smart_resize sur factor 32 (DETTE-014 explicite), opération à faire UNE fois et qui mérite le timing post-démo.
**Question ouverte pour Dom :** est-ce que l'écart 45.2 → 51.9 SSPro (option A) suffit pour débloquer les cas Easily où le grounding échoue actuellement ? Si la cause primaire est transport (cf. diagnostic 8 mai, [`REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md`](../REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md)), un modèle SOTA ne corrigera rien.
---
## 7. Sources
### Benchmarks et leaderboards
- [ScreenSpot-Pro paper arXiv:2504.07981](https://arxiv.org/abs/2504.07981) — *ScreenSpot-Pro: GUI Grounding for Professional High-Resolution Computer Use* (ICLR 2025)
- [ScreenSpot-Pro leaderboard llm-stats](https://llm-stats.com/benchmarks/screenspot-pro) — leaderboard tiers (21 modèles, GPT-5.2 leader 86.3 %)
- [gui-agent.github.io grounding-leaderboard](https://gui-agent.github.io/grounding-leaderboard/) — infra leaderboard académique
- [ScreenSpot-Pro GitHub likaixin2000](https://github.com/likaixin2000/ScreenSpot-Pro-GUI-Grounding) — repo officiel benchmark
- [HF blog Ziyang ScreenSpot-Pro](https://huggingface.co/blog/Ziyang/screenspot-pro) — annonce HF du benchmark
- [WindowsAgentArena Microsoft](https://microsoft.github.io/WindowsAgentArena/) — environnement Windows benchmark
- [Awesome Agents Computer Use leaderboard](https://awesomeagents.ai/leaderboards/computer-use-leaderboard/) — leaderboard tiers
- [OSU GUI-Agents Paper List](https://github.com/OSU-NLP-Group/GUI-Agents-Paper-List/blob/main/paper_by_key/paper_visual_grounding.md) — recensement papiers
### Modèles open-source (Apache / MIT) — repos et papiers
- **InfiGUI-G1** : [HF 3B](https://huggingface.co/InfiX-ai/InfiGUI-G1-3B), [HF 7B](https://huggingface.co/InfiX-ai/InfiGUI-G1-7B), [paper arXiv:2508.05731](https://arxiv.org/abs/2508.05731), [GitHub InfiXAI/InfiGUI-G1](https://github.com/InfiXAI/InfiGUI-G1)
- **InfiGUI-R1** : [paper arXiv:2504.14239](https://arxiv.org/abs/2504.14239), [GitHub InfiXAI/InfiGUI-R1](https://github.com/InfiXAI/InfiGUI-R1)
- **UI-TARS-1.5** : [HF ByteDance-Seed/UI-TARS-1.5-7B](https://huggingface.co/ByteDance-Seed/UI-TARS-1.5-7B), [GitHub bytedance/UI-TARS](https://github.com/bytedance/ui-tars), [GitHub UI-TARS-desktop](https://github.com/bytedance/UI-TARS-desktop), [paper arXiv:2501.12326](https://arxiv.org/abs/2501.12326), [UI-TARS-2 tech report arXiv:2509.02544](https://arxiv.org/html/2509.02544v1)
- **Qwen3-VL** : [HF Qwen/Qwen3-VL-8B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-8B-Instruct), [GitHub QwenLM/Qwen3-VL](https://github.com/QwenLM/Qwen3-VL), [HF Qwen/Qwen3-VL-4B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct)
- **Qwen2.5-VL** : [HF Qwen/Qwen2.5-VL-7B-Instruct](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct), [discussion #13 bbox_2d resize bug](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct/discussions/13)
- **Holo1 / Holo1.5** : [HF Hcompany/Holo1.5-7B](https://huggingface.co/Hcompany/Holo1.5-7B), [3B](https://huggingface.co/Hcompany/Holo1.5-3B), [HF blog Holo1](https://huggingface.co/blog/Hcompany/holo1), [paper Surfer-H arXiv:2506.02865](https://arxiv.org/pdf/2506.02865), [HF blog GRPO grounding-r1](https://huggingface.co/blog/HelloKKMe/grounding-r1)
- **UGround** : [HF osunlp/UGround-V1-7B](https://huggingface.co/osunlp/UGround-V1-7B), [2B](https://huggingface.co/osunlp/UGround-V1-2B), [72B](https://huggingface.co/osunlp/UGround-V1-72B), [paper arXiv:2410.05243](https://arxiv.org/abs/2410.05243), [GitHub OSU-NLP-Group/UGround](https://github.com/OSU-NLP-Group/UGround)
- **OS-Atlas** : [HF OS-Copilot/OS-Atlas-Base-7B](https://huggingface.co/OS-Copilot/OS-Atlas-Base-7B), [Base-4B](https://huggingface.co/OS-Copilot/OS-Atlas-Base-4B), [Pro-7B](https://huggingface.co/OS-Copilot/OS-Atlas-Pro-7B), [paper arXiv:2410.23218](https://arxiv.org/abs/2410.23218), [GitHub OS-Copilot/OS-Atlas](https://github.com/OS-Copilot/OS-Atlas)
- **AGUVIS** : [HF xlangai/Aguvis-7B-720P](https://huggingface.co/xlangai/Aguvis-7B-720P)
- **Magma** : [HF microsoft/Magma-8B](https://huggingface.co/microsoft/Magma-8B), [paper arXiv:2502.13130](https://arxiv.org/abs/2502.13130), [GitHub microsoft/Magma](https://github.com/microsoft/Magma), [Microsoft blog](https://www.microsoft.com/en-us/research/blog/magma-a-foundation-model-for-multimodal-ai-agents-across-digital-and-physical-worlds/)
- **GUI-Actor** : [HF microsoft/GUI-Actor-7B-Qwen2.5-VL](https://huggingface.co/microsoft/GUI-Actor-7B-Qwen2.5-VL), [Qwen2-VL variant](https://huggingface.co/microsoft/GUI-Actor-7B-Qwen2-VL), [paper arXiv:2506.03143](https://arxiv.org/abs/2506.03143), [GitHub microsoft/GUI-Actor](https://github.com/microsoft/GUI-Actor), [project page](https://microsoft.github.io/GUI-Actor/)
- **MolmoPoint-GUI** : [HF allenai/MolmoPoint-GUI-8B](https://huggingface.co/allenai/MolmoPoint-GUI-8B), [blog Ai2 MolmoPoint](https://allenai.org/blog/molmopoint), [GitHub allenai/molmo2](https://github.com/allenai/molmo2), [MolmoWeb blog](https://allenai.org/blog/molmoweb)
- **CogAgent v2** : [HF zai-org/cogagent-9b-20241220](https://huggingface.co/zai-org/cogagent-9b-20241220), [GitHub zai-org/CogAgent](https://github.com/zai-org/CogAgent), [paper v1 arXiv:2312.08914](https://arxiv.org/abs/2312.08914), [MarkTechPost announcement](https://www.marktechpost.com/2024/12/25/tsinghua-university-researchers-just-open-sourced-cogagent-9b-20241220-the-latest-version-of-cogagent/)
- **ShowUI / FocusUI** : [HF showlab/ShowUI-2B](https://huggingface.co/showlab/ShowUI-2B), [paper arXiv:2411.17465](https://arxiv.org/abs/2411.17465), [GitHub showlab/ShowUI](https://github.com/showlab/showui), [GitHub showlab/FocusUI](https://github.com/showlab/FocusUI)
- **SeeClick** : [HF cckevinn/SeeClick](https://huggingface.co/cckevinn/SeeClick), [paper arXiv:2401.10935](https://arxiv.org/abs/2401.10935), [GitHub njucckevin/SeeClick](https://github.com/njucckevin/SeeClick)
- **GUI-G2** : [HF inclusionAI/GUI-G2-7B](https://huggingface.co/inclusionAI/GUI-G2-7B), [GitHub ZJU-REAL/GUI-G2](https://github.com/ZJU-REAL/GUI-G2)
- **OmniParser V2** : [GitHub microsoft/OmniParser](https://github.com/microsoft/omniparser), [Microsoft Research V2 blog](https://www.microsoft.com/en-us/research/articles/omniparser-v2-turning-any-llm-into-a-computer-use-agent/)
### Annexes
- [Codersera Qwen3-VL Instruct vs Thinking guide 2026](https://codersera.com/blog/qwen3-vl-8b-instruct-vs-qwen3-vl-8b-thinking-2025-guide/)
- [Skywork blog Qwen3-VL GUI Automation 2025](https://skywork.ai/blog/llm/qwen3-vl-gui-automation-2025-visual-agent-revolution/)
- [BinaryVerse Qwen3-VL benchmarks](https://binaryverseai.com/qwen3-vl-benchmarks-local-installation-guide-use/)
- [The Decoder Qwen3-VL videos](https://the-decoder.com/qwen3-vl-can-scan-two-hour-videos-and-pinpoint-nearly-every-detail/)
- [HF discussion #13 Qwen2.5-VL bbox resize bug](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct/discussions/13) — racine documentée du bug que nous vivons
- [GitHub QwenLM/Qwen3-VL issue #1831 — image zoom factor 32 vs 28](https://github.com/QwenLM/Qwen3-VL/issues/1831) — directement lié à notre DETTE-014
---
## 8. Liens avec autres axes de recherche du projet
| Axe | Lien |
|---|---|
| **A2 — smart_resize** | Le choix de modèle conditionne le `factor` à utiliser : Qwen2.5-VL = 28, Qwen3-VL = 32, OS-Atlas/UGround = pas de smart_resize (espace 0-1000). DETTE-014 du repo (`feedback_reread_before_code.md`) doit être recalibrée selon le modèle final retenu. |
| **A3 — Bench grounding bbox cible** | Le test à refaire (`MIGRATION_VLM_PLAN_2026-05-09.md` §5) doit inclure les 3 candidats top : InfiGUI-G1-7B, Qwen3-VL-8B-Instruct, Holo1.5-7B, sur la fixture `heartbeat_1773792436.png` 2560×1600. Critère : OK button à cx ≈ 0.45-0.55. |
| **B2 — Validator (Planner-Actor-Validator)** | GUI-Actor inclut un grounding verifier pour évaluer les candidats. MolmoPoint-GUI retourne topk points. Pattern à intégrer dans notre `replay_verifier.py` actuellement laxiste (cf. synthèse §5.2). |
| **B3 — Coordinate-free architecture** | GUI-Actor (NeurIPS 2025) et MolmoPoint-GUI (Ai2 2026) ouvrent une voie post-coordonnée. À explorer pour v2 grounding, indépendant de l'urgence démo. |
| **Démo GHT post-mortem** | Le bug primaire de la démo 8-19 mai était transport HTTP, pas grounding (cf. [`LESSONS_LEARNED_GHT_2026-05.md`](../LESSONS_LEARNED_GHT_2026-05.md)). Migrer le VLM n'a de sens qu'après stabilisation transport (5 bugs P0 toujours ouverts). |
---
*Document destiné à informer la décision de migration VLM post-démo GHT. Pas de modification de code. La décision opérationnelle (option A / B / C) doit être validée par Dom.*

View File

@@ -0,0 +1,759 @@
# Axe A2 — Doctrine smart_resize et convention bbox_2d (RPA Vision V3)
**Date :** 2026-05-23
**Auteur :** Claude (sous-agent recherche, contexte mission Dom)
**Périmètre :** clore techniquement DETTE-014, DETTE-010, DETTE-007, DETTE-006 par une doctrine officielle de preprocessing image et de parsing de coordonnées, par famille de modèle VLM grounding.
**Statut :** lecture seule, aucune modification de code. Synthèse de sources officielles HuggingFace, QwenLM, vLLM, Ollama, InfiX-AI, OS-Copilot.
---
## 1. TL;DR
Trois familles de modèles utilisent les chiffres suivants pour preprocesser l'image et exprimer les coordonnées de sortie :
| Famille | `patch_size` | `spatial_merge_size` | **Factor image** | Convention output |
|---|---:|---:|---:|---|
| **Qwen2-VL / Qwen2.5-VL** (et tous fine-tunes — InfiGUI-G1-3B, UI-TARS-1.5-7B, OS-Atlas-Base-7B, SeeClick) | **14** | 2 | **28** | bbox/point en **pixel absolu post-smart_resize** (sauf OS-Atlas qui normalise sur 1000) |
| **Qwen3-VL** (Instruct / Thinking, dense + MoE) | **16** | 2 | **32** | bbox/point en pixel absolu post-smart_resize, format `[x1,y1,x2,y2]` ou `point_2d` |
| **OS-Atlas-Base-4B** (backbone InternVL-2) | tiles 448×448 | dynamic_preprocess | n/a (max 6 tiles) | bbox/point **normalisés [0, 1000]** dans des balises `<box>`/`<point>` |
Trois constantes officielles, communes à toutes les variantes Qwen-VL via `qwen_vl_utils` :
- `IMAGE_MIN_TOKEN_NUM = 4``min_pixels = 4 × factor²`
- `IMAGE_MAX_TOKEN_NUM = 16384``max_pixels = 16384 × factor²`
- `MAX_RATIO = 200`
**Le bug d'échelle bbox_2d documenté (cf. `MIGRATION_VLM_PLAN_2026-05-09.md`) a deux racines distinctes** :
1. **Ollama opaque** : Ollama applique son propre redimensionnement interne (factor 28 pour qwen2.5-vl, factor 32 pour qwen3-vl) sans permettre au client de passer `resized_width` / `resized_height`, ni de récupérer la taille effective post-resize utilisée par le modèle. Conséquence : impossible de re-normaliser les coordonnées correctement. Ce n'est pas un bug réparable côté client — c'est une **limitation de protocole**. La citation mainteneur de la discussion HF #13 est exacte : « *there is no option (at least i don't found any) to set "resized_width": img_width, "resized_height": img_height* ».
2. **Code prod RPA Vision V3** : `resolve_engine.py:925` (`small_w, small_h = orig_w, orig_h # pas de redimensionnement`) puis `parse_bbox_to_norm(content, small_w, small_h)` divise les coordonnées du modèle par la taille PRE-resize au lieu de la taille POST-resize. C'est documenté dans `core/grounding/bbox_parser.py:10-13` (DETTE-006 ouverte). Côté Ollama, **même corrigé, ça ne marchera pas tant qu'on n'a pas la taille post-resize d'Ollama** ; côté vLLM/Transformers, ça marche dès qu'on impose nos propres `resized_height/resized_width`.
**Recommandation d'unification (DETTE-007)** :
- Conserver `core/grounding/smart_resize.py` comme **module de référence** (formule officielle correcte).
- Le corriger pour exposer la famille de modèle : `smart_resize_for_family(model_name, orig_h, orig_w) -> (resized_h, resized_w)` avec dispatch automatique `factor=28` (Qwen2-VL / Qwen2.5-VL / InfiGUI / UI-TARS / OS-Atlas-7B) vs `factor=32` (Qwen3-VL).
- Supprimer les deux copies inline (`core/grounding/server.py:15-26` et `core/grounding/infigui_worker.py:99-101` — cette dernière est tronquée et ne clampe pas MIN/MAX_PIXELS, source probable de désalignement sur petites images).
- Ajouter `parse_bbox_for_family(model_name, raw_output, resized_w, resized_h, orig_w, orig_h) -> (x_pct, y_pct)` qui encapsule la conversion (pixel post-resize → pourcentage de l'image originale envoyée).
- Migrer `resolve_engine.py` (4 sites bbox + `_locate_popup_button`) pour appeler ces deux fonctions au lieu de l'arithmétique inline (corrige DETTE-006).
- **Abandonner Ollama pour le grounding bbox** tant qu'il n'expose pas les dimensions post-resize. Cible vLLM ou Transformers in-process avec passage explicite de `resized_width`/`resized_height` (cf. §4).
---
## 2. Doctrine par famille de modèle
### 2.1. Qwen2-VL / Qwen2.5-VL (et tous les fine-tunes : InfiGUI-G1-3B, UI-TARS-1.5-7B, OS-Atlas-Base-7B, SeeClick)
**Architecture vision** (source : Qwen2.5-VL technical report, arXiv 2502.13923) :
- `patch_size = 14`
- `spatial_merge_size = 2`
- → Factor image = `14 × 2 = 28`
**Pourquoi 28 et pas 14 ?** Le ViT découpe l'image en patches de 14×14 px (stride 14). Puis le projector multimodal fusionne les patches voisins par groupes de `spatial_merge_size × spatial_merge_size = 2×2`. Donc les dimensions H et W doivent être divisibles par `14 × 2 = 28` pour que la fusion soit propre.
**Constantes officielles** (verbatim depuis `qwen-vl-utils/src/qwen_vl_utils/vision_process.py`, repo Qwen3-VL `main`) :
```python
MAX_RATIO = 200
SPATIAL_MERGE_SIZE = 2
IMAGE_MIN_TOKEN_NUM = 4
IMAGE_MAX_TOKEN_NUM = 16384
```
Pour Qwen2.5-VL avec `image_patch_size = 14` :
- `factor = 14 × 2 = 28`
- `min_pixels = 4 × 28² = 3136`
- `max_pixels = 16384 × 28² = 12 845 056`
**Snippet officiel `smart_resize`** (verbatim) :
```python
import math
from typing import Optional, Tuple
MAX_RATIO = 200
IMAGE_MIN_TOKEN_NUM = 4
IMAGE_MAX_TOKEN_NUM = 16384
def round_by_factor(number: int, factor: int) -> int:
"""Returns the closest integer to 'number' that is divisible by 'factor'."""
return round(number / factor) * factor
def ceil_by_factor(number: int, factor: int) -> int:
"""Returns the smallest integer greater than or equal to 'number' that is divisible by 'factor'."""
return math.ceil(number / factor) * factor
def floor_by_factor(number: int, factor: int) -> int:
"""Returns the largest integer less than or equal to 'number' that is divisible by 'factor'."""
return math.floor(number / factor) * factor
def smart_resize(
height: int,
width: int,
factor: int,
min_pixels: Optional[int] = None,
max_pixels: Optional[int] = None,
) -> Tuple[int, int]:
"""Rescales the image so that the following conditions are met:
1. Both dimensions (height and width) are divisible by 'factor'.
2. The total number of pixels is within the range ['min_pixels', 'max_pixels'].
3. The aspect ratio of the image is maintained as closely as possible.
"""
max_pixels = max_pixels if max_pixels is not None else (IMAGE_MAX_TOKEN_NUM * factor ** 2)
min_pixels = min_pixels if min_pixels is not None else (IMAGE_MIN_TOKEN_NUM * factor ** 2)
assert max_pixels >= min_pixels, "The max_pixels of image must be greater than or equal to min_pixels."
if max(height, width) / min(height, width) > MAX_RATIO:
raise ValueError(
f"absolute aspect ratio must be smaller than {MAX_RATIO}, got {max(height, width) / min(height, width)}"
)
h_bar = max(factor, round_by_factor(height, factor))
w_bar = max(factor, round_by_factor(width, factor))
if h_bar * w_bar > max_pixels:
beta = math.sqrt((height * width) / max_pixels)
h_bar = floor_by_factor(height / beta, factor)
w_bar = floor_by_factor(width / beta, factor)
elif h_bar * w_bar < min_pixels:
beta = math.sqrt(min_pixels / (height * width))
h_bar = ceil_by_factor(height * beta, factor)
w_bar = ceil_by_factor(width * beta, factor)
return h_bar, w_bar
```
**Convention coord output Qwen2.5-VL** : pixel absolu dans la résolution **post-smart_resize**. Confirmé verbatim par le mainteneur Qwen sur la discussion HF #13 de Qwen2.5-VL-7B-Instruct :
> "The bbox_2d coordinates are x1, y1, x2, y2 rather than x,y,w,h. And they will be relative to your resized image size if you are resizing."
Pour reconvertir en coord originale :
```python
x_orig = x_post_resize * (orig_w / resized_w)
y_orig = y_post_resize * (orig_h / resized_h)
```
### 2.2. Qwen3-VL (Instruct / Thinking, dense + MoE)
**Architecture vision** (source : `transformers/main/model_doc/qwen3_vl`, classe `Qwen3VLVisionConfig`) :
- `patch_size = 16` (au lieu de 14)
- `spatial_merge_size = 2`
- → Factor image = `16 × 2 = 32`
**Constantes officielles** : identiques à Qwen2.5-VL côté `qwen_vl_utils` (`IMAGE_MIN_TOKEN_NUM=4`, `IMAGE_MAX_TOKEN_NUM=16384`, `MAX_RATIO=200`). Seul le facteur change.
Pour Qwen3-VL avec `image_patch_size = 16` :
- `factor = 16 × 2 = 32`
- `min_pixels = 4 × 32² = 4096`
- `max_pixels = 16384 × 32² = 16 777 216`
**API officielle Qwen3-VL** (verbatim README qwen-vl-utils) :
```python
from qwen_vl_utils import process_vision_info
images, videos, video_kwargs = process_vision_info(
messages,
image_patch_size=16, # ← Qwen3-VL utilise 16
return_video_kwargs=True,
return_video_metadata=True,
)
```
Le paramètre `image_patch_size` est exposé pour permettre à `process_vision_info` de calculer `factor = image_patch_size * SPATIAL_MERGE_SIZE` dynamiquement. **Si tu utilises Qwen3-VL avec un module `smart_resize` figé sur `factor=28`, l'image envoyée n'est PAS divisible par 32, donc le processor va re-resizer derrière toi à une taille que tu ne connais pas.** C'est exactement la racine documentée de DETTE-014 (`core/grounding/smart_resize.py` est calé sur factor 28).
**Note importante** : l'issue [QwenLM/Qwen3-VL#1831](https://github.com/QwenLM/Qwen3-VL/issues/1831) signale que **`image_zoom_in_qwen3vl.py` utilise factor=32** alors que le rapport technique Qwen2.5-VL parle de 28. Confusion levée : c'est bien `32` pour Qwen3-VL (`patch_size=16`) et `28` pour Qwen2.5-VL (`patch_size=14`). Le rapport technique 2502.13923 décrit la famille Qwen2.5-VL, pas Qwen3-VL.
**Convention coord output Qwen3-VL** : pixel absolu post-smart_resize, idem Qwen2.5-VL. Issue [QwenLM/Qwen3-VL#1486](https://github.com/QwenLM/Qwen3-VL/issues/1486) mentionne aussi une variante 01000 (`convert_to_qwen2vl_format(bbox, h, w)`), utilisée lors du fine-tuning. **Pour l'inférence prod via process_vision_info, c'est pixel absolu post-resize.** À confirmer empiriquement par le test §8.
**Différence smart_resize transformers vs qwen-vl-utils** : signalée dans l'issue [QwenLM/Qwen3-VL#2068](https://github.com/QwenLM/Qwen3-VL/issues/2068) — la version transformers ajoute une contrainte `temporal_factor` pour les vidéos. Pour les **images**, les deux sont équivalents. Pour les vidéos, utiliser la version transformers.
### 2.3. InfiGUI-G1-3B (notre modèle grounding principal en place)
**Architecture** : fine-tune de Qwen2.5-VL-3B-Instruct (source : carte HF `InfiX-ai/InfiGUI-G1-3B`). Donc **toutes les conventions Qwen2.5-VL s'appliquent** : `patch_size=14`, `factor=28`, output en pixel absolu post-resize.
**Convention output InfiGUI** (verbatim carte HF) :
```
Format prompt : The screen's resolution is {width}x{height}.
Locate the UI element(s) for "{instruction}",
output the coordinates using JSON format: [{"point_2d": [x, y]}, ...]
Format output : <think>...</think>[{"point_2d": [x, y]}, ...]
où (x, y) est exprimé en pixel post-smart_resize.
```
Le `point_2d` est le centre de l'élément (pas une bbox). Conversion en pixel original :
```python
x_orig = x_point * (orig_w / resized_w)
y_orig = y_point * (orig_h / resized_h)
```
**MAX_IMAGE_PIXELS officiel InfiGUI** : `5600 * 28 * 28 = 4 390 400` (carte HF). C'est ce qui est utilisé par `core/grounding/server.py:11` et `core/grounding/infigui_worker.py:50` — cohérent avec la doctrine.
**Bug local DETTE-006/DETTE-007** : `core/grounding/infigui_worker.py:99-101` calcule rH/rW avec un `round_by_factor` simple **sans clamper sur MIN_PIXELS et MAX_PIXELS**. Sur une image en-dessous de 56×56 px (cas heartbeat 2560×60 cité dans `LESSONS_LEARNED_GHT_2026-05.md`), c'est une bombe : le ratio aspect dépasse `MAX_RATIO=200` et toute la logique de smart_resize tombe à plat. À corriger.
### 2.4. UI-TARS-1.5-7B (ancien modèle grounding, remplacé par InfiGUI)
**Architecture** : fine-tune de Qwen2.5-VL-7B. Mêmes conventions que Qwen2.5-VL : `patch_size=14`, `factor=28`.
**Convention output UI-TARS** : format d'action structuré `Thought:/Action: click(start_box='(x1, y1)')`. Coordonnées en **pixel absolu post-resize**, identique à Qwen2.5-VL. Le prompt officiel est récupérable via `git show 9da589c8c:core/grounding/server.py` (commit historique avant remplacement par InfiGUI).
Sur la doctrine smart_resize : aucune différence opérationnelle avec Qwen2.5-VL. Si on veut le réactiver, c'est interchangeable avec InfiGUI sous la doctrine factor=28.
### 2.5. OS-Atlas (Base-4B et Base-7B)
**Architecture** :
- OS-Atlas-Base-4B : fine-tune **InternVL-2** (PAS Qwen-VL). Preprocessing différent : `dynamic_preprocess` avec `max_dynamic_patch=6` tiles 448×448 + thumbnail global. **Pas de smart_resize.**
- OS-Atlas-Base-7B : fine-tune de **Qwen2-VL-7B-Instruct**. Donc `patch_size=14`, `factor=28`, conventions Qwen-VL.
**Convention output OS-Atlas** (les deux versions) : coordonnées **normalisées dans [0, 1000]**, format structuré entre balises spéciales :
```
<|object_ref_start|>language switch<|object_ref_end|><|box_start|>(576,12),(592,42)<|box_end|>
```
ou pour les points :
```
<|object_ref_start|>...<|object_ref_end|><|point_start|>(x,y)<|point_end|>
```
Conversion :
```python
x_orig = (x_normalized / 1000) * orig_w
y_orig = (y_normalized / 1000) * orig_h
```
**Pas de bug d'échelle bbox_2d** sur OS-Atlas — la normalisation 01000 absorbe le smart_resize côté training. Mais format de parsing complètement différent : il faut un regex séparé sur `<|box_start|>(x1,y1),(x2,y2)<|box_end|>` et non sur `"bbox_2d": [...]`.
OS-Atlas-Base-7B est intéressant à benchmarker côté ScreenSpot car il bat Qwen2-VL standard sur GUI grounding tout en restant techniquement compatible avec le pipeline Qwen2.5-VL (même backbone, même processor → même smart_resize factor=28).
---
## 3. Convention coord récapitulative et conversion
| Famille / modèle | Sortie | Range | Parsing regex | Reconversion → pixel orig |
|---|---|---|---|---|
| Qwen2.5-VL / qwen2.5vl:7b | `{"bbox_2d":[x1,y1,x2,y2]}` | [0, resized_w] × [0, resized_h] | `"bbox_2d"\s*:\s*\[([^\]]+)\]` | `x * orig_w / resized_w` |
| Qwen3-VL / qwen3-vl:8b | idem (avec prompt JSON imposé) | [0, resized_w] × [0, resized_h] | idem | idem |
| InfiGUI-G1-3B | `[{"point_2d":[x,y]}]` après `</think>` | [0, resized_w] × [0, resized_h] | `"point_2d"\s*:\s*\[(\d+),\s*(\d+)\]` | idem |
| UI-TARS-1.5-7B | `click(start_box='(x,y)')` | [0, resized_w] × [0, resized_h] | `start_box='\((\d+),\s*(\d+)\)'` | idem |
| OS-Atlas-Base-7B | `<\|box_start\|>(x1,y1),(x2,y2)<\|box_end\|>` | **[0, 1000]** | `<\|box_start\|>\((\d+),(\d+)\),\((\d+),(\d+)\)<\|box_end\|>` | `(x / 1000) * orig_w` |
| OS-Atlas-Base-4B | idem 7B | [0, 1000] | idem | idem |
**Règle de division universelle** : on divise par la taille **dans laquelle le modèle a vu l'image**, et on multiplie par la taille **de l'image originale envoyée**. Pour les modèles pixel-absolu post-resize, c'est `resized_w/h`. Pour les modèles normalisés 01000, c'est `1000`.
---
## 4. Cartographie vLLM — passage de `resized_width` / `resized_height`
### 4.1. État officiel
vLLM supporte deux mécanismes pour passer des paramètres au processor multimodal :
1. **Au démarrage du serveur** : `--mm-processor-kwargs '{"min_pixels": ..., "max_pixels": ...}'` (config globale).
2. **Par requête, via OpenAI client `extra_body`** : depuis le PR [vllm#13533](https://github.com/vllm-project/vllm/pull/13533) (mergé 20 février 2025 par simon-mo). C'est l'extension OpenAI propre à vLLM.
### 4.2. Méthode A — `extra_body.mm_processor_kwargs` (config globale)
```python
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8100/v1", api_key="EMPTY")
resp = client.chat.completions.create(
model="Qwen/Qwen2.5-VL-7B-Instruct-AWQ",
messages=[
{"role": "user", "content": [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{shot_b64}"}},
]},
],
temperature=0.1,
max_tokens=200,
extra_body={
"mm_processor_kwargs": {
"min_pixels": 4 * 28 * 28,
"max_pixels": 5600 * 28 * 28,
}
},
)
```
**Bug ouvert [vllm#15364](https://github.com/vllm-project/vllm/issues/15364)** : `mm_processor_kwargs` passés via `extra_body` au niveau requête sont **parfois ignorés** par le processor Qwen2.5-VL, contrairement à un passage au niveau image. Le warning révélateur :
> "Keyword argument `max_pixels` is not a valid argument for this processor and will be ignored."
### 4.3. Méthode B — `resized_height` / `resized_width` au niveau image (recommandée)
C'est la méthode citée par le mainteneur Qwen sur la discussion HF #13 de Qwen2.5-VL-7B-Instruct, et confirmée par les vLLM Recipes Qwen2.5-VL. C'est **la méthode qui contourne le bug #15364** : on impose la taille image par image, et c'est garanti respecté.
```python
# 1. Côté client : calculer la taille post-resize qu'on VEUT imposer
from core.grounding.smart_resize_unified import smart_resize_for_family
resized_h, resized_w = smart_resize_for_family("Qwen/Qwen2.5-VL-7B-Instruct-AWQ", orig_h, orig_w)
# 2. Encoder l'image (sans la redimensionner soi-même, ou la redimensionner exactement à
# resized_w x resized_h — les deux marchent, le processor matchera de toute façon)
buf = io.BytesIO()
img.save(buf, format="JPEG", quality=80)
shot_b64 = base64.b64encode(buf.getvalue()).decode()
# 3. Payload OpenAI vLLM avec resized_height/resized_width INLINE dans l'image
payload = {
"model": "Qwen/Qwen2.5-VL-7B-Instruct-AWQ",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{shot_b64}"},
"resized_height": resized_h,
"resized_width": resized_w,
},
],
}],
"temperature": 0.1,
"max_tokens": 200,
}
```
⚠ Le format exact `resized_height` / `resized_width` au niveau de l'item `image_url` est l'extension Qwen, supportée par vLLM via le processor Qwen2.5-VL. À valider empiriquement par le test §8 — si vLLM rejette ce format au niveau item, basculer sur méthode A en sachant qu'on est exposé au bug #15364.
### 4.4. Modèles vLLM supportés pour ce contrat
- ✅ Qwen2-VL (depuis vLLM 0.6.x)
- ✅ Qwen2.5-VL (depuis vLLM 0.7.x, AWQ et GPTQ depuis 0.8.x)
- ✅ Qwen3-VL (depuis vLLM ~0.10.x avec Qwen3-VL Usage Guide officielle)
- ❌ InfiGUI-G1-3B : pas directement supporté en vLLM (architecture custom QwenForConditionalGeneration mais avec checkpoint InfiX-AI). **Vérifier via test charge réel** que `Qwen2_5_VLForConditionalGeneration` peut loader le checkpoint InfiX-AI sans erreur.
### 4.5. Statut du commit dans le repo prod
`agent_v0/server_v1/resolve_engine.py:957-974` appelle déjà vLLM en OpenAI-compat mais **ne passe AUCUN `mm_processor_kwargs` ni `resized_height/resized_width`**. C'est l'écart à combler. Aucune autre modif structurelle nécessaire.
---
## 5. Cartographie Ollama — pourquoi ça casse
### 5.1. Faits
- Ollama applique un smart_resize interne (côté `runner.go` / GGUF llama.cpp vision backend) pour Qwen2.5-VL et Qwen3-VL afin de respecter `factor=28` (resp. 32).
- **Aucun champ HTTP `resized_width` / `resized_height` n'est exposé** dans l'API `/api/chat` ni dans `options.*`. La citation mainteneur de la discussion HF Qwen2.5-VL #13 est exacte : « *Could be also a problem with Ollama because there is no option (at least i don't found any) to set "resized_width": img_width, "resized_height": img_height* » (Phreak87, 16 juin 2025).
- L'issue [ollama#11217](https://github.com/ollama/ollama/issues/11217) (close, juin 2025) confirme que le modèle hallucine sur sa propre taille image (il répond « 1000×1000 » à un user demandant la résolution effective) — preuve indirecte que la taille post-resize n'est jamais retournée au client.
- L'issue [ollama#10753](https://github.com/ollama/ollama/issues/10753) (close, mai 2025) montre que Qwen2.5-VL-32b crash sur images ≥ 720p en Ollama — ce qui indique que le preprocessing GGUF n'est ni robuste ni inspecté côté client.
- L'issue [ollama#11297](https://github.com/ollama/ollama/issues/11297) montre que sur les très petites images (< 28 px), le modèle crash directement avec une erreur de factor 28.
### 5.2. Conséquence directe pour DETTE-006
Tant qu'on appelle Qwen2.5-VL via Ollama :
- On ne sait pas dans quelle résolution Ollama envoie l'image au modèle.
- Le `bbox_2d` retourné est donc dans une référence inconnue, non récupérable.
- **Aucune rustine client n'est correcte**. Pré-resizer l'image avant envoi à une taille qu'on calcule officiellement (`smart_resize(factor=28)`) **peut accidentellement matcher** ce qu'Ollama re-calcule, mais ça reste fragile et version-dépendant.
### 5.3. Rustine technique possible (à utiliser uniquement si bloqué Ollama)
Si pour une raison opérationnelle on doit garder Ollama (pas de vLLM disponible, perf qwen3-vl:8b acceptable) :
1. Redimensionner l'image côté client à une taille **exactement** `factor*round(orig/factor)` clampée à `[min_pixels, max_pixels]`.
2. Envoyer cette image redimensionnée à Ollama, en stockant la taille `(resized_w, resized_h)`.
3. **Forcer le prompt à inclure la résolution** : `"The screen's resolution is {resized_w}x{resized_h}."` — c'est ce que fait InfiGUI dans son prompt officiel. Le modèle est moins susceptible de re-resizer s'il croit que c'est sa résolution naturelle.
4. Diviser `bbox_2d` par `(resized_w, resized_h)`, pas par l'orig.
C'est une approximation, pas une garantie. Le résultat est probabiliste, à valider empiriquement (le bench du 8 mai a précisément montré que même `qwen3-vl:8b (prompt JSON explicite)` retourne des coords shiftées).
**Recommandation forte** : abandonner Ollama pour le grounding bbox. Le garder pour les LLM texte (safety_checks, t2a_decision) où le bug d'échelle n'existe pas.
---
## 6. Module unifié recommandé
### 6.1. Placement
**Fichier proposé** : `core/grounding/smart_resize.py` (à étendre, ne pas créer de doublon).
Motifs :
- Module déjà créé (commit `0d7bcd18a`), déjà appelé `smart_resize`. C'est ce que les call-sites futurs auront le réflexe d'importer.
- DETTE-014 actuelle dit qu'il est calé sur la mauvaise référence (factor 28 implicite via `FACTOR_DEFAULT`). On corrige en exposant le dispatch par modèle.
- DETTE-007 demande l'unification : c'est le bon endroit pour la centralisation, à condition d'ajouter `parse_bbox_for_family` à côté (sinon dispersion).
**Alternative** : créer `core/grounding/preprocessing.py` (smart_resize + parse_bbox + helpers PIL). Plus propre conceptuellement (preprocessing vs parsing séparés peut être confusant pour un newcomer). Tranchable avec Dom.
### 6.2. Snippet `smart_resize_for_family`
```python
"""Dispatch smart_resize par famille de modèle VLM grounding.
Couvre Qwen2-VL / Qwen2.5-VL (factor=28, patch_size=14) et Qwen3-VL
(factor=32, patch_size=16). Pour OS-Atlas-Base-4B (backbone InternVL),
lever une exception explicite : preprocessing différent (dynamic_preprocess).
"""
import math
from typing import Optional, Tuple
# Constantes officielles qwen_vl_utils (communes à toutes versions Qwen-VL)
MAX_RATIO = 200
IMAGE_MIN_TOKEN_NUM = 4
IMAGE_MAX_TOKEN_NUM = 16384
SPATIAL_MERGE_SIZE = 2
# Patch sizes par famille (source : Qwen3VLVisionConfig, Qwen2VLVisionConfig)
_PATCH_SIZE_BY_FAMILY = {
"qwen2-vl": 14,
"qwen2.5-vl": 14,
"qwen2.5vl": 14,
"qwen3-vl": 16,
"qwen3vl": 16,
"infigui": 14, # fine-tune Qwen2.5-VL-3B
"ui-tars": 14, # fine-tune Qwen2.5-VL-7B
"os-atlas-7b": 14, # fine-tune Qwen2-VL-7B
"seeclick": 14, # fine-tune Qwen-VL (legacy)
}
def _detect_family(model_name: str) -> str:
"""Retourne la clé de famille à partir d'un model_name humain ou HF.
Args:
model_name: ex. 'qwen2.5vl:7b', 'Qwen/Qwen3-VL-8B-Instruct',
'InfiX-ai/InfiGUI-G1-3B', 'ByteDance-Seed/UI-TARS-1.5-7B'.
Returns:
Clé de _PATCH_SIZE_BY_FAMILY ou raise.
"""
s = model_name.lower()
if "qwen3-vl" in s or "qwen3vl" in s:
return "qwen3-vl"
if "qwen2.5-vl" in s or "qwen2.5vl" in s:
return "qwen2.5-vl"
if "qwen2-vl" in s or "qwen2vl" in s:
return "qwen2-vl"
if "infigui" in s:
return "infigui"
if "ui-tars" in s or "uitars" in s:
return "ui-tars"
if "os-atlas-base-7b" in s:
return "os-atlas-7b"
if "os-atlas-base-4b" in s:
raise ValueError(
"OS-Atlas-Base-4B uses InternVL preprocessing (dynamic_preprocess), "
"not smart_resize. Use dedicated path."
)
if "seeclick" in s:
return "seeclick"
raise ValueError(f"Unknown VLM family for model {model_name!r}")
def _round_by_factor(n: float, factor: int) -> int:
return round(n / factor) * factor
def _floor_by_factor(n: float, factor: int) -> int:
return math.floor(n / factor) * factor
def _ceil_by_factor(n: float, factor: int) -> int:
return math.ceil(n / factor) * factor
def smart_resize_for_family(
model_name: str,
orig_h: int,
orig_w: int,
*,
min_pixels: Optional[int] = None,
max_pixels: Optional[int] = None,
) -> Tuple[int, int]:
"""Calcule (resized_h, resized_w) pour un modèle VLM donné.
Implémentation : formule officielle qwen_vl_utils.smart_resize avec
factor dispatché par famille.
Args:
model_name: nom du modèle (ex. 'Qwen/Qwen3-VL-8B-Instruct').
orig_h, orig_w: dimensions de l'image originale en pixels.
min_pixels, max_pixels: bornes optionnelles. Par défaut :
IMAGE_MIN_TOKEN_NUM * factor² et IMAGE_MAX_TOKEN_NUM * factor².
Returns:
(resized_h, resized_w) divisibles par factor, dans [min_pixels,
max_pixels], aspect ratio préservé au plus près.
Raises:
ValueError: famille inconnue ou aspect ratio > MAX_RATIO.
"""
family = _detect_family(model_name)
patch_size = _PATCH_SIZE_BY_FAMILY[family]
factor = patch_size * SPATIAL_MERGE_SIZE # 28 ou 32
max_pixels = max_pixels if max_pixels is not None else IMAGE_MAX_TOKEN_NUM * factor ** 2
min_pixels = min_pixels if min_pixels is not None else IMAGE_MIN_TOKEN_NUM * factor ** 2
if max(orig_h, orig_w) / max(1, min(orig_h, orig_w)) > MAX_RATIO:
raise ValueError(
f"absolute aspect ratio must be smaller than {MAX_RATIO}, "
f"got {max(orig_h, orig_w) / max(1, min(orig_h, orig_w)):.1f}"
)
h_bar = max(factor, _round_by_factor(orig_h, factor))
w_bar = max(factor, _round_by_factor(orig_w, factor))
if h_bar * w_bar > max_pixels:
beta = math.sqrt((orig_h * orig_w) / max_pixels)
h_bar = _floor_by_factor(orig_h / beta, factor)
w_bar = _floor_by_factor(orig_w / beta, factor)
elif h_bar * w_bar < min_pixels:
beta = math.sqrt(min_pixels / (orig_h * orig_w))
h_bar = _ceil_by_factor(orig_h * beta, factor)
w_bar = _ceil_by_factor(orig_w * beta, factor)
return h_bar, w_bar
```
### 6.3. Snippet `parse_bbox_for_family`
```python
import json
import re
from typing import Optional, Tuple
# Convention output par famille (cf. §3 du doc)
_OUTPUT_RANGE_BY_FAMILY = {
"qwen2-vl": "pixel_post_resize",
"qwen2.5-vl": "pixel_post_resize",
"qwen3-vl": "pixel_post_resize",
"infigui": "pixel_post_resize",
"ui-tars": "pixel_post_resize",
"os-atlas-7b": "normalized_1000",
"seeclick": "pixel_post_resize",
}
def parse_bbox_for_family(
model_name: str,
raw_output: str,
resized_w: int,
resized_h: int,
orig_w: int,
orig_h: int,
) -> Tuple[Optional[float], Optional[float]]:
"""Parse la sortie d'un VLM grounding en (x_pct, y_pct) normalisés sur l'image originale.
Args:
model_name: nom du modèle, sert au dispatch range/format.
raw_output: texte brut renvoyé par le VLM (peut contenir <think>...</think>).
resized_w, resized_h: dimensions effectivement utilisées par le modèle.
orig_w, orig_h: dimensions de l'image originale envoyée au client.
Returns:
(x_pct, y_pct) dans [0, 1] OU (None, None) si non parsable.
"""
family = _detect_family(model_name)
range_kind = _OUTPUT_RANGE_BY_FAMILY[family]
# Nettoyer le thinking et fences markdown
body = raw_output.split("</think>")[-1] if "</think>" in raw_output else raw_output
body = body.replace("```json", "").replace("```", "").strip()
cx_px = cy_px = None
# Cas OS-Atlas : balises <|box_start|>(x1,y1),(x2,y2)<|box_end|>
if range_kind == "normalized_1000":
m = re.search(
r"<\|box_start\|>\((\d+),\s*(\d+)\),\s*\((\d+),\s*(\d+)\)<\|box_end\|>",
body,
)
if m:
x1, y1, x2, y2 = (int(g) for g in m.groups())
cx_norm = (x1 + x2) / 2 # dans [0, 1000]
cy_norm = (y1 + y2) / 2
return cx_norm / 1000.0, cy_norm / 1000.0
m = re.search(
r"<\|point_start\|>\((\d+),\s*(\d+)\)<\|point_end\|>",
body,
)
if m:
return int(m.group(1)) / 1000.0, int(m.group(2)) / 1000.0
return None, None
# Cas Qwen-VL famille : pixel post-resize, plusieurs formats
# Format 1 : InfiGUI/UI-TARS point_2d
m = re.search(r'"point_2d"\s*:\s*\[\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*\]', body)
if m:
cx_px = float(m.group(1))
cy_px = float(m.group(2))
# Format 2 : Qwen2.5-VL / Qwen3-VL bbox_2d [x1,y1,x2,y2]
if cx_px is None:
m = re.search(r'"bbox_2d"\s*:\s*\[([^\]]+)\]', body)
if m:
coords = [float(v.strip()) for v in m.group(1).split(",")]
if len(coords) == 2:
cx_px, cy_px = coords[0], coords[1]
elif len(coords) >= 4:
cx_px = (coords[0] + coords[2]) / 2
cy_px = (coords[1] + coords[3]) / 2
# Format 3 : UI-TARS click(start_box='(x, y)')
if cx_px is None:
m = re.search(r"start_box\s*=\s*['\"]?\(\s*(\d+)\s*,\s*(\d+)\s*\)", body)
if m:
cx_px = float(m.group(1))
cy_px = float(m.group(2))
# Format 4 : array brut [x, y] ou [x1, y1, x2, y2] (fallback)
if cx_px is None:
m = re.search(
r"\[\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)"
r"(?:\s*,\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?))?\s*\]",
body,
)
if m:
vals = [float(v) for v in m.groups() if v is not None]
if len(vals) >= 4:
cx_px = (vals[0] + vals[2]) / 2
cy_px = (vals[1] + vals[3]) / 2
elif len(vals) == 2:
cx_px = vals[0]
cy_px = vals[1]
if cx_px is None or cy_px is None:
return None, None
# IMPORTANT : on divise par resized_w/h (taille POST-resize) et non orig_w/h.
# C'est le fix DETTE-006.
x_pct = cx_px / resized_w
y_pct = cy_px / resized_h
if not (0.0 <= x_pct <= 1.0 and 0.0 <= y_pct <= 1.0):
return None, None
return x_pct, y_pct
```
---
## 7. Recommandation de refactor `resolve_engine.py`
### 7.1. Sites à modifier
D'après l'audit déjà réalisé dans le repo (DETTE-006 dans `bbox_parser.py:10-13` et `MIGRATION_VLM_PLAN_2026-05-09.md` §4) :
| Site | Ligne | Modification |
|---|---|---|
| `_resolve_by_grounding` initialisation | 924925 | Calculer `resized_h, resized_w = smart_resize_for_family(_grounding_model or _vllm_model, orig_h, orig_w)` au lieu de `small_w, small_h = orig_w, orig_h`. |
| Payload vLLM | 957974 | Ajouter `"resized_height": resized_h, "resized_width": resized_w` dans l'item image (méthode B §4.3). |
| Payload Ollama | 985992 | Pré-resize PIL `img.resize((resized_w, resized_h))` avant b64 (rustine §5.3) + même prompt avec résolution annoncée. |
| Parse résultat | 1001 | Remplacer `parse_bbox_to_norm(content, small_w, small_h)` par `parse_bbox_for_family(model_name, content, resized_w, resized_h, orig_w, orig_h)`. |
| Parse retry multi-image | 10271029 | Idem. |
| `_locate_popup_button` | 25362585 | Mêmes 4 modifs (compute resized, payload, parse) sur cette fonction popup. |
### 7.2. Fichiers à supprimer / consolider (DETTE-007)
- `core/grounding/server.py` lignes 1026 : supprimer la définition locale `_smart_resize` et `MIN_PIXELS`/`MAX_PIXELS`, importer depuis `core/grounding/smart_resize.smart_resize_for_family`.
- `core/grounding/infigui_worker.py` lignes 99101 : remplacer le calcul `rH, rW` par un appel à `smart_resize_for_family("InfiX-ai/InfiGUI-G1-3B", H, W)`. **Au passage on corrige le bug de non-clamp MIN/MAX_PIXELS.**
- `core/grounding/bbox_parser.py` : peut être conservé comme parser texte générique, ou intégré dans `parse_bbox_for_family`. Décision Dom.
---
## 8. Protocole de test bbox cible (fixture heartbeat 2560×1600)
Fixture : `data/training/live_sessions/bg_DESKTOP-58D5CAC_windows/shots/heartbeat_1773792436.png` (boîte de dialogue OK/Cancel, bouton OK visuellement centré horizontalement ~ x_pct ≈ 0.48).
### 8.1. Étapes
1. **Préparer un script standalone** (pas de modif resolve_engine pour ce test).
2. Charger l'image avec PIL. `orig_w, orig_h = img.size` → attendu `(2560, 1600)`.
3. Appeler `smart_resize_for_family("Qwen/Qwen3-VL-8B-Instruct", orig_h, orig_w)`. Pour `factor=32`, `max_pixels=16384*32²=16 777 216` : `2560*1600=4 096 000` < max_pixels, donc resize attendu = round au plus près de 32 : `resized_w=2560, resized_h=1600`. (Cas où smart_resize est no-op car déjà conforme.)
4. Appeler `smart_resize_for_family("qwen2.5vl:7b", orig_h, orig_w)` (factor=28). `2560/28=91.43`, `round=91*28=2548`. `1600/28=57.14`, `round=57*28=1596`. Donc `(resized_w, resized_h) = (2548, 1596)`. Vérifier `4 067 808 ≤ max_pixels` : oui.
5. Envoyer la requête vLLM (ou Transformers in-process) avec `resized_width=2548, resized_height=1596` (Qwen2.5-VL) ou `(2560, 1600)` (Qwen3-VL) en méthode B.
6. Récupérer la réponse, par exemple `{"bbox_2d": [1175, 935, 1280, 985]}`.
7. Appeler `parse_bbox_for_family(...)`. Attendu : `x_pct = (1175+1280)/2/2548 ≈ 0.482`, `y_pct = (935+985)/2/1596 ≈ 0.602`.
8. **Critère de validation** : `0.45 ≤ x_pct ≤ 0.55` (bouton OK centré horizontalement ±5%) ET overlay visuel sur le screenshot montrant le marker centré sur le bouton OK.
### 8.2. Critère d'échec (= bug pas corrigé)
Si on observe `x_pct < 0.20` (toujours top-left, comme le bench du 8 mai), c'est que :
- la taille `resized_w/h` passée n'est PAS celle effectivement utilisée par le modèle (vLLM ignore les kwargs), OU
- on divise par la mauvaise dimension (bug de regression), OU
- le modèle ne respecte pas le contrat de pixel post-resize sur cette taille.
Faire passer le test **avant** d'attaquer la migration prod.
### 8.3. Baseline pour comparaison
Le test du 8 mai (cf. `MIGRATION_VLM_PLAN_2026-05-09.md` §2) a observé `bbox_2d=[422,604,462,624]` retourné par `qwen2.5vl:7b` Ollama, donnant `cx_px = 442`. Divisé par `orig_w=2560` : `0.17` (top-left). Divisé par `resized_w=2548` (notre nouveau dénominateur) : `0.174`**toujours faux**, parce que le bug n'est pas qu'on divise par 2560 au lieu de 2548 (écart 0.5%), c'est qu'**Ollama a probablement redimensionné l'image à une taille interne inconnue plus petite que (2548, 1596)**, et le modèle a retourné des coords dans cette résolution interne. CQFD : le bug est non-résolvable côté Ollama.
Sur vLLM avec `resized_width=2548` explicite : prédiction = le bbox passe à `[1150, 920, 1270, 970]` ou approchant, donnant `cx_px=1210`, `x_pct=0.475`. **À valider.**
---
## 9. Sources
### Officielles QwenLM
- [Qwen/Qwen2.5-VL-7B-Instruct discussion #13 — Bounding boxes coordinates](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct/discussions/13) — citations mainteneur sur convention post-resize + Ollama.
- [Qwen3-VL — qwen-vl-utils/src/qwen_vl_utils/vision_process.py](https://github.com/QwenLM/Qwen3-VL/blob/main/qwen-vl-utils/src/qwen_vl_utils/vision_process.py) — source officielle smart_resize, constantes.
- [Qwen3-VL — qwen-vl-utils/README.md](https://github.com/QwenLM/Qwen3-VL/blob/main/qwen-vl-utils/README.md) — `image_patch_size=16` pour Qwen3-VL, `resized_height`/`resized_width` au niveau message.
- [Qwen3-VL Issue #1831 — Image zoom tool uses incorrect resize factor 32 vs 28](https://github.com/QwenLM/Qwen3-VL/issues/1831) — confusion factor 32 Qwen3 vs 28 Qwen2.5.
- [Qwen3-VL Issue #1486 — Bounding Box Coordinate Format and Image Resizing for Qwen3-VL Fine-tuning](https://github.com/QwenLM/Qwen3-VL/issues/1486) — variante 01000 fine-tuning.
- [Qwen3-VL Issue #1616 — bbox scaling LLaMA-Factory](https://github.com/QwenLM/Qwen3-VL/issues/1616) — question ouverte.
- [Qwen3-VL Issue #1640 — question about smart_resize of qwen3vl](https://github.com/QwenLM/Qwen3-VL/issues/1640) — inconsistance cookbooks.
- [Qwen3-VL Issue #2068 — smart_resize transformers vs qwen-vl-utils](https://github.com/QwenLM/Qwen3-VL/issues/2068) — divergence video.
- [Qwen2.5-VL Technical Report (arXiv:2502.13923)](https://arxiv.org/pdf/2502.13923) — patch_size 14, stride 14, factor 28.
### Officielles HuggingFace Transformers
- [HF docs — Qwen3-VL](https://huggingface.co/docs/transformers/main/model_doc/qwen3_vl) — `Qwen3VLVisionConfig.patch_size=16`, `spatial_merge_size=2`.
- [HF docs — Qwen2-VL](https://huggingface.co/docs/transformers/en/model_doc/qwen2_vl) — `Qwen2VLImageProcessor` vs `Qwen2VLImageProcessorFast`.
- [transformers — qwen2_vl/image_processing_qwen2_vl.py](https://github.com/huggingface/transformers/blob/main/src/transformers/models/qwen2_vl/image_processing_qwen2_vl.py) — implémentation smart_resize ref.
- [transformers — qwen2_vl/image_processing_qwen2_vl_fast.py](https://github.com/huggingface/transformers/blob/main/src/transformers/models/qwen2_vl/image_processing_qwen2_vl_fast.py) — processor rapide.
### vLLM
- [vLLM PR #13533 — add mm_processor_kwargs to extra_body for Qwen2.5-VL](https://github.com/vllm-project/vllm/pull/13533) (mergé 2025-02-20).
- [vLLM Issue #15364 — Qwen2.5-VL mm_processor_kwargs not respected](https://github.com/vllm-project/vllm/issues/15364) — bug ouvert.
- [vLLM Issue #13143 — Qwen2-VL max_pixels not a valid argument](https://github.com/vllm-project/vllm/issues/13143).
- [vLLM Recipes — Qwen2.5-VL Usage Guide](https://docs.vllm.ai/projects/recipes/en/latest/Qwen/Qwen2.5-VL.html).
- [vLLM Recipes — Qwen3-VL Usage Guide](https://docs.vllm.ai/projects/recipes/en/latest/Qwen/Qwen3-VL.html).
- [vLLM Issue #20855 — Qwen2VLImageProcessorFast vs slow processor](https://github.com/vllm-project/vllm/issues/20855).
### Ollama
- [Ollama Issue #11217 — image size of qwen2.5-vl](https://github.com/ollama/ollama/issues/11217) — modèle hallucine sa propre résolution.
- [Ollama Issue #10753 — Qwen2.5-VL 32b crashes 720p+](https://github.com/ollama/ollama/issues/10753).
- [Ollama Issue #11297 — qwen2.5vl crashes small image](https://github.com/ollama/ollama/issues/11297).
- [Ollama Issue #9261 — Support for Qwen2.5-VL Model](https://github.com/ollama/ollama/issues/9261).
- [Ollama Issue #13113 — qwen3-vl small image error](https://github.com/ollama/ollama/issues/13113).
- [Ollama Issue #14388 — Qwen2-VL-2B GGUF fails image recognition](https://github.com/ollama/ollama/issues/14388).
- [llama.cpp Issue #13694 — Qwen2.5-VL-7B-Instruct returns extremely inaccurate bbox coordinates](https://github.com/ggml-org/llama.cpp/issues/13694) — confirme que c'est un problème de toute la stack GGUF, pas que d'Ollama.
### InfiGUI / UI-TARS / OS-Atlas
- [InfiX-ai/InfiGUI-G1-3B Hugging Face](https://huggingface.co/InfiX-ai/InfiGUI-G1-3B) — carte modèle, prompt officiel, MAX_PIXELS=5600·28².
- [InfiX-ai/InfiGUI-G1 GitHub](https://github.com/InfiXAI/InfiGUI-G1) — repo officiel AEPO.
- [InfiGUI-G1 paper (arXiv:2508.05731)](https://arxiv.org/html/2508.05731v1) — AEPO, backbone Qwen2.5-VL-3B-Instruct.
- [OS-Copilot/OS-Atlas GitHub](https://github.com/OS-Copilot/OS-Atlas) — README + inférence Base-4B/7B.
- [OS-Atlas paper (arXiv:2410.23218)](https://arxiv.org/html/2410.23218v1) — convention 01000 normalisée.
- [OS-Copilot/OS-Atlas-Base-7B HF](https://huggingface.co/OS-Copilot/OS-Atlas-Base-7B) — backbone Qwen2-VL-7B-Instruct.
- [OS-Copilot/OS-Atlas-Base-4B HF](https://huggingface.co/OS-Copilot/OS-Atlas-Base-4B) — backbone InternVL-2.
### Communauté / blogs
- [Qwen2.5-VL — A hands on code walkthrough (Towards AI)](https://towardsai.net/p/machine-learning/qwen2-5-vl-a-hands-on-code-walkthrough).
- [Qwen2-VL — A hands-on code walkthrough (Medium)](https://medium.com/data-science-collective/qwen2-vl-a-hands-on-code-walkthrough-c5a4e073e9b3).
- [What means "using a slow image processor" — vLLM Forums](https://discuss.vllm.ai/t/what-means-using-a-slow-image-processor/1607).
---
## 10. Annexe — `Qwen2VLImageProcessor` vs `Qwen2VLImageProcessorFast`
| Aspect | `Qwen2VLImageProcessor` (slow) | `Qwen2VLImageProcessorFast` |
|---|---|---|
| Default depuis transformers 4.48 | non | **oui** (`use_fast=True` par défaut) |
| Supporte vidéo | oui (deprecated → v5.0) | **non** (utiliser `Qwen2VLVideoProcessor` séparément) |
| Vitesse | référence | sensiblement plus rapide (torchvision/PIL optims) |
| Reproductibilité bit-exact avec ancien checkpoint | oui | légère différence possible sur cas limites |
**Recommandation** : utiliser `Qwen2VLImageProcessorFast` (default). Sauf besoin de reproductibilité bit-exact avec un ancien checkpoint, auquel cas passer `use_fast=False` au `from_pretrained`. Le projet n'a aucun cas d'usage qui justifie le slow.
---
*Document destiné à clore techniquement DETTE-006/007/010/014. Aucune modification du code n'a été effectuée. Toute migration nécessite une décision explicite de Dom, suivie d'un commit unique par dette.*

View File

@@ -0,0 +1,895 @@
# AXE A3 — Protocole de bench grounding VLM (post smart_resize)
**Date** : 2026-05-23
**Auteur** : Claude (session dispatchée AXE A3)
**Contexte** : `MIGRATION_VLM_PLAN_2026-05-09.md` §2 a relevé un bug d'échelle `bbox_2d` (cx ≈ 0.17 au lieu de ~0.45-0.55) sur 4 configs Ollama. Le module `core/grounding/smart_resize.py` a été commité (`0d7bcd18a`) mais **jamais reverifié** par un bench end-to-end. DETTE-014 indique qu'il est mal calé (factor 28 vs 32). Ce protocole comble le trou méthodologique.
**Statut** : protocole + script prêts. Aucune exécution réelle réalisée par Claude — Dom décide quand lancer.
---
## 1. TL;DR
1. **Une fixture** (`heartbeat_1773792436.png` 2560×1600, dialog OK/Cancel) sur laquelle on connaît la vérité-terrain (bouton OK ≈ mid-screen, `cx ≈ 0.50`).
2. **Trois backends** : Ollama (`qwen2.5vl:7b` = baseline buggy, `qwen3-vl:8b` JSON-explicit), vLLM (`Qwen3-VL-8B-Instruct` avec `resized_width/height` natifs), Transformers in-process (`InfiGUI-G1-3B` actuel + 1-2 SOTA optionnels OS-Atlas/Magma).
3. **Pour chaque modèle** : déchargement VRAM → 1 cold → 10 warm → mesure latence, VRAM pic, format brut, parse OK, `cx_pct` mesuré.
4. **Critère go/no-go** : `cx_pct ∈ [0.40, 0.60]` ET `cy_pct ∈ [0.40, 0.60]` (le bouton OK est au centre du dialog, le dialog est centré écran) ET parse regex prod OK ET latence cold < 12 s.
5. **Livrable** : un CSV `/tmp/bench_grounding_2026-05-23.csv` + overlay PNG par modèle pour validation visuelle.
**Go / no-go pour la migration AXE_A2** : si **aucun** modèle ne passe `cx_pct ∈ [0.40, 0.60]`, le bug n'est PAS uniquement `smart_resize` mais aussi côté preprocessing/prompt → escalader avant de remplacer la prod.
---
## 2. Protocole détaillé
### 2.1. Preprocessing image (par backend)
| Backend | Resize | resized_w/h passé au modèle | Coords retournées en |
|---|---|---|---|
| **Ollama** (qwen2.5vl, qwen3-vl) | Implicite côté serveur, opaque | NON (non supporté) | Pixels post-resize OLLAMA (inconnu) ⚠ |
| **vLLM** (qwen3-vl-8b) | Côté client via `smart_resize` officiel | OUI (via `min_pixels`/`max_pixels` extra body) | Pixels post-resize CLIENT (connu) |
| **Transformers** (InfiGUI, OS-Atlas, Magma) | Côté script via `core/grounding/smart_resize.py` | OUI (passé au `processor`) | Pixels post-resize CLIENT (connu) |
**Côté script** : on calcule `(rH, rW) = smart_resize(H, W)` sur l'image originale en utilisant `core.grounding.smart_resize` (factor 28 par défaut, à challenger après lecture du `probe_qwen3vl_processor.py` qui dump le factor effectif via `patch_size × merge_size`). On envoie l'image **redimensionnée** au backend (sauf Ollama qui re-resize de toute façon).
### 2.2. Prompt (par famille de modèle)
- **Qwen2.5-VL** (baseline) : `Detect 'OK button' in this image with a bounding box.` (prompt actuel `resolve_engine.py:942`)
- **Qwen3-VL Instruct** : prompt JSON explicite obligatoire — `Locate the "OK" button. Return ONLY this JSON: {"bbox_2d":[x1,y1,x2,y2],"label":"OK"}.` (Sans cette directive, sortie liste nue cf. MIGRATION_VLM_PLAN §2)
- **InfiGUI-G1-3B** : prompt du worker existant (`infigui_worker.py:130-135`) — `The screen's resolution is {rW}x{rH}. Locate the UI element(s) for "OK button", output the coordinates using JSON format: [{"point_2d": [x, y]}, ...]` + system avec `<think>`
- **OS-Atlas-Base-7B** (Qwen2-VL-7B-FT) : `In the image, please find the bbox of "OK button". Output format: [[x1,y1,x2,y2]] with each value in [0,1000].`
- **Magma-8B** : pas dans le périmètre v0 (Set-of-Mark requiert SomEngine, hors A3) — documenter comme extension.
### 2.3. Parsing sortie
Réutiliser `core.grounding.bbox_parser.parse_bbox_to_norm(content, divisor_w, divisor_h)`. **CLEF du bench** : appeler avec `divisor_w = rW` et `divisor_h = rH` (post-resize), pas avec `orig_w`/`orig_h`. C'est le fix qu'AXE_A2 documente comme nécessaire.
Pour OS-Atlas (sortie 0-1000), parser à part puis convertir : `cx_pct = (x1 + x2) / 2 / 1000`.
### 2.4. Métriques mesurées (par modèle, en sortie CSV)
| Colonne | Méthode |
|---|---|
| `model` | nom + backend |
| `cold_s` | 1er appel après `unload_all` (Ollama) ou après reload process (Transformers) |
| `warm_avg_s`, `warm_p50_s`, `warm_p95_s` | sur 10 runs warm (même image, même prompt) |
| `vram_pic_mib` | sample `nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits` toutes 200 ms pendant l'appel, max retenu |
| `raw_output` | premier 250 char de la réponse brute |
| `format_detected` | `bbox_2d` / `point_2d` / `xy_json` / `raw_array` / `unknown` |
| `parse_ok` | bool, parser regex prod renvoie un (x, y) |
| `cx_pct`, `cy_pct` | coords normalisées calculées avec `divisor = (rW, rH)` |
| `validated` | bool, `cx_pct ∈ [0.40, 0.60]` ET `cy_pct ∈ [0.40, 0.60]` |
| `error` | string si exception/timeout |
### 2.5. Overlay PNG pour validation visuelle
Pour chaque modèle qui retourne un (cx_pct, cy_pct), générer `bench_grounding_<model>.png` = fixture + croix rouge au point retourné + texte `<model> cx=0.XX cy=0.YY`. Dom regarde les overlays côte-à-côte.
---
## 3. Script Python autonome
À créer en `/home/dom/ai/rpa_vision_v3/tools/bench_grounding_2026-05-23.py`. Code prêt à coller :
```python
#!/usr/bin/env python3
"""Bench grounding VLM — AXE A3 post smart_resize (2026-05-23).
Objectif : refaire le bench bbox_2d du 8 mai 2026 après commit du module
`core/grounding/smart_resize.py` (0d7bcd18a) sur la fixture interne du projet.
Mesure latence cold/warm, VRAM pic, format brut, parse OK, cx_pct/cy_pct
mesuré contre vérité-terrain visuelle (bouton OK ≈ centre écran).
Usage :
.venv/bin/python tools/bench_grounding_2026-05-23.py
[--models qwen25vl_ollama,qwen3vl_ollama,qwen3vl_vllm,infigui_tx]
[--warm 10]
[--out /tmp/bench_grounding_2026-05-23.csv]
[--overlay-dir /tmp/bench_grounding_overlays]
Pré-requis runtime (à confirmer avant lancement, cf. §4) :
- Ollama tourne sur :11434 avec qwen2.5vl:7b et qwen3-vl:8b pull
- vLLM tourne sur :8100 avec Qwen3-VL-8B-Instruct (cf. §4)
- Transformers : .venv contient transformers + bitsandbytes
- nvidia-smi accessible (mesure VRAM)
"""
from __future__ import annotations
import argparse
import base64
import csv
import io
import json
import math
import os
import re
import statistics
import subprocess
import sys
import threading
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Optional
import requests
from PIL import Image, ImageDraw, ImageFont
# Ajout du repo au path pour réutiliser core.grounding.*
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT))
from core.grounding.smart_resize import smart_resize # noqa: E402
from core.grounding.bbox_parser import parse_bbox_to_norm # noqa: E402
# ============================================================================
# Configuration
# ============================================================================
FIXTURE_PATH = REPO_ROOT / "data/training/live_sessions/bg_DESKTOP-58D5CAC_windows/shots/heartbeat_1773792436.png"
TARGET_LABEL = "OK button"
# Vérité-terrain manuelle (cf. §7) : bouton OK approximativement au centre
GT_CX_RANGE = (0.40, 0.60)
GT_CY_RANGE = (0.40, 0.60)
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
VLLM_URL = os.environ.get("VLLM_URL", "http://localhost:8100")
# Une entrée = un modèle à bencher. Le champ "runner" identifie l'appel à faire.
MODEL_CATALOG: dict[str, dict[str, Any]] = {
"qwen25vl_ollama": {
"label": "qwen2.5vl:7b (Ollama, baseline buggy)",
"runner": "ollama",
"ollama_model": "qwen2.5vl:7b",
"prompt_style": "qwen25_bbox",
"num_predict": 100,
"think": None, # n/a
},
"qwen3vl_ollama": {
"label": "qwen3-vl:8b (Ollama, prompt JSON explicite)",
"runner": "ollama",
"ollama_model": "qwen3-vl:8b",
"prompt_style": "qwen3_bbox_explicit",
"num_predict": 128,
"think": False,
},
"qwen3vl_vllm": {
"label": "Qwen3-VL-8B-Instruct (vLLM, resized_w/h natif)",
"runner": "vllm",
"vllm_model": "Qwen/Qwen3-VL-8B-Instruct",
"prompt_style": "qwen3_bbox_explicit",
"max_tokens": 128,
},
"infigui_tx": {
"label": "InfiGUI-G1-3B (Transformers, prod)",
"runner": "transformers_infigui",
"model_id": "InfiX-ai/InfiGUI-G1-3B",
"prompt_style": "infigui_point",
},
# Extensions facultatives (AXE A1) — à activer via --models
"os_atlas_tx": {
"label": "OS-Atlas-Base-7B (Transformers, SOTA grounding)",
"runner": "transformers_qwen2vl",
"model_id": "OS-Copilot/OS-Atlas-Base-7B",
"prompt_style": "os_atlas_bbox_1000",
},
}
# ============================================================================
# Prompts par style
# ============================================================================
def build_prompt(style: str, rW: int, rH: int, label: str) -> tuple[str, str]:
"""Retourne (system, user). System "" si pas utile."""
if style == "qwen25_bbox":
return (
"You locate UI elements on screenshots. Return coordinates.",
f"Detect '{label}' in this image with a bounding box.",
)
if style == "qwen3_bbox_explicit":
return (
"You are a UI element locator. Output raw JSON only. No explanation.",
f'Locate the "{label}" in this {rW}x{rH} screenshot. '
f'Return ONLY this JSON object: '
f'{{"bbox_2d":[x1,y1,x2,y2],"label":"{label}"}}',
)
if style == "infigui_point":
return (
"You FIRST think about the reasoning process as an internal monologue "
"and then provide the final answer.\n"
"The reasoning process MUST BE enclosed within <think> </think> tags.",
f'The screen\'s resolution is {rW}x{rH}.\n'
f'Locate the UI element(s) for "{label}", '
f'output the coordinates using JSON format: '
f'[{{"point_2d": [x, y]}}, ...]',
)
if style == "os_atlas_bbox_1000":
return (
"",
f'In the image, please find the bbox of "{label}". '
f'Output the bounding boxes in this format: [[x1,y1,x2,y2]], '
f'where each value is normalized in [0, 1000].',
)
raise ValueError(f"prompt_style inconnu : {style}")
# ============================================================================
# VRAM monitoring (nvidia-smi sampling)
# ============================================================================
class VRAMSampler:
"""Sample nvidia-smi en thread, expose pic en MiB."""
def __init__(self, interval_s: float = 0.2):
self.interval = interval_s
self.peak_mib = 0
self._stop = threading.Event()
self._thread: Optional[threading.Thread] = None
def _loop(self) -> None:
while not self._stop.is_set():
try:
out = subprocess.check_output(
["nvidia-smi", "--query-gpu=memory.used",
"--format=csv,noheader,nounits", "-i", "0"],
timeout=2,
).decode().strip()
mib = int(out.splitlines()[0])
self.peak_mib = max(self.peak_mib, mib)
except Exception:
pass
self._stop.wait(self.interval)
def start(self) -> None:
self.peak_mib = 0
self._stop.clear()
self._thread = threading.Thread(target=self._loop, daemon=True)
self._thread.start()
def stop(self) -> int:
self._stop.set()
if self._thread:
self._thread.join(timeout=2)
return self.peak_mib
# ============================================================================
# Runners par backend
# ============================================================================
@dataclass
class CallResult:
elapsed_s: float
raw: str = ""
error: str = ""
vram_pic_mib: int = 0
def _encode_image(img: Image.Image) -> str:
buf = io.BytesIO()
img.save(buf, format="JPEG", quality=85)
return base64.b64encode(buf.getvalue()).decode("ascii")
def _ollama_unload_all() -> None:
try:
ps = requests.get(f"{OLLAMA_URL}/api/ps", timeout=5).json()
for m in ps.get("models", []):
requests.post(
f"{OLLAMA_URL}/api/generate",
json={"model": m["name"], "prompt": "", "keep_alive": 0, "stream": False},
timeout=10,
)
except Exception:
pass
time.sleep(2)
def run_ollama(cfg: dict, img: Image.Image, rW: int, rH: int) -> CallResult:
system, user = build_prompt(cfg["prompt_style"], rW, rH, TARGET_LABEL)
img_resized = img.resize((rW, rH))
img_b64 = _encode_image(img_resized)
messages = []
if system:
messages.append({"role": "system", "content": system})
messages.append({"role": "user", "content": user, "images": [img_b64]})
options: dict[str, Any] = {
"temperature": 0.1,
"num_predict": cfg.get("num_predict", 128),
}
payload = {
"model": cfg["ollama_model"],
"messages": messages,
"stream": False,
"options": options,
}
if cfg.get("think") is False:
payload["think"] = False
sampler = VRAMSampler()
sampler.start()
t0 = time.perf_counter()
try:
resp = requests.post(f"{OLLAMA_URL}/api/chat", json=payload, timeout=120)
elapsed = time.perf_counter() - t0
vram = sampler.stop()
if resp.status_code != 200:
return CallResult(elapsed, error=f"HTTP_{resp.status_code}", vram_pic_mib=vram)
content = resp.json().get("message", {}).get("content", "")
return CallResult(elapsed, raw=content, vram_pic_mib=vram)
except Exception as e:
sampler.stop()
return CallResult(time.perf_counter() - t0, error=f"NET:{type(e).__name__}")
def run_vllm(cfg: dict, img: Image.Image, rW: int, rH: int) -> CallResult:
system, user = build_prompt(cfg["prompt_style"], rW, rH, TARGET_LABEL)
img_resized = img.resize((rW, rH))
img_b64 = _encode_image(img_resized)
messages = []
if system:
messages.append({"role": "system", "content": system})
messages.append({
"role": "user",
"content": [
{"type": "text", "text": user},
{"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{img_b64}"}},
],
})
payload = {
"model": cfg["vllm_model"],
"messages": messages,
"temperature": 0.1,
"max_tokens": cfg.get("max_tokens", 128),
# vLLM Qwen-VL extension : passer min/max pixels en kwargs
# (cf. github QwenLM/Qwen3-VL issue #1434 — peut être ignoré selon version vllm)
"mm_processor_kwargs": {"min_pixels": rW * rH, "max_pixels": rW * rH},
}
sampler = VRAMSampler()
sampler.start()
t0 = time.perf_counter()
try:
resp = requests.post(
f"{VLLM_URL}/v1/chat/completions", json=payload, timeout=120,
)
elapsed = time.perf_counter() - t0
vram = sampler.stop()
if resp.status_code != 200:
return CallResult(elapsed, error=f"HTTP_{resp.status_code}",
raw=resp.text[:250], vram_pic_mib=vram)
content = (resp.json().get("choices", [{}])[0]
.get("message", {}).get("content", ""))
return CallResult(elapsed, raw=content, vram_pic_mib=vram)
except Exception as e:
sampler.stop()
return CallResult(time.perf_counter() - t0, error=f"NET:{type(e).__name__}")
# Pour les runners Transformers, on délègue au worker existant (subprocess)
# pour ne pas surcharger ce script en deps lourdes. Variante one-shot.
def run_transformers_infigui(cfg: dict, img: Image.Image, rW: int, rH: int) -> CallResult:
"""Appel one-shot via core/grounding/infigui_worker.py.
Note : InfiGUI utilise un système Unix-socket persistant en prod
(`core/grounding/infigui_server.py`). Pour le bench cold, on lance
le worker en subprocess one-shot (charge le modèle à chaque cold).
Pour les warms, on bench via le socket si dispo, sinon en subprocess
(et la mesure cold/warm sera identique — à documenter dans le CSV).
"""
req = {
"image_path": str(FIXTURE_PATH),
"target": TARGET_LABEL,
"description": "",
}
sampler = VRAMSampler()
sampler.start()
t0 = time.perf_counter()
try:
proc = subprocess.run(
[sys.executable, "-m", "core.grounding.infigui_worker"],
input=json.dumps(req),
capture_output=True, text=True,
timeout=180, cwd=str(REPO_ROOT),
)
elapsed = time.perf_counter() - t0
vram = sampler.stop()
if proc.returncode != 0:
return CallResult(elapsed, error=f"PROC_{proc.returncode}",
raw=(proc.stderr or "")[:250], vram_pic_mib=vram)
# Le worker écrit sur stdout du JSON final
out = proc.stdout.strip().splitlines()[-1] if proc.stdout.strip() else "{}"
return CallResult(elapsed, raw=out, vram_pic_mib=vram)
except Exception as e:
sampler.stop()
return CallResult(time.perf_counter() - t0, error=f"EXC:{type(e).__name__}")
def run_transformers_qwen2vl(cfg: dict, img: Image.Image, rW: int, rH: int) -> CallResult:
"""Stub pour OS-Atlas / autres Qwen2-VL fine-tunés.
À étoffer si Dom décide d'inclure OS-Atlas dans la 1ère salve. Procédure :
charger le modèle via Qwen2_5_VLForConditionalGeneration + AutoProcessor,
smart_resize côté script, prompt `os_atlas_bbox_1000`, parser 0-1000.
Implémentation effective hors périmètre v0 — retourne un CallResult vide.
"""
return CallResult(0.0, error="NOT_IMPLEMENTED_V0")
RUNNERS = {
"ollama": run_ollama,
"vllm": run_vllm,
"transformers_infigui": run_transformers_infigui,
"transformers_qwen2vl": run_transformers_qwen2vl,
}
# ============================================================================
# Parsing & validation
# ============================================================================
def detect_format(content: str) -> str:
if '"bbox_2d"' in content:
return "bbox_2d"
if '"point_2d"' in content:
return "point_2d"
if '"x_pct"' in content and '"y_pct"' in content:
return "xy_pct"
if re.search(r'"x"\s*:\s*[\d.]+.*?"y"\s*:\s*[\d.]+', content, re.S):
return "xy_json"
if re.search(r'\[\s*\[\s*\d+\s*,', content):
return "raw_2d_array" # OS-Atlas 0-1000 style
if re.search(r'\[\s*[\d.]+\s*,', content):
return "raw_array"
return "unknown"
def parse_coords(content: str, rW: int, rH: int, prompt_style: str
) -> tuple[Optional[float], Optional[float]]:
"""Retourne (cx_pct, cy_pct) normalisés ∈ [0, 1]."""
# OS-Atlas-style : 0-1000
if prompt_style == "os_atlas_bbox_1000":
m = re.search(r'\[\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\]', content)
if m:
x1, y1, x2, y2 = [int(v) for v in m.groups()]
return (x1 + x2) / 2 / 1000.0, (y1 + y2) / 2 / 1000.0
return None, None
# InfiGUI point_2d (parser dédié, sortie en pixels post-resize rW/rH)
if prompt_style == "infigui_point":
m = re.search(r'"point_2d"\s*:\s*\[(\d+)\s*,\s*(\d+)\]', content)
if m:
x, y = int(m.group(1)), int(m.group(2))
return x / rW, y / rH
# Le worker InfiGUI renvoie directement {"x": .., "y": ..} en pixels
# source résolution. On essaie aussi ce format.
m2 = re.search(r'"x"\s*:\s*(\d+).*?"y"\s*:\s*(\d+)', content, re.S)
if m2:
x, y = int(m2.group(1)), int(m2.group(2))
# ici x/y sont en pixels image source — diviser par fixture size
return x / 2560.0, y / 1600.0
return None, None
# Cas général : parser regex prod avec divisor post-resize
return parse_bbox_to_norm(content, rW, rH)
def is_validated(cx: Optional[float], cy: Optional[float]) -> bool:
if cx is None or cy is None:
return False
return (GT_CX_RANGE[0] <= cx <= GT_CX_RANGE[1]
and GT_CY_RANGE[0] <= cy <= GT_CY_RANGE[1])
# ============================================================================
# Overlay PNG pour validation visuelle
# ============================================================================
def draw_overlay(img: Image.Image, cx: float, cy: float, label: str,
out_path: Path) -> None:
overlay = img.copy().convert("RGB")
draw = ImageDraw.Draw(overlay)
W, H = overlay.size
px, py = int(cx * W), int(cy * H)
r = 30
draw.ellipse([px - r, py - r, px + r, py + r], outline="red", width=5)
draw.line([px - r * 2, py, px + r * 2, py], fill="red", width=3)
draw.line([px, py - r * 2, px, py + r * 2], fill="red", width=3)
try:
font = ImageFont.truetype(
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 28)
except OSError:
font = ImageFont.load_default()
txt = f"{label}\ncx={cx:.3f} cy={cy:.3f}"
draw.rectangle([10, 10, 700, 90], fill="white")
draw.text((20, 15), txt, fill="black", font=font)
overlay.save(out_path)
# ============================================================================
# Bench main
# ============================================================================
@dataclass
class ModelStats:
key: str
label: str
cold_s: float = 0.0
warm_times: list[float] = field(default_factory=list)
vram_peaks: list[int] = field(default_factory=list)
raw_first: str = ""
format_detected: str = "unknown"
parse_ok: bool = False
cx: Optional[float] = None
cy: Optional[float] = None
validated: bool = False
error: str = ""
def bench_one_model(key: str, cfg: dict, img: Image.Image, rW: int, rH: int,
warm_runs: int, overlay_dir: Optional[Path]) -> ModelStats:
stats = ModelStats(key=key, label=cfg["label"])
runner = RUNNERS[cfg["runner"]]
print(f"\n══════ {cfg['label']} ══════")
# Déchargement VRAM avant cold (Ollama seulement — Transformers/vLLM
# gardent le modèle, c'est attendu)
if cfg["runner"] == "ollama":
_ollama_unload_all()
# Cold
print(f" [cold]", end=" ", flush=True)
r0 = runner(cfg, img, rW, rH)
stats.cold_s = r0.elapsed_s
if r0.error:
stats.error = r0.error
print(f"{r0.error}")
return stats
stats.raw_first = r0.raw[:250]
stats.format_detected = detect_format(r0.raw)
cx, cy = parse_coords(r0.raw, rW, rH, cfg["prompt_style"])
stats.parse_ok = (cx is not None and cy is not None)
stats.cx, stats.cy = cx, cy
stats.validated = is_validated(cx, cy)
stats.vram_peaks.append(r0.vram_pic_mib)
print(f"{stats.cold_s:.2f}s | fmt={stats.format_detected} | "
f"cx={cx} cy={cy} | val={stats.validated}")
# Warm
print(f" [warm × {warm_runs}]", end=" ", flush=True)
for i in range(warm_runs):
rN = runner(cfg, img, rW, rH)
if rN.error:
print(f" run{i}=❌{rN.error}", end="")
continue
stats.warm_times.append(rN.elapsed_s)
stats.vram_peaks.append(rN.vram_pic_mib)
print()
if stats.warm_times:
print(f" warm avg={statistics.mean(stats.warm_times):.2f}s | "
f"p95={sorted(stats.warm_times)[int(len(stats.warm_times)*0.95)-1]:.2f}s")
# Overlay (si parse OK)
if overlay_dir and cx is not None and cy is not None:
overlay_dir.mkdir(parents=True, exist_ok=True)
draw_overlay(img, cx, cy, cfg["label"],
overlay_dir / f"bench_{key}.png")
return stats
def write_csv(all_stats: list[ModelStats], out_path: Path) -> None:
with out_path.open("w", newline="") as f:
w = csv.writer(f)
w.writerow([
"key", "label", "cold_s",
"warm_avg_s", "warm_p50_s", "warm_p95_s",
"vram_pic_mib",
"format", "parse_ok", "cx_pct", "cy_pct", "validated",
"raw_first", "error",
])
for s in all_stats:
warm_avg = statistics.mean(s.warm_times) if s.warm_times else 0.0
warm_p50 = statistics.median(s.warm_times) if s.warm_times else 0.0
warm_p95 = (sorted(s.warm_times)[int(len(s.warm_times)*0.95)-1]
if len(s.warm_times) > 1 else (s.warm_times[0]
if s.warm_times else 0.0))
vram = max(s.vram_peaks) if s.vram_peaks else 0
w.writerow([
s.key, s.label, f"{s.cold_s:.2f}",
f"{warm_avg:.2f}", f"{warm_p50:.2f}", f"{warm_p95:.2f}",
vram,
s.format_detected, s.parse_ok,
f"{s.cx:.4f}" if s.cx is not None else "",
f"{s.cy:.4f}" if s.cy is not None else "",
s.validated, s.raw_first.replace("\n", " "), s.error,
])
def print_summary(all_stats: list[ModelStats]) -> None:
print("\n\n══════════════════ SYNTHÈSE ══════════════════")
print("| Modèle | Cold (s) | Warm avg | VRAM MiB | Fmt | cx | cy | VAL |")
print("|---|---:|---:|---:|---|---:|---:|:---:|")
for s in all_stats:
warm_avg = statistics.mean(s.warm_times) if s.warm_times else 0.0
vram = max(s.vram_peaks) if s.vram_peaks else 0
cx_s = f"{s.cx:.3f}" if s.cx is not None else ""
cy_s = f"{s.cy:.3f}" if s.cy is not None else ""
mark = "" if s.validated else ("" if s.parse_ok else "")
print(f"| {s.label[:40]} | {s.cold_s:.1f} | {warm_avg:.1f} | "
f"{vram} | {s.format_detected} | {cx_s} | {cy_s} | {mark} |")
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--models", default="qwen25vl_ollama,qwen3vl_ollama,qwen3vl_vllm,infigui_tx",
help="liste séparée par virgules (clés dans MODEL_CATALOG)")
ap.add_argument("--warm", type=int, default=10)
ap.add_argument("--out", default="/tmp/bench_grounding_2026-05-23.csv")
ap.add_argument("--overlay-dir", default="/tmp/bench_grounding_overlays")
args = ap.parse_args()
if not FIXTURE_PATH.exists():
print(f"ERROR: fixture absente — {FIXTURE_PATH}")
return 2
img = Image.open(FIXTURE_PATH).convert("RGB")
W, H = img.size
rH, rW = smart_resize(H, W)
print(f"Fixture : {FIXTURE_PATH}")
print(f" source : {W}×{H}")
print(f" smart_resize() → {rW}×{rH} (factor=28)")
print(f" vérité-terrain : cx ∈ {GT_CX_RANGE}, cy ∈ {GT_CY_RANGE}")
print(f" cible : '{TARGET_LABEL}'")
keys = [k.strip() for k in args.models.split(",") if k.strip()]
unknown = [k for k in keys if k not in MODEL_CATALOG]
if unknown:
print(f"ERROR: modèles inconnus : {unknown}")
print(f"Catalog : {list(MODEL_CATALOG)}")
return 2
overlay_dir = Path(args.overlay_dir) if args.overlay_dir else None
all_stats: list[ModelStats] = []
for k in keys:
try:
stats = bench_one_model(
k, MODEL_CATALOG[k], img, rW, rH, args.warm, overlay_dir)
all_stats.append(stats)
except KeyboardInterrupt:
print(f"\n⚠ Interrompu pendant {k}")
break
except Exception as e:
print(f"\n❌ Crash {k}: {e}")
all_stats.append(ModelStats(key=k, label=MODEL_CATALOG[k]["label"],
error=f"crash:{e}"))
out = Path(args.out)
write_csv(all_stats, out)
print(f"\nCSV écrit : {out}")
if overlay_dir:
print(f"Overlays : {overlay_dir}/")
print_summary(all_stats)
return 0
if __name__ == "__main__":
sys.exit(main())
```
**Vérification syntaxique** : imports cohérents avec `core/grounding/smart_resize.py` (export `smart_resize`) et `core/grounding/bbox_parser.py` (export `parse_bbox_to_norm`). Pas de dépendance externe hors `requests`, `Pillow` (déjà dans `.venv`). `subprocess` pour Transformers worker + nvidia-smi. **À tester par Dom** avec `--models qwen25vl_ollama` seul d'abord pour valider l'I/O.
---
## 4. Procédure d'install pour Dom
### 4.1. Prérequis Ollama
```bash
# Vérifier que les 2 modèles sont pull
ollama list | grep -E "qwen2.5vl|qwen3-vl"
# Si manquants :
ollama pull qwen2.5vl:7b # ~8 GB
ollama pull qwen3-vl:8b # ~6 GB
```
### 4.2. Prérequis vLLM (option recommandée)
`vLLM` est listé dans `MIGRATION_VLM_PLAN_2026-05-09.md` §3 comme cible. Démarrage suggéré dans un terminal séparé (ou un service systemd dédié, voir `tools/start_grounding_server.sh` pour le pattern existant) :
```bash
# Dans venv_v3 (créer pip install vllm si manquant)
cd ~/ai/rpa_vision_v3 && source venv_v3/bin/activate
pip install --upgrade vllm # >= 0.6.5 pour Qwen3-VL
python -m vllm.entrypoints.openai.api_server \
--model Qwen/Qwen3-VL-8B-Instruct \
--host 0.0.0.0 --port 8100 \
--gpu-memory-utilization 0.55 \
--max-model-len 8192 \
--limit-mm-per-prompt image=1 \
--mm-processor-kwargs '{"min_pixels": 100352, "max_pixels": 1003520}'
```
**Note** : le hardware n'a que 12 GB VRAM. Si Ollama tourne en parallèle avec un autre modèle chargé, prévoir `unload_all` Ollama avant lancement vLLM (`for m in $(ollama ps | awk 'NR>1 {print $1}'); do curl -X POST localhost:11434/api/generate -d "{\"model\":\"$m\",\"keep_alive\":0}"; done`).
### 4.3. Modèles optionnels (HuggingFace)
```bash
# OS-Atlas (si on l'active dans --models)
huggingface-cli download OS-Copilot/OS-Atlas-Base-7B
# Magma (extension future)
huggingface-cli download microsoft/Magma-8B
```
Les modèles HF se cachent automatiquement dans `~/.cache/huggingface/hub/`. Comptez ~15 GB par modèle 7-8B en bf16, ~4 GB en 4-bit NF4 (notre stack quantize à la volée pour InfiGUI).
### 4.4. Lancement bench
```bash
cd ~/ai/rpa_vision_v3 && source venv_v3/bin/activate
# Salve baseline (sans vLLM, sans OS-Atlas)
.venv/bin/python tools/bench_grounding_2026-05-23.py \
--models qwen25vl_ollama,qwen3vl_ollama,infigui_tx \
--warm 10
# Salve complète (vLLM démarré préalablement)
.venv/bin/python tools/bench_grounding_2026-05-23.py \
--models qwen25vl_ollama,qwen3vl_ollama,qwen3vl_vllm,infigui_tx \
--warm 10
# Avec OS-Atlas (nécessite implémentation effective de run_transformers_qwen2vl)
.venv/bin/python tools/bench_grounding_2026-05-23.py \
--models qwen25vl_ollama,qwen3vl_vllm,infigui_tx,os_atlas_tx \
--warm 10
```
Durée estimée pour la salve baseline : 1 cold + 10 warm × 3 modèles. Si cold ≈ 11s et warm ≈ 2s : ~2 min/modèle, total ~7-10 min.
---
## 5. Modèles candidats avec configs précises
### 5.1. `qwen2.5vl:7b` (Ollama, baseline buggy attendue)
- Backend : Ollama HTTP `/api/chat`
- Prompt : `Detect 'OK button' in this image with a bounding box.` (prompt actuel `resolve_engine.py:942`)
- Options : `temperature=0.1`, `num_predict=100`
- **Attendu** : bbox_2d en pixels post-resize Ollama (opaque) → `cx_pct ≈ 0.17` (bug confirmé 8 mai)
- Rôle dans le bench : **témoin du bug**, doit échouer pour confirmer que le bug est reproductible et que le bench discrimine.
### 5.2. `qwen3-vl:8b` (Ollama, prompt JSON explicite)
- Backend : Ollama HTTP `/api/chat`
- Prompt système : `You are a UI element locator. Output raw JSON only. No explanation.`
- Prompt user : `Locate the "OK button" in this {rW}x{rH} screenshot. Return ONLY this JSON object: {"bbox_2d":[x1,y1,x2,y2],"label":"OK button"}`
- Options : `temperature=0.1`, `num_predict=128`, `think:false`
- ⚠ Note web search : Ollama issue #14798`think:false` est **silencieusement ignoré** pour qwen3-vl:8b (template bare). Vérifier sur les outputs si `<think>...</think>` apparaît. Workaround : préfixer le user prompt par `/no_think`.
- **Attendu** : si `smart_resize` côté script correspond au resize interne Ollama, `cx ≈ 0.50`. Sinon, même bug que qwen2.5vl.
### 5.3. `Qwen3-VL-8B-Instruct` (vLLM, cible migration)
- Backend : vLLM OpenAI-compat sur :8100
- Image envoyée **déjà resize côté client** au `smart_resize`-output → vLLM ne re-resize plus (à confirmer via `mm_processor_kwargs={"min_pixels": rW*rH, "max_pixels": rW*rH}` pour forcer no-op).
- Prompt : idem 5.2
- `max_tokens=128`, `temperature=0.1`
- **Attendu** : `cx_pct ∈ [0.40, 0.60]` si la chaîne resize+prompt+parse est cohérente. C'est la config qui valide AXE_A2.
### 5.4. `InfiGUI-G1-3B` (Transformers, prod actuelle)
- Backend : `core/grounding/infigui_worker.py` en subprocess (one-shot) OU socket Unix via `rpa-grounding.service` si actif.
- Prompt : du worker, sortie `point_2d` (pas `bbox_2d`).
- **Spécificité** : le worker écrit (x, y) en pixels **source** déjà (re-multiplie par `W/rW, H/rH` ligne 174-175). Donc `cx_pct = x_returned / 2560`, `cy_pct = y_returned / 1600` direct. Vérifier au runtime.
- **Attendu** : `cx_pct ≈ 0.50` (le worker est testé en prod, c'est la baseline qui marche déjà selon `HISTORIQUE_VLM_IMPLEMENTATIONS_2026-05-08.md`).
### 5.5. `OS-Atlas-Base-7B` (Transformers, candidate SOTA, AXE A1)
- Backend : Transformers Qwen2.5-VL chargé en 4-bit NF4 (à implémenter, runner `transformers_qwen2vl` est un stub v0).
- Prompt : `In the image, please find the bbox of "OK button". Output the bounding boxes in this format: [[x1,y1,x2,y2]], where each value is normalized in [0, 1000].`
- Output 0-1000 normalisé → conversion directe `cx_pct = (x1 + x2) / 2 / 1000`.
- **Attendu** : SOTA sur ScreenSpot/ScreenSpot-Pro selon HF README → `cx_pct ∈ [0.40, 0.60]` probable.
### 5.6. `Magma-8B` (extension future, non v0)
Magma utilise **Set-of-Mark** : il faut détecter d'abord les éléments (SomEngine ou OmniParser) puis lui demander de choisir un numéro. Pas directement comparable aux 5 candidats ci-dessus. À benchmarker dans un AXE A4 séparé.
---
## 6. Critère de validation success (matrice)
| Modèle | Latence cold | Warm avg | cx_pct ∈ [0.40, 0.60] | cy_pct ∈ [0.40, 0.60] | Parse regex prod | Verdict |
|---|---|---|---|---|---|---|
| qwen25vl_ollama | < 12 s | < 12 s | ❌ (attendu 0.17) | ? | ✅ | **Témoin OK si tout sauf cx pass** |
| qwen3vl_ollama | < 5 s | < 3 s | ✅ ou ❌ selon resize | ✅ | ✅ si prompt JSON | go si ✅ |
| qwen3vl_vllm | < 8 s | < 3 s | ✅ requis | ✅ requis | ✅ requis | **CIBLE migration AXE_A2** |
| infigui_tx | < 15 s | < 4 s | ✅ requis | ✅ requis | ✅ (point_2d) | Baseline prod |
| os_atlas_tx | < 15 s | < 5 s | ✅ requis | ✅ requis | ✅ (raw_2d 0-1000) | Candidat upgrade |
**Verdict global AXE_A3** :
- Si `qwen3vl_vllm` ET `infigui_tx` passent ✅ → migration vers vLLM Qwen3-VL est **safe**, AXE_A2 peut être considéré comme résolu.
- Si **seul `infigui_tx` passe** → la prod actuelle est valide, vLLM Qwen3-VL n'apporte rien à part la latence — décision business sur la migration.
- Si **rien ne passe** → le bug n'est PAS dans `smart_resize` seul. Investiguer `divisor_w/h` côté `parse_bbox_to_norm`, et la chaîne window crop (`window_rect` line 916-919) qui pourrait introduire un offset.
---
## 7. Vérité terrain manuelle pour Dom
Avant tout bench, Dom doit confirmer visuellement où est le bouton OK sur la fixture. Trois méthodes au choix :
### Méthode 1 : aperçu rapide via xdg-open
```bash
xdg-open /home/dom/ai/rpa_vision_v3/data/training/live_sessions/bg_DESKTOP-58D5CAC_windows/shots/heartbeat_1773792436.png
```
Ouvrir l'image, identifier le bouton OK à l'œil, estimer `cx_pct ≈ pixel_x / 2560` et `cy_pct ≈ pixel_y / 1600`.
### Méthode 2 : overlay grille via PIL (one-liner)
```bash
.venv/bin/python -c "
from PIL import Image, ImageDraw
img = Image.open('data/training/live_sessions/bg_DESKTOP-58D5CAC_windows/shots/heartbeat_1773792436.png').convert('RGB')
draw = ImageDraw.Draw(img)
W, H = img.size
# Grille de référence en %
for pct in [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]:
x = int(pct * W)
draw.line([(x, 0), (x, H)], fill='cyan', width=2)
draw.text((x + 5, 10), f'{pct:.1f}', fill='cyan')
y = int(pct * H)
draw.line([(0, y), (W, y)], fill='cyan', width=2)
draw.text((10, y + 5), f'{pct:.1f}', fill='cyan')
img.save('/tmp/heartbeat_grid.png')
print('grille → /tmp/heartbeat_grid.png')
"
xdg-open /tmp/heartbeat_grid.png
```
Dom regarde où est le bouton OK et note `(cx_gt, cy_gt)`. Si OK n'est pas dans `[0.40, 0.60]² ` après cette vérif, **ajuster `GT_CX_RANGE` / `GT_CY_RANGE` dans le script** avant tout bench.
### Méthode 3 : crop autour de la zone OK
```bash
.venv/bin/python -c "
from PIL import Image
img = Image.open('data/training/live_sessions/bg_DESKTOP-58D5CAC_windows/shots/heartbeat_1773792436.png')
# Crop la zone supposée 0.4-0.6 × 0.4-0.6
W, H = img.size
img.crop((int(0.35*W), int(0.35*H), int(0.65*W), int(0.65*H))).save('/tmp/heartbeat_center_crop.png')
"
xdg-open /tmp/heartbeat_center_crop.png
```
Si le bouton OK est visible dans ce crop, vérité-terrain `[0.40, 0.60]` est valide.
---
## 8. Extensions futures (autres fixtures à ajouter)
Une seule fixture = sous-dimensionné pour conclure. Au minimum 3 fixtures additionnelles, idéalement issues du replay réel post-démo :
| Fixture | Origine | Cible | cx_gt approx | Difficulté |
|---|---|---|---|---|
| **Tabs Easily Assure** | replay 8 mai bug step 10 | "Imagerie" / "Notes médicales" / "Synthèse Urgences" (3 cas) | par tab | discriminer 3 textes dans une barre — confondus en center-of-line (REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md §4.2) |
| **Popup IPP recherche** | recording VWB urgence_aiva_demo | bouton "Rechercher" | mid-screen | popup centré, peut avoir plusieurs occurrences du même texte (cf. `original_position` y_relative pour désambiguer) |
| **Bandeau outils Easily** | shot quelconque | icône "disquette" (sans texte) | top-left de la barre | grounding visuel pur (pas d'OCR), c'est le cas pour lequel le grounding VLM est censé exister |
Ces extensions justifieraient un **AXE A3.2 — Multi-fixture grounding bench** une fois A3.1 (cette spec) validé. Le script ci-dessus est déjà conçu pour accepter une liste de fixtures (refacto trivial : `FIXTURE_PATH` + `TARGET_LABEL` + `GT_*_RANGE` → liste de tuples, boucle externe).
---
## 9. Dépendances et liens avec autres AXES
- **AXE A1 (sélection modèles)** : alimente la section §5 — si A1 retient OS-Atlas-Pro-7B ou ShowUI-2B en lieu et place de OS-Atlas-Base-7B, ajuster `MODEL_CATALOG`.
- **AXE A2 (smart_resize calibration)** : ce bench est la **validation end-to-end** d'A2. Tant qu'A2 n'a pas tranché entre factor=28 (Qwen2-VL) et factor=32 (Qwen3-VL), lancer A3 d'abord avec `factor=28` (état actuel `core/grounding/smart_resize.py`) puis re-bench avec `factor=32` (modifier `FACTOR_DEFAULT` du module ou patcher le script).
- **Hors scope A3** : SomEngine + Magma (Set-of-Mark) ; ces 2 stratégies de grounding nécessitent un détecteur amont (cf. `_resolve_by_som` dans `resolve_engine.py:1095+`) et un bench distinct.
---
*Document destiné à être consommé par Dom et un agent d'exécution. Aucune action runtime déclenchée par cette spec. À mettre à jour quand A1 et A2 auront tranché leurs paramètres.*

View File

@@ -0,0 +1,487 @@
# AXE A4 — OCR, Template matching, pHash : revue 2026 + correctif `_resolve_by_ocr_text`
**Date :** 2026-05-23
**Auteur :** Claude (dispatch recherche)
**Périmètre :** revue littérature/écosystème 2025-2026 pour la cascade UI `OCR → template → VLM` + alternatives à `pHash` pour LoopDetector et VERIFY. Patch ciblé du bug *center-of-line* de `_resolve_by_ocr_text` (`agent_v0/server_v1/resolve_engine.py:1447-1527`).
**Lecture pré-requise :** `docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` §2, §4 ; `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` §1.2 et §5 ; `docs/BUG_PRECHECK_SPATIAL_BLINDNESS_2026-05-08.md` (DETTE-001).
**Statut :** recherche + propositions. **Aucune modification de code.** Toute application validée par Dom.
---
## 0. TL;DR
1. **Le bug primaire `center-of-line` est résolvable sans changer d'OCR.** docTR expose les `geometry` au niveau du `Word`, normalisées dans le **même repère que la ligne**. Le quick fix §5 du diagnostic 8 mai (cf. §5 ci-dessous, code copy-paste-ready) supprime la collision Imagerie/Notes/Synthèse en restant 100 % iso-stack.
2. **OCR : garder docTR comme moteur OCR-DIRECT** (mode strict + cascade) car c'est le seul, avec Tesseract et PaddleOCR `return_word_box=True`, à exposer des bbox **token-level dans le même repère que la ligne**. EasyOCR retourne par défaut des bbox merges niveau line/segment et **n'est pas adapté** à la résolution multi-tokens d'un onglet sur barre. Surya OCR = line-level uniquement, à écarter pour ce besoin. RapidOCR (PaddleOCR ONNX repackagé) → candidat 2026 pour OCR-DIRECT *léger sans dépendance Paddle*, à valider sur français accentué.
3. **Template matching : remplacer `cv2.matchTemplate` multi-scale par SuperPoint+LightGlue (ONNX, ~50 ms par paire sur RTX 5070).** C'est la sortie propre pour la drift exemption `≥ 0.95` actuelle, qui est un faux positif déguisé (score haut sur région différente). LightGlue est invariant à l'offset/scale/rotation et fournit un *score de cohérence géométrique* — donc plus de faux positifs « 0.95 sur mauvaise zone ». À encapsuler derrière `_resolve_by_template` sans casser la cascade.
4. **pHash : sortir du global. Deux modes complémentaires :**
- **LoopDetector (QW2)** → DINOv2 features sur l'écran entier, cos-sim < 0.99 = écran a bougé. Plus robuste qu'un pHash 64-bit à un curseur clignotant ou à un caret blinking.
- **VERIFY post-action** → **SSIM par ROI** (skimage `structural_similarity`, ~5-10 ms sur crop 400×200), avec ROI = bbox de la cible cliquée + halo 50 px. C'est la version *spatialisée* qui résout aussi DETTE-001 (BUG_PRECHECK_SPATIAL_BLINDNESS).
5. **Dépendances** : ce travail est **bloquant** pour AXE_A5 (tokenisation UI : OmniParser et UI-DETR-1 utilisent in fine un OCR + détection icônes — décider du moteur OCR avant tokenisation). Il **alimente** AXE_B2 (Validator) qui consommera SSIM-ROI comme signal sémantique de VERIFY.
---
## 1. Sous-axe 1 — OCR pour grounding
### 1.1. Question centrale : bbox token-level dans le même repère que la ligne
Le bug `center-of-line` apparaît parce que `_resolve_by_ocr_text` (resolve_engine.py:1486-1519) calcule `cx, cy` à partir de la `line_obj.geometry` (bbox de la ligne entière) alors que `target_text` n'est qu'un sous-fragment. Pour le résoudre **sans changer d'OCR**, il suffit que l'OCR expose, dans le même repère normalisé que la ligne, les bbox des **words** qui composent la ligne. C'est le critère discriminant.
### 1.2. Table comparative (mai 2026)
| OCR | Granularité bbox | Repère | Français/accents | Latence (CPU 2560×1600) | Stack | Licence | Date release majeure |
|---|---|---|---|---|---|---|---|
| **docTR** (`python-doctr`) | **word + line + block** | normalisé `[(xmin,ymin),(xmax,ymax)]` ∈ [0,1]², **commun line/word** | bon (modèle `crnn_vgg16_bn` français) | ~800 ms CPU, ~150 ms GPU | PyTorch + TF, ONNX optionnel | Apache 2.0 | v0.10 (2026-04, `python-doctr` PyPI) |
| **EasyOCR** | line merged (par défaut) + char optionnel via `ycenter_ths`/`width_ths` | pixel absolu | bon | ~1.2 s CPU, ~200 ms GPU | PyTorch, CRNN | Apache 2.0 | v1.7.x (2024) |
| **RapidOCR** (`rapidocr`) | line | pixel absolu | bon (modèle PP-OCRv4 fr) | ~200 ms ONNX-CPU, ~80 ms GPU | ONNXRuntime / OpenVINO / MNN / PaddlePaddle, **sans dépendance Paddle** | Apache 2.0 | v3.x (2026-04-11) |
| **PaddleOCR / PP-StructureV3** | line par défaut ; **`return_word_box=True`** en option | pixel absolu | bon | ~250 ms GPU (PP-OCRv4) | PaddlePaddle (lourd) | Apache 2.0 | v3.0 (2025-07) |
| **Surya OCR** (`surya-ocr`) | **line only** | pixel absolu | bon (90+ langues) | ~400 ms GPU (5070-class) | PyTorch | GPL-3.0 (commercial restrictif) | v0.17.x (2025) |
| **Tesseract** (via `pytesseract`) | **word + line + char** via `image_to_data` / `hOCR` | pixel absolu | moyen-bon (modèle `fra`) | 100-500 ms CPU | C++ LSTM | Apache 2.0 | v5.4 (2024) |
**Sources principales :** [docTR Word/Line geometry — Discussion #570](https://github.com/mindee/doctr/discussions/570), [PaddleOCR return_word_box — Issue #15760](https://github.com/PaddlePaddle/PaddleOCR/issues/15760), [Surya line-level — repo datalab-to/surya](https://github.com/datalab-to/surya), [EasyOCR character bbox limitation — Issue #631](https://github.com/JaidedAI/EasyOCR/issues/631), [RapidOCR releases](https://github.com/RapidAI/RapidOCR/releases), [pytesseract image_to_data — PyPI](https://pypi.org/project/pytesseract/), [Codesota benchmark PaddleOCR vs EasyOCR 2025](https://www.codesota.com/ocr/paddleocr-vs-easyocr), [Tildalice benchmark PaddleOCR vs Doctr](https://buttondown.com/ckae930413/archive/paddleocr-vs-easyocr-vs-doctr-memory-latency-test/).
### 1.3. Analyse du bug center-of-line
**Le bug est résolvable nativement avec docTR.** L'API expose `line_obj.words` (List[Word]) avec chaque `Word.geometry` au même format `((xmin,ymin),(xmax,ymax))` normalisé que `line_obj.geometry`. Il n'y a aucun changement de repère à faire — c'est le même page-relative ∈ [0,1]². Cf. [docTR I/O modules doc](https://mindee.github.io/doctr/modules/io.html).
EasyOCR a la **mauvaise granularité par défaut** : il merge les détections en segments via `ycenter_ths=0.5` et `width_ths=0.5`, donc une rangée de tabs serrée tombera comme une boîte unique, **sans accès aux sous-words**. Demander explicitement `width_ths=0.0` casserait la fusion mais aussi pour les vrais textes longs (« Justification de la décision »). **EasyOCR seul ne résout pas le bug.**
Surya OCR est annoncé explicitement comme line-only : « Surya predicts line-level bboxes, while tesseract and others predict word-level or character-level » (cf. [datalab-to/surya README](https://github.com/datalab-to/surya)). **À écarter** pour ce besoin.
PaddleOCR `return_word_box=True` est disponible en v3.0 mais nécessite une dépendance PaddlePaddle ~700 Mo et un init ~8-12 s sur CPU.
RapidOCR repackage les modèles PaddleOCR en ONNX (80 Mo install, init <2 s) ; **il faut vérifier en mai 2026 si `return_word_box` est exposé dans la couche `rapidocr.RapidOCR(__call__)` ou seulement dans `paddleocr.PaddleOCR`**. À ce jour, la doc publique RapidOCR ne mentionne pas explicitement le mode word-bbox.
### 1.4. Snippets Python — récupérer bbox word-level
**docTR (déjà utilisé en production)**
```python
from doctr.models import ocr_predictor
from doctr.io import DocumentFile
predictor = ocr_predictor(pretrained=True)
doc = DocumentFile.from_images("/path/screenshot.png")
result = predictor(doc)
# Navigation hiérarchique : pages -> blocks -> lines -> words
page = result.pages[0]
H, W = page.dimensions # (height, width) pixels
for block in page.blocks:
for line in block.lines:
# line.geometry == ((xmin, ymin), (xmax, ymax)) normalisé [0,1]²
# line.words == List[Word], chaque Word.geometry au même format
for word in line.words:
(xmin_n, ymin_n), (xmax_n, ymax_n) = word.geometry
# Pixels absolus
xmin_px = xmin_n * W
ymax_px = ymax_n * H
print(f"{word.value!r} bbox=({xmin_px:.0f},{ymin_px*H:.0f})-({xmax_px:.0f},{ymax_px:.0f})")
```
**Tesseract (alternative légère, fallback CPU)**
```python
import pytesseract
from PIL import Image
img = Image.open("/path/screenshot.png")
data = pytesseract.image_to_data(img, lang="fra", output_type=pytesseract.Output.DICT)
# data == dict with keys 'text','left','top','width','height','conf','line_num','word_num','block_num'
n = len(data['text'])
for i in range(n):
if data['text'][i].strip() and int(data['conf'][i]) > 50:
x, y, w, h = data['left'][i], data['top'][i], data['width'][i], data['height'][i]
# Pixels absolus directement
print(f"{data['text'][i]!r} bbox=({x},{y})-({x+w},{y+h}) line={data['line_num'][i]}")
```
**RapidOCR (candidat migration, ONNX léger)**
```python
from rapidocr import RapidOCR
engine = RapidOCR()
result, elapsed = engine("/path/screenshot.png")
# result == [[box, text, score], ...] avec box = [[x1,y1],[x2,y2],[x3,y3],[x4,y4]] pixel absolu
# ⚠ Niveau line par défaut — à valider en mai 2026 si word-level disponible
```
### 1.5. Recommandation
**Garder docTR pour OCR-DIRECT** (mode strict + cascade resolve_engine). C'est l'OCR qui colle déjà aux contraintes du bug. Le quick fix §5 (recalcul `cx, cy` depuis `line.words`) ne nécessite ni migration ni changement d'API.
**Ne PAS migrer en chaud vers EasyOCR ou Surya** : EasyOCR perd le sous-word, Surya est line-only par design.
**Évaluation parallèle** (post-démo, AXE_A5) :
- RapidOCR sur 10 captures Easily fr — gain potentiel : init 2 s vs 5-8 s docTR, install 80 Mo vs 500 Mo + PyTorch.
- Tesseract `image_to_data` lang `fra` — peut servir de **second moteur OCR de vérification** (vote OCR à 2 moteurs) pour DETTE-001.
---
## 2. Sous-axe 2 — Template matching (étage 2 cascade)
### 2.1. Question centrale : robustesse à l'offset/scale + élimination des faux positifs 0.95
`cv2.matchTemplate` multi-scale (range 0.25→2.0, `resolve_engine.py:130`) calcule un score de corrélation NCC pixel-à-pixel. Limites connues :
- **Aucune invariance à la rotation.** Easily/Edge sont fixes en rotation, donc OK ici.
- **Sensible à l'anti-aliasing** : un même bouton scaled 0.95× vs 1.0× peut perdre 0.10 sur le score.
- **Le score haut ne garantit pas la bonne région** : le match peut être 0.95 sur un patch visuellement similaire (autre bouton de la même barre, même icône de close, etc.). C'est exactement le mécanisme qui force aujourd'hui le `drift exemption ≥ 0.95` (`resolve_engine.py:2367-2390`) à être une rustine — score haut, mauvais endroit.
- Cf. [PyImageSearch multi-scale template matching](https://pyimagesearch.com/2015/01/26/multi-scale-template-matching-using-python-opencv/) et [Medium — Template Matching Beyond Basics](https://medium.com/@coders.stop/template-matching-beyond-basics-rotation-and-scale-invariant-detection-2ae78d8fa190).
### 2.2. Table comparative
| Méthode | Invariance | Score géométrique | Latence pair (RTX 5070, 800×500 vs 2560×1600) | Faux positif 0.95 ? | Licence | Maturité 2026 |
|---|---|---|---|---|---|---|
| **`cv2.matchTemplate` NCC multi-scale** (actuel) | scale ±20 % (force brute) | non — score pixel | ~50-200 ms CPU (multi-scale loop) | **oui** (rustine drift exemption) | BSD | mature |
| **SIFT / AKAZE / ORB (cv2)** | scale + rotation + offset | non — inliers RANSAC | ~30 ms CPU | filtré par RANSAC mais sensible aux UI peu texturées | BSD | mature |
| **SuperPoint + LightGlue (ONNX)** | scale + rotation + offset + photométrie | **oui** — score MNN + inliers | **~44 ms (22 FPS) pair complète RTX-class** | **non** si on prend `len(matches) > seuil` ET cohérence homographique | Apache 2.0 (modèle), [fabio-sim/LightGlue-ONNX](https://github.com/fabio-sim/LightGlue-ONNX) | très mature 2024-2026 |
| **LoFTR / Efficient LoFTR** | id. | id. + dense | ~80 ms pair RTX-class | non | Apache 2.0 | mature, +1-2 pp AUC vs LightGlue mais 2× plus lent |
| **DINOv2 patch features + kNN match** | id. + sémantique | cosine sim patch | ~150 ms (extract DINOv2 ViT-L) | rare (sémantique > pixel) | CC-BY-NC-4.0 ⚠ | très mature 2024-2026 |
| **RoMa / RoMa v2** | id. + dense, sub-pixel | warp + certainties | ~200 ms RTX-class (v2 = 1.7× v1) | non | non-commercial | CVPR 2024, v2 fin 2025 |
| **MASt3R-SfM** | id. + 3D | grid match | très lourd (~1 s+ par pair) | non | non-commercial | recherche 2024 |
| **CLIP visual similarity** (global embedding) | id. + sémantique | cos-sim global | ~30 ms ViT-B/32 | échoue : trop global, ne localise pas | MIT | mature |
**Sources :** [LightGlue ICCV 2023 paper](https://openaccess.thecvf.com/content/ICCV2023/papers/Lindenberger_LightGlue_Local_Feature_Matching_at_Light_Speed_ICCV_2023_paper.pdf), [Efficient LoFTR arXiv 2403.04765](https://arxiv.org/pdf/2403.04765), [RoMa v2 emergent mind](https://www.emergentmind.com/papers/2511.15706), [DINOv2 features](https://www.emergentmind.com/topics/dinov2-features), [Image Matching Challenge 2025 — DINO-RotateMatch arXiv 2512.03715](https://arxiv.org/pdf/2512.03715), [LightGlue ONNX](https://github.com/fabio-sim/LightGlue-ONNX), [LightGlue HF Transformers](https://huggingface.co/docs/transformers/model_doc/lightglue).
### 2.3. Critère faux-positif 0.95
C'est le critère discriminant pour sortir de la drift exemption rustine. **SuperPoint + LightGlue** fournit deux signaux séparables :
1. `n_matches` : nombre de keypoints appariés (typique 50-200 pour un widget visible).
2. **Cohérence géométrique** : on calcule l'homographie via `cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)` sur les matches et on garde le ratio inliers / total. Un faux positif 0.95 sur région différente aura `n_matches < 10` ou un ratio inliers < 0.5.
Cela élimine la classe de bug « score 0.95, mauvais bouton » sans avoir besoin d'un seuil bas qui ferait passer le faux positif.
### 2.4. Recommandation
**Phase 1 (court terme, post-démo)** : conserver `cv2.matchTemplate` mais **ajouter une vérification géométrique LightGlue+SuperPoint en ratification** quand le score est ∈ [0.80, 0.95] (zone aujourd'hui ambiguë). Si LightGlue confirme la cohérence homographique → garder le match. Sinon → fallback VLM. Cela réduit l'exemption drift de 0.95 vers 0.80.
**Phase 2 (moyen terme)** : remplacer la boucle multi-scale `cv2.matchTemplate` par LightGlue+SuperPoint en méthode primaire d'étage 2. Garder un fallback NCC pour les widgets très uniformes/texturés faiblement (icônes monochromes plates où LightGlue manque de keypoints).
**Snippet — intégration LightGlue compatible cascade actuelle**
```python
# Pseudo-code à brancher dans resolve_engine._resolve_by_template
# Ne PAS appliquer en l'état — validation syntaxique seulement.
from lightglue import LightGlue, SuperPoint
from lightglue.utils import load_image, rbd
import cv2, torch
_LG_DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
_LG_EXTRACTOR = SuperPoint(max_num_keypoints=2048).eval().to(_LG_DEVICE)
_LG_MATCHER = LightGlue(features="superpoint").eval().to(_LG_DEVICE)
def _verify_template_match_with_lightglue(
screenshot_bgr,
template_bgr,
candidate_xy, # (cx, cy) pixel renvoyé par cv2.matchTemplate
inlier_ratio_threshold=0.5,
min_matches=10,
):
"""Confirme géométriquement un match cv2.matchTemplate.
Returns:
dict(confirmed=bool, n_matches=int, inlier_ratio=float)
"""
# Crop autour du candidat (taille du template + halo)
th, tw = template_bgr.shape[:2]
cx, cy = candidate_xy
x0 = max(0, cx - tw)
y0 = max(0, cy - th)
x1 = min(screenshot_bgr.shape[1], cx + tw)
y1 = min(screenshot_bgr.shape[0], cy + th)
crop = screenshot_bgr[y0:y1, x0:x1]
# Tensors LightGlue (1, 1, H, W) float [0,1]
crop_t = torch.from_numpy(cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY)).float()[None, None] / 255.0
tpl_t = torch.from_numpy(cv2.cvtColor(template_bgr, cv2.COLOR_BGR2GRAY)).float()[None, None] / 255.0
with torch.no_grad():
feats0 = _LG_EXTRACTOR.extract(tpl_t.to(_LG_DEVICE))
feats1 = _LG_EXTRACTOR.extract(crop_t.to(_LG_DEVICE))
matches01 = _LG_MATCHER({"image0": feats0, "image1": feats1})
feats0, feats1, matches01 = (rbd(x) for x in [feats0, feats1, matches01])
matches = matches01["matches"] # (M, 2)
n_matches = matches.shape[0]
if n_matches < min_matches:
return {"confirmed": False, "n_matches": n_matches, "inlier_ratio": 0.0}
pts0 = feats0["keypoints"][matches[..., 0]].cpu().numpy()
pts1 = feats1["keypoints"][matches[..., 1]].cpu().numpy()
H_, mask = cv2.findHomography(pts0, pts1, cv2.RANSAC, 5.0)
if H_ is None:
return {"confirmed": False, "n_matches": n_matches, "inlier_ratio": 0.0}
inlier_ratio = float(mask.sum()) / n_matches
return {
"confirmed": inlier_ratio >= inlier_ratio_threshold,
"n_matches": n_matches,
"inlier_ratio": inlier_ratio,
}
```
À brancher en **post-process** de `cv2.matchTemplate` : si score ∈ [0.80, 0.95], appel LightGlue. Si confirmé → garder. Cela transforme la rustine drift exemption en *vérification ratifiée*.
---
## 3. Sous-axe 3 — pHash → alternatives 2026
### 3.1. Usages actuels et limites
| Usage | Implémentation actuelle | Limite documentée |
|---|---|---|
| **LoopDetector QW2** | pHash global (`screen_static` ≥ threshold) + `action_repeat` + `retry_threshold` | un caret blinking ou un curseur sur barre de chargement fait varier le hash → faux négatif (« écran a bougé » alors qu'il n'a rien changé fonctionnellement) |
| **VERIFY post-action** | pHash global avant/après click | un clic local sur un onglet change ~5 % de l'image (la zone des tabs + le contenu de l'onglet) — peut être absorbé par le hash global → faux négatif (le click n'a rien fait visible). Inversement, popup arrière-plan / curseur souris fait croire à un changement. |
Diagnostic principal : `feedback_phash_vs_dialog_in_vm.md` (memory) — pHash global est trop grossier pour la cascade VM. DETTE-001 (BUG_PRECHECK_SPATIAL_BLINDNESS) montre que c'est **spatialement aveugle** : `_text_match_fuzzy` valide le pré-check OCR au mauvais endroit parce que le radius 280 px englobe plusieurs tabs.
### 3.2. Table comparative — alternatives 2026
| Méthode | Mode | Latence (crop 400×200) | Mode ROI ? | Robustesse caret/curseur | Distingue mouvement local | Bibliothèque |
|---|---|---|---|---|---|---|
| **pHash global 64-bit** | actuel | <5 ms | non | mauvaise | non | `imagehash` |
| **pHash par ROI (rolling)** | extension simple | ~5 ms × N régions | oui (par tuiles) | OK | oui | `imagehash` |
| **SSIM** (skimage) | classique | 5-10 ms CPU | **oui native** | bonne | oui | `skimage.metrics.structural_similarity` |
| **MS-SSIM** | multi-échelle | 15-30 ms | oui | meilleure | oui | `pytorch-msssim` |
| **LPIPS** (AlexNet/VGG) | deep | 30-80 ms | oui via crop | excellente (sémantique) | oui | `lpips` |
| **DINOv2 patch features cos-sim** | deep semantic | 100-200 ms (ViT-S/14) | oui (patches) | excellente | oui | `transformers` + `dinov2_vits14` |
| **CLIP image embedding cos-sim** | global semantic | ~30 ms | non (perd info spatiale) | bonne mais pas local | non | `open_clip` |
**Sources :** [Eureka — SSIM vs LPIPS](https://eureka.patsnap.com/article/ssim-vs-lpips-which-metric-should-you-trust-for-image-quality-evaluation), [SSIM scikit-image doc](https://scikit-image.org/docs/dev/auto_examples/transform/plot_ssim.html), [Wopee — Screenshot Comparison Algorithms](https://wopee.io/blog/screenshot-comparison-algorithms-visual-testing/), [Medium CLIP vs DINOv2 image similarity](https://medium.com/aimonks/clip-vs-dinov2-in-image-similarity-6fa5aa7ed8c6), [DinoHash arXiv 2503.11195](https://arxiv.org/pdf/2503.11195).
### 3.3. Recommandation par usage
**LoopDetector QW2 (écran statique → boucle)**
- **Adopter** : DINOv2 features cos-sim sur frame entière (downscale 224×224 avant). Seuil cos < 0.99 = changement réel. Robuste au caret blinking, au scroll-bar position, à la souris.
- **Coût** : ~100 ms par frame sur RTX 5070. Acceptable pour un trigger appelé 1×/sec.
- **Alternative dégradée** : pHash par ROI (grille 4×4 tuiles), ré-utilise `imagehash` actuel, sans GPU.
**VERIFY post-action (a-t-on cliqué utilement ?)**
- **Adopter SSIM par ROI** :
- ROI = bbox du target résolu + halo 50 px (ou la zone qu'on s'attend à voir changer si elle est connue : par exemple, le contenu d'onglet pour un click sur onglet).
- `structural_similarity(roi_before, roi_after, multichannel=True)`.
- Seuil empirique à calibrer (0.85 = changement notable, 0.95 = rien n'a changé).
- **Coût** : ~5 ms CPU sur crop 400×200, négligeable.
- **Bénéfice transversal** : résout aussi DETTE-001 — au lieu de vérifier que `target_text` est présent dans un crop OCR autour du click, on vérifie que la **zone** elle-même a changé (= un click vraiment effectif déclenche un repaint local).
**Snippet — SSIM ROI VERIFY (drop-in dans `replay_verifier.py`)**
```python
from skimage.metrics import structural_similarity as ssim
import cv2, numpy as np
def verify_click_changed_roi(
screenshot_before_path: str,
screenshot_after_path: str,
cx_px: int,
cy_px: int,
roi_w: int = 400,
roi_h: int = 200,
threshold: float = 0.95,
) -> dict:
"""Vérifie qu'un click a effectivement modifié la ROI cible.
Returns:
dict(changed=bool, ssim=float, roi_bbox=(x0,y0,x1,y1))
"""
before = cv2.imread(screenshot_before_path)
after = cv2.imread(screenshot_after_path)
if before is None or after is None or before.shape != after.shape:
return {"changed": False, "ssim": 0.0, "roi_bbox": (0, 0, 0, 0)}
H, W = before.shape[:2]
x0 = max(0, cx_px - roi_w // 2)
y0 = max(0, cy_px - roi_h // 2)
x1 = min(W, cx_px + roi_w // 2)
y1 = min(H, cy_px + roi_h // 2)
crop_b = cv2.cvtColor(before[y0:y1, x0:x1], cv2.COLOR_BGR2GRAY)
crop_a = cv2.cvtColor(after[y0:y1, x0:x1], cv2.COLOR_BGR2GRAY)
score = float(ssim(crop_b, crop_a, data_range=255))
return {
"changed": score < threshold,
"ssim": score,
"roi_bbox": (x0, y0, x1, y1),
}
```
---
## 4. Patch ciblé — bug center-of-line de `_resolve_by_ocr_text`
### 4.1. Cible exacte
Fichier : `agent_v0/server_v1/resolve_engine.py`, fonction `_resolve_by_ocr_text`, lignes **1486-1519** (référence dans `REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` §1.2).
Bloc actuel reconstitué d'après §1.2 du diagnostic 8 mai :
```python
# resolve_engine.py:1486-1519 (état au 8 mai 2026)
# Match exact > contient > mot par mot
score = 0.0
if target_lower == line_lower:
score = 1.0
elif target_lower in line_lower:
score = 0.8
elif any(target_lower == w.value.lower() for w in line_obj.words):
score = 0.9
if score > best_score:
box = line_obj.geometry # ⚠ bbox de la LIGNE ENTIÈRE
cx = (box[0][0] + box[1][0]) / 2
cy = (box[0][1] + box[1][1]) / 2
best_score = score
best_match = {"cx": cx, "cy": cy, "score": score, "line": line_obj.value}
```
### 4.2. Patch proposé — center-of-span depuis `line.words`
**Principe** : pour les scores 0.8 (substring) et 0.9 (mot exact), recalculer `cx, cy` à partir des bbox des `words` qui couvrent le `target_text`, **pas** de la ligne entière.
Code copy-paste-ready (validation syntaxique seulement, **non exécuté**) :
```python
# resolve_engine.py:1486-1519 (proposition)
# Match exact > contient > mot par mot
score = 0.0
matched_words = [] # sous-ensemble de line_obj.words couvrant target_text
target_lower = target_text.lower().strip()
line_lower = line_obj.value.lower().strip()
# 1) Match exact ligne entière
if target_lower == line_lower:
score = 1.0
matched_words = list(line_obj.words)
# 2) Match substring (multi-mots possibles)
elif target_lower in line_lower:
score = 0.8
# Reconstruire le span de words couvrant target_lower par concat séquentielle
target_tokens = target_lower.split()
line_words_lower = [w.value.lower() for w in line_obj.words]
# Recherche d'une fenêtre contiguë qui matche tous les target_tokens dans l'ordre
for start in range(len(line_words_lower) - len(target_tokens) + 1):
window = line_words_lower[start:start + len(target_tokens)]
# Comparaison tolérante : un token cible peut être préfixe/égal au token line
if all(t == w or t in w or w in t for t, w in zip(target_tokens, window)):
matched_words = line_obj.words[start:start + len(target_tokens)]
break
if not matched_words:
# Fallback : tous les words contenant un token cible
matched_words = [w for w in line_obj.words if any(t in w.value.lower() for t in target_tokens)]
# 3) Match mot-exact dans la ligne (single token)
elif any(target_lower == w.value.lower() for w in line_obj.words):
score = 0.9
matched_words = [w for w in line_obj.words if w.value.lower() == target_lower]
if score > best_score:
if matched_words:
# ✅ Centre du SPAN matché, pas de la ligne entière
xs = []
ys = []
for w in matched_words:
(xmin, ymin), (xmax, ymax) = w.geometry
xs.extend([xmin, xmax])
ys.extend([ymin, ymax])
cx = (min(xs) + max(xs)) / 2
cy = (min(ys) + max(ys)) / 2
else:
# Fallback de sécurité : centre de la ligne (comportement actuel)
box = line_obj.geometry
cx = (box[0][0] + box[1][0]) / 2
cy = (box[0][1] + box[1][1]) / 2
best_score = score
best_match = {
"cx": cx,
"cy": cy,
"score": score,
"line": line_obj.value,
"matched_span": " ".join(w.value for w in matched_words) if matched_words else None,
}
```
### 4.3. Justification, risques, tests à faire avant merge
**Pourquoi ça résout le bug** : pour `target='Imagerie'` dans la ligne `"Motif d'admission Examens cliniques Imagerie Notes médicales Synthèse Urgences Codage >"`, `matched_words` capturera uniquement le `Word` `"Imagerie"` (geometry locale), pas tous les words de la ligne. `cx, cy` retomberont au centre exact de ce mot. Idem pour `'Notes médicales'` (2 words contigus) et `'Synthèse Urgences'` (2 words contigus). Plus de collision (0.23, 0.28).
**Repère identique** : `Word.geometry` est dans le **même repère normalisé** que `line_obj.geometry` (vérifié par doc docTR — cf. [Discussion #570](https://github.com/mindee/doctr/discussions/570) et [io modules](https://mindee.github.io/doctr/modules/io.html)). Aucune conversion d'échelle requise.
**Risques résiduels** :
1. **Casse/accents** : `target_lower in line_lower` puis comparaison `t == w or t in w or w in t` — il faut **normaliser les accents** (NFD + strip diacritics) si `target='Notes médicales'` vs `Word='médicales'` matche, mais `target='Notes medicales'` (sans accent venant du JSON workflow) peut rater. Mitigation : `unicodedata.normalize('NFKD', s).encode('ascii','ignore').decode()` sur les deux côtés avant la comparaison.
2. **Tokenisation docTR ≠ split blancs** : docTR sépare typiquement par espace mais peut séparer/grouper différemment des hyphens/apostrophes. Le fallback `matched_words = [w for w in line_obj.words if any(t in w.value.lower() for t in target_tokens)]` couvre ce cas mais peut sur-matcher.
3. **Performance** : O(n_words × n_target_tokens) — négligeable (n_words < 50 typiquement).
4. **Régressions cosmétiques** : `pre_check_text_match` (DETTE-001) actuellement OFF — à re-tester avec ce fix actif.
**Tests minimaux avant merge (10 min)** :
```bash
cd /home/dom/ai/rpa_vision_v3 && source .venv/bin/activate
python -c "
from agent_v0.server_v1.resolve_engine import _resolve_by_ocr_text
img='/home/dom/ai/rpa_vision_v3/visual_workflow_builder/backend/data/anchors/anchor_0438bd2d9bdd_1778161174_full.png'
for t in ['Imagerie','Notes médicales','Synthèse Urgences','Codage','Examens cliniques']:
r = _resolve_by_ocr_text(img, t, 2560, 1600)
print(f'{t:25s} -> cx={r[\"x_pct\"]:.4f} cy={r[\"y_pct\"]:.4f} score={r[\"score\"]:.2f}')
"
```
Critère succès : `Imagerie / Notes médicales / Synthèse Urgences` ont des `cx` séparés d'au moins 0.05 (≈ 130 px à 2560 px).
**À NE PAS faire en chaud démo** (cf. §5 du diagnostic 8 mai). Le quick fix démo reste le timeout client `5 → 30 s`. Ce patch s'applique sur runner 2 (post-démo).
---
## 5. Dépendances croisées avec les autres axes
- **AXE_A5 (tokenisation UI / OmniParser)** : OmniParser utilise PaddleOCR pour l'OCR d'icônes. Si on bascule vers tokenisation OmniParser-style en cascade `1.5` (entre OCR et VLM), il faudra décider **un seul moteur OCR pour tout le pipeline** ou accepter 2 moteurs (docTR pour resolve_engine, PaddleOCR/RapidOCR pour tokenisation). Voir AXE_A5 livrable.
- **AXE_B2 (Validator)** : SSIM-ROI proposé §3 alimente directement le composant Validator du Planner-Actor-Validator (cf. SYNTHESE §5.2). C'est le signal sémantique « le click a fait quelque chose dans la zone attendue » qui élimine la classe de bugs « cliqué quelque part, REPORT success=True ».
- **DETTE-001** : le patch §4 + SSIM-ROI §3 referment la dette (le pré-check OCR cesse d'être spatialement aveugle parce qu'il vise un span exact, et la vérification post-click se fait sur ROI ciblée).
- **Drift exemption ≥ 0.95** : la ratification LightGlue (§2.4) permet de baisser le seuil vers 0.80 sans réintroduire de faux positifs.
---
## 6. Sources (chronologie)
- [docTR — Word/Line geometry — Discussion #570 (2022, valide en 2026)](https://github.com/mindee/doctr/discussions/570)
- [docTR — I/O modules (doc officielle)](https://mindee.github.io/doctr/modules/io.html)
- [docTR — repo principal (release v0.10, 2026-04)](https://github.com/mindee/doctr)
- [docTR — PyPI python-doctr](https://pypi.org/project/python-doctr/)
- [PaddleOCR — return_word_box Issue #15760 (2024)](https://github.com/PaddlePaddle/PaddleOCR/issues/15760)
- [PaddleOCR 3.0 Technical Report (2025-07)](https://arxiv.org/pdf/2507.05595)
- [Surya OCR — datalab-to/surya](https://github.com/datalab-to/surya)
- [Surya OCR — PyPI v0.17.1](https://pypi.org/project/surya-ocr/)
- [EasyOCR — Character bbox Issue #631](https://github.com/JaidedAI/EasyOCR/issues/631)
- [RapidOCR — releases (v3.x, 2026-04-11)](https://github.com/RapidAI/RapidOCR/releases)
- [RapidOCR — repo](https://github.com/RapidAI/RapidOCR)
- [pytesseract — image_to_data + hOCR (PyPI)](https://pypi.org/project/pytesseract/)
- [Codesota — PaddleOCR vs EasyOCR Speed 2025](https://www.codesota.com/ocr/paddleocr-vs-easyocr)
- [Codesota — PaddleOCR vs Tesseract vs EasyOCR 2026](https://www.codesota.com/ocr/paddleocr-vs-tesseract)
- [Buttondown — PaddleOCR vs EasyOCR vs Doctr Memory & Latency](https://buttondown.com/ckae930413/archive/paddleocr-vs-easyocr-vs-doctr-memory-latency-test/)
- [LightGlue — ICCV 2023 paper](https://openaccess.thecvf.com/content/ICCV2023/papers/Lindenberger_LightGlue_Local_Feature_Matching_at_Light_Speed_ICCV_2023_paper.pdf)
- [LightGlue — repo cvg/LightGlue](https://github.com/cvg/LightGlue)
- [LightGlue ONNX — fabio-sim/LightGlue-ONNX](https://github.com/fabio-sim/LightGlue-ONNX)
- [LightGlue — HuggingFace Transformers integration](https://huggingface.co/docs/transformers/model_doc/lightglue)
- [Efficient LoFTR — arXiv 2403.04765 (CVPR 2024)](https://arxiv.org/pdf/2403.04765)
- [RoMa — CVPR 2024](https://openaccess.thecvf.com/content/CVPR2024/html/Edstedt_RoMa_Robust_Dense_Feature_Matching_CVPR_2024_paper.html)
- [RoMa v2 — emergent mind 2025-11](https://www.emergentmind.com/papers/2511.15706)
- [DINOv2 — features tutorial](https://www.lightly.ai/blog/dinov2)
- [DINO-RotateMatch — arXiv 2512.03715 (2025)](https://arxiv.org/pdf/2512.03715)
- [PyImageSearch — Multi-scale Template Matching (2015, ref classique)](https://pyimagesearch.com/2015/01/26/multi-scale-template-matching-using-python-opencv/)
- [Medium — Template Matching Beyond Basics: Rotation & Scale Invariant](https://medium.com/@coders.stop/template-matching-beyond-basics-rotation-and-scale-invariant-detection-2ae78d8fa190)
- [Eureka — SSIM vs LPIPS](https://eureka.patsnap.com/article/ssim-vs-lpips-which-metric-should-you-trust-for-image-quality-evaluation)
- [skimage — structural_similarity doc](https://scikit-image.org/docs/dev/auto_examples/transform/plot_ssim.html)
- [Wopee — Screenshot Comparison Algorithms](https://wopee.io/blog/screenshot-comparison-algorithms-visual-testing/)
- [Medium — CLIP vs DINOv2 image similarity](https://medium.com/aimonks/clip-vs-dinov2-in-image-similarity-6fa5aa7ed8c6)
- [DinoHash — arXiv 2503.11195](https://arxiv.org/pdf/2503.11195)
- [OmniParser — DeepWiki OCR module](https://deepwiki.com/microsoft/OmniParser/2.2-ocr-and-image-processing)
---
*Document de recherche. Aucun code modifié. Toute application validée par Dom au cas par cas.*

View File

@@ -0,0 +1,318 @@
# AXE A5 — Tokenisation d'écran (OmniParser, Set-of-Marks, modèles natifs)
**Date :** 2026-05-23
**Auteur :** Claude (agent dispatché par session principale)
**Statut :** lecture seule, recherche pour arbitrage post-démo. Pas d'action proposée.
**Axes liés :** A1 (modèles VLM grounding), A4 (OCR/docTR/RapidOCR), CLAUDE.md §asymétrie record/replay.
---
## 1. TL;DR
**Recommandation : Scénario B (log implicite, no risk) maintenant, Scénario A (OmniParser V2 dans la cascade replay) à instruire post-démo, Scénario C (OS-Atlas / GUI-Actor) à benchmarker en R&D mais hors trajectoire courte.**
**Top finding** : `core/detection/som_engine.py` est **déjà câblé** sur les weights OmniParser (`/home/dom/ai/OmniParser/weights/icon_detect/model.pt`, YOLOv8) côté serveur. Utilisé en **recording** (`stream_processor.py:607-638`) ET déclaré dans une voie `_resolve_set_of_marks` du `resolve_engine.py:1083-1325`. L'asymétrie pointée par CLAUDE.md n'est donc pas "UI-DETR-1 vs rien", elle est "SomEngine activé selon les paths". À vérifier au runtime si `_resolve_set_of_marks` est effectivement appelé en replay — possible code orphelin (cf. champs de mines CLAUDE.md).
**Trois faits 2025 qui changent l'arbitrage :**
1. **OmniParser V2** (publié 12 février 2025, MIT pour le code + Florence-2, **AGPL-3.0 pour les poids YOLOv8 icon_detect**) — 0.6 s/frame A100, 0.8 s/frame RTX 4090, état de l'art ScreenSpot-Pro 39.6 % combiné GPT-4o. Notre RTX 5070 ≈ 12 GB VRAM tient le pipeline complet (icon_detect ~150 MB + Florence-2 base ~750 MB).
2. **Set-of-Mark** (Yang et al., arXiv 2310.11441, NeurIPS 2023) — pattern qui sous-tend OmniParser, Magma, ShowUI. Devenu vocabulaire standard du domaine en 2025.
3. **Coordinate-free grounding émerge** : GUI-Actor (Microsoft, NeurIPS 2025) et Aguvis (ICML 2025) bypassent le besoin d'un étage de tokenisation séparé. GUI-Actor-7B sur Qwen2.5-VL = 44.6 % ScreenSpot-Pro, > UI-TARS-72B. **Tendance** : la tokenisation explicite OmniParser-style devient un raccourci d'aujourd'hui que les VLM "GUI-natifs" intégreront demain.
**Verdict honnête pour rpa_vision_v3** : la cause des bugs récents (cf. `REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md`) est **transport HTTP + OCR-DIRECT center-of-line**, pas la cascade vision. Ajouter OmniParser V2 en replay ne réparera **rien** des bugs P0 ouverts. Mais c'est la brique manquante pour le **Validator** (Skyvern-style) et pour un **log de candidats parsés** côté replay (suggestion §4.1 de `INSPIRATION_FRAMEWORKS`).
---
## 2. OmniParser V2 — fiche détaillée
### 2.1. Identité
- **Auteur** : Microsoft Research
- **Repo** : https://github.com/microsoft/OmniParser
- **Modèle HF** : https://huggingface.co/microsoft/OmniParser-v2.0
- **Release V2** : 12 février 2025
- **Stars GitHub mai 2026** : ~22k (cité dans `INSPIRATION_FRAMEWORKS_2026-05-10.md`)
- **Article officiel** : https://www.microsoft.com/en-us/research/articles/omniparser-v2-turning-any-llm-into-a-computer-use-agent/
### 2.2. Architecture interne (2 modèles)
| Sous-modèle | Rôle | Taille | Backend | Licence |
|---|---|---|---|---|
| **icon_detect** | YOLOv8 fine-tuné sur dataset Microsoft d'éléments interactifs (boutons, icônes, champs) | ~150 MB | Ultralytics `YOLO` | **AGPL-3.0** ⚠ |
| **icon_caption** | Florence-2 base fine-tuné sur 7K paires icon-description annotées GPT-4o | ~750 MB | Transformers | **MIT** |
**OCR** : OmniParser embarque aussi un OCR (PaddleOCR ou EasyOCR selon config). Notre `som_engine.py` actuel **a remplacé** par docTR (cf. lignes 117-127).
### 2.3. Format de sortie ("interactable elements")
Liste de dicts avec :
- `bbox` (coordonnées normalisées 0-1)
- `interactivity` (true / false)
- `content` (caption Florence-2 : "close button", "search field", "OK")
- `source` ("box_yolo_content_ocr" / "box_yolo_content_yolo" / "icon")
Cette sortie est conçue pour être insérée **dans le prompt du VLM principal** (GPT-4o, Claude) sous forme de tableau numéroté → c'est du Set-of-Marks appliqué automatiquement.
### 2.4. Performance benchmarks
| Métrique | Valeur | Source |
|---|---|---|
| ScreenSpot-Pro + GPT-4o | **39.6 %** | Microsoft Research article 2026-02-12 |
| ScreenSpot-Pro GPT-4o seul (sans OmniParser) | 0.8 % | idem |
| Latence A100 (1 frame) | **0.6 s** | idem |
| Latence RTX 4090 (1 frame) | **0.8 s** | idem |
| VRAM mini | 4 GB (inference) / 8 GB (recommandé loop agent) | GitHub Issue #31 |
| Résolution supportée | 640 → **1920 px** côté long (icon_detect) | doc HF V2.0 |
**Implication directe rpa_vision_v3** : nos screenshots Léa sont **2560×1600** (RTX 5070). Il faudra **resize** à 1920 px côté long → ratio ~0.75. Toutes les coordonnées YOLO de retour doivent être re-scalées. **Risque** : c'est le même piège que `smart_resize` Qwen2.5-VL bbox_2d (DETTE-006/010/014, `MIGRATION_VLM_PLAN_2026-05-09.md` §2). À traiter explicitement.
### 2.5. Licence — point sensible commercial
Combinaison **AGPL-3.0** (icon_detect) + **MIT** (icon_caption + code) :
- **AGPL-3.0** sur icon_detect = **interdit en produit commercial fermé** sans rachat de licence YOLOv8 commercial (Ultralytics, ~5 000 $/an estimé), ou remplacement par un détecteur alternatif (Florence-2 task `<OD>`, ou détecteur custom).
- Pour rpa_vision_v3 en POC interne / GHT / Anoust : usage actuel défendable. **Pour vente externe : showstopper sur icon_detect tel quel**.
Le code de notre `som_engine.py` charge directement `/home/dom/ai/OmniParser/weights/icon_detect/model.pt` (AGPL-3.0) → audit licence à reprendre.
### 2.6. État interne du projet — code existant
- `core/detection/som_engine.py` (316 lignes, complet) — singleton thread-safe, YOLO + docTR + annotation. Device par défaut `cpu` (cf. `get_shared_engine`).
- `core/detection/omniparser_adapter.py` — wrapper plus complet citant Florence-2 caption. **Sépare** du `som_engine.py` simplifié.
- `agent_v0/server_v1/stream_processor.py:607-700` — appel SoM au **recording** pour enrichir chaque event click avec l'élément cliqué (cf. UI-DETR-1 dans `INSPIRATION_FRAMEWORKS_2026-05-10.md` §4 — c'est en fait SoM, pas un détecteur tiers).
- `agent_v0/server_v1/resolve_engine.py:1083-1325` — voie `_resolve_set_of_marks` au **replay**. Lance SomEngine, fallback template matching anchor↔éléments YOLO. **À tracer au runtime** pour confirmer qu'elle est invoquée.
**L'asymétrie record/replay décrite dans CLAUDE.md n'est peut-être pas l'asymétrie réelle.** À valider explicitement avec Dom au prochain run.
---
## 3. Set-of-Marks original
### 3.1. Identité
- **Papier** : Yang et al., "Set-of-Mark Prompting Unleashes Extraordinary Visual Grounding in GPT-4V", arXiv 2310.11441 (oct 2023, dernière révision nov 2023)
- **Code** : https://github.com/microsoft/SoM
- **Lien** : https://arxiv.org/abs/2310.11441
- 124 citations (mai 2026)
### 3.2. Approche
1. Segmenter l'image en régions sémantiques (SEEM ou SAM, off-the-shelf)
2. Overlayer chaque région avec un **mark** (numéro, lettre, masque coloré, bbox)
3. Demander au VLM : "click on mark 7" plutôt que "click at (x=420, y=380)"
Bénéfice : le VLM raisonne en **identifiants discrets** et non en coordonnées continues. Évite le bug des coords mal calibrées (cf. notre DETTE-006 bbox_2d Qwen2.5-VL).
### 3.3. Résultats clés (papier 2023)
GPT-4V + SoM en zero-shot **bat le SOTA fine-tuned** sur RefCOCOg (referring expression comprehension). Pour la première fois, un modèle généraliste prompted dépasse les modèles spécialisés sur ce benchmark.
### 3.4. Complémentarité avec OmniParser
- **SoM = méthode de prompting** (overlay sur l'image, le VLM répond un ID)
- **OmniParser = pipeline complet** (détection + caption + format prompt)
- OmniParser = "SoM industrialisé pour les UIs". OmniParser fournit les régions ET les marks ET les captions, contre SoM original qui fournit seulement les marks à partir d'une segmentation SEEM/SAM générique.
- Magma (Microsoft, CVPR 2025) entraîne explicitement le modèle sur des images SoM-labellisées → SoM passe de "trick de prompting" à "supervision de pré-entraînement".
---
## 4. Modèles VLM qui tokenisent nativement
### 4.1. OS-Atlas-Base (ICLR 2025)
- **Repo** : https://github.com/OS-Copilot/OS-Atlas
- **Modèles** : OS-Atlas-Base-4B (sur InternVL2-4B) et OS-Atlas-Base-7B (sur Qwen2-VL-7B-Instruct)
- **Approche** : entraînement sur **13 M éléments GUI** cross-platform (Windows, Linux, macOS, Android, web). Tokens spéciaux `<|box_start|>(x1,y1),(x2,y2)<|box_end|>`. Coordonnées normalisées 0-1000.
- **Tokenise nativement ?** Pas au sens "produit une liste d'éléments en sortie", mais **internalise la grammaire bbox** comme tokens spéciaux. Le modèle apprend que "(124, 380) → (228, 412)" est une bbox, pas une séquence d'entiers libres.
- **Coût** : OS-Atlas-Base-7B = ~14 GB FP16, ~7 GB en 4-bit. Compatible RTX 5070 12 GB en 4-bit ou avec Flash-Attn.
### 4.2. ShowUI-2B (CVPR 2025, Outstanding Paper NeurIPS 2024 workshop)
- **Repo** : https://github.com/showlab/ShowUI
- **HF** : https://huggingface.co/showlab/ShowUI-2B
- **Innovation clé** : **UI-Guided Visual Token Selection** — construit un graphe UI au sein du modèle, élimine 33 % des tokens visuels redondants. **5× plus rapide, 2× plus précis** que les VLM généralistes en localisation.
- **Perf** : **75.1 % accuracy zero-shot ScreenSpot grounding** avec seulement 2B params + 256K échantillons de training.
- **Tokenise nativement ?** Oui, c'est sa nature : la sélection des tokens visuels EST une forme de tokenisation. Pas de pipeline externe.
- **Coût** : 2B params → ~4 GB FP16, ~2 GB en 4-bit. **Léger pour notre RTX 5070**.
### 4.3. Aguvis (ICML 2025)
- **Repo** : https://aguvis-project.github.io/
- **Approche** : framework "pure vision" multi-plateforme avec **inner monologue** structuré. Pipeline 2 étapes (grounding séparé de planification).
- **Caractéristique** : premier agent GUI pure-vision entièrement **open-source** (sans dépendance closed-source).
### 4.4. GUI-Actor (Microsoft, NeurIPS 2025) — **le plus pertinent**
- **Repo** : https://github.com/microsoft/GUI-Actor
- **HF** : https://huggingface.co/microsoft/GUI-Actor-7B-Qwen2.5-VL
- **Innovation** : **coordinate-free**. Au lieu de produire "(420, 380)", produit un token `<ACTOR>` qui s'attache via attention aux patches visuels pertinents. Génère plusieurs régions candidates en 1 forward.
- **Perf** : GUI-Actor-7B sur Qwen2.5-VL = **44.6 % ScreenSpot-Pro** (> UI-TARS-72B).
- **Implication directe** : **tue le bug d'échelle bbox_2d** (DETTE-006/010/014) à la racine. Pas de smart_resize à débugger. Pas de mismatch de résolution.
- **Coût** : 7B + Qwen2.5-VL → ~14 GB FP16, ~7 GB 4-bit. Compatible RTX 5070 en 4-bit.
### 4.5. Magma (Microsoft, CVPR 2025)
- **Repo** : https://github.com/microsoft/Magma
- **Approche** : pré-entraînement multimodal avec **Set-of-Mark labellisation automatique** des éléments cliquables + **Trace-of-Mark** pour la planification d'actions (videos). Premier modèle multi-domaine (GUI + robotique).
- **Tokenise nativement ?** Oui, SoM est dans la supervision de pré-entraînement, pas une étape inférence externe.
---
## 5. Comparaison latence / perf / robustesse
### 5.1. Tableau synthèse
| Approche | Latence / écran 2560×1600 | VRAM | Sortie | Licence | Tokenise nativement ? |
|---|---|---|---|---|---|
| **UI-DETR-1 record-only (actuel)** | n/a (recording offline) | n/a | événements VWB enrichis | propriétaire MS | non, étape séparée |
| **SomEngine actuel** (YOLO icon_detect + docTR) | ~150-300 ms estimé (docTR seul ~100 ms, YOLO ~15 ms, +overhead annotation) | ~1 GB | liste `SomElement` | AGPL-3.0 (YOLO) | non |
| **OmniParser V2 complet** (YOLO + Florence-2 caption) | **0.8 s** (RTX 4090) → ~1.0 s (RTX 5070 estimé) après resize 1920 | ~2 GB | liste `interactable_element` + caption sémantique | AGPL-3.0 + MIT | non |
| **YOLOv8-UI fine-tuned custom** (sans caption) | ~50-100 ms | ~200 MB | bbox seules | AGPL-3.0 par défaut, MIT possible si from scratch | non |
| **ShowUI-2B** | grounding direct, ~1-2 s estimé | ~2-4 GB | coord ou région | code MIT | **oui** (UI-guided token selection) |
| **GUI-Actor-7B** | grounding direct, ~2-3 s estimé | ~7-14 GB | régions multiples via attention | code MIT | **oui** (attention head dédiée) |
| **OS-Atlas-Base-7B** | grounding direct, ~2-3 s estimé | ~7-14 GB | bbox tokens spéciaux | code MIT (modèle Qwen2-VL Apache 2.0) | partiellement |
| **InfiGUI-G1-3B (actuel)** | déjà mesuré ~1-2 s | 3.9 GB | bbox | Apache 2.0 (Qwen) | partiellement |
### 5.2. Robustesse — ce que dit la littérature 2025
- **GUI-Robust** (arXiv 2506.14477) : tous les MLLM (y compris GUI-spécialisés) se dégradent significativement sur 7 types d'anomalies (popups, modifications de layout, désactivation). **OmniParser ne corrige PAS ces anomalies à lui seul** — il aide à voir les éléments, pas à comprendre les bugs UI.
- **Magma + SoM** (CVPR 2025) : SoM **en supervision** > SoM **en prompting inference-only**. Confirme que la prochaine génération est "SoM natif", pas "OmniParser externe".
### 5.3. Comparaison concrète avec notre cascade actuelle
Notre cascade `_resolve_target` (cf. `cartography_execution_flow.md`) :
```
OCR docTR (~200 ms) → template cv2 (~50 ms) → YOLO/SoM optionnel → VLM (1-15 s)
```
Avec SomEngine déjà existant et OmniParser V2 disponible localement (`/home/dom/ai/OmniParser/`), le coût marginal d'ajouter le caption Florence-2 = ~500-700 ms (Florence-2 base). Total cascade enrichie : **~2 s** vs actuel ~1-15 s suivant l'étage qui réussit. **Latence pas un blocker**.
---
## 6. Recommandation pour rpa_vision_v3 — 3 scénarios
### Scénario A — Intégrer OmniParser V2 complet dans la cascade replay
**Description** : activer `_resolve_set_of_marks` en replay, brancher icon_caption Florence-2 pour enrichir les éléments YOLO avec des descriptions sémantiques, exposer cette "vue parsée" à VWB UI et au VLM principal en prompt.
**Pour** :
- Asymétrie record/replay corrigée explicitement.
- Vue parsée disponible pour debug (UI VWB pourrait overlayer les éléments détectés sur captures Léa).
- Caption Florence-2 = donnée d'entrée pour un Validator sémantique post-clic (compare "j'attendais Notes médicales" vs "j'ai cliqué sur élément captioned 'Imagerie tab'").
- Coût latence ~1 s — compatible démo.
**Contre** :
- **Licence AGPL-3.0** sur icon_detect → audit légal avant déploiement commercial Anoust / GHT vente.
- N'efface AUCUN des 5 bugs P0 post-démo (transport HTTP, Stop VWB, mss monitors, échelle pixel, skip ord 13).
- Ajoute une dépendance lourde (`/home/dom/ai/OmniParser/`) au runtime.
- Le bug primaire diagnostiqué 8 mai est OCR-DIRECT center-of-line, pas un manque de candidats détectés.
**Effort** : 1-2 j (le code SomEngine existe, manque le câblage Florence-2 caption + UI debug).
**Risque** : moyen-faible. Tout est déjà en place côté code.
### Scénario B — Log implicite des candidats (no risk, à faire dès stabilisation post-démo)
**Description** : à chaque appel `_resolve_target`, logger systématiquement dans `logs/audit/` la liste des candidats produits par CHAQUE étage de la cascade (docTR lignes/spans, template matches > seuil, sorties YOLO si SomEngine actif, sorties VLM). JSON structuré, 1 ligne par résolution.
**Pour** :
- **Zéro nouveau modèle, zéro nouvelle dépendance**. Pure instrumentation.
- Vue parsée gratuite, exploitable hors-bande (analyse, dashboard).
- Comble l'asymétrie record/replay au sens "trace équivalente".
- Préalable indispensable à tout Validator sémantique futur.
- Aligné avec la suggestion §4.1 de `INSPIRATION_FRAMEWORKS_2026-05-10.md` ("logger systématiquement la liste des candidats détectés par chaque étage de la cascade").
**Contre** :
- N'apporte aucune nouvelle robustesse en propre — juste de l'observabilité.
**Effort** : 0.5 j.
**Risque** : très bas.
**→ À faire en priorité, indépendamment des autres scénarios.**
### Scénario C — Remplacer la cascade par un modèle VLM qui tokenise nativement
**Description** : remplacer `_resolve_target` par un appel direct à GUI-Actor-7B (ou ShowUI-2B en option légère) qui produit la zone-cible en coordinate-free. Peut coexister avec OCR docTR pour validation post-action.
**Pour** :
- **Tue le bug d'échelle bbox_2d** (DETTE-006/010/014) — plus de smart_resize à débugger.
- État de l'art ScreenSpot-Pro (44.6 % GUI-Actor > UI-TARS-72B).
- Architecture plus simple long-terme : 1 modèle = 1 grounder.
- ShowUI-2B = 2× moins gourmand que notre InfiGUI-G1-3B actuel.
**Contre** :
- **Décision structurelle**, pas réversible facilement. Réécrit la moitié de `resolve_engine.py`.
- Aucun bench interne pour valider sur nos écrans Easily / Citrix réels.
- Pas de caption sémantique = pas de Validator sémantique post-action.
- Effort production-grade : 1-2 semaines.
**Effort** : 5-10 j.
**Risque** : élevé tant que pas benché sur nos workflows.
---
## 7. Recommandation finale
**Maintenant (post-démo, semaine du 26 mai)** :
1. **Scénario B obligatoirement** — log des candidats. Coût minimal, valeur immense pour debug futur.
2. **Vérifier au runtime si `_resolve_set_of_marks` est appelé en replay**. Si non = code orphelin (CLAUDE.md §champs de mines). Si oui = on est déjà mi-Scénario A sans le savoir.
3. **Audit licence** AGPL-3.0 sur icon_detect avant tout pitch commercial.
**Sprint suivant (juin si bande passante)** :
4. **Scénario A partiel** — activer Florence-2 icon_caption sur l'événement de recording VWB ET sur les résolutions en échec replay (sentinelle, pas systématique). Caption disponible pour un Validator sémantique futur.
5. **Bench GUI-Actor-7B et ShowUI-2B** sur 10-20 captures Easily Assure réelles. Décision Scénario C **uniquement** sur preuves.
**À ne PAS faire** :
- Activer OmniParser V2 systématiquement en runtime tant que les 5 bugs P0 (`LESSONS_LEARNED_GHT_2026-05.md`) ne sont pas refermés. Latence supplémentaire sur démo fragile.
- Remplacer InfiGUI-G1-3B sans bench comparatif documenté.
---
## 8. Sources
### OmniParser V2
- [Microsoft Research — OmniParser V2: Turning Any LLM into a Computer Use Agent (2025-02-12)](https://www.microsoft.com/en-us/research/articles/omniparser-v2-turning-any-llm-into-a-computer-use-agent/)
- [GitHub microsoft/OmniParser](https://github.com/microsoft/OmniParser)
- [HF microsoft/OmniParser-v2.0](https://huggingface.co/microsoft/OmniParser-v2.0)
- [MarkTechPost — Microsoft AI Releases OmniParser V2 (2025-02-18)](https://www.marktechpost.com/2025/02/18/microsoft-ai-releases-omniparser-v2-an-ai-tool-that-turns-any-llm-into-a-computer-use-agent/)
- [LearnOpenCV — OmniParser Vision-Based GUI Agent](https://learnopencv.com/omniparser-vision-based-gui-agent/)
- [DeepWiki — OmniParser Icon Detection and Captioning Models](https://deepwiki.com/microsoft/OmniParser/2.2-ocr-and-image-processing)
### Set-of-Marks
- [Yang et al. arXiv 2310.11441 — Set-of-Mark Prompting (2023)](https://arxiv.org/abs/2310.11441)
- [GitHub microsoft/SoM](https://github.com/microsoft/SoM)
### Modèles GUI natifs 2025
- [arXiv 2410.23218 — OS-ATLAS Foundation Action Model (ICLR 2025)](https://arxiv.org/abs/2410.23218)
- [OS-Atlas Homepage](https://osatlas.github.io/)
- [GitHub showlab/ShowUI (CVPR 2025)](https://github.com/showlab/ShowUI)
- [arXiv 2411.17465 — ShowUI Vision-Language-Action GUI Visual Agent](https://arxiv.org/abs/2411.17465)
- [Aguvis Project — ICML 2025](https://aguvis-project.github.io/)
- [arXiv 2412.04454 — Aguvis Unified Pure Vision Agents](https://arxiv.org/abs/2412.04454)
- [GitHub microsoft/GUI-Actor (NeurIPS 2025)](https://github.com/microsoft/GUI-Actor)
- [arXiv 2506.03143 — GUI-Actor Coordinate-Free Grounding](https://arxiv.org/abs/2506.03143)
- [GitHub microsoft/Magma (CVPR 2025)](https://github.com/microsoft/Magma)
- [arXiv 2502.13130 — Magma Foundation Model](https://arxiv.org/abs/2502.13130)
### Florence-2 (sous-jacent OmniParser icon_caption)
- [HF microsoft/Florence-2-large](https://huggingface.co/microsoft/Florence-2-large)
- [Microsoft Research — Florence-2 Unified Representation](https://www.microsoft.com/en-us/research/publication/florence-2-advancing-a-unified-representation-for-a-variety-of-vision-tasks/)
### Robustesse GUI agents
- [arXiv 2506.14477 — GUI-Robust Dataset](https://arxiv.org/abs/2506.14477)
### Computer use / pure vision
- [Skyvern — Browser Automation with LLM and Computer Vision](https://github.com/Skyvern-AI/skyvern)
- [Claude Computer Use — Anthropic Docs](https://docs.claude.com/en/docs/agents-and-tools/tool-use/computer-use-tool)
- [Tech Insider — Claude Computer Use Citrix Legacy Apps 2026](https://tech-insider.org/anthropic-claude-computer-use-agent-2026/)
### Référence interne
- `core/detection/som_engine.py` (316 lignes) — SomEngine YOLO + docTR déjà câblé
- `core/detection/omniparser_adapter.py` — wrapper Florence-2 caption
- `agent_v0/server_v1/resolve_engine.py:1083-1325` — voie `_resolve_set_of_marks` au replay
- `agent_v0/server_v1/stream_processor.py:607-700` — SoM au recording
- `docs/INSPIRATION_FRAMEWORKS_2026-05-10.md` §4 — vague d'inspirations frameworks RPA visuels
- `docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` §5.3 — note sur l'asymétrie OmniParser ↔ cascade
- `docs/MIGRATION_VLM_PLAN_2026-05-09.md` §2 — bug d'échelle bbox_2d (lien Scénario C)
- `docs/LESSONS_LEARNED_GHT_2026-05.md` §🔴 — 5 bugs P0 qui restent prioritaires sur tout ajout d'étage

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,507 @@
# AXE B1 — Refonte du transport replay + watchdog d'orphelins
**Date :** 2026-05-23
**Auteur :** Claude (recherche dispatchée, lecture seule sur code)
**Périmètre :** B1 (transport) + B3 (watchdog `_retry_pending`)
**Statut :** Étude — aucun changement de code. Pseudo-code prêt à coller.
> **Lecture pré-requise :** `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` (diagnostic 9 actions perdues en 33 s) et `docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` §4.
---
## 1. TL;DR — recommandation
**Migration cible : SSE (`sse-starlette`).** En complément immédiat, **watchdog `_retry_pending` indépendant** activable sous flag, gardé même après bascule SSE (ceinture+bretelles).
WebSocket est techniquement supérieur mais coûte 2× plus cher à mettre en place pour un gain marginal vu notre besoin (push serveur → client, l'upload events/screenshots reste sur les endpoints HTTP POST existants, déjà robustes). **L'asymétrie « push descendant lourd / upload léger »** colle exactement à SSE.
HTTP/2 server push **éliminé** : déprécié au niveau protocole, non supporté par uvicorn, non implémenté côté `requests` Python. Hors course en 2026.
**Effort estimé :**
- Watchdog seul : 2-4 h dev + 1 h test E2E. Déployable indépendamment.
- Endpoint SSE serveur + client + bascule progressive : 2 j dev + 1 j test sur Léa Windows + démo de non-régression.
- Total reco : **3-4 jours** pour aboutir à un transport robuste.
**Risque principal :** NoMachine 9.5.7 sur lien LAN peut intercaler un proxy implicite ou couper l'idle TCP. SSE expose `X-Accel-Buffering: no` et un `ping=15` qui couvrent ce cas — WebSocket exigerait un ping/pong applicatif explicite. Avantage SSE.
---
## 2. Table comparative
| Critère | Pull/Poll actuel | **SSE** | WebSocket | HTTP/2 push |
|---|---|---|---|---|
| **Reconnexion auto** | manuelle | **native (`EventSource`/`sseclient`)** + `Last-Event-ID` | code applicatif | non std |
| **Latence push** | 01 s (polling) + timeout 5 s | **<50 ms** | <50 ms | <50 ms |
| **Trafic** | 1 GET/s minimum, headers à chaque appel | **0 quand idle (juste ping 15 s)** | 0 quand idle (ping app) | minimal |
| **Détection déco client** | indirecte (échec POST report) | **`await request.is_disconnected()` immédiat** | `WebSocketDisconnect` exception | n/a |
| **Détection déco serveur** | timeout client | reconnect auto via EventSource/sseclient | ping/pong manuel | n/a |
| **Complexité serveur** | basse (mais bug doc'd) | **basse** (`EventSourceResponse` + asyncio.Queue) | moyenne (gestion connexions, locks, broadcast) | élevée (hypercorn req, peu testé) |
| **Complexité client Léa** | basse | **basse** (`sseclient-py` + boucle for) | moyenne (`websockets` lib + reconnect manuel) | non supporté `requests` |
| **Compat. proxy / NoMachine / NPM** | OK (HTTP standard) | **OK avec `X-Accel-Buffering: no`** + ping 15 s | OK si proxy autorise Upgrade headers | mauvais |
| **Compat. firewall entreprise** | excellent | **excellent (HTTP)** | bon (Upgrade) mais parfois bloqué | mauvais |
| **Auth Bearer token (existant)** | OK | **OK (header `Authorization`)** | OK (header initial) | OK |
| **Idempotence actions perdues** | non géré → bug 8 mai | gérée via ack POST + watchdog | gérée via ack ws + watchdog | n/a |
| **Ressources Python** | basses (1 req à la fois) | basses (1 connexion persistante par client) | basses (idem) | élevées (h2 stack) |
| **Maturité 2026** | éprouvé mais inadapté | **mature, recommandé par Anthropic/Skyvern docs** | mature | en déclin (deprecated HTTP/3) |
**Verdict :** SSE remporte sur tous les axes pertinents pour notre cas. WebSocket reste optionnel si on a un besoin futur de bidirectionnel synchrone (ex. validation interactive temps réel pendant un step). Pour l'instant : dispatch d'actions = unidirectionnel descendant, parfait pour SSE.
---
## 3. Patterns adoptés par les frameworks de référence (2025-2026)
### 3.1. Anthropic Computer Use SDK
**Architecture observée :** boucle **in-process** (pas de transport réseau entre orchestrateur et exécuteur — c'est le SDK qui exécute localement les `tool_use` retournés par le LLM). Pattern « Gather Context → Take Action → Verify Work → Repeat ». Référence : [`claude-quickstarts/computer-use-demo/computer_use_demo/loop.py`](https://github.com/anthropics/claude-quickstarts/blob/main/computer-use-demo/computer_use_demo/loop.py).
**Notre cas est différent** : agent distant Windows ≠ machine du LLM. Mais la philosophie « boucle d'ack avec screenshot après chaque action » est exactement ce qu'on a déjà côté `report_action_result`. À conserver.
### 3.2. OpenAI Operator / CUA
**Architecture observée :** cloud-based virtual browser ; le modèle retourne un `computer_call` que **le client SDK** exécute dans son environnement puis renvoie le résultat + screenshot via le prochain `messages.create`. Pas de queue côté serveur OpenAI — c'est l'orchestrateur client qui maintient l'état. Source : [Computer use docs](https://developers.openai.com/api/docs/guides/tools-computer-use).
→ Notre architecture serveur autoritaire (queue + replay_states) est légitime ; la « source de vérité » côté OpenAI est portée par le client, chez nous par le serveur. Différence de philosophie mais validée.
### 3.3. Skyvern (Planner-Actor-Validator)
**Architecture observée d'après docs publiques et structure repo** : `/skyvern/{cli, client, core, errors, forge, services, webeye}`. Le `core` orchestre, `webeye` exécute (Playwright). Communication interne via objets Python — **monolithe** côté serveur Skyvern. L'agent Skyvern parle au browser via CDP (websocket), pas une queue HTTP. Source : [github.com/Skyvern-AI/skyvern](https://github.com/Skyvern-AI/skyvern), [Skyvern 2.0 blog](https://www.skyvern.com/blog/skyvern-2-0-state-of-the-art-web-navigation-with-85-8-on-webvoyager-eval/).
**Pas un précédent direct** : Skyvern contrôle Chrome via CDP intra-machine. Notre Léa = poste Windows distant. Mais validation du loop Planner-Actor-Validator (déjà signalée comme convergence dans `INSPIRATION_FRAMEWORKS_2026-05-10.md`).
### 3.4. browser-use
**Architecture observée :** **WebSocket CDP direct** vers le navigateur (event-driven), pas de queue HTTP intermédiaire. Migration explicite de Playwright vers CDP brut pour réduire la latence. Source : [browser-use.com/posts/playwright-to-cdp](https://browser-use.com/posts/playwright-to-cdp), [cdp-use](https://github.com/browser-use/cdp-use).
**Précédent intéressant** : choix WebSocket pour un transport descendant. Mais leur cas est intra-machine (LLM ↔ Chrome local) ; le nôtre est inter-machine Linux ↔ Windows. Notre raison de préférer SSE (asymétrie push/upload) ne s'applique pas chez eux.
### 3.5. Playwright MCP
**Architecture observée :** modèle client/serveur, **transport stdio (local) OU HTTP/SSE (remote)**. Le MCP client (LLM) envoie des tool calls, le MCP server exécute Playwright et renvoie un snapshot de l'accessibility tree. Source : [Playwright MCP 2026 architecture](https://testquality.com/playwright-test-agents-mcp-architecture-2026/), [doc officielle](https://github.com/microsoft/playwright-mcp).
**Précédent direct et fort** : Microsoft a explicitement choisi **SSE pour le remote transport**. C'est le standard MCP. Notre cas (LLM/serveur Linux ↔ exécuteur Léa Windows) est isomorphe. Confirmation que SSE est la bonne route.
### 3.6. Cradle (Microsoft, agent jeu vidéo)
Agent monolithe local, pas de transport distant. Hors périmètre.
### 3.7. Synthèse patterns externes
**Pattern dominant 2025-2026 pour dispatcher des actions à un agent distant** = SSE quand asymétrique, WebSocket quand bidirectionnel. **Playwright MCP** = précédent le plus proche de notre cas → SSE.
---
## 4. Pseudo-code endpoint serveur (FastAPI + sse-starlette)
À placer en complément de `get_next_action` (sans la supprimer pendant la phase 3 de migration — coexistence sous flag). Bibliothèque : `pip install sse-starlette>=2.1`.
```python
# api_stream.py — nouveau bloc, à insérer près de get_next_action
import asyncio
from sse_starlette.sse import EventSourceResponse, ServerSentEvent
# Une file asyncio par (session_id, machine_id) — décorrélée de _replay_queues
# qui reste source de vérité de l'ordre des steps.
_sse_subscribers: dict[tuple[str, str], asyncio.Queue] = {}
_sse_lock = asyncio.Lock()
async def _sse_subscribe(session_id: str, machine_id: str) -> asyncio.Queue:
"""Crée ou récupère la queue de notifications SSE d'un client connecté."""
key = (session_id, machine_id)
async with _sse_lock:
if key not in _sse_subscribers:
_sse_subscribers[key] = asyncio.Queue(maxsize=64)
return _sse_subscribers[key]
async def _sse_unsubscribe(session_id: str, machine_id: str) -> None:
key = (session_id, machine_id)
async with _sse_lock:
_sse_subscribers.pop(key, None)
def sse_notify_new_action(session_id: str, machine_id: str) -> None:
"""À appeler chaque fois qu'une action visuelle est mise en queue.
Pousse un signal léger ; le serveur prépare ensuite l'action complète
(avec resolve serveur côté get_next_action) et la pousse au client.
Aucun ack ici — c'est le client qui POSTera /replay/result."""
key = (session_id, machine_id)
q = _sse_subscribers.get(key)
if q is None:
return # client pas (encore) connecté → restera dans _replay_queues
try:
q.put_nowait("dispatch")
except asyncio.QueueFull:
logger.warning("SSE queue pleine pour %s — drop signal", key)
@app.get("/api/v1/traces/stream/replay/events")
async def replay_events_sse(
request: Request,
session_id: str,
machine_id: str = "default",
):
"""Endpoint SSE — push d'actions au client Léa Windows.
Le client se connecte une fois, reste connecté tant que la session vit.
Chaque action prête à être exécutée arrive comme event JSON.
Heartbeat 15s : maintient la connexion à travers NPM/NoMachine/proxies.
Reconnexion automatique côté sseclient-py.
"""
queue = await _sse_subscribe(session_id, machine_id)
async def event_generator():
# Au connect : si des actions sont déjà en queue (cas reconnect),
# les pousser immédiatement avant d'attendre.
try:
# Drain initial : récupérer ce qui est déjà dans _replay_queues
# pour cette machine et l'envoyer immédiatement.
initial = await _drain_pending_actions(session_id, machine_id)
for action in initial:
yield ServerSentEvent(
data=json.dumps(action),
event="action",
id=action.get("action_id"),
)
_mark_retry_pending(action)
while True:
if await request.is_disconnected():
logger.info("SSE client %s/%s disconnect détecté",
session_id, machine_id)
break
try:
# Attendre une notification (timeout = laisser is_disconnected
# check passer). 5 s = compromis trafic / réactivité.
signal = await asyncio.wait_for(queue.get(), timeout=5.0)
except asyncio.TimeoutError:
continue # repart sur is_disconnected check
if signal == "shutdown":
break
# Récupérer la(les) action(s) à dispatcher.
# Réutilise toute la logique server-side existante (pause_for_human,
# extract_text, t2a_decision, condition…) — refactor `get_next_action`
# pour en extraire une coroutine `_resolve_next_visual_action()`.
action = await _resolve_next_visual_action(session_id, machine_id)
if action is None:
continue
yield ServerSentEvent(
data=json.dumps(action),
event="action",
id=action.get("action_id"),
)
_mark_retry_pending(action)
logger.info("[REPLAY] DISPATCH(SSE) action_id=%s",
action.get("action_id"))
except asyncio.CancelledError:
logger.info("SSE %s/%s cancelled (server shutdown)",
session_id, machine_id)
raise
finally:
await _sse_unsubscribe(session_id, machine_id)
return EventSourceResponse(
event_generator(),
ping=15, # ping toutes les 15 s (NPM/NoMachine OK)
ping_message_factory=lambda: ServerSentEvent(comment="hb"),
headers={"X-Accel-Buffering": "no"}, # bypass nginx/NPM buffer
)
```
**Points clés :**
1. `EventSourceResponse(ping=15)` → heartbeat « `: hb\n\n` » toutes les 15 s. Tient les proxies NPM et NoMachine ouverts (idle timeout typique 60 s).
2. `X-Accel-Buffering: no` → désactive le buffering nginx/NPM (sinon l'event reste bloqué côté reverse-proxy jusqu'à ~16 KB).
3. `request.is_disconnected()` → détection instantanée de déconnexion Léa (NoMachine freeze, redémarrage Windows). Le serveur libère immédiatement les ressources.
4. `id=action_id` sur chaque event → permet au client de demander `Last-Event-ID` au reconnect (`sseclient-py` le gère automatiquement). À combiner avec le watchdog § 6 pour garantir zéro perte.
5. Coexistence avec pull-poll : ajouter env `RPA_REPLAY_TRANSPORT=sse|poll` côté serveur ET côté client (rollback 1-ligne en cas de pépin démo).
**Hook côté queue producer** (à insérer là où `_replay_queues[session_id].append(action)` est appelé, p. ex. dans `start_replay`) :
```python
# Après chaque append d'action visuelle dans _replay_queues :
sse_notify_new_action(session_id, machine_id)
```
---
## 5. Pseudo-code client Léa Windows (remplace polling actuel)
À placer dans `agent_v0/agent_v1/network/streamer.py` ou un nouveau module `replay_subscriber.py`. Bibliothèque : `pip install sseclient-py>=1.8`.
```python
# replay_subscriber.py — boucle de réception SSE côté Léa
import json
import time
import requests
import sseclient
from ..config import API_TOKEN, STREAMING_ENDPOINT
RECONNECT_BACKOFF = [1.0, 2.0, 5.0, 10.0, 30.0]
class ReplaySubscriber:
def __init__(self, session_id, machine_id, on_action):
self.session_id = session_id
self.machine_id = machine_id
self.on_action = on_action # callback exécuteur
self.running = False
self._last_event_id = None
self._thread = None
def start(self):
self.running = True
self._thread = threading.Thread(target=self._run, daemon=True)
self._thread.start()
def stop(self):
self.running = False
def _run(self):
attempt = 0
while self.running:
try:
url = (
f"{STREAMING_ENDPOINT}/api/v1/traces/stream/replay/events"
f"?session_id={self.session_id}&machine_id={self.machine_id}"
)
headers = {
"Authorization": f"Bearer {API_TOKEN}",
"Accept": "text/event-stream",
"Cache-Control": "no-cache",
}
if self._last_event_id:
headers["Last-Event-ID"] = self._last_event_id
# IMPORTANT : pas de read_timeout — c'est tout l'intérêt SSE.
# Le ping serveur 15 s + le TCP keepalive OS suffisent.
# On garde un connect_timeout pour ne pas bloquer si serveur down.
resp = requests.get(
url, headers=headers, stream=True,
timeout=(10, None), # (connect, read) ; read=None = infini
)
if resp.status_code != 200:
raise RuntimeError(f"SSE refusé HTTP {resp.status_code}")
attempt = 0 # reset backoff dès qu'on a une connexion stable
client = sseclient.SSEClient(resp)
for event in client.events():
if not self.running:
break
if event.event == "action" and event.data:
action = json.loads(event.data)
if event.id:
self._last_event_id = event.id
# Exécuter via le callback (cascade replay existante)
try:
result = self.on_action(action)
except Exception as e:
logger.exception("on_action levé : %s", e)
result = {"success": False, "error": str(e)}
# Reporter résultat — endpoint inchangé
self._post_result(action, result)
# event.event == "ping" → ignorer, c'est le heartbeat
except (requests.ConnectionError, requests.Timeout, RuntimeError) as e:
if not self.running:
break
delay = RECONNECT_BACKOFF[min(attempt, len(RECONNECT_BACKOFF)-1)]
logger.warning("SSE déconnecté (%s) → retry dans %ss", e, delay)
time.sleep(delay)
attempt += 1
def _post_result(self, action, result):
"""Réutilise l'endpoint POST /replay/result existant (inchangé)."""
# … identique à _report_action_result actuel d'executor.py
```
**Compatibilité avec `agent_frozen` :** côté Léa cliente, l'ajout est un nouveau module (pas un patch de code chaud). Modifier `main.py` pour instancier `ReplaySubscriber` au lieu du polling, sous flag env `RPA_REPLAY_TRANSPORT`. Redéploiement SCP vers `dom@192.168.1.11` requis (cf. `feedback_scp_auto_modif_client_windows.md`).
---
## 6. Watchdog `_retry_pending` (à brancher MAINTENANT, indépendamment de SSE)
**Justification :** même avec SSE, un crash entre le `yield` serveur et le `POST /replay/result` client peut laisser une action dans `_retry_pending` sans report. Le watchdog est l'ultime filet.
**À insérer dans `api_stream.py`** au démarrage de l'app (event `startup`) :
```python
# api_stream.py — watchdog d'orphelins
_RETRY_WATCHDOG_INTERVAL_S = 10.0
_RETRY_ORPHAN_THRESHOLD_S = 30.0 # action sans report depuis 30 s → re-dispatch
_RETRY_MAX_RESENDS = 2 # éviter boucle infinie
async def _retry_pending_watchdog():
"""Re-dispatche les actions dispatched depuis > 30s sans report.
Idempotence garantie par report_action_result qui pop _retry_pending."""
while True:
try:
await asyncio.sleep(_RETRY_WATCHDOG_INTERVAL_S)
now = time.time()
orphans = []
# Snapshot pour éviter mutation concurrente
async with _async_replay_lock():
for aid, info in list(_retry_pending.items()):
dispatched_at = info.get("dispatched_at", 0)
resent = info.get("resent_count", 0)
if dispatched_at == 0:
continue
if now - dispatched_at < _RETRY_ORPHAN_THRESHOLD_S:
continue
if resent >= _RETRY_MAX_RESENDS:
logger.error(
"[BUS] lea:dispatch_orphan_giveup action_id=%s "
"resent=%d age=%.1fs",
aid, resent, now - dispatched_at,
)
_retry_pending.pop(aid, None)
continue
orphans.append((aid, info))
for aid, info in orphans:
action = info["action"]
session_id = action.get("session_id") or info.get("session_id")
if not session_id:
continue
async with _async_replay_lock():
q = _replay_queues.setdefault(session_id, [])
# Repush en TÊTE (sinon ordre des steps cassé)
q.insert(0, action)
info["resent_count"] = info.get("resent_count", 0) + 1
info["dispatched_at"] = 0 # sera reset au prochain DISPATCH
logger.warning(
"[BUS] lea:dispatch_orphan_resent action_id=%s "
"resent=%d session=%s",
aid, info["resent_count"], session_id,
)
# Si SSE actif : notifier le subscriber
machine_id = info.get("machine_id", "default")
sse_notify_new_action(session_id, machine_id)
except asyncio.CancelledError:
break
except Exception:
logger.exception("Watchdog _retry_pending levé — continue")
@app.on_event("startup")
async def _start_retry_watchdog():
if os.environ.get("RPA_RETRY_WATCHDOG_ENABLED", "1") == "1":
app.state._retry_watchdog_task = asyncio.create_task(
_retry_pending_watchdog()
)
logger.info("Watchdog _retry_pending démarré (orphan>%.0fs, every %.0fs)",
_RETRY_ORPHAN_THRESHOLD_S, _RETRY_WATCHDOG_INTERVAL_S)
```
**Modifications minimales requises** pour que le watchdog ait les bonnes infos :
1. Au point de dispatch (ligne ~3354 actuelle), ajouter `dispatched_at: time.time()` et `session_id` / `machine_id` dans `_retry_pending[action_id]`.
2. Dans `report_action_result` (ligne 3491), `pop` reste la clé d'idempotence — **aucun changement**, le code actuel fonctionne déjà parfaitement avec resends.
**Concurrence avec un report tardif :** si le client renvoie un report APRÈS le re-dispatch (race), le second `pop` retourne `None``report_action_result` répond `{"status": "no_active_replay"}` (ligne 3488) ou un retry de retry — tous les chemins sont déjà idempotents grâce au `pop`.
**Kill-switch :** `RPA_RETRY_WATCHDOG_ENABLED=0` (mode legacy). Pattern aligné sur QW1/QW2/QW4 (cf. `LESSONS_LEARNED_GHT_2026-05.md` §kill-switches).
---
## 7. Plan de migration en 3 étapes
### Étape 1 — Watchdog SEUL (2-4 h, déployable demain)
- Ajouter `_retry_pending_watchdog` + champs `dispatched_at/session_id/machine_id` dans `_retry_pending`.
- Flag `RPA_RETRY_WATCHDOG_ENABLED=1` par défaut, désactivable.
- Garder le client à `read_timeout=30` (déjà recommandé quick fix démo).
- **Effet immédiat :** plus aucune action perdue silencieusement, même sur le transport pull-poll actuel.
- Test : injecter un sleep 35 s avant `report_action_result` côté client → vérifier `lea:dispatch_orphan_resent` dans logs et que l'action ré-arrive.
### Étape 2 — Endpoint SSE serveur en parallèle (1 j)
- Ajouter `/api/v1/traces/stream/replay/events` (code §4).
- Refactoriser `get_next_action` pour extraire `_resolve_next_visual_action(session_id, machine_id)` réutilisable depuis le SSE (DRY — c'est la même cascade pause_for_human / extract_text / t2a_decision / clic conditionnel).
- Tests serveur : `pytest tests/integration/test_stream_processor.py` + nouveau `test_sse_dispatch.py` (connect, push, disconnect, ping, Last-Event-ID).
- Le pull-poll continue de tourner en parallèle (zéro impact démo).
### Étape 3 — Bascule client Léa (1 j + redéploiement SCP)
- Ajouter `replay_subscriber.py` côté agent_v1.
- Flag `RPA_REPLAY_TRANSPORT=sse|poll` côté client, valeur par défaut = `poll` pendant 1 semaine, puis bascule à `sse` après runs validés.
- SCP vers `dom@192.168.1.11` : `network/streamer.py`, `network/replay_subscriber.py`, `core/executor.py`, `main.py`.
- Test E2E sur Demo_urgence_3_db (46 steps) avec NoMachine freeze simulé (cf. `feedback_agent_frozen.md`) → vérifier reconnect SSE + Last-Event-ID résume sans perte.
- Si OK : flag par défaut `sse` ; le watchdog reste actif comme filet.
---
## 8. Risques et tests E2E
### Risques techniques
| Risque | Mitigation |
|---|---|
| NoMachine 9.5.7 coupe la connexion idle même avec ping 15 s | `ping=10` au lieu de 15, et `tcp_keepalive` côté socket Python (`setsockopt SO_KEEPALIVE`) |
| NPM reverse-proxy bufferise SSE | `X-Accel-Buffering: no` + vérifier `proxy_buffering off` dans la conf NPM `lea.labs.laurinebazin.design` |
| Léa Windows freeze longue (>2 min) → SSE socket morte mais OS pense vivante | watchdog côté serveur tue la connexion si pas d'ack `report_action_result` reçu depuis 60 s (à ajouter) |
| Double-dispatch (race watchdog + reconnect Last-Event-ID) | idempotence côté client : `if action_id in self._processed: skip` (set bounded LRU 256) |
| Gemma cloud 503 (vécu 12 mai) bloque t2a_decision >> 30 s | watchdog re-pushe → mais le 2e essai re-bloque. Plafond `_RETRY_MAX_RESENDS=2` puis abandon → pause supervisée |
| Drift exemption template ≥0.95 / hybrid ≥0.80 (contournement actif) | aucun impact — c'est une logique de resolve, pas de transport |
| Fallback heartbeat capture <1200×800 (contournement) | aucun impact — c'est sur l'upload, pas le dispatch |
### Tests E2E à passer avant bascule
1. **Smoke** : démarrer replay 5 steps, vérifier dispatch SSE et arrivée chez client.
2. **Long action serveur** : step avec `extract_text` 8 s puis `click` — l'action `click` doit arriver SANS perte (le test 8 mai en a perdu 9).
3. **Déconnexion brutale** : `taskkill /F /IM python.exe` côté Léa puis relancer → SSE reconnect + Last-Event-ID résume sans re-dispatcher les actions déjà acquittées.
4. **NoMachine freeze simulé** : couper VPN 90 s → reconnect, vérifier que les actions empilées arrivent en rafale propre.
5. **Watchdog isolé** : passer `RPA_REPLAY_TRANSPORT=poll` + `RPA_RETRY_WATCHDOG_ENABLED=1`, faire dropper le report manuellement (sleep 35 s avant POST `/replay/result`) → vérifier resend + idempotence.
6. **Démo complète Demo_urgence_3_db** (46 steps, MOREL Catherine UHCD) : 0 action perdue, comparaison logs avant/après.
### Liens avec autres axes
- **AXE B2 (Validator)** : un Validator strict (vérif sémantique post-clic) n'a de sens que si on est sûr que toutes les actions arrivent. **B1 est prérequis de B2.**
- **AXE B4 (ORA — Observe Reason Act)** : ORA pousse des actions dans la queue exactement comme le replay classique. Le SSE bénéficie à ORA gratuitement (pas de refacto supplémentaire). **B1 dé-risque B4.**
---
## 9. Sources
### SSE / FastAPI
- [sse-starlette GitHub](https://github.com/sysid/sse-starlette) — référence implémentation
- [sse-starlette ping interval issue #16](https://github.com/sysid/sse-starlette/issues/16)
- [FastAPI tutorial SSE](https://fastapi.tiangolo.com/tutorial/server-sent-events/)
- [Real-Time Notifications Python FastAPI SSE](https://medium.com/@inandelibas/real-time-notifications-in-python-using-sse-with-fastapi-1c8c54746eb7)
- [Stop streaming response when client disconnects](https://github.com/fastapi/fastapi/discussions/7572)
- [Server-Sent Events Beat WebSockets for 95% of Real-Time Apps](https://dev.to/polliog/server-sent-events-beat-websockets-for-95-of-real-time-apps-heres-why-a4l)
### WebSocket / FastAPI
- [FastAPI WebSockets doc](https://fastapi.tiangolo.com/advanced/websockets/)
- [Weaponizing Real Time FastAPI](https://blog.greeden.me/en/2025/10/28/weaponizing-real-time-websocket-sse-notifications-with-fastapi-connection-management-rooms-reconnection-scale-out-and-observability/)
- [WebSocket Heartbeat Ping/Pong](https://websocket.org/guides/heartbeat/)
- [Handling WebSocket Disconnections FastAPI](https://hexshift.medium.com/handling-websocket-disconnections-gracefully-in-fastapi-9f0a1de365da)
### HTTP/2 status Python
- [Uvicorn HTTP/2 Issue #47](https://github.com/Kludex/uvicorn/issues/47) — non supporté
- [Gunicorn HTTP/2 guide](https://gunicorn.org/guides/http2/) — server push deprecated
- [The Three Python ASGI Servers](https://dev.to/bowmanjd/the-three-python-asgi-servers-5447)
### Frameworks externes
- [Anthropic computer-use-demo loop.py](https://github.com/anthropics/claude-quickstarts/blob/main/computer-use-demo/computer_use_demo/loop.py)
- [OpenAI Computer Use docs](https://developers.openai.com/api/docs/guides/tools-computer-use)
- [OpenAI Operator Explained](https://anchorbrowser.io/blog/how-openai-operator-works-with-ai-agents)
- [Skyvern GitHub](https://github.com/Skyvern-AI/skyvern)
- [Skyvern 2.0 architecture blog](https://www.skyvern.com/blog/skyvern-2-0-state-of-the-art-web-navigation-with-85-8-on-webvoyager-eval/)
- [browser-use Playwright to CDP](https://browser-use.com/posts/playwright-to-cdp)
- [browser-use cdp-use repo](https://github.com/browser-use/cdp-use)
- [Playwright MCP 2026 architecture](https://testquality.com/playwright-test-agents-mcp-architecture-2026/)
### Client SSE Python
- [sseclient-py PyPI](https://pypi.org/project/sseclient/)
- [requests-sse PyPI](https://pypi.org/project/requests-sse/)
- [LaunchDarkly Python SSE client](https://launchdarkly-sse-client-library.readthedocs.io/en/latest/)
### Patterns retry / idempotence / orphans
- [ARQ retry doc](https://arq-docs.helpmanual.io/)
- [Building Resilient Task Queues FastAPI ARQ](https://davidmuraya.com/blog/fastapi-arq-retries/)
- [Queue-Based Exponential Backoff](https://dev.to/andreparis/queue-based-exponential-backoff-a-resilient-retry-pattern-for-distributed-systems-37f3)
- [Interrupted Asynchronous Task Problem](https://medium.com/picus-security-engineering/the-interrupted-asynchronous-task-problem-and-solution-with-python-rq-435f1a597631)
### Proxy / NoMachine
- [SSE vs WebSocket Agent Readiness](https://agenthermes.ai/blog/sse-websocket-agent-readiness)
- [Troubleshooting SSE Multi-Service](https://medium.com/@wang645788/troubleshooting-server-sent-events-sse-in-a-multi-service-architecture-5084ce155ea0)
---
*Document destiné à servir de base de décision avant chiffrage final et implémentation. Lecture seule sur le code, aucune modification. À discuter avec Dom avant toute bascule.*

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,817 @@
# AXE B2 — Pattern Planner-Actor-Validator & validation sémantique post-action
**Date :** 2026-05-23
**Auteur :** agent recherche dispatché (Claude Opus 4.7 1M)
**Statut :** livrable de recherche, lecture seule, AUCUNE modification de code
**Lien dépendances :** AXE_A4 (OCR), AXE_A5 (tokenisation écran — déjà rédigé), AXE_B4 (ORA observe_reason_act)
---
## 1. TL;DR + recommandation
**Constat.** Skyvern (12k stars, SOTA 85.85 % WebVoyager) formalise le **Validator** comme un agent à part entière, séparé du Planner et de l'Actor. Son rôle : après chaque step, prendre une nouvelle capture, demander à un LLM (avec image + DOM élagué) si l'objectif courant est atteint, sinon renvoyer `continue` / `terminate`. C'est exactement ce qui manque à rpa_vision_v3 : VWB = Planner statique, Léa = Actor, et `replay_verifier.py` est un pixel-diff global qui n'a aucune notion de **sémantique** (« est-ce que l'onglet Imagerie de l'app Easily est maintenant actif ? »).
Le bug archétype step 10 démo GHT (« Imagerie » cliqué dans le bandeau Edge, REPORT success=True) tient **uniquement** à cette absence : pHash global voit du mouvement → conclut OK. Un Validator visuel par step le détecterait en 1-3 s.
**Recommandation design pour rpa_vision_v3** (justifiée §6, §9) :
1. **Garder** `replay_verifier.verify_action` (pixel) comme pré-filtre 10 ms.
2. **Réactiver et étendre** `verify_with_critic` déjà câblé (§6) en lui passant un `expected_result` **typé** par action.
3. **Ajouter un `Validator` pluggable** côté serveur, qui choisit la stratégie de check selon `action_type` (matrice §5). Implémentation Python = ~250 LOC.
4. **Pour le bug step 10 précisément** : `click_anchor` doit déclencher une vérif OCR-ROI **autour du point cliqué** (rayon 60 px) ET une vérif title-bar (déjà fait par `core/grounding/title_verifier.py`). Si la ROI contient le mot Edge / le mot URL / un domaine `.com`, c'est un faux clic → retry, pas continue.
5. **Latence cible** : pixel 10 ms, OCR-ROI 100 ms, LLM-judge 2-3 s. Ne lancer le LLM-judge que si pixel **OU** OCR-ROI suspect.
Le pattern Skyvern est directement adoptable. Le code Skyvern (Python, AGPL-3.0) montre que le Validator c'est **5 prompts Jinja2 + 1 méthode `complete_verify` + 1 dataclass `CompleteVerifyResult`**. Pas plus.
---
## 2. Skyvern Validator détaillé (code source 23 mai 2026)
### 2.1. Méthode `complete_verify` (extraite verbatim de `skyvern/forge/agent.py:2609-2730`)
Source : <https://github.com/Skyvern-AI/skyvern/blob/main/skyvern/forge/agent.py#L2609>
Le Validator chez Skyvern n'est pas un sous-processus exotique : c'est **une coroutine LLM** appelée après l'Actor, à chaque step où il n'y a pas déjà une `DecisiveAction` (= action terminale émise par l'Actor lui-même).
```python
# skyvern/forge/agent.py (résumé condensé du flux)
async def complete_verify(
self, page: Page, scraped_page: ScrapedPage, task: Task, step: Step
) -> CompleteVerifyResult:
# 1. RE-SCRAPE la page (DOM élagué + screenshots), pas la version utilisée par l'Actor
scraped_page_refreshed = await scraped_page.refresh(draw_boxes=False, scroll=scroll)
# 2. Construit le prompt avec : navigation_goal, payload, complete_criterion,
# action_history, elements parsés, datetime
template_name = "check-user-goal-with-termination" if use_termination_prompt else "check-user-goal"
verification_prompt = load_prompt_with_elements(
element_tree_builder=scraped_page_refreshed,
template_name=template_name,
navigation_goal=task.navigation_goal,
navigation_payload=task.navigation_payload,
complete_criterion=task.complete_criterion,
terminate_criterion=task.terminate_criterion,
action_history=actions_and_results_str,
local_datetime=...,
)
# 3. Appel LLM avec screenshots — un handler LLM dédié possible
# via flag PostHog USE_CHECK_USER_GOAL_HANDLER_FOR_VERIFICATION
verification_result = await llm_api_handler(
prompt=verification_prompt,
step=step,
screenshots=scraped_page_refreshed.screenshots,
prompt_name=prompt_name,
)
# 4. Parse JSON strict → 3 verdicts possibles
result = CompleteVerifyResult.model_validate(verification_result)
if result.is_complete:
verification_status = VerificationStatus.complete
elif result.is_terminate:
verification_status = VerificationStatus.terminate
else:
verification_status = VerificationStatus.continue_step
# 5. Trace OTEL : verification.status, verification.template, verification.reasoning_kind
span.set_attribute("verification.status", verification_status.value)
record_verification_span_attrs(span, result.thoughts)
return result
```
**Trois verdicts uniquement** : `complete` / `terminate` / `continue_step`. Pas de `success_partial` ni de `retry_silent`. C'est volontaire : la décision est forcée binaire.
Le `check_user_goal_complete` (lignes 2736+) wrap `complete_verify` et le convertit en `CompleteAction` ou `TerminateAction` pour l'orchestrateur.
### 2.2. Le prompt `check-user-goal.j2` (verbatim, fetch direct du repo)
Source : <https://raw.githubusercontent.com/Skyvern-AI/skyvern/main/skyvern/forge/prompts/skyvern/check-user-goal.j2>
```jinja
Your are here to help the user determine if the user has completed their goal on the web{{ " according to the complete criterion" if complete_criterion else "" }}. Use the content of the elements parsed from the page,{{ "" if without_screenshots else " the screenshots of the page," }} the user goal and user details to determine whether the {{ "complete criterion has been met" if complete_criterion else "user goal has been completed" }} or not.
Make sure to ONLY return the JSON object in this format with no additional text before or after it:
{
"page_info": str, // Think step by step. Describe all the useful information in the page related to the user goal.
"thoughts": str, // Think step by step. What information makes you believe whether user goal has completed or not. Use information you see on the site to explain.
"user_goal_achieved": bool // True if the user goal has been completed, false otherwise.
}
User Goal:
{{ navigation_goal }}
User Details:
{{ navigation_payload }}
Action History:
{{ action_history }}
Elements on the page:
{{ elements }}
Current datetime, ISO format:
{{ local_datetime }}
```
**Points clés** :
- Sortie JSON stricte, parsée par Pydantic `CompleteVerifyResult.model_validate`.
- Trois infos données au modèle : (a) screenshots, (b) elements parsés du DOM, (c) action_history textuelle. Multi-modal.
- Le `page_info``thoughts``user_goal_achieved` impose une chain-of-thought structurée. C'est ce qui rend l'erreur diagnosticable.
### 2.3. Le prompt `check-user-goal-with-termination.j2` (expérimental, verbatim)
Source : <https://raw.githubusercontent.com/Skyvern-AI/skyvern/main/skyvern/forge/prompts/skyvern/check-user-goal-with-termination.j2>
Ajoute un 3e statut explicite `terminate` + une **classification des échecs** en 12 catégories :
```jinja
"status": str, // Must be one of three values: "complete", "terminate", or "continue".
"failure_categories": array // Only populate when status is "terminate". Classify the root cause.
[{
"category": str, // ANTI_BOT_DETECTION | BROWSER_ERROR | NAVIGATION_FAILURE |
// PAGE_LOAD_TIMEOUT | AUTH_FAILURE | LLM_REASONING_ERROR |
// CREDENTIAL_ERROR | ELEMENT_NOT_FOUND | WRONG_PAGE_STATE |
// DATA_EXTRACTION_FAILURE | INFRASTRUCTURE_ERROR | UNKNOWN
"confidence_float": float,
"reasoning": str
}]
Important: Think carefully about the difference between "terminate" and "continue":
- "terminate" = impossible to achieve, stop trying
- "continue" = not done yet, but achievable with more steps
```
**À retenir** : Skyvern est très conservateur sur `terminate` (« only when CLEAR, EXPLICIT, UNAMBIGUOUS evidence »). C'est aligné avec le feedback `feedback_failure_is_learning.md` de Dom : échec ≠ stop avec erreur, c'est pause supervisée.
### 2.4. Quand le Validator se déclenche
Extrait `agent.py:1929-1971` :
```python
enable_parallel_verification = False
if (
not has_decisive_action # l'Actor n'a pas déjà émis un COMPLETE
and not task_completes_on_download
and not isinstance(task_block, ActionBlock)
and complete_verification # flag global activable par-task
and (task.navigation_goal or task.complete_criterion)
):
# Géré par feature flag PostHog
disable_user_goal_check = await app.EXPERIMENTATION_PROVIDER.is_feature_enabled_cached(
"DISABLE_USER_GOAL_CHECK",
task.task_id,
...
)
enable_parallel_verification = not disable_user_goal_check
```
**Le Validator tourne à CHAQUE step** par défaut (« deferred to handle_completed_step »). C'est désactivable par task ou globalement, mais l'état par défaut est ON. Skyvern accepte le coût LLM par step parce qu'un faux succès rend l'agent inutilisable.
### 2.5. Contrat de données
```python
# skyvern/forge/sdk/schemas/tasks.py (déduit du code agent.py)
class CompleteVerifyResult(BaseModel):
page_info: str
thoughts: str
is_complete: bool
is_terminate: bool = False
status: str | None = None # "complete" | "terminate" | "continue"
failure_categories: list[FailureCategory] = []
```
### 2.6. Latence et coût
D'après le post Skyvern 2.0 (<https://www.skyvern.com/blog/skyvern-2-0-state-of-the-art-web-navigation-with-85-8-on-webvoyager-eval/>) :
- Un step moyen prend 2-10 s.
- Validator = appel LLM séparé (souvent un GPT-4o-mini ou Claude Haiku), 1-3 s.
- ROI = sans Validator, accuracy 68.7 % WebVoyager ; avec Validator, **85.85 %**. Le delta de +17 points en accuracy justifie largement la latence.
Source : <https://browser-use.com/posts/our-browser-agent-evaluation-system> (browser-use rapporte +17 pts également : 45 → 68.7 → 85.85 selon Planner/Validator).
---
## 3. Tour d'horizon Validator dans 5 autres frameworks
### 3.1. OpenAdapt — Evaluation-Driven Feedback
Source : <https://github.com/OpenAdaptAI/OpenAdapt/wiki/OpenAdapt-Architecture-(draft)>, <https://github.com/OpenAdaptAI/openadapt-evals>.
OpenAdapt formalise le concept au niveau **Process Graph** (graphe de steps avec arêtes = critères de complétion) :
- **Code-based validation** : LLM génère du Python qui vérifie une condition d'état (présence d'un message de confirmation, état d'un bouton, etc.). Code stocké, ré-exécuté à chaque replay.
- **Model-based validation** : LMM (Large Multimodal Model) reçoit le screenshot courant + `completion_criteria` formulés en langage naturel → bool.
Particularité : si la validation échoue, OpenAdapt **bascule en mode recording** automatiquement → l'utilisateur démontre la suite → la trace devient training data. C'est l'« Evaluation-Driven Feedback ». Le sous-package `openadapt-evals` expose `evaluate_agent_on_benchmark`.
### 3.2. browser-use — agentic judge
Source : <https://browser-use.com/posts/our-browser-agent-evaluation-system>, <https://github.com/browser-use/browser-use>.
- LLM judge intégré dans le code agent, **tourne après `done`** ET « can also double as a real-time validation layer during regular use ».
- Modèle : `gemini-2.5-flash`. Accuracy juge vs labels humains : 87 %.
- Sortie JSON stricte :
```json
{
"reasoning": "Analysis covering what worked, failures, trajectory quality, tool usage, output quality",
"verdict": "true|false",
"failure_reason": "Max 5 sentences explanation if failed",
"impossible_task": "true|false",
"reached_captcha": "true|false"
}
```
- Philosophie : **simple prompts and absolute True/False verdicts work best**. Complex rubrics → indecisive judging.
### 3.3. Anthropic Computer Use
Source : <https://docs.anthropic.com/en/docs/build-with-claude/computer-use>.
Anthropic CU n'a pas de Validator nommé. Boucle minimaliste : `screenshot → action → screenshot → ...` jusqu'à ce que Claude lui-même décide qu'il a fini. **Validation = self-reflection implicite du modèle dans son raisonnement**.
→ Acceptable parce que Claude est puissant. **Pas applicable à rpa_vision_v3** où l'Actor n'est pas un LLM agentique mais un exécutant déterministe (Léa). Il faut un Validator externe.
### 3.4. OpenAI Operator / CUA
Source : <https://openai.com/index/operator-system-card/>.
Idem Anthropic CU : pas de Validator séparé. Le modèle CUA fait perception → reasoning → action en boucle. Selon le system card : « If it encounters challenges or makes mistakes, Operator can leverage its reasoning capabilities to self-correct ». Pas formalisé.
OpenCUA (open-source, <https://opencua.xlang.ai/>) entraîne avec « reflective Chain-of-Thought reasoning » mais pas de check externe.
### 3.5. Cradle (BAAI, Kunlun Tech) — Self-Reflection module
Source : <https://github.com/BAAI-Agents/Cradle>, <https://arxiv.org/pdf/2403.03186>.
Cradle décompose explicitement en 6 modules dont **Self-Reflection** :
> « Through this module, the agent assesses previous actions to understand their outcomes, evaluate successes or failures, and adjust behavior accordingly. »
Mesure : +20.41 points sur tâches « professional domain » vs baselines. Mais c'est un agent jeu/applications, pas RPA déclaratif → moins directement transposable.
### 3.6. Tableau récap
| Framework | Validator nommé ? | Modalité | Modèle | Latence | Verdict format |
|---|---|---|---|---|---|
| Skyvern 2.0 | **Oui** (`complete_verify`) | VLM + DOM élagué | GPT-4o ou handler dédié | 1-3 s | JSON `is_complete/is_terminate/status` |
| OpenAdapt | Oui (Process Graph) | LMM ou Python généré | Configurable | n/a | bool + falls back to recording |
| browser-use | Oui (agentic judge) | VLM + DOM | gemini-2.5-flash | 1-2 s | JSON `verdict/failure_reason` |
| Anthropic CU | Non (implicite) | Self-reflection | Claude lui-même | inclus | continuation libre |
| OpenAI Operator | Non (implicite) | Self-reflection | CUA | inclus | continuation libre |
| Cradle | Oui (Self-Reflection) | LMM | GPT-4V | 2-5 s | text reasoning |
**Convergence forte** : les 3 frameworks RPA matures (Skyvern, OpenAdapt, browser-use) ont un Validator **explicite, JSON-strict, multi-modal (VLM + structure DOM)**. Les agents généralistes (CU, Operator) délèguent au LLM agentique. Pour rpa_vision_v3 avec Actor déterministe = camp Skyvern.
---
## 4. Taxonomie des approches de validation post-action
| Approche | Coût | Précision | Faux-positifs | Quand l'utiliser |
|---|---|---|---|---|
| **A. LLM-as-judge (full VLM)** | 1-5 s | Très haute (sémantique) | Faibles | Validation finale de step / cas ambigus |
| **B. OCR ROI** (texte attendu autour du clic) | 80-200 ms | Haute si texte connu | Sensible OCR errors | Tabs, boutons, libellés |
| **C. OCR title-bar** (titre fenêtre) | ~120 ms (déjà câblé) | Moyenne | Bruit OCR sur petits crops | Navigation fenêtre / ouverture appli |
| **D. Visual diff pHash global** | 10 ms | Très basse (juste « ça a bougé ») | Énormes | Pré-filtre `nothing-happened` |
| **E. Visual diff pHash ROI** | 20 ms | Moyenne | Moyens | Détection focus tab (changement souligné) |
| **F. CLIP features cos-sim** | 50-200 ms | Moyenne | Confond visuellement proches | Reconnaissance d'écran connu |
| **G. DINOv2 features** | 100-300 ms | Haute (self-supervised, plus robuste que CLIP) | Faibles | Comparaison patches précis |
| **H. LPIPS** | 100 ms | Haute (perceptual) | Moyens | Vérif après animations / transitions |
| **I. Window-focus check** (win32 API ou OCR titlebar) | <50 ms | Très haute | Quasi nuls | Vérif que la bonne app est devant |
| **J. Dialog presence detect** | OCR + template | Très haute | Faibles | Détection popups bloquantes |
| **K. JSON schema validation** (extraction) | <10 ms | Déterministe | nuls | `extract_text`, `t2a_decision` |
**Source visual diff** : <https://wopee.io/blog/screenshot-comparison-algorithms-visual-testing/> — pHash est positionné comme « pre-filter, not a comparator ». Les VLM sont positionnés comme « triage layer on top of pixel diffs, not as the comparator itself ». Exactement le design pixel→sémantique déjà câblé dans `replay_verifier.verify_with_critic`.
**Pour DINOv2 / LPIPS / CLIP** : sources <https://github.com/facebookresearch/dinov2>, <https://medium.com/aimonks/clip-vs-dinov2-in-image-similarity-6fa5aa7ed8c6>. DINOv2 produit des features visuelles plus discriminantes que CLIP pour comparer deux crops d'UI (CLIP est entraîné texte↔image, pas pour le pixel-perfect).
---
## 5. Matrice type d'action → check recommandé pour rpa_vision_v3
Aligné avec `reference_vwb_action_types.md` (memory) et `_ALLOWED_ACTION_TYPES` de `replay_engine.py`.
| Action VWB (Léa) | Check primaire | Check secondaire (si primaire ambigu) | Budget latence |
|---|---|---|---|
| `click_anchor``click` | **B. OCR ROI** (rayon 60 px) + **I. Window focus** | A. LLM-as-judge si OCR ne trouve pas le label | 100 ms + 2 s si escalation |
| `double_click_anchor``click button="double"` | **C. OCR title-bar** (déjà câblé) + **B. OCR ROI** | A. LLM-as-judge | 200 ms + 2 s |
| `right_click_anchor``click button="right"` | **J. Dialog presence** (menu contextuel attendu) | B. OCR ROI sur menu | 150 ms |
| `type_text``type` | **B. OCR ROI** : le texte tapé est-il visible dans la ROI ? | A. LLM-as-judge si texte tronqué | 100 ms |
| `type_secret` | **D. pHash ROI** (vérifier qu'un input s'est rempli, pas le contenu) | — | 20 ms |
| `keyboard_shortcut``key_combo` | **C. OCR title-bar** OU **J. Dialog presence** selon raccourci | A. LLM-as-judge en cas de doute | 200 ms |
| `scroll_to_anchor``scroll` | **F. CLIP cos-sim** before/after ROI cible visible | D. pHash global change ≠ 0 | 100 ms |
| `wait_for_anchor``wait` | **B. OCR ROI** : l'ancre est-elle visible ? | A. LLM-as-judge | 100 ms |
| `extract_text` | **K. JSON schema** : type str, longueur > 0, langue fr ratio | A. LLM-as-judge sur le contenu plausibilité | 10 ms + 2 s si plausibilité requise |
| `extract_text_scroll` | K + **A. LLM-as-judge** si plusieurs pages | — | 10 ms + 2 s |
| `extract_table` | **K. JSON schema** : ≥ 1 row, headers attendus si fournis | A. LLM-as-judge | 10 ms |
| `screenshot_evidence` | — (action passive) | I. Window focus | <50 ms |
| `t2a_decision` | **K. JSON schema** strict (decision ∈ {UHCD, FORFAIT, NA}, JSON parseable) | — | 10 ms |
| `pause_for_human` | **Checklist QW4** (déjà fait, `SafetyChecksProvider`) | — | n/a |
| `db_save_data` | **K. Schema row sauvée** (SELECT verify) | — | <50 ms |
| `import_excel`, `db_read_data` | **K. Schema rows** | — | <50 ms |
| `visual_condition` | **A. LLM-as-judge** sur la condition formulée | — | 2 s |
| `ai_ocr`, `ai_summarize`, etc. | **K. JSON schema** + **A. plausibilité** | — | 10 ms + 2 s |
**Principe directeur** : la plupart des actions ont un check pas-cher (OCR ROI, JSON) qui suffit dans 90 % des cas. Le LLM-as-judge (2 s) ne tire qu'en escalation, ou sur les actions à risque élevé (`click_anchor` sur cibles ambiguës, `t2a_decision`, `visual_condition`).
---
## 6. Design d'un Validator pluggable — code copy-paste-ready
### 6.1. Interface
À placer dans `agent_v0/server_v1/validator.py` (nouveau fichier, complète `replay_verifier.py` existant) :
```python
# agent_v0/server_v1/validator.py
"""
Validator — vérification sémantique post-action pluggable.
Inspiré de Skyvern (Planner-Actor-Validator). Combine pixel-diff existant
(replay_verifier.py) avec une couche sémantique typée par action_type.
Trois verdicts possibles, calque sur Skyvern :
- COMPLETE → l'action a eu l'effet voulu, passer au step suivant
- CONTINUE → l'effet n'est pas encore visible, re-vérifier après wait
- TERMINATE → l'action a échoué de manière irrécupérable (pause supervisée)
"""
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Callable, Dict, Optional, Protocol
logger = logging.getLogger(__name__)
class Verdict(str, Enum):
COMPLETE = "complete"
CONTINUE = "continue"
TERMINATE = "terminate"
class FailureCategory(str, Enum):
WRONG_TARGET = "wrong_target" # cliqué ailleurs (ex. bug step 10)
NO_VISUAL_CHANGE = "no_visual_change" # action sans effet
UNEXPECTED_DIALOG = "unexpected_dialog" # popup bloque
WRONG_APPLICATION = "wrong_application" # focus sur mauvaise app (Edge vs Easily)
OCR_TEXT_MISSING = "ocr_text_missing" # texte attendu absent
SCHEMA_INVALID = "schema_invalid" # JSON/extract invalide
UNKNOWN = "unknown"
@dataclass
class ValidationResult:
verdict: Verdict
confidence: float # 0.0-1.0
check_used: str # "ocr_roi" | "llm_judge" | "title_bar" | ...
elapsed_ms: float
reasoning: str = ""
failure_category: Optional[FailureCategory] = None
raw_evidence: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
return {
"verdict": self.verdict.value,
"confidence": round(self.confidence, 3),
"check_used": self.check_used,
"elapsed_ms": round(self.elapsed_ms, 1),
"reasoning": self.reasoning,
"failure_category": self.failure_category.value if self.failure_category else None,
"raw_evidence": self.raw_evidence,
}
class ActionChecker(Protocol):
"""Contrat d'un checker spécifique par action_type."""
name: str
budget_ms: float
def check(
self,
action: Dict[str, Any],
result: Dict[str, Any],
screenshot_before: Optional[str],
screenshot_after: Optional[str],
context: Dict[str, Any],
) -> ValidationResult: ...
class Validator:
"""Orchestrateur : route action_type → checker, gère l'escalation."""
def __init__(
self,
checkers: Dict[str, list[ActionChecker]],
default_checker: ActionChecker,
escalation_checker: Optional[ActionChecker] = None,
escalation_threshold: float = 0.5,
):
"""
checkers: mapping action_type → liste de checkers à essayer en ordre.
default_checker: fallback si action_type pas dans le mapping.
escalation_checker: typiquement un LLM-as-judge, lancé si confidence < seuil.
"""
self._checkers = checkers
self._default = default_checker
self._escalation = escalation_checker
self._escalation_threshold = escalation_threshold
def validate(
self,
action: Dict[str, Any],
result: Dict[str, Any],
screenshot_before: Optional[str] = None,
screenshot_after: Optional[str] = None,
context: Optional[Dict[str, Any]] = None,
) -> ValidationResult:
context = context or {}
action_type = action.get("type", "")
candidates = self._checkers.get(action_type, [self._default])
last_result: Optional[ValidationResult] = None
for checker in candidates:
res = checker.check(action, result, screenshot_before, screenshot_after, context)
last_result = res
# Si verdict net + confiance haute → renvoyer
if res.confidence >= self._escalation_threshold and res.verdict != Verdict.CONTINUE:
return res
# Escalation LLM-as-judge si fourni
if self._escalation and last_result and last_result.confidence < self._escalation_threshold:
logger.info(
"Validator escalation LLM-judge (last_conf=%.2f, check=%s)",
last_result.confidence, last_result.check_used,
)
esc = self._escalation.check(action, result, screenshot_before, screenshot_after, context)
# On combine : si LLM contredit, LLM prime (sa confiance est bornée à 0.9)
return esc
return last_result or ValidationResult(
verdict=Verdict.CONTINUE,
confidence=0.3,
check_used="no_checker",
elapsed_ms=0.0,
reasoning="Aucun checker n'a produit de verdict",
)
```
### 6.2. Exemple de checker : `OcrRoiChecker` (pour click_anchor)
```python
# agent_v0/server_v1/checkers/ocr_roi.py
import time
from typing import Any, Dict, Optional
from PIL import Image
from agent_v0.server_v1.validator import (
ActionChecker, ValidationResult, Verdict, FailureCategory,
)
class OcrRoiChecker:
"""Vérifie que le texte attendu apparaît dans la ROI autour du clic.
Spécifiquement conçu pour résoudre le bug step 10 :
si on a cliqué sur 'Imagerie', la ROI 60px doit contenir 'Imagerie'.
Si elle contient 'Edge' ou 'urgence.labs.laurinebazin.design',
on a cliqué dans le bandeau navigateur → failure.
"""
name = "ocr_roi"
budget_ms = 200.0
# Mots suspects = on a cliqué hors-app
SUSPECT_TOKENS = {"edge", "chrome", "firefox", "http", "https", ".com", ".fr",
"favoris", "favorite", "onglet", "tab "}
def __init__(self, ocr_fn, radius_px: int = 60):
self._ocr = ocr_fn # callable(PIL.Image) -> str
self._radius = radius_px
def check(self, action, result, screenshot_before, screenshot_after, context) -> ValidationResult:
t0 = time.time()
expected_text = action.get("by_text") or context.get("expected_text", "")
x_pct = action.get("x_pct")
y_pct = action.get("y_pct")
if not screenshot_after or x_pct is None or y_pct is None:
return ValidationResult(
verdict=Verdict.CONTINUE, confidence=0.2,
check_used=self.name, elapsed_ms=(time.time() - t0) * 1000,
reasoning="ROI indéfinie (pas de coords ou pas de screenshot)",
)
img = self._load_image(screenshot_after)
w, h = img.size
cx, cy = int(x_pct * w), int(y_pct * h)
r = self._radius
roi = img.crop((max(0, cx - r), max(0, cy - r), min(w, cx + r), min(h, cy + r)))
text = (self._ocr(roi) or "").lower()
expected_lower = expected_text.lower().strip()
elapsed_ms = (time.time() - t0) * 1000
# 1) Vérif : un token suspect (navigateur) dans la ROI → faux clic
for suspect in self.SUSPECT_TOKENS:
if suspect in text and suspect not in expected_lower:
return ValidationResult(
verdict=Verdict.TERMINATE, confidence=0.85,
check_used=self.name, elapsed_ms=elapsed_ms,
failure_category=FailureCategory.WRONG_APPLICATION,
reasoning=f"Token navigateur '{suspect}' dans ROI clic — cible probablement hors-app",
raw_evidence={"roi_text": text[:200], "expected": expected_lower},
)
# 2) Vérif : le texte attendu est dans la ROI ?
if expected_lower and expected_lower in text:
return ValidationResult(
verdict=Verdict.COMPLETE, confidence=0.9,
check_used=self.name, elapsed_ms=elapsed_ms,
reasoning=f"Texte '{expected_lower[:40]}' trouvé dans ROI",
raw_evidence={"roi_text": text[:200]},
)
# 3) Pas trouvé mais pas suspect non plus → confiance basse, escalation
return ValidationResult(
verdict=Verdict.CONTINUE, confidence=0.4,
check_used=self.name, elapsed_ms=elapsed_ms,
failure_category=FailureCategory.OCR_TEXT_MISSING,
reasoning=f"Texte '{expected_lower[:40]}' non trouvé dans ROI",
raw_evidence={"roi_text": text[:200]},
)
@staticmethod
def _load_image(source: str) -> Image.Image:
# Délégué à replay_verifier._load_single_image, ou copy-paste équivalent
from agent_v0.server_v1.replay_verifier import ReplayVerifier
return ReplayVerifier()._load_single_image(source)
```
### 6.3. Intégration avec `replay_verifier.py` existant
Le `replay_verifier.verify_with_critic` couvre déjà 80 % du besoin LLM-as-judge (étape sémantique VLM). Il suffit de :
1. Le wrapper dans un `LlmJudgeChecker` qui implémente `ActionChecker`.
2. L'utiliser comme `escalation_checker` du `Validator`.
```python
# agent_v0/server_v1/checkers/llm_judge.py
import time
from agent_v0.server_v1.replay_verifier import ReplayVerifier
from agent_v0.server_v1.validator import (
ActionChecker, ValidationResult, Verdict, FailureCategory,
)
class LlmJudgeChecker:
"""Wrapper autour de ReplayVerifier.verify_with_critic (VLM gemma4)."""
name = "llm_judge"
budget_ms = 3000.0
def __init__(self, verifier: ReplayVerifier):
self._verifier = verifier
def check(self, action, result, screenshot_before, screenshot_after, context) -> ValidationResult:
t0 = time.time()
expected = context.get("expected_result", "")
intention = context.get("action_intention", "")
workflow_ctx = context.get("workflow_context", "")
critic = self._verifier.verify_with_critic(
action=action, result=result,
screenshot_before=screenshot_before,
screenshot_after=screenshot_after,
expected_result=expected,
action_intention=intention,
workflow_context=workflow_ctx,
)
elapsed_ms = (time.time() - t0) * 1000
if critic.semantic_verified is True:
verdict = Verdict.COMPLETE
conf = max(critic.confidence, 0.7)
elif critic.semantic_verified is False:
verdict = Verdict.TERMINATE
conf = 0.8
else:
verdict = Verdict.CONTINUE
conf = 0.4
return ValidationResult(
verdict=verdict, confidence=conf,
check_used=self.name, elapsed_ms=elapsed_ms,
reasoning=critic.semantic_detail or critic.detail,
raw_evidence={"pixel_change_pct": critic.change_area_pct,
"semantic_verified": critic.semantic_verified},
)
```
### 6.4. Câblage côté `api_stream.py` (post-action)
Pseudo-diff (NE PAS appliquer, juste pour montrer le point d'insertion) :
```python
# agent_v0/server_v1/api_stream.py — handler de REPORT
from agent_v0.server_v1.validator import Validator, Verdict
from agent_v0.server_v1.checkers.ocr_roi import OcrRoiChecker
from agent_v0.server_v1.checkers.llm_judge import LlmJudgeChecker
# Init au boot
_validator = Validator(
checkers={
"click": [OcrRoiChecker(ocr_fn=_easyocr_fn)],
"type": [OcrRoiChecker(ocr_fn=_easyocr_fn)],
"key_combo": [TitleBarChecker()], # voir core/grounding/title_verifier.py
# ...
},
default_checker=PixelDiffChecker(), # wrapper ReplayVerifier.verify_action
escalation_checker=LlmJudgeChecker(ReplayVerifier()),
escalation_threshold=0.55,
)
# Dans report_action_result, après le pixel-diff actuel
async def report_action_result(payload):
...
if RPA_VALIDATOR_ENABLED: # kill-switch env var
val = _validator.validate(
action=action, result=result,
screenshot_before=before, screenshot_after=after,
context={"expected_text": action.get("by_text"),
"expected_result": step.get("expected_result", ""),
"action_intention": step.get("label", ""),
"workflow_context": f"step {step_idx}/{total_steps}"},
)
if val.verdict == Verdict.TERMINATE:
# Pause supervisée, pas stop avec error (cf. feedback_failure_is_learning)
_enter_paused_state(reason=val.reasoning, evidence=val.to_dict())
elif val.verdict == Verdict.CONTINUE:
# Re-vérifier après wait, ou retry
_schedule_recheck(action_id, after_ms=1500)
# COMPLETE → continue normalement
```
---
## 7. Application au bug step 10 démo GHT
**Rappel du bug** (cf. `REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md`) : step 10 « cliquer onglet Imagerie », OCR-DIRECT renvoie centre de la rangée de tabs → le clic tombe **dans la URL bar Edge** (au-dessus). pHash global voit du changement → REPORT success=True. Cascade.
**Avec le Validator proposé** :
1. Action `click_anchor` (`by_text="Imagerie"`, `x_pct=0.23`, `y_pct=0.28`).
2. Léa rapporte success après mouseclick. Screenshot_after capturé.
3. `Validator.validate(action_type="click", ...)` route vers `OcrRoiChecker`.
4. ROI 60 px autour de (0.23, 0.28) → réellement la URL bar.
5. EasyOCR du crop renvoie texte type : `« urgence.labs.laurinebazin.design/aiva-urgence/dossier.html#imagerie »`
6. Token `.com` ou `https` détecté → **`Verdict.TERMINATE`** avec `FailureCategory.WRONG_APPLICATION`.
7. Reasoning : « Token navigateur 'https' dans ROI clic — cible probablement hors-app ».
8. `api_stream` entre en pause supervisée avec `evidence={roi_text, expected}`. Dom voit dans le dashboard ce qui s'est mal passé. Pas d'enchainement vers step 11.
**Latence ajoutée** : 100-200 ms (EasyOCR sur 120×120 px). **Négligeable** vs. les 6 s passés à enchaîner 5 steps faux et à entrer en pause supervisée 33 s plus tard.
**Effet secondaire bénéfique** : le même mécanisme attrape :
- Clics sur popups Windows (Hello / UAC) → ROI contient « Sécurité Windows » → TERMINATE.
- Clics sur le menu démarrer ou la barre des tâches.
- Tout clic qui tombe dans une zone système non prévue.
---
## 8. Budget latence par check — qu'accepter en démo ?
Hypothèse démo GHT (40 steps, 2 min de pipeline cible) :
| Check | Latence | × 40 steps | Acceptable démo ? |
|---|---|---|---|
| Pixel diff global (existant) | 10 ms | 0.4 s | ✅ ON par défaut |
| OCR ROI EasyOCR | 100-200 ms | 4-8 s | ✅ ON sur `click`, `type` |
| OCR title-bar (existant) | 120 ms | 4.8 s | ✅ ON sur navigation |
| Schema validation (JSON) | <10 ms | 0.4 s | ✅ ON sur `extract_*`, `t2a_decision` |
| LLM-judge gemma4 critic | 2-3 s | 80-120 s | ⚠️ SEULEMENT en escalation |
| LLM-judge cloud (Claude Haiku) | 1-2 s | 40-80 s | ⚠️ SEULEMENT en escalation |
| DINOv2 features ROI | 150 ms | 6 s | ❓ pas nécessaire pour démo |
**Recommandation budget** :
- Démo : pixel + OCR ROI + title-bar + schema = ~10 s de latence cumulée sur 40 steps. Acceptable.
- LLM-judge escalation déclenché ~5 fois max par démo = 10 s ajoutés. Tolérable si placé sur les steps à risque (clics ambigus sur tabs).
- DINOv2 hors-périmètre démo. À benchmarker post-démo.
**Kill-switch** obligatoire (cf. QW Suite Mai, conventions Dom) :
```bash
RPA_VALIDATOR_ENABLED=true # active la couche entière
RPA_VALIDATOR_LLM_JUDGE_ENABLED=true # active escalation LLM (coûteuse)
RPA_VALIDATOR_OCR_ROI_RADIUS=60 # tunable
RPA_VALIDATOR_ESCALATION_THRESHOLD=0.55
```
---
## 9. Plan d'intégration gradué
### 9.1. Court terme — 1 jour, faisable avant prochaine démo (P0)
**But** : éliminer la classe « clic hors-app silencieusement success=True ».
1. Créer `agent_v0/server_v1/validator.py` (squelette §6.1) — 1 h.
2. Créer `OcrRoiChecker` (§6.2) — 2 h.
3. Wrapper `LlmJudgeChecker` autour de `verify_with_critic` existant (§6.3) — 30 min.
4. Ajouter hook dans `api_stream.report_action_result` derrière `RPA_VALIDATOR_ENABLED=false` par défaut — 2 h.
5. Tests :
- Unit : ROI text matching, suspect tokens, escalation logic — 2 h.
- Integration : rejouer step 10 sur fixture screenshot — 1 h.
6. Démo interne avec `RPA_VALIDATOR_ENABLED=true` sur Demo_urgence_3_db, mesure latence + faux positifs — 1 h.
**Livrable** : pas de régression démo si flag off ; quand on, le bug step 10 est attrapé en TERMINATE → pause supervisée.
### 9.2. Moyen terme — 1-2 semaines (P1)
**But** : matrice complète action → check (§5).
1. `TitleBarChecker` adapté de `core/grounding/title_verifier.py` existant — 2 h.
2. `JsonSchemaChecker` pour `extract_text`, `t2a_decision`, `extract_table` — 4 h.
3. `DialogPresenceChecker` réutilisant la cascade de modaux VM (`feedback_phash_vs_dialog_in_vm.md`) — 4 h.
4. `PixelDiffChecker` (wrapper de l'existant) avec verdict adapté au contrat Verdict — 2 h.
5. Câblage de la matrice complète selon §5 — 4 h.
6. Dashboard : panneau « Validator stats » par session — pourcentage COMPLETE / CONTINUE / TERMINATE, top failure_categories — 1 j.
### 9.3. Long terme — post-démo (P2)
1. Évaluer **DINOv2** vs OCR ROI sur fixtures GHT : meilleur signal pour distinguer « tab activé vs tab survolé » ? Bench 100 steps.
2. Migration LLM-judge de gemma4:e4b (local) vers un handler dédié — séparer le « LLM décisionnel T2A » du « LLM judge ». Skyvern expose `USE_CHECK_USER_GOAL_HANDLER_FOR_VERIFICATION` qui sépare déjà.
3. Apprentissage : enregistrer dans `TargetMemoryStore` chaque verdict TERMINATE pour produire du training data (pattern OpenAdapt « success traces become new training data »).
4. Re-planification : si TERMINATE répété → renvoyer info au Planner pour ajuster le workflow (cf. Skyvern « reporting any errors / tweaks back to the Planner so it can make adjustments in real-time »). Pour rpa_vision_v3 : signaler à VWB que l'ancre est foireuse → suggestion recapture.
---
## 10. Sources avec liens
### Skyvern (Planner-Actor-Validator)
- Blog Skyvern 2.0 — <https://www.skyvern.com/blog/skyvern-2-0-state-of-the-art-web-navigation-with-85-8-on-webvoyager-eval/> (annonce de l'archi Planner-Actor-Validator, score WebVoyager 85.85 %)
- GitHub repo — <https://github.com/Skyvern-AI/skyvern>
- `agent.py` (méthode `complete_verify`) — <https://github.com/Skyvern-AI/skyvern/blob/main/skyvern/forge/agent.py> ligne 2609 (au 23 mai 2026)
- Prompt `check-user-goal.j2` — <https://raw.githubusercontent.com/Skyvern-AI/skyvern/main/skyvern/forge/prompts/skyvern/check-user-goal.j2>
- Prompt `check-user-goal-with-termination.j2` — <https://raw.githubusercontent.com/Skyvern-AI/skyvern/main/skyvern/forge/prompts/skyvern/check-user-goal-with-termination.j2>
- Prompt `decisive-criterion-validate.j2` — <https://raw.githubusercontent.com/Skyvern-AI/skyvern/main/skyvern/forge/prompts/skyvern/decisive-criterion-validate.j2>
- Hacker News show — <https://news.ycombinator.com/item?id=42724616>
### browser-use (agentic judge)
- Blog « Our browser agent evaluation system » — <https://browser-use.com/posts/our-browser-agent-evaluation-system>
- AGENTS.md — <https://github.com/browser-use/browser-use/blob/main/AGENTS.md>
### OpenAdapt (Evaluation-Driven Feedback)
- GitHub OpenAdapt — <https://github.com/OpenAdaptAI/OpenAdapt>
- GitHub openadapt-evals — <https://github.com/OpenAdaptAI/openadapt-evals>
- Wiki architecture — <https://github.com/OpenAdaptAI/OpenAdapt/wiki/OpenAdapt-Architecture-(draft)>
### Anthropic Computer Use & OpenAI Operator
- Operator system card — <https://openai.com/index/operator-system-card/>
- OpenCUA (open foundations CUA, xLANG / HKU) — <https://opencua.xlang.ai/>
- Computer Use 2026 review — <https://tech-insider.org/anthropic-claude-computer-use-agent-2026/>
### Cradle (BAAI)
- Paper arXiv 2403.03186 — <https://arxiv.org/pdf/2403.03186>
- GitHub — <https://github.com/BAAI-Agents/Cradle>
- Project page — <https://baai-agents.github.io/Cradle/>
### Visual diff / VLM-as-judge / LLM-as-judge
- « Screenshot Comparison Algorithms » — <https://wopee.io/blog/screenshot-comparison-algorithms-visual-testing/> (pHash positionné comme pre-filter, VLM comme triage layer)
- DINOv2 (Meta) — <https://github.com/facebookresearch/dinov2>
- CLIP vs DINOv2 image similarity — <https://medium.com/aimonks/clip-vs-dinov2-in-image-similarity-6fa5aa7ed8c6>
- « Aha Moment Revisited: Are VLMs Truly Capable of Self Verification » (arXiv 2506.17417) — <https://arxiv.org/pdf/2506.17417>
- Vision-Language Model Verifier (review) — <https://www.emergentmind.com/topics/vision-language-model-vlm-verifier>
- LLM-as-a-Judge guide 2026 — <https://labelyourdata.com/articles/llm-as-a-judge>
- « Why Success is Lying to You: The 2026 Agent Eval Stack » — <https://micheallanham.substack.com/p/why-success-is-lying-to-you-the-2026>
### EDDOps (Evaluation-Driven Development & Operations)
- Paper arXiv 2411.13768 (v3, 2026) — <https://arxiv.org/html/2411.13768v3>
### Doc interne rpa_vision_v3 (référencée)
- `docs/INSPIRATION_FRAMEWORKS_2026-05-10.md` §3.1 — Planner-Actor-Validator
- `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` — bug archétype step 10
- `docs/BUG_PRECHECK_SPATIAL_BLINDNESS_2026-05-08.md` — DETTE-001
- `agent_v0/server_v1/replay_verifier.py``verify_with_critic` déjà câblé
- `core/grounding/title_verifier.py` — TitleVerifier déjà câblé
- Memory `reference_vwb_action_types.md` — matrice action_types VWB
---
## 11. Dépendances avec autres axes
- **AXE_A4 (OCR)** : `OcrRoiChecker` repose sur EasyOCR/docTR rapides. Si AXE_A4 livre un OCR ROI < 100 ms calibré sur petits crops, le check primaire devient ultra-fiable. **Bloquant** : qualité OCR sur crop 120×120 px.
- **AXE_A5 (tokenisation écran)** : si on a un parseur UI type OmniParser qui renvoie une liste d'éléments avec bbox + label, le check ROI devient déterministe (matche `target == element_at_point(cx, cy).label`). **Forte synergie** : un Validator + un tokenizer = on rentre dans le territoire Skyvern 2.0.
- **AXE_B4 (ORA)** : ORA peut consommer les `ValidationResult` du Validator comme signal d'observation. Si TERMINATE → ORA ré-observe et propose une re-action. Le Validator devient l'œil de l'Actor.
- **DETTE-008** (pre-check VLM par-clic désactivé par `if False:`) : ce Validator est sa version refaite-proprement. La désactivation actuelle est juste, mais le besoin reste — c'est ce livrable.
- **DETTE-001** (pre-check OCR spatialement aveugle) : `OcrRoiChecker` avec `radius_px=60` est exactement l'Option B mentionnée dans la note de Dom. Réduire radius + bboxes individuelles = même direction.
---
*Document de recherche, lecture seule. Aucune décision d'implémentation prise par cet axe — décision relève de Dom et d'un planning de réintégration coordonné avec AXE_A4, A5, B4.*

View File

@@ -0,0 +1,259 @@
# AXE B4 — Agents GUI autonomes vs replay déclaratif : où placer le curseur pour rpa_vision_v3 ?
**Date** : 2026-05-23
**Auteur** : Claude (agent dispatché, recherche prospective)
**Statut** : Note de cadrage. Pas d'action de code. Décision Dom requise.
**Périmètre** : état de l'art 2025-2026 des frameworks computer-use / GUI agents, en miroir de l'architecture actuelle (replay VWB déclaratif + Léa Windows + cascade OCR/template/VLM).
---
## 1. TL;DR et recommandation
**Insight central** : entre mars 2025 et mai 2026, l'autonomie GUI a fait un bond brutal. Les benchmarks de référence (OSWorld-Verified, WindowsAgentArena, ScreenSpot-Pro) sont passés de ~38 % (CUA d'OpenAI, fin 2024) à **>80 %** (Holo3, Claude Mythos Preview, Agent S3 Behavior Best-of-N) en moins d'un an, dépassant ou rivalisant avec le baseline humain expert (~72 %). Mais ces scores ne disent rien des deux contraintes qui dictent notre choix : **latence par step** (10-30 s pour les modèles autonomes contre <2 s pour le replay cache-hit) et **coût d'inférence cloud** (rédhibitoire pour un déploiement healthtech on-premise).
**Recommandation 3-6 mois** : **rester sur l'axe replay déclaratif amélioré**, mais ouvrir un **bac à sable autonomous "Copilot"** sur le pattern Skyvern "Planner-Actor-Validator" (cf. §3) câblé sur le module ORA existant. Concrètement :
1. **Fermer la dette transport** (HTTP → SSE/WebSocket, cf. SYNTHESE_TECHNOS §5.1) avant toute escalade vers l'autonome — sinon on bâtit un agent autonome sur un transport qui perd 9 actions sur 33 s.
2. **Réactiver le pre-check ORA `if False:` ligne 1705** uniquement en mode "Copilot supervisé" (toggle par workflow), pas en autonome silencieux. C'est le pas le plus court vers l'échelle Skyvern niveau Validator-as-component, dont notre dette est explicite (`feedback_phash_vs_dialog_in_vm.md`).
3. **Adopter explicitement le vocabulaire Shadow → Copilot → Autonomous** comme palier produit, avec des métriques de bascule mesurables (success rate ≥ 95 %, intervention rate < 1 step sur 20) issues de la littérature (Turian, SAFe-Copilot, cf. §5).
4. **Ne PAS courir derrière Holo3 ou Claude Mythos** : ces modèles sont SOTA en autonomie mais cloud-only ou >35B params. Notre contrainte VRAM 12 GB et notre exigence on-premise les excluent.
**Dépendances directes** :
- **AXE B2 Validator** : prérequis. Sans Validator sémantique solide, le mode Copilot ne peut pas détecter ses échecs → boucle d'erreur sans recovery. Le pattern Reflexion (§4) ne fonctionne que si l'évaluateur est fiable.
- **AXE C apprentissage** : `TargetMemoryStore` (Phase 1 du PLAN_APPRENTISSAGE_LEA) devient le fondement d'une "memory tier" type Letta/MemGPT pour le mode Copilot. Brancher la mémoire AVANT toute escalade autonome.
---
## 2. Table comparative — frameworks GUI agents autonomes mai 2026
| Framework / Modèle | OSWorld-Verified | WindowsAgentArena | ScreenSpot-Pro | Latence/step (estim.) | On-prem ? | Licence | Notes |
|---|---:|---:|---:|---:|:---:|---|---|
| **Claude Sonnet 5** (Anthropic CU) | **88.3%** | n/a (CU générique) | n/a | 10-30 s (LLM agentic) | ❌ cloud only | propriétaire | Dépasse human baseline 72.4 %. API "computer use" tool. Coût ~$5/$25 par MTok |
| **Claude Opus 4.7** | 78.0% | n/a | n/a | 10-30 s | ❌ | propriétaire | Successeur 4.6 (72.7 %). |
| **Holo3-122B-A10B** (H Company) | 78.85% (mars) | n/a | n/a | n/a | ⚠ Apache 2.0 mais 10B actifs / 122B totaux | Apache 2.0 | MoE desktop-spécialisé, sort proprio |
| **Holo3-35B-A3B** | **82.6%** (avril) | n/a | n/a | n/a | ⚠ 3B actifs / 35B totaux | Apache 2.0 | SOTA leaderboard fin avril 2026 |
| **GPT-5.4 / OpenAI CUA** | 75.0% | n/a | **85.4%** (SS-Pro) | 10-20 s | ❌ cloud only | propriétaire | Computer Use tool API tiers 3-5, $3/$12 MTok |
| **Agent S3** (Simular) | 66% (100 steps) / 72.6% (Best-of-N) | n/a | n/a | LLM-dépendant | ✅ orchestrateur open | Apache 2.0 | Compose any VLM (Claude/GPT/local) |
| **Agent S2** (Simular) | 34.5% (50 steps) | +52.8% vs SOTA prec. | n/a | LLM-dépendant | ✅ | Apache 2.0 | Generalist-Specialist framework |
| **UI-TARS-2** (ByteDance) | 47.5% | 50.6% | n/a | end-to-end, ~5 s GPU local | ✅ open weights | Apache 2.0 | 7B params, déployable local. Multi-turn RL |
| **Magma** (Microsoft) | n/a (focus robotique + GUI) | n/a | n/a | n/a | ✅ open | MIT | Foundation model SoM/ToM, 39M samples. Pas de score OSWorld direct. |
| **OS-Atlas-Pro-7B** | n/a | n/a | strong (focus grounding) | <2 s GPU local | ✅ open weights | Apache 2.0 | 3 modes : Grounding / Action / Agent |
| **Skyvern v2** | n/a (browser-only) | n/a (browser) | n/a | Agent: ~5 s/step ; Script: 10-100× plus rapide | ✅ self-host | AGPL-3.0 | WebVoyager 85.85%. Dual mode agent/script |
| **browser-use v2** | n/a (browser) | n/a | n/a | LLM-dépendant | ✅ self-host | MIT | 78k★ GitHub. Reasoning loop pure |
| **Cradle** (BAAI) | OSWorld testé | n/a | n/a | élevé (6 modules) | ✅ open | Apache 2.0 | 6 modules : Info Gather, Self-Reflection, Task Inference, Skill Curation, Action Planning, Memory |
| **AppAgent v2** (Tencent) | mobile-focused | n/a | n/a | n/a | ✅ open | MIT | Combine parser + visuel, flexible action space |
| **OS-Genesis** (Shanghai AI Lab) | training pipeline | n/a | n/a | n/a | ✅ open (ACL 2025) | Apache 2.0 | **Reverse Task Synthesis** — pertinent pour Shadow→Copilot, cf. §5 |
**Lecture critique** :
- Le **plafond verre des 85 %** sur OSWorld est dépassé par les cloud SOTA (Claude Sonnet 5, Holo3). Mais on parle de tâches **simples** type ouvrir LibreOffice, modifier un fichier. RIEN sur OSWorld ne ressemble à Easily Assure (UI métier propriétaire dans Edge/Citrix, 22+ steps, T2A médical).
- Les modèles **vraiment on-premise <8B** (UI-TARS-2, OS-Atlas-Pro) plafonnent à **47-50 %** sur OSWorld — performance insuffisante pour de l'autonomie en production healthtech.
- **WindowsAgentArena** reste le benchmark le plus proche de notre cible (154 tâches Windows multi-app). Score de référence UI-TARS-2 = **50.6 %**. À retenir : aucun modèle <100B ne dépasse 60 % sur WAA en mai 2026.
- **OSWorld-Human** (arxiv 2506.16042) montre que **les meilleurs agents prennent 2.7 à 4.3× plus de steps que nécessaire**, et que chaque step successif peut prendre **3× plus longtemps** que le premier. Le coût latence n'est pas linéaire — il explose en fin de tâche.
---
## 3. L'échelle d'abstraction — 4 paliers, où on est, où aller
Reprise du §2.3 d'INSPIRATION_FRAMEWORKS_2026-05-10.md, instrumentée avec les benchmarks 2026.
| Palier | Description | Exemples framework | Robustesse cible | Latence/step | Coût LLM | Notre position |
|---|---|---|---|---|---|---|
| **L1 — Replay déclaratif pur** | Workflow recorded → rejoué step par step. Aucun raisonnement runtime. | UiPath classique, TagUI, **Skyvern Script Mode** (cache) | Très haute si UI stable, fragile sur changement | <500 ms (resolve memory hit) à ~2 s (VLM grounding) | ~0 (un appel VLM si miss) | **C'est ici qu'on opère.** VWB = Planner statique, cascade = Grounding |
| **L2 — Replay avec runtime fallback** | Replay déclaratif + fallback intelligent quand un step échoue : retry visuel, re-grounding, escalade VLM | **Skyvern dual mode** (script + agent fallback), Anthropic Computer Use en mode "tool" | Haute, dégradation gracieuse | 2-5 s en moyenne, pic 15 s au fallback | Faible (fallback rare) | **Cible 3-6 mois**. Le pre-check ORA `if False:` ligne 1705 est l'opportunité d'amorçage |
| **L3 — Autonomous avec checkpoint** | Plan dynamique + Validator post-step + ability de re-planifier. Human-on-the-Loop. | **Skyvern Agent Mode v2** (Planner-Actor-Validator), **Cradle** (6 modules), **Agent S2/S3**, **MGA observation-centric** | Moyenne, dépend du Validator | 5-15 s/step | Significatif (validator + replan) | **Cible 12-18 mois**, après AXE B2 Validator solide |
| **L4 — Autonomous full** | Goal → décomposition + exécution + recovery sans intervention humaine. Human-out-of-the-Loop. | **Claude CU**, **OpenAI CUA**, **Holo3** end-to-end | Variable — SOTA 88 % sur tâches simples, chute sur UI métier propriétaire | 10-30 s/step | Élevé (cloud) ou très VRAM-gourmand (local 35B+) | **Hors périmètre POC santé**. Risque juridique RGPD/AI Act, coût cloud, instabilité UI Easily |
**Position critique** : OpenAdapt, Skyvern, OmniParser et **toute la littérature 2026** convergent sur l'idée que **L1 → L2 est le saut le plus rentable**. L'écart L2 → L3 demande un Validator robuste qui n'existe pas encore chez nous (pHash global insuffisant, cf. bug step 10 du diagnostic 8 mai). L'écart L3 → L4 demande des modèles qu'on n'a pas (cloud only) ou qu'on ne peut pas servir (>35B params).
---
## 4. Recovery patterns 2026 — lequel adopter
Quatre familles de patterns dominent en 2026. Classés par robustesse vs effort d'implémentation chez nous.
| Pattern | Principe | Effort impl. | Robustesse | Recommandé pour rpa_vision_v3 ? |
|---|---|---|---|---|
| **Retry immédiat** | Refaire la même action 1-3 fois avec back-off | Trivial | Faible (n'aide pas si cause structurelle) | ✅ déjà partiellement en place, OK |
| **Backtrack agent** (BacktrackAgent arxiv 2505.20660) | Verifier + Judger en pipeline. Si fail détecté → rollback step n, retry avec stratégie alternative | Moyen | Haute si Verifier solide | ⚠ utile, mais nécessite Verifier sémantique = AXE B2 |
| **Reflexion** (NeurIPS 2023, Shinn et al.) | Verbal RL : LLM observe son échec, génère feedback texte stocké en mémoire épisodique, ré-essaie en lisant ce feedback | Élevé (Actor + Evaluator + Self-Reflection) | Très haute en long-horizon, surcoût LLM élevé | ❌ pas avant L3. Surcoût LLM rédhibitoire sur démo répétitive |
| **Checkpoint + idempotency** (Agent DR 2026) | Checkpoint après chaque step validé, replay depuis le dernier checkpoint sain. Idempotency keys au scope task | Moyen | Très haute pour tâches state-mutating | ✅ **Pertinent pour T2A** : checkpoint après chaque ord validé, reprise depuis là si crash |
| **Pause supervisée** (Human-on-the-Loop) | À la moindre détection d'anomalie : pause, demande validation humaine, reprend ou abandonne | Faible | Très haute (humain = oracle) | ✅ **Cohérent avec `feedback_failure_is_learning.md`** ("échec clic = pause supervisée, pas stop avec error"). DÉJÀ NOTRE PATTERN |
| **Observation-centric (MGA)** | Closed loop observe-plan-act-verify ; "occlusion signals + failure clusters" déclenchent replan explicite | Moyen-élevé | Bonne en GUI dynamique | ⚠ pertinent pour Citrix/popups mais nécessite OmniParser-like |
**Recommandation** : combiner **(1) Pause supervisée** (déjà notre devise) + **(2) Checkpoint+idempotency au niveau workflow VWB** (chaque ord T2A = un checkpoint, reprise possible sans réexécution amont). Bonus : ces deux patterns sont **vendables** au pitch healthtech (sécurité, traçabilité). Reflexion et Backtrack agent restent en R&D pour AXE C.
---
## 5. Cycle Shadow → Copilot → Autonomous — état de la littérature
### 5.1 Qui le formalise ?
Le triptyque est **largement adopté en 2026** mais sous des noms variables :
- **Microsoft Copilot vs Agent vs Autonomous** (Microsoft 2026 Copilot Update, mai 2026) : trois layers explicites — "human-in-the-loop AI", "supervised agent AI", "autonomous agent AI". Microsoft Agent 365 = control plane de cette progression.
- **5 levels of AI autonomy** (Turian.ai) : Manual → Assisted → Augmented → Autonomous → Fully Autonomous. Très repris en blogs entreprise.
- **HITL / HOTL / Human-out-of-the-loop** (autonomous-systems-explained.com) : trois niveaux canoniques en robotique appliqués à l'IA.
- **SAFe-Copilot** (arxiv 2511.04664) : unified shared autonomy framework — formalise les seuils de bascule.
- **AI Autonomy Coefficient α** (arxiv 2512.11295) : tente une formalisation quantitative.
**Aucun papier** ne propose exactement notre triptyque "Shadow → Copilot → Autonomous" mais **tous les frameworks 2026 ont 3 paliers équivalents**. Notre vocabulaire produit (cf. `memory/project_vision.md`) est cohérent avec le mainstream.
### 5.2 Métriques de bascule entre paliers
Synthèse littérature + nos contraintes :
| Bascule | Métrique | Seuil indicatif littérature | Adaptation rpa_vision_v3 |
|---|---|---|---|
| **Shadow → Copilot** | Précision de la suggestion shadow validée par l'humain | 80-90 % d'acceptation des suggestions | Workflow VWB construit en Shadow accepté ≥ 80 % par le TIM sans modif majeure |
| **Copilot → Autonomous** | Success rate replay sans intervention | ≥ 95 % sur N runs consécutifs (N≥50) | 50 runs MOREL Catherine successifs sans intervention humaine. Aucun aujourd'hui. |
| **Recul Autonomous → Copilot** | Intervention rate > seuil | >5 % des steps requièrent humain | Tableau de bord temps réel intervention rate par workflow |
**Pratique concrète** : OS-Genesis (Shanghai AI Lab) propose un pipeline "Reverse Task Synthesis" qui est **conceptuellement Shadow → Copilot inverse** : l'agent explore d'abord, dérive ensuite les tâches. Pertinent pour notre vision **TargetMemoryStore → généralisation** (PLAN_APPRENTISSAGE_LEA Phase 2-3).
---
## 6. MCP (Model Context Protocol) — place dans une archi RPA on-premise
**Statut MCP** : standard ouvert Anthropic 2024, adopté largement en 2026. Architecture client-serveur. Anthropic, OpenAI, Microsoft Agent 365 le supportent.
**Pertinence pour rpa_vision_v3** :
1. **Notre serveur RPA pourrait s'exposer en MCP server** — déjà signalé dans INSPIRATION_FRAMEWORKS §5 et CLAUDE.md memory (`reference_mcp_servers.md`, on a 13 MCP actifs côté outillage). Cela permettrait à Claude Desktop / Cursor / VS Code d'invoquer nos workflows.
2. **Le serveur on-prem peut exposer en MCP** : tables PostgreSQL T2A, dossiers DPI, modèles VLM locaux, dashboards. Pas de cloud requis pour la couche MCP elle-même.
3. **Risque** : si on expose Léa en MCP, on rentre dans l'écosystème "shadow AI agents" pointé par les analyses Microsoft RSAC 2026 (gouvernance, traçabilité). Acceptable seulement avec audit log strict.
4. **Pas de blocage RGPD spécifique** : MCP est juste un protocole, la souveraineté dépend de qui héberge le serveur.
**Recommandation MCP** : **horizon 12+ mois**. Pas de valeur immédiate démo. Mais positionnement commercial fort (« notre RPA est un MCP server consommable par n'importe quel agent IA, on-premise et conforme »).
---
## 7. Trois scénarios pour rpa_vision_v3
### Scénario A — Rester replay déclaratif amélioré (RECOMMANDÉ)
**Description** : on consolide L1, on ferme les 5 bugs P0, on adopte le vocabulaire Skyvern (Policy/Grounding/Validator) dans la doc et le code, on garde la cascade actuelle.
**Effort** : 4-6 semaines (clôture dette transport + Validator pHash → sémantique + smart_resize DETTE-014).
**Risque** : faible. On capitalise sur l'existant.
**Bénéfice** : démo robuste, vendable POC clinique. Pas de saut techno.
**Coût** : ne répond pas à l'objectif "Léa apprend / Léa comprend" du `memory/project_vision.md`.
---
### Scénario B — Hybride L2 + Copilot ORA (BAC À SABLE PARALLÈLE)
**Description** : Scénario A + on rebranche `_verify_pre_click` dans ORA (DETTE-008, ligne 1705), uniquement en mode toggle "Copilot supervisé" sur un workflow expérimental. Le pre-check VLM devient le Validator-as-component du pattern Skyvern.
**Effort** : 8-10 semaines (B2 Validator sémantique + un workflow expérimental en Copilot mode + métriques d'intervention rate).
**Risque** : moyen. Risque d'éparpillement entre L1 stable et L2 expérimental. Nécessite discipline forte (toggle ENV, pas de mélange runtime).
**Bénéfice** : on prépare AXE C apprentissage et AXE B2 Validator, on a un POC démontrable de "Léa qui vérifie avant de cliquer". Vendable au pitch healthtech.
**Coût** : double surface de maintenance.
---
### Scénario C — Sauter vers Autonomous L4 avec Holo3 ou Claude CU
**Description** : on abandonne progressivement VWB déclaratif, on bascule sur un modèle SOTA (Holo3-35B-A3B en open weights, ou Claude Sonnet 5 cloud) qui décompose le goal "T2A patient X" en steps autonomes.
**Effort** : 6-12 mois minimum. Recodage majeur. Infrastructure GPU >70 GB VRAM (Holo3) ou cloud bill significatif (Claude).
**Risque** : très élevé. Easily Assure n'est pas dans le set d'entraînement de ces modèles. Performance OSWorld 80 % ne se transfère pas à UI métier propriétaire. Risque RGPD si Claude (envoi screenshots à Anthropic). Risque hallucination en production médicale.
**Bénéfice** : narrative "vraiment agentique". Compétitif vs Skyvern/UiPath agentic.
**Coût** : casse la démo, désaligne avec contrat "100% vision" on-premise, casse l'asset commercial healthtech RGPD.
**Rejeté pour 2026**. Reconsidérer en 2027 si Holo3-7B (hypothétique) sort, ou si on a un client GPU H100 sur site.
---
## 8. Recommandation finale
**Adopter Scénario A en main track, Scénario B en bac à sable parallèle**, avec ces étapes ordonnées :
1. **S1-S2** : SSE/WebSocket transport (clôt §4 de SYNTHESE_TECHNOS, sans ça rien d'autre n'est crédible).
2. **S3-S4** : Validator sémantique (AXE B2) — remplacer pHash global par vérification texte attendu présent dans zone visée. C'est aussi la condition d'AXE C.
3. **S5-S6** : Sur un workflow expérimental, toggle `RPA_ORA_PRECHECK=true` → mode Copilot. Mesurer intervention rate.
4. **S7-S8** : Brancher `TargetMemoryStore` Phase 1 (PLAN_APPRENTISSAGE_LEA) — bascule "Léa apprend" mesurable.
5. **Post-S8** : décision Dom autonomous L3 oui/non, sur base métriques réelles.
**Dépendances explicites** :
- AXE B2 Validator → débloque Copilot et toute progression L2 → L3.
- AXE C apprentissage (TargetMemoryStore) → débloque la mémoire long-terme nécessaire à Copilot+.
- Clôture dette transport → prérequis dur, indépendant des autres axes.
---
## 9. Sources (priorité < 6 mois)
### Benchmarks et leaderboards
- [OSWorld-Verified leaderboard (llm-stats)](https://llm-stats.com/benchmarks/osworld-verified)
- [OSWorld 2026 Benchmark Results (Coasty)](https://coasty.ai/blog/ai-agent-benchmark-results-2026-osworld-leaderboard-slashing)
- [Windows Agent Arena (Microsoft GitHub)](https://microsoft.github.io/WindowsAgentArena/)
- [ScreenSpot-Pro leaderboard](https://gui-agent.github.io/grounding-leaderboard/)
- [Computer Use Leaderboard (Awesome Agents)](https://awesomeagents.ai/leaderboards/computer-use-leaderboard/)
- [OSWorld-Human: Benchmarking Efficiency of CU Agents (arxiv 2506.16042)](https://arxiv.org/abs/2506.16042)
### Frameworks autonomes
- [Anthropic Claude Computer Use 2026 (TokenMix)](https://tokenmix.ai/blog/claude-computer-use-api-2026)
- [Claude Sonnet 5 benchmarks (DEV.to)](https://dev.to/best_codes/anthropic-just-dropped-claude-sonnet-5-and-the-benchmarks-are-kind-of-insane-3ppc)
- [OpenAI CUA / Operator](https://openai.com/index/computer-using-agent/)
- [Holo3 35B-A3B leaderboard top (ChatForest)](https://chatforest.com/guides/holo3-desktop-agent-osworld-record/)
- [Holo Company launches Holo3 (TestingCatalog)](https://www.testingcatalog.com/holo-company-launches-holo3-sota-computer-use-model/)
- [Magma foundation model (Microsoft Research)](https://www.microsoft.com/en-us/research/blog/magma-a-foundation-model-for-multimodal-ai-agents-across-digital-and-physical-worlds/)
- [Magma arxiv 2502.13130](https://arxiv.org/abs/2502.13130)
- [Agent S2 paper (arxiv 2504.00906)](https://arxiv.org/abs/2504.00906)
- [Agent S Github (Simular)](https://github.com/simular-ai/agent-s)
- [Skyvern dual mode (DEV.to)](https://dev.to/stevengonsalvez/browser-tools-for-ai-agents-part-2-the-framework-wars-browser-use-stagehand-skyvern-4gn)
- [Skyvern Github](https://github.com/Skyvern-AI/skyvern)
- [UI-TARS-2 technical report (arxiv 2509.02544)](https://arxiv.org/html/2509.02544v1)
- [UI-TARS Github (ByteDance)](https://github.com/bytedance/UI-TARS)
- [OS-Atlas-Pro-7B HuggingFace](https://huggingface.co/OS-Copilot/OS-Atlas-Pro-7B)
- [OS-Atlas paper (arxiv 2410.23218)](https://arxiv.org/abs/2410.23218)
- [Cradle BAAI (general computer control)](https://baai-agents.github.io/Cradle/)
- [Cradle Github](https://github.com/BAAI-Agents/Cradle)
- [OS-Genesis Reverse Task Synthesis (arxiv 2412.19723)](https://arxiv.org/abs/2412.19723)
- [OS-Copilot Github](https://github.com/OS-Copilot)
- [AppAgent v2 (arxiv 2408.11824)](https://arxiv.org/pdf/2408.11824)
### Patterns recovery / autonomie
- [Reflexion paper (NeurIPS 2023, Shinn et al.)](https://arxiv.org/abs/2303.11366)
- [BacktrackAgent (arxiv 2505.20660)](https://arxiv.org/pdf/2505.20660)
- [MGA Memory-Driven GUI Agent (arxiv 2510.24168)](https://arxiv.org/html/2510.24168v1)
- [Agentic Workflow Incident Response 2026 (DigitalApplied)](https://www.digitalapplied.com/blog/agentic-workflow-incident-response-playbook-2026)
- [Agent Disaster Recovery (TianPan)](https://tianpan.co/blog/2026-04-28-agent-dr-working-memory-region-failover)
- [Agentic Design Patterns 2026 (SitePoint)](https://www.sitepoint.com/the-definitive-guide-to-agentic-design-patterns-in-2026/)
- [AI Agent Reflection patterns (Zylos)](https://zylos.ai/research/2026-03-06-ai-agent-reflection-self-evaluation-patterns)
### Mémoire long-terme
- [State of AI Agent Memory 2026 (Mem0)](https://mem0.ai/blog/state-of-ai-agent-memory-2026)
- [Best AI Agent Memory Frameworks 2026 (Atlan)](https://atlan.com/know/best-ai-agent-memory-frameworks-2026/)
- [Memory in Agents — Short/Long-term with LangGraph (Medium)](https://medium.com/@anilnishad19799/memory-in-agents-complete-guide-to-short-term-long-term-memory-with-langgraph-c21d27455a77)
### MCP
- [MCP introduction (Anthropic)](https://www.anthropic.com/news/model-context-protocol)
- [MCP docs (Anthropic)](https://docs.anthropic.com/en/docs/agents-and-tools/mcp)
- [Code execution with MCP (Anthropic engineering)](https://www.anthropic.com/engineering/code-execution-with-mcp)
- [MCP overview (Phil Schmid)](https://www.philschmid.de/mcp-introduction)
### Autonomy frameworks (Shadow → Copilot → Autonomous)
- [5 Levels of AI Autonomy (Turian)](https://www.turian.ai/blog/the-5-levels-of-ai-autonomy)
- [Human-in-the-Loop vs Full Autonomy (Autonomous Systems Explained)](https://www.autonomous-systems-explained.com/articles/human-in-the-loop-autonomy.html)
- [SAFe-Copilot Unified Shared Autonomy (arxiv 2511.04664)](https://arxiv.org/pdf/2511.04664)
- [AI Autonomy Coefficient α (arxiv 2512.11295)](https://arxiv.org/pdf/2512.11295)
- [Computer Use Agents 2026 Claude vs OpenAI vs Gemini (DigitalApplied)](https://www.digitalapplied.com/blog/computer-use-agents-2026-claude-openai-gemini-matrix)
### Healthcare RPA / agents
- [Built 11 Autonomous Agents Healthcare RCM (Medium Apr 2026)](https://medium.com/@anilAmbharii/built-11-autonomous-agents-to-fix-healthcare-revenue-cycle-9d0c9f8d662a)
- [Manus AI Enterprise Healthcare Evaluation Guide 2026 (Ventus)](https://www.ventus.ai/blog/manus-ai-agentic-ai-enterprise-healthcare-evaluation-guide/)
- [Future of RPA Trends 2026 (Blue Prism)](https://www.blueprism.com/resources/blog/future-of-rpa-trends-predictions/)
---
*Document à débattre avec Dom. Pas d'action de code engagée. Le scénario retenu doit aussi être croisé avec les conclusions d'AXE B2 (Validator) et d'AXE C (apprentissage) avant arbitrage final.*

View File

@@ -0,0 +1,453 @@
# AXE B5 + D1 — Capture multi-écran Windows & Desktop distant sans accessibility tree
**Date :** 2026-05-23
**Auteur :** Claude (agent recherche sous-traité), brief Dom
**Périmètre :** B5 (capture Windows 11 robuste, bug `mss.monitors[N]=2560×60`, DPI) + D1 (NoMachine/Citrix/RDP, capture sans accessibility tree)
**Statut :** recommandations techniques, pas de modif code. À valider Dom avant action.
---
## 1. TL;DR
**Bug racine identifié** : `mss.monitors[1]=2560×60` n'est PAS un bug du multi-écran, c'est un **effet de bord DPI** documenté du couple `mss` + Windows. Quand un autre composant du process (PyQt5 GUI Léa, NoMachine, ou un appel `GetSystemMetrics` antérieur) modifie le `DPI_AWARENESS_CONTEXT` du process pendant l'exécution, `mss` (qui s'appuie sur `EnumDisplayMonitors` + `MONITORINFO`) renvoie des dims tronquées intermittemment. La 1re capture est saine, les suivantes peuvent dériver. Issue documentée : `BoboTiG/python-mss#197`, `#257`, `#108`, `#49`.
**Recommandation principale :** déclarer **`DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2`** AU LANCEMENT du process Léa Windows (avant tout `import mss`, avant PyQt5), via `ctypes.windll.user32.SetProcessDpiAwarenessContext(-4)`. Conserver le garde-fou `_acquire_safe_grab` actuel comme filet de sécurité (post-incident il a déjà sauvé la démo). Migrer à moyen terme vers **`dxcam`** (DXGI Desktop Duplication) qui est immunisé par construction (l'API DXGI travaille en pixels physiques sans dépendre du DPI awareness du process).
**Recommandation NoMachine :** garder NoMachine 9.5.7 pour la démo client (terrain connu), mais évaluer **RustDesk** ou **Parsec** comme alternative pour POC suivants. Le freeze NoMachine "clics avalés après quelques minutes" est confirmé par 9+ threads du forum officiel depuis v4.x, **jamais corrigé**. Implémenter un **heartbeat actif côté Léa** (capture pHash toutes les 5 s + détection écran figé > 30 s) avant tout déploiement client.
**Fix court terme bug coord Y cassé (P0)** : injecter `SetProcessDpiAwarenessContext` dans `executor.py` au démarrage + serrer le garde-fou existant (refuser TOUTE capture < 200 px de haut, pas seulement secondaire). Code copy-paste-ready en §4.
---
## 2. Section B5 — Capture Windows
### 2.1. Table comparative — bibliothèques de capture Windows (mai 2026)
| Lib | Backend | Cross-OS | DPI-safe | Multi-monitor | FPS 1080p | Statut maint. | Verdict RPA Vision |
|---|---|---|---|---|---|---|---|
| **mss** (BoboTiG) | GDI BitBlt | Linux/Mac/Win | ⚠ Buggy (Win) | OK via `monitors[]` | 30-60 | Actif mais bug DPI ouvert depuis 2018 | **Actuel** — conserver avec ceinture DPI |
| **pyautogui** | Pillow + GDI | Cross-OS | ⚠ Idem mss | ❌ Composite seulement | 5-15 | Stable, fonctionnalités figées | À éviter pour capture (ok pour mouse) |
| **Win32 GDI direct** (`BitBlt + GetDC`) | GDI | Win | ✅ Si DPI déclaré | OK manuel | 20-40 | Stable, bas niveau | Trop verbeux, ne pas réinventer |
| **DXGI Desktop Duplication** (Win32 natif) | DXGI | Win 8+ | ✅ Pixels physiques | OK via `IDXGIOutput` | 240+ | Microsoft, stable | Cible idéale mais complexe en pur Win32 |
| **dxcam** (ra1nty) | DXGI | Win | ✅ Natif | OK `output_idx=N` | **240+** | **Actif 2026** (release juin 2025 + maj mars 2026) | ⭐ **Migration cible** |
| **D3DShot** (Serpent-AI) | DXGI | Win | ✅ | OK | 60-100 | **Quasi abandonné** (dernier commit 2022) | NON, deprecated |
| **windows-capture** (NiiightmareXD) | DXGI + WGC | Win 10+ | ✅ | OK | 240+ | Actif, Rust+Python | Alternative à dxcam, plus jeune |
| **BetterCam** (RootKit-Org) | DXGI fork DXcam | Win | ✅ | OK | 240+ | Actif | Fork sécurité/gaming, marketing FPS |
### 2.2. Diagnostic du bug `mss.monitors[N]=2560×60`
#### Root cause confirmée
Issue `BoboTiG/python-mss#197` documente précisément le pattern :
> *« After running `sct.grab(monitor)`, `GetSystemMetrics(0/1)` returns physical pixels (2560×1600) instead of scaled logical (1463×914). »*
Lecture causale (mainteneur + reproductions) :
1. Le process Léa Windows démarre **DPI-unaware** (défaut Python 3.x sur Windows sans `SetProcessDpiAwarenessContext` explicite).
2. `mss.mss()` au premier appel passe par `ctypes.windll.user32` et **modifie implicitement** le DPI context du thread (effet de bord interne `mss` pour pouvoir capturer en pixels physiques sur écran HiDPI).
3. À partir de là, `MONITORINFO.rcMonitor` peut renvoyer des coords incohérentes : la combinaison "process unaware → context modifié à la volée" laisse Windows dans un état intermédiaire où certains monitors logiques sont **réduits à la zone non-DPI-aware** (typiquement la barre de tâches = 60 px de haut sur écran 1600 px).
4. Le bug est **intermittent** parce qu'il dépend de l'ordre des appels d'API par le process, de la présence d'une fenêtre PyQt5 active, et de l'événement NoMachine (resize remote → callback Windows qui re-évalue les monitors).
L'observation 2560×60 chez Léa correspond exactement à : "monitor reconnu, mais sa hauteur effective dans le contexte non-aware est la zone d'overlay NoMachine (≈ taskbar)".
#### Pourquoi `dxcam` est immunisé
DXGI Desktop Duplication s'appuie sur `IDXGIOutput::GetDesc` qui retourne `DXGI_OUTPUT_DESC.DesktopCoordinates` en **pixels physiques** indépendamment du DPI awareness du process. Le bug ne peut littéralement pas se produire.
#### Workaround documenté officiellement (mss issue #197)
```python
# AVANT tout import mss / PyQt5 / win32api
import ctypes
# DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4
try:
ctypes.windll.user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(-4))
except Exception:
# Fallback Windows 8.1 (avant V2) : per-monitor v1
try:
ctypes.windll.shcore.SetProcessDpiAwareness(2)
except Exception:
ctypes.windll.user32.SetProcessDPIAware() # legacy
```
### 2.3. Recommandation patterns multi-DPI
1. **Déclarer le DPI awareness TÔT** dans le process (avant tout `import mss`, avant `from PyQt5 import ...`). Idéalement dans `main.py` du client Léa, ligne 1-5.
2. **Une fois en V2**, `mss.monitors[i]['width'/'height']` est cohérent avec les coords composite Windows (logique = physique pour le process).
3. **Coordonnées agent → serveur** : toujours en pixels physiques globaux (origine = top-left du virtual desktop, qui peut être négative si moniteur secondaire à gauche). Pas de pourcentage tant que le DPI peut bouger.
4. **Pour cliquer en pixels physiques** : utiliser `SendInput` (`ctypes.windll.user32.SendInput`) plutôt que `pyautogui.click``pyautogui` re-divise par le DPI scale.
5. **Tester sur écran HiDPI** : la box dev Dom est 1×, le client cible peut être 1.5× ou 1.75× (cas réel issue #197). Bench obligatoire avant déploiement client.
### 2.4. Snippet Python — capture moderne dxcam multi-monitor prêt à coller
```python
# capture_dxgi.py — capture Windows via DXGI Desktop Duplication
# Drop-in pour remplacer mss.mss().grab(monitor) côté Léa Windows.
import ctypes
import dxcam
import numpy as np
from PIL import Image
from typing import Optional, Tuple
# Étape 1 : déclarer le DPI awareness avant toute capture
def _ensure_dpi_aware_v2() -> bool:
"""Déclare PER_MONITOR_AWARE_V2. À appeler en tout début de process."""
try:
# -4 = DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 (Win 10 1703+)
ok = ctypes.windll.user32.SetProcessDpiAwarenessContext(
ctypes.c_void_p(-4)
)
return bool(ok)
except (AttributeError, OSError):
try:
ctypes.windll.shcore.SetProcessDpiAwareness(2) # PER_MONITOR
return True
except Exception:
try:
ctypes.windll.user32.SetProcessDPIAware()
return True
except Exception:
return False
_ensure_dpi_aware_v2() # exécuté à l'import
# Étape 2 : cache d'instances dxcam par output_idx (création coûteuse)
_camera_cache: dict[int, "dxcam.DXCamera"] = {}
def get_camera(output_idx: int = 0, device_idx: int = 0) -> "dxcam.DXCamera":
"""Récupère (ou crée) une caméra DXGI pour un monitor donné."""
key = (device_idx, output_idx)
if key not in _camera_cache:
_camera_cache[key] = dxcam.create(
device_idx=device_idx,
output_idx=output_idx,
output_color="BGR", # cohérent avec cv2 / mss legacy
)
return _camera_cache[key]
def list_monitors() -> list[dict]:
"""Retourne la géométrie de chaque monitor en pixels physiques."""
# dxcam.output_info() retourne une string formatée — on parse via API bas niveau
monitors = []
for output_idx, info in enumerate(dxcam.device_info().splitlines()):
# Fallback robuste : interroger les outputs via dxcam directement
try:
cam = get_camera(output_idx=output_idx)
# cam.width / cam.height exposés depuis dxcam 0.0.5+
monitors.append({
"idx": output_idx,
"width": int(cam.width),
"height": int(cam.height),
"left": int(getattr(cam, "left", 0)),
"top": int(getattr(cam, "top", 0)),
"primary": output_idx == 0,
})
except Exception:
continue
return monitors
def grab_monitor(output_idx: int = 0) -> Optional[Image.Image]:
"""Capture un monitor en PIL.Image RGB (drop-in pour mss + Image.frombytes).
Retourne None si capture échoue (frame skipped par DXGI = no-change).
"""
cam = get_camera(output_idx=output_idx)
frame = cam.grab() # ndarray (H, W, 3) BGR, ou None si frame inchangée
if frame is None:
# DXGI ne re-livre pas une frame identique → forcer
frame = cam.grab(region=None)
if frame is None:
return None
# BGR → RGB pour PIL
return Image.fromarray(frame[:, :, ::-1])
def safe_grab(
output_idx: int = 0,
min_width: int = 200,
min_height: int = 200,
max_attempts: int = 2,
) -> Tuple[Optional[dict], Optional[Image.Image]]:
"""Drop-in pour _acquire_safe_grab actuel mais sur DXGI.
Vérifie que la frame retournée a des dimensions plausibles.
"""
for attempt in range(max_attempts):
img = grab_monitor(output_idx=output_idx)
if img is None:
continue
w, h = img.size
if w >= min_width and h >= min_height:
return (
{"width": w, "height": h, "idx": output_idx},
img,
)
return None, None
```
**Notes d'intégration :**
- `pip install "dxcam[cv2]"` (ajoute opencv-headless si pas déjà installé).
- Python 3.10-3.14 supporté.
- À tester sur la box Léa avant migration globale : confirmer que `output_idx=0` correspond bien au monitor principal physique de NoMachine.
- **Ne pas migrer en chaud avant la démo client** — l'archi actuelle marche grâce au garde-fou `_acquire_safe_grab`. Migration = post-démo Anouste.
### 2.5. Capture fenêtre vs capture écran (cas Easily Assure dans Edge)
Pour le cas spécifique de la démo (Easily Assure rendu dans Microsoft Edge plein écran sur Léa Windows) :
- **Ne pas utiliser `PrintWindow`** : sur Edge moderne (Chromium/WebView2), `PrintWindow` retourne souvent une image noire ou figée (composition GPU contourne GDI). Issue connue : `Microsoft/microsoft-ui-xaml#7170`.
- **Privilégier capture écran complet + crop** : c'est ce que `capture_active_window` fait déjà dans `capturer.py:381`. Conserver.
- **Pour Edge fullscreen** : la frame DXGI inclut toujours le contenu Chromium même en mode exclusive (contrairement à GDI). DXcam est donc encore mieux ici.
---
## 3. Section D1 — Desktop distant sans accessibility tree
### 3.1. Table comparative — remote desktop pour RPA visuel (mai 2026)
| Solution | Latence LAN | Couleur | Color depth | RPA-friendly | Freeze pattern | Verdict |
|---|---:|---|---|---|---|---|
| **NoMachine 9.5.7** | 15-30 ms | NX H.264 | 24 bpp | Moyen (clipboard cassé, input passive grab) | ⚠ **Confirmé** clics avalés après N min, forum officiel | Actuel, à remplacer dès Anouste |
| **RustDesk** (open source) | 18-30 ms | VP9 | 24 bpp | Moyen | Pas de freeze connu, mais latence WAN x2 vs Parsec | Alternative crédible, on-prem possible |
| **Parsec** | 7-10 ms | H.264 NVENC | 24/32 bpp | Bon (input fiable) | Aucun rapporté | Excellent mais cloud + fermé |
| **AnyDesk** | 20-40 ms | DeskRT | 24 bpp | Bon, support commercial | Rare, restart fix | Standard entreprise, on-prem cher |
| **Citrix Workspace** | variable | HDX (YUV420/YUV444) | 8-24 bpp configurable | **Difficile** (color depth réduit, lag) | Spécifique app | Terrain réel hôpital, accepter contraintes |
| **RDP vanille** | 20-50 ms | RemoteFX/AVC | 16-32 bpp | Bon | "RDP freezing Win11 24H2" connu, fix par TCP-only | Acceptable mais Win-Win only |
| **VNC** | 50-100 ms | divers | 8-24 bpp | Pauvre (latence input) | Variable | Éviter |
### 3.2. NoMachine 9.5.7 — analyse freeze
**Pattern documenté** (forum officiel NoMachine, multiples threads depuis v4) :
- *"NoMachine stops accepting mouse and keyboard input"* — persiste v4 → v8, **pas de fix officiel**.
- *"Mouse click not working"*, *"Inputs suddenly stopped working"*.
- Workaround unique remonté : toucher physiquement la souris sur l'hôte pour débloquer.
**Cause probable** (lecture forum + connaissance archi) : NoMachine utilise du *passive grab* X11 pour propager les events Windows→Linux. Quand le buffer X11 est saturé (compositor lent, refresh display, sleep system court), le grab est libéré silencieusement mais NoMachine ne réinjecte plus les events.
**Conséquences pour RPA :**
1. **Les clics côté Léa Windows partent**, mais ne sont pas vus par l'host Linux.
2. Pas de feedback d'erreur — Léa croit avoir cliqué.
3. La capture côté Léa Windows continue à montrer l'écran AVANT le clic (puisque le compositor host n'a rien repeint).
4.**L'agent boucle sur "je vois l'écran avant clic, je reclique"** — c'est exactement le pattern du LoopDetector QW2.
**Recommandations P1 (cf. §5) :** instrumenter un heartbeat actif côté client Léa qui détecte l'écran figé > 30 s et **pause supervisée explicite** ("la connexion NoMachine semble figée, restart NoMachine puis reprendre ?").
### 3.3. Cradle (BAAI-Agents) — comment ils capturent en jeu vidéo Windows
**Cradle** n'est pas de Microsoft (commune confusion) mais de BAAI (Beijing Academy of AI). Code GitHub : `BAAI-Agents/Cradle`.
Leur stack capture (regard rapide du repo, à confirmer si pertinent) :
- Capture via **`mss`** également (!), avec `monitor_index` configurable
- Pour RDR2 / fullscreen exclusive DirectX : capture via **window-handle ciblé** par `pygetwindow` + `mss` sur la zone
- Pas de wrapper DXGI custom dans la branche main → ils sont confrontés aux mêmes bugs que nous, leur env est juste plus contrôlé (un seul écran, pas de remote desktop intermédiaire)
**Apprentissage :** Cradle n'a PAS résolu le problème, ils l'évitent en contrôlant l'environnement (PC dédié, un écran, pas de scaling). Notre setup remote desktop multi-écran est intrinsèquement plus difficile.
### 3.4. Patterns côté hôte vs côté viewer
| Approche | Description | Avantages | Inconvénients |
|---|---|---|---|
| **Capture côté hôte distant** (Windows Léa) | Léa capture localement Windows, envoie au serveur Linux | Pixels physiques natifs, pas de re-encoding NoMachine | Bug DPI, nécessite agent sur l'hôte |
| **Capture côté viewer Linux** (notre poste Dom) | Linux capture la fenêtre NoMachine via mss/dxcam Linux | Pas besoin d'agent Windows | "screenshots through screenshots", artefacts H.264 NoMachine, perte qualité OCR |
| **Hybride : agent host + screenshot fallback viewer** | Pondération selon dispo réseau | Robuste | Complexité, désync entre les 2 sources |
**Recommandation projet :** rester sur capture côté hôte (Léa Windows). C'est ce qui est implémenté et c'est la bonne décision : la qualité OCR sur capture re-encodée par NoMachine (H.264 lossy) est mauvaise (cf. bug OCR-DIRECT 8 mai sur tabs Easily). Si NoMachine devient bloquant, migrer vers **RustDesk auto-hébergé** plutôt que vers une capture côté viewer.
### 3.5. Heartbeat actif — détecter le freeze AVANT d'envoyer les clics
Pattern à implémenter côté Léa Windows (P1, cf. §5) :
1. Toutes les 5 s, capture pHash de l'écran complet.
2. Comparer au pHash N-1. Si identique pendant **> 30 s** ET un input a été émis dans cette fenêtre → considérer connexion gelée.
3. Notifier serveur via heartbeat enrichi : `remote_session_status: "frozen_suspected"`.
4. Côté serveur, basculer en `replay_paused` automatique, dialogue VWB *"Restart NoMachine ?"*.
Code de référence existant : `windows.forum.nomachine.com` confirme que toucher physiquement la souris débloque. Donc le restart NoMachine est la bonne action de récupération — mais il faut un humain.
---
## 4. Recommandations P0 — Fix bug coord Y intermittent (`executor.py:606-617`)
Le code actuel `_resolve_via_uia_local` (lignes 606-617 d'`executor.py`) n'est PAS l'origine du bug coord Y — c'est UIA, pas la capture. Le bug racine est dans `capturer.py` (déjà partiellement traité par `_acquire_safe_grab`) ET dans le manque de déclaration DPI au démarrage.
**Fix recommandé en 2 temps :**
### Fix #1 — Déclarer DPI awareness au démarrage du client Léa
Ajouter en **première ligne** de `agent_v0/agent_v1/main.py` (avant tout autre import) :
```python
# agent_v0/agent_v1/main.py — TOUT EN HAUT, avant tout import
import platform
if platform.system() == "Windows":
import ctypes
try:
# DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4
# Cf. mss issue #197 : sans ça, mss.monitors retourne intermittemment
# des dims tronquées (cas observé 2560×60 démo GHT 19 mai 2026).
ctypes.windll.user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(-4))
except (AttributeError, OSError):
try:
ctypes.windll.shcore.SetProcessDpiAwareness(2) # PER_MONITOR (Win 8.1)
except Exception:
try:
ctypes.windll.user32.SetProcessDPIAware() # legacy
except Exception:
pass # fallback silencieux si vraiment ancien
```
**Validation attendue :** après ce fix + restart Léa, `mss.monitors[1]` doit toujours retourner `2560×1600` (jamais `2560×60`). Si le bug persiste → c'est un autre composant qui modifie le DPI context après start (suspect : PyQt5 GUI). Investiguer via instrumentation.
### Fix #2 — Renforcer `_acquire_safe_grab` en filet de sécurité
Le code actuel de `capturer.py:115-203` est **déjà solide**. Une seule amélioration : refuser le fallback secondaire dans **toutes** les méthodes coord-bearing (déjà fait pour `capture_dual`, vérifier que `capture_active_window` standalone fait pareil — c'est le cas L:371).
**Aucune modification recommandée sur le fichier** dans le scope court terme. Le fix #1 suffit en théorie ; `_acquire_safe_grab` est la ceinture, pas la cause.
### Fix #3 — Coordonnées agent serveur
Vérifier dans `executor.py` autour de L:606-617 (et plus largement) que **toute coord renvoyée au serveur** est en pixels physiques absolus du virtual desktop. Si ailleurs dans le code des pourcentages sont calculés AVEC `mss.monitors[1]['height']` qui peut être 60 → division par 60 → `y_pct × 1600 = grand nombre`. **Le bug Y ÷27 vient probablement de là, pas du clic en sortie**.
Action : `grep -rn "monitors\[" agent_v0/agent_v1/` et auditer chaque site. Hors scope de ce doc.
---
## 5. Recommandations P1 — Détection + recovery freeze NoMachine
### Détection (côté client Léa)
Ajouter à `capturer.py` un thread heartbeat de monitoring :
```python
# pseudocode pour ajout heartbeat dans VisionCapturer
import threading, time
class FreezeDetector(threading.Thread):
def __init__(self, capturer, on_freeze_callback,
interval_s=5.0, freeze_threshold_s=30.0):
super().__init__(daemon=True)
self.capturer = capturer
self.callback = on_freeze_callback
self.interval = interval_s
self.threshold = freeze_threshold_s
self._last_hash = None
self._last_change_ts = time.time()
self._stop = threading.Event()
def run(self):
while not self._stop.is_set():
try:
_mon, img = _acquire_safe_grab()
if img is not None:
h = self.capturer._compute_quick_hash(img)
now = time.time()
if h != self._last_hash:
self._last_hash = h
self._last_change_ts = now
elif (now - self._last_change_ts) > self.threshold:
self.callback(stale_for_s=(now - self._last_change_ts))
self._last_change_ts = now # éviter re-trigger en rafale
except Exception:
pass
self._stop.wait(self.interval)
def stop(self):
self._stop.set()
```
### Recovery (côté serveur / VWB)
Sur réception d'un heartbeat enrichi `remote_session_status="frozen_suspected"` :
1. Pause replay (`replay_paused=True`) + bulle Léa *"Connexion NoMachine figée détectée"*.
2. Dialog VWB côté Dom : `[Restart NoMachine] [J'ai débloqué, reprendre] [Stop]`.
3. Tracer le freeze dans `data/runner_captures/freeze_events.jsonl` pour stats post-démo.
---
## 6. Dépendances / liens avec AXE B1 (transport)
Le bug coord Y et le freeze NoMachine ont une **interaction critique** avec le bug transport diagnostiqué le 8 mai (`REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md`) :
- Si client Léa freeze (NoMachine) ET timeout HTTP client = 5 s → l'action est doublement perdue.
- Le watchdog `_retry_pending` côté serveur (fix moyen-terme du doc 8 mai) doit s'articuler avec le heartbeat freeze : ne PAS re-dispatcher une action si la session client est suspectée gelée (sinon empilement).
Recommandation transverse : **ne pas implémenter le watchdog `_retry_pending` sans intégrer le heartbeat freeze**. Sinon on multiplie les clics fantômes pendant un freeze.
---
## 7. Sources
### Bug `mss` DPI / monitors
- [BoboTiG/python-mss#197 — GetSystemMetrics wrong after sct.grab](https://github.com/BoboTiG/python-mss/issues/197) — root cause DPI shift
- [BoboTiG/python-mss#30 — monitors does not correspond with screen resolution](https://github.com/BoboTiG/python-mss/issues/30)
- [BoboTiG/python-mss#108 — combined monitor image and monitors incorrect](https://github.com/BoboTiG/python-mss/issues/108)
- [BoboTiG/python-mss#49 — secondary screen Windows 8.1](https://github.com/BoboTiG/python-mss/issues/49)
- [BoboTiG/python-mss#257 — scaling factor 2 applied](https://github.com/BoboTiG/python-mss/issues/257)
- [Lightrun answer — GetSystemMetrics wrong screen resolution](https://lightrun.com/answers/bobotig-python-mss-getsystemmetrics-module-returns-wrong-screen-resolution-after-running-sctgrabmonitor)
### DXGI / dxcam / windows-capture
- [ra1nty/DXcam — high-performance Python DXGI Desktop Duplication, updated 2026](https://github.com/ra1nty/DXcam)
- [DXcam Releases](https://github.com/ra1nty/DXcam/releases)
- [dxcam PyPI](https://pypi.org/project/dxcam/0.1.0.dev2/)
- [NiiightmareXD/windows-capture — Rust+Python DXGI+WGC](https://github.com/NiiightmareXD/windows-capture)
- [windows-capture PyPI](https://pypi.org/project/windows-capture/)
- [RootKit-Org/BetterCam — DXcam fork 240+Hz](https://github.com/RootKit-Org/BetterCam)
- [Microsoft Learn — Desktop Duplication API](https://learn.microsoft.com/en-us/windows/win32/direct3ddxgi/desktop-dup-api)
- [microsoft/Windows-classic-samples — DXGIDesktopDuplication](https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/DXGIDesktopDuplication/README.md)
- [Kyle Fu — Python Fast Screen Capture benchmark](https://kylefu.me/2023/02/18/python-fast-screen-capture.html)
- [ScreenshotOne — How to Capture Desktop Screen with DXcam in Python](https://screenshotone.com/blog/dxcam-python-screenshots/)
- [SerpentAI/D3DShot — historique, quasi abandonné](https://github.com/SerpentAI/D3DShot)
### DPI awareness Windows
- [Microsoft Learn — SetProcessDpiAwarenessContext](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setprocessdpiawarenesscontext)
- [Microsoft Learn — Setting default DPI awareness for a process](https://learn.microsoft.com/en-us/windows/win32/hidpi/setting-the-default-dpi-awareness-for-a-process)
- [Microsoft Learn — DPI_AWARENESS_CONTEXT handle](https://learn.microsoft.com/en-us/windows/win32/hidpi/dpi-awareness-context)
- [Win32 DPI And Monitor Scaling — gist marler8997](https://gist.github.com/marler8997/9f39458d26e2d8521d48e36530fbb459)
- [Python issue 33656 — IDLE DPI awareness on Windows](https://bugs.python.org/issue33656)
### NoMachine freeze (forum officiel)
- [NoMachine stops accepting mouse and keyboard input](https://forum.nomachine.com/topic/nomachine-stops-accepting-mouse-and-keyboard-input)
- [Mouse click not working](https://forum.nomachine.com/topic/mouse-click-not-working)
- [NoMachine connects but doesn't accept keyboard or mouse input](https://forum.nomachine.com/topic/nomachine-connects-but-doesnt-accept-keyboard-or-mouse-input)
- [Inputs suddenly stopped working](https://forum.nomachine.com/topic/inputs-suddenly-stopped-working)
- [Mouse click unresponsive until Alt-key pressed](https://forum.nomachine.com/topic/mouse-click-unresponsive-until-alt-key-pressed)
- [Mouse freeze after forwarding](https://forum.nomachine.com/topic/mouse-freeze-after-forwarding)
### Alternatives remote desktop
- [Bundl — RustDesk vs Parsec 2026](https://bundl.run/compare/rustdesk-vs-parsec)
- [QuantVPS — Parsec vs RDP vs Rustdesk](https://www.quantvps.com/blog/parsec-vs-rdp-vs-rustdesk)
- [Ultimate Systems Blog — AnyDesk vs Parsec vs RustDesk Showdown](https://blog.usro.net/2025/06/anydesk-vs-parsec-vs-rustdesk-showdown/)
- [Fileion — AnyDesk vs RustDesk 2026](https://fileion.com/blog/anydesk-vs-rustdesk-best-free-remote-desktop-2026)
- [AlternativeTo — NoMachine Alternatives](https://alternativeto.net/software/nomachine/)
- [Cendio ThinLinc — NoMachine Alternative](https://www.cendio.com/blog/nomachine-alternative/)
- [TechTarget — Windows 11 Remote Desktop freezing](https://www.techtarget.com/searchvirtualdesktop/tip/What-to-do-when-a-Windows-11-remote-desktop-keeps-freezing)
- [Windows Forum — RDP Freezing 24H2](https://windowsforum.com/threads/how-to-fix-rdp-freezing-issues-in-windows-11-24h2-complete-guide.362476/)
### Citrix / RDP / accessibility tree
- [Citrix Docs — Graphics policy settings](https://docs.citrix.com/en-us/xenapp-and-xendesktop/7-15-ltsr/policies/reference/ica-policy-settings/graphics-policy-settings.html)
- [Citrix Docs — Visual display policy](https://docs.citrix.com/en-us/xenapp-and-xendesktop/7-15-ltsr/policies/reference/ica-policy-settings/visual-display-policy-settings.html)
- [Citrix CTX202687 — HDX Graphics Modes DCR/Thinwire/H.264](https://support.citrix.com/article/CTX202687)
- [UiPath — Citrix Automation](https://www.uipath.com/platform/agentic-automation/ai-ecosystem/citrix-automation)
### Cradle / agent computer use
- [BAAI-Agents/Cradle](https://github.com/BAAI-Agents/Cradle)
- [arXiv 2403.03186 — Cradle: Empowering Foundation Agents Towards General Computer Control](https://arxiv.org/abs/2403.03186)
- [Cradle project page](https://baai-agents.github.io/Cradle/)
- [CursorTouch/Windows-MCP — MCP Server for Computer Use in Windows](https://github.com/CursorTouch/Windows-MCP)
---
*Document de recherche, lecture seule. Toute implémentation nécessite arbitrage explicite de Dom — la migration `mss → dxcam` est NON urgente tant que la ceinture `_acquire_safe_grab` tient. Le fix DPI awareness §4.1 est par contre **chirurgical, 8 lignes, à valider rapidement** (post-démo client Anouste).*

View File

@@ -0,0 +1,352 @@
# AXE C — Shadow learning, Fine-tuning VLM grounding, Memory store visuel
**Date :** 2026-05-23
**Auteur :** Claude (agent recherche prospective)
**Statut :** lecture seule, recherche externe + croisement avec dette interne (DETTE-005, DETTE-009). Aucune modif de code. Sources web < 6 mois priorisées.
**Périmètre :** 3 sous-axes prospectifs sur l'apprentissage et la mémoire visuelle pour rpa_vision_v3.
---
## 1. TL;DR + recommandation priorisée
**Insight central :** les 3 axes (C1 Shadow, C2 Fine-tuning, C3 Memory) ne sont **pas substituables** mais **séquentiels** :
- **C3 (memory)** est l'**investissement le plus immédiatement rentable** : `VisualEmbeddingManager` est déjà écrit, le pattern Skyvern (cache + fallback IA) est éprouvé à 10100× speed-up, et ça réduit la latence de la démo SANS toucher au grounding.
- **C1 (shadow)** est la **collecte de carburant** pour C2 : sans traces propres (DETTE-009), pas de dataset de fine-tuning Easily Assure. `ShadowLearningHook` orphelin = blocage prioritaire à lever.
- **C2 (fine-tuning)** est le **différenciateur à 612 mois** : la littérature 20252026 (Visual-RFT, UI-R1, GUI-R1, SE-RFT, GUI-Actor) prouve qu'on peut atteindre SOTA avec **3k10k exemples** via GRPO sur Qwen2.5-VL-3B/7B, pour quelques dizaines d'euros HF Jobs. Mais ça suppose un dataset propre, donc C1 d'abord.
**Séquence recommandée :**
1. **Vague courte (12 semaines)** : activer `VisualEmbeddingManager` (C3) en cache opportuniste devant la cascade `_resolve_target` + activer `ShadowLearningHook` (C1) sur les replays réussis. Aucun ML, juste du câblage.
2. **Vague moyenne (12 mois)** : collecter ≥ 1k traces Shadow propres sur Easily Assure via la démo récurrente, formaliser le format de trace inspiré OpenAdapt (SQLite trajectoires).
3. **Vague longue (36 mois post-démo client)** : fine-tuning GRPO Qwen2.5-VL-3B sur 3k5k exemples Easily Assure spécifiques, via HF Jobs ou DGX Spark. Coût attendu < 50 €.
**Dépendance critique avec AXE_B2 Validator** : un Shadow learning sur traces sales (replay qui croit avoir réussi mais a cliqué à côté — cf. bug step 10 Imagerie/bandeau Edge) injecte du poison dans la mémoire. **Le Validator sémantique doit précéder l'activation de C1**, sinon `SignatureStore` accumule des faux positifs.
---
## 2. C1 — Shadow learning : OpenAdapt + ShadowLearningHook orphelin
### 2.1 OpenAdapt — pipeline détaillé
**Repo :** [github.com/OpenAdaptAI/OpenAdapt](https://github.com/OpenAdaptAI/OpenAdapt) (~7k★ mai 2026)
**Architecture meta-package modulaire** (refonte récente vs monorepo initial) :
| Sous-paquet | Rôle |
|---|---|
| `openadapt-capture` | Enregistrement actions utilisateur + screenshots → SQLite. Paires **observation/action** = "trajectoires". |
| `openadapt-privacy` | Scrub PII/PHI avant stockage (intérêt direct pour notre démo médicale). |
| `openadapt-retrieval` | Index sémantique (FAISS-like) sur les démonstrations stockées. |
| `openadapt-ml` | Moteur ML : charge trajectoires, fine-tune VLM, génère templates automation. |
| `openadapt-grounding` | Mappe intention → coords UI au runtime. |
| `openadapt-evals` | Bench sur benchmarks publics. |
**Pipeline complet :** `Demonstrate → Learn → Execute`. La devise "**success traces become new training data**" (citée `INSPIRATION_FRAMEWORKS_2026-05-10.md` §5) est instanciée par : `openadapt-capture` (Demonstrate) → `openadapt-ml` (Learn fine-tune VLM) → `openadapt-grounding` (Execute conditionné sur les démos via retrieval ET fine-tuning, combinés).
**Format de trace** (déduit du repo, pas accessible en détail sans cloner) : table SQLite `recordings` + tables `screenshots`, `action_events`, `window_events`, chaînées par `recording_id`. Chaque clic = ligne `action_events` avec coords + screenshot lié + window title. **Schéma très proche** de notre `data/runner_captures/` actuel + métadonnées agent_v0.
**Différenciateur** : OpenAdapt **conditionne au runtime** l'agent sur des démos humaines récupérées via retrieval sémantique. C'est la combo cache + fine-tuning + grounding que rpa_vision_v3 a en pièces détachées (TargetMemoryStore Phase 1 = retrieval embryonnaire, ShadowLearningHook = capture, fine-tuning = TODO).
### 2.2 Comment activer `ShadowLearningHook` orphelin (DETTE-009)
Constat investigation `INVESTIGATION_MEMOIRE_VISUELLE_ORPHELINE_2026-05-09.md` :
- Fichier : `core/grounding/shadow_learning_hook.py` (156 lignes, commit `73cea2385` du 2026-04-25, "Phase 6").
- **0 site d'instanciation** au runtime.
- Attend un callback `on_click_observed(x, y, screenshot, window_title, target_label)` qui n'est jamais appelé.
- Le `ShadowObserver` (`core/workflow/shadow_observer.py`) ne configure aucun callback de ce type.
- Phase 5 (`e2046837c`) câble FAST→SMART→THINK dans ORA mais oublie d'instancier la Phase 6.
**Hooks d'activation envisageables** (à valider avec Dom avant code) :
| Point d'accroche | Avantage | Risque |
|---|---|---|
| Callback dans `ShadowObserver._on_mouse_click` | Apprentissage en temps réel sur démo humaine | Pollution si Validator absent (cf. dépendance B2) |
| Hook post-success dans `replay_engine.py` (après REPORT success=True) | Apprentissage uniquement sur traces "vérifiées" | Faux positifs si pHash global laxiste (bug step 10 connu) |
| Job offline batch sur `data/runner_captures/<session>/events.jsonl` | Aucun risque runtime, repassable | Pas de boucle d'amélioration continue |
**Recommandation :** commencer par le **job batch offline** (sécurisé), puis migrer vers post-success hook quand AXE_B2 Validator sémantique sera en place. Ne PAS activer sur `ShadowObserver` direct tant que Validator pHash global est en vigueur — c'est précisément ce que `feedback_phash_vs_dialog_in_vm.md` reproche.
### 2.3 Autres frameworks — patterns Shadow 20252026
- **Skyvern** : pattern "**explore → replay**" déterministe. Quand un agent réussit une tâche, le sélecteur (ou la coord visuelle) est mémorisé dans un cache indexé par `cache_key` Jinja2 rendu à partir des paramètres du workflow. Réutilisation 10100× plus rapide. Si le cache miss ou si replay échoue, fallback IA + mise à jour du cache. ([deepwiki](https://deepwiki.com/Skyvern-AI/skyvern/1-overview))
- **browser-use** : tableau `learned_skills` avec `success_rate`, `usage_count`, `last_used`. Modèle de skills nommés réutilisables. ([source](https://lobehub.com/skills/saik0s-mcp-browser-use-browser-use))
- **AGUVIS** : 4.2M trajectoires GUI multimodales (grounding + planning). Format normalisé multi-OS. ([aguvis-project](https://aguvis-project.github.io/))
- **UGround** : 10M éléments / 1.3M screenshots, ~95% web. Dataset le plus volumineux du domaine. ([osu-nlp-group](https://osu-nlp-group.github.io/UGround/))
### 2.4 Datasets RPA traces publiquement disponibles
Repérés via [Computer-Browser-Phone-Use-Agent-Datasets](https://github.com/Khang-9966/Computer-Browser-Phone-Use-Agent-Datasets) :
- **AGUVIS** — 4.2M sample elements + 1.3M trajectoires (license à vérifier).
- **UGround** — 10M GUI elements, ~95% web (license à vérifier).
- **ScreenSpot / ScreenSpot-v2 / ScreenSpot-Pro** — benchmarks d'évaluation, pas vraiment training (mais utilisables en few-shot).
- **AndroidControl** — mobile, utilisé par UI-R1.
- **Mind2Web** — web tasks.
- **WebVoyager** — Skyvern à 85.85%, benchmark.
**Aucun de ces datasets ne couvre Easily Assure ni le domaine hospitalier français**. Ils servent de **base pré-entraînement**, pas de **dataset cible**. Notre asset : les traces internes de la démo MOREL Catherine et autres.
### 2.5 Recommandation d'intégration C1
**Court terme (12 semaines)** :
1. Job batch offline : itérer sur `data/runner_captures/<session>/events.jsonl` post-démo réussie, appeler `ShadowLearningHook.on_click_observed` rétrospectivement → enrichit `SignatureStore`.
2. Mesurer le hit-rate de `SignatureStore` au replay suivant.
**Moyen terme (1 mois)** :
3. Définir notre **format de trace canonique** (inspirer du schéma OpenAdapt SQLite + privacy scrub avant tout).
4. Brancher le hook sur `ShadowObserver._on_mouse_click` UNIQUEMENT après merge du Validator sémantique (B2).
**Anti-pattern à éviter** : activer le hook sur des replays VWB sans Validator. Cf. bug step 10 mai : on apprendrait à cliquer dans le bandeau Edge au lieu du tab Imagerie.
---
## 3. C2 — Fine-tuning VLM grounding 20252026
### 3.1 Table comparée des techniques
| Méthode | Modèle base | Données | Gain vs SFT | Source |
|---|---|---|---|---|
| **Visual-RFT** (mars 2025) | Qwen2-VL-2B / 7B | **239 images** (LISA few-shot) à 500 steps | +24.3% classif, +21.9 mAP COCO 2-shot | [arxiv 2503.01785](https://arxiv.org/html/2503.01785v1) |
| **UI-R1** (AAAI 2026, mars 2025) | Qwen2.5-VL-3B | **2k3k exemples** | +22.1% ScreenSpot, +6.0% ScreenSpot-Pro, +12.7% AndroidControl | [github UI-R1](https://github.com/lll6gg/UI-R1) |
| **GUI-R1** (avril 2025) | Qwen2.5-VL | **3k exemples** (0.02% du training OS-Atlas) | Bat OS-Atlas-7B SFT sur tous les bench | [arxiv 2504.10458](https://www.emergentmind.com/papers/2504.10458) |
| **GUI-G1** (mai 2025) | Qwen2.5-VL | R1-Zero style | Analyse théorique R1 grounding | [openreview](https://openreview.net/forum?id=1XLjrmKZ4p) |
| **SE-GUI / SE-RFT** (mai 2025) | 7B | **3k samples** | SOTA ScreenSpot-Pro auto-supervisé | [arxiv 2505.12370](https://arxiv.org/html/2505.12370) |
| **GUI-Actor** (NeurIPS 2025) | Qwen2.5-VL-7B | Coordinate-free grounding | 42.2/44.6 ScreenSpot-Pro | [microsoft/GUI-Actor](https://github.com/microsoft/GUI-Actor) |
| **GUI-CURSOR** (2025) | Qwen2.5-VL-7B | — | 88.8→93.9 SS-v2, 26.8→56.5 SS-Pro | [arxiv 2509.21552](https://arxiv.org/pdf/2509.21552) |
| **POINTS-GUI-G** (2026) | 8B | — | 59.9 SS-Pro, 66.0 OSWorld-G | [arxiv 2602.06391](https://arxiv.org/pdf/2602.06391) |
| **GUI-AIMA** (2025) | 3B | **509k samples / 101k screenshots** | SOTA data-efficient | [arxiv 2511.00810](https://arxiv.org/pdf/2511.00810) |
**Convergence claire** :
- **GRPO** (Group Relative Policy Optimization, originaire de DeepSeek-R1) > SFT pour le grounding GUI.
- **Reward function rule-based** : IoU pour box, accuracy 0/1 pour clic point, format compliance JSON.
- **Quelques milliers d'exemples suffisent** (3k10k) pour un gain SOTA sur Qwen2.5-VL-3B/7B base.
- Le **base model Qwen2.5-VL** domine la littérature (vs UI-TARS, vs InfiGUI).
### 3.2 Datasets et licences
| Dataset | Volume | License | Utilisable commercialement ? |
|---|---|---|---|
| ScreenSpot | 1.2k instructions | À vérifier (paperswithcode) | Probable academic only |
| ScreenSpot-v2 | corrigé v1 | OS-Copilot/ScreenSpot-v2 HF | À vérifier |
| ScreenSpot-Pro | high-res pro | likaixin2000/ScreenSpot-Pro-GUI-Grounding | À vérifier |
| UGround | 10M elements | OSU-NLP-Group | Academic, vérifier CC-BY-NC |
| AGUVIS | 4.2M | aguvis-project | Académique, vérifier |
| UI-R1 (modèle + data) | 2k3k | **Apache-2.0** ✅ | Commercial OK |
| GUI-Actor | — | Microsoft, MIT-like | Commercial OK |
**Action immédiate** : **lire chaque LICENSE avant entraînement commercial**. La conclusion forte est qu'**aucun dataset public ne couvre Easily Assure**. La valeur sera dans **notre propre dataset interne** dérivé des traces démo.
### 3.3 Plan concret pour fine-tuner sur Easily Assure
**Hypothèse de travail** : on cible un fine-tuning **complémentaire** au modèle de base (Qwen2.5-VL-3B ou Qwen3-VL-8B), spécifique au domaine Easily Assure (UI typique, fonts, layout, vocabulaire médical FR).
**Étapes proposées (à valider Dom)** :
1. **Collecte** (C1) : 100 sessions Shadow réussies × ~30 clics utiles = **3 000 paires (screenshot, target_text, click_xy)**. Ratio compatible UI-R1 / GUI-R1.
2. **Format** : JSONL `{image_path, instruction, bbox_xyxy_or_point, screen_resolution}`. Pré-process via `smart_resize` officiel (DETTE-014 résolue d'abord).
3. **Anonymisation** : appliquer `core/anonymize/*` (déjà existant t2a) sur chaque crop avant export. Critique pour healthtech.
4. **Méthode** : **GRPO + LoRA** sur Qwen2.5-VL-3B base.
- Reward function : IoU > 0.5 (cible 1.0, sinon 0) + format JSON valide.
- 5001000 steps suffisent (cf. Visual-RFT 200 steps + 500 grounding).
5. **Hardware** : **HF Jobs / DGX Cloud H100** (8.25 $/h, [source](https://huggingface.co/blog/train-dgx-cloud) — ATTENTION : service deprecated avril 2025, vérifier alternative HF Jobs actuel). Ou **DGX Spark** Dom roadmap (`memory/project_roadmap_vision.md`).
- Alternative cloud GPU 2026 : H100 $4.50$36/h AWS, $2.99/h en marketplace ([spendark](https://spendark.com/blog/machine-learning-cloud-cost/)).
6. **Durée estimée** : 812h sur 1× H100 (basé sur QLoRA 7B), ou 46h sur 8× H100 parallèle.
7. **Coût estimé** : **3080 €** (1 run complet) ou **< 10 €** si A100 marketplace + QLoRA 3B.
8. **Évaluation** : split 80/20 sur les traces internes + non-régression sur ScreenSpot-Pro public.
### 3.4 LoRA vs Full Fine-Tuning — recommandation
**Source clé :** [arxiv 2410.21228 "LoRA vs Full Fine-tuning: An Illusion of Equivalence"](https://arxiv.org/pdf/2410.21228) — LoRA introduit des "intruder dimensions" qui réduisent la généralisation hors-domaine.
**Mais pour notre cas (spécialisation Easily Assure)** :
- **On NE VEUT PAS généraliser** — on veut sur-fit (sainement) une UI spécifique.
- LoRA / QLoRA suffisent : Qwen2.5-VL-3B + LoRA tient sur **RTX 4070 12 GB** ([source](https://datature.io/blog/how-to-fine-tune-qwen2-5-vl)).
- Full FT sur 3B "hit practical limits with unstable loss curves and VRAM pressure" ([kaitchup](https://kaitchup.substack.com/p/qwen25-qlora-lora-and-full-fine-tuning)).
**Reco :** **QLoRA 4-bit sur Qwen2.5-VL-3B**, rang 1664. Iteration locale possible sur le RTX 5070 du Dom. Pas besoin HF Jobs pour le prototype. HF Jobs uniquement pour les runs de production.
**À éviter** : full FT 7B+ pour un usage Easily Assure. ROI insuffisant face à la complexité ops.
---
## 4. C3 — Memory store visuel
### 4.1 Skyvern prompt caching — architecture détaillée
**Sources :** [skyvern deepwiki](https://deepwiki.com/Skyvern-AI/skyvern/1-overview), [skyvern blog MCP](https://www.skyvern.com/blog/mcp-server-architecture-explained/), [zread optimization](https://zread.ai/Skyvern-AI/skyvern/29-optimization-strategies)
**Mécanisme** :
1. **Cache key** : template Jinja2 rendu à partir des paramètres du workflow (URL cible, valeurs de form, etc.).
2. **Stockage** : quand une action AI réussit, Skyvern stocke le `selector_path` (DOM) ou la coord visuelle.
3. **Replay** : runs suivants avec mêmes paramètres → match cache_key → exécute le script pré-généré (Python ou JS), **sans appeler le LLM**.
4. **Fallback** : si replay échoue (UI a changé), AI re-engage automatiquement et **met à jour le cache**.
**Gain mesuré :** **10100× speed-up** sur runs cachés. "Deterministic replay flags if a price or SKU changes" — Skyvern utilise les changements détectés comme signal d'invalidation.
**Différence vs Anthropic/OpenAI "prompt caching" classique** : Skyvern cache **les artefacts d'exécution** (scripts/sélecteurs), pas les tokens du prompt. C'est plus proche de notre `TargetMemoryStore` que d'un cache LLM.
**Transposable à rpa_vision_v3** :
- Cache key = `(workflow_id, step_id, target_label, target_description, screen_context_hash)`.
- Cache value = `(click_xy, source: ocr|template|vlm, confidence, last_validated_at)`.
- Invalidation = pHash zone d'intérêt change OU REPORT failure récent.
### 4.2 FAISS + CLIP/DINOv2 vs ColPali/ColQwen — comparaison
| Approche | Index size | Latence query | Granularité | Pertinence UI |
|---|---|---|---|---|
| **CLIP global + FAISS** | 1 vec/image (512768d) | < 1 ms | Image entière | Bonne pour "ai-je déjà vu cet écran" |
| **DINOv2 + FAISS** | 1 vec/image (7681536d) | < 1 ms | Image entière | Meilleur que CLIP en self-sup, robuste aux occlusions ([source](https://medium.com/aimonks/image-similarity-with-dinov2-and-faiss-741744bc5804)) |
| **DINOv2 crops + FAISS** | 1 vec/widget (768d) | 15 ms | Par widget | Cas idéal pour "ai-je déjà vu ce bouton dans CE contexte" |
| **ColPali / ColQwen** | N patches × 128d | 550 ms (late interaction) | Patches × tokens query | SOTA documents visuels, plus lourd, [Nemotron ColEmbed V2](https://arxiv.org/html/2602.03992v1) NDCG@10=63.42 sur Vidore V3 |
| **UISearch** ([arxiv 2511.19380](https://arxiv.org/pdf/2511.19380)) | Graph attributé | 47.5 ms median | Hiérarchie + spatial | 0.92 Top-5 sur 20k UIs financières |
**Recommandation pour rpa_vision_v3 :**
- **Niveau 1 (cache exact)** — `pHash` zone d'intérêt (déjà partiellement utilisé). Latence < 1 ms.
- **Niveau 2 (cache flou widget)** — **DINOv2-base crops + FAISS** indexé par widget. Réutilise `VisualEmbeddingManager` (déjà écrit, DETTE-005) mais swap CLIP → DINOv2 (gain mesuré sur déduplication, [encord](https://encord.com/blog/dinov2-self-supervised-learning-explained/)).
- **Niveau 3 (recherche sémantique cross-screens)** — différé. ColPali/ColQwen attrayants mais lourd à déployer ; UISearch très prometteur mais code non-public.
**Verdict :** **FAISS + DINOv2** = bon compromis pragmatique 2026. ColPali = différé si besoin de retrieval cross-écrans riches.
### 4.3 Comment activer `VisualEmbeddingManager` orphelin (DETTE-005)
Constat investigation 2026-05-09 :
- `core/visual/visual_embedding_manager.py` — 651 lignes, commit `a27b74cf2` (2026-01-29).
- `core/visual/screenshot_validation_manager.py` — 571 lignes, idem.
- **0 site d'instanciation runtime**, ni VEM ni SVM.
- Investigation recommandait **ARCHIVE** sauf cas d'usage prod identifié, **car redondant avec `SignatureStore` + `fusion_engine.embedding_cache`**.
**Réévaluation à la lumière de l'AXE_C :**
Le verdict "archivage" du 9 mai était correct dans le cadre étroit "ce composant est-il actuellement utile ?". Mais l'AXE_C ouvre un cas d'usage clair :
- **VEM = couche cache widget visuel** devant la cascade `_resolve_target`.
- **SVM = validation continue** des targets stockées (cache invalidation par re-validation périodique).
**Recommandation révisée (à statuer Dom)** :
| Option | Pro | Contra |
|---|---|---|
| **A. Archiver VEM/SVM (verdict initial), refaire propre dans `core/grounding/`** | Pas de dette de réécriture, unifié avec `SignatureStore` | Perd 1200 lignes + tests existants |
| **B. Réactiver VEM/SVM en swappant backbone CLIP → DINOv2** | Tests existants, archi serializer HMAC déjà signée | Architecture dupliquée avec `SignatureStore`, risque incohérence |
| **C. Hybride : extraire la logique de cache HMAC + serializer signé de VEM dans `SignatureStore`, archiver le reste** | Best-of-both, signature crypto réutilisée | 1 jour de refactor |
**Mon vote** : **C**, après livraison démo client. Mais **acter B comme MVP rapide** si la fenêtre est < 1 semaine et qu'on veut tester C3 sans refactor lourd.
### 4.4 Cache invalidation — quand la mémoire devient stale
**Stratégies observées dans la littérature 20252026 :**
1. **Re-validation périodique** (SVM ScreenshotValidationManager prévoit ça) — recapture le widget, vérifie embedding similarity > seuil, sinon `STATUS=WARNING`.
2. **Invalidation sur échec** — Skyvern : si replay déterministe échoue, fallback IA + update cache.
3. **TTL versionné** — invalidation forcée à chaque changement de version du logiciel cible (Easily Assure update).
4. **Watermark pHash** — pHash du widget au moment du cache. Si match au runtime → réutilise. Sinon → re-grounding.
5. **Counter d'échecs**`success_rate`, `usage_count` (pattern browser-use). Si `success_rate < 0.6` après N usages → purge.
**Recommandation pour rpa_vision_v3 :**
- Combo **pHash watermark** (rapide) + **counter d'échecs glissant** (robuste). Pas de SVM périodique au runtime (coût pour bénéfice douteux).
- TTL nominal 7 jours, reset sur chaque succès vérifié par Validator sémantique (B2).
- **Hard requirement** : ne JAMAIS écrire dans le cache sans Validator sémantique OK. Sinon poison cache cf. bug step 10 mai.
---
## 5. Recommandation séquence — 3 vagues d'effort
### Vague 1 — Court terme (12 semaines, post-démo client)
**Objectif :** quick wins sans toucher ML.
1. **C3 niveau 1** : étendre le cache `pHash` existant aux widgets résolus avec succès. Storage SQLite simple.
2. **C1 batch offline** : script ad-hoc qui rejoue `data/runner_captures/<session>/events.jsonl` post-réussite, appelle `ShadowLearningHook.on_click_observed` rétroactivement. Mesure hit-rate `SignatureStore` au replay suivant.
3. **Définir le format de trace canonique** (inspirer OpenAdapt SQLite + anonymisation t2a-style).
**Coût :** ~35 j-h. **Risque :** très bas.
**Dépendance :** aucune (offline, hors chemin chaud).
### Vague 2 — Moyen terme (12 mois)
**Objectif :** collecter du dataset et fiabiliser le cache.
1. **Attendre AXE_B2 Validator sémantique** (cf. dépendance critique §1).
2. Une fois B2 livré : activer `ShadowLearningHook` sur callback post-success replay (PAS sur Shadow direct).
3. **C3 niveau 2** : prototyper DINOv2 + FAISS sur 100 widgets démo. Option B (VEM réactivé) ou C (refactor propre).
4. **Collecter ≥ 1k traces** Easily Assure propres via démos répétées + anonymisation.
**Coût :** ~510 j-h. **Risque :** moyen (Validator semantic = chemin critique).
**Dépendance forte :** AXE_B2 Validator sémantique merged.
### Vague 3 — Long terme (36 mois post-démo client)
**Objectif :** fine-tuning VLM spécifique Easily Assure.
1. Dataset 3k5k paires propres + anonymisées + smart_resize correct.
2. **GRPO + QLoRA 4-bit sur Qwen2.5-VL-3B** (méthode UI-R1 / Visual-RFT). 5001000 steps.
3. Run local RTX 5070 ou cloud H100 marketplace. **Coût attendu : 1080 €**.
4. Évaluation : split interne 80/20 + non-régression ScreenSpot-Pro.
5. Déploiement progressif : modèle fine-tuné en fallback du modèle base, A/B test sur démo.
**Coût :** ~1015 j-h + budget cloud < 100 €. **Risque :** moyen (succès très dépendant qualité dataset C1).
**Dépendance forte :** AXE_A1 (choix modèle base finalisé : Qwen2.5-VL-3B vs Qwen3-VL-8B vs UI-R1-3B comme nouveau base déjà fine-tuné).
---
## 6. Dépendances avec autres AXEs
- **AXE_B2 Validator** (collecte de traces propres) : **bloquant pour C1 sur replay live** + **prérequis pour cache C3 sans poison**. Tant que Validator pHash global laxiste (bug step 10) → C1 et C3 sur traces démo seulement.
- **AXE_A1 (modèles à fine-tuner)** : choix base entre Qwen2.5-VL-3B (UI-R1 le valide), Qwen3-VL-8B (`memory/reference_vlm_models.md` retient), InfiGUI-G1-3B (production actuelle). À trancher avant C2.
- **DETTE-014 smart_resize** : doit être résolue avant fine-tuning sinon coords mal calées dans le dataset.
---
## 7. Sources
### C1 — Shadow learning
- [OpenAdaptAI/OpenAdapt — GitHub](https://github.com/OpenAdaptAI/OpenAdapt)
- [Skyvern-AI/skyvern — DeepWiki overview](https://deepwiki.com/Skyvern-AI/skyvern/1-overview)
- [Skyvern optimization strategies — zread.ai](https://zread.ai/Skyvern-AI/skyvern/29-optimization-strategies)
- [browser-use skills tracking — lobehub](https://lobehub.com/skills/saik0s-mcp-browser-use-browser-use)
- [Computer-Browser-Phone-Use-Agent-Datasets](https://github.com/Khang-9966/Computer-Browser-Phone-Use-Agent-Datasets)
- [15 Datasets for Training and Evaluating AI Agents — ODSC, avril 2026](https://odsc.medium.com/15-datasets-for-training-and-evaluating-ai-agents-c171dde4e0ce)
### C2 — Fine-tuning VLM grounding
- [Visual-RFT — arxiv 2503.01785](https://arxiv.org/html/2503.01785v1)
- [UI-R1 (AAAI 2026) — GitHub lll6gg/UI-R1](https://github.com/lll6gg/UI-R1)
- [UI-R1 — arxiv 2503.21620](https://arxiv.org/html/2503.21620)
- [GUI-R1 — arxiv 2504.10458](https://www.emergentmind.com/papers/2504.10458)
- [GUI-G1 R1-Zero training — openreview](https://openreview.net/forum?id=1XLjrmKZ4p)
- [SE-RFT Self-Evolutionary — arxiv 2505.12370](https://arxiv.org/html/2505.12370)
- [GUI-Actor NeurIPS 2025 — microsoft/GUI-Actor](https://github.com/microsoft/GUI-Actor)
- [GUI-CURSOR — arxiv 2509.21552](https://arxiv.org/pdf/2509.21552)
- [POINTS-GUI-G — arxiv 2602.06391](https://arxiv.org/pdf/2602.06391)
- [GUI-AIMA — arxiv 2511.00810](https://arxiv.org/pdf/2511.00810)
- [ScreenSpot-Pro — arxiv 2504.07981](https://arxiv.org/html/2504.07981v1)
- [UGround — OSU NLP Group](https://osu-nlp-group.github.io/UGround/)
- [AGUVIS Project](https://aguvis-project.github.io/)
- [FocusUI CVPR 2026 — github.com/showlab/FocusUI](https://github.com/showlab/FocusUI)
- [LoRA vs Full Fine-tuning — arxiv 2410.21228](https://arxiv.org/pdf/2410.21228)
- [Qwen2.5 QLoRA / LoRA / Full FT comparison — kaitchup](https://kaitchup.substack.com/p/qwen25-qlora-lora-and-full-fine-tuning)
- [How to Fine-Tune Qwen2.5-VL — Datature](https://datature.io/blog/how-to-fine-tune-qwen2-5-vl)
- [HF Jobs / Train on DGX Cloud — HF blog](https://huggingface.co/blog/train-dgx-cloud) (⚠ deprecated avril 2025, vérifier alt)
- [GPU Cloud Pricing 2026 — spendark](https://spendark.com/blog/machine-learning-cloud-cost/)
- [How to Fine-Tune LLMs in 2026 — Spheron](https://www.spheron.network/blog/how-to-fine-tune-llm-2026/)
### C3 — Memory store visuel
- [Skyvern cache_key Jinja2 — deepwiki](https://deepwiki.com/Skyvern-AI/skyvern)
- [Skyvern MCP architecture — blog](https://www.skyvern.com/blog/mcp-server-architecture-explained/)
- [Late Interaction Retrieval (ColBERT, ColPali, ColQwen) — Weaviate](https://weaviate.io/blog/late-interaction-overview)
- [ColPali — arxiv 2407.01449](https://arxiv.org/pdf/2407.01449)
- [illuin-tech/colpali — GitHub](https://github.com/illuin-tech/colpali)
- [Nemotron ColEmbed V2 — arxiv 2602.03992](https://arxiv.org/html/2602.03992v1)
- [DINOv2 — Encord blog](https://encord.com/blog/dinov2-self-supervised-learning-explained/)
- [DINOv2 + FAISS image similarity — Medium](https://medium.com/aimonks/image-similarity-with-dinov2-and-faiss-741744bc5804)
- [UISearch graph embeddings UI screenshots — arxiv 2511.19380](https://arxiv.org/pdf/2511.19380)
- [State of AI Agent Memory 2026 — mem0.ai](https://mem0.ai/blog/state-of-ai-agent-memory-2026)
- [Best Vector Databases For Multimodal 2026 — acecloud](https://acecloud.ai/blog/best-vector-databases-for-multimodal-genai/)
---
*Document destiné à être lu en complément de `SYNTHESE_TECHNOS_REPLAY_2026-05-23.md`, `INSPIRATION_FRAMEWORKS_2026-05-10.md`, et `INVESTIGATION_MEMOIRE_VISUELLE_ORPHELINE_2026-05-09.md`. Toute action à prendre nécessite décision Dom.*

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,552 @@
# AXE D2 — Gestion des modaux & popups imprévus
**Date :** 2026-05-23
**Auteur :** agent recherche (dispatch session principale)
**Périmètre :** stratégie pour gérer en 100% vision les modaux Windows / navigateur / métier qui interrompent un replay Léa.
**Statut :** brief de recherche — aucune modification de code proposée. Décisions = Dom.
---
## 1. TL;DR + recommandation architecture
### 1.1. Diagnostic
- Le projet a déjà 80% de l'outillage : `core/grounding/dialog_handler.py` (EasyOCR + InfiGUI + fallback OCR direct) couvre la **résolution** d'un dialog connu.
- Trois lacunes documentées :
1. Côté **client Léa**, `_handle_possible_popup` est défini avec **0 site d'appel** (`LESSONS_LEARNED_GHT_2026-05.md`, F5.5.1) — il existe également un `_handle_popup_vlm` qui prend le relais mais sans déclencheur générique.
2. Pas de **détecteur d'apparition** (la cascade ne sait pas qu'un modal vient d'apparaître — elle continue à viser l'élément initial, qui est désormais masqué).
3. Pas de **politique de fail-safe** différenciée par type de modal (UAC ≠ "fichier déjà existe" ≠ Windows Hello).
- Anti-pattern interdit (`feedback_100pct_visual.md`) : inventer un raccourci Win+R / Ctrl+X / Échap-systématique. La cascade reste **OCR → template → VLM**, jamais "réflexe magique".
### 1.2. Recommandation
Une stack en trois couches, branchée APRÈS chaque action (et au démarrage du tick d'observation), réutilisant le code existant :
```
ChangeDetector ──► DialogClassifier ──► DialogResolver
(léger, < 50ms) (OCR titre + LLM) (catalogue déclaratif
+ InfiGUI/OCR + escalation)
```
1. **ChangeDetector** — détecte qu'un modal *vient d'apparaître*. Combinaison foreground-window-change (Windows API côté client) + screenshot diff région centrale + heuristiques visuelles (ombre, centrage). Latence cible < 50 ms.
2. **DialogClassifier** — décide *quoi faire* (catalog match → action déterministe ; sinon → VLM pour catégoriser). Réutilise `_read_title` + `KNOWN_DIALOGS` du `dialog_handler.py` actuel, étend avec une catégorisation par type (`UAC` / `HELLO` / `SMARTSCREEN` / `BROWSER_PERM` / `METIER_SAVE` / `INCONNU`).
3. **DialogResolver** — applique la **matrice modal → action** (§5). En santé : aucun auto-accept système (UAC/Hello/SmartScreen) — uniquement pause supervisée. Auto-dismiss déterministe **uniquement** pour les modaux métier déclarés dans le workflow (ex. "fichier déjà existe → Oui").
Cette stack résout **mécaniquement** la dépendance avec AXE_B2 (Validator) : un modal non détecté = un Validator post-action raté ; un Validator sémantique strict force le détecteur à être appelé avant le verdict.
---
## 2. Taxonomie des modaux Windows 11 / navigateur (mai 2026)
Sept catégories qui couvrent ~95% des cas terrain healthtech.
### 2.1. UAC (User Account Control)
- **Quand** : installation, élévation `runas`, modification système.
- **Aspect visuel** : fond bleu ou assombri global (secure desktop), titre `Contrôle de compte d'utilisateur`, deux boutons `Oui` / `Non` ou champ mot de passe administrateur.
- **Particularité** : sur **secure desktop**, screenshot Léa peut être noir/inaccessible (`pyautogui` ne voit rien). Microsoft a livré des fixes UAC en oct. 2025 → janv. 2026 (KB5063878 / KB5074109) — quelques replays plantent encore sur 24H2 idle/locked.
- **Politique healthtech recommandée** : **JAMAIS auto-accept**. Pause supervisée immédiate.
### 2.2. Windows Hello / WebAuthn / FIDO2
- **Quand** : déverrouillage app sensible (gestionnaire de mots de passe, Outlook), site avec WebAuthn, élévation UAC configurée pour exiger biométrie.
- **Aspect visuel** : popup système centré, icône empreinte ou caméra, message "Touchez le capteur" ou "Saisissez votre code PIN".
- **Particularité** : nécessite **interaction physique humaine** par construction. Aucune solution 100% vision ne peut résoudre. Anti-pattern absolu : tenter de cliquer "Annuler" pour passer outre.
- **Politique** : pause supervisée + **tip** suggérant "désactiver Windows Hello pour la session de démo" en config préalable.
### 2.3. Microsoft Defender SmartScreen
- **Quand** : exe non signé, téléchargement suspect, premier lancement Léa.
- **Aspect visuel** : bandeau bleu ciel "Windows a protégé votre PC", lien "Informations complémentaires" qui révèle le bouton "Exécuter quand même".
- **Politique** : pause supervisée + log security. Notre `feedback_auth_dialogs_runtime.md` rappelle d'anticiper AVANT démo client (signature code, allowlist hash SHA256 — voir `project_code_signing.md`).
### 2.4. Permissions navigateur (caméra, micro, notifications, géoloc, clipboard)
- **Quand** : première visite site `https://`, démo Easily Assure si elle demande accès clipboard / notifications.
- **Aspect visuel** : popup ancrée à l'URL bar (Chrome/Edge), boutons `Autoriser` / `Bloquer`. Variant inline overlay (Edge 2026).
- **Particularité critique sécurité (CVE-2026-0628 janv. 2026)** : malveillances via extensions Chrome qui détournent des permissions. Auto-accept = risque RGPD/HDS.
- **Politique** : **déclaratif dans le workflow** ("À cette étape, autoriser le micro") ou pause supervisée. Aucun auto-accept générique.
### 2.5. Dialog métier (sauvegarde, écrasement, perte de modifications)
- **Quand** : fermeture document non sauvé, "fichier existe déjà", "Voulez-vous quitter ?".
- **Aspect visuel** : popup applicative au-dessus de la fenêtre parente, OCR titre fiable, boutons texte clair (`Oui` / `Non` / `Annuler` / `Enregistrer`).
- **Politique** : **catalogue déclaratif**. C'est exactement ce que `KNOWN_DIALOGS` du `dialog_handler.py` actuel gère. Résolution InfiGUI + fallback OCR direct.
### 2.6. Avertissements / erreurs applicatives (popup OK)
- **Quand** : timeout réseau, validation backend KO, message info-utilisateur.
- **Aspect visuel** : popup centré, icône triangle/croix, un seul bouton `OK` / `Fermer`.
- **Politique** : auto-dismiss déterministe SAUF si message critique (regex blocklist "données perdues", "supprimé"). Log obligatoire.
### 2.7. Modaux inattendus / inconnus (notification push Teams, Outlook reminder, popup mise à jour, cookie banner, newsletter)
- **Quand** : tout le temps, surtout en démo live (Slack ping, Outlook reminder à xx:00, popup Windows Update).
- **Aspect visuel** : très variable. Souvent en coin (toast) plutôt que centré, mais peut voler le focus.
- **Politique** : VLM classifier (catégoriser : "publicité" / "système" / "métier inconnu") → pause supervisée avec proposition humaine ou auto-dismiss conservateur (Échap visuel via clic croix détectée).
---
## 3. Comparaison frameworks 2026
| Framework | Détection modal | Politique | Pause humaine | Catalogue déclaratif |
|---|---|---|---|---|
| **Anthropic Computer Use** (avril 2026) | classifiers anti-prompt-injection détectent screenshots suspects → demande confirmation user | permission-first par défaut, refuse "high-risk", monitor model peut pause | oui (Auto Mode mai 2026 = classifier décide auto-approve vs prompt) | non public |
| **OpenAI Operator / ChatGPT Agent** (juillet 2025 → 2026) | "monitor model" surveille comportement suspect ; CAPTCHA / login → cède le contrôle à l'humain (screenshots OFF pendant ce temps) | confirme avant action critique, prend la main sur prompts sécurité | oui, prend-le-relais explicite | non |
| **Skyvern 2.0** (oct. 2025) | **Validator** regarde écran post-action, détecte "popup blocked the click", redonne main au Planner pour retry | retry visuel ; built-in TOTP / 2FA / file downloads | non documenté publiquement | implicite via Validator |
| **browser-use** (issue #1996 ouvert mai 2026) | **pas de solution intégrée** — la communauté demande explicitement une infra générique | au cas par cas | non | non |
| **Cradle** (R&D fin 2025) | screenshot-only, demande au VLM principal d'identifier popup à chaque tick | dépend prompt | non | non |
| **PopSweeper** (recherche acad. 2024, applicable mobile) | classifier deux étages **ResNet50 + MobileNetV2** + **YOLO-World** pour bouton croix ; image-diff 100 ms tick ; **60 ms par frame** | auto-dismiss centré sur close-button | non | non |
| **rpa_vision_v3 (actuel)** | aucun détecteur ; `dialog_handler.py` ne s'exécute que si appelé explicitement | catalogue déclaratif + InfiGUI/OCR fallback | feedback_failure_is_learning oui | oui (`KNOWN_DIALOGS`) |
**Lecture** :
- Skyvern et OpenAI Operator **valident l'idée centrale** : un Validator strict détecte le modal indirectement (action attendue échoue → l'écran a changé sans cause connue → modal).
- Anthropic et OpenAI **convergent** sur le "monitor model" parallèle qui pause sans toucher au workflow principal.
- PopSweeper démontre qu'un détecteur **léger spécialisé** (CNN custom < 60 ms) suffit pour ne PAS ajouter de coût VLM à chaque tick.
- L'écosystème open source (browser-use) n'a **toujours pas** de solution générique — le sujet est ouvert.
---
## 4. Architecture proposée pour rpa_vision_v3
### 4.1. Vue d'ensemble
```
┌───────────────────────────────────────────┐
│ Léa (Windows client) │
│ │
action click ───►│ executor │
│ │ │
│ ▼ │
│ perform_click │
│ │ │
│ ▼ │
│ ChangeDetector ◄─── screenshot t+1 ──┐ │
│ │ │ │
│ ▼ │ │
│ is_modal_appeared? │ │
│ │ │ │
│ no ┴ yes │ │
│ │ │ │
│ ▼ │ │
│ DialogClassifier │ │
│ │ │ │
│ ▼ │ │
│ type ∈ {UAC, HELLO, SMART, PERM, │ │
│ SAVE, OK, INCONNU} │ │
│ │ │ │
│ ▼ │ │
│ DialogResolver │ │
│ │ │ │
│ ▼ │ │
│ policy(type, workflow_ctx) │ │
│ │ │ │
│ ▼ │ │
│ ┌─ AUTO_DISMISS (OK trivial) │ │
│ ├─ DECLARATIVE (catalog match) │ │
│ ├─ ASK_HUMAN (pause supervisée) │ │
│ └─ ESCALATE_SECURITY (log + pause) │ │
│ │ │
│ ▼ │ │
│ resume_or_wait │ │
│ │ │
└───────────────────────────────────────┘
serveur (api_stream)
report dialog event + replay state
```
### 4.2. Étendre `core/grounding/dialog_handler.py` existant
Le fichier actuel (lecture seule pour ce doc, voir Read) fait déjà :
- `_read_title` (EasyOCR full-screen, `fr+en`, GPU)
- catalogue `KNOWN_DIALOGS` ordonné (popups modaux prioritaires avant fenêtres parents)
- `_click_via_infigui` (UI-TARS / InfiGUI grounder déjà branché)
- `_click_via_ocr` (fallback OCR direct)
- retour `dict` avec `handled / title / dialog_type / action / position / time_ms`
**À ajouter (esquisses, pas de code à committer) :**
```python
# core/grounding/change_detector.py — nouveau fichier proposé
from dataclasses import dataclass
from typing import Optional, Tuple
import time
import numpy as np
@dataclass
class ChangeSignal:
"""Signal qu'un changement écran significatif vient d'apparaître."""
is_modal: bool # heuristique modal vs scroll normal
foreground_hwnd_changed: bool # côté Windows uniquement
diff_ratio: float # 0.0 = identique, 1.0 = tout différent
central_diff_ratio: float # diff zone centrale (modaux centrés)
timestamp: float
class ChangeDetector:
"""Détecte qu'un modal *vient d'apparaître* sans appeler le VLM principal.
Stratégie :
1) foreground window change (Windows API, ~1 ms)
2) screenshot diff centre vs périphérie (numpy diff sur sous-region, ~10 ms)
3) heuristique "centré + bordure assombrie" (modal pattern, ~5 ms)
Budget total cible : < 50 ms pour ne PAS ralentir la boucle replay.
"""
def __init__(self):
self._last_screenshot = None
self._last_hwnd = None
def detect(self, screenshot_pil) -> ChangeSignal:
t0 = time.time()
arr = np.asarray(screenshot_pil.convert("L"))
fg_changed = self._check_foreground_changed()
diff_ratio = 0.0
central_diff = 0.0
if self._last_screenshot is not None:
prev = np.asarray(self._last_screenshot.convert("L"))
if prev.shape == arr.shape:
diff = np.abs(prev.astype(int) - arr.astype(int))
diff_ratio = float((diff > 25).mean())
# zone centrale = modaux Windows typiques
h, w = arr.shape
cy0, cy1 = h // 4, 3 * h // 4
cx0, cx1 = w // 4, 3 * w // 4
central_diff = float((diff[cy0:cy1, cx0:cx1] > 25).mean())
is_modal = (
fg_changed
or (central_diff > 0.10 and diff_ratio < 0.40) # changement centré
)
self._last_screenshot = screenshot_pil
return ChangeSignal(
is_modal=is_modal,
foreground_hwnd_changed=fg_changed,
diff_ratio=diff_ratio,
central_diff_ratio=central_diff,
timestamp=t0,
)
def _check_foreground_changed(self) -> bool:
"""Côté Windows uniquement — sinon retourne False."""
try:
import ctypes # noqa
hwnd = ctypes.windll.user32.GetForegroundWindow()
changed = (self._last_hwnd is not None) and (hwnd != self._last_hwnd)
self._last_hwnd = hwnd
return changed
except Exception:
return False
```
**Note importante** : `feedback_popup_vlm.md` documente que `GetForegroundWindow` est **non fiable seul** (retourne 0 en SSH, popups Windows modernes partagent hwnd du parent). On l'utilise comme **signal complémentaire**, jamais comme source unique. La détection finale repose sur le diff écran (vision).
### 4.3. DialogClassifier — extension du `dialog_handler.py`
```python
# core/grounding/dialog_classifier.py — proposition
from enum import Enum
class DialogType(str, Enum):
UAC = "uac"
HELLO = "windows_hello"
SMARTSCREEN = "defender_smartscreen"
BROWSER_PERMISSION = "browser_permission"
METIER_SAVE = "metier_save" # match catalog KNOWN_DIALOGS
METIER_CONFIRM = "metier_confirm"
OK_TRIVIAL = "ok_trivial" # popup avec 1 seul bouton OK
INCONNU = "inconnu"
# Signatures texte par type (extension du catalogue actuel)
TYPE_SIGNATURES = {
DialogType.UAC: [
"contrôle de compte d'utilisateur",
"user account control",
"voulez-vous autoriser cette application",
],
DialogType.HELLO: [
"windows hello", "saisissez votre code pin",
"touchez le capteur d'empreintes",
],
DialogType.SMARTSCREEN: [
"windows a protégé votre pc",
"smartscreen", "informations complémentaires",
],
DialogType.BROWSER_PERMISSION: [
"autoriser", "bloquer",
"souhaite utiliser votre caméra",
"souhaite utiliser votre microphone",
"souhaite afficher des notifications",
],
# METIER_SAVE et METIER_CONFIRM = KNOWN_DIALOGS existant
}
class DialogClassifier:
"""Classifie un dialogue détecté en type connu.
Stratégie cascade :
1) match signatures texte (OCR titre) — ~150 ms (EasyOCR cache)
2) si pas de match → VLM compact (qwen3-vl:8b) avec prompt
"Classify this dialog: uac / hello / smartscreen / browser_perm /
metier / unknown" — ~1.7 s
3) fallback : INCONNU
"""
def classify(self, screenshot_pil, ocr_text: str) -> DialogType:
text = ocr_text.lower()
for dtype, signatures in TYPE_SIGNATURES.items():
for sig in signatures:
if sig in text:
return dtype
# Catalogue métier existant (KNOWN_DIALOGS du dialog_handler.py)
from core.grounding.dialog_handler import KNOWN_DIALOGS
for key in KNOWN_DIALOGS:
if key in text:
return DialogType.METIER_SAVE # ou METIER_CONFIRM selon key
# Fallback VLM si rien ne matche et qu'on a un signal modal fort
return self._classify_via_vlm(screenshot_pil) or DialogType.INCONNU
def _classify_via_vlm(self, screenshot_pil) -> Optional[DialogType]:
# Appel qwen3-vl:8b via Ollama LAN (port 11434)
# Prompt court, format=json strict
# ⚠ qwen3-vl:8b ignore parfois format=json — fallback regex sur stdout
...
```
### 4.4. DialogResolver — politique par type
Voir matrice §5 ci-dessous. Le resolver applique la politique et émet un événement structuré au serveur :
```python
@dataclass
class DialogEvent:
type: DialogType
title_ocr: str
policy_applied: str # "auto_dismiss" / "declarative" / "ask_human" / "escalate_security"
action_taken: Optional[str] # "click 'Oui' (123,456)" / "paused"
duration_ms: float
screenshot_path: str # toujours archivé pour audit
```
---
## 5. Matrice modal → action
| Type | Détection signature | Politique healthtech | Action concrète | Audit |
|---|---|---|---|---|
| **UAC** | "contrôle de compte", "user account control" | **escalate_security + ask_human** — JAMAIS auto-accept | `pause_for_human` ; toast "élévation requise — opérateur valide" | log full + screenshot |
| **Windows Hello** | "windows hello", "code pin", "touchez le capteur" | **ask_human** — interaction physique requise par construction | pause + tip pré-démo : "désactiver Hello pour la session" (paramètres Windows) | log |
| **Defender SmartScreen** | "windows a protégé votre pc", "smartscreen" | **escalate_security + ask_human** | pause + log security ; rappel `project_code_signing.md` (signature SHA256) | log full |
| **Permission navigateur** (cam/mic/notif/geoloc) | "souhaite utiliser votre", "autoriser / bloquer" | **declarative** si déclaré dans workflow ; sinon **ask_human** | catalog match → click `Autoriser` ; sinon pause | log + screenshot |
| **Métier sauvegarde** ("Voulez-vous enregistrer ?", "Enregistrer sous") | `KNOWN_DIALOGS` existant | **declarative** | InfiGUI click `Enregistrer` (catalog priority basse — fenêtre parent) | log standard |
| **Métier confirmation** ("Voulez-vous remplacer ?", "Existe déjà", "Écraser") | `KNOWN_DIALOGS` priorité HAUTE | **declarative** | InfiGUI click `Oui` ; fallback OCR direct (code actuel) | log standard |
| **OK trivial** (erreur app, info) | 1 seul bouton détecté, mots-clés "erreur/error/warning" ; pas de mot blocklist | **auto_dismiss** | click `OK` | log standard |
| **OK trivial SUSPECT** (mots-clés "supprimé", "perdu", "irréversible") | blocklist regex | **ask_human** | pause | log full |
| **INCONNU** | aucun match | **ask_human** par défaut (pas d'auto-dismiss aveugle) | pause + capture VLM pour catégorisation a posteriori → enrichit catalogue | log full |
**Garde-fou healthtech** : tout dialog non métier listé dans `KNOWN_DIALOGS` ou dans le workflow déclaratif **escalade en pause supervisée** par défaut. C'est cohérent avec `feedback_failure_is_learning.md` (échec = pause, pas stop) et le constat OpenAI/Anthropic 2026 (login/sécurité → cède la main à l'humain).
---
## 6. Détection rapide d'apparition de modal
Trois pistes, à composer plutôt qu'à choisir :
### 6.1. Foreground window change (Windows API)
- **Coût** : ~1 ms.
- **Fiabilité** : faible seul (`feedback_popup_vlm.md` 27 mars 2026 : popups Windows modernes partagent hwnd du parent, retourne 0 en SSH).
- **Usage** : **signal complémentaire** dans `ChangeSignal`, jamais source unique.
```python
import ctypes
hwnd = ctypes.windll.user32.GetForegroundWindow()
title_buf = ctypes.create_unicode_buffer(256)
ctypes.windll.user32.GetWindowTextW(hwnd, title_buf, 256)
# Comparer hwnd_n vs hwnd_n-1 ; titre dans title_buf.value
```
### 6.2. Screenshot diff zone centrale vs périphérie
- **Coût** : ~10 ms (numpy `abs(prev - curr) > seuil`, downscale 1/4).
- **Fiabilité** : bonne pour modaux centrés (UAC, dialog métier, Hello). Faible pour toasts en coin.
- **Heuristique** : `central_diff_ratio > 0.10` **ET** `diff_ratio < 0.40` → modal centré probable (le centre change beaucoup, le reste peu).
- **Pattern visuel auxiliaire** : zone "assombrie" en bordure (`secure desktop` UAC = pixels < 50 en luminance sur > 60% de l'écran).
### 6.3. PopSweeper-style classifier (option future, si latence VLM trop forte)
- ResNet50 + MobileNetV2 deux étages → 60 ms/frame, 91.7% précision sur RICO (mobile).
- À envisager **uniquement** si la détection diff+heuristique laisse passer trop de cas. Pour l'instant, surcoût d'un modèle dédié non justifié — l'OCR titre + signatures couvre 80%.
### 6.4. Pas de pHash global pour détection modal
`feedback_phash_vs_dialog_in_vm.md` est explicite : **pHash global est inadapté** à la cascade de modaux en VM. Le pHash compare des images entières, masquant les changements locaux qui sont précisément l'indice d'un modal. Utiliser screenshot diff zoné OU OCR titre — pas pHash global.
---
## 7. Activation de `_handle_possible_popup` orphelin
### 7.1. État actuel
- **Côté client (Léa Windows)** : `_handle_possible_popup` défini, **0 site d'appel** (`LESSONS_LEARNED_GHT_2026-05.md`, F5.5.1). Un `_handle_popup_vlm` existe en parallèle (le "remplacement" mentionné dans l'audit) mais sans déclencheur générique.
- **Côté serveur** : `core/grounding/dialog_handler.py` (étudié pour ce doc) — handler complet, ne se déclenche que si on l'appelle explicitement.
- **Constat memoire 27 mars 2026** : `qwen3-vl:8b` détecte popup en 3.6 s avec coordonnées précises depuis le client (appel direct LAN port 11434), pas via le serveur. Donc le VLM-post-clic est **techniquement validé**.
### 7.2. Câblage proposé (sans modifier le code dans ce doc)
Le pattern à brancher dans `agent_v1/core/executor.py` ressemble à :
```
def perform_click(self, x, y, ...):
screenshot_before = grab()
do_click(x, y)
time.sleep(short_delay)
screenshot_after = grab()
signal = ChangeDetector().detect(screenshot_after)
if signal.is_modal:
ocr_text = read_ocr(screenshot_after)
dtype = DialogClassifier().classify(screenshot_after, ocr_text)
event = DialogResolver(policy).resolve(dtype, screenshot_after, workflow_ctx)
report_to_server(event)
if event.policy_applied == "ask_human":
return WAIT_HUMAN
return OK
```
**Sites d'appel à brancher** (à valider avec Dom avant tout commit) :
1. Après chaque `_replay_action` côté client (suit la mémoire 27 mars).
2. Avant chaque vérification de Validator post-action (cohérence AXE_B2).
3. Au démarrage du tick d'observation `observe_reason_act` côté serveur (capture un modal apparu pendant un wait long).
### 7.3. Décision sur `_handle_popup_vlm` vs `_handle_possible_popup`
À trancher avec Dom : garder UN seul handler (probablement `_handle_popup_vlm` qui appelle l'extension proposée ici), supprimer l'orphelin. Sinon dette technique persistante (DETTE-XXX à créer si décision prise).
---
## 8. Anti-patterns à proscrire
### 8.1. Hardcoder un raccourci système "fix"
`feedback_100pct_visual.md` est sans appel : **JAMAIS** :
- `keyboard.press_and_release('escape')` pour "fermer un popup".
- `keyboard.press_and_release('win+r')` pour "ouvrir un truc rapidement".
- `keyboard.press_and_release('ctrl+x')` pour "annuler".
Raisons :
- Casse le récit "Léa comprend visuellement". Démontage immédiat face à un DSI healthtech.
- Échappe à la cascade de validation (OCR → template → VLM).
- Effets de bord imprévisibles : Échap dans un formulaire peut purger des données saisies ; Win+R sur un secure desktop ne fait rien et perd l'état.
**Exception unique** : `gesture_catalog.py` autorise les réflexes système **explicitement référencés** (voir `feedback_lea_reflexes_catalog.md`). Mais c'est une **composition** orchestrée, pas un "fix popup ad hoc".
### 8.2. Auto-accept système (UAC, Hello, SmartScreen)
Interdit en healthtech. Toujours pause supervisée. Cohérent avec FDA / RGPD / HDS — un agent qui élève des privilèges seul est un risque inacceptable.
### 8.3. pHash global pour détecter un modal
Voir §6.4. Utiliser screenshot diff zoné + OCR titre.
### 8.4. Polling VLM principal à chaque tick
Latence Qwen2.5-VL = 8-11 s par appel (synthèse `MIGRATION_VLM_PLAN_2026-05-09.md`). Détection modal doit rester < 100 ms — sinon on bloque la boucle replay. Le VLM est appelé **uniquement** quand le ChangeDetector signale `is_modal=True` ET que le catalogue texte n'a pas matché.
### 8.5. Tenter de "désactiver" Windows Hello programmatiquement
Solution = config humaine pré-démo, pas action runtime de l'agent. L'agent **détecte et escalade**, il ne modifie pas la config sécurité Windows.
---
## 9. Liens forts avec les autres AXES et la dette projet
- **AXE_A4 (OCR/Template/pHash)** : la **détection** modal réutilise screenshot diff zoné — surface à coordonner avec la décision pHash de A4 (pas pHash global ici).
- **AXE_A5 (Screen Tokenization)** : un parser d'écran qui produit la liste de regions interactives détecte aussi les "boutons de modaux" — la classification peut bénéficier de cette liste sans coût VLM supplémentaire.
- **AXE_B2 (Validator)** : dépendance forte. Un Validator strict (texte attendu présent dans la zone visée) **force** l'appel au DialogResolver quand le check post-action échoue. Sans ce couplage, un modal non vu reste un échec silencieux (cf. bug step 10 démo 8 mai : Imagerie cliqué dans bandeau Edge, REPORT success=True).
- **AXE_A3 (Bench Protocol)** : ajouter un harness "modal injection" — pendant un replay test, injecter UAC simulé / popup métier / SmartScreen factice, mesurer detect→classify→resolve latency et taux pause vs auto-dismiss.
- **DETTE existante** : DETTE-008 (`if False:` pré-check VLM par-clic, `observe_reason_act.py:1704-1713`) sera **résolue** par le ChangeDetector léger — on n'a plus besoin d'un VLM par clic, le détecteur fait le filtrage en amont.
- **`feedback_capture_purge_policy.md`** : screenshots de DialogEvent à conserver pour audit (RGPD / HDS) — politique de rétention à valider avec Dom.
---
## 10. Sources (liens cliquables)
### Frameworks comparés (publications, blogs, papers)
- [Anthropic Computer Use tool — docs API](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/computer-use-tool)
- [Anthropic Claude Desktop browser permissions controversy (avril 2026)](https://www.sovereignmagazine.com/article/anthropic-claude-desktop-browser-permissions)
- [Anthropic — Auto Mode permission classifier (mars 2026)](https://medium.com/@joe.njenga/anthropic-adds-new-claude-code-auto-mode-no-more-permission-modes-52c8094ab742)
- [OpenAI Operator System Card](https://openai.com/index/operator-system-card/)
- [OpenAI ChatGPT Agent System Card — juillet 2025](https://cdn.openai.com/pdf/839e66fc-602c-48bf-81d3-b21eacc3459d/chatgpt_agent_system_card.pdf)
- [ChatGPT Agent — login takeover + screenshot OFF](https://help.openai.com/en/articles/11752874-chatgpt-agent)
- [Skyvern — AI RPA Guide (oct. 2025)](https://www.skyvern.com/blog/ai-rpa-guide-intelligent-browser-automation/)
- [Skyvern — API-less Legacy System Automation (mai 2026)](https://www.skyvern.com/blog/api-less-system-automation-tools-legacy-enterprise/)
- [Skyvern GitHub — Planner-Actor-Validator](https://github.com/Skyvern-AI/skyvern)
- [browser-use — Issue #1996 : Need Robust Strategy for Handling Dynamic Popups (ouvert mai 2026)](https://github.com/browser-use/browser-use/issues/1996)
- [OmniParser V2 — Microsoft Research](https://microsoft.github.io/OmniParser/)
- [OmniParser arXiv 2408.00203](https://arxiv.org/abs/2408.00203)
- [UI-TARS arXiv 2501.12326](https://arxiv.org/abs/2501.12326)
### Détection popup, classifiers légers
- [PopSweeper — arXiv 2412.02933 (déc. 2024)](https://arxiv.org/abs/2412.02933)
- [PopSweeper — HTML lecture directe](https://arxiv.org/html/2412.02933v1)
- [ShowUI — arXiv 2411.17465 (2B GUI grounding)](https://arxiv.org/pdf/2411.17465)
- [ZonUI-3B cross-resolution GUI grounding](https://arxiv.org/pdf/2506.23491)
### Sécurité Windows 11 / browser
- [Microsoft KB UAC fixes oct. 2025 → janv. 2026 — Microsoft Q&A](https://learn.microsoft.com/en-nz/answers/questions/5733506/windows-uac-prompt-becomes-unresponsive-after-cred)
- [NinjaOne — Change UAC Behavior Windows 11](https://www.ninjaone.com/blog/change-uac-behavior-for-administrators-in-windows-11/)
- [CVE-2026-0628 Chrome Gemini Live panel takeover](https://news.corksafetyalerts.com/chrome-flaw-allowed-extensions-to-hijack-googles-ai-assistant-camera-and-microphone/)
- [AI-powered phishing leveraging camera/mic permissions (2026)](https://www.scworld.com/brief/ai-powered-phishing-campaign-leverages-hardware-access-for-data-theft)
### Safety gates / human-in-the-loop healthcare
- [Agentic Workflow Approval Gate Framework (4 gate types)](https://www.digitalapplied.com/blog/agentic-workflow-approval-gate-framework-governance)
- [AI Agents for Healthcare — Architecture and Safety Guide (Momentum)](https://www.themomentum.ai/blog/ai-agents-healthcare-architecture-safety-implementation)
- [Human-in-the-Loop Agentic AI (Elementum)](https://www.elementum.ai/blog/human-in-the-loop-agentic-ai)
### Références internes (à charger en parallèle de ce doc)
- `docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` (synthèse maîtresse)
- `docs/LESSONS_LEARNED_GHT_2026-05.md` (zones popup F5.5.1, F6.1.1, DETTE-008)
- `core/grounding/dialog_handler.py` (commit `487bcb861`)
- `memory/feedback_popup_vlm.md` (VLM post-clic, pas ctypes seul)
- `memory/feedback_lea_reflexes_catalog.md` (gesture_catalog autorisé, pas hardcode ad hoc)
- `memory/feedback_phash_vs_dialog_in_vm.md` (pas pHash global)
- `memory/feedback_100pct_visual.md` (jamais raccourci inventé)
- `memory/feedback_auth_dialogs_runtime.md` (anticiper Hello/UAC/Basic Auth AVANT démo)
---
## 11. Hors-périmètre de ce doc
À demander à Dom si besoin avant action :
- Décision finale sur unification `_handle_possible_popup` orphelin vs `_handle_popup_vlm` (les deux côté client).
- Politique de rétention RGPD/HDS des screenshots `DialogEvent` (par défaut `data/runner_captures/dialogs/` purge ACK serveur).
- Choix exact du modèle VLM compact pour la classification fallback (qwen3-vl:8b acceptable mais ignore parfois `format=json` Ollama — voir §2.4 synthèse).
- Bench de la latence `ChangeDetector` sur capture réelle 2560×1600 (cible < 50 ms à vérifier empiriquement).
- Politique sur la suppression auto de toasts (Teams, Outlook) pendant démo : déclaratif "do not disturb" en amont vs détection runtime.
---
*Document de recherche. Lecture seule. Toute mise en code = décision explicite Dom puis chirurgie itérative supervisée (CLAUDE.md projet).*

View File

@@ -0,0 +1,549 @@
# AXE D4 — Patterns de déploiement multi-tenant / multi-user d'un agent RPA Windows on-premise (2026)
**Date :** 2026-05-23
**Auteur :** Claude (agent de recherche, dispatché)
**Périmètre :** packaging, code signing, multi-tenant, silent install, auto-update, observabilité, comparaison concurrents, plan en 3 paliers (POC 2 → 10 → 100+ postes).
**Statut :** recherche en lecture seule, aucune modification de code. Sources cliquables en §10.
---
## 1. TL;DR — Recommandations immédiates
**Packaging exe Windows** : **Nuitka commercial standalone** pour Léa client (Agent V1). PyInstaller `--onefile` est à proscrire en milieu hospitalier : 100% des sources convergent sur le taux énorme de faux positifs antivirus, car le pattern self-extracting est indistinguable de malwares connus. Nuitka compile vers du C natif et obfusque le code Python — double bénéfice : moins de faux positifs ET protection IP modérée. Coût : temps de build x10100, à intégrer dans la CI.
**Code signing** : pour Phase 1 (POC AIVANOV + GHT démo) **rester en non signé** (statu quo). Pour Phase 2 (Anouste + 10 postes), **Azure Trusted Signing** = bloqué car réservé US/Canada + 3 ans d'ancienneté (pas applicable à un éditeur français). **Acheter directement un certificat OV Sectigo** chez un revendeur (CheapSSLSecurity ~120-200 €/an OV, ou 280-500 €/an EV via token HSM). L'EV n'apporte plus le bypass SmartScreen automatique depuis 2023 — l'OV suffit pour la confiance DSI, l'EV n'est utile que pour Windows Defender Application Control et certains pilotes signés. Pour la Phase 3 (>100 postes), envisager SignPath.io comme orchestrateur de signature (politiques + audit + intégration CI).
**Multi-tenant 3 paliers** :
1. **POC 2 postes** (actuel) : `lea / Medecin2026!` HTTP Basic statique, machine_id = UUID WMI `wmic csproduct get uuid`. Ne pas durcir.
2. **10 postes Anouste** : table `users` + `client_machines` + tokens API par machine (Fernet, rotation manuelle), JWT pour le dashboard, RBAC 3 rôles (admin / superviseur / viewer). Logs par tenant via préfixe fichier.
3. **100+ postes GHT/ARS** : Postgres + Row-Level Security ou schéma par tenant, refresh tokens avec rotation auto, observabilité Loki/Grafana avec header `X-Scope-OrgID`, SCIM pour AD/Entra, audit trail signé.
**Dépendances explicites** :
- AXE_B1 (transport HTTP → SSE/WebSocket) : tout watchdog et révocation de token dépendent de la couche transport — un client en long-poll qui rate l'ACK ne peut pas être proprement révoqué.
- AXE_B5_D1 (capture distante NoMachine/AnyDesk) : la confusion "agent Léa" vs "outil de prise en main" doit rester explicite dans le packaging (nom binaire `lea-agent.exe`, jamais `rpa-controller.exe` ou autre nom qui prête à confusion DSI).
---
## 2. Packaging Python → exe Windows (2026)
### 2.1. Table comparative
| Outil | Maturité 2026 | Taille .exe Léa (estim.) | Démarrage à froid | Faux positifs AV | Build time | Recommandation |
|---|---|---:|---|---|---|---|
| **PyInstaller** `--onefile` | très mature | 80-150 Mo (PyQt5+mss+pynput) | 3-8 s (extraction temp) | **élevé** (heuristique self-extract) | rapide (< 1 min) | À éviter |
| **PyInstaller** `--onedir` | très mature | 200 Mo dossier | < 1 s | moyen | rapide | Acceptable si MSI |
| **Nuitka standalone** | mature 2026 | 100-180 Mo | < 1 s (binaire natif) | **faible** (binaire C) | lent (10-30 min) | **Recommandé** |
| **Nuitka onefile** | mature 2026 | 100-150 Mo | 2-4 s | faible | lent | OK pour distrib |
| **Briefcase BeeWare** | mature (WiX 5.0.2) | 80-120 Mo MSI natif | < 1 s | moyen | moyen | Très bon pour MSI/AD |
| **cx_Freeze** | maintenu, marginal | 100 Mo | < 1 s | moyen | moyen | Pas de raison de le choisir |
| **PyOxidizer** | **abandonné** (dernier commit jan 2023) | — | — | — | — | Ne pas adopter |
### 2.2. Recommandation détaillée pour Léa
Léa Agent V1 = client léger Python (capture mss, pynput, requests/websocket, PyQt5 system tray). Pas de Transformers ni torch côté client (resté sur le serveur GPU).
**Choix recommandé : Nuitka commercial standalone, packagé dans un MSI WiX**.
Pourquoi cette combinaison :
- **Nuitka** élimine le pattern self-extract qui fait que `PyInstaller --onefile` est régulièrement signalé par Windows Defender / Kaspersky / Trend Micro en milieu hospitalier. C'est confirmé par plusieurs retours 2025-2026 (cf. sources). Compilation en C natif → l'exécutable ressemble à un binaire C/Rust normal.
- **MSI WiX via Briefcase OU MSI WiX maison autour du build Nuitka** : MSI est le format attendu par les DSI hospitalières pour déploiement GPO/SCCM/Intune. Évite la friction "exécutable inconnu" et permet l'installation silencieuse `msiexec /i lea-agent.msi /qn ALLUSERS=1`.
**Variante de transition** : si l'effort Nuitka est trop lourd à mettre en CI dans le mois qui vient pour Anouste, accepter `PyInstaller --onedir` (dossier, pas onefile) packagé dans un MSI WiX. Le `--onedir` a des taux de faux positifs **nettement inférieurs** au `--onefile` car il n'y a pas de bootloader d'extraction.
**Liste de paquets exclus du bundle Léa** (à expliciter dans le `.spec` Nuitka/PyInstaller pour réduire la taille) :
- `torch`, `transformers`, `triton`, `nvidia-*` (côté serveur uniquement)
- `faiss`, `sentence-transformers` (côté serveur)
- `docTR`, `easyocr` (côté serveur — sauf si Léa fait OCR local prévu en P3)
- modules `tests`, `pytest`, `notebook`, `jupyter*`
### 2.3. Tests à mener avant de figer le choix
- Build Léa minimal (capture + envoi HTTP) en PyInstaller `--onedir`, PyInstaller `--onefile`, Nuitka standalone, Nuitka onefile.
- Soumission à VirusTotal de chaque artefact (les 4 binaires). Cible : 0/72 sur Nuitka, <5/72 sur PyInstaller onedir, échec confirmé sur PyInstaller onefile.
- Mesure démarrage à froid sur un poste Windows 10 (Pauline) et Windows 11 (TIM Anoust).
- Mesure RAM résidente.
---
## 3. Code signing 2026
### 3.1. Table comparative des CA et services
| Solution | Type | Prix annuel (HT) | Bypass SmartScreen | KYC France | Verdict pour Léa |
|---|---|---:|---|---|---|
| **Pas de signature** | — | 0 € | Non, "Plus d'infos → Exécuter quand même" | — | OK Phase 1 |
| **Azure Trusted Signing** (ex Azure Artifact Signing) | OV-like (non EV) | ~120 € (10 €/mois Basic) | Réputation progressive, pas immédiat | **Bloqué** : US/Canada uniquement, +3 ans d'ancienneté requis | **NON applicable** |
| **Sectigo OV** (via revendeur SignMyCode/CheapSSL) | OV | 120-200 € | Réputation progressive | OK (KGI standard) | **Bon choix Phase 2** |
| **Sectigo EV** (token HSM ou eToken) | EV | 280-500 € | Plus de bypass auto depuis 2023, mais meilleure réputation initiale | OK + KYC renforcé (Dun & Bradstreet, registre commerce) | Si DSI exige EV |
| **DigiCert EV** | EV | 500-700 € | Idem Sectigo EV | OK + KYC renforcé | Plus cher, image premium |
| **SignPath.io** | Orchestrateur (utilise CA tierce) | dès ~50 €/mois (Foundation gratuit pour open-source) | dépend du certificat sous-jacent | Indirect via CA | Pertinent Phase 3 (audit + CI/CD + politiques signature) |
| **Whitelist SHA256 DSI** | hors CA | 0 € | DSI pousse le hash via GPO Defender ASR | Discussion contractuelle | Plan A documenté (Anoust) |
### 3.2. Changements 2026 à intégrer
- **CA/B Forum 15 février 2026** : les certificats de signature de code à 2 et 3 ans sont désormais réservés à l'option "Install on Existing HSM" (le client a déjà son HSM/yubikey). L'option Token + Shipping est limitée à 1 an. Pour Léa, prévoir un renouvellement annuel ou un investissement HSM (YubiKey 5C FIPS ~95 €).
- **EV ne bypasse plus SmartScreen automatiquement** depuis 2023. Le différentiel EV vs OV ne se justifie que pour : pilotes signés, Windows Defender Application Control en mode allow-listing certaines DSI, ou imagerie pro.
- **Azure Trusted Signing** : a beaucoup bougé 2025-2026 mais **reste fermé aux entreprises françaises** pendant toute la durée du POC. À surveiller mais pas planifier dessus.
### 3.3. Plan d'acquisition recommandé pour un éditeur santé français
**Phase 1 (maintenant, GHT démo + AIVANOV)** : aucune signature. Documenter dans `deploy/installer/README.md` la procédure "Plus d'infos → Exécuter quand même" + capture d'écran.
**Phase 2 (Anouste, 10 postes)** :
1. Acheter **Sectigo OV Code Signing 1 an** via CheapSSLSecurity ou SignMyCode (~130-180 € HT). Validation : 3-7 jours ouvrés avec extrait Kbis + facture pro + appel téléphonique de vérification.
2. En parallèle, ouvrir la négociation avec Fabrice DUPOUY (DSI Anoust) sur la **whitelist SHA256** dans Defender + leur antivirus principal. Combiner : DSI whitelist + signature OV = sécurité maximale + 0 friction utilisateur final.
3. Documenter le hash SHA256 du MSI dans la docstring du release tag git.
**Phase 3 (GHT/ARS, 100+ postes)** :
1. Passer à **Sectigo EV Code Signing avec HSM** (YubiKey FIPS) ou **DigiCert EV** si DSI hospitalier exige une CA "tier 1 reconnue".
2. Intégrer **SignPath.io** dans la CI GitHub Actions / Gitea Actions : politique de signature (qui peut signer, depuis quelle branche, quel commit signataire), logs d'audit consultables, intégration avec le HSM cloud.
### 3.4. Recommandation transverse
Ne pas attendre Phase 3 pour signer. Une signature OV obtenue dès Phase 2 commence à accumuler de la réputation SmartScreen / Defender (le binaire devient connu). Plus tôt on signe, plus tôt la friction disparaît.
---
## 4. Multi-tenant — modèle de données, tokens, machine_id, auth
### 4.1. Modèle de données recommandé (Phase 2-3)
```
tenants (id PK, name, contact_email, status, hds_certified BOOL, created_at)
users (id PK, tenant_id FK, username, password_hash BCRYPT, role ENUM admin|superviseur|viewer, totp_secret NULL, last_login, created_at)
client_machines (id PK, tenant_id FK, machine_id UNIQUE, hostname, os_version, user_id FK NULLABLE, status ENUM active|revoked|paused, api_token_hash, token_expires_at, last_seen)
api_tokens_audit (id PK, machine_id FK, action ENUM created|rotated|revoked, actor_user_id FK, timestamp, reason TEXT)
workflows_per_tenant (id PK, tenant_id FK, workflow_name, version)
audit_log (id PK, tenant_id FK, machine_id FK NULLABLE, user_id FK NULLABLE, action, payload_json, timestamp)
```
Trois patterns d'isolation possibles, par ordre croissant d'isolation et de complexité :
- **Shared DB + tenant_id column** : 1 base, 1 schéma, colonne `tenant_id` partout + RLS Postgres. **Recommandé Phase 2-3**.
- **Shared DB + schema per tenant** : 1 base, N schémas. Plus d'isolation mais migrations N fois.
- **DB per tenant** : isolation maximale, coût opérationnel élevé. À garder pour clients exigeants (ARS, ministériel).
Phase 2 (SQLite actuel) : viable si moins de 50 postes total, mais migration Postgres dès qu'on dépasse 5 tenants.
### 4.2. Génération de tokens API par machine
**Pattern recommandé** : token aléatoire 32 bytes URL-safe, stocké hashé (Argon2id ou SHA-256+sel) en base, présenté à chaque requête en header `Authorization: Bearer <token>`. Le token contient en clair (avant hash) un préfixe identifiant : `lea_<tenant_id>_<machine_id_short>_<random_32>` pour faciliter le debug logs sans révéler le secret.
Snippet de référence (Python serveur) :
```python
import secrets
import hashlib
from datetime import datetime, timedelta
def generate_machine_token(tenant_id: str, machine_id: str) -> tuple[str, str]:
"""Retourne (token_clair, token_hash). Stocker uniquement le hash."""
random_part = secrets.token_urlsafe(32)
token = f"lea_{tenant_id}_{machine_id[:8]}_{random_part}"
token_hash = hashlib.sha256(token.encode()).hexdigest()
return token, token_hash
def verify_token(token_clair: str, stored_hash: str) -> bool:
return hashlib.sha256(token_clair.encode()).hexdigest() == stored_hash
```
**Rotation** : tokens long-lived (90 jours) côté machine, refresh manuel depuis dashboard admin. Pour Phase 3 : pattern OAuth2 client credentials + refresh tokens avec rotation à chaque usage (TOTP-like).
**Révocation** : flag `status='revoked'` en base + cache court (60s) côté serveur. Avec le transport HTTP pull/long-poll actuel, la révocation prend effet au prochain poll (5-30s). **Avec SSE/WebSocket (AXE_B1)**, le serveur peut fermer la connexion immédiatement à la révocation.
### 4.3. machine_id unique Windows
Une seule source ne suffit pas (UUID virtualisé, MAC qui change, hostname dupliqué chez les vraies clinique). Recommandation : **identifiant composite stable**.
```python
import uuid
import subprocess
import socket
import hashlib
def get_machine_id_composite() -> str:
"""ID composite stable sur Windows, fallback Linux/macOS."""
components = []
# 1. WMI UUID (Win32_ComputerSystemProduct) — stable même après reset partiel
try:
out = subprocess.check_output(
["wmic", "csproduct", "get", "uuid"],
timeout=5, stderr=subprocess.DEVNULL
).decode().strip().split("\n")
if len(out) > 1:
wmi_uuid = out[1].strip()
if wmi_uuid and "FFFFFFFF" not in wmi_uuid:
components.append(wmi_uuid)
except Exception:
pass
# 2. MAC address de la première interface active (uuid.getnode)
mac = uuid.getnode()
if (mac >> 40) % 2 == 0: # éviter les MAC aléatoires (bit local)
components.append(str(mac))
# 3. Hostname (NetBIOS / DNS)
components.append(socket.gethostname())
if not components:
# Fallback : MachineGuid registre Windows
try:
out = subprocess.check_output(
["reg", "query", r"HKLM\SOFTWARE\Microsoft\Cryptography", "/v", "MachineGuid"],
timeout=5
).decode()
for line in out.split("\n"):
if "MachineGuid" in line:
components.append(line.split()[-1])
except Exception:
components.append(str(uuid.uuid1())) # ultime fallback
raw = "|".join(components)
return hashlib.sha256(raw.encode()).hexdigest()[:32]
```
**Important** : ne JAMAIS cloner une VM avec Léa installée sans regenerer le `machine_id` (cf. doc Power Automate Desktop : "if you reset your PC, your machine registration will be lost"). Prévoir un script `python -m lea_agent.reset_machine_id` documenté.
### 4.4. Pattern auth bouchon → cible
**État actuel (bouchon, Phase 1)** : `DASHBOARD_USER=lea`, `DASHBOARD_PASSWORD=Medecin2026!` en HTTP Basic. Un token global Bearer dans `.env.local` côté NPM reverse proxy pour `lea.labs.laurinebazin.design`.
**Cible Phase 2** (3 jours d'effort) :
1. Migration SQLite → table `users` + `client_machines`.
2. `flask-login` ou `fastapi-users` pour sessions dashboard (cookies signés).
3. Endpoint admin `POST /api/v1/admin/machines` qui génère un token, retourne le clair UNE seule fois, stocke le hash.
4. Endpoint client `GET /api/v1/replay/next` qui vérifie `Authorization: Bearer ...` et update `last_seen`.
5. Page dashboard `/admin/machines` : liste, révoquer, rotation, télécharger ZIP installer pré-configuré avec le token.
**Cible Phase 3** (1 semaine d'effort) :
1. Postgres + RLS, JWT avec refresh tokens (durée courte access 15 min, refresh 7 jours).
2. SCIM pour synchroniser AD/Entra (utilisateurs hospitaliers).
3. TOTP RFC 6238 pour les admins (déjà câblé dans `core/auth/`, à exposer).
4. Audit trail signé (chaîne de hash) pour conformité HDS V2.
---
## 5. Déploiement silent install + auto-update
### 5.1. Silent install Windows entreprise
**MSI > exe self-extracting** en hôpital. Les DSI hospitalières utilisent Intune, SCCM ou GPO, qui sont câblées sur MSI.
**Commande de référence** (à documenter dans le dossier DSI Anoust) :
```
msiexec /i Lea-Agent-v1.0.0.msi /qn /norestart ALLUSERS=1 \
LEA_SERVER_URL=https://lea.anoust.fr \
LEA_MACHINE_TOKEN=lea_anoust_a3b9c2_xxxxxxxxx \
INSTALLDIR="C:\Program Files\Lea Agent" \
/l*v "C:\Windows\Temp\lea-install.log"
```
**Patterns DSI hospitalier** :
- Déploiement par OU (Organisational Unit) AD pour cibler "Postes TIM" ou "Postes Médecins urgences".
- Per-machine install (`ALLUSERS=1`) car les RPA tournent sous SYSTEM ou compte de service.
- Pré-provisionning du token à l'installation : éviter le pattern "lance Léa, copie-colle un token". À la place, le ZIP téléchargé depuis le dashboard contient un MSI avec le token déjà intégré (custom MSI per machine).
**Si MSI trop complexe en Phase 2** : `Inno Setup` (gratuit, plus simple que WiX) avec `/VERYSILENT /SUPPRESSMSGBOXES` accepte les déploiements GPO/Intune en mode "Win32 app" Intune. Briefcase BeeWare expose WiX 5.0.2 sans complexité, c'est probablement la voie la plus simple si on reste Python-first.
### 5.2. Auto-update — recommandation
**Pour Phase 2 (10 postes Anouste)** : pas d'auto-update automatique. Procédure manuelle : pousser un nouveau MSI via SCCM/Intune ou via un script PowerShell que la DSI exécute. Notification dans le tray Léa "Une nouvelle version est disponible, contactez votre DSI".
**Pour Phase 3 (100+ postes)** : framework d'auto-update.
| Framework | Pertinence Léa | Verdict |
|---|---|---|
| **Velopack** | Réécriture moderne de Squirrel.Windows en Rust, delta updates, multi-langage, multi-plateforme. Actif 2025-2026. | **Recommandé** |
| **Squirrel.Windows** | Historique .NET, maintenu mais Velopack est le successeur. | À éviter (legacy) |
| **tufup** | Successeur Python de PyUpdater, basé sur python-tuf (sécurisé par design), indépendant du packaging. | **Très pertinent** si on reste Python natif et qu'on ne passe pas par Velopack. |
| **PyUpdater** | **Archivé**, ne pas utiliser. | À proscrire |
**Recommandation finale** : **tufup**. Avantages pour Léa :
- Indépendant du packaging (compatible Nuitka et PyInstaller),
- Sécurité basée sur TUF (The Update Framework) — signature des manifests, protection contre rollback attacks,
- Maintenance active 2025-2026,
- Manifest hosting peut être le serveur Linux on-premise existant.
**Pattern d'update** : side-by-side (installer dans `C:\Program Files\Lea Agent\v1.0.1\` à côté de `v1.0.0\`), bascule du raccourci système au prochain redémarrage. Rollback = pointer le raccourci vers la version précédente, qui reste sur disque pendant N versions (paramétrable). Avantage : zéro corruption sur update interrompue, rollback en < 5 minutes.
**Audit trail update** : chaque agent log dans `audit_log` côté serveur le succès/échec de l'update, version source, version cible, durée. Permet d'alerter si > 5% de la flotte est sur une ancienne version.
---
## 6. Stack serveur multi-tenant on-premise
### 6.1. docker-compose multi-tenant (squelette de référence)
Pour Phase 3 (le stack actuel `svc.sh` reste valable Phase 1-2) :
```yaml
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: lea_main
POSTGRES_USER: lea_admin
POSTGRES_PASSWORD_FILE: /run/secrets/pg_password
volumes:
- pg_data:/var/lib/postgresql/data
networks: [lea_internal]
api_stream:
image: rpa-vision/api-stream:1.0.0
environment:
DATABASE_URL: postgresql://lea_admin@postgres/lea_main
TENANT_RESOLUTION_HEADER: X-Lea-Tenant-Id
networks: [lea_internal, lea_public]
depends_on: [postgres]
ollama:
image: ollama/ollama:latest
deploy:
resources:
reservations:
devices:
- capabilities: [gpu]
volumes:
- ollama_models:/root/.ollama
networks: [lea_internal]
loki:
image: grafana/loki:3.0.0
command: -config.file=/etc/loki/loki-config.yaml
volumes:
- ./loki-config.yaml:/etc/loki/loki-config.yaml
- loki_data:/loki
networks: [lea_internal]
# auth_enabled: true dans loki-config.yaml
promtail:
image: grafana/promtail:3.0.0
volumes:
- /var/log:/var/log
- ./promtail.yaml:/etc/promtail/config.yaml
networks: [lea_internal]
grafana:
image: grafana/grafana:11.0.0
networks: [lea_internal, lea_public]
environment:
GF_AUTH_GENERIC_OAUTH_ENABLED: "true"
# SSO via Keycloak ou Entra
```
### 6.2. Observabilité par tenant
**Logs (Loki)** : auth multi-tenant activé (`auth_enabled: true`), chaque pod/agent envoie `X-Scope-OrgID: <tenant_id>`. Promtail config :
```yaml
clients:
- url: http://loki:3100/loki/api/v1/push
tenant_id: ${LEA_TENANT_ID}
```
Côté Grafana : un datasource Loki par tenant, ou un seul datasource avec le tenant_id injecté par variable dashboard.
**Métriques (Prometheus)** : labels `tenant_id="anoust"`, `machine_id="abc123..."`. Quotas par tenant via Cortex/Mimir si on monte en charge.
**Audit trail RGPD/HDS** : tableau `audit_log` partitionné par tenant + signature SHA-256 chaînée (chaque ligne contient le hash de la précédente). Conservation : 3 ans en hot, 7 ans en cold (S3 ou disque externe), conforme HDS V2.
### 6.3. Secrets management
Phase 1-2 : `.env.local` + chmod 600. C'est OK tant qu'on est < 5 tenants.
Phase 3 : **HashiCorp Vault** self-hosted (image officielle dispo) ou **age** + secrets chiffrés en git. Pour les tokens API machines, déjà couvert par le hash en base. Pour les mots de passe Citrix/Easily Assure, utiliser `core/auth/credential_vault.py` déjà câblé (Fernet+PBKDF2).
### 6.4. HDS V2 — implications concrètes
Si Léa stocke des screenshots patients côté serveur ET que ce serveur est on-premise hôpital, l'hôpital est l'hébergeur (HDS interne) et n'a pas besoin de certifier l'éditeur. Si le serveur est chez Dom / chez le client / dans un cloud, **certification HDS V2 obligatoire** (article L1111-8 CSP). Implication produit : prioriser le déploiement on-premise hôpital pour Phase 2-3, le SaaS multi-tenant centralisé est un sujet HDS-lourd (~30-60 k€ d'audit + 6 mois) à n'envisager qu'après stabilisation produit.
---
## 7. Comparaison concurrents — multi-tenant et déploiement
### 7.1. UiPath Robot
Modèle : licence par runtime (Production Unattended), allocation par tenant (Orchestrator → Admin → Tenants → Edit license allocation). Le robot consomme des runtimes du pool tenant à la connexion, les libère à la déconnexion. **Limite** : robot licences cantonnées à UN tenant — pas de "robot multi-tenant". Pour un hôpital multi-établissements GHT, on créerait 1 tenant par établissement.
Pattern à reprendre : pool de runtimes au niveau tenant, allocation dynamique. Pertinent pour Phase 3 GHT.
### 7.2. Power Automate Desktop (Microsoft)
Modèle : **silent registration** des machines via `Power-Automate-machine-runtime.exe /SILENT REGISTRATIONKEY=xxxx`. Service principal pour l'enrôlement bulk. Multi-session sur Windows Server (RDS) pour scaler à 10-20 bots par machine physique. **Limite critique** : si on clone une VM avec PAD installé, casse l'enrôlement.
Pattern à reprendre : enrôlement silencieux avec clé pré-générée, **interdiction de cloner les VM** documentée.
### 7.3. Automation Anywhere Bot Runner
Modèle : Bot Runners = comptes utilisateurs taggés "runner license" dans Control Room. Déploiement via RDP-based : le Control Room ouvre une session RDP sur la machine cible et lance le bot. Device Pools = groupes logiques pour distribuer la charge.
Pattern à reprendre : Device Pools pour parallélisation (intéressant pour un déploiement multi-postes TIM dans le même service).
### 7.4. Skyvern (open source, le plus proche de nous)
Modèle : docker-compose `postgres + skyvern (API+browser) + skyvern-ui`. Self-hosting complet possible. **Limite documentée** : pas de VNC multi-session en self-hosted, problème d'observabilité multi-bot. Pas de tenancy native dans la version open-source (à coder soi-même).
Pattern à reprendre : isolation par container Docker pour chaque session active, pratique pour scaler horizontalement. Différentiel pour nous : Léa tourne sur Windows utilisateur, pas dans un container — donc le pattern Skyvern ne s'applique qu'au serveur.
### 7.5. browser-use
Modèle : Cloud Skyvern-like, self-hosting Docker en cours de maturation (issue #658 GitHub février 2025). Multi-tenant : non documenté officiellement, communauté demande.
À surveiller mais pas d'inspiration directe en 2026.
### 7.6. Stack hospitalière française (Dedalus, Maincare, Easily Assure)
**Dedalus** et **Maincare** déploient leurs DPI via MSI signés + GPO. Auth utilisateur via SSO LDAP/Active Directory (Kerberos). Pas de modèle "agent par poste" car le DPI est web ou client-serveur — pas applicable directement.
**Easily Assure** : client lourd Windows (.NET) + serveur central. Authentification par compte utilisateur (couplé AD).
Implication pour Léa : nos clients DSI savent gérer GPO + MSI signé + AD SSO. **C'est le standard attendu**. Notre dispositif actuel (token Bearer envoyé manuellement) est en dessous des standards de la branche.
---
## 8. Plan en 3 paliers
### Palier 1 — POC 2 postes (actuel, mai 2026)
**État** : 1 démo + 1 dev. `lea / Medecin2026!` HTTP Basic, MSI absent, `.exe` PyInstaller `--onefile` non signé, install manuel via AnyDesk.
**Action** : ne rien casser. Documenter clairement le delta produit/cible.
**Effort** : 0.
### Palier 2 — 10 postes Anouste + 5 postes GHT pilote (T3 2026)
**À livrer** :
1. **Packaging** : migration Nuitka standalone (commercial, ~250 €/an) ou PyInstaller `--onedir` packagé MSI Briefcase. Cible : binaire signé OV, démarrage < 2s, taille < 200 Mo.
2. **Code signing** : achat Sectigo OV 1 an (~150 €) + négociation whitelist SHA256 avec DSI Anoust en parallèle (filet de sécurité 0 €).
3. **Multi-tenant** : migration SQLite → tables `tenants`, `users`, `client_machines`, `api_tokens_audit`. `flask-login` pour dashboard. 3 rôles (admin / superviseur / viewer).
4. **machine_id composite** (snippet §4.3) intégré au démarrage agent.
5. **Token API par machine** généré depuis dashboard admin, MSI custom par machine.
6. **Logs par tenant** : préfixe fichier `logs/<tenant_id>/<machine_id>/agent.log`. Rotation logrotate.
7. **Silent install MSI** documenté pour DSI Anoust.
8. **Audit trail** basique : table `audit_log` (qui a lancé quel replay sur quelle machine).
9. **Procédure update** manuelle documentée (DSI pousse le MSI).
**Effort estimé** : 10-15 jours dev, 3-5 jours validation site Anoust.
**Dépendance critique** : AXE_B1 transport. Tant qu'on est en HTTP long-poll, la révocation de token a une latence 5-30s. Acceptable Phase 2 mais à corriger Phase 3.
### Palier 3 — 100+ postes GHT/ARS (T4 2026 - T2 2027)
**À livrer** :
1. **Postgres + RLS** ou schéma par tenant (selon profil clients).
2. **JWT access + refresh tokens** avec rotation (15 min / 7 jours).
3. **SCIM** pour synchroniser AD/Entra hospitalier.
4. **TOTP** pour admins (déjà câblé dans `core/auth/`, à exposer).
5. **SignPath.io** pour orchestrer signature CI/CD + audit (~600 €/an).
6. **Sectigo EV** ou DigiCert EV avec HSM YubiKey (~400 €/an).
7. **tufup** pour auto-update side-by-side + rollback.
8. **Loki + Grafana** observabilité avec `X-Scope-OrgID`.
9. **Audit trail signé** chaîné SHA-256 pour HDS V2.
10. **Quotas par tenant** (nombre de replays/h, taille captures, etc.).
11. **Onboarding self-service** tenant via dashboard admin global (création tenant, premier admin, premier token).
**Effort estimé** : 30-60 jours dev, 10-15 jours sécurité/audit.
**Décision HDS** à trancher : on-premise hôpital systématique (pas de certification HDS éditeur requise) vs SaaS centralisé (HDS V2 obligatoire, 30-60 k€).
---
## 9. Restitution finale (< 250 mots)
**Packaging** : passer Léa de PyInstaller `--onefile` (faux positifs AV massifs en milieu hospitalier) à **Nuitka standalone** (compilation C native, faux positifs faibles) packagé dans un **MSI WiX via Briefcase** pour déploiement GPO/Intune/SCCM. Étape transitoire acceptable : PyInstaller `--onedir` dans MSI WiX.
**Code signing — Phase 1** : rester non signé (statu quo). **Phase 2 Anouste** : achat **Sectigo OV 1 an ~150 € HT** + négociation whitelist SHA256 DSI en parallèle. **Phase 3** : Sectigo EV avec HSM YubiKey + orchestration SignPath.io (~600 €/an). Azure Trusted Signing **bloqué** car réservé US/Canada + 3 ans d'ancienneté — ne pas planifier dessus, contrairement à ce qui était espéré dans `project_code_signing.md`.
**Multi-tenant 3 paliers** :
1. **POC 2 postes** : statu quo, machine_id = UUID WMI.
2. **10 postes** : tables `users`/`client_machines`, tokens API par machine, 3 rôles RBAC, `flask-login`, silent MSI, logs par tenant.
3. **100+ postes** : Postgres + RLS, JWT refresh, SCIM AD, TOTP admins, tufup auto-update, Loki/Grafana multi-tenant, audit trail signé HDS V2.
**Dépendances** :
- **AXE_B1 (transport HTTP → SSE/WebSocket)** : prérequis pour révocation de token immédiate et fermeture de session propre. Avec HTTP long-poll, latence 5-30s sur révocation.
- **AXE_B5_D1 (capture distante)** : ne pas brouiller le nom du binaire (Léa ≠ outil de prise en main DSI).
**HDS V2** : on-premise hôpital systématique en Phase 2-3 pour éviter le coût de certification HDS éditeur (30-60 k€, 6 mois). SaaS centralisé hors périmètre tant que produit non stabilisé.
---
## 10. Sources
### Packaging Python → exe
- [From PyInstaller to Nuitka: Convert Python to EXE Without False Positives (DEV.to)](https://dev.to/weisshufer/from-pyinstaller-to-nuitka-convert-python-to-exe-without-false-positives-19jf)
- [How to Fix Antivirus False Positives with PyInstaller Executables (Python GUIs)](https://www.pythonguis.com/faq/problems-with-antivirus-software-and-pyinstaller/)
- [Compilation vs Bundling: The Real Differences Between Nuitka and PyInstaller (KRRT7)](https://krrt7.dev/en/blog/nuitka-vs-pyinstaller)
- [Best PyInstaller Alternatives 2026 (No AV Flags)](https://beatsyncpro.ai/alternative/pyinstaller.html)
- [2026 Showdown: PyInstaller vs cx_Freeze vs Nuitka For Python EXE Builds](https://ahmedsyntax.com/2026-comparison-pyinstaller-vs-cx-freeze-vs-nui/)
- [Nuitka Performance documentation](https://nuitka.net/user-documentation/performance.html)
- [Nuitka Compilation: C-Level Python Performance Boost 2026](https://www.johal.in/nuitka-compilation-c-level-python-performance-boost-2026/)
- [PyOxidizer has been abandoned (Anki Issue #3081)](https://github.com/ankitects/anki/issues/3081)
- [Briefcase Windows MSI documentation](https://briefcase.beeware.org/en/stable/reference/platforms/windows/)
- [GitHub - beeware/briefcase-windows-msi-template](https://github.com/beeware/briefcase-windows-msi-template)
- [PyInstaller AV false positive Issue #6754](https://github.com/pyinstaller/pyinstaller/issues/6754)
### Code signing
- [Azure Artifact Signing FAQ (Microsoft Learn)](https://learn.microsoft.com/en-us/azure/artifact-signing/faq)
- [Azure Artifact Signing Pricing](https://azure.microsoft.com/en-us/pricing/details/artifact-signing/)
- [Trusted Signing open for individual developers - Public Preview](https://techcommunity.microsoft.com/blog/microsoft-security-blog/trusted-signing-is-now-open-for-individual-developers-to-sign-up-in-public-previ/4273554)
- [Trusted Signing 3-year requirement Q&A](https://learn.microsoft.com/en-us/answers/questions/2261318/is-there-any-exception-process-for-the-azure-trust)
- [Fighting through Setting up Microsoft Trusted Signing (Rick Strahl)](https://weblog.west-wind.com/posts/2025/Jul/20/Fighting-through-Setting-up-Microsoft-Trusted-Signing)
- [Code signing on Windows with Azure Trusted Signing (Melatonin)](https://melatonin.dev/blog/code-signing-on-windows-with-azure-trusted-signing/)
- [Top 10 Best Code Signing Certificate Providers in 2026](https://sslinsights.com/best-code-signing-certificate-providers/)
- [Sectigo EV Code Signing Certificate ($279.99/yr SignMyCode)](https://signmycode.com/sectigo-ev-code-signing)
- [Sectigo EV Code Signing Certificates (TheSSLStore)](https://www.thesslstore.com/sectigo/sectigo-ev-code-signing-certificate.aspx)
- [SignPath Pricing](https://about.signpath.io/product/pricing)
- [SmartScreen reputation for Windows app developers (Microsoft Learn)](https://learn.microsoft.com/en-us/windows/apps/package-and-deploy/smartscreen-reputation)
- [Code signing options for Windows app developers (Microsoft Learn)](https://learn.microsoft.com/en-us/windows/apps/package-and-deploy/code-signing-options)
- [Automate PyInstaller Builds and Code Signing (johanneskinzig)](https://johanneskinzig.com/automating-pyinstaller-builds-and-code-signing-with-powershell.html)
### Multi-tenant, auth, machine_id
- [Multi-Tenant Architecture with FastAPI: Design Patterns and Pitfalls (Medium)](https://medium.com/@koushiksathish3/multi-tenant-architecture-with-fastapi-design-patterns-and-pitfalls-aa3f9e75bf8c)
- [Multitenancy with FastAPI - A practical guide](https://app-generator.dev/docs/technologies/fastapi/multitenancy.html)
- [Building Multi-Tenant APIs with FastAPI and Subdomain Routing](https://medium.com/@diwasb54/building-multi-tenant-apis-with-fastapi-and-subdomain-routing-a-complete-guide-cc076cb02513)
- [FastAPI RBAC Permissions: Role-Based Access for ML Resources 2026](https://www.johal.in/fastapi-rbac-permissions-role-based-access-for-ml-resources-2026/)
- [Get a unique computer ID in Python on Windows and Linux](https://www.iditect.com/faq/python/get-a-unique-computer-id-in-python-on-windows-and-linux.html)
- [Get Windows Unique ID by Python](https://nashorn892087495.wordpress.com/2019/09/12/get-windows-unique-id-by-python/)
- [JWT in FastAPI - Refresh Tokens Explained (Medium)](https://medium.com/@jagan_reddy/jwt-in-fastapi-the-secure-way-refresh-tokens-explained-f7d2d17b1d17)
### Silent install et auto-update
- [Silent install cheatsheet (GitHub)](https://github.com/offlineinstallersetup/silent-install-cheatsheet)
- [A Guide to Install MSI Silently (Server Scheduler)](https://serverscheduler.com/blog/install-msi-silently)
- [Velopack - Cross-platform installer and auto-update framework](https://velopack.io/)
- [Velopack documentation - Migrating from Squirrel](https://docs.velopack.io/migrating/squirrel)
- [tufup - Automated updates for Python apps (GitHub)](https://github.com/dennisvang/tufup)
- [PyUpdater archived](https://github.com/Digital-Sapphire/PyUpdater)
### Concurrents RPA
- [UiPath Orchestrator About Licensing](https://docs.uipath.com/orchestrator/docs/about-licensing)
- [UiPath Robot Licensing Standalone 2025.10](https://docs.uipath.com/robot/standalone/2025.10/admin-guide/licensing-troubleshooting)
- [UiPath Automation Suite - Allocating Robot Licenses to Tenants](https://docs.uipath.com/automation-suite/docs/allocating-robot-and-service-licenses-to-tenants)
- [Power Automate Desktop - Silent registration for machines](https://learn.microsoft.com/en-us/power-automate/desktop-flows/machines-silent-registration)
- [Power Automate Desktop - Manage machine groups](https://learn.microsoft.com/en-us/power-automate/desktop-flows/manage-machine-groups)
- [Power Automate Desktop - Hosted machines](https://learn.microsoft.com/en-us/power-automate/desktop-flows/hosted-machines)
- [Automation Anywhere - RDP-based bot deployment](https://docs.automationanywhere.com/bundle/enterprise-v11.3/page/enterprise/topics/control-room/bots/my-bots/rdp-based-approach-to-bot-deployment.html)
- [Skyvern Docker Setup](https://docs-new.skyvern.com/self-hosted/docker)
- [Skyvern Issue #4392 - Multi-session VNC support](https://github.com/Skyvern-AI/skyvern/issues/4392)
- [browser-use Issue #658 - Docker Image for Self Hosting](https://github.com/browser-use/browser-use/issues/658)
### Observabilité multi-tenant
- [Grafana Loki - Manage tenant isolation](https://grafana.com/docs/loki/latest/operations/multi-tenancy/)
- [Creating Multi-Tenant Observability Dashboards with Grafana & Loki (2025)](https://sollybombe.medium.com/creating-multi-tenant-observability-dashboards-with-grafana-loki-2025-edition-85a673eff596)
- [Managing Grafana and Loki in a regulated multitenant environment (AWS)](https://aws.amazon.com/blogs/opensource/how-to-manage-grafana-and-loki-in-a-regulated-multitenant-environment/)
### HDS / RGPD / DSI hospitalière France
- [Certification HDS en 2026 - ce que chaque hôpital doit vérifier (Galeon)](https://www.galeon.care/fr/blog/certification-hds-en-2026-ce-que-chaque-hopital-doit-verifier-avant-de-signer)
- [HDS V2 certification framework (LSTI)](https://www.lsti-certification.fr/en/News/certification-HDS)
- [HDS - Agence du Numérique en Santé](https://esante.gouv.fr/ens/offre/hds)
- [Health Data Hosting (HDS) France - Microsoft Compliance](https://learn.microsoft.com/en-us/compliance/regulatory/offering-hds-france)
- [Doctrine Numérique Santé 2025 - Règles de sécurité](https://esante.gouv.fr/doctrine/securite)
---
*Document destiné à être consommé en lecture seule par Dom comme support de décision sur les axes packaging, code signing et architecture multi-tenant. Pas d'action engagée. Validation explicite requise avant chaque étape de Palier 2 ou 3.*

View File

@@ -0,0 +1,408 @@
# Axe E — Référentiel benchmarks GUI 2026 & delta frameworks RPA visuels
**Date :** 2026-05-23
**Auteur :** Claude (subagent veille) via Dom
**Périmètre :** veille externe — pas de modif code.
**Source de référence à mettre à jour :** `docs/INSPIRATION_FRAMEWORKS_2026-05-10.md` (10 mai 2026) + `docs/superpowers/specs/2026-05-05-qw-suite-mai-design.md` (5 mai 2026).
**Statut :** veille brute, à valider avec Dom avant toute action.
---
## 1. TL;DR
**En 2 semaines (10 → 23 mai 2026), 3 mouvements à retenir :**
1. **OSWorld n'est plus humain-level, il est passé super-humain.** Coasty (open source, github `coasty-ai/open-computer-use`) annonce 82 % sur OSWorld vs ~72 % humain, devant Claude Sonnet 4.6 à 73 % et Agent-S3 (Simular) à 69,9 %72,6 % (bBoN). OpenAI Operator stagne à 38 %. La marche entre "agent qui copie l'humain" et "agent qui fait mieux" est franchie côté frontière open source.
2. **WebVoyager est saturé.** Om Labs 98,9 %, Alumnium 98,5 %, Magnitude 94 %. Skyvern 2.0 (85,85 %) n'est plus SOTA. Le benchmark ne discrimine plus — Skyvern a anticipé en lançant **Web Bench** (5 750 tâches × 452 sites, partenariat Halluminate).
3. **MCP est devenu standard d'agent.** 97 M downloads SDK mensuel en mars 2026 (+970× en 18 mois), 78 % des équipes IA enterprise déclarent au moins un agent MCP en prod (avril 2026). Microsoft Agent 365 (GA 1er mai 2026) intègre gouvernance MCP au niveau tenant. Anthropic, OpenAI, Google Gemini et Vercel SDK supportent tous MCP nativement.
**Tendances 2026 :**
- Mixture-of-Grounding et Best-of-N rollouts (Agent S2/S3 « bBoN ») remplacent le single-pass.
- Continual learning sur GUI (GUI-AiF, AAAI 2026) émerge — replay engine devient training ground.
- Le rythme des sorties papier sur arXiv (AAAI 2026, ICLR 2026) double vs 2025 sur la verticale "GUI agent".
**Recommandation immédiate pour rpa_vision_v3 :**
- Adopter **ScreenSpot-Pro** (1 581 instructions, 23 apps, 3 OS, leaderboard maintenu jusqu'à mai 2026) comme bench de grounding interne — c'est le seul qui a des screenshots haute résolution réalistes (notre cas Easily Assure).
- Surveiller **Coasty open-computer-use** (apparu post-doc inspiration) et **Agent-S3 bBoN** — les deux poussent un pattern Best-of-N qui résoudrait notre Validator laxiste (cf. §7).
---
## 2. Carte référentiel benchmarks GUI 2026
| Benchmark | Mesure | Type tâches | Utilité rpa_vision_v3 |
|---|---|---|---|
| **ScreenSpot** (V1) | Grounding pur (clic) sur captures recadrées | 1 272 instructions web/desktop/mobile | Faible (résolutions trop basses, "consumer apps") |
| **ScreenSpot-V2** | Idem V1, 11,32 % de samples re-annotés | Idem V1 corrigé | Référence académique, pas notre cas réel |
| **ScreenSpot-Pro** | **Grounding haute résolution pro** | 1 581 instructions, 23 apps pro, 3 OS, écrans HD | **★★★★★ — notre cas** |
| **WindowsAgentArena** | Agent autonome end-to-end Windows | 154 tâches Windows (Notepad, Paint, navigateurs, etc.) | **★★★★ — OS cible** |
| **OSWorld** | Agent autonome end-to-end multi-OS | 369 tâches (LibreOffice, Chrome, VS Code, file mgmt) | **★★★★ — gold standard "agent"** |
| **OSWorld-Verified** | OSWorld durci anti-gaming (juillet 2025) | Sous-ensemble vérifié humain | ★★★ |
| **WebVoyager** | Agent web SOTA | 610 tâches sites live, jugement GPT | ★★ — saturé, pas notre cible (browser only) |
| **Online-Mind2Web** | Agent web réaliste | 300 tâches × 136 sites | ★★ |
| **Web Bench** (Skyvern + Halluminate) | Agent web large couverture | 5 750 tâches × 452 sites | ★★ |
| **AgentBench** (THUDM) | LLM-as-agent multi-environnement | 8 envs (OS, SQL, KG, jeux, web, etc.) | ★ — trop générique |
| **VisualWebBench** | Compréhension/grounding web MLLM | 1,5 k instances × 139 sites | ★ |
| **GUI-World** (ICLR 2025) | Compréhension **vidéo** GUI | 6 scénarios × 8 types Q dynamiques | ★ — pas notre angle replay |
| **AndroidWorld** | Mobile Android agent | 116 tâches × 20 apps Android | ✗ — hors scope healthtech desktop |
| **AndroidArena / A3** | Mobile dynamique | Tâches réalistes en ligne | ✗ |
| **MobileWorld** (ACL 2026) | Mobile + MCP-augmented | Tâches user-interactive | ✗ |
**Carte de couverture qui mesure quoi :**
- **Grounding seul (point/bbox)** → ScreenSpot-Pro (★ pour nous), ScreenSpot-V2, VisualWebBench.
- **Agent autonome Windows** → WindowsAgentArena (★ pour nous).
- **Agent autonome multi-OS** → OSWorld, OSWorld-Verified (★ pour nous, partiellement).
- **Agent web** → WebVoyager (saturé), Online-Mind2Web, Web Bench.
- **Compréhension vidéo GUI** → GUI-World.
- **Mobile** → AndroidWorld, AndroidArena, MobileWorld (hors scope).
---
## 3. Fiches des 5 benchmarks les plus pertinents pour nous
### 3.1 ScreenSpot-Pro — `arxiv:2504.07981`
- **Composition** : 1 581 instructions, 1 instruction par screenshot unique, 23 applications professionnelles, 5 secteurs (CAD, dev, ingénierie, science, design), 3 OS (Windows, macOS, Linux). Annotations expert humain.
- **Métriques** : taux de clic correct (point dans bbox vérité-terrain), bbox IoU.
- **Dataset accessible** : github `likaixin2000/ScreenSpot-Pro-GUI-Grounding`, leaderboard public `gui-agent.github.io/grounding-leaderboard/` (MAJ 14 avril 2026).
- **SOTA mai 2026** :
- GPT-5.2 (OpenAI) : 86,3 %
- GPT-5.4 (OpenAI) : 85,4 % (référence `benchlm.ai`)
- Muse Spark : 84,1 %
- Gemini 3 Pro (Google) : 72,7 %
- Qwen3.5 (féb 2026) : 70,3 % overall
- Qwen3.5-35B-A3B : 68,6 %
- Qwen2.5-VL-72B + RegionFocus : 61,6 %
- Baseline historique (papier original) : 18,9 % (modèles non spécialisés).
- **Lien** : https://arxiv.org/abs/2504.07981
- **Pertinence rpa_vision_v3** : c'est **le seul bench grounding qui ressemble vraiment à Easily Assure** — résolutions ≥ 1920×1080, mix de menus denses, panneaux à droite, tableaux. Notre `MIGRATION_VLM_PLAN_2026-05-09.md` cite ScreenSpot-Pro mais nous n'avons pas de score interne récent à comparer.
### 3.2 WindowsAgentArena (WAA) — `arxiv:2409.08264`
- **Composition** : 154 tâches Windows réelles (Notepad, Paint, File Explorer, Clock, Settings, browsers, documents, vidéo, code).
- **Métriques** : success rate task-level, parallélisable en Azure (~20 min run complet).
- **Dataset accessible** : github `microsoft/WindowsAgentArena`, paper page `huggingface.co/papers/2409.08264`.
- **SOTA mai 2026** :
- UI-TARS-2 (ByteDance, sept 2025) : 50,6 %
- Multi-modal Navi (Microsoft, baseline) : 19,5 %
- Humain : 74,5 %
- **Lien** : https://microsoft.github.io/WindowsAgentArena/
- **Pertinence rpa_vision_v3** : **★★★★★ pour positionnement client GHT** — Windows = terrain réel des TIM. Le gap humainmachine (74,5 % vs 50,6 % SOTA) est exactement le créneau où on opère (supervision médicale). Bench non saturé.
### 3.3 OSWorld / OSWorld-Verified
- **Composition** : 369 tâches sur OS réels (Ubuntu/Windows), apps réelles (LibreOffice, Chrome, VS Code, file mgmt, multi-app workflows). OSWorld-Verified = sous-ensemble durci (juillet 2025) pour empêcher le gaming.
- **Métriques** : success rate avec vérificateur déterministe par tâche (état final fichier, contenu DOM, etc.).
- **Dataset accessible** : leaderboard public maintenu.
- **SOTA mai 2026** :
- **Coasty open-cu** : 82 % (super-humain) — open source, gh `coasty-ai/open-computer-use`
- Claude Opus 4.6 (Anthropic) : 72,7 %
- Claude Sonnet 4.6 : 73 %
- **Agent-S3 + bBoN** (Simular) : 72,6 % — premier à passer humain
- Agent-S3 vanilla : 69,9 %
- GPT-5.3 Codex : 65 %
- GPT-5.2 Codex : 38 %
- OpenAI Operator (CUA) : 38,1 %
- Agent S2 (avril 2025) : 34,5 %
- UI-TARS-2 (ByteDance) : 47,5 %
- **Lien** : leaderboard via Coasty et Awesome Agents.
- **Pertinence rpa_vision_v3** : reference pour mesurer "où on en est par rapport au monde". Si on touche 30 % sur ces tâches en local-only, on est déjà compétitif.
### 3.4 WebVoyager — `arxiv:2401.13919` + extension Web Bench
- **Composition initiale** : 643 tâches × 15 sites (huit retirés post-Skyvern car obsolètes). Jugement GPT contre ≤ 15 screenshots/tâche.
- **Web Bench (Skyvern × Halluminate, 2026)** : 5 750 tâches × 452 sites.
- **SOTA mai 2026** (WebVoyager) :
- Om Labs (Claude Code + Opus 4.7 + GPT-5.4 Nano) : 98,9 %
- Alumnium MCP (Claude Code + Selenium) : 98,5 %
- Surfer 2 (H Company) : 97,1 %
- Magnitude : 94 %
- OpenAI CUA / Operator : 87 %
- **Skyvern 2.0 : 85,85 %** (référence doc 10 mai 2026 — plus SOTA)
- **Lien** : https://webvoyager.omlabs.xyz/
- **Pertinence rpa_vision_v3** : ★★ — pas notre cible (DPI Easily est partiellement web mais via Citrix souvent). À surveiller comme indicateur de saturation des benchs publics.
### 3.5 Bench candidat desktop Windows-spécifique → **Online-Mind2Web** + ScreenSpot-Pro suffisent
Aucun bench n'est plus "Windows-desktop natif" que WindowsAgentArena à date. Pour la verticale healthtech, **il n'existe pas de bench public** — c'est probablement une opportunité (créer `EasilyBench-1` interne à partir de nos 11 dossiers GHT serait un asset commercial).
---
## 4. Mise à jour frameworks vs doc 10 mai 2026
### 4.1 OpenAdapt (OpenAdaptAI)
| Aspect | 10 mai 2026 | 23 mai 2026 |
|---|---|---|
| Stars | ~7 k | en croissance |
| Dernier release PyPI | non précisé | **4 mars 2026** (PyPI) |
| Capacités VLM | LLM/LMM/VLM/LAM | + adaptateurs Qwen3-VL et Qwen2.5-VL via HF + PEFT |
| Phase produit | Phase 2 (retrieval-only) validée | **Phase 3** (demo-conditioned fine-tuning) en cours |
| Infra | local | + intégration AWS C8i/M8i/R8i nested virt (févr 2026, ~$0,19/h) |
| Sous-projets | mono-repo | **`openadapt-ml`** + **`openadapt-evals`** splittés |
**Delta clé** : OpenAdapt a structuré son écosystème en 3 dépôts (core + ML + evals). Le pattern "Evaluation-Driven Feedback" cité dans le doc 10 mai est désormais matérialisé dans `openadapt-evals` (infrastructure benchmarks). À étudier comme template pour notre `TargetMemoryStore` → pipeline d'entraînement.
### 4.2 Skyvern (Skyvern-AI)
| Aspect | 10 mai 2026 | 23 mai 2026 |
|---|---|---|
| WebVoyager | 85,85 % (cité comme SOTA) | **plus SOTA** — 4 acteurs au-dessus |
| Nouveauté | Planner-Actor-Validator + VWB | **Web Bench** (5 750 tâches × 452 sites) avec Halluminate, fév 2026 |
| Layout-resistant | non cité | dossier Layout-Resistant Tools (fév 2026) |
**Delta clé** : Skyvern a réagi à la saturation de WebVoyager en lançant son propre méga-bench. Notre VWB partage le naming `Visual Workflow Builder` avec eux, pas un problème, convergence indépendante.
### 4.3 OmniParser (Microsoft)
| Aspect | 10 mai 2026 | 23 mai 2026 |
|---|---|---|
| Version | V2 (févr 2025) | V2.0.1 (12 sept 2025) — **patch sécurité CVE-2025-55322 RCE** |
| Latence | non précisée | **60 % réduction vs V1**, 0,60,8 s sur A100/4090 |
| ScreenSpot-Pro | non précisé | **39,6 %** sur détection d'interactables |
| V3 | — | **non annoncé** |
**Delta clé** : OmniParser V2 reste la référence "screen tokenizer". Pas de V3 en vue. Le patch CVE-2025-55322 est à connaître si on auto-héberge.
### 4.4 TagUI (AI Singapore)
| Aspect | 10 mai 2026 | 23 mai 2026 |
|---|---|---|
| Statut | actif mais "moins LLM-first" | inchangé. V6 en chantier (Chrome visible par défaut) |
| Roadmap | non précisée | IDE + Orchestrator + Reporting Dashboard prévus |
**Delta clé** : aucun mouvement majeur. TagUI évolue vers UI/orchestration, pas vers le RPA visuel LLM-first.
### 4.5 Anthropic Computer Use SDK / Claude
- **Claude Opus 4.6** annoncé.
- **Claude Sonnet 4.6** : 72,573 % OSWorld (qualifié de "barely human-level").
- **Claude Opus 4.6** : 72,7 % OSWorld.
- **Claude Opus 4.7** présent dans `Om Labs` (top WebVoyager 98,9 %).
- Postmortem Anthropic mars-avril 2026 : 3 bugs latence/qualité (reasoning effort, caching, verbosity prompt). Résolus le 20 avril.
### 4.6 OpenAI Operator (CUA)
- **OSWorld** : 38,1 % — **n'a pas bougé**. Coasty publie un Review titré "A 38% Score Is Not an AI Agent, It's a Beta Product" (mai 2026).
- **WebVoyager** : 87 %, devancé.
- Operator standalone sunset → fusionné dans ChatGPT "agent mode" depuis juillet 2025.
- **CUA exposé via API** (Responses API, tier 3-5 select developers, research preview).
### 4.7 Simular Agent-S → Agent-S2 → Agent-S3
| Version | Date | OSWorld | Innovation |
|---|---|---|---|
| Agent-S | 2024 | — | architecture computer-use mature |
| Agent-S2 | avril 2025 | 34,5 % (50 step) | **Mixture-of-Grounding** + Proactive Hierarchical Planning |
| Agent-S3 | déc 2025 / 2026 | **69,9 %** (vanilla) → **72,6 %** (Best-of-N "bBoN") | suppression hiérarchie, **native coding agent** Python/Bash, Behavior Best-of-N (sample multiple rollouts, garde le meilleur) |
**Delta clé** : Agent-S3 est devenu le premier agent à passer humain-level OSWorld (avant Coasty). Le pattern **bBoN** est probablement le quick-win le plus rentable pour notre Validator (cf. §7).
### 4.8 Magma (Microsoft Research)
- Foundation model multimodal **digital + physique** (CVPR 2025, github `microsoft/Magma` MIT licence).
- Innovations : **Set-of-Mark** (SoM) pour grounding action + **Trace-of-Mark** (ToM) pour planification.
- Magma-8B sur HuggingFace.
- Pas de release majeure en mai 2026, mais le pattern SoM/ToM est repris dans plusieurs papiers AAAI/ICLR.
### 4.9 Cradle (Microsoft Research)
- Le terme "Cradle" est concurrencé en mai 2026 par **Microsoft Agent 365** (GA 1er mai 2026) qui couvre la gouvernance/observabilité d'agents (incluant MCP servers). Pas de release Cradle spécifique.
### 4.10 OS-Atlas (OS-Copilot)
- Statut : ICLR 2025 accepted, modèles OS-Atlas-Base-4B/7B + OS-Atlas-Pro-7B/4B sur HuggingFace.
- **ScreenSpot-V2** : re-annoté par OS-Atlas team (11,32 % de samples corrigés).
- Pas de **V2 OS-Atlas** annoncée à mai 2026.
### 4.11 UI-TARS / UI-TARS-2 (ByteDance)
| Version | Date | Notes |
|---|---|---|
| UI-TARS-1.5-7B | mars 2026 (notre repo l'avait, commit `9da589c8c` du 25 avril) | abandonné par nous le 26 avril pour InfiGUI-G1-3B |
| **UI-TARS-2** | **4 sept 2025** | All-In-One Agent (GUI + Game + Code + Tool), Apache 2.0 |
| UI-TARS-desktop | mai 2026 | **33 573 stars** = plus gros projet open source GUI agent |
**Scores UI-TARS-2** :
- Online-Mind2Web : 88,2
- OSWorld : 47,5
- WindowsAgentArena : 50,6
- AndroidWorld : 73,3
**Delta clé** : UI-TARS-2 est sorti AVANT le doc 10 mai mais n'y est pas mentionné. ByteDance détient désormais le plus grand écosystème GUI agent open source (33 k stars) — à reconsidérer comme alternative à InfiGUI-G1-3B sur notre serveur grounding.
### 4.12 AGUVIS (Salesforce + HKU)
- Pas de release Salesforce 2026.
- Toujours référencé comme baseline pure vision (89,2 grounding multi-plateforme, 51,9 % step success rate).
- ICML 2025 accepted.
### 4.13 MCP (Model Context Protocol)
| Métrique | Mars-Avril 2026 |
|---|---|
| Downloads SDK mensuels | **97 millions** (+970× en 18 mois) |
| Servers publics | 9 400+ (vs 1 200 Q1 2025), +18 % mom Q1 2026 |
| Adoption enterprise | 78 % équipes IA ont ≥ 1 agent MCP en prod |
| CTOs déclarant MCP "default" | 67 % dans 12 mois |
| Support LLM | Claude (natif), ChatGPT (Apps SDK), Gemini (mars 2026), Cursor, Windsurf, Zed, JetBrains, Vercel AI SDK, OpenAI Agents SDK |
| Roadmap 2026 | audit trails, SSO auth, gateway, config portability |
**Delta clé pour rpa_vision_v3** : on est dans la fenêtre où exposer notre engine via MCP serait un asset commercial (Skyvern, OpenAdapt, browser-use l'ont fait). Microsoft Agent 365 prévoit la **gouvernance MCP au niveau tenant** — vendeur d'argument healthtech (audit, conformité).
---
## 5. Nouveaux entrants 2026 — non couverts par les docs internes
### 5.1 Coasty (gh `coasty-ai/open-computer-use`)
- **82 % OSWorld** — premier au-dessus de Claude Sonnet 4.6 (73 %) et Agent-S3 (72,6 %).
- "Production-ready, remote and local, one API key".
- Open source.
- **À étudier ASAP** : architecture probablement utile pour pousser notre OSWorld interne.
### 5.2 Agent-S3 bBoN (Simular)
- Pattern Behavior Best-of-N : exécute N rollouts en parallèle, sélectionne le meilleur via judge.
- 18,9 % et 32,7 % relative improvements vs baseline.
- **Lien direct avec notre Validator laxiste** (bug step 10 Imagerie dans bandeau Edge) : bBoN éviterait que le mauvais rollout passe le VERIFY.
### 5.3 InfiGUI-G1 + AEPO (AAAI 2026 Oral)
- **Notre serveur grounding actuel** (`InfiGUI-G1-3B`, commit `77faa03ec` du 26 avril) repose dessus.
- Adaptive Exploration Policy Optimization : +9 % vs RLVR baseline.
- Acceptance AAAI 2026 Oral confirme robustesse.
### 5.4 Magnitude / Alumnium / Om Labs
- Magnitude (gh `magnitudedev/webvoyager`) : 94 % WebVoyager.
- Alumnium : 98,5 % WebVoyager via Claude Code + Selenium + MCP.
- Om Labs (`webvoyager.omlabs.xyz`) : 98,9 % WebVoyager (avril 2026).
- Pattern commun : couplage browser engine **classique** (Selenium/Playwright) + agent LLM. Pas notre angle (Citrix interdit DOM), mais à surveiller.
### 5.5 GUI-Actor (Microsoft)
- `microsoft/GUI-Actor-7B-Qwen2.5-VL` sur HF.
- Attention-based action head **sans coordonnées** (coordinate-free visual grounding).
- 44,6 sur ScreenSpot-Pro avec Qwen2.5-VL backbone.
### 5.6 Papiers AAAI/ICLR/ICML 2026 à surveiller
- **TreeCUA** (fév 2026, `arxiv:2602.09662`) — tree-structured verifiable evolution.
- **LiteGUI** (`arxiv:2605.07505`) — distillation compact GUI via RL.
- **UltraCUA** (`arxiv:2510.17790`) — foundation model CUA hybrid action.
- **Continual GUI Agents** (`arxiv:2601.20732`) — continual learning sur GUI.
- **GUI-RCPO** (`arxiv:2509.21552`) — self-improvement, +5 % ScreenSpot-V2.
- **MobileWorld** (ACL 2026) — mobile + MCP-augmented.
---
## 6. Tendances 2026 — patterns émergents
1. **Best-of-N rollouts** (Agent-S3 bBoN, Om Labs WebVoyager) : un seul agent run ne suffit plus, on parallélise et on garde le meilleur. Implication directe pour rpa_vision_v3 : notre VERIFY post-action devrait être un judge entre plusieurs candidats de grounding, pas un pHash global.
2. **Mixture-of-Grounding** (Agent-S2, GUI-Actor) : différents modèles de grounding spécialisés pilotés par un routeur adaptatif. C'est exactement la spec **F2** déclarée out-of-scope dans `QW_SUITE_MAI` mais qui devient mainstream.
3. **Continual learning on-the-fly** (GUI-AiF AAAI 2026, OpenAdapt phase 3) : l'agent apprend pendant le replay. Notre `TargetMemoryStore` est conceptuellement aligné mais sans pipeline d'entraînement.
4. **MCP-first architecture** : tous les acteurs majeurs (Anthropic, OpenAI, Google, Skyvern, browser-use, Alumnium) exposent ou consomment MCP. Le standard d'interop est tranché.
5. **Synthesis frameworks** : on n'oppose plus RPA classique et AI agent. Skyvern (Planner-Actor-Validator), Agent-S3 (manager + native coding), Coasty (production-ready), OpenAdapt 3 dépôts. Le vainqueur est celui qui combine déclaratif + LLM + grounding spécialisé.
6. **Saturation des benchs publics et création de méga-benchs privés** : WebVoyager saturé → Web Bench (5 750 × 452). OSWorld passé humain → futur OSWorld-2 inévitable.
---
## 7. Implications pour rpa_vision_v3
### 7.1 Frameworks méritant exploration deeper
| Framework | Pourquoi | Effort lecture |
|---|---|---|
| **Coasty open-computer-use** (82 % OSWorld, OS) | Architecture production-ready, "remote and local" qui matche notre Léa Windows + serveur Linux | 12 j |
| **Agent-S3 bBoN** (72,6 % OSWorld, open) | Best-of-N résout notre Validator laxiste (bug step 10) | 0,51 j paper + code |
| **OpenAdapt phase 3** (demo-conditioned fine-tuning) | Template pour brancher `TargetMemoryStore` sur un pipeline d'entraînement | 1 j paper + code |
| **UI-TARS-2 + UI-TARS-desktop** (33 k stars) | Alternative à InfiGUI-G1-3B sur notre serveur grounding | 1 j eval |
| **MCP serveur** (Skyvern, browser-use, Anthropic) | Exposer rpa_vision_v3 en MCP = standard interop healthtech | 23 j POC |
### 7.2 Benchmarks à adopter pour mesurer notre progrès
1. **ScreenSpot-Pro** (priorité 1) — refaire un bench grounding sur les 5 modèles déjà testés (qwen2.5vl:7b Ollama, qwen3-vl:8b, InfiGUI-G1-3B, UI-TARS-2, qwen3.5). Permet de positionner notre stack sur un référentiel public.
- Notre `BENCH_GROUNDING_INTERNE_2026-05-08` ne contient qu'1 fixture (heartbeat dialog OK/Cancel) — c'est trop pauvre.
2. **WindowsAgentArena** (priorité 2) — adapter 510 tâches du WAA "browsers/documents" à notre stack pour avoir un repère agent autonome public.
3. **EasilyBench-1 interne** (priorité 3) — créer un bench fermé à partir des 11 dossiers GHT (workflow `Urgence_aiva_demo` + variantes). Asset commercial : "on a notre propre eval validée par médecin DIM".
### 7.3 Patterns à formaliser dans la doc (gratuit, zéro code)
Le doc 10 mai recommandait déjà Policy / Grounding / Safety Gate / Validator. À ajouter :
- **Best-of-N rollouts** (bBoN) comme alternative au pHash VERIFY.
- **Mixture-of-Grounding** comme nom officiel de notre cascade.
- **Screen Tokenizer** comme nom de la suggestion §4.1 du doc 10 mai (log candidats à chaque `_resolve_target`).
- **MCP-first** dans la roadmap interop.
### 7.4 Mises à jour à porter dans `INSPIRATION_FRAMEWORKS_2026-05-10.md`
- §3.1 Skyvern : retirer "85,85 % WebVoyager SOTA" — ajouter "85,85 % avant Om Labs/Alumnium/Magnitude/Surfer 2 — Skyvern a lancé Web Bench (5 750 × 452)".
- §4.1 OmniParser : préciser V2.0.1 + patch CVE-2025-55322 + 39,6 % ScreenSpot-Pro + 60 % latence réduite.
- §5 ajouter Coasty, Agent-S3, UI-TARS-2 comme entrants 2026 majeurs.
- §6 ajouter MCP server architecture comme **présent**, pas long-terme.
- §7 ajouter "Best-of-N" et "Continual learning" comme nouveaux patterns convergents.
---
## 8. Sources (avec dates)
### Benchmarks
- ScreenSpot-Pro paper — https://arxiv.org/abs/2504.07981 (avril 2025, leaderboard MAJ avril 2026)
- ScreenSpot-Pro leaderboard — https://gui-agent.github.io/grounding-leaderboard/ (MAJ 14 avril 2026)
- ScreenSpot-Pro models avg — https://benchlm.ai/benchmarks/screenSpotPro (mai 2026)
- WindowsAgentArena paper — https://huggingface.co/papers/2409.08264
- WindowsAgentArena GH — https://github.com/microsoft/WindowsAgentArena
- OSWorld leaderboard via Coasty — https://coasty.ai/blog/osworld-benchmark-results-2026-who-actually-wins (mai 2026)
- WebVoyager leaderboard — https://webvoyager.omlabs.xyz/ (avril 2026)
- Online-Mind2Web GH — https://github.com/OSU-NLP-Group/Online-Mind2Web (mars 2025)
- VisualWebBench — https://visualwebbench.github.io/
- AgentBench GH — https://github.com/THUDM/AgentBench
- Holistic Agent Leaderboard — https://hal.cs.princeton.edu/
### Frameworks (delta 10 → 23 mai 2026)
- OpenAdapt — https://github.com/OpenAdaptAI/OpenAdapt (PyPI 4 mars 2026)
- OpenAdapt evals — https://github.com/OpenAdaptAI/openadapt-evals
- Skyvern 2.0 launch — https://www.skyvern.com/blog/skyvern-2-0-state-of-the-art-web-navigation-with-85-8-on-webvoyager-eval/
- Skyvern Web Bench — https://www.skyvern.com/blog/web-bench-a-new-way-to-compare-ai-browser-agents/
- OmniParser V2.0.1 release — https://github.com/microsoft/OmniParser/releases (12 sept 2025)
- OmniParser V2 perf — https://www.microsoft.com/en-us/research/articles/omniparser-v2-turning-any-llm-into-a-computer-use-agent/
- TagUI — https://github.com/aisingapore/TagUI
- Browser Use changelog — https://browser-use.com/changelog (CLI 2.0, BU 2.0 jan 2026, V3 sessions avril 2026)
- Anthropic postmortem Claude — https://www.anthropic.com/engineering/april-23-postmortem (23 avril 2026)
- Anthropic Opus 4.6 — https://www.anthropic.com/news/claude-opus-4-6
- OpenAI Operator — https://openai.com/index/introducing-operator/
- OpenAI Operator critique — https://coasty.ai/blog/openai-operator-review-2026-20260504 (4 mai 2026)
- Simular Agent-S2 — https://www.simular.ai/articles/agent-s2
- Simular Agent-S3 — https://www.simular.ai/articles/agent-s3
- Simular Agent-S GH — https://github.com/simular-ai/Agent-S
- Microsoft Magma — https://microsoft.github.io/Magma/
- Microsoft Magma GH — https://github.com/microsoft/Magma
- Microsoft Agent 365 GA — https://www.microsoft.com/en-us/security/blog/2026/05/01/microsoft-agent-365-now-generally-available-expands-capabilities-and-integrations/ (1er mai 2026)
- OS-Atlas — https://github.com/OS-Copilot/OS-Atlas
- UI-TARS-2 paper — https://arxiv.org/abs/2509.02544 (sept 2025)
- UI-TARS GH — https://github.com/bytedance/ui-tars
- UI-TARS-desktop GH — https://github.com/bytedance/UI-TARS-desktop (33,5 k stars mai 2026)
- AGUVIS — https://aguvis-project.github.io/
### MCP & adoption
- MCP roadmap 2026 — https://blog.modelcontextprotocol.io/posts/2026-mcp-roadmap/
- MCP adoption stats — https://www.digitalapplied.com/blog/mcp-adoption-statistics-2026-model-context-protocol (avril 2026)
- MCP 97 M downloads — https://www.digitalapplied.com/blog/mcp-97-million-downloads-model-context-protocol-mainstream (mars 2026)
- The New Stack MCP — https://thenewstack.io/model-context-protocol-roadmap-2026/
### Nouveaux entrants & papiers
- Coasty open-cu GH — https://github.com/coasty-ai/open-computer-use
- InfiGUI-G1 AAAI 2026 — https://github.com/InfiXAI/InfiGUI-G1 + https://arxiv.org/abs/2508.05731
- GUI-Actor — https://microsoft.github.io/GUI-Actor/
- Alumnium WebVoyager — https://alumnium.ai/blog/webvoyager-benchmark/
- Magnitude WebVoyager — https://github.com/magnitudedev/webvoyager
- Awesome GUI Agent — https://github.com/showlab/Awesome-GUI-Agent
---
*Document de veille à 23 mai 2026, lecture seule. Toute action (adoption framework, intégration bench, refonte) nécessite une décision explicite de Dom et un spec dédié.*

View File

@@ -0,0 +1,59 @@
# Compte Rendu : Stabilisation par Ancres Visuelles (Inspiration Coasty/Agent-S3)
**Date :** 24 mai 2026
**Objet :** Résolution des régressions "Enregistrer sous" et "Start Button" via la triangulation visuelle.
**Statut :** Spécifications de recherche (Zéro modification de code).
---
## 1. Synthèse de la Problématique
Les échecs actuels de Léa sur les dialogues Windows (Bloc-notes) et le menu Démarrer proviennent d'une dépendance excessive à l'OCR du titre de la fenêtre. Sous Windows 11, ces titres sont instables ou asynchrones.
La solution identifiée dans les frameworks SOTA (Coasty, Agent-S3) consiste à utiliser des **Ancres Visuelles** : des éléments graphiques invariants qui permettent de trianguler la position des cibles sans lire le texte.
---
## 2. Fiche Ancre : Dialogue "Enregistrer sous" (Notepad Windows 11)
Cette fiche définit la "signature sémantique" du dialogue de sauvegarde pour permettre à Léa de le reconnaître instantanément, même avant que l'OCR n'ait fini de lire le titre.
| Élément | Type d'Ancre | Rôle dans la Triangulation | Signature Visuelle (Invariants) |
| :--- | :--- | :--- | :--- |
| **Bouton Fermer (X)** | Ancre de Structure | Définit le coin supérieur droit de la zone de recherche. | Petit rectangle rouge ou gris avec une croix blanche, situé en haut à droite. |
| **Bouton Annuler** | Ancre de Référence | Permet de localiser le bouton "Enregistrer" par proximité. | Bouton textuel situé systématiquement en bas à droite du dialogue. |
| **Champ Nom de fichier** | Ancre de Focus | Indique où l'agent doit cliquer pour saisir le texte. | Rectangle blanc allongé, souvent situé juste au-dessus des boutons d'action. |
### Stratégie de Triangulation (SANS coordonnée fixe) :
1. **Détection du Dialogue** : Si un nouveau rectangle de ~800x600 pixels apparaît au centre de l'écran avec une bordure fine.
2. **Localisation de l'Ancre de Référence** : Trouver le bouton "Annuler" (bas-droite).
3. **Déduction de la Cible** : Le bouton "Enregistrer" est le bouton immédiatement situé à **gauche** de l'Ancre de Référence (distance ~10-50px).
4. **Validation** : Si le bouton "Annuler" disparaît, le dialogue est fermé -> Succès.
---
## 3. Application au Cas "Start Button" (Boucle de Retries)
Le menu Démarrer échoue car il n'a pas de titre de fenêtre exploitable par le `DialogHandler` classique.
### Signature d'Ancre pour le Menu Démarrer :
* **Ancre Primaire** : Le logo Windows dans la barre des tâches (Ancre de déclenchement).
* **Ancre de Validation** : Apparition d'un grand panneau translucide (Mica/Acrylic) ancré en bas de l'écran.
* **Point Critique** : Le menu Démarrer de Windows 11 ne touche pas forcément le bord de l'écran. L'ancre doit être la **Barre de Recherche** interne au menu.
* **Validation de succès** : Si la zone de recherche "Taper ici pour rechercher" devient visible, le menu est considéré comme "Ouvert" (State-Centric Success).
---
## 4. Recommandations pour le Pilotage de Léa
Pour intégrer ces concepts dans la logique actuelle de Léa sans coder, voici les principes à suivre lors de la création des scénarios :
1. **Utiliser des Offsets Relatifs** : Dans le VWB, privilégier les clics relatifs à une ancre (ex: "Clic à gauche de 'Annuler'") plutôt que des clics sur un texte "Enregistrer" qui peut varier.
2. **Introduire un "Visual Wait for Anchor"** : Avant de saisir le nom du fichier, forcer l'agent à attendre l'apparition visuelle de l'icône de la fenêtre de dialogue.
3. **Le "pHash de Stabilité"** : Ne pas valider l'action `start_button` tant que le pHash de la zone centrale de l'écran n'a pas arrêté de changer (signe que l'animation d'ouverture est finie).
---
## 5. Conclusion Technique
Le passage à une détection par **Ancres et Triangulation** résout le problème de la latence Windows. Léa devient capable de "comprendre" la structure d'un dialogue Windows sans avoir besoin que l'OS lui fournisse des informations textuelles (souvent en retard).
*Document d'analyse stratégique. Zéro modification de code effectuée.*

View File

@@ -0,0 +1,279 @@
# Index — Specs opérationnelles replay (transport / validator / popups)
**Date :** 2026-05-24
**Auteur :** Claude (session principale) à partir des 3 specs ciblées + 6 docs de recherche préalables.
**Public :** humain ou agent qui doit prendre en charge l'un des 3 chantiers de fiabilisation replay post-démo GHT.
**Statut :** lecture seule. Aucune décision figée. Plan d'action proposé à valider par Dom.
---
## 0. Comment lire ce document
Tu prends en charge un chantier replay → tu lis §1 (TL;DR) et tu sautes directement à la spec correspondante (§3.1, §3.2 ou §3.3). Tu reviens à §2 (croisements) si ton chantier touche les deux autres. Les décisions ouvertes Dom sont consolidées §4.
Les 3 specs s'appuient sur des **recherches préalables** déjà livrées (`AXE_B1`, `AXE_B1_DEEP`, `AXE_B2`, `AXE_B2_DEEP`, `AXE_D2`, `AXE_D2_DEEP`). Les specs ne refont pas l'étude — elles produisent le contrat opérationnel.
---
## 1. TL;DR
**3 chantiers replay à mener en parallèle pour fermer les bugs racines post-démo GHT (~3 j homme MVP cumulés)** :
| # | Chantier | Bug racine fermé | Spec | Code prêt | Effort MVP |
|---|---|---|---|---|---|
| **B1** | Transport + watchdog | Désync 8 mai (9 actions perdues en 33s) | `SPEC_TRANSPORT_CONTRAT.md` | `replay_watchdog.py` ~270 LOC + patches `api_stream.py` | **3h30** |
| **B2** | Validator post-action | Step 10 (clic Imagerie dans Edge, REPORT success=True) | `SPEC_VALIDATOR_MATRICE.md` | package `core/validation/` ~590 LOC (MVP P0 ~190 LOC) | **8h** |
| **D2** | Chaîne popup propre | `_handle_possible_popup` orphelin + auto-dismiss risqué | `SPEC_POPUPS_CATALOGUE.md` | package `core/dialog/` ~700 LOC + 59 entrées catalogue | **1j** |
**Résultat attendu cumulé** : démo prochaine sans contournement `static_result/static_text`, sans `cancel-replays.sh` manuel, sans pause humaine sur dialogs métier connus.
**Contraintes invariantes respectées** :
- 100% vision (aucun raccourci système inventé)
- Healthtech (jamais d'auto-accept UAC / Hello / SmartScreen / suppression non déclarée)
- Backward compatible (kill-switches env var par défaut OFF sur chaque chantier)
---
## 2. Croisements entre les 3 specs
```
┌─────────────────────────────────────────────────────────────────┐
│ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ B1 Transport │◄────────│ B2 Validator │ │
│ │ + Watchdog │ ack │ + 6 Checkers │ │
│ └───────────────┘ └───────────────┘ │
│ ▲ │ │
│ │ purge si paused │ failure_category= │
│ │ ▼ UNEXPECTED_DIALOG │
│ │ ┌───────────────┐ │
│ └──────────────────│ D2 Popup │ │
│ │ Resolver │ │
│ └───────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
### Dépendances et contrats inter-modules
| Source | Cible | Contrat | Quand |
|---|---|---|---|
| B1 | B2 | Action arrive bien côté serveur (REPORT non perdu) | Prérequis. Sans B1, B2 vote sur du vide. |
| B2 | D2 | `failure_category=UNEXPECTED_DIALOG/NO_VISUAL_CHANGE/WRONG_APPLICATION``DialogResolver.resolve()` | Au verdict d'échec d'un Checker |
| D2 | B1 | Si modal détecté pendant qu'une action est en `_retry_pending`, watchdog doit purger | v1.1 du watchdog (déjà partiel via `cancel_replay` ligne 4489) |
| D2 | B2 | `DialogPresenceChecker` (Checker B2) fournit la bbox utilisée par `DialogClosedChecker` | Coordination intra-Validator |
| B2 | B1 | Verdict `WRONG_APPLICATION` (bug step 10) → override `success=True → False` → relance watchdog | Au report client |
### Orthogonalité
- **B1 ↔ B2** : techniquement orthogonaux (peuvent être codés en parallèle), mais **les deux ensemble** = fermeture totale du bug du 8 mai (transport + validation).
- **D2** : indépendant de B1 et B2 pour l'implémentation, mais consomme leurs interfaces au runtime.
---
## 3. Résumé par spec
### 3.1. SPEC_TRANSPORT_CONTRAT.md
**Chemin** : `/home/dom/ai/rpa_vision_v3/docs/recherche/SPEC_TRANSPORT_CONTRAT.md`
**Volume** : 766 lignes, 169 lignes de tables.
**Recherches sources** : `AXE_B1_REPLAY_TRANSPORT.md`, `AXE_B1_DEEP_WATCHDOG.md`.
**Contenu** :
- 2 state machines ASCII (serveur PENDING → DISPATCHED → ORPHAN → ACKED → ABANDONED → PAUSE | client POLLING → RECEIVED → DEDUP → EXECUTING → REPORTING → ACKED) avec invariants I1-I6 et C1-C5.
- Contrats JSON DISPATCH / REPORT / re-dispatch / pause / resume / cancel, avec lignes de code source précises (`api_stream.py:626-651` schema, `3354-3359` retry_pending, `4361-4474` resume, `4489` cancel).
- **Matrice 21 cas limites** (a→u) couvrant timeout pré-réponse, déconnexion post-réception, report perdu, watchdog race, client mort, server restart, polls concurrents, idempotence double-clic, pause pendant action en vol, cancel UI, abandon MAX_RESENDS, etc.
- Sémantique d'idempotence à 7 couches + spec `dedup_set` client copy-paste + idempotence par type d'action.
- 18 timeouts/seuils tabulés avec env vars (`RPA_WATCHDOG_ORPHAN_TIMEOUT_S=30`, `RPA_WATCHDOG_SCAN_INTERVAL_S=10`, `RPA_WATCHDOG_MAX_RESENDS=2`, etc.).
- Transitions pause supervisée & resume (5 déclencheurs `paused_need_help`).
- Compatibilité transport polling ↔ SSE (table d'invariance).
- 7 fiches précédents externes (SQS, NATS, Skyvern, browser-use, Anthropic CU, Playwright MCP) + table comparative.
**Code production-ready dans le doc** : module `agent_v0/server_v1/replay_watchdog.py` (~270 lignes), 4 patches diff unified `api_stream.py`, 8 tests pytest `tests/integration/test_replay_watchdog.py`.
**Effort intégration** : **3h30** (45min schéma+watchdog, 30min câblage, 1h tests pytest, 1h chasse races sur E2E réel, 15min DETTE).
---
### 3.2. SPEC_VALIDATOR_MATRICE.md
**Chemin** : `/home/dom/ai/rpa_vision_v3/docs/recherche/SPEC_VALIDATOR_MATRICE.md`
**Volume** : ~1300 lignes denses.
**Recherches sources** : `AXE_B2_VALIDATOR_PATTERN.md`, `AXE_B2_DEEP_VALIDATOR.md`, `AXE_A4_OCR_TEMPLATE_PHASH.md`.
**Contenu** :
- Matrice principale **27 types d'action × 9 colonnes** (signal primaire / secondaire / fallback / verdicts possibles / latence cible / coût / Checker à utiliser).
- **4 fiches détaillées** pour les cas listés explicitement par Dom :
- `switch_tab` (le bug step 10) : signal primaire = `OcrRoiChecker` ROI 120×40 autour du tab attendu cherchant label exact dans words OCR ; secondaire = pHash de la zone contenu sous les tabs a changé ≥ 5%.
- `close_tab` : `TabAbsenceChecker` + visibilité tab voisin actif.
- `save` : disparition indicateur "modifié" + apparition toast "Enregistré".
- `dialog_button` : disparition du dialog (handoff D2 via `DialogClosedChecker`).
- 5 fiches secondaires (`click_anchor`, `type_text`, `extract_text`, `t2a_decision`, `keyboard_shortcut`).
- **6 Checkers production-ready** : `PixelDiffChecker` (15ms), `OcrRoiChecker` (80ms, **résout step 10**), `TitleBarChecker` (130ms wrapper existant), `JsonSchemaChecker` (10ms), `LlmJudgeChecker` (3s wrapper `verify_with_critic` existant `replay_verifier.py:367`), `TabActiveChecker`/`TabAbsenceChecker`/`SaveSuccessChecker`/`DialogClosedChecker` (nouveaux).
- Confidence scoring + règles d'agrégation multi-checker (`switch_tab` → SUCCESS si primaire ≥ 0.85 OU primaire ≥ 0.65 AND secondaire ≥ 0.70).
- **Anti-patterns table 12 entrées** : pHash global pour switch_tab, title-bar seule pour SPA, `success=True` parce que coords envoyées, SSIM global dialog, etc.
- 6 précédents externes (Skyvern `complete_verify`, browser-use `evaluation` agent, Playwright assertions, Selenium `expected_conditions`, SikuliX `waitVanish`, PyImageSearch).
- Plan d'intégration en 3 étapes graduées (1j MVP / 2 sem couverture complète / 1 mois bench).
**Réutilisation existant** : `OcrRoiChecker` réutilise le singleton EasyOCR de `TitleVerifier._get_ocr()` (zéro coût d'init).
**Reproduction offline du bug step 10** : script `repro_bug_step10_validator.py` fourni + test pytest `test_validator_step10.py`, sur capture `visual_workflow_builder/backend/data/anchors/anchor_0438bd2d9bdd_1778161174_full.png`.
**Wiring** : diff unified format insertion à `api_stream.py:3447-3582` derrière flag `RPA_VALIDATOR_V2_ENABLED=false` (default OFF, aucune régression flag off).
**Budget latence démo 46 steps** : **+11s cumulés** (vs 33s perdus en pause/reprise step 10).
**Effort intégration P0** : **8h** (création package 4h + tests 1.5h + patch api_stream 2h + smoke 30min).
---
### 3.3. SPEC_POPUPS_CATALOGUE.md
**Chemin** : `/home/dom/ai/rpa_vision_v3/docs/recherche/SPEC_POPUPS_CATALOGUE.md`
**Volume** : 1758 lignes après enrichissement.
**Recherches sources** : `AXE_D2_DIALOG_POPUP.md`, `AXE_D2_DEEP_POPUP_CHAIN.md`.
**Contenu** :
- **Section 2bis — Catalogue compact (format Dom)** avec exactement 5 colonnes : `ID | Titre exact (FR/EN) | Appli source | Boutons attendus | Politique`.
- **Politique trichotomie stricte** :
- **`auto`** = Léa clique un bouton précis (action explicite définie)
- **`pause`** = Léa s'arrête et attend décision humaine
- **`skip`** = Léa ignore le modal (ne clique rien, ne s'arrête pas)
- **59 entrées catalogue** réparties en 5 catégories (A SYSTÈME, B NAVIGATEUR, C MÉTIER, D APP TIERS, E INCONNU) :
- **44 `pause`** : toute la catégorie A SYSTÈME + identification + suppression + warnings cliniques
- **10 `auto`** : save/confirm Easily, dialogs métier connus, dialogs disposables avec clic explicite
- **5 `skip`** : `browser-translate-prompt`, `easily-toast-saved`, `outlook-reminder`, `chrome-update`, `edge-update`
- 10 fiches détaillées des modaux critiques (UAC, Hello, SmartScreen, save unconfirmed, browser perms, etc.) avec capture-type ASCII.
- **Workflow VWB déclaratif** `expected_modal` (YAML + Pydantic schema) avec validateur `system_modals_cannot_be_overridden` qui **rejette toute politique ≠ `pause` sur préfixes `windows-` / `defender-`** — un workflow VWB ne peut pas forcer un UAC en auto.
- Snippet Python `KNOWN_DIALOGS` étendu (781 lignes, syntaxe valide `ast.parse` OK) avec champs `window_title`, `app_source`, `policy` (`auto`/`pause`/`skip`), `declarative_override: bool`. Helpers `get_metadata()` et `can_be_overridden()`.
- Tests offline pytest + protocole de capture.
- 7 précédents externes (Skyvern issue #69, browser-use issue #1996, Anthropic CU human takeover, OpenAI Operator watch mode, AutoIt/Sikuli, pywinauto, Selenium JS alerts).
**Décisions tranchées par les agents** :
- `_handle_possible_popup` orphelin **supprimé** (0 site d'appel + antipattern Tab+Enter aveugle).
- `_handle_popup_vlm` actif **conservé mais simplifié** → devient client léger d'un nouvel endpoint `POST /api/v1/dialog/resolve`.
- DialogResolver côté **serveur** (pas client) pour mutualiser avec `dialog_handler.py` existant.
**Couverture estimée** : **~85%** des modaux courants sans intervention humaine. Les 15% restants = pause supervisée par design healthtech.
**Effort intégration** : **1j MVP** (P0 démo) → 1 sem couverture complète + tests fixtures (P1) → 1 mois bench injection + apprentissage catalogue (P2).
---
## 4. Décisions ouvertes consolidées pour Dom
Les 3 specs remontent des points qu'un agent ne peut pas trancher seul. Regroupés ici par thème.
### 4.1. Transport / watchdog (8 décisions — `SPEC_TRANSPORT_CONTRAT.md` §12)
| ID | Question | Recommandation auteur |
|---|---|---|
| D1 | Persistance `_retry_pending` au restart serveur ? | Non par défaut (cas extrême ; restart démo = redémarrage replay manuel) |
| D2 | Politique d'abandon par type d'action (click vs wait) ? | Différenciée : click MAX_RESENDS=2, extract_text MAX_RESENDS=1 |
| D3 | Retry des actions server-side (`extract_text`, `t2a_decision`) ? | Non (idempotence non garantie : LLM peut diverger) |
| D4 | Purge `_retry_pending` à la complétion workflow ? | **Oui** (recommandé) |
| D5 | `dedup_set` client : taille LRU + clé ? | LRU 50 entrées, clé = `action_id` + `attempt_id` |
| D6 | Génération `attempt_id` côté serveur ? | UUID4 court 8 chars, incrémenté à chaque resend |
| D7 | Backward-compat client v1 ↔ serveur v2 ? | Header `X-Replay-Protocol-Version` côté client |
| D8 | `cancel_in_flight` (annuler action en cours sur le client) ? | **Non** (recommandé) — trop de risques |
### 4.2. Validator (7 actions où le signal "qui fait foi" reste flou — `SPEC_VALIDATOR_MATRICE.md` §10)
| Action | Question |
|---|---|
| `paste_and_execute` | Vérif côté Léa Windows ou côté SSH VM (cas NoMachine pixel intermédiaire) ? |
| `screenshot_evidence` | `TitleBarChecker` suffit ou exiger netteté image ? |
| `pause_for_human` mode autonome | Aujourd'hui silencieusement ignorée (`api_stream.py:3011-3017`) — laisser ou changer ? |
| `t2a_decision = NA` | Verdict métier vs erreur LLM (hors-scope médical Claude) — qui décide ? |
| Tab déjà actif au moment du clic | Idempotence (SUCCESS) ou NO_VISUAL_CHANGE ? |
| `drag_drop_anchor` | Whitelist serveur mais pas de handler Léa — implémenter ou retirer du whitelist ? |
| Animations longues > 1s | `wait_after_action_ms` par type d'action vs généralisé ? |
### 4.3. Popups (5 modaux ambigus — `SPEC_POPUPS_CATALOGUE.md` §10)
| Modal | Politique proposée | Risque à valider |
|---|---|---|
| `easily-required-field` | `auto` | Peut masquer un bug de grounding réel |
| `outlook-reminder` | `skip` | Risque clinique de masquer un rappel pro |
| `chrome-update` / `edge-update` | `skip` | À confirmer si ne prend pas le focus après inactivité |
| `easily-clinical-warning` | `pause` non surchargeable | Volontairement strict, à valider |
| `browser-perm-microphone` (Easily dictée vocale) | `pause` | Déclaration globale `declared_dialogs` ou par-workflow ? |
---
## 5. Plan d'attaque concret proposé (~3 jours homme cumulés)
### Vague 1 (jour 1) — Transport + start Validator
- **Matin (3h30)** — Watchdog `replay_watchdog.py` + patches `api_stream.py`. Kill-switch `RPA_WATCHDOG_ENABLED=false` par défaut → activable progressivement. Tests pytest sans Windows. Smoke E2E réel.
- **Après-midi (4h)** — Validator MVP P0 : `PixelDiffChecker` + `OcrRoiChecker` + orchestrateur, sans `LlmJudgeChecker`. Flag `RPA_VALIDATOR_V2_ENABLED=false`. Test offline du bug step 10 sur fixture du 8 mai.
### Vague 2 (jour 2) — Finir Validator + Popup chain MVP
- **Matin (4h)** — Compléter Validator : `TabActiveChecker`, `SaveSuccessChecker`, `DialogClosedChecker`, `JsonSchemaChecker`. Tests par type d'action.
- **Après-midi (4h)** — Chaîne popup MVP : signatures FR+EN, `ChangeDetector` + `DialogClassifier` + `DialogResolver` côté serveur. Endpoint `/api/v1/dialog/resolve`. Simplification `_handle_popup_vlm` client.
### Vague 3 (jour 3) — Intégration et démo
- **Matin (3h)** — Coordination Validator → DialogResolver (handoff `UNEXPECTED_DIALOG`). Test bout-en-bout démo MOREL Catherine avec les 3 chantiers actifs.
- **Après-midi (3h)** — Bench latence sur démo réelle (cible : +20s overhead max sur 46 steps). Activation progressive des flags. Mise à jour `DETTE_TECHNIQUE.md` (DETTE-001, 008 closables).
**Sortie attendue** : démo prochaine `Demo_urgence_3_db` qui tourne **sans** `static_result/static_text`, **sans** `cancel-replays.sh` manuel, **sans** pause humaine sur dialogs métier connus.
---
## 6. Hors-périmètre de ces 3 specs (rappel)
Ces 3 specs **ne traitent pas** :
- Migration grounding VLM (`AXE_A1`, `AXE_A2`, `AXE_A3` — Qwen3-VL, smart_resize, bench bbox).
- Bug capture client Y `mss.monitors[N]=2560×60` (`AXE_B5_D1_CAPTURE_REMOTE.md` — fix DPI 8 lignes en tête de `main.py`).
- Shadow learning / fine-tuning / memory store (`AXE_C_LEARNING_SHADOW.md`).
- Packaging / code signing / multi-tenant (`AXE_D4_MULTI_TENANT_DEPLOY.md`).
- Veille frameworks externes (`AXE_E_FRAMEWORKS_BENCHMARKS.md`).
- Bug recapture anchor VWB silencieuse (P0 — non couvert par cette vague).
- Bug skip ord 13 orchestration (P0 — non identifié, NOT REPRO 100%).
Ces sujets sont couverts par les autres docs `docs/recherche/AXE_*.md`. Voir `SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` pour la carte d'ensemble.
---
## 7. Cartographie complète des livrables de recherche
### Vague 1 — Panorama 13 axes (23 mai 2026)
```
docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md (synthèse croisée initiale)
docs/recherche/AXE_A1_VLM_GROUNDING_SOTA.md (état art VLM grounding)
docs/recherche/AXE_A2_SMART_RESIZE_BBOX.md (DETTE-014/010/007/006)
docs/recherche/AXE_A3_BENCH_PROTOCOL.md (script bench reproductible)
docs/recherche/AXE_A4_OCR_TEMPLATE_PHASH.md (cascade déterministe)
docs/recherche/AXE_A5_SCREEN_TOKENIZATION.md (OmniParser / SoM)
docs/recherche/AXE_B1_REPLAY_TRANSPORT.md (SSE / WS / pull-poll)
docs/recherche/AXE_B2_VALIDATOR_PATTERN.md (Planner-Actor-Validator)
docs/recherche/AXE_B4_ORA_VS_REPLAY.md (autonomous vs déclaratif)
docs/recherche/AXE_B5_D1_CAPTURE_REMOTE.md (mss / DXGI / NoMachine)
docs/recherche/AXE_C_LEARNING_SHADOW.md (Shadow + FT + memory)
docs/recherche/AXE_D2_DIALOG_POPUP.md (chaîne dialog handling)
docs/recherche/AXE_D4_MULTI_TENANT_DEPLOY.md (packaging + code signing)
docs/recherche/AXE_E_FRAMEWORKS_BENCHMARKS.md (delta veille + benchmarks)
```
### Vague 2 — Approfondissement 3 axes replay (24 mai 2026)
```
docs/recherche/AXE_B1_DEEP_WATCHDOG.md (watchdog production-ready)
docs/recherche/AXE_B2_DEEP_VALIDATOR.md (Validator package)
docs/recherche/AXE_D2_DEEP_POPUP_CHAIN.md (chaîne popup complète)
```
### Vague 3 — Specs opérationnelles (24 mai 2026)
```
docs/recherche/SPEC_TRANSPORT_CONTRAT.md (contrat dispatch→ack→retry→orphan→resume)
docs/recherche/SPEC_VALIDATOR_MATRICE.md (matrice action → signal qui fait foi)
docs/recherche/SPEC_POPUPS_CATALOGUE.md (catalogue 59 entrées + politique auto/pause/skip)
```
### Vague 4 — Index (ce document)
```
docs/recherche/INDEX_REPLAY_SPECS_2026-05-24.md (point d'entrée brief agent / humain)
```
---
*Index maintenu par Dom. À mettre à jour quand de nouvelles specs replay sont livrées ou quand une décision §4 est tranchée.*

View File

@@ -0,0 +1,109 @@
# Journal seance 1 micro-apprentissage Lea - 2026-05-27
Competence cible: ouvrir le menu Demarrer / Recherche Windows.
## Demo A - clic logo Windows
- Session: `sess_20260527T170656_e16163`
- Methode humaine: clic gauche sur la barre des taches / logo Windows.
- Signal utile:
- clic gauche capture a `[755, 1556]`,
- transition observee vers `Rechercher` / `SearchHost.exe`,
- heartbeat avec `active_window_title = Rechercher`.
- Bruit:
- fenetre active initiale: `Acces vocal` (`VoiceAccess.exe`),
- clics de fin de session dans zone systeme / fenetre Lea,
- `window_capture` faux pour le clic barre des taches car la fenetre active `Acces vocal` mesure seulement 60 px de haut.
- Verdict: observation utile, pas preuve propre de competence.
## Demo B - touche Windows
- Session: `sess_20260527T171110_ca856a`
- Methode humaine: touche Windows.
- Signal utile:
- transition `Acces vocal` -> `Rechercher` / `SearchHost.exe`,
- heartbeat avec `active_window_title = Rechercher`.
- Bruit:
- aucun evenement clavier `Win` capture dans la trace,
- clics de fin de session dans zone systeme / fenetre Lea,
- `Acces vocal` reste la fenetre active parasite avant/apres le geste.
- Verdict: bonne observation de postcondition, mais capture clavier incomplete.
## Demo C - Win+S
- Session: `sess_20260527T171412_737571`
- Methode humaine: raccourci `Win+S`.
- Signal utile:
- transition `Acces vocal` -> `Rechercher` / `SearchHost.exe`,
- heartbeat avec `active_window_title = Rechercher`,
- fenetre `Rechercher` positionnee differemment de la demo B, ce qui confirme un contexte visuel distinct.
- Bruit:
- aucun evenement clavier `Win+S` capture dans la trace,
- clics de fin de session dans zone systeme / fenetre Lea,
- `Acces vocal` reste la fenetre active parasite avant/apres le geste,
- `screenshot_context` vide sur le focus `Rechercher` dans la session serveur.
- Verdict: bonne observation de postcondition, mais methode clavier non apprise explicitement.
## Points techniques a corriger
1. Capturer explicitement les touches systeme utiles (`Win`, `Win+S`, `Esc`) ou inferer proprement la methode a partir de la transition.
2. Filtrer les clics de controle de Lea / zone systeme apres la postcondition observee.
3. Ne pas utiliser `window_capture` quand `click_inside_window = false`; preferer coordonnees ecran + moniteur + postcondition.
4. Neutraliser ou ignorer `Acces vocal` comme fenetre parasite.
5. Ne pas promouvoir une competence clavier tant que la methode n'est pas tracee ou declaree par l'humain.
## Correction appliquee apres Demo C
- `agent_v0/agent_v1/core/captor.py`: emission de `key_combo ["win"]` sur relachement de la touche Windows seule, emission de `key_combo ["escape"]`, et annulation du `win` seul quand un vrai combo `Win+...` est capture.
- `agent_v0/deploy/windows_client/agent_v1/core/captor.py`: meme correction minimale pour le client Windows deploye.
- `agent_v0/server_v1/stream_processor.py`: conservation explicite de `Win` comme geste systeme actionnable, tout en filtrant encore `Ctrl`/`Alt`/`Shift` seuls.
- `agent_v0/server_v1/stream_processor.py`: ajout des waits post-raccourci pour `Win`, `Win+S` et `Escape`.
- `agent_v0/agent_v1/network/streamer.py`: priorisation des vrais types emis par le captor (`mouse_click`, `key_combo`, `text_input`, `mouse_scroll`) pour eviter une perte sous backpressure.
- `tests/unit/test_keyboard_system_keys.py`: couverture de `Win`, `Win+S`, `Escape`, filtrage serveur et priorite streamer.
Tests executes:
- `pytest tests/unit/test_keyboard_system_keys.py tests/unit/test_lea_message_contract.py tests/unit/test_lea_micro_preflight.py tests/unit/test_safety_checks_provider.py tests/integration/test_chat_window_templates.py -q`
- `pytest tests/unit/test_keyboard_system_keys.py tests/integration/test_streamer_buffer_and_purge.py -q`
- `pytest tests/unit/test_keyboard_system_keys.py tests/integration/test_stream_processor.py tests/integration/test_streamer_buffer_and_purge.py -q`
- `python3 -m py_compile agent_v0/agent_v1/core/captor.py agent_v0/deploy/windows_client/agent_v1/core/captor.py agent_v0/server_v1/stream_processor.py agent_v0/agent_v1/network/streamer.py tests/unit/test_keyboard_system_keys.py`
- `git diff --check`
Risque restant: si Windows / NoMachine / pynput ne remonte toujours pas `Win+S`, il faudra ajouter un hook Windows-only sous flag. On ne doit pas inferer durablement `Win+S` uniquement depuis `SearchHost.exe`.
## Deploiement runtime
- `2026-05-27 17:29`: SCP vers `dom@192.168.1.11:C:/rpa_vision/agent_v1/`:
- `core/captor.py`
- `network/streamer.py`
- Sauvegardes Windows creees avec suffixe `.bak_codex_20260527_172924`.
- Verification distante par `findstr`: `_pending_standalone_win` present dans `captor.py`, priorites `mouse_click/key_combo/text_input/mouse_scroll` presentes dans `streamer.py`.
- Serveur streaming `:5005` relance avec `setsid`; PID observe `4124151`, endpoint `/health` OK.
- Healthcheck global: OK fonctionnel sur `:5005`, `:5004`, Ollama et SSH Windows; WARN attendu `qwen2.5vl:7b-rpa` non resident; FAIL non bloquant `systemd:rpa-streaming.service inactive` car serveur relance manuellement hors service systemd.
- `2026-05-27 18:43`: arret distant cible de l'arbre `run_agent_v1.py` via PowerShell/`taskkill /T`, avec fallback `Stop-Process -Force`; verification `remaining_count=0`.
- `tools/lea_healthcheck.py`: correction du diagnostic Windows pour compter les arbres de processus Lea, pas les processus bruts. Une relance normale peut produire un `pythonw.exe` parent et un `pythonw.exe` enfant pour une seule instance Lea.
- `2026-05-27 18:44`: relance Lea confirmee; healthcheck: `1 Lea instance tree(s), 2 run_agent_v1.py process(es)`, flux `/replay/next` actif.
- `2026-05-27 18:45`: micro-demo `sess_20260527T184533_8512ac`.
- OK: transition `Acces vocal` -> `Rechercher` / `SearchHost.exe`.
- OK: saisie captee dans `Rechercher`: `test` + espace + `lea apprentissage`.
- KO: pas de `key_combo`; raw_keys du premier texte montrent seulement `release s` puis `release cmd` avant la saisie. Conclusion: Windows/NoMachine/pynput avale les press de `Win+S` et ne remonte que les releases.
- `2026-05-27 18:49`: correction supplementaire dans `agent_v0/agent_v1/core/captor.py`: inference ciblee `release(s)` puis `release(cmd)` -> emission `key_combo ["win", "s"]`, avec suppression des releases du prochain `text_input`.
- Test ajoute: `test_release_only_windows_shortcut_is_inferred`.
- SCP Windows du captor corrige, sauvegarde `.bak_releaseonly_20260527_184903`, relance via tache `LeaInteractive`; healthcheck: `1 Lea instance tree(s), 2 run_agent_v1.py process(es)`.
- `2026-05-27 18:52`: micro-demo `sess_20260527T185155_98ad9a`.
- OK: `live_events.jsonl` contient `key_combo ["win", "s"]` avec `raw_keys` release-only (`s`, puis `cmd`).
- OK: transition `Acces vocal` -> `Rechercher` / `SearchHost.exe`.
- OK: la saisie arrive ensuite dans `Rechercher` sans polluer le premier `text_input` par les releases `Win+S`.
- Bruit restant: clics de fin de session dans `Rechercher`, zone systeme, puis `pythonw.exe`; a filtrer/rogner apres postcondition.
- `2026-05-27 18:55`: correction serveur dans `agent_v0/server_v1/stream_processor.py`: `_restore_user_events()` restaure aussi `key_combo` depuis `live_events.jsonl`. Sans cela, le fichier consolide `streaming_sessions/*.json` perdait `Win+S` malgre la capture brute correcte.
- Test ajoute: `tests/integration/test_stream_processor.py::TestStreamProcessor::test_restore_user_events_keeps_key_combo`.
- Session consolidee reparee: `data/training/live_sessions/streaming_sessions/sess_20260527T185155_98ad9a.json` contient maintenant 10 events, dont le premier est `key_combo ["win", "s"]`.
- `2026-05-27 18:58`: serveur streaming rebascule sous `systemd --user rpa-streaming.service` au lieu du lancement manuel; endpoint `/health` OK. Healthcheck global: `WARN` uniquement (`qwen2.5vl:7b-rpa` non resident, tache Windows `Ready` mais un arbre Lea actif).
## Marqueur de succes retenu
La competence `ouvrir le menu Demarrer` est consideree observee quand:
- la fenetre active devient `Rechercher` ou `SearchHost.exe`,
- ou le champ `Rechercher` est visible dans un heartbeat,
- sans se baser uniquement sur un clic ou une coordonnee.

View File

@@ -0,0 +1,79 @@
# Rapport d'Analyse : Pilotage de l'Agent Léa & Architecture "Juge VLM"
**Date :** 2026-05-24
**Objet :** Optimisation du Core Central et fiabilisation des interactions OS Windows.
**Statut :** Recherche & Suggestions (Zéro modification de code source).
---
## 1. Diagnostic de l'Architecture de Pilotage Actuelle
L'analyse du core central (`core/execution/observe_reason_act.py` et `agent_v0/agent_v1/core/executor.py`) révèle une architecture de type **"Open-Loop Execution"** (Exécution en boucle ouverte) :
1. **Le Serveur (Cerveau)** : Envoie une commande atomique (ex: `click` à `x,y`).
2. **L'Agent (Muscle)** : Exécute la commande et renvoie un rapport immédiat.
3. **Le Critic (Post-Vérification)** : Compare les screenshots avant/après via pHash ou vision.
### Points de friction identifiés :
* **Asynchronisme de l'OS** : Windows 11 est asynchrone. L'agent renvoie son rapport de succès *avant* que l'effet visuel ne soit stabilisé (ex: animation du menu Démarrer ou ouverture du dialogue "Enregistrer sous").
* **Frustation du Critic** : Le serveur reçoit une image "en transition" (noire, floue ou incomplète), échoue à valider l'action, et ordonne un `RETRY`. Cela crée la boucle infinie observée sur le `start_button`.
* **Cécité Contextuelle** : L'agent local n'a pas d'autonomie pour dire "Attends, une popup bloque le chemin" ; il obéit aveuglément aux coordonnées reçues.
---
## 2. Deep Dive : Le Concept de "Juge VLM"
Le **Juge VLM** est l'évolution majeure des frameworks SOTA de 2026 (Agent-S3, Coasty). Il consiste à découpler la **réussite technique** (le clic a été fait) de la **réussite sémantique** (l'objectif est atteint).
### 2.1. Fonctionnement du Juge VLM
Au lieu de renvoyer des coordonnées, le Juge VLM répond à des questions binaires ou sémantiques sur l'état du système. Il intervient à trois niveaux :
#### A. Le Juge de Pré-Condition (Visual Guard)
Avant d'exécuter l'action, l'agent interroge le VLM local (Ollama `qwen3-vl:8b`) :
* **Prompt** : *"Est-ce que le bouton 'Enregistrer' est visible et cliquable sans obstacle ?"*
* **Utilité** : Évite de cliquer dans le vide si un dialogue n'est pas encore apparu ou si une fenêtre UAC bloque l'écran.
#### B. Le Juge de Stabilisation (Visual Anchor)
Après l'action, l'agent ne rend pas la main au serveur tant que le Juge n'a pas validé la transition.
* **Prompt** : *"Le menu Démarrer est-il maintenant ouvert ? Répondre par OUI ou NON."*
* **Utilité** : Absorbe la latence de l'OS. L'agent fait du polling interne jusqu'au "OUI" ou au timeout. Le serveur ne voit que le résultat final stabilisé.
#### C. Le Juge de Conformité (Semantic Critic)
C'est le niveau le plus élevé, utilisé pour les dialogues complexes.
* **Prompt** : *"L'utilisateur a demandé d'enregistrer le fichier sous le nom 'test.txt'. Est-ce que le dialogue actuel confirme que le fichier sera enregistré dans le dossier 'Documents' ?"*
* **Utilité** : Détecte les erreurs métier que le pixel-matching (pHash) ignore totalement.
### 2.2. Intégration avec le pattern "Best-of-N"
Le Juge VLM permet d'implémenter le **Best-of-N rollouts**. Si le Juge dit "NON" après une action, l'agent peut tenter localement une variante (ex: presser `Enter` au lieu de cliquer sur `Enregistrer`) sans que le serveur n'ait à gérer la complexité du retry.
---
## 3. Suggestions de Pilotage pour "Léa V3"
### Suggestion 1 : L'Autonomie de la "Dernière Milliseconde"
L'agent local (`executor.py`) doit cesser d'être un simple terminal. Il doit intégrer une **boucle de rétroaction locale**.
* **Idée** : Si le serveur demande un clic sur "Enregistrer", l'agent local vérifie par OCR/Vision rapide que le texte "Enregistrer" est bien sous le curseur avant de cliquer. S'il ne l'est pas, il attend 500ms et ré-essaie localement.
### Suggestion 2 : Normalisation sémantique des Dialogues
Utiliser des signatures sémantiques pour les fenêtres Windows.
* **Idée** : Créer un dictionnaire de "DialogStates" (ex: `STATE_SAVE_AS`, `STATE_START_MENU`). Chaque état est défini par un ensemble de mots-clés et d'icônes. Le pilotage devient une suite de transitions d'états : `IDLE -> STATE_START_MENU -> STATE_APP_OPENED`.
### Suggestion 3 : La Cascade de Modèles (Mixture-of-Grounding)
* **Rapide/Local** : Pour le mouvement de souris et la détection de texte simple (EasyOCR).
* **Lourd/Serveur** : Pour la résolution de cibles complexes (UI-TARS-2 / InfiGUI).
* **Cognitif/Ollama** : Pour le "Juge VLM" et la validation de l'intention.
---
## 4. Conclusion : Pourquoi Léa boucle sur le `start_button` ?
D'après cette analyse, le blocage est une **crise de confiance entre l'Agent et le Serveur** :
1. L'Agent fait le fallback `Win`.
2. L'Agent envoie le screenshot trop tôt (pendant l'animation Windows).
3. Le Serveur (Critic) voit un écran flou, ne trouve pas le menu, et dit "Échec, recommence".
4. L'Agent reçoit l'ordre de recommencer et boucle.
**La solution recommandée (sans code)** : Introduire une pause de stabilisation visuelle pilotée par un Juge VLM local qui ne libère l'agent que lorsque le menu est "vrai" à l'écran.
---
*Ce rapport est une étude technique destinée à orienter les futures itérations de l'architecture Léa. Aucune modification n'a été apportée aux fichiers sources du projet.*

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,766 @@
# SPEC TRANSPORT — Contrat dispatch / ack / retry / orphan / resume
**Date :** 2026-05-24
**Auteur :** Claude (recherche dispatchée, lecture seule sur code)
**Statut :** spécification contractuelle. Aucune modif code. Toutes les sections sont en tables ou state machines, prose minimale.
**Pré-requis :**
- `docs/recherche/AXE_B1_REPLAY_TRANSPORT.md` (transport SSE/WebSocket — choix techno)
- `docs/recherche/AXE_B1_DEEP_WATCHDOG.md` (implémentation watchdog)
- `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` (bug 9 actions perdues)
- `docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` §4
Ce doc fige **le contrat** entre `api_stream.py` (FastAPI Linux) et `agent_v1/core/executor.py` + `agent_v1/network/streamer.py` (Léa Windows). Il est **isomorphe poll ↔ SSE** (cf. §9).
---
## 1. TL;DR + diagramme d'ensemble
Une action visuelle est un **message borné par 2 IDs** : `action_id` (unique par step de replay, déterministe) + `attempt_id` (UUID, incrémenté à chaque re-dispatch transport). Le serveur tient une **mini-visibility-timeout in-memory** (`_retry_pending`). Le client tient un **LRU `dedup_set`** des `attempt_id` récemment exécutés. La pause supervisée (`paused_need_help`) est l'état absorbant en cas d'épuisement des essais ou de signal explicite (`system_dialog`, `wrong_window`, `target_not_found`). Le contrat est identique en polling (transport actuel) et en SSE (cible AXE_B1).
```
┌───────────── server (Linux, FastAPI :5005) ─────────────┐
│ │
_replay_queues │ ┌──────────┐ DISPATCH ┌─────────────┐ │
[session_id] │ │ PENDING │ ──────────────►│ DISPATCHED │ │
│ └──────────┘ │ (_retry_ │ │
│ ▲ │ pending) │ │
│ │ repush head └─────┬───────┘ │
│ │ (watchdog) │ REPORT(success) │
│ ┌────┴─────┐ age>30s, resent │ verify OK │
│ │ ORPHAN │ ◄─── timeout ◄────────┤ │
│ │ (resent_ │ │ │
│ │ count++)│ ▼ │
│ └──────────┘ ┌──────────┐ │
│ │ resent ≥ MAX │ ACKED │ │
│ ▼ └──────────┘ │
│ ┌──────────┐ REPORT(fail+system_dialog/wrong_window) │
│ │ ABANDONED│◄──┬────────────────┐ │
│ └──────────┘ │ │ │
│ │ ▼ ▼ │
│ │ ┌──────────────┐ ┌──────────┐ │
│ └──►│ PAUSE_NEED_ │ │ FAILED │ │
│ │ HELP │ │ (retry │ │
│ └─────┬────────┘ │ budget │ │
│ │ /resume │ out) │ │
│ ▼ └──────────┘ │
│ (resume_action en tête queue) │
└──────────────────────────────────────────────────────────┘
▲ │
│ POST /replay/result │ GET /replay/next (poll)
│ │ ou SSE event
│ ▼
┌───────────┴──────────── client Léa (Windows) ────────────┐
│ ┌──────────┐ parse ┌──────────┐ execute_replay_ │
│ │ RECEIVED │──────────►│ DEDUP │ action() │
│ └──────────┘ │ CHECK │ │
│ └────┬─────┘ │
│ hit LRU │ miss │
│ │ ▼ │
│ │ ┌──────────┐ │
│ │ │EXECUTING │ │
│ │ └────┬─────┘ │
│ ▼ ▼ │
│ ┌──────────────┐ │
│ │ REPORTING │ → POST /replay/result │
│ └──────────────┘ │
└──────────────────────────────────────────────────────────┘
```
---
## 2. State machine serveur — une action dans `_retry_pending`
```
start
┌───────────────────────────────────────────────────────────────┐
│ PENDING │
│ présent dans _replay_queues[session_id], pas encore │
│ extrait par get_next_action │
└───────────────────────┬───────────────────────────────────────┘
│ get_next_action → pop queue + write
│ _retry_pending[action_id]={dispatched_at=now}
│ + log [REPLAY] DISPATCH
┌───────────────────────────────────────────────────────────────┐
│ DISPATCHED │
│ dans _retry_pending, attente du REPORT │
│ invariant : action absente de _replay_queues │
└───┬───────────────────────┬───────────────────────────────────┘
│ REPORT(success=True) │ REPORT(success=False, error=*)
│ verify OK ou skip │ watchdog scan age>ORPHAN_TIMEOUT
│ │
│ pop _retry_pending │ resent_count < MAX_RESENDS
│ completed_actions++ │ → repush head + resent++
│ current_idx++ │ → dispatched_at=0
▼ │ → log [BUS] lea:dispatch_
┌────────┐ │ orphan_resent
│ ACKED │ │
│ (term) │ │ resent_count ≥ MAX_RESENDS
└────────┘ │ → pop _retry_pending
│ → log [BUS] lea:dispatch_
│ orphan_giveup
│ → ABANDONED
┌─────────────────┐ watchdog repush
│ ORPHAN_RESENT │ ───────────► PENDING
│ (transitoire) │ (avec resent++)
└─────────────────┘
REPORT(verify failed AND retry_count<MAX_RETRIES_PER_ACTION):
_schedule_retry crée action_id_retry{N+1}, repush head
le nouveau action_id entre en PENDING (nouvelle entrée)
l'ancien action_id sort de DISPATCHED via pop
REPORT(system_dialog | wrong_window | target_not_found):
──► PAUSE_NEED_HELP (replay_state.status)
_retry_pending non purgé sur-le-champ (peut être réutilisé par /resume)
ABANDONED:
entrée _retry_pending supprimée
──► si politique = pause sur Nème giveup → PAUSE_NEED_HELP
sinon : action perdue, replay continue sur l'action suivante
CANCELLED (POST /replay/<id>/cancel):
purge _retry_pending par replay_id (api_stream.py:4489)
_replay_queues[session_id] = []
state.status = "cancelled"
```
**Invariants serveurs :**
| # | Invariant | Garanti par |
|---|---|---|
| I1 | Une action en `DISPATCHED` est absente de `_replay_queues` | pop atomique sous `_replay_lock` (api_stream.py:3346-3348) |
| I2 | `action_id` unique dans `_retry_pending` à un instant t | clé dict + check `if action_id_sent not in _retry_pending` (api_stream.py:3354) |
| I3 | `report_action_result.pop(action_id)` est idempotent | `pop(key, None)` retourne None si déjà acquitté (api_stream.py:3491) |
| I4 | Cancel purge bien `_retry_pending` pour ce replay | iter `_retry_pending.items() if v["replay_id"]==replay_id` (api_stream.py:4489-4491) |
| I5 | Watchdog re-check sous lock avant repush | pattern `if aid not in self._retry_pending: skip` (AXE_B1_DEEP §3) |
| I6 | Pause `paused_need_help` ne distribue aucune action | `get_next_action` retourne `replay_paused=True` (api_stream.py:2951) |
---
## 3. State machine client Léa — une action côté `executor.py`
```
┌──────────────┐
│ POLLING │ thread `_poll_loop`, every 1s (+backoff)
│ (idle) │ GET /replay/next?session_id&machine_id
└──────┬───────┘
│ HTTP 200 + action ≠ null
┌──────────────┐
│ RECEIVED │ data["action"] parsé
└──────┬───────┘
┌───────────────┴────────────────┐
│ attempt_id ∈ dedup_set ? │
▼ ▼
OUI : SKIP NON
(log warning, ack synthetique) │
┌──────────────┐
│ EXECUTING │ execute_replay_action()
│ │ ├─ pre-check window
│ │ ├─ resolve target visuel
│ │ ├─ click / type / key
│ │ └─ screenshot_after
└──────┬───────┘
┌─────────────────┼──────────────────┐
│ │ │
success=True success=False system_dialog
warning=None error=* détecté
│ │ │
▼ ▼ ▼
┌─────────────────────────────────┐
│ REPORTING │
│ POST /replay/result + retry │
│ (timeout=10s, allow_redirects=False)
└─────────────────┬────────────────┘
┌──────────┴──────────┐
HTTP 200 fail/timeout
│ │
▼ ▼
┌──────────┐ ┌──────────────┐
│ ACKED │ │ REPORT_RETRY │ (PAS implémenté
│ + LRU │ │ (in-memory) │ v1 ; à v2)
│ store │ └──────┬───────┘
│ attempt │ │
│ _id │ ▼
└──────────┘ (perte report : serveur watchdog
rattrapera via orphan)
Pendant POLLING : si data.replay_paused → afficher PauseDialog
Pendant EXECUTING : timeout par étape gérée par execute_replay_action
(resolve serveur 30s, _wait_for_screen_change 1000ms+, capture 0.5s)
```
**Invariants clients :**
| # | Invariant | Garanti par |
|---|---|---|
| C1 | Une action reçue est TOUJOURS reportée (succès ou échec) | try/except global executor.py:2429-2503, fallback `result={success:False, error=…}` |
| C2 | Un seul `poll_and_execute` à la fois | `self._replay_lock.acquire(blocking=False)` executor.py:2291 |
| C3 | Pas de blocage event tray UI pendant exécution | thread dédié `_poll_loop` |
| C4 | Idempotence côté action (v2) | dedup_set LRU bornée 256 entrées sur `attempt_id` (§6.2) — **À AJOUTER** |
| C5 | Pause UI déclenchée uniquement sur signal serveur explicite | `data.get("replay_paused")` executor.py:2346 |
---
## 4. Contrats JSON
### 4.1. Payload DISPATCH serveur → client (GET /replay/next OU SSE event)
**Cas nominal : action visuelle**
| Champ | Type | Obligatoire | Description | Source code |
|---|---|---|---|---|
| `action` | object \| null | oui | l'action ou `null` si rien | api_stream.py:3436 |
| `session_id` | str | oui | session active | api_stream.py:3438 |
| `machine_id` | str | oui | machine cible | api_stream.py:3439 |
| `action.action_id` | str | oui | identifiant unique step, ex. `step_4c0663941f22` | DB workflow + suffixes `_retry1/_resume` |
| `action.attempt_id` | str | **À AJOUTER** | UUID hex 16, nouveau à chaque dispatch (initial OU resend) | n/a, v2 |
| `action.type` | enum | oui | `click`/`type`/`key_combo`/`wait`/`scroll`/`pause_for_human`/`extract_text`/... | core/executor.py:2422 |
| `action.target_spec` | object | si visuelle | `{by_text,vlm_description,anchor_image_base64,resolve_order,window_title,uia_target}` | api_stream.py:3364 |
| `action.parameters` | object | dep. type | `{text,keys,duration_ms,condition,…}` | dépend du type |
| `action.expected_window_before` | str | non | titre fenêtre attendue avant clic | api_stream.py:3366 |
| `action.expected_window_title` | str | non | titre fenêtre attendue après clic | api_stream.py:3369 |
| `action.success_strict` | bool | non | mode strict (skip OCR fuzzy) | api_stream.py:3387 |
| `action.intention` | str | non | description humaine | api_stream.py:3379 |
| `action.monitor_resolution` | object | oui (QW1) | `{idx,offset_x,offset_y,w,h,source}` | api_stream.py:3403 |
| `action.from_node` | str | non | id node WorkflowGraph (active pre-check) | api_stream.py:3229 |
| `action.dispatch_meta` | object | **À AJOUTER** | `{first_dispatched_at,resent_count,last_resent_at}` pour visibilité client | v2 |
| `precheck` | object | non | résultat pre-check serveur `{match,similarity,popup_detected}` | api_stream.py:3441 |
| `server_busy` | bool | non | lock occupé, retry plus tard | api_stream.py:2944 |
| `replay_paused` | bool | non | replay en pause supervisée | api_stream.py:2960 |
| `pause_message` | str | si paused | message à afficher dans bulle | api_stream.py:2961 |
| `replay_id` | str | si paused | pour ack ciblé via /resume | api_stream.py:2962 |
| `auth_detected` | bool | non | injection automatique d'actions d'auth | api_stream.py:3304 |
**Exemple complet (v2 cible) :**
```json
{
"action": {
"action_id": "step_4c0663941f22",
"attempt_id": "a8f3c2d1e9b4f720",
"type": "click",
"target_spec": {
"by_text": "Imagerie",
"resolve_order": ["ocr","template","vlm"],
"anchor_image_base64": "iVBORw0KGgo…",
"window_title": "MOREL Catherine — Easily Assure",
"uia_target": null
},
"parameters": {},
"expected_window_before": "MOREL Catherine — Easily Assure",
"expected_window_title": "MOREL Catherine — Easily Assure",
"success_strict": true,
"intention": "Cliquer onglet Imagerie",
"monitor_resolution": {"idx":1,"offset_x":0,"offset_y":0,"w":2560,"h":1600,"source":"action_hint"},
"from_node": "node_tab_imagerie",
"dispatch_meta": {
"first_dispatched_at": 1779015600.123,
"resent_count": 0,
"last_resent_at": 0.0
}
},
"session_id": "sess_demo_42",
"machine_id": "DESKTOP-58D5CAC"
}
```
**Cas pause supervisée :**
```json
{
"action": null,
"session_id": "sess_demo_42",
"machine_id": "DESKTOP-58D5CAC",
"replay_paused": true,
"pause_message": "Je n'y arrive pas (« Coller ou saisir... »)",
"replay_id": "replay_free_68ca51ab"
}
```
**Cas idle / server_busy :**
```json
{"action": null, "session_id":"sess_demo_42", "machine_id":"…", "server_busy": true}
```
### 4.2. Payload REPORT client → serveur (POST /replay/result)
| Champ | Type | Obligatoire | Description | Source code |
|---|---|---|---|---|
| `session_id` | str | oui | | api_stream.py:628 |
| `action_id` | str | oui | identifiant de l'action acquittée | api_stream.py:629 |
| `attempt_id` | str | **À AJOUTER** | echo du `attempt_id` reçu (corrélation watchdog ↔ client) | v2 |
| `success` | bool | oui | résultat global | api_stream.py:630 |
| `error` | str \| null | dep. success | message court (`target_not_found`, `system_dialog:uac_consent`, …) | api_stream.py:631 |
| `warning` | str \| null | non | `no_screen_change`/`popup_handled`/`visual_resolve_failed`/`wrong_window` | api_stream.py:632 |
| `screenshot_after` | str \| null | recommandé | base64 PNG ou path | api_stream.py:634 |
| `screenshot_before` | str \| null | recommandé (clic) | base64 PNG du frame pre-action (Critic) | api_stream.py:635 |
| `actual_position` | object | si visuel | `{x_pct: float, y_pct: float}` coords cliquées | api_stream.py:636 |
| `resolution_method` | str | si visuel | `server_resolve_hybrid`/`template_match`/... | api_stream.py:638 |
| `resolution_score` | float | si visuel | 0.01.0 | api_stream.py:639 |
| `resolution_elapsed_ms` | float | si visuel | latence cascade | api_stream.py:640 |
| `target_description` | str | si fail | description humaine pour bulle pause | api_stream.py:642 |
| `target_spec` | object | si fail | echo target_spec pour reconstruction | api_stream.py:643 |
| `correction` | object | si pédagogique | `{x_pct,y_pct,uia_snapshot,crop_b64}` mode supervisé | api_stream.py:645 |
| `system_dialog` | object | si dialog | `{category,matched_signal,matched_value,reason,context}` | api_stream.py:650 |
| `needs_human` | bool | non | force pause supervisée | api_stream.py:651 |
**Exemple succès :**
```json
{
"session_id": "sess_demo_42",
"action_id": "step_4c0663941f22",
"attempt_id": "a8f3c2d1e9b4f720",
"success": true,
"actual_position": {"x_pct":0.2305,"y_pct":0.2805},
"resolution_method": "server_resolve_hybrid_text_direct",
"resolution_score": 0.80,
"resolution_elapsed_ms": 412.7,
"screenshot_before": "iVBO…",
"screenshot_after": "iVBO…"
}
```
**Exemple échec target_not_found :**
```json
{
"session_id": "sess_demo_42",
"action_id": "step_36346c1c40b9",
"attempt_id": "b9e4d3c2f0a5e831",
"success": false,
"error": "target_not_found",
"warning": "visual_resolve_failed",
"target_description": "Coller ou saisir le dossier patient",
"target_spec": {"by_text":"Coller ou saisir le dossier patient", "…":"…"},
"screenshot_after": "iVBO…"
}
```
**Exemple system_dialog (UAC) :**
```json
{
"session_id":"sess_demo_42",
"action_id":"step_xxx",
"attempt_id":"…",
"success": false,
"error": "system_dialog:uac_consent",
"system_dialog": {
"category": "uac_consent",
"matched_signal": "window_title",
"matched_value": "Contrôle de compte d'utilisateur",
"reason": "UAC consent prompt blocking click",
"context": "handle_popup_vlm"
},
"needs_human": true,
"screenshot_after": "iVBO…"
}
```
### 4.3. Payload re-dispatch (orphan resent)
Identique au DISPATCH normal **sauf** `dispatch_meta` enrichi :
```json
{
"action": {
"action_id": "step_4c0663941f22", // INCHANGÉ
"attempt_id": "c2d5e8f1a3b7c049", // NOUVEAU (UUID frais)
"type": "click",
"...": "…",
"dispatch_meta": {
"first_dispatched_at": 1779015600.123,
"resent_count": 1,
"last_resent_at": 1779015630.987,
"resend_reason": "orphan_timeout"
}
}
}
```
**Règle :** `action_id` reste stable (preserve idempotence côté serveur via `_retry_pending`). **Seul `attempt_id` change** (permet au client de distinguer un vrai re-dispatch d'un doublon réseau).
### 4.4. Payload escalation pause supervisée
Envoyé par le serveur dans la réponse au prochain poll après bascule `paused_need_help` (cf. §4.1 cas pause). Le client doit afficher la bulle et arrêter d'exécuter jusqu'à reception d'un nouveau dispatch (qui sera l'action de resume).
**Payload /replay/{replay_id}/resume (POST) :** corps optionnel `{"acknowledged_check_ids": ["chk_1","chk_2"]}` (QW4). Réponse :
```json
{
"status": "resumed",
"replay_id": "replay_free_68ca51ab",
"session_id": "sess_demo_42",
"remaining_actions": 12
}
```
**Payload /replay/{replay_id}/cancel (POST) :** corps vide. Réponse :
```json
{"status": "cancelled", "replay_id": "…", "session_id": "…"}
```
---
## 5. Matrice des cas limites — la table principale du document
Notation : **S** = état serveur (PENDING/DISPATCHED/ORPHAN/ACKED/ABANDONED/PAUSE), **C** = état client (POLLING/RECEIVED/EXECUTING/REPORTING/IDLE/DEAD).
| # | Scénario | État serveur (avant→après) | État client (avant→après) | Comportement attendu (v2 contrat) | Risque idempotence | Status code / contournement |
|---|---|---|---|---|---|---|
| **a** | Client coupe AVANT réception réponse `/replay/next` (bug 8 mai) | DISPATCHED→ORPHAN→PENDING→DISPATCHED | POLLING→POLLING (timeout) | Watchdog détecte age>30s, re-dispatch (`attempt_id` neuf, `resent_count=1`). Bulle "action retentée" facultative. | **Faible** : action_id stable, client n'a JAMAIS exécuté → pas de double-effet. | OK via AXE_B1_DEEP §3 |
| **b** | Client coupe APRÈS réception, AVANT exécution | DISPATCHED→ORPHAN→PENDING→DISPATCHED | RECEIVED→DEAD | Watchdog re-dispatch. Si client revit, reçoit nouvelle attempt → exécute. Si client mort, watchdog finit en ABANDONED. | **Faible** : action perdue avant tout effet. | OK |
| **c** | Client exécute, coupe AVANT envoi report | DISPATCHED→ORPHAN→PENDING→DISPATCHED | EXECUTING→REPORTING→DEAD (avant POST) | Watchdog re-dispatch. **2e exécution probable** côté client. Idempotence action requise (§6.3). | **ÉLEVÉ** : double clic, double saisie possibles. Cible critique : `type` → préfixer `Ctrl+A`. | dedup_set côté client (§6.2) BLOQUE la 2e si même `action_id` reçu < 256 messages |
| **d** | Client report success, serveur ne reçoit pas (HTTP 502, timeout serveur côté POST) | DISPATCHED→ORPHAN→PENDING | EXECUTING→REPORTING(echec POST)→IDLE | Client doit **retenter** le POST (boucle interne avec backoff). v1 : un seul essai (executor.py:2476 timeout=10s, pas de retry). **À AJOUTER v2** : retry 3× backoff [1,3,7]s. Sinon watchdog re-dispatch + dedup_set côté client→ack synthétique. | Moyen | À ajouter retry POST côté client |
| **e** | Client report success=false, retry budget restant | DISPATCHED→FAILED(verify)→PENDING(retry_N+1) | EXECUTING→REPORTING→POLLING | `_schedule_retry` crée nouvelle entrée `{action_id}_retry{N+1}` (replay_engine.py:2604), repush head. `verify_failed` ne consomme PAS le budget orphan. | Géré | OK |
| **f** | Watchdog re-dispatch ALORS QUE client envoie report tardif | DISPATCHED→ORPHAN→repush en cours | EXECUTING→REPORTING(en vol) | Race window. Re-check sous lock dans watchdog `if aid not in _retry_pending: skip` (AXE_B1_DEEP §3 _scan_once). Si pop arrive 1er : repush skip. Si repush arrive 1er : pop ignoré (`no_active_replay`). | Faible | Géré par I3 + I5 |
| **g** | Watchdog re-dispatch, mais client a déjà ré-exécuté (cas c+f combinés) | DISPATCHED→ORPHAN→repush | EXECUTING(1)→REPORTING(1)→RECEIVED(2)→EXECUTING(2) | dedup_set client détecte 2e `action_id` identique → log warning + ack synthétique `{success:true, warning:"already_executed"}`. **Sans dedup** : double exécution = bug applicatif. | **CRITIQUE sans dedup** | dedup_set v2 obligatoire |
| **h** | Client mort silencieux (Léa crash, NoMachine freeze) | DISPATCHED→ORPHAN→PENDING→…→ABANDONED→PAUSE | DEAD | Watchdog MAX_RESENDS=2 puis ABANDONED. **Hook v1.1** : si ≥2 give-ups en 60s sur même session → bascule replay_state.status=paused_need_help + message "Léa ne répond plus" (AXE_B1_DEEP §6 R4). | OK avec hook | À ajouter hook dead_client_signal |
| **i** | Serveur restart pendant actions en `_retry_pending` | TOUT en mémoire → PERTE | POLLING → reçoit 404 / 503 / ConnectionError | `_retry_pending` est in-memory. Au restart : queue vide, replay_state perdu (sauf si persisté en DB — vérifier). Client backoff exponentiel ; quand serveur revient, replay_state restaurable depuis SQLite mais `_retry_pending` non. **Décision v1** : rebuild best-effort — `_replay_states` sauvegardés en SQLite ont les `completed_actions`, on relance depuis `current_action_index+1`. Pas de rejeu des actions en vol. **Lacune connue** : si action mid-flight ; à valider avec Dom. | n/a (état perdu) | **À TRANCHER avec Dom** : persistance _retry_pending ? |
| **j** | Polls clients simultanés en course (2 process Léa, ou retry rapide) | DISPATCHED (1 seul vainqueur du lock) | 2× POLLING | `_replay_lock.acquire(timeout=4.5)` : 1er gagne, 2e reçoit `{server_busy:true}` (api_stream.py:2944). Client backoff. Ordre des steps préservé (lock global). | Faible | OK |
| **k** | Action arrivée 2× côté client (double-clic même bouton) | DISPATCHED (attempt_1) puis DISPATCHED (attempt_2) | RECEIVED→DEDUP_CHECK→SKIP (2e) | dedup_set client = LRU 256 sur `action_id`. 2e réception → ack synthétique success=true warning="already_executed", PAS de ré-exécution. | Bloqué côté client | dedup_set v2 |
| **l** | Pause supervisée serveur déclenchée pendant action en vol | DISPATCHED→PAUSE | EXECUTING→REPORTING (résultat ignoré côté serveur ?) | Le serveur applique la pause sur le step SUIVANT (boucle `while queue` voit `paused`). L'action en vol s'achève normalement, report traité (pop+verify), puis prochain poll → `replay_paused=true`. **PAS de cancel de l'action en vol** (pas de protocole serveur→client pour interrompre une action en cours). | Faible | OK |
| **m** | Cancel replay côté VWB UI pendant action en vol | DISPATCHED→cancelled (purge _retry_pending) | EXECUTING→REPORTING | Cancel purge `_retry_pending` par replay_id (api_stream.py:4489) ET vide `_replay_queues[session_id]`. Le report tardif arrive : `pop(action_id)→None` → réponse `no_active_replay` (api_stream.py:3488). Client log info. Pas d'erreur. | Faible | OK |
| **n** | Cap MAX_RESENDS atteint | ORPHAN→ABANDONED | DEAD ou EXECUTING (cas g) | Log `[BUS] lea:dispatch_orphan_giveup`. v1 = action perdue silencieusement, replay continue (peut bloquer step suivant si dépendance). **Politique v2 :** si action critique (type ∈ {click,type,t2a_decision}) → bascule `paused_need_help` immédiatement avec message "Léa n'a pas répondu, vérifie". Si non critique (wait,scroll) → log seul, continue. | n/a | **À TRANCHER** : seuil par type ? |
| **o** | Action non-visuelle (`extract_text`, `t2a_decision`) vs visuelle (`click`) | Non-visuelle : pas de DISPATCHED, exécutée *server-side* dans la même boucle `get_next_action` (api_stream.py:3132-3197) | Jamais reçue par le client | **Contrats distincts** : non-visuelles n'entrent JAMAIS dans `_retry_pending`. Le watchdog n'a rien à scanner pour elles. Si extract_text plante (Ollama 503), `queue.pop(0)` + log warning + continue (api_stream.py:3195) → action serveur perdue silencieusement. **Risque pas couvert par watchdog.** | n/a | **À TRANCHER** : retry serveur sur actions serveur ? séparé du watchdog actions visuelles |
| **p** | Workflow se termine alors qu'action est encore en `_retry_pending` | DISPATCHED en cours → workflow.completed | n/a | `_replay_states[replay_id].status = "completed"`. Si une action est encore en `_retry_pending`, le watchdog la verra orphan ; au resend, `get_next_action` ne trouvera pas de `owning_replay` (status `running` requis ligne 2974) → queue vide retournée. **Fuite mémoire** : entrée `_retry_pending` jamais purgée tant que pas de cancel ou age > MAX_RESENDS. **Mitigation** : ajouter purge `_retry_pending` sur transition vers completed/error/failed (analogue à cancel ligne 4489). | Faible (mais leak) | **À AJOUTER v2** : purge à la complétion |
| **q** | Précheck "wait" injecté (popup détectée) — n'est PAS une action workflow | Pas dans `_retry_pending` (action synthétique) | RECEIVED→EXECUTING(wait 2000ms)→REPORTING(success=true) | wait_action a un `action_id=precheck_wait_<6hex>` non stockée côté serveur. Le report arrive : `pop(action_id)→None`, action ignorée gracieusement. | Faible | OK |
| **r** | Replay paused, client continue à poller | PAUSE | POLLING reçoit `replay_paused:true` à chaque tick | Client affiche bulle 1 fois (dedup sur `_last_pause_msg_shown` executor.py:2351), continue à poller. CPU loss négligeable. | Faible | OK |
| **s** | Reverse-proxy NPM bufferise SSE | DISPATCHED, event jamais reçu | POLLING/SSE silencieux | `X-Accel-Buffering: no` côté server response. Ping 15s force flush. AXE_B1 §4 §8 risques. | Faible avec headers | OK avec headers |
| **t** | NoMachine timeout idle (>60s) | DISPATCHED dormant | SSE→reconnect via Last-Event-ID | sseclient-py auto-reconnect, `Last-Event-ID` header repris au reconnect → serveur peut sauter les events déjà acquittés. v1 polling : pas de Last-Event-ID, juste réacheminement via watchdog. | Faible | OK |
| **u** | Bearer token expire/révoqué pendant un replay actif | DISPATCHED en attente | POLLING reçoit 401 | Client doit re-auth (hors scope v1 : tokens longue durée). v1 : crash + tray notification. Watchdog côté serveur continue à scanner — actions partent en ABANDONED après MAX. | Faible | hors scope v1 |
---
## 6. Sémantique d'idempotence
### 6.1. Couches d'idempotence
| Couche | Mécanisme | Effet | Implémenté ? |
|---|---|---|---|
| **Serveur — pop sur report** | `_retry_pending.pop(action_id, None)` retourne None silencieux si déjà acquitté | report en double n'augmente pas `completed_actions` 2× | ✅ api_stream.py:3491 |
| **Serveur — re-check watchdog** | `if aid not in _retry_pending: skip` sous lock | re-dispatch annulé si report arrivé entre snapshot et repush | ✅ AXE_B1_DEEP §3 |
| **Serveur — cancel purge** | itération `_retry_pending` par replay_id | aucun ghost-resend après cancel | ✅ api_stream.py:4489 |
| **Action — `action_id` stable** | identifiant unique step (`step_<hex>` puis suffixes `_retry{N}` ou `_resume`) | clé du `pop` côté serveur, clé du dedup côté client | ✅ DB workflow + replay_engine.py:2609 |
| **Action — `attempt_id` rotatif** | UUID nouveau à chaque DISPATCH (initial + chaque resend) | distingue un re-dispatch légitime d'un doublon réseau, permet stats orphan | ❌ **À AJOUTER v2** |
| **Client — dedup_set LRU 256** | `set` bornée de `(action_id)` ou `(action_id, attempt_id)` récemment exécutés | bloque ré-exécution en cas g/k | ❌ **À AJOUTER v2 obligatoire** |
| **Action — idempotence intrinsèque** | clear field avant `type`, idempotence native du `click` sur tab actif | minimise dégât en cas de double exécution résiduelle | ⚠ **À documenter dans VWB**, pas dans code |
### 6.2. Spec dedup_set client (v2)
```python
# agent_v1/core/executor.py — à ajouter
from collections import OrderedDict
class ActionDedupSet:
"""LRU bornée d'action_id récemment exécutées.
Bloque ré-exécution si action arrive 2 fois (orphan resent + double réseau).
"""
def __init__(self, max_size: int = 256):
self._store: OrderedDict[str, float] = OrderedDict() # action_id → ts
self._max = max_size
def seen(self, action_id: str) -> bool:
if action_id in self._store:
# Touch (LRU)
self._store.move_to_end(action_id)
return True
return False
def mark(self, action_id: str) -> None:
self._store[action_id] = time.time()
self._store.move_to_end(action_id)
if len(self._store) > self._max:
self._store.popitem(last=False)
```
**Usage dans `poll_and_execute_inner` AVANT `execute_replay_action` :**
```python
if self._dedup.seen(action.get("action_id","")):
logger.warning(f"[DEDUP] action {action.get('action_id')} déjà exécutée — ack synthétique")
self._post_synthetic_ack(action, server_url, replay_result_url,
success=True, warning="already_executed")
return True
self._dedup.mark(action.get("action_id",""))
# … execute_replay_action(action) …
```
### 6.3. Idempotence intrinsèque par type d'action
| Type | Idempotent nativement ? | Mitigation si exécuté 2× |
|---|---|---|
| `click` sur tab/bouton actif | OUI (le tab reste actif) | aucune |
| `click` sur bouton "Submit"/"Valider" | NON (double formulaire) | **dedup_set CRITIQUE** + dialog confirm côté app |
| `type` texte | NON (double saisie) | préfixer `Ctrl+A` (clear) + dedup_set |
| `keyboard_shortcut` Ctrl+S | dep. (1 sauvegarde = 1 dialog) | dedup_set |
| `keyboard_shortcut` Ctrl+V | NON (double collage) | dedup_set + clear avant |
| `scroll` | OUI mais déplace 2× | tolérable, dedup_set conseillé |
| `wait` | OUI | aucun risque |
| `extract_text` (server-side) | OUI (lecture pure) | n/a |
| `t2a_decision` (server-side, LLM) | OUI mais re-coût LLM ($/temps) | retry serveur, pas client |
---
## 7. Timeouts et seuils
| Nom | Défaut | Env var | Effet | Source |
|---|---:|---|---|---|
| `client_poll_timeout` | 30 s | non, en dur | `requests.get(/replay/next, timeout=30)` côté Léa | executor.py:2320 |
| `client_report_timeout` | 10 s | non, en dur | `requests.post(/replay/result, timeout=10)` | executor.py:2480 |
| `client_resolve_timeout` | 30 s | non, en dur | appel serveur `/resolve_target` | executor.py:1898 |
| `server_replay_lock_timeout` | 4.5 s | non, en dur | `_async_replay_lock(timeout=4.5)` → 503 ou server_busy | api_stream.py:539, 2938 |
| `server_action_server_side_timeout` | 180 s | non, en dur | `asyncio.wait_for(extract_text/t2a, 180)` | api_stream.py:3141 |
| `server_paste_and_execute_timeout` | 30 s | non, en dur | paste+execute ydotool | api_stream.py:3192 |
| `server_precheck_timeout` | 0.5 s | non, en dur | CLIP embed pre-check | api_stream.py:3250 |
| `heartbeat_max_age` | varie | `RPA_HEARTBEAT_MAX_AGE_SECONDS` | utilité pre-check | api_stream.py:3235 |
| `WATCHDOG_SCAN_INTERVAL_S` | 10 s | `RPA_WATCHDOG_SCAN_INTERVAL_S` | période scan orphan | AXE_B1_DEEP §11 |
| `WATCHDOG_ORPHAN_TIMEOUT_S` | 30 s | `RPA_WATCHDOG_ORPHAN_TIMEOUT_S` | age sans report → orphan | AXE_B1_DEEP §11 |
| `WATCHDOG_MAX_RESENDS` | 2 | `RPA_WATCHDOG_MAX_RESENDS` | give-up après N resends | AXE_B1_DEEP §11 |
| `WATCHDOG_REPUSH_POSITION` | `head` | `RPA_WATCHDOG_REPUSH_POSITION` | head/tail | AXE_B1_DEEP §11 |
| `WATCHDOG_ENABLED` | `1` | `RPA_WATCHDOG_ENABLED` | kill-switch | AXE_B1_DEEP §11 |
| `REPLAY_STATE_TTL_SECONDS` | varie | `RPA_REPLAY_STATE_TTL` | purge states finis | api_stream.py:726 |
| `MAX_REPLAY_STATES` | n | en dur | borne `_replay_states` | api_stream.py:735 |
| `MAX_RETRIES_PER_ACTION` | 3 | en dur | budget retry métier `_schedule_retry` | replay_engine.py:2591 |
| `_poll_backoff_min` | varie | n/a | reset après HTTP 200 | executor.py:2334 |
| `_poll_backoff_max` | varie | n/a | plafond backoff exponentiel | executor.py:2326 |
| `_poll_backoff_factor` | 2.0 | n/a | facteur multiplicatif | executor.py:2326 |
| `SSE_PING_INTERVAL_S` (cible) | 15 | env futur | heartbeat SSE | AXE_B1 §4 |
**Cohérence des seuils :**
- `client_poll_timeout (30s) > server_replay_lock_timeout (4.5s)` → OK, le client attend bien la réponse server_busy.
- `server_action_server_side_timeout (180s) > client_poll_timeout (30s)` → SI extract_text dure 35s sans dispatcher d'action visuelle entre temps, le client coupe MAIS le serveur continue ; au prochain poll le serveur a fini, dispatche l'action visuelle suivante. **Pas de perte tant que l'action visuelle dispatch est rapide après extract_text.** Bug 8 mai = extract_text + dispatch click dans la MÊME réponse → 5s timeout dépassé → fix `timeout=30` adopté.
- `WATCHDOG_ORPHAN_TIMEOUT_S (30s) > client_poll_timeout (30s)` → frontière dangereuse. **Recommandation : remonter à 45s** pour laisser le temps au client de retenter au moins 1 poll naturellement avant que le watchdog résende.
---
## 8. Transitions vers pause supervisée et resume
### 8.1. Déclencheurs `status = "paused_need_help"`
| Déclencheur | Source | État avant | État après | Champ enrichi |
|---|---|---|---|---|
| `pause_for_human` en mode supervised ou safety_checks présents | api_stream.py:3066-3111 | running | paused_need_help | `safety_checks`, `pause_payload`, `failed_action.reason="user_request"` |
| Report `system_dialog:*` (UAC/CredUI/SmartScreen) | api_stream.py:3785-3870 | running | paused_need_help | `failed_action.reason="system_dialog"`, message contextualisé |
| Report `warning="wrong_window"` | api_stream.py:3872-3920 | running | paused_need_help | `failed_action.reason="wrong_window"` |
| Report `success=false` + `error="target_not_found"` après MAX_RETRIES_PER_ACTION (3) | api_stream.py:3949-4030 | running | paused_need_help | `failed_action.target_description` |
| Hook v1.1 dead client signal (2+ giveups en 60s) | À AJOUTER (AXE_B1_DEEP §6 R4) | running | paused_need_help | `failed_action.reason="dead_client"` |
| Hook v2 (cas n) : N orphan giveup sur action type critique | À DÉCIDER avec Dom | running | paused_need_help | `failed_action.reason="orphan_max_resends"` |
### 8.2. État de `_retry_pending` au moment de la pause
- **Pause via `pause_for_human`** : aucune action en vol (pause arrive *avant* dispatch).
- **Pause via report failed** : l'action qui a déclenché la pause vient d'être `pop`pée. `_retry_pending` est **vide pour cet action_id** (déjà acquittée). Aucune purge supplémentaire nécessaire.
- **Pause via watchdog hook (v1.1)** : `_retry_pending` peut contenir des entrées orphelines avec age > MAX. **Politique :** purger en transition (à ajouter dans le hook).
### 8.3. /resume — reconstruction de l'action
`resume_replay` (api_stream.py:4361-4474) :
1. Vérifie state.status == `paused_need_help` (sinon 409).
2. Vérifie acquittement safety_checks required (sinon 400).
3. Reset state : `status="running"`, `failed_action=None`, `pause_message=None`, `safety_checks=[]`.
4. Reconstruit l'action :
- Priorité 1 : `failed_action.original_action` si présent.
- Priorité 2 : `_retry_pending.pop(failed_action.action_id, {}).get("action")`.
- Priorité 3 : minimum `{action_id, type, target_spec, visual_mode}`.
5. Nouveau `action_id = "{original}_resume"`.
6. Enregistre dans `_retry_pending[resume_id] = {action,retry_count:0,replay_id,reason:"resume_after_pause"}`.
7. Insère en tête `_replay_queues[session_id]`.
**Lacune v1 :** le nouveau `action_id` (`_resume`) **n'a pas de `attempt_id`** explicite. Au prochain dispatch, le watchdog démarre le compteur à 0. Cohérent.
### 8.4. Event bus `[BUS]`
| Event | Quand | Payload (log structuré) | Source |
|---|---|---|---|
| `[BUS] lea:safety_checks_generated` | Pause `pause_for_human` avec checks | `replay=<id> count=N sources=[…]` | api_stream.py:3081 |
| `[BUS] lea:monitor_routed` | Dispatch action visuelle (résolution monitor) | `replay=<id> action=<id> idx=N source=<…>` | api_stream.py:3419 |
| `[BUS] lea:dispatch_orphan_resent` (v1.1) | Watchdog repush | `action_id=X resent=N/MAX age=Ts session machine replay` | AXE_B1_DEEP §3 |
| `[BUS] lea:dispatch_orphan_giveup` (v1.1) | Watchdog abandon | `action_id=X resent=N age_total=Ts session machine replay` | AXE_B1_DEEP §3 |
| `[BUS] lea:dead_client_signal` (v2) | Hook ≥2 giveups/60s | `session=<S> dead_count=N period=60s` | À AJOUTER |
Tous les events sont consommables via `journalctl --user -u rpa-streaming -f | grep '\[BUS\]'`. Pas de bus pub/sub réel (pattern QW1/QW4 = log structuré).
---
## 9. Compatibilité polling actuel ↔ futur SSE
Le contrat des §4 / §5 / §6 / §8 est **invariant** par rapport au transport. Voici ce qui change vs ce qui ne change pas :
| Aspect | Polling (v1 actuel) | SSE (cible AXE_B1) | Change ? |
|---|---|---|---|
| Endpoint dispatch | `GET /api/v1/traces/stream/replay/next` (1 réponse JSON) | `GET /api/v1/traces/stream/replay/events` (stream `text/event-stream`) | OUI |
| Format payload action | JSON dans body | JSON dans `data:` field d'un `ServerSentEvent` (event=`action`) | NON (même schéma) |
| Endpoint report | `POST /api/v1/traces/stream/replay/result` | identique | NON |
| ID corrélation | `action_id` + (v2) `attempt_id` | identique + `id:` SSE = `action_id` | NON |
| Détection déco client | Indirecte (pas de poll suivant) | `await request.is_disconnected()` immédiat | OUI (gain) |
| Détection déco serveur | Timeout client 30s | `EventSource.onerror` + reconnect natif | OUI (gain) |
| Reprise après reconnect | Pas de Last-Event-ID, watchdog seul | `Last-Event-ID` header automatique côté sseclient-py | OUI (gain) |
| Watchdog `_retry_pending` | **ACTIF** | **ACTIF** (ceinture+bretelles, cf. AXE_B1_DEEP §12) | NON |
| dedup_set client | **ACTIF v2** | **ACTIF v2** | NON |
| `_replay_lock` serveur | Tient pendant exécution serveur (extract_text…) | Idem (les actions server-side restent dans la même boucle) | NON |
| Bulle pause client | Reçue via `replay_paused:true` au prochain poll | Reçue via event `event=paused` ou via `replay_paused:true` dans event `action` | NON (même UX) |
**Flag de bascule :** `RPA_REPLAY_TRANSPORT=poll|sse` côté client (executor.py choisit poll vs `replay_subscriber.py`) et serveur (les 2 endpoints coexistent — pas de mutual exclusion). Permet rollback 1-ligne.
**Garantie de migration :** un client v2 polling et un client v2 SSE consomment **strictement le même contrat de message**. Le watchdog serveur scanne `_retry_pending` indépendamment du transport. Tous les invariants I1I6 et C1C5 tiennent identiquement.
**Seul écart pratique :** en SSE, `WATCHDOG_ORPHAN_TIMEOUT_S` peut descendre à 15s (déconnexion détectée plus tôt). En polling, garder 30s (laisser une chance au polling naturel).
---
## 10. Précédents externes — fiches courtes
### 10.1. AWS SQS — visibility timeout
- **Contrat :** message reçu devient *invisible* pour `VisibilityTimeout` secondes (défaut 30s). Si pas `DeleteMessage` avant expiration → redevient visible, redélivrable.
- **Modèle :** at-least-once delivery (standard queues), exactly-once (FIFO via `MessageDeduplicationId`).
- **Idempotence :** **côté consommateur obligatoire** (chez AWS « your processing logic must be idempotent »). DLQ pour les empoisonnés.
- **Cap :** `ChangeMessageVisibility` pour étendre dynamiquement. Limite dure 12h.
- **Notre mapping :** `_retry_pending[action_id] = {dispatched_at}` = visibility timeout in-memory. `WATCHDOG_ORPHAN_TIMEOUT_S = 30s` = `VisibilityTimeout`. `WATCHDOG_MAX_RESENDS = 2` = `maxReceiveCount` avant DLQ. ABANDONED = DLQ équivalent (mais sans queue physique, juste log).
- **Source :** [SQS visibility timeout doc](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html) (consulté 2026-05-24)
### 10.2. NATS JetStream — pull consumer ack
- **Contrat :** `AckExplicit` par défaut. `AckWait` (défaut 30s) = délai avant redélivrance. `MaxDeliver` = N attempts max. `MaxAckPending` = window flow control (défaut 1000).
- **NAK :** redélivrance immédiate (ou `nakWithDelay`).
- **Backoff :** liste `[5s, 30s, 300s, …]` qui *override* `AckWait`. Si liste plus courte que `MaxDeliver`, dernier délai répété.
- **Notre mapping :** `AckWait` = `WATCHDOG_ORPHAN_TIMEOUT_S`. `MaxDeliver` = `WATCHDOG_MAX_RESENDS+1`. **Pas de NAK explicite chez nous** : un report success=false suit la voie retry métier (`_schedule_retry`), pas la voie transport. **Pas de backoff** dans la v1 du watchdog (justification AXE_B1_DEEP §5 : démo médicale, réactivité prime). Adoptable si besoin.
- **Source :** [NATS JetStream Consumers doc](https://docs.nats.io/nats-concepts/jetstream/consumers) (consulté 2026-05-24)
### 10.3. Skyvern — `execute_step` + `handle_failed_step`
- **Contrat :** boucle récursive `execute_step` (forge/agent.py lignes 10941577). À chaque step :
- `step.status == failed``handle_failed_step()` retourne *next step* (retry) ou *None* (terminal).
- `step.status == completed``handle_completed_step()` décide advance vs verify vs finalize.
- **Cap :** `max_steps_per_run` global, hiérarchie task → org → settings (ligne 1169-1176).
- **Idempotence :** PR récente a *retiré* le retry interne du `fail_task` (transition status uniquement). Skyvern délègue le retry au LLM via re-emit du prochain action_use.
- **Différence avec nous :** Skyvern = monolithe local (browser CDP), pas de transport HTTP entre dispatcher et exécuteur. Notre cas nécessite un layer transport en plus, d'où `_retry_pending` qui n'a pas d'équivalent direct.
- **Source :** [Skyvern agent.py main](https://github.com/Skyvern-AI/skyvern/blob/main/skyvern/forge/agent.py), [PR #434 better catch exceptions](https://github.com/Skyvern-AI/skyvern/pull/434)
### 10.4. browser-use — action_id + idempotency guard
- **Contrat :** `max_failures` config (défaut 3). Action cache court-terme keyed sur `(command, selector, value)` pour éviter side-effects dupliqués si retry rapide.
- **Pattern d'idempotency key :** « *deterministic key before execution, generated from workflow run ID, step index, and action type* » (cf. mightybot blog).
- **Notre mapping :** `action_id` déterministe = `step_<hex_workflow>` + suffixes. dedup_set client = équivalent action cache court-terme.
- **Différence :** browser-use est intra-process (loop Python contrôle Chromium via CDP local). Notre cas inter-process inter-machine.
- **Source :** [browser-use AGENTS.md](https://github.com/browser-use/browser-use/blob/main/AGENTS.md), [Idempotent AI agents — buildmvpfast 2026](https://www.buildmvpfast.com/blog/idempotent-ai-agent-retry-safe-patterns-production-workflow-2026)
### 10.5. Anthropic Computer Use SDK — tool_use_id binding
- **Contrat :** chaque `tool_use` retourné par Claude a un `id` ; le code applicatif doit retourner un `tool_result` avec `tool_use_id` identique (loop.py lignes 234-254).
- **Retry :** uniquement au niveau API (`max_retries=4` côté client Anthropic, ligne 182). **Pas de retry au niveau tool execution** — c'est le modèle qui re-décide au prochain tour.
- **Idempotence :** non garantie par le SDK. Délégué à l'application (« deduplication or idempotency key handling visible in this loop : none »).
- **Notre mapping :** `tool_use_id``action_id`. Mais notre boucle est *server-driven* (queue d'actions pré-compilée par VWB), pas LLM-driven. Plus déterministe, donc plus simple à idempotenter.
- **Source :** [computer-use-demo/loop.py main](https://github.com/anthropics/claude-quickstarts/blob/main/computer-use-demo/computer_use_demo/loop.py), [Tool use Claude API docs](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/overview)
### 10.6. Playwright MCP — SSE remote transport
- **Contrat :** transport stdio (local) OU HTTP/SSE (remote). Tools/list au handshake, tool/call par event SSE descendant, tool/result POST remontant.
- **Issue connue 2026 :** « SSE stream disconnected » après idle (cline/cline #8367). Mitigation = ping applicatif.
- **Timeout :** 30s par défaut sur CDP endpoint connect.
- **Notre mapping :** très proche du pattern cible AXE_B1 §4 (SSE descendant + POST ack). Confirme robustesse du choix techno. Précaution : prévoir reconnect natif (sseclient-py).
- **Source :** [microsoft/playwright-mcp](https://github.com/microsoft/playwright-mcp), [cline issue #8367 SSE disconnect](https://github.com/cline/cline/issues/8367)
### 10.7. Synthèse comparative
| Système | ID corrélation | Visibility/Ack timeout | Max retry transport | Dedup client | Modèle delivery |
|---|---|---|---|---|---|
| **AWS SQS std** | MessageId + ReceiptHandle | VisibilityTimeout 30s | maxReceiveCount (DLQ) | obligatoire app | at-least-once |
| **NATS JetStream** | StreamSeq + ConsumerSeq | AckWait 30s | MaxDeliver | obligatoire app | at-least-once |
| **Skyvern** | step.step_id | n/a (monolithe) | max_steps_per_run | n/a | exactly-once local |
| **browser-use** | (cmd, selector, value) | n/a | max_failures=3 | action cache | exactly-once local |
| **Anthropic CU** | tool_use_id | n/a | max_retries client (API) | non garanti | exactly-once par tour |
| **Playwright MCP** | request_id | 30s CDP | n/a (LLM décide) | non garanti | best-effort |
| **Nous (v2 cible)** | `action_id` + `attempt_id` | ORPHAN_TIMEOUT 30s | MAX_RESENDS=2 | dedup_set 256 LRU | at-least-once + dedup → effectif exactly-once |
---
## 11. Sources
### Code interne (lecture seule, lignes vérifiées 2026-05-24)
- `agent_v0/server_v1/api_stream.py:520-559``_replay_lock`, `_async_replay_lock`, `_replay_queues`, `_replay_states`, `_machine_replay_target`
- `agent_v0/server_v1/api_stream.py:626-651``ReplayResultReport` Pydantic schema
- `agent_v0/server_v1/api_stream.py:2906-3443``get_next_action` (DISPATCH path)
- `agent_v0/server_v1/api_stream.py:3132-3197` — actions server-side `extract_text/t2a_decision/...`
- `agent_v0/server_v1/api_stream.py:3354-3359` — création `_retry_pending` (à enrichir AXE_B1_DEEP §4.1)
- `agent_v0/server_v1/api_stream.py:3446-3491``report_action_result`, pop idempotent
- `agent_v0/server_v1/api_stream.py:3785-3870` — bascule `paused_need_help` sur system_dialog
- `agent_v0/server_v1/api_stream.py:4361-4474``resume_replay` + safety_checks
- `agent_v0/server_v1/api_stream.py:4477-4494``cancel_replay` + purge
- `agent_v0/server_v1/replay_engine.py:2583-2642``_schedule_retry` (retry métier, distinct du retry transport)
- `agent_v0/agent_v1/core/executor.py:2275-2503``poll_and_execute` + `_poll_and_execute_inner`
- `agent_v0/agent_v1/core/executor.py:2308-2321``requests.get(/replay/next, timeout=30)` (fix 8 mai)
- `agent_v0/agent_v1/core/executor.py:2476-2501``requests.post(/replay/result, timeout=10)`
- `agent_v0/agent_v1/network/streamer.py:1-120` — streaming events/screenshots (canal séparé du replay)
### Docs internes
- `docs/recherche/AXE_B1_REPLAY_TRANSPORT.md` (2026-05-23) — choix SSE vs WebSocket, pseudo-code endpoint
- `docs/recherche/AXE_B1_DEEP_WATCHDOG.md` (2026-05-24) — implémentation watchdog complète
- `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` — diagnostic 9 actions perdues, racine du contrat
- `docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` §4 — synthèse replay
- `docs/LESSONS_LEARNED_GHT_2026-05.md` — bugs P0 post-démo
- `docs/SMOKE_TEST_FINALIZE_REPLAY_2026-05-20.md` — contrat finalize → replay
### Sources externes (consultées 2026-05-24)
**Patterns queue / visibility timeout / idempotence**
- [Amazon SQS visibility timeout](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html)
- [Amazon SQS exactly-once processing FIFO](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues-exactly-once-processing.html)
- [Amazon SQS message deduplication ID](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/using-messagededuplicationid-property.html)
- [NATS JetStream Consumers](https://docs.nats.io/nats-concepts/jetstream/consumers)
- [NATS JetStream Model Deep Dive](https://docs.nats.io/using-nats/developer/develop_jetstream/model_deep_dive)
- [How to Handle SQS Visibility Timeout (oneuptime 2026-01-27)](https://oneuptime.com/blog/post/2026-01-27-sqs-message-visibility-timeout/view)
- [Achieving idempotency in AWS serverless (Albaqali)](https://qasimalbaqali.medium.com/achieving-idempotency-in-the-aws-serverless-space-d0671a521479)
**Frameworks RPA / Computer Use**
- [Skyvern forge/agent.py main](https://github.com/Skyvern-AI/skyvern/blob/main/skyvern/forge/agent.py) — `execute_step` 1094-1577
- [Skyvern webeye/actions/handler.py](https://github.com/Skyvern-AI/skyvern/blob/main/skyvern/webeye/actions/handler.py)
- [Skyvern PR #434 better catch exceptions](https://github.com/Skyvern-AI/skyvern/pull/434)
- [Skyvern retry run webhook docs](https://www.skyvern.com/docs/api-reference/api-reference/agent/retry-run-webhook)
- [Anthropic computer-use-demo loop.py](https://github.com/anthropics/claude-quickstarts/blob/main/computer-use-demo/computer_use_demo/loop.py) — dispatch 234-254
- [Anthropic Tool use overview docs](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/overview)
- [Microsoft Playwright MCP](https://github.com/microsoft/playwright-mcp)
- [Cline SSE disconnect issue #8367](https://github.com/cline/cline/issues/8367)
- [browser-use AGENTS.md main](https://github.com/browser-use/browser-use/blob/main/AGENTS.md)
- [browser-use issue #3615 agent not stopping](https://github.com/browser-use/browser-use/issues/3615)
**Patterns idempotence agents AI**
- [Idempotent AI Agents — buildmvpfast 2026](https://www.buildmvpfast.com/blog/idempotent-ai-agent-retry-safe-patterns-production-workflow-2026)
- [Fault-Tolerant AI Agent Pipelines — MightyBot](https://mightybot.ai/blog/fault-tolerant-ai-agent-pipelines/)
- [Action Verification and Retries in LLM Agent Loops — ingramhaus](https://ingramhaus.com/action-verification-and-retries-in-llm-agent-execution-loops)
- [Idempotency in Distributed Systems — aloknecessary](https://aloknecessary.github.io/blogs/idempotency-distributed-systems/)
- [Stripe API idempotent requests](https://docs.stripe.com/api/idempotent_requests)
**Transport SSE / FastAPI**
- [sse-starlette GitHub](https://github.com/sysid/sse-starlette)
- [FastAPI lifespan events](https://fastapi.tiangolo.com/advanced/events/)
- [Stop streaming response when client disconnects (FastAPI #7572)](https://github.com/fastapi/fastapi/discussions/7572)
---
## 12. Décisions non tranchables sans Dom (sortir explicitement du contrat v2)
Ces points sont identifiés mais demandent un arbitrage produit :
| # | Cas limite | Question | Recommandation Claude |
|---|---|---|---|
| D1 | Cas (i) — restart serveur pendant actions en `_retry_pending` | Faut-il persister `_retry_pending` (SQLite) pour rebuild ? Ou accepter perte transport au restart ? | **Accepter perte v2** : le restart serveur est volontaire (`systemctl restart`), Pauline relance le replay depuis VWB. Surcoût persistance > bénéfice. |
| D2 | Cas (n) — politique abandon | Bascule en `paused_need_help` après MAX_RESENDS atteint ? Pour quels types d'action ? | **OUI pour `click`/`type`/`t2a_decision`** (critiques). **Log seul pour `wait`/`scroll`** (continuer). À ajouter dans hook watchdog v1.1. |
| D3 | Cas (o) — actions server-side perdues | Retry serveur sur `extract_text` qui timeout ? Watchdog dédié actions server-side ? | **Différer** : v1 = un seul try + log warning, comportement actuel acceptable. v2 envisageable si bench Ollama montre instabilités fréquentes. |
| D4 | Cas (p) — purge `_retry_pending` à la complétion workflow | Ajouter purge automatique en transition vers `completed/error/failed` ? | **OUI**, simple à ajouter analogue à cancel (api_stream.py:4489). |
| D5 | dedup_set côté client | Implémenter v2 obligatoire ? Quelle taille LRU ? Inclure `attempt_id` ou juste `action_id` ? | **OUI obligatoire v2.** Taille 256 (couvre largement les workflows GHT 50 steps). Key = `action_id` seul (le `attempt_id` n'apporte rien côté dedup — l'objectif est de bloquer la double exécution même action). |
| D6 | `attempt_id` côté serveur | Générer UUID à chaque dispatch (initial + resend) ? Stocker l'historique ? | **OUI v2.** Génération à chaque DISPATCH dans `get_next_action`. Pas d'historique nécessaire (logs structurés `[BUS] lea:dispatch_orphan_resent` suffisent). |
| D7 | Migration backward-compat | Si client v1 (sans dedup_set, sans `attempt_id` echo) parle à serveur v2, casse-t-il ? | **NON** : `attempt_id` est optionnel côté serveur (toléré absent). dedup_set est purement défensif côté client. Migration progressive sans rupture. |
| D8 | Cas (l) — protocole d'interruption serveur→client d'une action en vol | Ajouter mécanisme `cancel_in_flight` ? | **NON v1, v2** : pas nécessaire pour la démo. Pause supervisée sur step suivant suffit. |
---
## 13. Liens vers autres specs en cours
- **`spec_validator`** (à venir) — un Validator strict (sémantique post-action) ne peut être fiable que si toutes les actions arrivent. **SPEC_TRANSPORT est prérequis logique de spec_validator.** Le contrat REPORT.warning peut s'enrichir de codes de Validator (`semantic_fail`, `expected_text_not_found`) sans casser ce contrat.
- **`spec_popups`** (à venir) — la détection popup côté serveur (pre-check) ET côté client (DialogHandler) émet des actions synthétiques `wait` ou des reports `warning="popup_handled"`. **Cas (q) du §5 documente la non-interférence.** Le contrat dialog/popup s'imbrique sur les mêmes endpoints sans extension.
- **AXE_B2 (Validator)** : couvre le côté `verify_action` + Critic sémantique (déjà partiellement codé dans `replay_verifier.py`). À spécifier en parallèle.
- **AXE_B4 (ORA Observe-Reason-Act)** : pousse aussi dans `_replay_queues` → bénéficie gratuitement du watchdog et du contrat.
- **AXE_D2 (Dialog/Popup)** : `system_dialog` + `wrong_window` + DialogHandler — branches bascule pause supervisée déjà tracées §8.1.
---
*Document de spécification contractuelle. Lecture seule sur code, aucune modification. À valider par Dom avant implémentation v2 (dedup_set, attempt_id, hooks watchdog).*

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,274 @@
# Spec technique — Raccord agent-chat (5004) → Shadow (5005)
**Ticket:** Q-P1-agentchat
**Date:** 2026-06-01
## 1. État des lieux
| Composant | Fichier | État |
|---|---|---|
| Endpoints Shadow | `agent_v0/server_v1/api_stream.py` L2415-2650 | **Existent**, fonctionnels |
| ShadowObserver | `core/workflow/shadow_observer.py` | **Existe**, get_shared_observer() |
| ShadowValidator | `core/workflow/shadow_validator.py` | **Existe**, apply_feedback + build_workflow_ir |
| Agent-chat intent | `agent_chat/intent_parser.py` | "apprends-moi" → **IntentType.EXECUTE** (L120) |
| Agent-chat caller | `agent_chat/app.py` | **AUCUN appel Shadow** — endpoints orphelins |
| Persist competence | `api_stream.py` | **N'existe pas** — tâche séparée |
## 2. Contrats d'endpoints Shadow (port 5005)
### 2.1 `POST /api/v1/shadow/start`
```json
// Request body
{"session_id": "sess_xxx"}
// Response 200
{"status": "shadow_started", "session_id": "sess_xxx", "message": "..."}
```
Appelle `observer.start(session_id)`. Aucune autre dépendance.
### 2.2 `POST /api/v1/shadow/stop`
```json
// Request body
{"session_id": "sess_xxx"}
// Response 200
{
"status": "shadow_stopped",
"session_id": "sess_xxx",
"steps_count": N,
"understanding": [{"step": 1, "intent": "...", "confidence": 0.x, ...}]
}
```
Appelle `observer.stop(session_id)` puis `observer.get_understanding()`.
### 2.3 `GET /api/v1/shadow/{session_id}/understanding`
Retourne steps, current_step, notifications (optionnel `?since_ts=...`).
### 2.4 `POST /api/v1/shadow/feedback`
```json
{
"session_id": "sess_xxx",
"action": "validate|correct|undo|cancel|merge_next|split",
"step_index": 1, // optionnel sauf pour correct/undo/merge/split
"new_intent": "...", // requis si action=correct
"at_event_index": 5 // requis si action=split
}
```
Appelle `validator.apply_feedback()`. Retourne `{"status": "feedback_applied|feedback_rejected", "result": {...}}`.
### 2.5 `POST /api/v1/shadow/build`
```json
{"session_id": "sess_xxx", "name": "Nom workflow", "domain": "generic", "require_all_validated": false}
```
Retourne `{"status": "workflow_built", "workflow_ir": {...}}` ou `{"status": "cancelled"}`.
### Auth
Tous les endpoints exigent `Authorization: Bearer <RPA_API_TOKEN>` (sauf si `RPA_AUTH_DISABLED=true`). Agent-chat utilise déjà `_streaming_headers()` pour ses appels replay — réutiliser le même mécanisme.
## 3. Résolution session_id
**Problème :** le ShadowObserver est indexé par `session_id`, mais les événements sont streamés par l'agent V1 (sur la machine de Dom) via `POST /api/v1/traces/stream/event`. Le `session_id` du Shadow start/stop doit correspondre au `session_id` utilisé par l'agent V1 pour streamer ses événements.
**Mécanisme existant :** le StreamProcessor auto-enregistre les sessions à la réception du premier event (`_ensure_session_registered`). Le `session_id` est donc déterminé par l'agent V1 côté client.
**Solution retenue :** agent-chat ne génère pas de session_id. Il utilise la **session active** de la machine cible.
### 3.1 Résolution machine_id
Agent-chat appelle `GET /api/v1/traces/stream/machines` (déjà implémenté dans `_fetch_connected_machines()`, app.py L193). En mode mono-machine (cas Dom seul), une seule machine est connectée → `machine_id` = celle de la liste.
### 3.2 Résolution session_id (flux)
```
1. Dom: "Léa, observe ce que je fais"
2. Agent-chat → GET /api/v1/traces/stream/machines
→ machine_id = "DESKTOP-XXX" (seule machine connectée)
3. Agent-chat → GET /api/v1/traces/stream/sessions?machine_id=DESKTOP-XXX
→ session_active = sessions non-finalisées [0].session_id
→ Si aucune session active, agent-chat dit "Je ne vois pas de session active,
commence par faire une action d'abord"
4. Agent-chat → POST /api/v1/shadow/start {"session_id": session_active}
```
**Alternative (plus simple) :** si l'agent V1 stream déjà des événements (session existe), le ShadowObserver peut auto-start via `observe_event` (L355 de shadow_observer.py : "Auto-start si pas encore démarré"). Dans ce cas, agent-chat peut appeler `/shadow/start` avec le session_id résolu pour être explicite, mais ce n'est pas strictement nécessaire — les événements alimenteront l'observer même sans start explicite.
**Décision :** start explicite requis pour la sémantique ("Léa, observe" = début intentionnel) et pour que `observer.stop()` puisse finaliser le segment en cours proprement.
## 4. Flux complet spécifié
```
┌─────────────────────────────────────────────────────┐
│ Phase 1 : DÉMARRAGE OBSERVATION │
│ │
│ Dom → Agent-chat (5004): "Léa, observe ce que je fais"│
│ ↓ │
│ Agent-chat: │
│ 1. IntentParser.parse() → IntentType.EXECUTE │
│ workflow_hint = "observe ce que je fais" │
│ (pattern "apprends-moi" ou "observe" détecté) │
│ 2. Résoudre session_id: │
│ GET /api/v1/traces/stream/sessions │
│ → session_active = première session non-finalisée│
│ → Si aucune: "Aucune session active détectée" │
│ 3. POST /api/v1/shadow/start │
│ {"session_id": session_active} │
│ headers = _streaming_headers() │
│ 4. Si erreur 404 → session inactive │
│ Si erreur 401 → token manquant │
│ Si connexion error → "Streaming server injoignable"│
│ 5. Réponse Dom: "J'observe. Fais ta tâche." │
│ │
├─────────────────────────────────────────────────────┤
│ Phase 2 : DOM EFFECTUE L'ACTION │
│ │
│ Agent V1 (machine Dom) → Streaming server (5005): │
│ POST /api/v1/traces/stream/event (en continu) │
│ → worker.process_event_direct() │
│ → shadow_observe_event(session_id, event) │
│ → ShadowObserver.observe_event() (incrémental) │
│ │
│ (Aucune action requise d'agent-chat pendant cette │
│ phase — les événements sont streamés directement) │
│ │
├─────────────────────────────────────────────────────┤
│ Phase 3 : ARRÊT OBSERVATION │
│ │
│ Dom → Agent-chat: "Léa, c'est fini" │
│ ↓ │
│ Agent-chat: │
│ 1. IntentParser.parse() → IntentType.EXECUTE │
│ (détecté par verbe "fini" + contexte shadow) │
│ OU par contexte conversation (pending shadow) │
│ 2. POST /api/v1/shadow/stop │
│ {"session_id": session_active} │
│ 3. Retour: {understanding: [...], steps_count: N} │
│ 4. Affiche à Dom les étapes comprises: │
│ "J'ai observé N étapes:" │
│ 1. Ouvrir le Bloc-notes (confiance 0.8) │
│ 2. Saisir du texte (confiance 0.5) │
│ ... │
│ 5. Demande validation: "C'est correct ?" │
│ │
├─────────────────────────────────────────────────────┤
│ Phase 4 : FEEDBACK (N tours) │
│ │
│ Dom → Agent-chat: "L'étape 2, c'est 'Rechercher X'" │
│ ↓ │
│ Agent-chat: │
│ 1. Parse la correction → action=correct │
│ step_index=2, new_intent="Rechercher X" │
│ 2. POST /api/v1/shadow/feedback │
│ {session_id, action:"correct", │
│ step_index:2, new_intent:"Rechercher X"} │
│ 3. Réaffiche les étapes mises à jour │
│ 4. Redemande: "Et là, c'est bon ?" │
│ │
│ Si Dom dit "oui" → action=validate pour chaque étape │
│ Si Dom dit "supprime l'étape 3" → action=undo │
│ Si Dom dit "annule tout" → action=cancel │
│ │
├─────────────────────────────────────────────────────┤
│ Phase 5 : BUILD + PERSIST │
│ │
│ Après validation complète: │
│ 1. POST /api/v1/shadow/build │
│ {session_id, name:"Nom auto ou donné par Dom", │
│ domain:"generic", require_all_validated:false} │
│ 2. Retour: {workflow_ir: {...}} │
│ 3. POST /api/v1/lea/competences/candidate/persist │
│ ← N'EXISTE PAS ENCORE (tâche séparée) │
│ À implémenter : reçoit workflow_ir + machine_id │
│ → crée YAML dans data/competences/candidate/ │
│ 4. Réponse Dom: "Tâche apprise et sauvegardée" │
│ 5. observer.reset(session_id) via call Shadow │
│ (nettoyage état mémoire) │
└─────────────────────────────────────────────────────┘
```
## 5. Gestion des intentions agent-chat
### Détection existante (intent_parser.py)
| Phrase type | Pattern | IntentType actuel |
|---|---|---|
| "apprends-moi à ..." | `r"(?:apprends|apprenez)[- ]moi\s+(.+)"` | EXECUTE |
| "observe ce que je fais" | **NON DÉTECTÉ** — pas de pattern | UNKNOWN → CLARIFY |
### Modifications requises dans `agent_chat/intent_parser.py`
Ajouter une nouvelle intention `IntentType.LEARN` (ou `SHADOW`) dans l'enum, avec les patterns :
```python
IntentType.LEARN = "learn" # Démarrer/arrêter une session d'observation Shadow
```
Patterns à ajouter dans `INTENT_PATTERNS[IntentType.LEARN]` :
- `r"(?:observe|regarde)[ -]?(?:moi|ce que je fais)"`
- `r"(?:apprend|apprenez)[- ]moi"`
- `r"(?:montre[- ]moi\s+comment|fais\s+comme\s+moi)"`
- `r"(?:c'est\s+fini|j'ai\s+fini|stop\s+observation)"` (pour le stop, détectable aussi par contexte)
### Ou approche contextuelle (recommandée)
Ne pas créer de nouveau IntentType. Utiliser un **flag de contexte** dans le ConversationManager :
```python
# Quand Dom dit "observe ce que je fais" (EXECUTE, non-match workflow)
if result.get("teach_me") and not matcher:
conversation_manager.set_session_flag(session, "shadow_pending")
# Au prochain message, si flag shadow_pending:
if conversation_manager.has_session_flag(session, "shadow_pending"):
# Dom dit "oui", "c'est parti" → start shadow
# Dom dit autre chose → interpréter comme contexte shadow
```
**Décision recommandée :** approche contextuelle. Elle évite de polluer l'enum et permet de gérer le cycle start/stop comme un sous-état de la conversation, pas comme une intention globale.
## 6. Gestion des erreurs
| Erreur | Endpoint | Réponse agent-chat à Dom |
|---|---|---|
| Streaming server injoignable (ConnectionError) | start/stop/feedback/build | "Je n'arrive pas à joindre le serveur de streaming. Vérifie qu'il tourne (port 5005)." |
| 401 Unauthorized | start/stop/feedback/build | "Problème d'authentification — le token API est invalide." |
| 404 Not Found (session inactive) | start/stop | "Je ne trouve pas de session active. Commence par faire une action pour que je puisse observer." |
| 400 Bad Request (build sans étapes validées) | build | "Impossible de construire le workflow — aucune étape n'a été validée." |
| Timeout (>15s) | tout | "Le serveur met trop de temps à répondre. Réessaie." |
| Session déjà active (double start) | start | No-op côté observer (reset implicite) — informer Dom: "J'observe déjà cette session." |
| Stop sans session active | stop | "Je n'étais pas en mode observation." |
## 7. Paramètres par appel
| Appel | Méthode | Body | Headers | Timeout |
|---|---|---|---|---|
| Résoudre session | GET | (query params) | `_streaming_headers()` | 3s |
| Shadow start | POST | `{"session_id": "..."}` | `_streaming_headers()` | 5s |
| Shadow stop | POST | `{"session_id": "..."}` | `_streaming_headers()` | 5s |
| Shadow feedback | POST | `{"session_id": "...", "action": "...", ...}` | `_streaming_headers()` | 5s |
| Shadow build | POST | `{"session_id": "...", "name": "...", ...}` | `_streaming_headers()` | 10s |
| Persist | POST | `{"workflow_ir": {...}, "machine_id": "..."}` | `_streaming_headers()` | 10s |
Tous les appels utilisent `STREAMING_SERVER_URL` (déjà configuré via `RPA_STREAMING_URL`, défaut `http://localhost:5005`).
## 8. Ce qui n'est PAS dans le scope
- **Endpoint `/persist`** : n'existe pas. Tâche séparée (~80-120 lignes, cf. handoff).
- **Bouton dashboard** : interdit par contrainte.
- **Canvas/nodes VWB** : interdit par contrainte.
- **Génération de session_id par agent-chat** : le session_id est résolu côté serveur.
- **Multi-machine** : en mono-machine Dom, la résolution est triviale (seule machine connectée). Multi-machine = évolution future.
## 9. Fichiers à modifier
| Fichier | Modification |
|---|---|
| `agent_chat/app.py` | Ajouter fonctions `_shadow_start()`, `_shadow_stop()`, `_shadow_feedback()`, `_shadow_build()` + logique de détection dans `api_chat()` |
| `agent_chat/intent_parser.py` | Ajouter patterns de détection "observe"/"c'est fini" (shadow) OU approche contextuelle via ConversationManager |
| `agent_chat/conversation_manager.py` | Ajouter `set_session_flag` / `has_session_flag` pour état `shadow_session` |
## 10. Implémentation suggérée (ordre)
1. **Fonctions callers Shadow** dans `app.py` — fonctions pures qui appellent les endpoints (analogues à `_try_streaming_server_replay`)
2. **Détection intention shadow** — patterns + contexte dans `api_chat()`
3. **Cycle start → stop → understanding** — afficher les étapes à Dom
4. **Cycle feedback** — parser les corrections de Dom et appeler `/shadow/feedback`
5. **Build** — appeler `/shadow/build` après validation complète
6. **Persist** — brancher quand l'existe (tâche séparée)