docs(refs): commit groupé docs de référence session 2026-05-08

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-05-09 11:32:52 +02:00
parent 5dc20cc85b
commit e0b47e4518
7 changed files with 2124 additions and 0 deletions

View File

@@ -0,0 +1,859 @@
# AUDIT — Contrôles débranchés (serveur)
Date : 2026-05-08
Branche : feature/qw-suite-mai
HEAD : 56e869c46
Périmètre : agent_v0/server_v1/ + core/* importés. Client exclu.
## 1. Inventaire des fichiers audités
| Fichier | Lignes |
|---|---|
| agent_v0/server_v1/__init__.py | 0 |
| agent_v0/server_v1/visual_wait.py | 54 |
| agent_v0/server_v1/monitor_router.py | 99 |
| agent_v0/server_v1/replay_failure_logger.py | 143 |
| agent_v0/server_v1/vm_controller.py | 143 |
| agent_v0/server_v1/loop_detector.py | 154 |
| agent_v0/server_v1/worker_stream.py | 172 |
| agent_v0/server_v1/workflow_replay.py | 185 |
| agent_v0/server_v1/safety_checks_provider.py | 195 |
| agent_v0/server_v1/session_worker.py | 253 |
| agent_v0/server_v1/agent_registry.py | 296 |
| agent_v0/server_v1/replay_memory.py | 323 |
| agent_v0/server_v1/execution_plan_runner.py | 373 |
| agent_v0/server_v1/audit_trail.py | 393 |
| agent_v0/server_v1/replay_learner.py | 395 |
| agent_v0/server_v1/run_worker.py | 397 |
| agent_v0/server_v1/live_session_manager.py | 464 |
| agent_v0/server_v1/task_planner.py | 596 |
| agent_v0/server_v1/chat_interface.py | 622 |
| agent_v0/server_v1/replay_verifier.py | 632 |
| agent_v0/server_v1/domain_context.py | 1020 |
| agent_v0/server_v1/replay_engine.py | 1643 |
| agent_v0/server_v1/resolve_engine.py | 2585 |
| agent_v0/server_v1/stream_processor.py | 5137 |
| agent_v0/server_v1/api_stream.py | 5445 |
Total serveur : 21 719 lignes.
Modules core/ effectivement importés par le serveur :
- core.detection.omniparser_adapter (resolve_engine.py:272)
- core.detection.ollama_client, core.detection.vlm_config (resolve_engine.py:502-503, api_stream.py:790)
- core.detection.som_engine (resolve_engine.py:977)
- core.embedding.clip_embedder (resolve_engine.py:1658)
- core.anonymisation (api_stream.py:47)
- core.auth.credential_vault, core.auth.auth_handler (api_stream.py:84-85)
- core.llm.ocr_extractor (api_stream.py:824)
- core.models.workflow_graph (api_stream.py:845)
- core.workflow.shadow_observer, shadow_validator, execution_plan, execution_compiler, ir_builder (api_stream.py:1601-2656)
- core.federation.learning_pack, faiss_global (api_stream.py:4647-4690)
- core.learning.target_memory_store (replay_memory.py:62)
## 2. Findings par catégorie
### 2.1 Validations désactivées ou non consommées
**F2.1.1 — Pré-check OCR sémantique (`_validate_text_at_position`) gardé par flag off-by-default**
- `agent_v0/server_v1/api_stream.py:4519-4533`
- Citation :
```
_text_precheck_enabled = os.environ.get(
"RPA_ENABLE_TEXT_PRECHECK", "false"
).lower() in ("true", "1", "yes")
if _text_precheck_enabled and result and result.get("resolved"):
_by_text = (request.target_spec.get("by_text") or "").strip()
if _by_text:
from agent_v0.server_v1.resolve_engine import _validate_text_at_position
_is_valid, _observed, _ocr_ms = _validate_text_at_position(
tmp_path,
float(result.get("x_pct", 0) or 0),
float(result.get("y_pct", 0) or 0),
_by_text,
effective_w,
effective_h,
)
```
- Statut : off-by-default — l'appel à `_validate_text_at_position` ne s'exécute QUE si `RPA_ENABLE_TEXT_PRECHECK=true`. La fonction reste définie en `resolve_engine.py:2239-2289` mais n'est jamais consommée en production tant que la variable env n'est pas positionnée.
**F2.1.2 — `_validate_text_at_position` retourne `True` en cas d'échec OCR (politique permissive)**
- `agent_v0/server_v1/resolve_engine.py:2253-2261, 2280, 2287-2289`
- Citation :
```
Politique en cas d'échec OCR (lib absente, exception) : retourne
(True, "", 0.0) pour ne pas bloquer le flux. Mieux vaut un faux positif
rare qu'une régression bloquante introduite par la validation elle-même.
"""
reader = _get_validation_ocr_reader()
if reader is None:
return True, "", 0.0
if not expected_text or not expected_text.strip():
return True, "", 0.0
[...]
if x2 - x1 < 10 or y2 - y1 < 10:
return True, "", 0.0
[...]
except Exception as e:
logger.warning("[REPLAY] _validate_text_at_position erreur (%s) — pas de blocage", e)
return True, "", 0.0
```
- Statut : actif (quand le flag global est on) mais résultat documenté comme intentionnellement permissif sur tout chemin d'erreur. Une erreur OCR = pas de blocage.
**F2.1.3 — `_pre_check_screen_state` (CLIP) bascule `match=True` sur exception**
- `agent_v0/server_v1/replay_engine.py:1374-1379`
- Citation :
```
except Exception as e:
# Ne jamais bloquer le replay en cas d'erreur du pre-check
logger.error(f"Pre-check échoué (non bloquant): {e}")
result["match"] = True # Fallback permissif
result["reason"] = f"precheck_error: {e}"
```
- Statut : actif mais permissif explicitement (commentaire `# Fallback permissif`). Toute exception interne du pre-check CLIP renvoie `match=True` et l'action passe.
**F2.1.4 — Vérification post-action (`verify_action`/`verify_with_critic`) skippée pour type/key_combo/wait et popup gérée**
- `agent_v0/server_v1/api_stream.py:3394-3399`
- Citation :
```
action_type_for_verify = (original_action or {}).get("type", "unknown")
skip_verify = action_type_for_verify in ("type", "key_combo", "wait")
# Skip aussi la vérification serveur si l'agent a déjà géré la popup
skip_verify = skip_verify or agent_handled_popup
verification = None
if report.success and screenshot_after and not skip_verify:
```
- Statut : actif. La vérification visuelle post-action ne tourne que pour les click et seulement si `agent_handled_popup` est faux.
**F2.1.5 — `_validate_match_context` consommé uniquement dans la branche template strict**
- `agent_v0/server_v1/resolve_engine.py:201, 1864`
- Le seul appel est `resolve_engine.py:1864` à l'intérieur du mode strict, pour le fallback template. Pas appelé dans `_resolve_with_precompiled_order` (V4) ni dans le mode classique.
- Statut : actif sur un seul chemin de la cascade.
### 2.2 Garde-fous court-circuités (seuils, flags, conditions)
**F2.2.1 — Drift > 0.20 ignoré quand `template_matching ≥ 0.95` ou `hybrid_text_direct ≥ 0.80`**
- `agent_v0/server_v1/resolve_engine.py:2367-2390`
- Citation :
```
if dx > _RESOLUTION_MAX_DRIFT or dy > _RESOLUTION_MAX_DRIFT:
[...]
_high_confidence_method = (
(method.startswith("template_matching") and score >= 0.95)
or (method == "hybrid_text_direct" and score >= 0.80)
)
if _high_confidence_method:
logger.info(
"[REPLAY] Drift (%.3f, %.3f) > %.2f IGNORÉ : score=%.3f "
"sur %s — résultat visuel fiable, on l'utilise",
dx, dy, _RESOLUTION_MAX_DRIFT, score, method,
)
return result
```
- Statut : actif. Exemption introduite par 35b27ae49 (template ≥ 0.95) puis élargie par 40440f1ca à hybrid_text_direct ≥ 0.80. La garde de drift est neutralisée pour deux familles de méthodes.
**F2.2.2 — Drift check inactif si fallback x/y_pct ressemblent à un placeholder 0.5/0.5 ou 0.0/0.0**
- `agent_v0/server_v1/resolve_engine.py:2359-2363`
- Citation :
```
_has_recorded_coords = (
fallback_x_pct > 0.001
and fallback_y_pct > 0.001
and not (abs(fallback_x_pct - 0.5) < 0.001 and abs(fallback_y_pct - 0.5) < 0.001)
)
if _has_recorded_coords:
```
- Statut : actif. Sans coords enregistrées exploitables, la garde drift est inerte.
**F2.2.3 — Self-healing Win+D au retry 1 désactivé (revert)**
- `agent_v0/server_v1/replay_engine.py` (commit 22c0a2ba6, branche `next_retry == 2` conservée seule)
- Citation post-revert :
```
if next_retry == 2:
# Retry 2 : injecter un wait de 2s avant l'action
wait_action = {
"action_id": f"wait_retry_{uuid.uuid4().hex[:6]}",
"type": "wait",
"duration_ms": 2000,
}
actions_to_insert.append(wait_action)
```
- Statut : retiré. Aucune injection de gesture de récupération avant le retry 1 — boucle directe sur la même action.
**F2.2.4 — Pre-check skip si heartbeat > 10s ou timeout > 500ms**
- `agent_v0/server_v1/api_stream.py:999-1001, 3100-3130`
- Citation :
```
_HEARTBEAT_MAX_AGE_SECONDS = 10.0
_PRECHECK_SIMILARITY_THRESHOLD = 0.85
[...]
if age <= _HEARTBEAT_MAX_AGE_SECONDS:
[...]
precheck_result = await asyncio.wait_for(
loop.run_in_executor(...),
timeout=0.5, # Max 500ms pour le pre-check
)
except asyncio.TimeoutError:
logger.warning(...)
precheck_result = None
```
- Statut : actif. Le pre-check CLIP est skip silencieusement si heartbeat trop ancien ou si l'embed prend > 500ms.
**F2.2.5 — VLM Quick Find : confidence < 0.3 → ignoré (résultat valide perdu sous le seuil)**
- `agent_v0/server_v1/resolve_engine.py:655-662`
- Citation :
```
if x_pct is None or y_pct is None or confidence < 0.3:
logger.info(
"VLM Quick Find : élément non trouvé ou confiance trop basse "
"(%.1fs, confidence=%.2f) pour '%s'",
```
- Statut : actif. Tout retour VLM avec confidence < 0.3 est dropé.
**F2.2.6 — Image client tronquée → remplacement silencieux par dernier heartbeat**
- `agent_v0/server_v1/api_stream.py:4422`
- Citation :
```
if img.height < 800 or img.width < 1200:
logger.warning(
"[RESOLVE_TARGET] Image client tronquée %dx%d (declared %dx%d) — "
"fallback heartbeat full screen",
```
- Statut : actif. Toute image reçue < 1200x800 est remplacée par un screenshot heartbeat (mémoire ou disque) avant cascade. Seuil élargi par 7233df2bb (était 400x200 avant).
**F2.2.7 — CLIP mismatch < 0.75 retourne resolved=False mais ne bloque qu'en mode strict avec embedding fourni**
- `agent_v0/server_v1/resolve_engine.py:1655-1691`
- Citation :
```
clip_embedding = target_spec.get("clip_embedding")
if clip_embedding:
[...]
if clip_sim < 0.75:
logger.warning(
f"CLIP MISMATCH : sim={clip_sim:.3f} < 0.75 — "
f"écran actuel trop différent de l'enregistrement"
)
return {
"resolved": False,
"method": "clip_mismatch",
```
- Statut : actif uniquement si `clip_embedding` fourni ET mode strict. Pour les workflows qui n'embarquent pas l'embedding, ce filet est inerte.
### 2.3 Flags d'environnement avec défaut permissif
**F2.3.1 — `RPA_ENABLE_TEXT_PRECHECK`, défaut `"false"`**
- `agent_v0/server_v1/api_stream.py:4519-4521`
- Citation :
```
_text_precheck_enabled = os.environ.get(
"RPA_ENABLE_TEXT_PRECHECK", "false"
).lower() in ("true", "1", "yes")
```
- Statut : off par défaut. Sans surcharge en environnement, le pré-check OCR ne s'exécute jamais.
**F2.3.2 — `RPA_AUTH_DISABLED`, défaut absent (auth obligatoire) mais permet de tout débrayer**
- `agent_v0/server_v1/api_stream.py:107-119`
- Citation :
```
_AUTH_DISABLED = os.environ.get("RPA_AUTH_DISABLED", "").lower() in (
"1", "true", "yes",
)
[...]
if _AUTH_DISABLED:
logger.warning(
"[SÉCURITÉ] RPA_AUTH_DISABLED=true — authentification Bearer DÉSACTIVÉE. ..."
)
API_TOKEN = _API_TOKEN_ENV or secrets.token_hex(32)
```
- Statut : par défaut auth obligatoire, mais flag explicite documenté pour la débrayer.
**F2.3.3 — `RPA_LOOP_DETECTOR_ENABLED`, défaut `"1"` (activé)**
- `agent_v0/server_v1/loop_detector.py:42-47, 78-79`
- Citation :
```
def _env_bool_enabled(name: str) -> bool:
val = os.environ.get(name, "1").strip().lower()
return val not in ("0", "false", "no", "off", "")
[...]
if not _env_bool_enabled("RPA_LOOP_DETECTOR_ENABLED"):
return LoopVerdict(detected=False)
```
- Statut : on par défaut, désactivable via `RPA_LOOP_DETECTOR_ENABLED=0`.
**F2.3.4 — `RPA_SAFETY_CHECKS_LLM_ENABLED`, défaut `"1"` (activé)**
- `agent_v0/server_v1/safety_checks_provider.py:42-44, 70`
- Citation :
```
if safety_level == "medical_critical" and _env_bool_enabled("RPA_SAFETY_CHECKS_LLM_ENABLED"):
```
- Statut : on par défaut, mais ne tourne que si `safety_level == "medical_critical"` dans l'action.
**F2.3.5 — `RPA_PII_BLUR_SERVER`, défaut `"true"` (activé)**
- `agent_v0/server_v1/api_stream.py:1023`
- Citation :
```
_PII_BLUR_ENABLED = os.environ.get("RPA_PII_BLUR_SERVER", "true").lower() in ("true", "1", "yes")
```
- Statut : on par défaut.
### 2.4 Étapes de cascade neutralisées
**F2.4.1 — `_resolve_by_yolo` défini, importé, jamais appelé**
- Définition : `agent_v0/server_v1/resolve_engine.py:293`
- Import : `agent_v0/server_v1/api_stream.py:4363`
- Recherche `_resolve_by_yolo(` dans le serveur entier : 0 site d'appel.
- Statut : fonction morte. La détection OmniParser/YOLO n'est plus dans la cascade exécutée.
**F2.4.2 — `_resolve_with_precompiled_order` (V4) appelé seulement si `target_spec["resolve_order"]` présent**
- `agent_v0/server_v1/resolve_engine.py:1613-1635`
- Citation :
```
resolve_order = target_spec.get("resolve_order")
if resolve_order and isinstance(resolve_order, list):
[...]
result = _resolve_with_precompiled_order(...)
if result and result.get("resolved"):
return result
[...]
logger.info(
"V4 resolve : toutes les méthodes pré-compilées ont échoué, "
"fallback cascade legacy"
)
```
- Statut : actif quand un plan V4 est compilé ; sinon inerte. Fallback cascade legacy systématique en cas d'échec.
**F2.4.3 — Étape grounding VLM directe conditionnée à `by_text_source ∈ {ocr, vlm}` ET `has_window`**
- `agent_v0/server_v1/resolve_engine.py:1696-1715`
- Citation :
```
by_text_source = target_spec.get("by_text_source", "")
has_window = bool(target_spec.get("window_capture", {}).get("rect"))
if by_text_strict and by_text_source in ("ocr", "vlm") and has_window:
grounding_result = _resolve_by_grounding(...)
```
- Statut : actif sur ces deux conditions seulement. Si `by_text_source` est vide ou autre, ou si `window_capture.rect` absent, le grounding direct est sauté.
**F2.4.4 — `_resolve_by_ocr_text` (hybrid_text_direct) reconnecté le 2026-05-06 dans la cascade strict (commit 1cbec2806)**
- `agent_v0/server_v1/resolve_engine.py:1750-1790`
- Citation du commit :
```
fix(resolve): rebrancher hybrid_text_direct dans _resolve_target_sync
[...] la fonction _resolve_by_ocr_text (resolve_engine.py:1447) existait
déjà mais [...] n'était appelée QUE depuis le runtime V4 [...]
```
- Statut : actif depuis 1cbec2806. Avant : étape OCR direct n'était pas dans la cascade strict pour les workflows non-V4.
**F2.4.5 — Template matching mode strict : seuil 0.90 (étape 2 fallback)**
- `agent_v0/server_v1/resolve_engine.py:1733, 1847-1875`
- Citation :
```
result = _resolve_by_template_matching(
[...]
confidence_threshold=0.90,
)
if result:
score = result.get("score", 0)
# Score >= 0.95 : match quasi-parfait, pas besoin de valider le contexte
if score >= 0.95:
[...]
return result
elif _validate_match_context(result, fallback_x_pct, fallback_y_pct, target_spec):
[...]
```
- Statut : actif. `_validate_match_context` skippé si score ≥ 0.95.
### 2.5 Fonctions améliorantes définies mais non appelées
**F2.5.1 — `_resolve_by_yolo` (résolution OmniParser+template, défini resolve_engine.py:293, jamais appelé)**
- Voir F2.4.1.
**F2.5.2 — `_fuzzy_match` importé dans api_stream.py mais jamais appelé**
- Définition : `agent_v0/server_v1/resolve_engine.py:2086`
- Import : `agent_v0/server_v1/api_stream.py:4372`
- Recherche `_fuzzy_match(` dans api_stream.py : 0 appel hors la ligne d'import.
- Statut : import mort. Le fuzzy match utilisé en runtime est `_text_match_fuzzy` (resolve_engine.py:2213), distinct.
**F2.5.3 — `_get_omniparser`, `_build_target_description` importés dans api_stream.py mais non appelés directement**
- `agent_v0/server_v1/api_stream.py:4362, 4365`
- Statut : imports utilisés indirectement via `_resolve_target_sync` qui les appelle en interne. Pas un finding bloquant — pas de fonction améliorante hors usage interne.
### 2.6 Marqueurs de dette (TODO/FIXME/disabled/démo) dans le serveur
**F2.6.1 — TODO `task_planner.py:400`**
- Citation : `# Boucle : TODO — lister les éléments puis itérer`
- Statut : commentaire de dette dans `task_planner.py`.
**F2.6.2 — Commentaire « 8 mai 2026 : désactivé par défaut pour la démo GHT »**
- `agent_v0/server_v1/api_stream.py:4512`
- Citation :
```
# 8 mai 2026 : désactivé par défaut pour la démo GHT. Calibrage du
# radius_px et min_token_ratio à finaliser post-démo (cf. rapport
# docs/E2E_TEST_RUN_2026-05-08.md). Le pré-check était trop strict
# sur les onglets à 2 tokens (Examens cliniques, Synthèse Urgences)
# → faux rejets → cascade locale Léa V1 → clic au pif. Réactivable
# via env RPA_ENABLE_TEXT_PRECHECK=true. Le code et les tests
# restent en place pour reprise post-démo.
```
- Statut : marqueur démo explicite.
**F2.6.3 — Mention « Fallback permissif » dans `_pre_check_screen_state`**
- `agent_v0/server_v1/replay_engine.py:1377``result["match"] = True # Fallback permissif`
**F2.6.4 — Ré-introduction explicite « non-bloquant » dans `_validate_text_at_position`**
- `agent_v0/server_v1/resolve_engine.py:2288``logger.warning("[REPLAY] _validate_text_at_position erreur (%s) — pas de blocage", e)`
**F2.6.5 — Mode autonome → pause_for_human ignorée silencieusement**
- `agent_v0/server_v1/api_stream.py:3011-3017`
- Citation :
```
# Mode autonome sans safety_checks → skip (comportement legacy)
logger.info(
"pause_for_human ignorée (mode autonome) — replay %s continue",
owning_replay["replay_id"] if owning_replay else "?"
)
queue.pop(0)
_replay_queues[session_id] = queue
continue
```
- Statut : actif. La supervision n'est utilisée que si `execution_mode != "autonomous"` ou si `safety_level`/`safety_checks` déclarés. Ce câblage `execution_mode → supervised` a été corrigé par 7233df2bb.
**F2.6.6 — Commentaire `# Fallback permissif`/`pas de blocage` cumulés**
- Présents dans 3 fonctions de validation : `_pre_check_screen_state` (replay_engine.py:1377), `_validate_text_at_position` (resolve_engine.py:2288), et politique de `_get_validation_ocr_reader` (resolve_engine.py:2196).
## 3. Commits récents qui ont désactivé des contrôles
(Sur les 20 derniers commits du dossier `agent_v0/server_v1/`)
- `56e869c46` (8 mai) — `fix(replay): bug TypeError log + flag pré-check OCR off par défaut (démo GHT)` : ajoute `RPA_ENABLE_TEXT_PRECHECK` (default `"false"`) qui débraye intégralement l'appel à `_validate_text_at_position`.
- `40440f1ca` (7 mai) — `fix(replay): cure régression b584bbabc — fallback recorded_coords aveugle` : restaure `resolved=False` sur drift trop grand (annule le fallback aveugle introduit par b584bbabc) ; étend l'exemption drift à `hybrid_text_direct ≥ 0.80` (resolve_engine.py:2380-2390).
- `7233df2bb` (7 mai) — `fix(replay): câblage execution_mode supervised + seuil large fallback heartbeat` : élargit le seuil de détection image tronquée à `< 1200×800` (était `< 400×200`) → fallback heartbeat plus fréquent ; force `execution_mode='supervised'` par défaut quand non précisé.
- `f62fda575` (7 mai) — `fix(stream): /resolve_target — fallback heartbeat full si image client tronquée` : introduit le remplacement silencieux de l'image client par un heartbeat disque/mémoire si tronquée.
- `22c0a2ba6` (6 mai) — `revert: désactiver self-healing Win+D auto (cercle vicieux)` : retire l'injection automatique de Win+D au retry 1 sur `verification_failed`/`no_screen_change`.
- `c969f93a2` (6 mai) — `fix(replay): self-healing Win+D auto au retry 1` : commit de la fonctionnalité, reverté par 22c0a2ba6.
- `1cbec2806` (6 mai) — `fix(resolve): rebrancher hybrid_text_direct dans _resolve_target_sync` : reconnecte `_resolve_by_ocr_text` dans la cascade strict, qui auparavant n'était appelé que par le chemin V4 pré-compilé (non actif dans la majorité des workflows).
- `b584bbabc` (1 mai) — `fix(stream): robustesse proxy VWB→streaming + ciblage textuel pour démo UHCD` : avait remplacé le rejet strict du drift par un `fallback_recorded_coords` (resolved=True). Reverté factuellement par 40440f1ca le 7 mai.
- `35b27ae49` (2 mai) — `fix(stream+vwb): chaîne replay robuste — auth, anchor type_text, lock async, drift, prompt LLM` : introduit l'exemption drift pour `template_matching ≥ 0.95` (point d'entrée du finding F2.2.1).
## 4. Pistes prioritaires (P0 « pré-check OCR rejette systématiquement »)
Findings directement reliés au P0 (motif factuel : « pré-check OCR rejette systématiquement, contrôles débranchés suite à checkout antérieur ») :
1. **F2.3.1 + F2.6.2**`agent_v0/server_v1/api_stream.py:4519-4521` : flag `RPA_ENABLE_TEXT_PRECHECK` à défaut `"false"`. C'est le geste d'extinction explicite mentionné dans le commit `56e869c46` ; le pré-check OCR ne peut rejeter quoi que ce soit en l'état tant que la variable d'environnement n'est pas positionnée à `"true"` côté service rpa-streaming. Confirme directement la piste signalée.
2. **F2.1.1**`agent_v0/server_v1/resolve_engine.py:2239-2289` (`_validate_text_at_position`) + `agent_v0/server_v1/api_stream.py:4525-4533` (point d'appel) : la fonction est conservée en place mais son point d'appel est conditionné par F2.3.1. Tant que le flag est off, la fonction est définie mais non consommée — état exact « contrôle débranché ».
3. **F2.6.5**`agent_v0/server_v1/api_stream.py:3011-3017` : `pause_for_human ignorée (mode autonome) — replay continue`. Si `execution_mode` n'est pas propagé jusqu'au replay (cf. commit 7233df2bb qui corrige ce câblage), la pause supervisée censée intercepter un rejet OCR est skipée et la file d'actions continue. Combiné à F2.3.1, donne le motif observé : « rejets pré-check silencieux → cascade locale Léa → clic au pif » (extrait commit message `56e869c46`).
## 5. Findings côté CLIENT (`agent_v0/agent_v1/core/executor.py`)
Fichier audité : `agent_v0/agent_v1/core/executor.py` (2893 lignes). Aucune occurrence de `RPA_ENABLE_*`, `RPA_DISABLE_*`, `if False:` ou `if 0:` dans ce fichier.
### 5.1 Validations désactivées ou non consommées
Aucun finding (le client ne contient aucun bloc de validation neutralisé : la pré-vérif titre fenêtre, `_check_and_pause_on_system_dialog` et la cascade Observer→Policy sont toutes appelées en flux nominal).
### 5.2 Garde-fous court-circuités (seuils, flags, conditions)
**F5.2.1 — `_check_and_pause_on_system_dialog` fail-closed sur exception (durcissement, pas une désactivation)**
- `agent_v0/agent_v1/core/executor.py:2001-2043`
- Citation :
```
except Exception as e:
# Fix P0-D : fail-closed (principe "faux positif tolérable,
# faux négatif catastrophique"). [...]
self._system_dialog_pause = {
"category": "unknown_check_failed",
[...]
}
[...]
return True
```
- Statut : actif. C'est un fail-closed (toute erreur de détection → pause supervisée), pas un fallback permissif. Listé pour traçabilité.
**F5.2.2 — Seuil template-matching `_find_text_on_screen` durci à 0.75**
- `agent_v0/agent_v1/core/executor.py:2367`
- Citation :
```
threshold = 0.75 # Démo GHT 8 mai — éviter faux positifs (placeholders italiques, tabs voisins). En dessous, mieux vaut tomber en mode apprentissage humain qu'un clic au pif.
```
- Statut : actif. Seuil élevé en démo GHT (commit `7847a0e82`, 7 mai) — under threshold = pas de match retourné. Pas une désactivation, un durcissement.
**F5.2.3 — Skip conditional_on_window : action acquittée success=True quand le dialogue n'est pas apparu**
- `agent_v0/agent_v1/core/executor.py:567-592`
- Citation :
```
if not match:
[...]
print(
f" [SKIP] Dialogue '{cond_window}' absent → action skippée"
)
result["success"] = True
result["warning"] = "conditional_skipped"
return result
```
- Statut : actif. Comportement attendu (skip explicite avec `warning=conditional_skipped`) mais l'action est rapportée `success=True` au serveur — pas une erreur côté replay engine.
**F5.2.4 — `wrong_window_skipped` : action skippée silencieusement après timeout apprentissage**
- `agent_v0/agent_v1/core/executor.py:754-764`
- Citation :
```
else:
# Timeout ou pas d'action → skipper cette action
# L'état est peut-être déjà correct (ex: Ctrl+S
# a sauvé sans dialogue → action de dialogue inutile)
result["success"] = True
result["warning"] = "wrong_window_skipped"
logger.info(
f"[LEA] Wrong window sans correction → skip "
f"(l'état est peut-être déjà atteint)"
)
```
- Statut : actif. Si l'humain ne corrige pas dans les 120s (`_capture_human_correction(timeout_s=120)`), l'action est marquée success=True. Mêmes lignes pour `policy_skip` (`executor.py:993-996`).
**F5.2.5 — Polling timeout REPLAY étendu à 30s pour démo GHT**
- `agent_v0/agent_v1/core/executor.py:1786-1794`
- Citation :
```
# 8 mai 2026 — démo GHT : 5s → 30s. Le serveur peut exécuter
# extract_text (5-7s) PUIS dispatcher l'action suivante dans
# la même réponse HTTP. À 5s, le client coupait avant la
# réponse [...]
timeout=30,
```
- Statut : actif. Marqueur démo, modification non commitée à HEAD (status `Not Committed Yet 2026-05-08`). Pas un contrôle débranché, contournement coûts d'IO.
### 5.3 Flags d'environnement avec défaut permissif
**F5.3.1 — `RPA_OLLAMA_HOST`, défaut `"localhost"`**
- `agent_v0/agent_v1/core/executor.py:2224` (et autres sites)
- Citation :
```
ollama_host = os.environ.get("RPA_OLLAMA_HOST", "localhost")
```
- Statut : configuration uniquement, pas un garde-fou.
Aucun autre flag environnemental dans le client : pas de `RPA_ENABLE_*`/`RPA_DISABLE_*` côté agent V1.
### 5.4 Étapes de cascade neutralisées
**F5.4.1 — Self-healing désactivé côté client (revert miroir du serveur)**
- Le client n'embarque pas de logique self-healing autonome — l'injection Win+D était purement serveur (cf. F2.2.3, commit `22c0a2ba6`). Côté client, la branche d'apprentissage humain (`_capture_human_correction`) reste l'unique recours en cas d'échec retry.
- Statut : pas un finding spécifique au client.
### 5.5 Fonctions améliorantes définies mais non appelées
**F5.5.1 — `_handle_possible_popup` (legacy clavier Enter/Escape/Tab+Enter) toujours définie**
- `agent_v0/agent_v1/core/executor.py:2430-2472`
- Citation :
```
def _handle_possible_popup(self) -> bool:
"""Tenter de gerer une popup imprevue.
[...]
Strategie simple (non bloquante, max ~3s) :
1. Essayer Enter (valide le bouton par defaut de la popup)
2. Si ca ne marche pas, essayer Escape (ferme la popup)
3. Si ca ne marche pas, essayer Tab + Enter [...]
```
- Recherche `_handle_possible_popup(` dans le client : 1 site (la définition). 0 site d'appel hors la définition.
- Statut : fonction morte côté client. Le chemin actif est `_handle_popup_vlm` (executor.py:2102) + Observer/Policy. La version "clavier seul" est conservée mais non câblée.
### 5.6 Marqueurs de dette (TODO/FIXME/disabled/démo) dans le client
**F5.6.1 — Marqueur explicite « démo GHT » multiple**
- `agent_v0/agent_v1/core/executor.py:1786, 1813, 1835, 2367` : 4 commentaires « 8 mai 2026 — démo GHT » documentant des changements ciblés (timeout polling, plan B pause UX, threshold FIND-TEXT 0.75).
**F5.6.2 — Plusieurs `except Exception: pass` silencieux**
- `agent_v0/agent_v1/core/executor.py:455-456, 722-723, 958-959, 1017-1018, 1127-1128, 1244-1245, 1286-1287, 2619-2620`
- Statut : 8 sites, tous sur des chemins best-effort (notification, snapshot UIA, log d'apprentissage). Aucun ne masque une décision de sécurité.
## 6. Autres fichiers .py modifiés < 14 jours (hors serveur/client déjà audités)
Périmètre : 23 fichiers Python modifiés depuis 2026-04-24 hors `tests/`, `docs/`, `visual_workflow_builder/`, `web_dashboard/`, `agent_chat/`, `_archive/`, `tools/` et le dossier `agent_v0/server_v1/` (déjà audité §1-§4) et `agent_v0/agent_v1/core/executor.py` (déjà audité §5).
### 6.1 `core/execution/observe_reason_act.py` (2008 lignes)
**F6.1.1 — Bloc `if False:` désactivant le pre-check VLM par-clic**
- `core/execution/observe_reason_act.py:1704-1713`
- Citation :
```
# --- Pas de pre-check VLM (le pipeline FAST→SMART→THINK a déjà validé) ---
if False:
try:
pre_check = self._verify_pre_click(x, y, target_text, target_desc)
if not pre_check:
print(f"⛔ [ORA/pre-check] L'élément à ({x}, {y}) ne correspond PAS à '{target_text}' — abandon du clic")
return False
except Exception as e:
print(f"⚠️ [ORA/pre-check] Erreur vérification: {e}")
```
- Statut : désactivation explicite par `if False:`. Le commentaire justifie : « le pipeline FAST→SMART→THINK a déjà validé ».
**F6.1.2 — `_verify_pre_click` : `return True` permissif sur erreur HTTP/exception**
- `core/execution/observe_reason_act.py:1917, 1921`
- Citation :
```
return True # En cas d'erreur HTTP, on laisse passer
[...]
return True # En cas d'erreur, on laisse passer
```
- Statut : fonction conservée mais devenue inerte (cf. F6.1.1). Si réactivée, retourne `True` (passe le clic) sur toute erreur Ollama/réseau.
**F6.1.3 — `_act_type` : texte vide → `return True`**
- `core/execution/observe_reason_act.py:1740-1742`
- Citation :
```
if not decision.value:
logger.warning("🎯 [ORA/type] Pas de texte à saisir")
return True # Vide = rien à faire, pas un échec
```
- Statut : actif. Comportement documenté.
**F6.1.4 — Cascade post-shortcut : timeout retourne `True` après ≥1 dialog géré**
- `core/execution/observe_reason_act.py:1547-1550`
- Citation :
```
if _elapsed() >= total_timeout:
print(f"⏳ [ORA/post-shortcut] Timeout cascade ({total_timeout:.0f}s, "
f"{dialogs_handled} dialog(s) géré(s))")
return True # au moins un dialog traité → considéré OK
```
- Statut : actif. Politique permissive sur timeout cascade dialogues.
**F6.1.5 — Flag `RPA_USE_FAST_PIPELINE`, défaut `"1"` (activé)**
- `core/execution/observe_reason_act.py:1634`
- Citation :
```
_use_fast = os.environ.get('RPA_USE_FAST_PIPELINE', '1') == '1'
```
- Statut : on par défaut. Désactivable via env.
### 6.2 `core/grounding/fast_pipeline.py` (216 lignes)
**F6.2.1 — Expression mort-née `if False else screenshot_pil` dans appel arbiter**
- `core/grounding/fast_pipeline.py:163`
- Citation :
```
screenshot_pil=screenshot_pil or snapshot.elements[0] if False else screenshot_pil,
```
- Statut : à cause du `if False`, l'expression est équivalente à `screenshot_pil=screenshot_pil`. La branche `snapshot.elements[0]` n'est jamais évaluée. Probable reliquat d'expérimentation.
### 6.3 `core/grounding/title_verifier.py` (174 lignes)
**F6.3.1 — `has_title_changed` retourne `True` si un seul titre est vide**
- `core/grounding/title_verifier.py:73-74`
- Citation :
```
if not title_before or not title_after:
return True # Un des deux est vide = changement
```
- Statut : actif. Politique documentée — `not bloquante` (échec lecture titre = signal de changement).
### 6.4 `core/grounding/ui_tars_grounder.py` (288 lignes)
**F6.4.1 — `available` toujours `True` sans vérifier le worker**
- `core/grounding/ui_tars_grounder.py:135-137`
- Citation :
```
@property
def available(self) -> bool:
return True # Toujours disponible — le script se lance à la demande
```
- Statut : actif. Pas de probe socket — la disponibilité est assumée et l'erreur réelle remonte au moment de `ground()`.
### 6.5 Fichiers sans finding
Audit pattern par pattern (`if False`, `return True/False` suspects, `RPA_ENABLE_/RPA_DISABLE_`, `# disabled`, `# bypass`, `# TODO re-enable`, marqueurs démo, blocs `try` swallow exception sur fonction de validation) :
| Fichier | Findings |
|---|---|
| `agent_v0/agent_v1/main.py` | aucun finding |
| `agent_v0/agent_v1/network/feedback_bus.py` | aucun finding |
| `agent_v0/agent_v1/ui/chat_window.py` | aucun finding (le marqueur `# démo GHT` ligne 846 documente uniquement un comportement UX) |
| `agent_v0/agent_v1/ui/notifications.py` | aucun finding (idem ligne 143) |
| `agent_v0/agent_v1/ui/paused_toast.py` | aucun finding |
| `agent_v0/agent_v1/vision/capturer.py` | aucun finding |
| `core/execution/input_handler.py` | aucun finding |
| `core/grounding/dialog_handler.py` | aucun finding |
| `core/grounding/element_signature.py` | aucun finding |
| `core/grounding/fast_detector.py` | aucun finding |
| `core/grounding/infigui_worker.py` | aucun finding |
| `core/grounding/pipeline.py` | aucun finding |
| `core/grounding/server.py` | aucun finding |
| `core/grounding/shadow_learning_hook.py` | aucun finding |
| `core/grounding/smart_matcher.py` | aucun finding |
| `core/grounding/template_matcher.py` | aucun finding |
| `core/grounding/think_arbiter.py` | aucun finding (`available = True` ligne 38 même pattern que F6.4.1, mais arbiter délègue au grounder qui détient F6.4.1) |
| `core/knowledge/ui_patterns.py` | aucun finding |
| `core/llm/ocr_extractor.py` | aucun finding |
| `core/llm/t2a_decision.py` | aucun finding |
## 7. Datation git des findings (toutes sections confondues)
| Finding | Fichier:ligne | Commit | Date | Message commit (raccourci) |
|---|---|---|---|---|
| F2.1.1 | api_stream.py:4519-4533 | `56e869c46` (gate) + `40440f1ca` (corps) | 2026-05-08 / 2026-05-07 | flag pré-check OCR off / cure régression b584bbabc |
| F2.1.2 | resolve_engine.py:2253-2289 | `40440f1ca` | 2026-05-07 | cure régression b584bbabc |
| F2.1.3 | replay_engine.py:1374-1379 | `4509038bf` | 2026-04-09 | refactor api_stream.py 6400→3350 |
| F2.1.4 | api_stream.py:3394-3399 | `d5deac302` (corps) + `ae65be255` (3398) | 2026-03-26 / 2026-03-18 | feat replay visuel VLM-first |
| F2.1.5 | resolve_engine.py:201, 1864 | `4509038bf` | 2026-04-09 | refactor api_stream.py |
| F2.2.1 | resolve_engine.py:2367-2390 | `35b27ae49` (intro) → `40440f1ca` (élargi) | 2026-05-02 / 2026-05-07 | chaîne replay robuste / cure régression |
| F2.2.2 | resolve_engine.py:2359-2363 | `a21f1ea9f` | 2026-04-11 | garde qualité résolution |
| F2.2.3 | replay_engine.py (revert) | `c969f93a2` (intro) → `22c0a2ba6` (revert) | 2026-05-06 | self-healing Win+D / désactivation cercle vicieux |
| F2.2.4 | api_stream.py:999-1001 | `d5deac302` | 2026-03-26 | feat replay visuel VLM-first |
| F2.2.5 | resolve_engine.py:655-662 | `4509038bf` | 2026-04-09 | refactor api_stream.py |
| F2.2.6 | api_stream.py:4422 | `7233df2bb` | 2026-05-07 | câblage execution_mode + seuil heartbeat élargi |
| F2.2.7 | resolve_engine.py:1655-1691 | `4509038bf` | 2026-04-09 | refactor api_stream.py |
| F2.3.1 | api_stream.py:4519-4521 | `56e869c46` | 2026-05-08 | flag pré-check OCR off par défaut (démo GHT) |
| F2.3.2 | api_stream.py:107-119 | `93ef93e56` | 2026-04-14 | API streaming fail-closed |
| F2.3.3 | loop_detector.py:42-47 | `2a51a844b` | 2026-05-05 | LoopDetector composite |
| F2.3.4 | safety_checks_provider.py:42-44 | `7c6945171` | 2026-05-05 | SafetyChecksProvider hybride |
| F2.3.5 | api_stream.py:1023 | `93ef93e56` | 2026-04-14 | API streaming fail-closed |
| F2.4.1 | resolve_engine.py:293 (def) | `4509038bf` | 2026-04-09 | refactor api_stream.py |
| F2.4.2 | resolve_engine.py:1613-1635 | `f6ad5ff2b` | 2026-04-10 | runtime V4 honore resolve_order |
| F2.4.3 | resolve_engine.py:1696-1715 | `4509038bf` | 2026-04-09 | refactor api_stream.py |
| F2.4.4 | resolve_engine.py:1750-1790 | `1cbec2806` | 2026-05-06 | rebrancher hybrid_text_direct |
| F2.4.5 | resolve_engine.py:1733, 1847-1875 | `4509038bf` | 2026-04-09 | refactor api_stream.py |
| F2.5.1 | resolve_engine.py:293 | `4509038bf` | 2026-04-09 | refactor api_stream.py |
| F2.5.2 | api_stream.py:4372 (import mort) | `4509038bf` | 2026-04-09 | refactor api_stream.py |
| F2.5.3 | api_stream.py:4362, 4365 | `4509038bf` | 2026-04-09 | refactor api_stream.py |
| F2.6.1 | task_planner.py:400 | `99041f011` | 2026-04-09 | pipeline complet MACRO/MÉSO/MICRO |
| F2.6.2 | api_stream.py:4512 | `56e869c46` | 2026-05-08 | flag pré-check OCR off par défaut |
| F2.6.3 | replay_engine.py:1377 | `4509038bf` | 2026-04-09 | refactor api_stream.py |
| F2.6.4 | resolve_engine.py:2288 | `40440f1ca` | 2026-05-07 | cure régression b584bbabc |
| F2.6.5 | api_stream.py:3011-3017 | `964856ab3` (intro) → `35b27ae49` / `65da55731` (raffinements) | 2026-04-29 / 2026-05-02 / 2026-05-05 | extract_text serveur / chaîne replay robuste / SafetyChecksProvider |
| F2.6.6 | replay_engine.py:1377 + resolve_engine.py:2196, 2288 | `4509038bf` + `40440f1ca` | 2026-04-09 / 2026-05-07 | refactor / cure régression |
| F5.2.1 | executor.py:2001-2043 | (déjà fail-closed antérieur) | — | durcissement, pas désactivation |
| F5.2.2 | executor.py:2367 | `7847a0e82` | 2026-05-07 | toast paused supervisée + threshold FIND-TEXT 0.75 |
| F5.2.3 | executor.py:567-592 | (antérieur) | — | (chemin conditionnel intégré) |
| F5.2.4 | executor.py:754-764 | (antérieur) | — | mode apprentissage humain |
| F5.2.5 | executor.py:1786-1794 | non commité (workdir) | 2026-05-08 | démo GHT (uncommitted change) |
| F5.5.1 | executor.py:2430-2472 | (antérieur) | — | legacy popup handler |
| F5.6.1 | executor.py:1786, 1813, 1835, 2367 | `7847a0e82` + workdir | 2026-05-07 / 2026-05-08 | démo GHT |
| F6.1.1 | observe_reason_act.py:1705 | `e2046837c` | 2026-04-25 | Phase 5 — pipeline FAST→SMART→THINK dans ORA |
| F6.1.2 | observe_reason_act.py:1917, 1921 | `8903f3543` | 2026-04-22 | feat ORA — vérification pré-action VLM |
| F6.1.3 | observe_reason_act.py:1742 | `0c5fffe95` | 2026-04-22 | boucle ORA observe→raisonne→agit |
| F6.1.4 | observe_reason_act.py:1550 | `487bcb861` | 2026-04-26 | cascade post-raccourci DialogHandler/OCR |
| F6.1.5 | observe_reason_act.py:1634 | `e2046837c` | 2026-04-25 | Phase 5 — FAST→SMART→THINK dans ORA |
| F6.2.1 | fast_pipeline.py:163 | `b30d4b665` | 2026-04-25 | Phase 4 — Pipeline orchestré FAST→SMART→THINK |
| F6.3.1 | title_verifier.py:73-74 | `343d6fbe9` | 2026-04-26 | EasyOCR remplace docTR (FastDetector + TitleVerifier) |
| F6.4.1 | ui_tars_grounder.py:137 | `487bcb861` | 2026-04-26 | cascade post-raccourci DialogHandler/OCR |
## 8. Code original avant désactivation (quand récupérable)
### F2.1.1 / F2.3.1 / F2.6.2 — Pré-check OCR (`api_stream.py:4519-4533`)
**Avant** (commit `40440f1ca`, 2026-05-07) — pré-check appelé inconditionnellement après résolution :
```python
if result and result.get("resolved"):
_by_text = (request.target_spec.get("by_text") or "").strip()
if _by_text:
from agent_v0.server_v1.resolve_engine import _validate_text_at_position
_is_valid, _observed, _ocr_ms = _validate_text_at_position(
tmp_path,
float(result.get("x_pct", 0) or 0),
float(result.get("y_pct", 0) or 0),
_by_text,
effective_w,
effective_h,
)
```
**Après** (HEAD `56e869c46`, 2026-05-08) — pré-check gardé par flag off-by-default :
```python
_text_precheck_enabled = os.environ.get(
"RPA_ENABLE_TEXT_PRECHECK", "false"
).lower() in ("true", "1", "yes")
if _text_precheck_enabled and result and result.get("resolved"):
_by_text = (request.target_spec.get("by_text") or "").strip()
if _by_text:
from agent_v0.server_v1.resolve_engine import _validate_text_at_position
_is_valid, _observed, _ocr_ms = _validate_text_at_position(...)
```
### F2.2.3 — Self-healing Win+D au retry 1 (revert)
**Avant** (commit `c969f93a2`, 2026-05-06) — code introduit (non récupéré ici via `git show` car branche restaurée par revert immédiat).
**Après** (HEAD via `22c0a2ba6`, 2026-05-06) — branche `next_retry == 1` retirée, seule `next_retry == 2` (wait 2s) conservée :
```python
if next_retry == 2:
wait_action = {
"action_id": f"wait_retry_{uuid.uuid4().hex[:6]}",
"type": "wait",
"duration_ms": 2000,
}
actions_to_insert.append(wait_action)
```
### F2.2.6 — Seuil image tronquée
**Avant** (`f62fda575`, 2026-05-07) — seuil minimal `< 400×200` (placeholders triviaux) :
```python
if img.height < 200 or img.width < 400:
[...]
```
**Après** (`7233df2bb`, 2026-05-07) — seuil élargi à `< 1200×800` :
```python
if img.height < 800 or img.width < 1200:
[...]
```
### F2.4.4 — Reconnect `hybrid_text_direct` dans cascade strict
**Avant** (avant `1cbec2806`, ≤ 2026-05-05) — `_resolve_by_ocr_text` n'était appelée QUE depuis le runtime V4 pré-compilé (extrait commit message). Code original non re-extrait ligne par ligne (commit message factuel suffit).
**Après** (HEAD via `1cbec2806`, 2026-05-06) — appel ajouté dans `_resolve_target_sync` (cascade strict, resolve_engine.py:1750-1790).
### F6.1.1 — Désactivation pre-check VLM par-clic (`observe_reason_act.py:1704-1713`)
**Avant** (commit `8903f3543`, 2026-04-22) :
```python
# --- Vérification pré-action (skip si UI-TARS a déjà validé visuellement) ---
if target_text and method_used not in ('template', 'ui_tars') and MSS_AVAILABLE and PIL_AVAILABLE:
try:
pre_check = self._verify_pre_click(x, y, target_text, target_desc)
if not pre_check:
print(f"⛔ [ORA/pre-check] L'élément à ({x}, {y}) ne correspond PAS à '{target_text}' — abandon du clic")
return False
except Exception as e:
print(f"⚠️ [ORA/pre-check] Erreur vérification: {e}")
```
**Après** (HEAD via `e2046837c`, 2026-04-25) :
```python
# --- Pas de pre-check VLM (le pipeline FAST→SMART→THINK a déjà validé) ---
if False:
try:
pre_check = self._verify_pre_click(x, y, target_text, target_desc)
if not pre_check:
print(f"⛔ [ORA/pre-check] L'élément à ({x}, {y}) ne correspond PAS à '{target_text}' — abandon du clic")
return False
except Exception as e:
print(f"⚠️ [ORA/pre-check] Erreur vérification: {e}")
```
### Findings sans version « avant » récupérable (≤ 14 jours)
Les findings suivants n'ont pas de version « activée » dans la fenêtre 14 jours (la désactivation est antérieure ou native dès l'introduction du code) :
- F2.1.3, F2.1.4, F2.1.5 (commits `4509038bf` du 2026-04-09 et `d5deac302` du 2026-03-26)
- F2.2.2, F2.2.4, F2.2.5, F2.2.7 (commits 2026-04-09 à 2026-04-11)
- F2.3.2, F2.3.5 (commit `93ef93e56` du 2026-04-14)
- F2.4.1, F2.4.3, F2.4.5, F2.5.1, F2.5.2, F2.5.3 (commit `4509038bf`)
- F2.6.1 (commit `99041f011` du 2026-04-09)
- F6.1.2, F6.1.3, F6.1.4 (commits 2026-04-22 à 2026-04-26 — `return True` permissif natif)
- F6.2.1, F6.3.1, F6.4.1 (commits 2026-04-25 à 2026-04-26 — états natifs)
- F5.2.1, F5.2.3, F5.2.4, F5.5.1 (chemins clients antérieurs, non touchés < 14 jours)
Pour ces findings, le motif factuel est « introduction native du contrôle déjà à l'état permissif/désactivé », pas une bascule postérieure.

View File

@@ -0,0 +1,277 @@
# Carte fonctionnelle RPA Vision V3 — 2026-05-08
Branche : `feature/qw-suite-mai` | HEAD : `731b5bcae`
Vue produit (pas code). Inventaire des fonctionnalités telles qu'elles existent réellement dans le repo à cette date.
---
## 1. Modes opérationnels de Léa (agent_v1)
Le client Léa V1 (`agent_v0/agent_v1/`) n'expose **pas** d'enum `MODE_*` discret. Son comportement runtime est piloté par trois booléens cumulables dans `AgentState` (`ui/shared_state.py`) :
| Mode | Module(s) concerné(s) | Activation | Statut |
|---|---|---|---|
| Capture / enregistrement | `ui/shared_state.py:30` (`_recording`), `core/captor.py`, `vision/capturer.py` | Bouton "Démarrer" dans systray (`smart_tray.py:377`) ou ChatWindow → `state.start_recording(name)` | actif |
| Replay (polling serveur) | `main.py:130` (`_replay_poll_loop`), `core/executor.py:510-1900` | Boucle daemon permanente, lancée à l'init de `AgentV1` indépendamment de toute session — poll `GET /replay/next` toutes les 1 s sur `agent_{user_id}` | actif |
| Heartbeat permanent (background) | `main.py:131` (`_background_heartbeat_loop`) | Daemon permanent, screenshot toutes les 5 s vers `POST /traces/stream/image` (session `bg_{machine_id}`) | actif |
| Heartbeat session | `main.py:434` (`_heartbeat_loop`) | Démarre seulement quand `session_id` actif (pendant un enregistrement) | actif |
| Watchdog fichier `command.json` | `main.py:247` (`_command_watchdog_loop`) | Poll fichier `C:\rpa_vision\command.json` toutes les 1 s, exécute `execute_normalized_order` | actif (legacy GHOST replay) |
| Capture à la demande HTTP | `ui/capture_server.py` | Mini-serveur HTTP local port 5006 lancé au boot | actif |
| Auto-stop session | `main.py:160` (`_auto_stop_loop`) | Notifie 10 min avant et stoppe à `MAX_SESSION_DURATION_S` | actif |
**Modes "shadow / copilot / assisté / autonomous"** : ils n'existent **pas** côté client Léa V1. Côté serveur, `execution_mode` est un paramètre de replay (`"autonomous"` par défaut, voir `api_stream.py:2969`, `replay_engine.py:1520`). Les valeurs détectées : `"autonomous"`, `"verified"`, `"supervised"` (déduit du test `_exec_mode != "autonomous"` à `api_stream.py:2974`). Le frontend VWB définit en plus `'basic' | 'intelligent' | 'debug' | 'verified'` (`types.ts:15`) — **ce sont des modes VWB, pas des modes Léa**. [À VÉRIFIER PAR DOM]
Endpoints `/api/v1/shadow/*` (start/stop/feedback/build/understanding) existent côté serveur (`api_stream.py:1661-1820`) mais aucun n'est appelé depuis le client Léa V1 (grep dans `agent_v0/agent_v1/` : zéro hit). [À VÉRIFIER PAR DOM]
---
## 2. Capacités du serveur (rpa-streaming + dépendances)
54 endpoints exposés par `agent_v0/server_v1/api_stream.py`.
### 2.1 Streaming session / heartbeat
- `POST /api/v1/traces/stream/register` — Enregistrer une session (session_id + machine_id)
- `POST /api/v1/traces/stream/event` — Pousser un événement clavier/souris/fenêtre
- `POST /api/v1/traces/stream/image` — Pousser un screenshot (heartbeat ou shot d'action)
- `POST /api/v1/traces/stream/finalize` — Clore une session
- `GET /api/v1/traces/stream/processing/status` — État de la file de traitement
- `POST /api/v1/traces/stream/processing/requeue` — Re-traiter une session déjà finalisée
- `GET /api/v1/traces/stream/stats` — Statistiques globales du serveur
- `GET /api/v1/traces/stream/machines` — Liste machines enrôlées
- `GET /api/v1/traces/stream/sessions` — Liste sessions (filtrable par machine_id)
### 2.2 Replay (next/report/resolve_target/pause)
- `POST /api/v1/traces/stream/replay` — Lancer un replay depuis un workflow_id
- `POST /api/v1/traces/stream/replay/raw` — Lancer un replay depuis une liste d'actions brutes
- `POST /api/v1/traces/stream/replay-session` — Re-rejouer une session enregistrée
- `POST /api/v1/traces/stream/replay/single` — Enqueuer une action unique
- `POST /api/v1/traces/stream/replay/plan` — Lancer depuis un ExecutionPlan (V4)
- `POST /api/v1/traces/stream/workflow/compile` — Compiler session → WorkflowIR + ExecutionPlan
- `GET /api/v1/traces/stream/replay/next` — Action suivante à exécuter (pollée par Léa)
- `POST /api/v1/traces/stream/replay/result` — Rapport d'exécution d'une action
- `POST /api/v1/traces/stream/replay/error_callback` — Callback erreur configurable
- `GET /api/v1/traces/stream/replay/{replay_id}` — État d'un replay
- `GET /api/v1/traces/stream/replays` — Liste des replays
- `POST /api/v1/traces/stream/replay/{replay_id}/resume` — Reprendre après pause supervisée
- `POST /api/v1/traces/stream/replay/{replay_id}/cancel` — Annuler un replay
- `POST /api/v1/traces/stream/replay/resolve_target` — Résoudre la position d'une ancre (cascade vLLM/Ollama)
- `POST /api/v1/traces/stream/replay/pre_analyze` — Pré-analyse de l'écran avant action
### 2.3 Extraction (text / table / décision T2A)
Pas d'endpoint HTTP dédié — ces actions sont enqueuées côté serveur via le replay et traitées sans round-trip Léa par `replay_engine.py:_handle_extract_text_action / _handle_extract_table_action / _handle_t2a_decision_action` (modules `core/llm/ocr_extractor.py` et `core/llm/t2a_decision.py`).
### 2.4 Federation / learning packs
- `GET /api/v1/traces/stream/learning-pack/export` — Export anonymisé (par client_id)
- `POST /api/v1/traces/stream/learning-pack/import` — Import + merge dans FAISS global
### 2.5 Health / monitoring
- `GET /health` — Healthcheck simple
- `GET /api/v1/traces/stream/workflows` — Liste workflows visibles
- `POST /api/v1/traces/stream/reload-workflows` — Rechargement à chaud
- `GET /api/v1/traces/stream/workflow/{workflow_id}` — Détail workflow
- `GET /api/v1/traces/stream/session/{session_id}` — Détail session
- `GET /api/v1/audit/history` — Historique audit (RGPD/IA Act)
- `GET /api/v1/audit/summary` — Résumé audit
- `GET /api/v1/audit/export` — Export audit
### 2.6 Autres
- `POST /api/v1/shadow/start` — Démarrer un observateur shadow (existe, voir §1)
- `POST /api/v1/shadow/stop` — Arrêter
- `POST /api/v1/shadow/feedback` — Feedback humain sur une étape observée
- `GET /api/v1/shadow/{session_id}/understanding` — Lire la compréhension construite
- `POST /api/v1/shadow/build` — Compiler en workflow
- `POST /api/v1/task` — Tâche planifiée (TaskPlanner)
- `GET /api/v1/task/capabilities` — Capacités déclarées (action types)
- `POST /api/v1/chat/session` — Créer une session de chat serveur
- `POST /api/v1/chat/{session_id}/message` — Envoyer message
- `GET /api/v1/chat/{session_id}/history` — Historique
- `POST /api/v1/chat/{session_id}/confirm` — Confirmer un plan
- `GET /api/v1/chat/sessions` — Liste sessions chat
- `POST /api/v1/agents/enroll` — Enrôler un nouvel agent (nouvelle machine)
- `POST /api/v1/agents/uninstall` — Désenrôler
- `GET /api/v1/agents/fleet` — État de la flotte
---
## 3. Stack VLM / grounding active
Synthèse de `docs/HISTORIQUE_VLM_IMPLEMENTATIONS_2026-05-08.md` (§1, §6).
| Modèle | Backend | Module appelant | Statut |
|---|---|---|---|
| `InfiX-ai/InfiGUI-G1-3B` 4-bit NF4 | Transformers in-process (Flask) | `core/grounding/server.py` (port 8200) | câblé mais inactif (pas dans la cascade actuelle) |
| `InfiX-ai/InfiGUI-G1-3B` 4-bit NF4 | Transformers subprocess one-shot | `core/grounding/infigui_worker.py` | câblé mais inactif (utilisé en fallback de la socket) |
| `InfiX-ai/InfiGUI-G1-3B` 4-bit NF4 | Transformers daemon Unix socket `/run/rpa/grounding.sock` | `core/grounding/infigui_server.py` (service systemd `rpa-grounding.service`) | service présent — `rpa-grounding.service.parked` détecté dans `/etc/systemd/system/` [À VÉRIFIER PAR DOM] |
| `Qwen/Qwen2.5-VL-7B-Instruct-AWQ` | vLLM HTTP OpenAI-compat (port `VLLM_PORT=8100`) | `agent_v0/server_v1/resolve_engine.py:785-816` (`_resolve_by_grounding`) | utilisé en prod — essai 1 (fallback Ollama) [À VÉRIFIER PAR DOM si vLLM tourne] |
| `qwen2.5vl:7b` | Ollama HTTP `/api/chat` | `resolve_engine.py:818-832` (fallback de vLLM) | utilisé en prod — fallback principal de la cascade |
| `qwen2.5vl:7b` | Ollama HTTP `/api/chat` | `resolve_engine.py:2536-2585` (`_locate_popup_button`) | utilisé en prod — cas spécifique popup |
| `qwen3-vl:8b`, `gemma4:e4b` | Ollama HTTP | `core/detection/ollama_client.py` (utilisé par `ui_detector.py`, `som_engine.py`, `vram_orchestrator.py`) | utilisé en prod — détection UI + SoM côté core/detection |
| `qwen2.5vl:3b` | Ollama HTTP | `visual_workflow_builder/backend/api_v3/capture.py:245` (description anchor) | utilisé en prod — chaîne capture VWB |
| `qwen3-vl:8b` | Ollama HTTP | `visual_workflow_builder/backend/api_v3/dag_execute.py:468` (LLMActionHandler) | utilisé en prod — DAG executor LLM |
| `qwen2.5vl` | Ollama HTTP | `visual_workflow_builder/backend/catalog_routes_v2_vlm.py` | utilisé en prod — catalog UI |
| OpenAI-compat cloud (OpenAI/Gemini/Anthropic) | HTTP cloud (opt-in `VLM_ALLOW_CLOUD=true`) | `visual_workflow_builder/backend/vlm_provider.py` | câblé mais inactif (cloud désactivé par défaut, contraire à la directive 100% local) |
| `cckevinn/SeeClick` (Qwen-VL) | Transformers in-process | `core/detection/seeclick_adapter.py` | téléchargé non utilisé (signalé "cassé" par commit `d1b556b6c`, exporté par `__init__.py` mais zéro call site actif) |
| `Owlv2` (Google OWL-v2) | Transformers in-process | `core/detection/owl_detector.py` (via `ui_detector.py:31,113,126`) | câblé mais inactif (présent dans la chaîne de détection — bench récent inconnu) [À VÉRIFIER PAR DOM] |
| `ByteDance-Seed/UI-TARS-1.5-7B` | Transformers (référencé) | `tools/start_grounding_server.sh` | référencé en doc seulement (modèle remplacé par InfiGUI dans le code par commit `77faa03ec`) |
---
## 4. Capacités du VWB (visual_workflow_builder)
### 4.1 Modes de construction de workflows
Trois voies coexistent :
1. **Capture interactive** : sélection de zones/ancres via `POST /api/v3/capture/screen` + `POST /api/v3/capture/select` (frontend `CapturePanel.tsx`, `CaptureLibrary.tsx`).
2. **Édition manuelle dans le canvas** : ajout d'étapes via `POST /api/v3/workflow/{id}/step` (frontend `StepNode.tsx`, `ToolPalette.tsx`, `PropertiesPanel.tsx`).
3. **Import de workflow appris par Léa** : `POST /api/v3/learned-workflows/{id}/import` lit les workflows produits côté streaming server (sessions enregistrées) et les insère en SQLite VWB.
### 4.2 Types d'actions supportées
36 types listés dans `frontend_v4/src/types.ts:40-82` (constante `ACTIONS`).
**Souris** :
- `click_anchor` — Clic gauche sur élément visuel — needs anchor : oui
- `double_click_anchor` — Double-clic — needs anchor : oui
- `right_click_anchor` — Clic droit (menu contextuel) — needs anchor : oui
- `hover_anchor` — Survol — needs anchor : oui
- `drag_drop_anchor` — Glisser-déposer vers cible — needs anchor : oui
- `scroll_to_anchor` — Défiler jusqu'à élément — needs anchor : oui
- `focus_anchor` — Donner focus clavier — needs anchor : oui
**Clavier** :
- `type_text` — Saisir texte (templating `{{var}}`) — needs anchor : non
- `type_secret` — Saisir secret depuis coffre-fort — needs anchor : non
- `keyboard_shortcut` — Combinaison touches — needs anchor : non
**Attente** :
- `wait_for_anchor` — Attendre apparition élément — needs anchor : oui
**Données** :
- `extract_text` — OCR EasyOCR fr+en sur dernier screenshot → variable — needs anchor : non
- `extract_table` — OCR + filtre regex → liste structurée → variable — needs anchor : oui
- `screenshot_evidence` — Capture preuve — needs anchor : non
- `download_to_folder` — Télécharger fichier — needs anchor : non
- `db_save_data` / `db_read_data` — BDD locale — needs anchor : non
- `import_excel` / `db_foreach` — Boucle Excel/CSV → BDD — needs anchor : non
**Logique** :
- `visual_condition` — Branchement si ancre trouvée — needs anchor : oui (hidden : true)
- `loop_visual` — Boucle tant qu'ancre visible — needs anchor : oui (hidden : true)
- `pause_for_human` — Pause supervisée + safety_checks (QW4) — needs anchor : non
- `t2a_decision` — Analyse DPI urgences via LLM local (qwen2.5:7b par défaut) — needs anchor : non
**IA (Ollama vision/text)** :
- `ai_ocr` — OCR IA sur ancre — needs anchor : oui
- `ai_summarize` — Résumé LLM — needs anchor : non
- `ai_extract` — Extraction structurée IA — needs anchor : oui
- `ai_classify` — Classification — needs anchor : non
- `ai_analyze_text` — Analyse libre — needs anchor : non
- `ai_custom` — Appel IA libre avec system prompt — needs anchor : non
**LLM via DAGExecutor (parallèle)** :
- `llm_analyze` / `llm_translate` / `llm_extract_data` / `llm_generate` — needs anchor : non
**Fichiers** :
- `file_list_dir` / `file_create_dir` / `file_move` / `file_copy` / `file_sort_by_ext` — needs anchor : non
**Validation** :
- `verify_element_exists` — needs anchor : oui
- `verify_text_content` — needs anchor : oui
### 4.3 Intégration avec Léa
Le VWB **ne pousse pas** un workflow à Léa : il l'**enregistre** côté streaming server. Mécanisme :
1. Workflow sauvé en SQLite VWB (`workflows.db`).
2. `POST /api/v3/workflow/{id}/export-for-lea` (`learned_workflows.py:413`) sérialise et envoie au streaming server (proxy `STREAMING_SERVER_URL=http://localhost:5005`).
3. Lancement : frontend appelle `POST /api/v3/execute/start` (`execute.py:1528`) qui transite vers `POST /api/v1/traces/stream/replay` côté streaming server.
4. Léa V1 récupère ensuite les actions une à une via son polling `GET /replay/next` (cf. §1).
### 4.4 Bibliothèque de captures
Disponible. Architecture v2 (avril 2026) :
- PNG HD écrit dans `data/library_captures/{id}.png` (source de vérité)
- `data/capture_library.json` = métadonnées + thumbnail base64 640×360 q85 (rapide à charger pour la grille)
- Endpoints : `GET/POST /api/v3/capture/library`, `POST /api/v3/capture/library/upload`, `GET /api/v3/capture/library/{id}/full`, `DELETE /api/v3/capture/library/{id}`
- Permet à l'utilisateur de réutiliser des captures (ancres) entre workflows sans recapturer.
---
## 5. Capacités de l'agent_chat
### 5.1 Endpoints
23 routes Flask dans `agent_chat/app.py` :
- `GET /` — UI principale chat
- `GET /classic` — UI classique
- `GET /api/status` — Statut serveur
- `GET /api/workflows` — Liste workflows disponibles
- `POST /api/workflows/refresh` — Recharger
- `GET /api/machines` — Liste machines
- `POST /api/search` — Recherche workflow
- `POST /api/execute` — Exécuter un workflow nommé
- `GET /api/history` — Historique conversations
- `POST /api/chat` — Endpoint chat principal (routage NLP)
- `POST /api/gpu/<action>` — Contrôle GPU (start/stop/status)
- `GET /api/llm/status` — Statut Ollama
- `POST /api/llm/model` — Changer modèle actif
- `POST /api/agent/plan` — Planifier (autonomous_planner)
- `POST /api/agent/execute` — Lancer plan
- `GET /api/agent/status` — Statut agent
- `GET /api/gestures` — Catalogue de gestures réflexes
- `POST /api/chat/upload` — Upload pièce jointe
- `GET /api/help` — Aide
- `POST /api/urgences/parse` — Parsing intent "traite N dossiers" (gemma3:1b)
- `POST /api/urgences/start` — Démarre l'orchestrateur urgences
- `GET /api/urgences/status/<orch_id>` — État orchestrateur
- `GET /api/urgences/list` — Liste orchestrations en cours
### 5.2 Cas d'usage métier
- **Orchestration urgences GHT** (`urgences_orchestrator.py`) : reçoit "traite N dossiers" en chat, parse via `gemma3:1b`, ouvre Chrome (Win+R) sur la maquette Easily Assure via `/replay/raw`, extrait la liste IPP avec `extract_table`, puis pour chaque IPP lance le workflow `Urgence_unit` via `/replay` avec `variables={"patient_id": ipp}`. Synthèse finale postée dans le chat. État pollable via `/api/urgences/status/<id>`.
- **Recherche/exécution workflow par nom naturel** (`/api/search` + `/api/execute`) — résolution sémantique nom utilisateur → workflow_id.
- **Plan autonome** (`/api/agent/plan` + `/api/agent/execute`) — `autonomous_planner.py` planifie un workflow inédit à partir d'un objectif libre via `qwen2.5:7b`.
### 5.3 Modèles LLM utilisés
- `gemma3:1b` — NLP intent parsing urgences (`urgences_orchestrator.py:58`, env `LEA_NLP_MODEL`)
- `qwen2.5:7b` — chat principal + autonomous_planner (`app.py:229,319`, `intent_parser.py:283,690`)
- `qwen3:8b` — modèle Léa par défaut env `LEA_LLM_MODEL` (`app.py:675`), avec `think=False` désactivé (qwen3)
---
## 6. Modules orphelins (code présent mais non câblé)
| Module | Chemin | Pourquoi orphelin (factuel) | Mentionné en commit ou doc ? |
|---|---|---|---|
| `core/grounding/fast_pipeline.py` | `core/grounding/` | Référencé uniquement par `core/execution/observe_reason_act.py:1639` (lui-même semi-orphelin, voir ci-dessous). Zéro import depuis `agent_v0/server_v1/`, `visual_workflow_builder/backend/`, `agent_chat/`. | commit `b30d4b665` (Phase 4 FAST→SMART→THINK) |
| `core/grounding/fast_detector.py` | `core/grounding/` | Zéro import depuis le code actif. | commit `ea36bba5c` (Phase 1-2) |
| `core/grounding/smart_matcher.py` | `core/grounding/` | Zéro import depuis le code actif. | commit `ea36bba5c` |
| `core/grounding/think_arbiter.py` | `core/grounding/` | Zéro import depuis le code actif. | commit `e4a48e78b` (Phase 3) |
| `core/grounding/shadow_learning_hook.py` | `core/grounding/` | Zéro import dans `agent_v0/server_v1/`, `visual_workflow_builder/`, `agent_chat/`. | commit `73cea2385` (Phase 6) ; mémoire mentionne "ShadowLearningHook non branché" |
| `core/grounding/template_matcher.py` | `core/grounding/` | Zéro import depuis le code actif. | commit `9da589c8c` (création) |
| `core/grounding/pipeline.py` | `core/grounding/` | Zéro import depuis le code actif. | commit `9da589c8c` |
| `core/grounding/element_signature.py` | `core/grounding/` | Zéro import depuis le code actif. | commit `e4a48e78b` |
| `core/grounding/server.py` (Flask 8200) | `core/grounding/` | Daemon Flask single-thread alternatif à `infigui_server.py` (Unix socket). Pas de service systemd actif pointant dessus. | doc `tools/start_grounding_server.sh` (toujours obsolète, pointe UI-TARS) |
| `core/detection/seeclick_adapter.py` | `core/detection/` | Encore exporté par `core/detection/__init__.py` mais zéro call site actif. Signalé "cassé" par commit `d1b556b6c`. | commit `21bfa3b33` (création) ; `d1b556b6c` (suppression call site) |
| `core/learning/target_memory_store.py` | `core/learning/` | Référencé par `agent_v0/server_v1/replay_memory.py:62` et `replay_learner.py:210`**pas orphelin** (semi-actif via replay_memory). | mémoire "V3/V4 découplés au runtime" |
| `core/learning/continuous_learner.py` | `core/learning/` | Zéro import depuis code actif. | — |
| `core/learning/feedback_processor.py` | `core/learning/` | Zéro import. | — |
| `core/learning/versioned_store.py` | `core/learning/` | Zéro import. | — |
| `core/execution/target_resolver.py` | `core/execution/` | Zéro import depuis code actif. | mémoire "TargetResolver cross-frame bug" |
| `core/execution/target_memory.py` | `core/execution/` | Zéro import. | — |
| `core/execution/action_executor.py` | `core/execution/` | Zéro import. | — |
| `core/execution/execution_robustness.py` | `core/execution/` | Zéro import. | — |
| `core/execution/recovery_strategies.py` | `core/execution/` | Zéro import. | — |
| `core/execution/observe_reason_act.py` | `core/execution/` | Importé par `visual_workflow_builder/backend/api_v3/execute.py:1431,1955` (mode `verified` ORALoop). Pas pleinement orphelin mais activé seulement dans ce mode. | mémoire "Phase 5 intégration FAST→SMART→THINK dans ORA" |
| `core/healing/healing_engine.py` + `confidence_scorer.py` + `recovery_logger.py` + `learning_repository.py` | `core/healing/` | Zéro import depuis `agent_v0/server_v1/` et `visual_workflow_builder/backend/api_v3/`. Seul `execution_integration.py` est référencé (2 occurrences). | — |
| Service systemd `rpa-streaming.service` | `deploy/systemd/` | Présent dans le repo mais `/etc/systemd/system/rpa-streaming.service.parked`**inactif** sur la machine de Dom. | — |
| Service systemd `rpa-grounding.service` | `deploy/systemd/` | `/etc/systemd/system/rpa-grounding.service.parked`**inactif**. | commit `3d6868f02` |
| Service systemd `rpa-vision-v3-api.service` / `rpa-vision-v3-worker.service` / `rpa-vision-v3-dashboard.service` / `rpa-agent-chat.service` | `deploy/systemd/` | Tous trouvés `.parked` dans `/etc/systemd/system/`. | — |
| `agent_v0/server_v1/safety_checks_provider.py` | `agent_v0/server_v1/` | **Pas orphelin** : importé par `api_stream.py:2980` (QW4 build_pause_payload). Listé pour contexte. | mémoire "QW4 safety_checks hybrides" |
| Endpoints `/api/v1/shadow/*` | `agent_v0/server_v1/api_stream.py:1661-1820` | Définis côté serveur, **aucun appelant identifié** dans `agent_v0/agent_v1/` (Léa client) ni dans `visual_workflow_builder/`, ni dans `agent_chat/`. | — [À VÉRIFIER PAR DOM] |
---
## 7. À vérifier avec Dom (synthèse des `[À VÉRIFIER PAR DOM]`)
- **Modes Léa "shadow / copilot / assisté"** : seuls les booléens `_recording` / `_replay_active` existent côté client. Confirme-tu qu'aujourd'hui Léa V1 n'a effectivement que ces deux modes runtime (et que les modes VWB `basic/intelligent/debug/verified` ne pilotent rien du client) ?
- **Endpoints `/api/v1/shadow/*`** côté serveur : zéro appelant identifié dans le repo. Sont-ils consommés par un script externe / une démo, ou candidats à archivage ?
- **Service `rpa-grounding.service`** : présent dans `deploy/systemd/` mais en `.parked` sur la machine. La cascade vLLM→Ollama tourne donc sans `infigui_server.py` ? Confirmer que le grounding Transformers est bien désactivé en prod actuellement.
- **Service `rpa-streaming.service`** : trouvé `.parked` dans `/etc/systemd/system/`. Le streaming server tourne-t-il via `svc.sh` / `run.sh` au lieu de systemd ?
- **`core/detection/seeclick_adapter.py`** : encore exporté par `__init__.py` mais signalé cassé. Sortir de l'export ou tenter une réparation pour Qwen3-VL ?
- **`core/detection/owl_detector.py` (Owlv2)** : câblé via `ui_detector.py` mais aucun bench récent. Encore appelé en prod ou candidat à l'archivage ?
- **vLLM (port 8100)** : code prêt dans `resolve_engine.py:785-816`. Confirmer si vLLM tourne actuellement ou si la cascade saute systématiquement à Ollama.
- **Mode VWB `verified`** : seul mode qui active `core/execution/observe_reason_act.py` (ORALoop). Est-il utilisé en démo GHT ou réservé au debug ?

View File

@@ -0,0 +1,191 @@
# Handoff session 2026-05-06 — démo GHT Sud 95 (J-2)
**Auteur :** Claude (session précédente, contexte saturé)
**Pour :** Claude (nouvelle session)
**Démo :** dans ~2 jours, pour DSI Carvella + DIM/TIM/DG GHT Sud 95
**Branche git :** `feature/feedback-bus`
---
## 🎯 Objectif de la nouvelle session
Finir la prep démo : terminer corrections `data.js` + structurer onglet Imagerie + accompagner Dom sur l'enregistrement workflow Urgence_unit + tests E2E.
---
## 📊 État actuel — ce qui est FAIT
### Code & infrastructure
-**Maquette aiva-vision** déployée : `https://urgence.labs.laurinebazin.design/codage.html?id=XXX` + service systemd `rpa-mockup-easily` (Flask, port 8765)
-**Backend `/api/analyse`** branché sur `core.llm.t2a_decision.analyze_dpi` (qwen2.5:7b par défaut)
-**Maquette codage.html refondue** avec layout aiva-vision 2 colonnes + auto-trigger paste
-**5e onglet "Imagerie"** ajouté côté UI (HTML + JS) — mais **`data.js` pas encore alimenté** (sauf le champ vide qui est traité comme "Aucun examen")
-**Pipeline Léa orchestrateur** dans `agent_chat/urgences_orchestrator.py` (NLP gemma3:1b + thread + boucle + synthèse) — fonctionne en bout-en-bout, validé matin du 2026-05-05
-**`extract_table` backend** dans `core/llm/ocr_extractor.py` + `agent_v0/server_v1/replay_engine.py` (regex IPP `^25\d{6}$`) — testé OK 11/11 sur capture liste patients
-**Préchargement EasyOCR au boot** du streaming server (3.8s, log confirmé) — fini le cold start qui bloquait 2 min
-**Templating `{{patient_id}}`** sur `by_text` dans replay_engine + variables runtime initiales via `ReplayRequest.variables`
-**Agent V1 mis à jour** sur PC Windows (32 fichiers .py, hashes vérifiés, compile clean) — 2026-05-04
-**Catalogue de réflexes** (`gesture_catalog.py`) utilisé par l'orchestrateur (composition réflexes + workflows appris, pas de hardcode)
### Bench LLM — 18 modèles testés
- ✅ Rapport complet : `docs/BENCH_T2A_DECISION_11DOSSIERS.md`
- 🥇 **`gemma3:27b-cloud` : 8/11 (73%)** sur vérité-terrain corrigée — recommandé démo
- 🥈 `qwen3:8b` : 7/11 (64%) — backup local, 7.6s/dossier, 5 GB → tient large dans 12 GB GPU
- ⚠️ Bench fait sur DPI partiellement fictifs (cf. revue Pauline) → ré-évaluer après corrections data.js
### Documentation produite
- `docs/BENCH_T2A_DECISION_11DOSSIERS.md` — bench 18 modèles
- `docs/BENCH_MINI_LLM_NLP.md` — bench gemma3:1b vainqueur (NLP commande chat)
- `docs/REVUE_DOSSIERS_PAULINE.md` — revue 11 dossiers vs captures (fait par sous-agent, **avec quelques inexactitudes** : "médecins du sport 0559447669" remonté comme inventé, en réalité présent dans la capture)
- `docs/POINTS_SUSPECTS_PAULINE.md` — synthèse pour visio Pauline (méthodo : on ne signale PAS les noms anonymisés, on focus hallucinations cliniques + constantes + imagerie)
- `docs/MAIL_PAULINE_AVANT_VISIO.md` — mail-template à adapter
### Mémoires importantes (`~/.claude/.../memory/`)
- `feedback_anonymisation_stricte.md` ⭐ — règle absolue : anonymiser = remplacement chirurgical des identités, **JAMAIS réécrire le contenu clinique**. Erreur historique : "anhydrose" vs "ankylose" sur 25003475
- `feedback_lea_reflexes_catalog.md` — utiliser `gesture_catalog.py`, ne pas hardcoder Win+R+type+Enter
- `feedback_auth_dialogs_runtime.md` — Windows Hello / Basic Auth bloquent le replay, anticiper avant chaque démo client
- + toutes les feedbacks existantes dans `MEMORY.md`
### Corrections data.js DÉJÀ FAITES
1. **25003475** (UHCD aura migraineuse) — 3 hallucinations cliniques graves corrigées :
- `symptomes_orientation` : "Migraines de membre" → "**Faiblesse** de membre"
- `notes_paramedicales[0]` (03:09) : "Pansement compressif possible si perfusé" → "**--> RAD possible. dé perfusé**"
- `notes_medicales[1]` (Histoire maladie) : "anhydrose au talon supérieur" → "**ankylose du membre supérieur gauche**"
2. **25151530** (Forfait colique néphrétique) :
- "TDM sans injection" → "**TDM avec injection**" (2 occurrences)
- Histoire de la maladie enrichie avec **ATCD RGO + TTT ESOMEPRAZOLE**
3. **`server.py` `VERITES_TERRAIN`** : 25003284 reclassé `FORFAIT_URGENCE` (sortie domicile en 3h37, J12.1 VRS)
4. **`app.js` signes_vitaux** : adapté pour itérer sur N colonnes dynamiquement (au lieu de v1/v2 fixe). **MAIS** `data.js` a toujours 2 cols partout → rétrocompatible.
---
## ❌ Ce qui RESTE à faire (priorité décroissante)
### 🔴 PRIORITÉ 1 — corrections data.js (1-2h)
#### Constantes vitales tronquées (4 dossiers)
Captures Pauline source : `/home/dom/Téléchargements/Exemples Dossiers UHCD - Forfaits (1)/Exemples Dossiers UHCD - Forfaits/[UHCD|FORFAITS]/<IPP>/`
| IPP | Actuel data.js | Capture | Cols à ajouter | Priorité |
|---|---|---|---|---|
| 25003364 | 2 cols (21:02 + 14:45) | 4 cols | 19:45 (volume miction 700) + 18:44 (T 71, FC 87, PA 168/92, débit O2 2L) | 🟠 |
| 25005866 | 2 cols (10:56 + 23:01) | 5 cols | 08:20, 06:25, 02:00 (surveillance neuro post-trauma) | 🔴 grave |
| 25048485 | 2 cols (12:09 + 10:52) | 5 cols | 10:58, 10:54, 10:53 + ligne PA Latéralité | 🔴 grave |
| 25151530 | 2 cols (06:41 + 03:25) | 7 cols | 08:15, 07:37, 06:00, 04:45, 04:01 (évolution douleur EN: 7→0→5→10→6→4) | 🔴 très grave |
**Format à adopter** : passer de `{item, v1, v2}` à `{item, v1, v2, v3, v4, ...}`. `app.js` itère déjà dynamiquement sur N cols.
#### Imagerie à structurer (7 dossiers)
Le champ `imagerie: [{date, type, par, role, horodatage, contenu}, ...]` est lu par `renderImagerie()` dans `app.js` (déjà branché).
| IPP | CR à déplacer | Source actuelle data.js |
|---|---|---|
| 25003284 | RX thorax (signé Dr LAURENT Charles) | `notes_medicales[3]` |
| 25003364 | RX pulmonaire (foyer condensation lobaire D) | `notes_medicales[0]` |
| 25003475 | Scanner cérébral sans injection (normal) | `notes_medicales[0]` |
| 25005866 | 3 examens : TDMc 01:53 + RX thorax 01:54 + TDMc contrôle 10:18 | `notes_medicales[1, 2]` |
| 25012257 | TDM AP sans injection (allergie iode) + ECG | `notes_medicales[0]` |
| 25056615 | Scanner AP avec injection (CR complet) | `notes_medicales[0]` |
| 25151530 | Scanner AP avec injection | `notes_medicales[1]` |
**Règle** : extraire le CR du `notes_medicales` (ou autre source), le placer dans `imagerie` **mot pour mot** (procédure stricte). Le retirer de `notes_medicales` si entièrement déplacé OU laisser une mention "voir onglet Imagerie".
#### Enrichir 25048485 (2 motifs CTCG)
Captures montrent 2 motifs distincts le **même jour 28/02/2025** : 1ère CTCG le matin 9h15, 2e CTCG l'après-midi (récidive). data.js modélise UN seul passage 10:40→17:30 → cohérent (le patient a été gardé entre les 2). **Action** : enrichir l'histoire de la maladie pour mentionner explicitement les 2 épisodes (matin + après-midi). **Pas une question Pauline** — juste clarification de présentation.
### 🟠 PRIORITÉ 2 — re-bench T2A après corrections (30 min)
Après corrections data.js, relancer `bench_t2a_cloud.py` (top 5 modèles seulement) pour avoir les chiffres réels. Les scripts existent dans `/tmp/bench_t2a*.py`. Mettre à jour `BENCH_T2A_DECISION_11DOSSIERS.md`.
### 🟠 PRIORITÉ 3 — workflow Urgence_unit (Dom, sur PC Windows)
Dom enregistre le workflow VWB qui traite 1 dossier de bout-en-bout :
1. Click sur lien IPP `{{patient_id}}` (variabilisé)
2. Navigation dans les onglets dossier (Motif, Examens, Imagerie, Notes médicales, Synthèse)
3. extract_text par onglet → DPI consolidé
4. Click "Coder >" → arrive sur aiva-vision
5. type_text DPI dans `#dpi-input` (auto-trigger analyse)
6. Wait + extract_text décision aiva-vision
7. Click "Liste patients" pour revenir
**Tu peux le faire MAINTENANT** (data.js stable, aiva-vision opérationnelle, agent V1 à jour). Mais **attendre que les corrections data.js soient finies** est plus sûr (sinon les anchors visuels peuvent se déplacer si la table signes vitaux gagne des colonnes).
### 🟡 PRIORITÉ 4 — visio Pauline (pour les questions ouvertes)
Pauline doit répondre à 4-5 questions critiques :
1. **25048485** : confirmer 2 épisodes le même jour ✓ (pas urgent, je gère seul)
2. **25005866** : "médecins du sport 0559447669" — Pauline avait dit inventé, le sous-agent a vu présent → contradiction à trancher
3. **25003284 étiquette workflow** : "UHCD asthme" → "Pneumopathie VRS" ?
4. **Onglet Imagerie** : niveau de détail attendu
Mail-template prêt : `docs/MAIL_PAULINE_AVANT_VISIO.md`
### 🟡 PRIORITÉ 5 — Tests E2E (J-1, dernier jour)
10 répétitions du scénario démo complet (chat Léa "traite-moi 3 dossiers" → orchestration → boucle → synthèse).
---
## 🚨 Contraintes critiques
1. **Procédure d'anonymisation stricte** (cf. `feedback_anonymisation_stricte.md`) : pour toute correction data.js, **NE JAMAIS reformuler/synthétiser** le contenu clinique. Remplacer chirurgicalement les identités/dates uniquement. Erreur historique grave : "anhydrose"/"ankylose".
2. **Catalogue de réflexes** (cf. `feedback_lea_reflexes_catalog.md`) : utiliser `gesture_catalog.py` pour les raccourcis natifs (Win+R, etc.), ne pas hardcoder.
3. **Pas de cloud LLM dans le projet** sauf pour la démo (Ollama Cloud via clés Dom). Prod 100% local.
4. **Captures Pauline = source de vérité** : `/home/dom/Téléchargements/Exemples Dossiers UHCD - Forfaits (1)/...` (8 dossiers) + `/tmp/captures_pauline_3manquants/` (3 dossiers extraits du docx). En cas de conflit avec rapport revue, **la capture prime**.
5. **Noms substitués = anonymisation volontaire** : ne pas signaler comme erreur, ne pas chercher à les réaligner avec captures.
---
## 📁 Fichiers clés à connaître
- `/home/dom/ai/rpa_vision_v3/docs/clients/ght_sud_95/mockup_easily_assure/data.js` — base 11 dossiers (~2100 lignes)
- `/home/dom/ai/rpa_vision_v3/docs/clients/ght_sud_95/mockup_easily_assure/server.py` — backend Flask + `/api/analyse` + `VERITES_TERRAIN`
- `/home/dom/ai/rpa_vision_v3/docs/clients/ght_sud_95/mockup_easily_assure/app.js` — rendering frontend (signes_vitaux dynamique appliqué)
- `/home/dom/ai/rpa_vision_v3/agent_v0/server_v1/api_stream.py` — streaming server (boot avec préchargement EasyOCR)
- `/home/dom/ai/rpa_vision_v3/agent_chat/urgences_orchestrator.py` — orchestrateur démo
- `/home/dom/ai/rpa_vision_v3/core/llm/t2a_decision.py` — décision T2A LLM
- `/home/dom/ai/rpa_vision_v3/core/llm/ocr_extractor.py``extract_text_from_image` + `extract_table_from_image`
---
## 🔧 Commandes utiles
```bash
# Vérifier syntaxe data.js
node -e "var fs=require('fs'); var c=fs.readFileSync('docs/clients/ght_sud_95/mockup_easily_assure/data.js','utf-8'); c=c.replace(/^const /gm, 'var '); var s={}; require('vm').runInContext(c, require('vm').createContext(s)); console.log('OK', Object.keys(s.DOSSIERS).length);"
# Restart streaming server
cd /home/dom/ai/rpa_vision_v3 && ./svc.sh restart streaming
# Restart maquette
sudo systemctl restart rpa-mockup-easily
# Re-bench T2A (après corrections data.js)
node /tmp/extract_dpi.js > /tmp/dpis.json
/home/dom/ai/rpa_vision_v3/.venv/bin/python /tmp/bench_t2a_cloud.py
```
---
## ⏱ Timeline démo
- **Aujourd'hui** : finir corrections data.js + visio Pauline
- **Demain (J-2 / 2026-05-07)** : enregistrement workflow Urgence_unit sur PC Windows
- **J-1 (2026-05-08)** : tests E2E répétés
- **Jour J** : démo GHT Sud 95
---
## 📌 Ce qui marche aujourd'hui (pas casser)
- Maquette aiva-vision visible et fonctionnelle
- Pipeline orchestrateur opérationnel (testé matin 05/05)
- Préchargement EasyOCR opérationnel (3.8s au boot)
- Bench complet avec rapport
- Mail-template Pauline prêt
**Ne pas relancer les services systemd sans raison**, ne pas modifier `server.py` sauf pour les corrections explicites.

View File

@@ -0,0 +1,175 @@
# Historique des implémentations VLM — Audit 2026-05-08
Branche : `feature/qw-suite-mai`
HEAD : `731b5bcae`
Périmètre : tout backend VLM (Ollama, vLLM, Transformers, services dédiés) — code actif, archivé, ou disparu de l'historique.
---
## 1. Implémentations VLM actuellement actives
### 1.1. Transformers in-process (Qwen2.5-VL family)
| Fichier | Fonction(s) | Modèle / Backend | Commentaire |
|---|---|---|---|
| `core/grounding/server.py` | `load_model`, `ground` (Flask `/ground`) | `InfiX-ai/InfiGUI-G1-3B` 4-bit NF4 via `Qwen2_5_VLForConditionalGeneration` + `qwen_vl_utils.process_vision_info` | Serveur Flask single-thread port 8200, contient `_smart_resize` (factor 28, MIN_PIXELS=100·28², MAX_PIXELS=5600·28²). |
| `core/grounding/infigui_worker.py` | `load_model`, `infer`, `main` (one-shot stdin/stdout) | Idem (`InfiX-ai/InfiGUI-G1-3B` 4-bit NF4, transformers + qwen_vl_utils) | Mode subprocess one-shot : lit JSON sur stdin, écrit sur stdout. Pas de `_smart_resize` complet (formule courte L99-L101 sans clamp min/max). |
| `core/grounding/infigui_server.py` | `InfiGUIServer.start`, `_do_ground`, `_do_ping` | Réutilise `infigui_worker.load_model` / `infer` | Daemon Unix socket (`/run/rpa/grounding.sock`), protocole length-prefixed JSON. Service systemd `rpa-grounding.service`. |
| `core/grounding/ui_tars_grounder.py` | `UITarsGrounder.ground`, `_send_socket_request`, fallback subprocess | Client : socket → fallback subprocess (`python -m core.grounding.infigui_worker`) | Ne charge plus rien in-process. Coordonne socket+subprocess. Fichier mis à jour 2026-05-05. |
| `core/grounding/think_arbiter.py` | `ThinkArbiter.arbitrate` | Délègue à `UITarsGrounder` | Layer THINK du pipeline FAST→SMART→THINK. |
| `core/detection/owl_detector.py` | `OwlDetector` | `Owlv2Processor` + `Owlv2ForObjectDetection` (Google OWL-v2) via transformers | Câblé dans `core/detection/ui_detector.py` (L31, L113, L126). Pas un VLM grounding GUI mais détecteur open-vocabulary. |
| `core/detection/seeclick_adapter.py` | `SeeClickAdapter._load_model`, `ground` | `cckevinn/SeeClick` (Qwen-VL) via `AutoModelForCausalLM` + `AutoTokenizer` | Encore exporté par `core/detection/__init__.py` mais signalé "cassé" par le commit `d1b556b6c` (avril 2026) qui l'a retiré de `intelligent_executor.py`. Pas d'autre call site actif. |
### 1.2. HTTP OpenAI-compatible (vLLM)
| Fichier | Fonction | Détails |
|---|---|---|
| `agent_v0/server_v1/resolve_engine.py` (L785-L816) | `_resolve_by_grounding` | Essai 1 vLLM `http://localhost:${VLLM_PORT}/v1/chat/completions`, modèle `Qwen/Qwen2.5-VL-7B-Instruct-AWQ` (env `VLLM_PORT=8100`, `VLLM_MODEL`). Format : POST OpenAI chat.completions avec `image_url: data:image/jpeg;base64`. Fallback Ollama si échec. |
Verbatim L789-L816 :
> ```
> # Port vLLM configurable via env
> _vllm_port = os.environ.get("VLLM_PORT", "8100")
> _vllm_model = os.environ.get("VLLM_MODEL", "Qwen/Qwen2.5-VL-7B-Instruct-AWQ")
>
> # Essai 1 : vLLM (API OpenAI-compatible, GPU)
> try:
> vllm_resp = _requests.post(
> f"http://localhost:{_vllm_port}/v1/chat/completions",
> json={
> "model": _vllm_model,
> "messages": [
> {"role": "system", "content": "You locate UI elements on screenshots. Return coordinates."},
> {"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": 80,
> },
> timeout=30,
> )
> if vllm_resp.ok:
> content = vllm_resp.json().get("choices", [{}])[0].get("message", {}).get("content", "")
> if content:
> logger.debug("Grounding via vLLM OK")
> except Exception as e:
> logger.debug("vLLM non disponible (%s), fallback Ollama", e)
> ```
### 1.3. HTTP Ollama (état dominant en prod aujourd'hui)
| Fichier | Fonction | Modèle |
|---|---|---|
| `agent_v0/server_v1/resolve_engine.py` (L818-L832) | `_resolve_by_grounding` (fallback de vLLM) | `qwen2.5vl:7b` via `/api/chat` Ollama. |
| `agent_v0/server_v1/resolve_engine.py` (L2536-L2585) | `_locate_popup_button` | `qwen2.5vl:7b` via `/api/chat`. |
| `core/detection/ollama_client.py` | `OllamaClient` | `qwen3-vl:8b`, `gemma4:e4b`, etc. — utilisé par `core/detection/ui_detector.py`, `core/detection/som_engine.py`, `core/cognition/vram_orchestrator.py`. |
| `core/detection/vlm_config.py` | `FALLBACK_VLM_MODELS` | `["qwen3-vl:8b", "0000/ui-tars-1.5-7b-q8_0:7b"]` |
| `visual_workflow_builder/backend/vlm_provider.py` | `VLMProvider.detect_ui_element` | Hub Ollama prioritaire + cloud opt-in (OpenAI/Gemini/Anthropic) si `VLM_ALLOW_CLOUD=true`. |
| `visual_workflow_builder/backend/api_v3/capture.py` (L245) | description anchor | `qwen2.5vl:3b` |
| `visual_workflow_builder/backend/api_v3/dag_execute.py` (L468) | LLMActionHandler | `qwen3-vl:8b` |
| `visual_workflow_builder/backend/catalog_routes_v2_vlm.py` | détection visuelle catalog | `qwen2.5vl` |
| `core/llm/ocr_extractor.py`, `core/llm/t2a_decision.py` | LLM text only | Ollama (modèles non-vision). |
---
## 2. Implémentations VLM archivées dans le filesystem
| Chemin | Taille | Mtime | Backend identifié |
|---|---|---|---|
| `_archive/dead_code_20260424/...` (9 fichiers, ~6300 lignes) | divers | 2026-04-24 | **Aucun fichier VLM** — il s'agit de modules workflow/visual non liés à un backend VLM (ex. `visual_persistence_manager.py`, `workflow_simulation_report.py`). Recherche `vllm|transformers|smart_resize|InfiGUI|UI-TARS|qwen2.5vl|qwen2-vl` : 0 hit. |
| `archive/business_docs/`, `archive/historical_recall/` | 3 fichiers .md | 2026-01 / 2026-05-04 | Pas de code (Markdown business / mémoire). |
Aucun fichier `*_old.py`, `*_v1.py`, `*_backup.py`, `*.py.bak` ou `tests_disabled/` détecté. Les seuls `*_v1.py` existants sont `agent_v0/run_agent_v1.py` (non VLM).
---
## 3. Commits historiques mentionnant VLM/vLLM/Transformers/grounding
Liste chronologique inverse (≤ 25 commits pertinents). SHA court · date · message · fichiers VLM touchés (résumé `--stat`).
| SHA | Date | Message | Fichiers VLM clé |
|---|---|---|---|
| `487bcb861` | 2026-04-26 | feat(execution): cascade post-raccourci pilotée par DialogHandler/OCR | `core/grounding/{dialog_handler,infigui_worker,think_arbiter,ui_tars_grounder}.py` |
| `3d6868f02` | 2026-04-26 | docs: cartographie + worker InfiGUI fichiers | `core/grounding/{server,ui_tars_grounder,infigui_worker,dialog_handler}.py` (création worker, refonte server.py de 494→124 lignes) |
| `343d6fbe9` | 2026-04-26 | perf(ocr): EasyOCR remplace docTR | `core/grounding/{fast_detector,title_verifier}.py` |
| `cc6443973` | 2026-04-26 | feat(grounding): vérification titre OCR post-action | `core/grounding/title_verifier.py` (+158) |
| `90007cc7c` | 2026-04-26 | perf(grounding): réflexe pHash-only + max_new_tokens 64 | `core/grounding/server.py` |
| `77faa03ec` | 2026-04-26 | feat(grounding): InfiGUI-G1-3B remplace UI-TARS 7B | `core/grounding/server.py` (-75/+67) |
| `73cea2385` | 2026-04-25 | feat(grounding): Phase 6 Shadow Learning Hook | `core/grounding/shadow_learning_hook.py` (+156) |
| `e2046837c` | 2026-04-25 | feat(grounding): Phase 5 intégration FAST→SMART→THINK dans ORA | `core/execution/observe_reason_act.py` |
| `b30d4b665` | 2026-04-25 | feat(grounding): Phase 4 pipeline orchestré FAST→SMART→THINK | `core/grounding/fast_pipeline.py` |
| `e4a48e78b` | 2026-04-25 | feat(grounding): Phase 3 ThinkArbiter + SignatureStore | `core/grounding/{think_arbiter,element_signature}.py` |
| `ea36bba5c` | 2026-04-25 | feat(grounding): Phase 1-2 FAST→SMART détection + matching | `core/grounding/{fast_detector,smart_matcher,fast_types}.py` |
| `9da589c8c` | 2026-04-25 | feat(grounding): pipeline centralisé + serveur UI-TARS transformers | **Création** `core/grounding/{server,pipeline,template_matcher,ui_tars_grounder,target,__init__}.py` + `tools/start_grounding_server.sh`. server.py FastAPI port 8200, modèle `ByteDance-Seed/UI-TARS-1.5-7B` 4-bit NF4. |
| `73ddcdb29` | 2026-04-21 | feat: chaîne de grounding 3 niveaux + refonte capture | `core/execution/input_handler.py`, `visual_workflow_builder/.../execute.py` |
| `d1b556b6c` | 2026-04-21 | fix(grounding): supprimer SeeClick cassé | `intelligent_executor.py` (-46) |
| `91614fbff` | 2026-04-04 | fix: prompt natif bbox_2d Qwen2.5-VL | `agent_v0/server_v1/api_stream.py` |
| `c1ce6a396` | (avril) | fix: séparer grounding (qwen2.5vl) et compréhension (gemma4) | api_stream.py |
| `394342be7` | 2026-03-31 | **feat: support vLLM (GPU) comme moteur de grounding, Ollama en fallback** | `agent_v0/server_v1/api_stream.py` (+47/-14) — c'est l'unique commit qui ajoute vLLM. |
| `d99b17394` | 2026-03-31 | feat: VLM grounding direct (Qwen2.5-VL) — nouvelle stratégie de résolution | `agent_v0/server_v1/api_stream.py` (+230) |
| `cbe8dc95d` | (mars) | feat(cognition): timing + auto-apprentissage Shadow + VLM qwen2.5vl | — |
| `ad15237fe` | (mars) | feat: smart systray Léa + support qwen3-vl | — |
| `38966de0d` | (antérieur) | Feat: Action analyser_avec_ia (Ollama qwen2.5-vl) | — |
| `728fac3b5` | (antérieur) | Feat: Actions validation avec OCR Ollama (qwen2.5-vl:7b) | — |
| `21bfa3b33` | 2026-01-24 | feat(vwb): SeeClick + Self-Healing | `core/detection/seeclick_adapter.py` (+) |
| `4509038bf` | 2026-04-09 | refactor: éclater api_stream.py 6400→3350 | déplace le code vLLM/Ollama vers `agent_v0/server_v1/resolve_engine.py` |
Sur `git reflog | head -100` : aucune trace d'opération destructive (pas de `reset --hard`, pas de checkout détruit) qui aurait perdu un commit lié au VLM. Toutes les opérations sont des commits propres.
---
## 4. Code dans des stashes ou branches non mergées
`git stash list` : **aucun stash**.
Branches existantes :
- `main`
- `master` (remote gitea uniquement)
- `feature/qw-suite-mai` (HEAD courant)
- `feature/feedback-bus`
- `backup/pre-qw-suite-mai-2026-05-05`
- `demo/ght-2026-05-08`
- `dev/ia-tools-improvement`
Aucune branche divergente n'apparaît dans `git log --all -S "vllm"` au-delà des deux commits déjà recensés (tous accessibles depuis `feature/qw-suite-mai`). Idem pour `Qwen2_5_VL`, `smart_resize`, `qwen_vl_utils`, `BitsAndBytesConfig` : tous les commits qui les introduisent ou les modifient sont accessibles depuis HEAD.
→ Aucun code VLM unique perdu dans une branche divergente ou un stash.
---
## 5. Code potentiellement perdu (commits de suppression VLM)
| SHA | Date | Action | Résumé |
|---|---|---|---|
| `d1b556b6c` | 2026-04-21 | suppression | Retire SeeClick de `intelligent_executor.py` (-46). Le **fichier `core/detection/seeclick_adapter.py` n'a jamais été supprimé** : il vit toujours dans `core/detection/` (11 421 octets, mtime 2026-01-24) et est encore exporté par `core/detection/__init__.py`. → Code utilisable mais signalé "cassé" (config QWenConfig incompatible). |
| `3d6868f02` | 2026-04-26 | refonte | Réduit `core/grounding/server.py` de 494 → 124 lignes en sortant la logique d'inférence vers `infigui_worker.py`. La logique transformers complète est conservée dans `infigui_worker.py` (et reprise par `infigui_server.py`). → Aucune perte. |
| `77faa03ec` | 2026-04-26 | remplacement modèle | UI-TARS-1.5-7B remplacé par InfiGUI-G1-3B dans `core/grounding/server.py`. Le **prompt UI-TARS officiel** (`Thought:/Action: click(start_box='(x1, y1)')`) et la fonction `_evict_ollama_models()` ont disparu mais restent récupérables via `git show 9da589c8c:core/grounding/server.py`. |
| `9da589c8c` | 2026-04-25 | nettoyage | "9 fichiers morts archivés dans `_archive/` (~6300 lignes)". Vérifié : aucun fichier VLM dans `_archive/dead_code_20260424/`. Ces 9 fichiers sont du visual/workflow, pas du grounding. → Aucune perte VLM. |
| (autres) | | | Pas d'autre commit qui supprime du code VLM exploitable. |
**Pas de code VLM utile irrémédiablement perdu** : tout est récupérable via `git show`. Le seul élément à signaler est le **prompt officiel UI-TARS** présent dans la version `9da589c8c:core/grounding/server.py`, utile si on veut comparer un modèle UI-TARS reload.
---
## 6. Synthèse factuelle
- **Nombre d'implémentations distinctes ayant existé** :
- 7 implémentations actives aujourd'hui (cf. §1.1 + §1.2 + §1.3 modèles distincts).
- 2 implémentations historiques fortes ayant été remplacées en-place : UI-TARS-1.5-7B (transformers) → InfiGUI-G1-3B ; SoM+VLM intermédiaire → grounding direct Qwen2.5-VL.
- **Backends testés au fil du temps** : Ollama (HTTP), vLLM (HTTP OpenAI-compat), Transformers in-process (Flask `server.py`, subprocess one-shot `infigui_worker.py`, daemon Unix socket `infigui_server.py`), HuggingFace direct (SeeClick standalone, OWL-v2 standalone), Cloud opt-in (OpenAI/Gemini/Anthropic via `vlm_provider.py`).
- **Code directement utilisable pour la migration vers vLLM ou Transformers** :
- **Oui pour Transformers** : `core/grounding/server.py` (loader + `_smart_resize` complet avec MIN/MAX_PIXELS) et `core/grounding/infigui_worker.py` (`load_model`, `infer` mode classique + fusion image+anchor) sont quasi clé-en-main pour Qwen2.5-VL / Qwen3-VL. Il suffit de changer `MODEL_ID` (env `GROUNDING_MODEL` déjà supporté).
- **Oui pour vLLM** : `agent_v0/server_v1/resolve_engine.py` lignes 785-816 contient déjà l'appel HTTP OpenAI-compatible avec `image_url: data:image/jpeg;base64`. Il manque uniquement le passage explicite de `resized_width`/`resized_height` (extension OpenAI vLLM) — le bug d'échelle bbox_2d documenté dans `docs/MIGRATION_VLM_PLAN_2026-05-09.md`.
- L'infrastructure socket persistant + fallback subprocess (`infigui_server.py` + `ui_tars_grounder.py`) est réutilisable telle quelle pour servir un autre modèle Transformers ou pour wrapper un client vLLM.
---
## 7. À clarifier avec Dom
1. **`core/detection/seeclick_adapter.py`** est encore exporté par `core/detection/__init__.py` mais le commit `d1b556b6c` indique qu'il est cassé. Faut-il le sortir de l'import et l'archiver, ou tenter de le réparer pour Qwen3-VL ?
2. **`core/detection/owl_detector.py`** (Owlv2) est câblé via `core/detection/ui_detector.py` (L31, L113, L126) mais aucun trace de bench récent. Est-il encore appelé en prod ou candidat à l'archivage ?
3. **`tools/start_grounding_server.sh`** parle encore de `UI-TARS-1.5-7B` dans son banner alors que le serveur charge InfiGUI depuis le commit `77faa03ec`. Doc obsolète mais sans impact runtime — à fixer si on documente la migration.
4. **`core/grounding/server.py` (Flask port 8200)** vs **`core/grounding/infigui_server.py` (Unix socket)** vs **`core/grounding/infigui_worker.py` (subprocess one-shot)** : trois entry-points distincts pour la même logique transformers. Le service systemd `rpa-grounding.service` ne lance que `infigui_server`. Confirmer que `server.py` (Flask) est conservé volontairement comme alternative dev / test.
5. **Modèle vLLM par défaut hardcodé** `Qwen/Qwen2.5-VL-7B-Instruct-AWQ` (resolve_engine L791) alors que le plan migration cible Qwen3-VL — env `VLLM_MODEL` permet le switch sans toucher au code, à confirmer comme méthode de migration.

View File

@@ -0,0 +1,73 @@
# Migration grounding VLM — qwen2.5vl Ollama → vLLM/Transformers (Qwen3-VL)
Date plan : 2026-05-09 (rédigé le 2026-05-08 au soir)
Branche cible : feature/<à-créer-demain>
État actuel : grounding passe par Ollama, qwen2.5vl:7b en split CPU/GPU 42/58.
## 1. Constat
Deux problèmes structurels relevés le 8 mai 2026.
### 1.1. VRAM saturée
`qwen2.5vl:7b` chargé via Ollama pèse 14 GB en mémoire totale alors que la machine n'a que 12 GB de VRAM. Ollama bascule en split CPU/GPU 42/58. Latence mesurée par appel grounding : ~11 s. À titre de référence, `qwen3-vl:8b` (6 GB) tient en full GPU et descend à 1.7 s sur le même cas.
### 1.2. Bug d'échelle bbox_2d (root cause documentée)
La doc officielle Qwen2.5-VL précise que les coordonnées renvoyées sont dans la résolution post-`smart_resize`, pas dans la résolution de l'image envoyée par le client. Or Ollama applique son propre `smart_resize` en interne sans exposer la taille effective au client. Conséquence : le code prod divise par `small_w = orig_w` (taille envoyée) au lieu de `resized_w` → coordonnées toutes shiftées vers le top-left. Bug systémique présent dans 4 occurrences de `agent_v0/server_v1/resolve_engine.py` et dans `_locate_popup_button` (L:2576).
Tant que le grounding passe par Ollama, le fix ne peut être qu'une rustine (resize forcé côté serveur AVANT envoi pour matcher la convention que Ollama va appliquer ensuite — fragile, dépend de la version Ollama).
Source : [HF discussion #13 sur Qwen2.5-VL-7B-Instruct](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct/discussions/13)
Citation mainteneur : « The bbox_2d coordinates ... will be relative to your **resized image size** if you are resizing. »
Citation discussion : « **resized dimensions parameter is not supported in OLLAMA**, which complicates coordinate translation. »
## 2. Rappel bench 8 mai 2026
Screenshot fixture : `data/training/live_sessions/bg_DESKTOP-58D5CAC_windows/shots/heartbeat_1773792436.png` (2560×1600, boîte de dialogue OK/Cancel).
| Modèle / config | Latence | Format bbox_2d | Parse regex prod | Coords cohérentes |
|---|---|---|---|---|
| qwen2.5vl:7b Ollama (num_predict=50, prod) | 11.0 s | `{"bbox_2d":[422,604,462,624]}` | oui | non (cx ≈ 0.17, top-left) |
| qwen3-vl:8b Ollama (params prod stricts) | 8.0 s | vide (50 tokens absorbés par thinking) | non | n/a |
| qwen3-vl:8b Ollama (think:false, num_predict=256) | 1.7 s | liste nue `[332,487,362,507]` | non (regex attend `"bbox_2d":[...]`) | n/a |
| qwen3-vl:8b Ollama (prompt JSON explicite) | 1.8 s | `{"bbox_2d":[...]}` | oui | non (même bug d'échelle) |
## 3. Chemin technique cible
vLLM ou Transformers direct, avec passage explicite de `resized_width` et `resized_height` au modèle (paramètre supporté par les deux backends), garantissant que les `bbox_2d` retournés sont dans la même résolution que celle qu'on a passée. Le service `core/grounding/server.py` (déjà en place pour InfiGUI via Unix socket, avec `_smart_resize`, `MIN_PIXELS=100*28*28`, `MAX_PIXELS=5600*28*28`) sert de référence architecturale.
Modèle pressenti : `qwen3-vl:8b` (6 GB en VRAM, full GPU possible), avec :
- `think:false` (désactiver le mode thinking par défaut)
- `num_predict >= 128` (50 insuffisant : tokens absorbés par thinking quand activé)
- prompt imposant le format JSON `[{"bbox_2d":[...],"label":"..."}]`
- preprocessing image côté serveur : `_smart_resize` officiel (max_size=1280, multiples de 28, code dans la discussion HF)
## 4. Étapes de migration (5)
1. **[Setup] Choix du backend grounding** : vLLM, Transformers direct, ou réorientation de `core/grounding/server.py` vers Qwen3-VL plutôt que démarrer une 4ème stack. Décision à acter en début de chantier.
2. **[Preprocessing] Implémenter `_smart_resize` côté `resolve_engine.py`** avec la formule officielle (max_size=1280, multiples de 28). Capturer la nouvelle taille (`resized_w`, `resized_h`) et la passer au backend choisi.
3. **[Parser] Adapter les 4 occurrences `bbox_2d` parsing** (`resolve_engine.py:840-848`, 870-880, 908-917, et `_locate_popup_button` L:2576) pour diviser par `resized_w`/`resized_h` et non plus `small_w/h` (= `orig_w/h`). Centraliser dans une seule fonction utilitaire.
4. **[Prompt] Imposer le format JSON `bbox_2d`** dans le prompt envoyé (Qwen3-VL ne le sort pas spontanément, sortie liste nue par défaut). Ajouter `think:false` et `num_predict>=128` aux options.
5. **[Test bbox_2d cible] Refaire le bench du 8 mai** avec la convention contrôlée. Critère de validation : sur le screenshot heartbeat de référence, le bouton OK doit être localisé à un `cx ≈ 0.45-0.55` (mid-screen) et non plus 0.17.
## 5. Test bbox_2d à refaire (post-migration)
Reproduire le test du 8 mai avec :
- Même screenshot fixture : `data/training/live_sessions/bg_DESKTOP-58D5CAC_windows/shots/heartbeat_1773792436.png`
- Même cible : bouton "OK" de la boîte de dialogue
- Backend nouveau (vLLM ou Transformers + `smart_resize` côté client)
Sortie attendue : `bbox_2d` en pixels de l'image redimensionnée connue → division par `resized_w``cx` visuellement ≈ centre du bouton (vérification par overlay sur le screenshot).
## 6. Hors scope du chantier migration (à traiter après)
- Nettoyage de la logique « décharger gemma4 pour qwen2.5vl » dans `agent_v0/server_v1/stream_processor.py:442,1742` — devient inutile une fois qwen2.5vl écarté.
- Nettoyage des 9 mentions/commentaires `qwen2.5vl` dans le code.
- Décision sur le maintien d'une compat fallback Ollama (utile en dev sans GPU) ou abandon complet.
## 7. Risques / points de vigilance
- Si vLLM tourne sur le même GPU que streaming/agent-chat, vérifier que la VRAM tient : qwen3-vl 6 GB + streaming 0.8 GB + agent-chat 0.8 GB = 7.6 GB → marge OK sur 12 GB.
- Le bench du 8 mai montrait des coordonnées visuellement incorrectes MÊME avec un format `bbox_2d` valide. La résolution du bug d'échelle est nécessaire mais peut-être pas suffisante. Vérifier après l'étape 5.
- `_locate_popup_button` (`resolve_engine.py:2536-2585`) est plus simple à migrer que la cascade principale. Faire un POC sur cette fonction avant d'attaquer la cascade.

View File

@@ -0,0 +1,375 @@
# Diagnostic — Replay `replay_free_68ca51ab` bloqué sur l'onglet Imagerie
Date : 8 mai 2026 (matin avant démo GHT Sud 95)
Workflow : `Urgence_aiva_demo` (`wf_a38aeebea5e6_1778162737`)
Replay : `replay_free_68ca51ab` (cancellé manuellement à 10:34, après pause supervisée step 18)
Auteur : ingénieur senior debug RPA / vision
> **TL;DR — il y a deux causes simultanées, l'une explique l'autre.**
>
> 1. **Cause primaire (réseau) :** le client Léa V1 Windows utilise un `read_timeout=5 s` sur `GET /replay/next`. Sur cette même connexion, le serveur exécute parfois un `extract_text` (57 s) PUIS dispatche un `click` dans le même appel. Le client coupe avant la réponse. L'action `click` était déjà *poppée* de la queue serveur (`_retry_pending`), donc **elle est perdue silencieusement** — pas de retry automatique, pas de re-dispatch.
> C'est ce qui s'est passé pour les steps 10, 12, 14 et 17 (clic Imagerie, Notes médicales, Synthèse Urgences, Codage). Le client a poursuivi son polling, mais quand il est revenu à l'écoute, le serveur en était déjà au step 18 (`Coller ou saisir le dossier patient`) — qui n'existe que sur la maquette aiva-vision (`codage.html`), donc échec `target_not_found` → pause supervisée.
>
> 2. **Cause aggravante (vision) :** la cascade `OCR-DIRECT` (`_resolve_by_ocr_text`) renvoie le **centre de la ligne entière** quand le `target_text` n'est qu'un sous-fragment (`score=0.8`). Pour la barre de tabs Easily, docTR détecte les 5 tabs comme une seule ligne. Conséquence : `Imagerie`, `Notes médicales` et `Synthèse Urgences` retournent quasiment les mêmes coordonnées (~0.23, 0.28) — c'est-à-dire le centre de la rangée de tabs (qui tombe sur Imagerie). Même si le client avait reçu chaque action, le clic aurait probablement raté la cible.
---
## 1. Reconstruction temporelle
Workflow `Urgence_aiva_demo` (steps issues de la DB SQLite, ordre 1→22) :
| Order | id (court) | type | label |
|-------|----------------|------------------|-----------------------------------------------|
| 1 | 43ab3c1417d3 | extract_table | Lire liste patients (IPP) |
| 2 | 1dada40f6a44 | pause_for_human | Confirmer démarrage |
| 3 | 288d0bceea90 | click_anchor | Ouvrir dossier MOREL (25003284) |
| 4 | 5388268582d6 | extract_text | Lire Motif d'admission (`t_motif`) |
| 5 | b18e530526bb | keyboard_shortcut| Scroll fin de page (End) |
| 6 | b425b17b37f6 | extract_text | Lire bas de Motif (`t_motif_bas`) |
| 7 | fc4cf0a78b65 | keyboard_shortcut| Retour haut (Home) |
| 8 | 45f5d7fb7456 | click_anchor | Onglet Examens cliniques |
| 9 | 4148c9e8caa4 | extract_text | Lire Examens cliniques (`t_examens`) |
| 10 | 4c0663941f22 | click_anchor | Onglet Imagerie |
| 11 | 93cf4c6651f3 | extract_text | Lire Imagerie (`t_imagerie`) |
| 12 | **3b13c973d737** | click_anchor | **Onglet Notes médicales** |
| 13 | a5840d6bf8ed | extract_text | Lire Notes médicales (`t_notes`) |
| 14 | 8767d8e2e221 | click_anchor | Onglet Synthèse Urgences |
| 15 | 835e5dd54bb7 | extract_text | Lire Synthèse (`t_synthese`) |
| 16 | fc5a9676af55 | t2a_decision | Décision T2A (LLM) |
| 17 | 156d7cd29ebb | click_anchor | Onglet « Codage > » (vers maquette aiva) |
| 18 | 36346c1c40b9 | click_anchor | Cliquer textarea DPI (sur codage.html) |
| ... | | | |
### Logs serveur — `journalctl --user -u rpa-streaming`
Filtrage `replay_free_68ca51ab` + `RESOLVE_*` + `REPORT` + `extract_text`. Extraits pertinents :
```
10:25:46 RESOLVE_ENTRY by_text='Examens cliniques' strict_mode=True screen=2560x1490 has_anchor=True
10:25:48 Strict resolve OCR-DIRECT : OK 'Examens cliniques' → (0.2305, 0.2676) score=0.80
10:25:48 RESOLVE_EXIT resolved=True method='hybrid_text_direct' coords=(0.2305, 0.2676) score=0.8
10:25:49 REPORT step_45f5d7fb7456 success=True actual_position=(0.2305, 0.2798)
10:25:55 extract_text → variable 't_examens' (1689 chars)
10:25:55 DISPATCH action_id=step_4c0663941f22 (click) by_text='Imagerie' ← client a déjà timeout
10:26:01 extract_text → variable 't_imagerie' (1084 chars)
10:26:01 DISPATCH action_id=step_3b13c973d737 (click) by_text='Notes médicales' ← perdu
10:26:08 extract_text → variable 't_notes' (1084 chars)
10:26:08 DISPATCH action_id=step_8767d8e2e221 (click) by_text='Synthèse Urgences' ← perdu
10:26:17 extract_text → variable 't_synthese' (1084 chars)
10:26:27 t2a_decision → variable 'dec' decision=FORFAIT_URGENCE (10.0s)
10:26:27 DISPATCH action_id=step_156d7cd29ebb (click) by_text='Codage' ← perdu (concurrence de polls)
10:26:27 DISPATCH action_id=step_36346c1c40b9 (click) by_text='Coller ou saisir le dossier patient'
10:26:28 RESOLVE_ENTRY by_text='Coller ou saisir le dossier patient' strict_mode=True
10:26:30 Strict resolve OCR-DIRECT : 'Coller ou saisir le dossier patient' non trouvé, passage VLM
10:26:36 RESOLVE_EXIT resolved=False method='strict_vlm_template_failed'
10:28:47 REPORT step_36346c1c40b9 success=False error='target_not_found' warning='visual_resolve_failed'
10:34:00 Replay annulé manuellement
```
**Ce qui ne figure PAS dans les logs serveur** : aucun `RESOLVE_ENTRY` pour `by_text='Imagerie'`, `'Notes médicales'`, `'Synthèse Urgences'` ou `'Codage'` côté replay live. La cascade de résolution n'a JAMAIS été appelée pour ces tabs. → Le client n'a jamais frappé `/resolve_target` ni reçu l'action.
### Logs client — `C:\rpa_vision\agent_debug.log`
```
10:25:44.710 Action de replay recue : click (id=step_45f5d7fb7456 — Examens cliniques)
10:25:47.448 Server resolve OK [hybrid_text_direct] score=0.80
10:25:48.008 Replay click [VISUAL] : (0.230, 0.280) -> (590, 447) sur (2560x1600)
10:25:48.324 Ecran change apres ~200ms
10:25:48.537 Resultat rapporte : replay_status=running, restant=14
10:25:53.771 WARNING : HTTPConnectionPool(host=192.168.1.40, port=5005): Read timed out (read timeout=5)
10:25:53.771 Replay termine - retour en mode capture
10:25:53.780 shared_state Replay termine
← 33 s de silence
10:26:26.608 Action de replay recue : click (id=step_36346c1c40b9 — Coller ou saisir...)
10:26:35.409 Server resolve échoué : vlm_and_template_all_failed
10:26:39.096 Server resolve échoué : no_target_criteria
10:26:44.178 Server resolve échoué : vlm_and_template_all_failed
10:26:45.585 ERROR [LEA] Léa a besoin d'aide: Je n'y arrive pas (« Coller ou saisir... »)
10:28:45.762 [APPRENTISSAGE] Timeout global → 0 actions capturées
10:28:46.231 Replay termine
```
→ Confirmation directe : le client a sauté **9 actions serveur+visuelles** entre l'OK Examens cliniques (10:25:48) et la réception de step 18 (10:26:26).
---
## 2. Diagnostic causal
### Chaîne de responsabilité
```
+---------------------------------------------------------------------------+
| Hyp #1 (cascade serveur foire) — INFIRMÉE |
| La cascade serveur n'est même jamais invoquée pour ces 4 tabs. |
| |
| Hyp #2 (cascade locale Léa V1 prend le relais) — INFIRMÉE |
| Le client n'a pas reçu d'action → rien à résoudre localement. |
| |
| Hyp #3 (coords brutes du record obsolètes) — INFIRMÉE |
| L'ancre `anchor_0438bd2d9bdd_1778161174` (« Notes médicales ») a |
| bbox (444, 424, 146, 48) qui dans l'image de référence pointe sur |
| « Imagerie » et NON Notes médicales (le crop le confirme : |
| /tmp/anchor_0438bd2d9bdd_1778161174_bbox.png montre « Imagerie »). |
| Pareil pour anchor_6a2591e7c51c (« Synthèse Urgences ») dont la |
| bbox (580, 423, 192, 47) crop « Notes médicales ». |
| → Les bboxes des tabs sont décalées d'un cran à gauche dans la DB, |
| mais ce n'est PAS la cause du blocage actuel : le mode strict + OCR- |
| DIRECT ignore la bbox et part de by_text. Anomalie cosmétique à |
| nettoyer hors-démo. |
| |
| Hyp #4 (offset écran live vs record) — PARTIELLEMENT VRAIE |
| Voir §3. |
| |
| Hyp #5 (event onclick JS) — INFIRMÉE |
| Voir §3. |
| |
| Hyp #6 (cache client/serveur) — INFIRMÉE |
| Aucun `from_memory=True` dans les logs ; TargetMemoryStore pas hit. |
| |
| Cause primaire = HTTP TIMEOUT 5 s côté client |
| + actions serveur lentes (extract_text 5-7 s, t2a_decision 10 s) |
| + pas de watchdog d'orphelins dans `_retry_pending` |
| |
| Cause aggravante = OCR-DIRECT center-of-line bug |
| score=0.8 → coords = centre de la ligne docTR entière, pas du span. |
+---------------------------------------------------------------------------+
```
### Mécanique exacte du timeout
`agent_v0/server_v1/api_stream.py` (`get_next_action`, lignes 2816-3083) :
1. Acquiert `_replay_lock` avec `acquire_timeout=4.5 s`. Sinon retourne `{server_busy: True}`**OK**.
2. Une fois le lock pris, boucle `while queue:` qui exécute toutes les actions « serveur » (`extract_text`, `extract_table`, `t2a_decision`, `pause_for_human` non bloquant) **dans le même appel HTTP**, jusqu'à tomber sur une action visuelle (`click`/`type`/`key_combo`) qu'il dispatch et retourne.
3. `extract_text` est wrappé dans `loop.run_in_executor(...)` (timeout 180 s) pour ne pas bloquer l'event loop FastAPI — bon design.
4. **Mais le client appelle ce endpoint avec `timeout=5` (executor.py:1786).** Si la chaîne `extract_text + dispatch_click` prend plus de 5 s, la réponse arrive après que le client ait fermé sa socket. La réponse contient le `click` action et est perdue.
5. Côté serveur (ligne 3209-3224), l'action est déjà *poppée* de la queue et stockée dans `_retry_pending[action_id]` au moment du dispatch. Pas de retry automatique tant que le client ne renvoie pas un report (qui ne viendra pas).
6. Le client repasse en `_replay_active=False` (`main.py:331`) — *cosmétique* — puis continue de poller. Au poll suivant, la queue est passée à l'action suivante (`extract_text`), idem boucle.
**Aucun watchdog ne ré-énonce `_retry_pending` au client.** L'unique chemin pour récupérer une action perdue serait que le client envoie un report avec `success=False` (jamais le cas ici puisqu'il n'a pas reçu l'action).
### Mécanique exacte de l'OCR-DIRECT center-of-line
`agent_v0/server_v1/resolve_engine.py:1447-1527` :
```python
# 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
```
Quand docTR voit la barre de tabs Easily comme **une seule ligne** : `"Motif d'admission Examens cliniques Imagerie Notes médicales Synthèse Urgences Codage >"`, il retourne pour CHAQUE `target` qui est un sous-fragment de cette ligne **le centre de la rangée entière** (~50 % en x, ~28 % en y). Les coords ne dépendent pas du tab demandé.
Preuve archivée dans les logs e2e_singleshot du 8 mai à 09:58 (single screenshot, donc résultats reproductibles) :
- `'Imagerie'``(0.2305, 0.2805)` score 0.8
- `'Notes médicales'``(0.2285, 0.2805)` score 0.8
- `'Synthèse Urgences'``(0.2285, 0.2805)` score 0.8
- (delta ~5 px, trois tabs visuellement à 4-5 cm d'écart les uns des autres)
→ Si le client avait reçu les 4 actions, il aurait cliqué 4 fois quasiment au même endroit (vers Imagerie). Bug latent indépendant du timeout.
---
## 3. Vérification des hypothèses 4 et 5
### Hyp #4 — offset écran live vs record
Géométrie réelle du rendu Easily Assure dans une fenêtre Edge fullscreen 2560×1600 :
- Edge title bar : ~40 px (offset_y de la fenêtre = 49 d'après le log « Grounding contraint à la fenêtre : 2560x1490 (0, 49) »)
- Edge tabs/URL/bookmarks : ~250 px
- `.app-header` Easily (bleu) : 36 px (padding 8 + font 18)
- `.menu-bar` (Patients/Planning/...) : 32 px
- `.patient-banner` (IPP MOREL...) : ~50 px
- `.tabs` (Motif/Examens/...) : 36 px (height CSS) → **y range ≈ 410-450 dans l'image 2560x1600**
Le crop de référence (ancre `anchor_0438bd2d9bdd_1778161174`) à y=420-480 montre exactement la rangée de tabs (cf. `/tmp/tabs_row_full.png`). Pas d'offset majeur entre record et live. Une éventuelle dérive ±10-30 px est gérable par un click au pixel central.
**Hypothèse 4 partiellement vraie** : il y a effectivement un offset, mais il n'est pas la cause du bug. Et il est dégradé par le bug OCR-DIRECT center-of-line (cause #2) puisque le centre de la ligne tombe au milieu de la barre, pas sur le tab demandé.
### Hyp #5 — event onclick JS de la maquette
`docs/clients/ght_sud_95/mockup_easily_assure/dossier.html:36-43` :
```html
<div class="tabs">
<a class="tab active" data-tab="motif">Motif d'admission</a>
<a class="tab" data-tab="examens">Examens cliniques</a>
<a class="tab" data-tab="imagerie">Imagerie</a>
<a class="tab" data-tab="notes">Notes médicales</a>
<a class="tab" data-tab="synthese">Synthèse Urgences</a>
<a class="tab" id="tab-vers-codage" href="codage.html">Codage &gt;</a>
</div>
```
`app.js:377-401` :
```js
function installTabs() {
const tabs = document.querySelectorAll('.tabs .tab[data-tab]');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const target = tab.getAttribute('data-tab');
history.replaceState(null, '', '#' + target + location.search);
activate(target);
window.scrollTo(0, 0);
});
});
}
```
→ Mécanisme propre. `addEventListener('click')` directement sur les `<a class="tab">`. Aucun overlay, aucun event swallow. Un MouseEvent (Win32 SendInput → Windows Edge → DOM) sur le pixel d'un tab DÉCLENCHE le listener. Le tab `Codage` est un lien `href="codage.html"` → navigation native. Aucun problème côté maquette.
**Hypothèse 5 infirmée**.
---
## 4. Reproduction en isolation
Données déjà disponibles via le test e2e_singleshot du 8 mai 09:58 (`session=e2e_singleshot_1778227119_1fe686`), qui appelle `/resolve_target` sur un screenshot fixe (probablement l'onglet Imagerie ouvert) :
| target_spec.by_text | Résolution | x_pct | y_pct | score |
|------------------------------|-----------------------|--------|--------|-------|
| `25003284` | hybrid_text_direct | 0.0312 | 0.3539 | 1.00 |
| `Examens cliniques` | hybrid_text_direct | 0.0610 | 0.3195 | 1.00 |
| `Imagerie` | hybrid_text_direct | 0.2305 | 0.2805 | 0.80 |
| `Notes médicales` | hybrid_text_direct | 0.2285 | 0.2805 | 0.80 |
| `Synthèse Urgences` | hybrid_text_direct | 0.2285 | 0.2805 | 0.80 |
| `Codage` | hybrid_text_direct | 0.1279 | 0.1641 | 0.80 |
| `Coller ou saisir le dossier patient` | hybrid_text_direct | 0.0630 | 0.4125 | 1.00 |
| `Justification de la décision`| template_matching | 0.5000 | 0.5000 | 1.00 |
→ Reproduction confirmée : trois tabs (Imagerie / Notes / Synthèse) renvoyés à coords pratiquement identiques. Le test du 10:01 (1920x1080) reproduit la même chose : `Notes médicales` → (0.2227, 0.1259), `Imagerie` → (0.2256, 0.1267), même row.
Pour reproduire en CLI sans flask :
```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} -> {r}')
"
```
Ce smoke test (offline, ~30 s la 1re fois pour télécharger le modèle docTR) prouve la cause #2 sans dépendre du PC Windows.
---
## 5. Trois correctifs proposés (sans appliquer)
### Quick fix démo (510 min) — **passer le client en `timeout=30` pour `/replay/next`**
Fichier : `agent_v1/core/executor.py:1786` côté Windows.
Changer :
```python
timeout=5,
```
en :
```python
timeout=30,
```
Justification : la borne dure côté serveur est déjà 180 s par action serveur ; le serveur retourne aussi `server_busy=True` au plus tard à 4.5 s. Un timeout client à 30 s laisse passer un `extract_text` de 5-10 s + dispatch d'un click sans couper la connexion. Pas d'effet de bord majeur — au pire le client attend 30 s en cas de mort serveur, déjà couvert par la backoff.
**Effet** :
- Le client ne loupera plus aucun click même si extract_text/t2a_decision est devant.
- Le bug OCR-DIRECT center-of-line reste, mais `Examens cliniques` (score 1.0) et `Codage` (autre ligne) seront correctement résolus → la maquette aiva-vision finira par s'afficher.
- Imagerie/Notes/Synthèse cliqueront tous les trois sur le centre de la rangée (en pratique au-dessus d'Imagerie). C'est cosmétiquement faux mais **t_imagerie/t_notes/t_synthese seront tous identiques** ; il faut prévenir Amina qu'on n'aura qu'une seule lecture du DPI multi-onglets.
**Risque** : très bas. Modifier un seul littéral. Redéploiement SSH du fichier executor.py.
### Quick fix démo bis (1015 min) — **boost OCR-DIRECT pour ne renvoyer que le centre du span matché**
Fichier : `agent_v0/server_v1/resolve_engine.py:1486-1519`.
Idée : quand le score est 0.8 (substring match) ou 0.9 (mot exact dans la ligne), recalculer `cx, cy` à partir des **bboxes des words** qui composent le `target_text`, pas de la ligne entière.
Pseudo-patch (à appliquer après-démo) :
```python
elif target_lower in line_lower:
score = 0.8
# Recalculer la bbox du span uniquement
matched_words = [w for w in line_obj.words if w.value.lower() in target_lower]
if matched_words:
xs = [pt[0] for w in matched_words for pt in w.geometry]
ys = [pt[1] for w in matched_words for pt in w.geometry]
cx = (min(xs) + max(xs)) / 2
cy = (min(ys) + max(ys)) / 2
```
**Effet** : chaque tab résolu à son propre centre, plus de collision.
**Risque** : moyen — il faut tester avec docTR pour vérifier que les `geometry` des words sont normalisées dans le même repère que celui de la ligne. Possible que le nettoyage du substring matching soit tordu par les accents/casse. À NE PAS appliquer à la chaude pour la démo, mais pour le runner 2.
> **Combo conseillé pour la démo** : appliquer SEULEMENT le fix #1 (timeout 30 s). Le bug center-of-line tab fait que t_imagerie/t_notes/t_synthese seront tous = même contenu (Imagerie). Si Amina utilise déjà `t_imagerie t_notes t_synthese` dans le prompt T2A, ça reste exploitable (juste moins de variété). Le clic Codage > marchera (autre ligne docTR).
### Fix moyen terme (3060 min) — **watchdog `_retry_pending` côté serveur**
Ajout d'une boucle background dans `api_stream.py` qui scanne `_retry_pending` toutes les 10 s et :
- Si une action a été dispatchée il y a > 30 s sans `REPORT` → la repush en tête de queue (avec un `_resent=True` flag pour stats).
- Émission `[BUS] lea:dispatch_orphan_resent`.
Justification : aujourd'hui une action perdue (timeout, kill client, déconnexion réseau) est perdue silencieusement. C'est un trou de fiabilité indépendamment de la démo GHT. Le watchdog garantit la reprise sans intervention manuelle.
**Risque** : moyen — il faut bien gérer la concurrence avec le client qui pourrait finalement renvoyer le report tardivement. Idempotence des reports déjà gérée dans `report_action_result` (line 3356 : `_retry_pending.pop(action_id)`), donc resend = réponse éventuelle ignorée.
### Fix structurel (post-démo, refonte) — **Server-Sent Events (SSE) ou WebSocket pour le push d'actions**
Le pattern « pull avec long poll 5 s » est intrinsèquement fragile dès que les étapes serveur sont imprévisibles. Solutions architecturales :
1. **SSE** (`text/event-stream`) : connexion persistante, le serveur push chaque action quand prête. Pas de timeout client à régler. Reconnexion automatique gérée nativement par EventSource. Plus simple à implémenter que WebSocket en FastAPI.
2. **WebSocket** : full duplex, idéal pour heartbeat + actions + monitoring. Plus de code mais futur-proof.
3. **HTTP/2 server push + chunked responses** : entre les deux. Pas standard côté requests Python.
Bénéfices :
- Suppression du bug timeout pour de bon.
- `_retry_pending` devient quasi inutile (push ack-based).
- Réduction du trafic (pas de poll inutile ~1/s).
- Détection immédiate de déconnexion client → déclenche pause supervisée serveur.
Coût : 1-2 jours dev + tests E2E.
---
## 6. Notes annexes (à nettoyer hors démo)
1. **Anomalie d'ancrage DB** : les ancres `anchor_0438bd2d9bdd_1778161174` (Notes médicales label) et `anchor_6a2591e7c51c_1778229076` (Synthèse Urgences label) ont des bboxes pointant un cran à gauche du tab nommé. Ce n'est pas la cause du bug (mode strict + OCR-DIRECT bypass la bbox) mais c'est trompeur en debug. À reposer en VWB record session post-démo.
2. **`target_text` mal-OCRisé en DB** : le champ `target_text` de l'ancre Notes médicales contient `"ine Né(e) le 14/03/1947 I 77 ans es Imagerie Notes médical J scan, echograj phie"`. C'est un OCR brut de la zone capturée — utile en debug, à ne pas confondre avec un identifiant fiable.
3. **Pré-check OCR post-cascade désactivé** (`RPA_ENABLE_TEXT_PRECHECK=false`) : pour la démo c'est OK. Mais à activer post-démo car il aurait peut-être attrapé le cas (clic sur centre de rangée = OCR autour ne voit pas exactement le by_text demandé). À recalibrer (radius_px et min_token_ratio) pour ne pas faux-rejeter sur les tabs à 2 tokens.
4. **Pas de `RESOLVE_ENTRY` dans les logs serveur du replay live pour les tabs perdus** : confirme que `/resolve_target` n'est PAS appelé tant que le client n'a pas reçu l'action. Aucun chemin caché côté serveur.
5. **Concurrence de polls vue à 10:26:27** : deux DISPATCHes en 0.6 s pour 2 polls quasi-simultanés. C'est cohérent avec deux requêtes en attente sur l'acquire lock + une qui retourne `server_busy` puis une qui acquiert. Le bug fundamental reste le timeout client trop court, pas la concurrence.
---
## Synthèse (≤ 400 mots)
Le replay s'est bloqué non pas à cause d'un échec de résolution visuelle, mais à cause d'une **désynchronisation client-serveur silencieuse**.
À 10:25:48, le client Léa Windows a cliqué avec succès « Examens cliniques » et reporté `success=True`. Cinq secondes plus tard (10:25:53.771), il poste un nouveau `GET /replay/next` qui timeout à 5 s — parce que côté serveur l'appel commence par `extract_text` (~57 s pour récupérer `t_examens` 1689 chars) puis dispatche le click `Imagerie`. Le serveur a déjà *poppé* l'action de la queue et stocké dans `_retry_pending`, mais la réponse HTTP arrive après que le client ait fermé sa socket. **L'action est perdue.** Aucun watchdog côté serveur ne la republie. Le client repasse en mode capture cosmétique mais continue à poller. Pendant 33 s, à chaque /next il aspire de l'action serveur (extract Imagerie, dispatch Notes — perdu, extract Notes, dispatch Synthèse — perdu, extract Synthèse, t2a_decision 10 s, dispatch Codage — perdu) jusqu'à recevoir directement step 18 (« Coller ou saisir le dossier patient ») qui n'existe que sur la maquette `codage.html`. Échec `target_not_found` → pause supervisée → l'utilisateur cancel.
C'est pour ça que `t_examens`, `t_imagerie`, `t_notes`, `t_synthese` ont tous le même contenu (1689 puis 1084 chars répétés) : l'écran n'a jamais changé d'onglet ; le DPI envoyé à T2A est mutilé.
Bug aggravant **indépendant** : `_resolve_by_ocr_text` (resolve_engine.py:1447) renvoie le **centre de la ligne docTR entière** quand le `target_text` est un sous-fragment (score 0.8). docTR détecte la barre de tabs comme une ligne unique → Imagerie/Notes/Synthèse renvoient tous (0.23, 0.28). Confirmé par le test e2e_singleshot du même jour à 09:58. Même si le client recevait les actions, le clic raterait la cible. Latent dès que plusieurs tokens cibles partagent la même ligne docTR.
**Recommandation démo** : passer le `read_timeout` client de 5 s à 30 s (`agent_v1/core/executor.py:1786`). Quick win, zéro risque, suffit pour que le pipeline aboutisse à `codage.html` et que la maquette aiva-vision se remplisse. Accepter pour la démo que les 3 tabs Imagerie/Notes/Synthèse cliqueront tous au centre de la rangée (le DPI multi-onglets sera dégradé mais le t2a_decision restera exploitable car `t_motif` et `t_motif_bas` portent l'essentiel du diagnostic).
**Priorité post-démo** : (1) watchdog `_retry_pending`, (2) fix OCR-DIRECT center-of-span, (3) refonte SSE/WebSocket.

View File

@@ -0,0 +1,174 @@
# UX Fix — bulle pause Léa (1 affichage, scroll, boutons fonctionnels)
**Date** : 2026-05-08 (matin, J démo GHT Sud 95)
**Branche** : `feature/qw-suite-mai`
**Périmètre** : `agent_v0/agent_v1/` uniquement (client Windows). Pas de modif serveur, pas de modif VWB.
## 1. Constat utilisateur
Verbatim Dom (chat avant démo) :
> "Pour le thinker, il y en a 3 maintenant ! un en haut à droite de l'écran, l'autre dans le chat (qui n'a pas de scroll donc message tronqué) puis dans le vwb. Je précise également que les boutons annulé sur toutes les questions ne fonctionnent pas !"
Quatre bugs UX simultanés au déclenchement d'un `pause_for_human` :
1. **Trois affichages parallèles** — toast Tkinter `paused_toast` en haut à droite, bulle dans la fenêtre chat Tkinter, popup PauseDialog dans le frontend VWB React.
2. **Message tronqué dans la bulle chat** — pas de scroll interne, donc un `pause_message` long (>200 chars, fréquent côté serveur) déborde de la fenêtre.
3. **Bouton "Annuler" inopérant** — clic sans effet visuel.
4. **Bouton "Continuer" sans feedback** — pas de plainte explicite mais aucune confirmation visuelle après clic.
## 2. Diagnostic technique
### Bug 1 — 3 toasts paused_toast en parallèle
`grep -rn "show_paused_toast" agent_v0/agent_v1/` a remonté **trois call sites** qui se déclenchent en cascade pour la même pause :
| # | Fichier | Ligne | Contexte |
|---|---|---|---|
| (a) | `core/executor.py` | 1831 | Plan B polling — `replay_paused=True` détecté |
| (b) | `ui/chat_window.py` | 860 | `_add_paused_bubble` appelait aussi le toast en complément |
| (c) | `ui/notifications.py` | 156 | `notify_message(BLOCAGE)` déclenché en parallèle par `executor` (mécanisme legacy) |
Chacun des trois chemins force le toast topmost ⇒ Dom voit 3 popups + 1 bulle = 4 éléments UI. Confirmé dans `agent_debug.log` Windows :
```
2026-05-08 10:24:37,750 [paused_toast] INFO: paused_toast scheduled on existing Tk root
2026-05-08 10:26:45,586 [paused_toast] INFO: paused_toast scheduled on existing Tk root
2026-05-08 10:28:46,217 [paused_toast] INFO: paused_toast scheduled on existing Tk root
```
Plus la PauseDialog du VWB côté Linux (non concernée par la démo Windows mais visible si on ouvre le navigateur).
### Bug 2 — bulle non scrollable
La fenêtre chat dispose déjà d'un Canvas + Scrollbar global (`_build_messages_area`, ligne 464-507). Mais `_render_paused_bubble` n'appelait pas `_scroll_to_bottom()` après le `pack()`, donc une bulle insérée en bas de la zone restait potentiellement masquée. Et le contenu de la bulle (`reason`) était rendu via un `tk.Label` à `wraplength` fixe : pas de scroll interne pour les messages très longs (>300 chars).
### Bug 3 — bouton "Annuler" inopérant
Lecture de `_on_paused_abort` (ligne 975) :
```python
self._bus.abort_replay(replay_id) # émet lea:replay_abort
if self._active_paused_bubble:
self._active_paused_bubble["btn_resume"].config(state="disabled")
self._active_paused_bubble["btn_abort"].config(state="disabled")
```
Le bus émet bien `lea:replay_abort`. Côté serveur (`agent_chat/app.py:1720`), le handler met `execution_status["running"] = False` et émet `lea:abort_acked`. Mais :
- `_on_lea_event` ligne 768 ignore explicitement `lea:abort_acked` (silencieux côté UI).
- Aucun `lea:resumed` n'est émis pour un abort (ce serait sémantiquement faux).
- Donc `_close_active_paused_bubble` n'est jamais déclenché ⇒ la bulle reste affichée avec ses boutons disabled, sans aucun message de fermeture. Pour Dom, "rien ne se passe".
### Bug 4 — bouton "Continuer" sans feedback immédiat
Même mécanisme : le clic émet `lea:replay_resume`, le serveur relaie en POST HTTP vers le streaming server. La fermeture de la bulle ne survient qu'à la réception ultérieure de `lea:resumed` (plusieurs secondes plus tard). Pas de feedback sur le clic lui-même.
## 3. Solution implémentée
### 3.1 Un seul affichage canonique : la bulle chat
`core/executor.py` — Plan B simplifié :
```python
# UX fix 8 mai 2026 : un seul affichage. La bulle ChatWindow EST l'affichage
# canonique (force show + topmost + bell sonore). Plus de paused_toast en double.
chat_window = getattr(self, "_chat_window_ref", None)
if chat_window is not None:
chat_window._add_paused_bubble(payload)
else:
# Fallback headless / tests : toast Tkinter custom
from ..ui.paused_toast import show_paused_toast
show_paused_toast(title=..., message=pause_msg[:300])
```
`ui/chat_window.py:_add_paused_bubble` — suppression du `show_paused_toast` en complément, remplacé par un `root.bell()` natif Tkinter + `attributes("-topmost", True)` + `lift()` pour la mise au premier plan.
`ui/notifications.py:notify_message` — suppression du `show_paused_toast` BLOCAGE (devenu redondant). Plyer reste actif comme notification système Windows discrète.
### 3.2 Scroll dans la bulle pour messages longs
`_render_paused_bubble` — remplacement du `tk.Label` par un `tk.Text` read-only avec hauteur calculée dynamiquement (2-8 lignes selon longueur), et scrollbar interne au-delà de 280 caractères :
```python
approx_lines = max(2, min(8, (len(reason_str) // 60) + 1))
reason_text = tk.Text(msg_frame, height=approx_lines, wrap=tk.WORD, ...)
reason_text.insert("1.0", reason_str)
reason_text.configure(state="disabled")
if len(reason_str) > 280:
reason_scroll = tk.Scrollbar(msg_frame, command=reason_text.yview, ...)
reason_text.configure(yscrollcommand=reason_scroll.set)
```
Ajout d'un appel `self._scroll_to_bottom()` à la fin de `_render_paused_bubble` ET de `_render_action_bubble` pour que la bulle apparaisse toujours dans la zone visible.
### 3.3 Fermeture immédiate sur Annuler + feedback visuel
`_on_paused_abort` :
```python
emitted = self._bus.abort_replay(replay_id) if (self._bus and self._bus.connected) else False
self._disable_paused_buttons()
self._update_paused_feedback("✗ Annulé" if emitted else "✗ Annulé (bus indisponible)")
self._close_active_paused_bubble(reason="abort_local") # NEW : fermeture locale immédiate
```
`_on_paused_resume` : même structure avec feedback `"→ Reprise demandée…"`. La bulle reste visible avec boutons disabled jusqu'à réception de `lea:resumed` qui déclenche `_close_active_paused_bubble("lea:resumed")`.
Helpers ajoutés : `_disable_paused_buttons()` et `_update_paused_feedback(text)`. Un `feedback_label` (label vide) est intégré dans la bulle au render et mis à jour à chaque clic.
## 4. Test isolé
Script ajouté : `agent_v0/agent_v1/tools/test_lea_pause_flow.py` (déployé `C:\rpa_vision\agent_v1\tools\`).
Commande exacte sur le PC Windows :
```cmd
cd C:\rpa_vision
.venv\Scripts\python.exe -m agent_v1.tools.test_lea_pause_flow
```
Le script ouvre une ChatWindow, simule un `paused_need_help` avec un message de 350 chars (« Je n'arrive pas à trouver le champ Numéro de dossier patient... »), et garde la fenêtre ouverte 30s pour validation visuelle. Vérifications attendues :
1. **UN SEUL popup** (la bulle chat dans la fenêtre Léa, pas de toast Tkinter en plus).
2. Message long visible avec scroll interne si débordement.
3. Boutons Continuer / Annuler fonctionnels.
4. Clic Annuler ⇒ bulle fermée + feedback `✗ Annulé`.
## 5. Tests automatisés exécutés
```bash
$ pytest tests/unit/test_lea_notifications.py
101 passed in 0.69s
$ pytest tests/integration/test_chat_window_templates.py tests/integration/test_feedback_bus_client.py
35 passed
```
Aucune régression. Les tests existants vérifient `notify_message(BLOCAGE)` retourne True — le retour reste True via `notify(...)` (le toast en complément a juste été retiré).
## 6. Déploiement Windows
| Fichier | MD5 Linux | MD5 Windows | Match |
|---|---|---|---|
| `agent_v1/ui/chat_window.py` | `50597f1f7531ab8e15fdc91e3a03e98a` | identique | OK |
| `agent_v1/ui/notifications.py` | `8382ce3cbbc819af0e1a25fc708a0596` | identique | OK |
| `agent_v1/core/executor.py` | `dfec3a9da28ef44019fd705404d670a5` | identique | OK |
| `agent_v1/tools/test_lea_pause_flow.py` | `edd66b613430d10e1fce8c50f478c90c` | identique | OK |
Cache `__pycache__` purgé sur Windows :
```powershell
Get-ChildItem -Recurse -Path C:\rpa_vision\agent_v1 -Include *.pyc | Remove-Item -Force
Get-ChildItem -Recurse -Path C:\rpa_vision\agent_v1 -Include __pycache__ -Directory | Remove-Item -Recurse -Force
```
**Action restante avant démo** : redémarrer l'agent Léa V1 sur le PC Windows (le client doit recharger les modules). Procédure standard : tray icon Léa → Quitter, puis `Win+R``C:\rpa_vision\start_lea.cmd` (ou équivalent dans la doc reference_windows_pc.md).
## 7. Synthèse fichiers modifiés
- `agent_v0/agent_v1/ui/chat_window.py` : `_add_paused_bubble`, `_render_paused_bubble`, `_on_paused_resume`, `_on_paused_abort` + helpers `_disable_paused_buttons`, `_update_paused_feedback`. Auto-scroll ajouté à `_render_action_bubble` aussi.
- `agent_v0/agent_v1/ui/notifications.py` : `notify_message` — suppression du déclenchement `show_paused_toast` BLOCAGE.
- `agent_v0/agent_v1/core/executor.py` : Plan B polling — suppression du `show_paused_toast` direct, remplacé par fallback uniquement si `_chat_window_ref` est None.
- `agent_v0/agent_v1/tools/test_lea_pause_flow.py` : nouveau script de smoke test.
Aucun fichier serveur ni VWB modifié. Conforme `feedback_agent_frozen.md` (modif client validée par Dom ce matin).