From f2e9aac6b703bf0e4c02cdae79893485b0f278c5 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 2 Jun 2026 16:28:34 +0200 Subject: [PATCH] docs: add POC specs, handoffs, and research notes --- docs/AUDIT_20260404.md | 20 +- ...INALIZE_CONTRACT_INTEGRATION_2026-05-20.md | 100 + docs/AUDIT_LEA_FIRST_RUNTIME_2026-05-19.md | 320 +++ ...DIT_WINDOW_TITLE_MEMORY_PATH_2026-05-19.md | 109 + docs/CR_AUDIT_PAUSED_RESUME_BUS_2026-05-22.md | 132 ++ ...CR_AUDIT_SETUP_VISUAL_GUARDS_2026-05-22.md | 154 ++ docs/HEALTHCHECK_LEA_STACK_2026-05-25.md | 76 + docs/LESSONS_LEARNED_GHT_2026-05.md | 233 +++ .../OLLAMA_MODEL_STORE_INCIDENT_2026-05-25.md | 93 + .../AUDIT_CHAINE_APPRENTISSAGE_2026-06-01.md | 268 +++ docs/POC/AUDIT_TOKEN_PAR_POSTE_2026-06-01.md | 729 +++++++ ...QUIREMENTS_DGX_AARCH64_DRAFT_2026-06-01.md | 313 +++ ...PECS_AGENT_CHAT_LEARN_ACTION_2026-06-01.md | 240 +++ docs/POC/SPECS_ENDPOINT_PERSIST_2026-06-01.md | 210 ++ .../SPECS_PHASE_25_SEMANTIQUE_2026-06-01.md | 169 ++ docs/SMOKE_TEST_FINALIZE_REPLAY_2026-05-20.md | 158 ++ docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md | 393 ++++ ...HIE_BRIQUES_MANDAT_PROTOCOLS_2026-05-25.md | 474 +++++ ...PHIE_MICRO_APPRENTISSAGE_LEA_2026-05-27.md | 99 + .../MODELE_MANDAT_PROTOCOLS_LEA_2026-05-25.md | 609 ++++++ ...LE_MANDAT_PROTOCOLS_LEA_2026-05-25_v0.2.md | 953 +++++++++ ...COLS_LEA_2026-05-25_v0.3_ARBITRAGES_DOM.md | 146 ++ .../misc/agent/AGENT_AUTHENTICATION_FIX.md | 14 +- .../AGENT_UPLOAD_ENCRYPTION_FIX_COMPLETE.md | 6 +- .../agent/AGENT_V0_AUTHENTICATION_STATUS.md | 6 +- docs/demo/test-humain-batch1.md | 164 ++ docs/demo/test-humain-e2e-poc.md | 554 ++++++ docs/guides/GUIDE_REPARATION_RAPIDE.md | 4 +- ...05-19_handoff_fix_capture_monitor_guard.md | 120 ++ .../2026-05-19_handoff_post_demo_GHT.md | 113 ++ ...05-20_handoff_claude_horizon1_lea_first.md | 138 ++ .../2026-05-20_handoff_horizon1_lea_first.md | 142 ++ ...6-05-23_handoff_codex_lea_replay_resume.md | 100 + ...ff_codex_leabench_grounding_supervision.md | 142 ++ ...5_handoff_claude_phase2_notepad_success.md | 127 ++ ...25_handoff_codex_notepad_success_phase2.md | 261 +++ ...ex_demo_aiva_urgence_repetition_humaine.md | 262 +++ ...apprentissage_lea_p0_reprise_2026-05-28.md | 141 ++ ...-01_handoff_codex_fin-soir-poc-bi-turbo.md | 468 +++++ ..._handoff_codex_p0-p1-lea-session-propre.md | 128 ++ ...off_qwen_fin_session_reprise_2026-06-02.md | 160 ++ .../PROMPT_REPRISE_CLAUDE_2026-05-23.md | 16 + .../PROMPT_REPRISE_CLAUDE_2026-05-25_SOIR.md | 56 + .../PROMPT_REPRISE_CLAUDE_2026-05-26_SOIR.md | 105 + .../PROMPT_REPRISE_CLAUDE_2026-05-28_MATIN.md | 70 + .../PROMPT_REPRISE_CODEX_2026-05-23.md | 23 + .../PROMPT_REPRISE_CODEX_2026-05-25.md | 19 + .../PROMPT_REPRISE_CODEX_2026-05-25_SOIR.md | 140 ++ .../PROMPT_REPRISE_GEMINI_2026-05-25.md | 114 ++ .../PROMPT_REPRISE_GEMINI_2026-05-25_SOIR.md | 33 + .../PROMPT_REPRISE_GEMINI_2026-05-26_MATIN.md | 85 + .../PROMPT_REPRISE_QWEN_2026-05-26_HANDOFF.md | 161 ++ .../PROMPT_REPRISE_QWEN_2026-05-26_SOIR.md | 208 ++ .../BACKLOG_HORIZON1_LEA_FIRST_2026-05-19.md | 257 +++ .../CADRAGE_COEUR_PRODUIT_LEA_2026-05-19.md | 313 +++ ...XECUTABLE_PIPELINE_LEA_FIRST_2026-05-19.md | 403 ++++ ...E_DE_ROUTE_PRODUIT_LEA_FIRST_2026-05-19.md | 307 +++ ..._ACTION_COLLEGES_2026-05-25_APRES_C1_G2.md | 111 ++ ...HASE2_TRACE_MANDAT_PROTOCOLS_2026-05-25.md | 274 +++ .../PLAN_REMISE_EN_ORDRE_POCS_2026-05-19.md | 217 ++ .../PLAN_STABILISATION_DEMO_2026-06-01.md | 67 + ...NCE1_MICRO_APPRENTISSAGE_LEA_2026-05-27.md | 200 ++ docs/recherche/AXE_A1_VLM_GROUNDING_SOTA.md | 402 ++++ docs/recherche/AXE_A2_SMART_RESIZE_BBOX.md | 759 +++++++ docs/recherche/AXE_A3_BENCH_PROTOCOL.md | 895 +++++++++ docs/recherche/AXE_A4_OCR_TEMPLATE_PHASH.md | 487 +++++ docs/recherche/AXE_A5_SCREEN_TOKENIZATION.md | 318 +++ docs/recherche/AXE_B1_DEEP_WATCHDOG.md | 1117 +++++++++++ docs/recherche/AXE_B1_REPLAY_TRANSPORT.md | 507 +++++ docs/recherche/AXE_B2_DEEP_VALIDATOR.md | 1350 +++++++++++++ docs/recherche/AXE_B2_VALIDATOR_PATTERN.md | 817 ++++++++ docs/recherche/AXE_B4_ORA_VS_REPLAY.md | 259 +++ docs/recherche/AXE_B5_D1_CAPTURE_REMOTE.md | 453 +++++ docs/recherche/AXE_C_LEARNING_SHADOW.md | 352 ++++ docs/recherche/AXE_D2_DEEP_POPUP_CHAIN.md | 1545 +++++++++++++++ docs/recherche/AXE_D2_DIALOG_POPUP.md | 552 ++++++ docs/recherche/AXE_D4_MULTI_TENANT_DEPLOY.md | 549 +++++ docs/recherche/AXE_E_FRAMEWORKS_BENCHMARKS.md | 408 ++++ ...NDU_ANCRES_VISUELLES_NOTEPAD_2026-05-24.md | 59 + .../INDEX_REPLAY_SPECS_2026-05-24.md | 279 +++ ...NCE1_MICRO_APPRENTISSAGE_LEA_2026-05-27.md | 109 + ...PORT_PILOTAGE_CORE_JUDGE_VLM_2026-05-24.md | 79 + docs/recherche/SPEC_POPUPS_CATALOGUE.md | 1758 +++++++++++++++++ docs/recherche/SPEC_TRANSPORT_CONTRAT.md | 766 +++++++ docs/recherche/SPEC_VALIDATOR_MATRICE.md | 1319 +++++++++++++ docs/specs/Q-P1-agentchat-shadow-spec.md | 274 +++ 86 files changed, 27615 insertions(+), 25 deletions(-) create mode 100644 docs/AUDIT_FINALIZE_CONTRACT_INTEGRATION_2026-05-20.md create mode 100644 docs/AUDIT_LEA_FIRST_RUNTIME_2026-05-19.md create mode 100644 docs/AUDIT_WINDOW_TITLE_MEMORY_PATH_2026-05-19.md create mode 100644 docs/CR_AUDIT_PAUSED_RESUME_BUS_2026-05-22.md create mode 100644 docs/CR_AUDIT_SETUP_VISUAL_GUARDS_2026-05-22.md create mode 100644 docs/HEALTHCHECK_LEA_STACK_2026-05-25.md create mode 100644 docs/LESSONS_LEARNED_GHT_2026-05.md create mode 100644 docs/OLLAMA_MODEL_STORE_INCIDENT_2026-05-25.md create mode 100644 docs/POC/AUDIT_CHAINE_APPRENTISSAGE_2026-06-01.md create mode 100644 docs/POC/AUDIT_TOKEN_PAR_POSTE_2026-06-01.md create mode 100644 docs/POC/REQUIREMENTS_DGX_AARCH64_DRAFT_2026-06-01.md create mode 100644 docs/POC/SPECS_AGENT_CHAT_LEARN_ACTION_2026-06-01.md create mode 100644 docs/POC/SPECS_ENDPOINT_PERSIST_2026-06-01.md create mode 100644 docs/POC/SPECS_PHASE_25_SEMANTIQUE_2026-06-01.md create mode 100644 docs/SMOKE_TEST_FINALIZE_REPLAY_2026-05-20.md create mode 100644 docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md create mode 100644 docs/architecture/CARTOGRAPHIE_BRIQUES_MANDAT_PROTOCOLS_2026-05-25.md create mode 100644 docs/architecture/CARTOGRAPHIE_MICRO_APPRENTISSAGE_LEA_2026-05-27.md create mode 100644 docs/architecture/MODELE_MANDAT_PROTOCOLS_LEA_2026-05-25.md create mode 100644 docs/architecture/MODELE_MANDAT_PROTOCOLS_LEA_2026-05-25_v0.2.md create mode 100644 docs/architecture/MODELE_MANDAT_PROTOCOLS_LEA_2026-05-25_v0.3_ARBITRAGES_DOM.md create mode 100644 docs/demo/test-humain-batch1.md create mode 100644 docs/demo/test-humain-e2e-poc.md create mode 100644 docs/handoffs/2026-05-19_handoff_fix_capture_monitor_guard.md create mode 100644 docs/handoffs/2026-05-19_handoff_post_demo_GHT.md create mode 100644 docs/handoffs/2026-05-20_handoff_claude_horizon1_lea_first.md create mode 100644 docs/handoffs/2026-05-20_handoff_horizon1_lea_first.md create mode 100644 docs/handoffs/2026-05-23_handoff_codex_lea_replay_resume.md create mode 100644 docs/handoffs/2026-05-24_handoff_codex_leabench_grounding_supervision.md create mode 100644 docs/handoffs/2026-05-25_handoff_claude_phase2_notepad_success.md create mode 100644 docs/handoffs/2026-05-25_handoff_codex_notepad_success_phase2.md create mode 100644 docs/handoffs/2026-05-26_handoff_codex_demo_aiva_urgence_repetition_humaine.md create mode 100644 docs/handoffs/2026-05-27_handoff_codex_micro_apprentissage_lea_p0_reprise_2026-05-28.md create mode 100644 docs/handoffs/2026-06-01_handoff_codex_fin-soir-poc-bi-turbo.md create mode 100644 docs/handoffs/2026-06-01_handoff_codex_p0-p1-lea-session-propre.md create mode 100644 docs/handoffs/2026-06-01_handoff_qwen_fin_session_reprise_2026-06-02.md create mode 100644 docs/handoffs/PROMPT_REPRISE_CLAUDE_2026-05-23.md create mode 100644 docs/handoffs/PROMPT_REPRISE_CLAUDE_2026-05-25_SOIR.md create mode 100644 docs/handoffs/PROMPT_REPRISE_CLAUDE_2026-05-26_SOIR.md create mode 100644 docs/handoffs/PROMPT_REPRISE_CLAUDE_2026-05-28_MATIN.md create mode 100644 docs/handoffs/PROMPT_REPRISE_CODEX_2026-05-23.md create mode 100644 docs/handoffs/PROMPT_REPRISE_CODEX_2026-05-25.md create mode 100644 docs/handoffs/PROMPT_REPRISE_CODEX_2026-05-25_SOIR.md create mode 100644 docs/handoffs/PROMPT_REPRISE_GEMINI_2026-05-25.md create mode 100644 docs/handoffs/PROMPT_REPRISE_GEMINI_2026-05-25_SOIR.md create mode 100644 docs/handoffs/PROMPT_REPRISE_GEMINI_2026-05-26_MATIN.md create mode 100644 docs/handoffs/PROMPT_REPRISE_QWEN_2026-05-26_HANDOFF.md create mode 100644 docs/handoffs/PROMPT_REPRISE_QWEN_2026-05-26_SOIR.md create mode 100644 docs/plans/BACKLOG_HORIZON1_LEA_FIRST_2026-05-19.md create mode 100644 docs/plans/CADRAGE_COEUR_PRODUIT_LEA_2026-05-19.md create mode 100644 docs/plans/CARTOGRAPHIE_EXECUTABLE_PIPELINE_LEA_FIRST_2026-05-19.md create mode 100644 docs/plans/FEUILLE_DE_ROUTE_PRODUIT_LEA_FIRST_2026-05-19.md create mode 100644 docs/plans/PLAN_ACTION_COLLEGES_2026-05-25_APRES_C1_G2.md create mode 100644 docs/plans/PLAN_PHASE2_TRACE_MANDAT_PROTOCOLS_2026-05-25.md create mode 100644 docs/plans/PLAN_REMISE_EN_ORDRE_POCS_2026-05-19.md create mode 100644 docs/plans/PLAN_STABILISATION_DEMO_2026-06-01.md create mode 100644 docs/plans/PROTOCOLE_SEANCE1_MICRO_APPRENTISSAGE_LEA_2026-05-27.md create mode 100644 docs/recherche/AXE_A1_VLM_GROUNDING_SOTA.md create mode 100644 docs/recherche/AXE_A2_SMART_RESIZE_BBOX.md create mode 100644 docs/recherche/AXE_A3_BENCH_PROTOCOL.md create mode 100644 docs/recherche/AXE_A4_OCR_TEMPLATE_PHASH.md create mode 100644 docs/recherche/AXE_A5_SCREEN_TOKENIZATION.md create mode 100644 docs/recherche/AXE_B1_DEEP_WATCHDOG.md create mode 100644 docs/recherche/AXE_B1_REPLAY_TRANSPORT.md create mode 100644 docs/recherche/AXE_B2_DEEP_VALIDATOR.md create mode 100644 docs/recherche/AXE_B2_VALIDATOR_PATTERN.md create mode 100644 docs/recherche/AXE_B4_ORA_VS_REPLAY.md create mode 100644 docs/recherche/AXE_B5_D1_CAPTURE_REMOTE.md create mode 100644 docs/recherche/AXE_C_LEARNING_SHADOW.md create mode 100644 docs/recherche/AXE_D2_DEEP_POPUP_CHAIN.md create mode 100644 docs/recherche/AXE_D2_DIALOG_POPUP.md create mode 100644 docs/recherche/AXE_D4_MULTI_TENANT_DEPLOY.md create mode 100644 docs/recherche/AXE_E_FRAMEWORKS_BENCHMARKS.md create mode 100644 docs/recherche/COMPTE_RENDU_ANCRES_VISUELLES_NOTEPAD_2026-05-24.md create mode 100644 docs/recherche/INDEX_REPLAY_SPECS_2026-05-24.md create mode 100644 docs/recherche/JOURNAL_SEANCE1_MICRO_APPRENTISSAGE_LEA_2026-05-27.md create mode 100644 docs/recherche/RAPPORT_PILOTAGE_CORE_JUDGE_VLM_2026-05-24.md create mode 100644 docs/recherche/SPEC_POPUPS_CATALOGUE.md create mode 100644 docs/recherche/SPEC_TRANSPORT_CONTRAT.md create mode 100644 docs/recherche/SPEC_VALIDATOR_MATRICE.md create mode 100644 docs/specs/Q-P1-agentchat-shadow-spec.md diff --git a/docs/AUDIT_20260404.md b/docs/AUDIT_20260404.md index 62ee84ead..7c4afdf0b 100644 --- a/docs/AUDIT_20260404.md +++ b/docs/AUDIT_20260404.md @@ -1,8 +1,8 @@ # Audit Complet — RPA Vision V3 -**Date** : 4 avril 2026 -**Auditeur** : Claude Sonnet 4.6 + 5 agents d'exploration spécialisés -**Périmètre** : Projet complet (code source, tests, sécurité, déploiement, qualité) +**Date** : 4 avril 2026 +**Auditeur** : Claude Sonnet 4.6 + 5 agents d'exploration spécialisés +**Périmètre** : Projet complet (code source, tests, sécurité, déploiement, qualité) **Environnement** : Ubuntu 24.04, Python 3.12.3, NVIDIA RTX 5070 (12 Go VRAM) --- @@ -34,8 +34,8 @@ RPA Vision V3 est un système d'automatisation RPA 100% basé sur la vision (pas d'accessibilité, pas de sélecteurs DOM). Il utilise CLIP, FAISS, Ollama (VLM local), SomEngine (YOLO + docTR) et le template matching pour identifier et interagir avec les éléments d'interface. -**État** : Phase 0 complète, Phase 1 (streaming agent) en stabilisation. -**Maturité** : Prototype avancé / pré-production. +**État** : Phase 0 complète, Phase 1 (streaming agent) en stabilisation. +**Maturité** : Prototype avancé / pré-production. **Risque principal** : Tokens de production hardcodés dans le code source. Le projet est fonctionnel : le replay visuel fonctionne sur Windows, le VWB permet de construire des workflows, le dashboard de monitoring est opérationnel. Cependant, la dette technique s'accumule (fichiers monolithiques, 47 Go de venvs dupliqués, code mort) et des failles de sécurité critiques doivent être corrigées avant toute mise en production. @@ -387,9 +387,9 @@ filterwarnings = ignore::DeprecationWarning **Fichier** : `.env.local` (gitignored mais sur disque) Le fichier contient en clair : -- `ANTHROPIC_API_KEY=sk-ant-api03-...` (clé Anthropic complète) -- `OPENAI_API_KEY=sk-proj-...` (clé OpenAI complète) -- `GOOGLE_API_KEY=AIzaSy...` (clé Google complète) +- `ANTHROPIC_API_KEY=REDACTED` (clé Anthropic complète) +- `OPENAI_API_KEY=REDACTED` (clé OpenAI complète) +- `GOOGLE_API_KEY=REDACTED` (clé Google complète) - `DEEPSEEK_API_KEY=3d7b...` (clé Deepseek complète) - `ENCRYPTION_PASSWORD`, `SECRET_KEY`, `RPA_TOKEN_ADMIN`, `AUTOHEAL_ADMIN_TOKEN`, `RPA_API_TOKEN` @@ -406,8 +406,8 @@ Le fichier contient en clair : ```python # Temporary fix: Add production tokens directly -prod_admin_token = "73cf0db73f9a5064e79afebba96c85338be65cc2060b9c1d42c3ea5dd7d4e490" -prod_readonly_token = "7eea1de415cc69c02381ce09ff63aeebf3e1d9b476d54aa6730ba9de849e3dc6" +prod_admin_token = "REDACTED" +prod_readonly_token = "REDACTED" ``` Ces tokens **admin** sont dans le code source, visibles dans git. Ils donnent un accès complet à l'API de streaming (port 5005) exposé sur Internet via `lea.labs.laurinebazin.design`. diff --git a/docs/AUDIT_FINALIZE_CONTRACT_INTEGRATION_2026-05-20.md b/docs/AUDIT_FINALIZE_CONTRACT_INTEGRATION_2026-05-20.md new file mode 100644 index 000000000..39a65c836 --- /dev/null +++ b/docs/AUDIT_FINALIZE_CONTRACT_INTEGRATION_2026-05-20.md @@ -0,0 +1,100 @@ +# Audit — Point d'intégration agent pour le contrat `finalize` étendu + +**Date** : 2026-05-20 +**Mission** : Claude 4 (lecture seule) +**Périmètre** : `agent_v0/agent_v1/network/streamer.py`, `agent_v0/agent_v1/main.py`, `agent_v0/agent_v1/ui/smart_tray.py`, `agent_v0/agent_v1/ui/chat_window.py`, `agent_v0/agent_v1/ui/shared_state.py`, `agent_v0/lea_ui/server_client.py` +**Contexte** : le serveur va enrichir la réponse `POST /api/v1/traces/stream/finalize` avec un nouveau contrat `{ replay_ready, replay_request, launch_replay }`. Objectif : identifier le **plus petit point d'intégration** côté agent pour consommer ce contrat proprement, **sans remettre le VWB au centre**. + +## Constat + +Aujourd'hui, `streamer.py:622-623` reçoit la réponse `/finalize` (`result = resp.json()`) et la jette dans un `logger.info(f"Session finalisée: {result}")`. **Aucun consommateur ne voit ce payload**. Tous les fils en aval (`main.AgentV1.stop_session`, `smart_tray._on_stop_session`, `chat_window._on_quick_stop`) terminent sans rien savoir du résultat serveur. La surface unique de capture du payload existe déjà — il suffit de l'exploiter. + +Côté déclenchement replay, **une seule surface client** émet l'appel HTTP : `smart_tray._launch_replay(workflow_id, workflow_name)` (ligne 459-505) qui fait `POST /api/v1/traces/stream/replay/start` avec `{"workflow_id": ...}`. Tout le reste (chat_window, lea_ui/server_client) consomme un replay déjà initié (resume/abort, polling actions). Le nouveau contrat `replay_request` pourrait juste enrichir le payload passé à cet appel existant. + +L'orchestration globale (`AgentV1` dans `main.py`) wire déjà session start/stop via `AgentState` (`shared_state.py`). C'est le seul endroit qui voit à la fois le moment précis de la fin d'enregistrement (`stop_session`), l'identité de la session, et a accès à toutes les UI (tray + chat) via `self._state`. + +## Tableau — surfaces d'arrêt / relance et recommandations + +### Arrêt d'un enregistrement (où voir la fin) + +| Fichier | Point d'entrée | Rôle | Risque branchement | Recommandation | +|---|---|---|---|---| +| `streamer.py:602-627` | `_finalize_session()` | Site **unique** qui reçoit `resp.json()` de `/finalize` | Aucun — actuellement payload jeté | 🟢 **POINT D'INTÉGRATION RECOMMANDÉ** — ajouter callback `on_finalize_result: Callable[[dict], None]` invoqué après `resp.ok`. Découplé, non-bloquant, contenu. | +| `streamer.py:131-157` | `TraceStreamer.stop()` | Drain queue + appel `_finalize_session` | Modifier le retour casse l'API publique (appelée par `main.stop_session`) | 🟡 Acceptable mais moins propre — retourner le payload via `stop()` couple les appelants à un retour qui n'existait pas. Préférer le callback. | +| `main.py:395-430` | `AgentV1.stop_session()` | Orchestre captor.stop + sleep + streamer.stop | Lieu naturel pour router le payload reçu — mais doit l'obtenir d'amont | 🟢 Destinataire idéal du callback — a accès à `self._state` pour notifier UI, et `self.user_id` pour reconstituer `agent_` session de polling. | +| `shared_state.AgentState.stop_recording()` | Méthode | Source de vérité de l'arrêt UI | Étendre la signature `_on_stop` casse rétrocompat | 🟡 À éviter en première intention — l'état partagé doit rester muet sur le résultat serveur. | +| `smart_tray._on_stop_session()` (ligne 396) | Bouton tray "C'est terminé" | Notif "Merci ! J'ai bien mémorisé..." | Faire du HTTP ici dupliquerait `_launch_replay` | 🔴 À éviter — la couche UI ne doit pas appeler `/finalize` ni interpréter sa réponse. | +| `smart_tray._on_emergency_stop()` (ligne 507) | Bouton ARRÊT D'URGENCE | Stop immédiat sans finalize | Hors scope (arrêt brutal, pas de proposition replay) | ⚫ Aucun branchement — un emergency stop n'attend pas le replay. | +| `chat_window._on_quick_stop()` (ligne 1458) | Bouton chat "Arrêter" | Appelle `shared_state.stop_recording()` | Idem ligne au-dessus | 🔴 À éviter — duplication. | + +### Relance d'un replay (où injecter `replay_request` / `launch_replay`) + +| Fichier | Point d'entrée | Rôle | Risque branchement | Recommandation | +|---|---|---|---|---| +| `smart_tray._launch_replay(workflow_id, workflow_name)` (ligne 459-505) | Appel HTTP `POST /replay/start` | **Seul site** qui déclenche un replay neuf côté client | Élargir la signature pour accepter un `replay_request: dict` (ou écraser `workflow_id` par tout le dict si fourni) | 🟢 Réutiliser tel quel — passer le `replay_request` du serveur comme JSON body. Surface existante, mature, notification Article 50 déjà cablée ligne 469-472. | +| `smart_tray._make_replay_callback()` (ligne 326) | Wrapping pour menu | Fabrique callbacks pour menu "Mes tâches" | Non touché par ce changement | ⚫ Aucun branchement | +| `main._replay_poll_loop()` (ligne 276-345) | Boucle permanente | **Récepteur** d'actions, pas déclencheur de replay | Auto-déclencher ici = invisible, non conforme Article 14 | 🔴 À éviter formellement — le poll loop reste polymorphique sur la session, ne doit pas démarrer de session. | +| `chat_window._on_paused_resume/_abort` (ligne 1016-1080) | Boutons bulle Pause | Resume/abort d'un replay **déjà en cours** | Hors scope (action sur replay existant) | ⚫ Aucun branchement | +| `lea_ui/server_client.LeaServerClient.start_polling()` (ligne 261) | API alternative | Polling parallèle non utilisé par main (qui a son propre loop) | Confusion : code dormant en partie | 🟡 Ne pas ajouter de logique ici — confirmer d'abord si ce client est encore actif au runtime. | + +### Notification / proposition utilisateur post-enregistrement + +| Surface | Mécanisme dispo | Utilisation recommandée | +|---|---|---| +| `smart_tray._notifier.notify(title, msg)` (NotificationManager) | Notification système non-bloquante | 🟢 Notif simple "Tâche prête, voulez-vous la tester ?" si `replay_ready=true` mais `launch_replay=false`. | +| `smart_tray._ask_consent(title, msg)` (ligne 74) | Dialog tkinter Yes/No | 🟢 Si le serveur signale `replay_ready=true`, dialog post-enregistrement (réutilise le même pattern que le consentement avant enregistrement). | +| `chat_window` (bulle paused) | Tkinter custom bubble | 🟡 Réutiliser le pattern paused_bubble (ligne 902-1014) pour proposer un "Tester maintenant" / "Plus tard" serait propre mais plus de code. À garder pour une v2 produit. | +| `shared_state` listeners | Notification générique | 🟡 Émettre un nouvel événement type `recording_finalized` peut servir si plusieurs UI doivent réagir — mais ajoute du contrat. | + +## Trois options d'intégration + +### Option A — Solution minimale (recommandée) + +1. `TraceStreamer` reçoit un setter optionnel `set_on_finalize_result(callback: Callable[[dict], None])`. Stocké en attribut. +2. Dans `_finalize_session()` après `resp.ok`, appel non-bloquant `self.on_finalize_result(result)` si défini. +3. `AgentV1.__init__` wire le callback : `self.streamer.set_on_finalize_result(self._on_finalize_result)`. +4. `AgentV1._on_finalize_result(payload)` : + - si `payload.get("launch_replay") is True` ET `replay_request` présent → invoque `self.ui._launch_replay(replay_request)` (avec adaptation signature pour accepter un dict) + - sinon si `payload.get("replay_ready") is True` → `self.ui._notifier.notify("Léa", "J'ai compris la tâche. Voulez-vous la tester ?")` + ouvre `_ask_consent` (en thread) + - sinon : silencieux + +**Volume estimé** : ~15-20 lignes de code dans 2-3 fichiers. Aucun changement de contrat dans `shared_state`, `chat_window`, `lea_ui`. Réutilise `_launch_replay` et `_ask_consent` existants. + +**Conforme** : Article 14 (contrôle humain) si dialog `_ask_consent` ; Article 50 (transparence) si `_launch_replay` est utilisé (notification déjà émise ligne 469-472). + +### Option B — Solution produit propre (à viser ensuite) + +Tout de Option A, plus : +- Ajouter une bulle interactive dans `chat_window` (pattern paused_bubble), au lieu/en plus du dialog tkinter, pour proposer le test avec contexte (nom de la tâche, durée, nombre d'actions). +- Persister la proposition non répondue dans `AgentState` pour que l'utilisateur la retrouve même s'il a fermé la notif. +- Logger côté audit_trail l'événement "test post-enregistrement proposé / accepté / refusé" pour conformité Article 12. + +**Volume estimé** : +50-80 lignes, principalement UI chat_window. Plus de couplage shared_state. + +### Option C — À éviter + +- **Auto-déclencher le replay sans confirmation** depuis le callback finalize, même si `launch_replay=true`. Sans dialog, non conforme Article 14 (l'utilisateur peut ne pas être devant l'écran à ce moment). Si le serveur impose vraiment `launch_replay=true`, l'agent doit quand même afficher un compte à rebours visible (3-5s) avec annulation possible — mais pas exécuter silencieusement. +- **Faire le HTTP `/finalize` depuis `chat_window` ou `smart_tray`** pour lire la réponse. Duplication, l'API serveur est déjà appelée par streamer. +- **Modifier `_replay_poll_loop` pour intercepter une transition "finalize→start replay"**. Couplage faux entre poll permanent et événement ponctuel ; race condition si l'utilisateur enchaîne plusieurs enregistrements. +- **Étendre la signature de `AgentState.stop_recording()`** pour propager le payload. La couche état partagé doit rester muette sur le contenu serveur. +- **Modifier `_finalize_session()` pour retourner la valeur via `streamer.stop()`**. Casse l'API publique de `stop()` (utilisée par `main.stop_session`), oblige à modifier deux endroits, et empêche un fan-out à plusieurs listeners éventuels. + +## Conclusion + +**Un seul point d'intégration recommandé** : ajouter un callback `on_finalize_result` à `TraceStreamer`, invoqué dans `_finalize_session` après réception de la réponse, wired par `AgentV1` qui dispatche selon `launch_replay` / `replay_ready` vers les surfaces UI déjà existantes (`smart_tray._launch_replay` ou `smart_tray._ask_consent` + `_notifier.notify`). + +Ce point : +- **Capture la réponse là où elle existe déjà** (`streamer.py:622`) sans dupliquer d'appel HTTP. +- **Découpe la responsabilité** : streamer = transport, main = orchestration, smart_tray = UI/consentement. +- **Préserve le VWB hors-jeu** : aucun appel ne transite par VWB, le replay est lancé directement par l'agent via l'endpoint `/replay/start` déjà éprouvé. +- **Respecte Article 14** si l'auto-lancement passe par `_ask_consent` ou une notif validable. +- **Coût d'implémentation** : ~15-20 lignes de code, 2-3 fichiers. + +Hors périmètre de cette mission : la définition exacte du contenu de `replay_request` (qui touche au workflow_id, à la machine cible, aux paramètres execution_mode). À spécifier côté serveur avant de figer la signature client. + +## Méthode d'audit + +- Lecture intégrale : `shared_state.py` (190 lignes), `smart_tray.py` (781 lignes), `lea_ui/server_client.py` (375 lignes). +- Lecture déjà réalisée à l'audit Léa-first (2026-05-19) : `streamer.py` (734 lignes), `main.py` (561 lignes). +- Grep ciblé `chat_window.py` (1619 lignes) : `stop|replay|workflow|finalize|launch|on_stop|on_replay` (60 résultats analysés). +- Aucune modification de code, aucune exploration VWB, aucune refonte UI proposée — conformément aux interdits. diff --git a/docs/AUDIT_LEA_FIRST_RUNTIME_2026-05-19.md b/docs/AUDIT_LEA_FIRST_RUNTIME_2026-05-19.md new file mode 100644 index 000000000..7e997cfd4 --- /dev/null +++ b/docs/AUDIT_LEA_FIRST_RUNTIME_2026-05-19.md @@ -0,0 +1,320 @@ +# Audit runtime Léa-first — `capture → replay direct → memory` + +**Date** : 2026-05-19 +**Auteur** : Claude (mission audit n°1, lecture seule) +**Périmètre** : `agent_v0/agent_v1/`, `agent_v0/server_v1/`, `core/learning/` +**Branche** : `backup/post-demo-2026-05-19` (HEAD `5ea4960e6`) +**Objectif** : identifier 5-10 blocages concrets qui empêchent la voie nominale `capture → replay direct → memory` d'être fiable. **Pas de VWB, pas de démo, pas de bench modèles, pas de refonte large.** + +--- + +## Verdict global + +La voie nominale **existe partiellement en code** mais comporte 3 ruptures fonctionnelles (P0) et 4 dégradations silencieuses (P1) qui la rendent **non fiable en pratique**. Le contournement actuel = passer par VWB pour fabriquer un workflow réutilisable, ou par le worker VLM offline. Pas de boucle "Léa capture → Léa rejoue" directe. + +État composant par composant : + +| Composant | État réel | +|---|---| +| Capture événements client (`captor.py`, `vision/capturer.py`) | mature, production-grade, bug coord critique | +| Streaming vers serveur (`streamer.py`) | mature, robuste (retry, buffer SQLite, backpressure) | +| Accumulation côté serveur (`live_session_manager.py`) | OK, `to_raw_session` câblé au worker VLM | +| Construction workflow depuis session Léa (`workflow_replay.py`) | **ORPHELIN** (0 caller runtime) | +| Replay direct sans VWB | **N'EXISTE PAS** (replay actuel consomme workflow VWB) | +| Memory lookup (resolve_engine + replay_memory) | branché, **gated silencieusement** sur `window_title` | +| Memory record_success / failure | branché, même gating | +| Memory record_human_correction (apprentissage supervisé) | **CASSÉ** (double bug) | +| `core/learning/*` (continuous_learner, feedback_processor, learning_manager) | **NON BRANCHÉ** au runtime serveur Léa-first | +| Observabilité mémoire | **AVEUGLE** (logs only, aucun endpoint) | + +--- + +## P0 — Ruptures (la voie nominale ne marche pas) + +### Blocage #1 — `record_human_correction` cassé, double bug + +**Fichier** : `agent_v0/server_v1/replay_learner.py:210-219` +**Fonction** : `ReplayLearner.record_human_correction()` + +**Bug A — import inexistant** : +```python +from .replay_memory import get_target_memory_store +store = get_target_memory_store() +``` +La fonction `get_target_memory_store` **n'existe pas** dans `replay_memory.py`. La vraie s'appelle `get_memory_store` (`replay_memory.py:47`). Le `try/except` à la ligne 224 avale silencieusement l'`ImportError`. **Aucune trace dans les logs au niveau INFO.** + +**Bug B — signature obsolète** : +```python +store.record_success( + screen_signature="human_correction", + target_spec=target_spec, + resolved_position={"x_pct": x_pct, "y_pct": y_pct}, + method="human_supervised", + score=1.0, +) +``` +La vraie signature (`core/learning/target_memory_store.py:212-219`) attend : +```python +def record_success( + self, + screen_signature: str, + target_spec, + fingerprint: TargetFingerprint, + strategy_used: str, + confidence: float, +) +``` +Les paramètres `resolved_position`, `method`, `score` **n'existent pas**. `TypeError` garanti si bug A est fixé sans fixer B. + +**Impact produit** : l'apprentissage par correction humaine — la boucle "Léa apprend en regardant l'humain corriger" — est **totalement inopérant**. La correction est juste loguée en JSONL local (`record()` ligne 206), jamais hoistée dans la mémoire persistante consultée au prochain run. + +**Gravité** : P0 +**Catégorie** : bug réel (double) + +--- + +### Blocage #2 — `build_workflow_replay` orphelin (pas de pont capture → replay direct) + +**Fichier** : `agent_v0/server_v1/workflow_replay.py:29-186` +**Fonction** : `build_workflow_replay()` + +**Constat** : +```bash +$ grep -rn "build_workflow_replay" --include="*.py" | grep -v "workflow_replay.py:" +# (vide) +``` +0 caller runtime. Le code décrit pourtant exactement le pont attendu (workflow enrichi → actions de replay avec vérification FAISS par node), mais il n'est appelé **nulle part**. + +**Ce qui marche aujourd'hui à la place** : +- `api_stream.py:1479-1525` (`POST /finalize`) → enqueue session au worker VLM (process séparé) +- Le worker construit un workflow via `GraphBuilder` (cf. `stream_processor.py:2306-2335`) +- Mais **rien ne renvoie ces actions à un replay direct**. Le replay (`replay_engine.py`) consomme un workflow VWB (table `steps` DB), pas une séquence construite à partir d'une session Léa. + +**Impact produit** : pas de chemin "Léa enregistre → on rejoue la session telle quelle". Toute session Léa doit transiter par VWB ou un commit DB manuel pour devenir rejouable. + +**Gravité** : P0 +**Catégorie** : branche non branchée (code mort) + +--- + +### Blocage #3 — Memory gated sur `target_spec.window_title` silencieusement inopérante + +**Fichiers** : +- `agent_v0/server_v1/resolve_engine.py:1541-1554` (lookup) +- `agent_v0/server_v1/api_stream.py:3634-3639, 3666-3672` (record) + +**Bug structurel** : la signature d'écran V4 = `sha256(normalize(window_title))[:16]` (cf. `replay_memory.py:94-103`). Si `target_spec.window_title` est vide ou absent : +```python +def compute_screen_sig(window_title: str) -> str: + norm = _norm_text(window_title) + if not norm: + return "" # → memory_lookup/record skip silencieux + return hashlib.sha256(norm.encode("utf-8")).hexdigest()[:16] +``` + +**Conséquence runtime** : sur les workflows édités à la main dans VWB ou construits sans renseigner `window_title` (cas dominant aujourd'hui), `screen_sig=""` → **ni lookup ni record déclenchés**. Pas de log d'erreur, pas de signal. La mémoire reste vide pendant des semaines sans alerter. + +**Validation** : sur le workflow Demo_urgence_3_db, beaucoup de steps ont `target_spec` sans `window_title` (anchors ciblés par `by_text`). Vérifiable rapidement par : +```bash +sqlite3 visual_workflow_builder/backend/instance/workflows.db \ + "SELECT json_extract(parameters_json, '$.target_spec.window_title') + FROM steps WHERE workflow_id='wf_483910cdd851_1778750587';" +``` + +**Impact produit** : la mémoire persistante peut paraître branchée (singleton init OK, JSONL/SQLite créés) et **ne stocker aucune entrée** sur les workflows réels. + +**Gravité** : P0 +**Catégorie** : dette (gating sur condition fragile) + +--- + +### Blocage #4 — `mss.monitors[1]` aveugle aux dims aberrantes (corruption en amont) + +**Fichier** : `agent_v0/agent_v1/vision/capturer.py` +**Sites** : `:107` (`capture_full_context`), `:150` (`capture_dual`), `:247` (`capture_active_window`) + +**Code commun** : +```python +with mss.mss() as sct: + monitor = sct.monitors[1] + sct_img = sct.grab(monitor) + img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX") +``` + +**Bug observé en démo (19 mai)** : `mss.monitors[1]` retourne intermittemment `{width: 2560, height: 60}` au lieu de `{width: 2560, height: 1600}` → coords `y_pct × 60 = 16 px` au lieu de `y_pct × 1600 = 424 px`. Aucune défense dans le code. + +**Impact produit** : toute capture servant de référence à la mémoire peut être corrompue. Un fingerprint enregistré avec `y_pct = 0.0099` au lieu de `0.265` **empoisonne le store** : au prochain hit, Léa clique à 16 px du haut au lieu du bon endroit. Et le `fail_count` augmente sans que la cause soit visible. + +**Fix attendu** (lecture seule, donc juste indiqué) : refuser la capture si `monitor.height < 200` (ou autre seuil sain), fallback sur autre monitor ou nouvelle tentative. + +**Gravité** : P0 +**Catégorie** : bug réel + +--- + +## P1 — Dégradations silencieuses (la voie marche mais fausse) + +### Blocage #5 — Captures Léa downscalées 800×500 envoyées au serveur + +**Fichier** : `agent_v0/agent_v1/core/executor.py:2895` +**Défaut** : `_capture_screenshot_b64(max_width=800, quality=60)` + +**7 sites d'appel sans override** : `:633, :801, :824, :894, :935, :989, :1055, :1303`. Seuls `:1334` et `:2147` (resolve_target) passent `max_width=0` (full-res). + +**Impact produit** : +- Matching template au serveur reçoit du 800×500 → compensé par multi-scale étendu côté serveur (`resolve_engine.py:130`, fix du 18 mai) +- **Mais** les coords stockées dans la mémoire dépendent du chemin de projection (full-res vs downscale). Bruit imprécis sur le store. + +**Gravité** : P1 (workaround serveur tient, base mémoire bruitée) +**Catégorie** : dette + +--- + +### Blocage #6 — `_replay_active` flag mal géré pendant les pauses + +**Fichier** : `agent_v0/agent_v1/main.py:319-345` + +**Code problématique** : +```python +if had_action: + if not self._replay_active: + self._replay_active = True + ... +else: + if self._replay_active: + print("[REPLAY] Replay termine — retour en mode capture") + self._replay_active = False +``` + +**Bug** : si le serveur renvoie `action=null + replay_paused=true` (attente humaine), `had_action=False` → Léa interprète "fin du replay" → cleanup UI + bulle paused n'apparaît plus. Comportement déjà observé en démo (cf. handoff 19 mai bug P1 "Léa client interprète action=null + replay_paused=true comme fin du replay"). + +**Impact produit** : tracking de replay corrompu côté client pendant les pauses. Désaligne aussi le `ChatWindow` (bulle paused invisible après plusieurs replays). + +**Gravité** : P1 +**Catégorie** : bug réel + +--- + +### Blocage #7 — `core/learning/*` Phase 7 non branché au runtime serveur Léa + +**Fichiers** : +- `core/learning/continuous_learner.py` (644 lignes) +- `core/learning/feedback_processor.py` (176 lignes) +- `core/learning/learning_manager.py` (180 lignes) +- `core/learning/versioned_store.py` (592 lignes) + +**Consumers réels** (grep `from core.learning`) : +| Caller | Statut | +|---|---| +| `core/execution/execution_loop.py:49, 71` | runtime alternatif, pas le serveur Léa | +| `core/pipeline/workflow_pipeline.py:29` | pipeline batch GUI legacy | +| `gui/orchestrator.py:52` | GUI PyQt5 legacy | +| `visual_workflow_builder/backend/services/learning_integration.py:36` | service VWB | +| `examples/test_phase7_*.py` | exemples | +| `tests/unit/test_versioned_store.py` | tests | +| `tests/test_correction_pack_integration.py` | tests | + +**Le runtime serveur Léa-first** (`api_stream.py` + `replay_engine.py` + `resolve_engine.py` + `stream_processor.py`) n'instancie **rien** de tout ça. Seul `TargetMemoryStore` est consommé via `replay_memory.py`. + +**Impact produit** : +- Drift detection (`ContinuousLearner`) = mort en flux Léa-first +- Versioned prototypes (`VersionedStore`) = morts +- Retraitement feedback bus (`FeedbackProcessor`) = mort +- Stats globales (`LearningManager`) = mortes + +**Gravité** : P1 +**Catégorie** : branche non branchée + +--- + +### Blocage #8 — Pas de signal "session enregistrée → workflow rejouable" exposé côté API + +**Fichier** : `agent_v0/server_v1/api_stream.py:1479-1525` (`POST /api/v1/traces/stream/finalize`) + +**Constat** : `finalize` marque la session, enqueue au worker VLM (`_enqueue_to_worker(session_id)`), rend la main. **Aucun endpoint** : +- Pour savoir quand le workflow construit est prêt +- Pour le déclencher en replay direct sur une cible (machine, agent) +- Pour récupérer la liste des "workflows construits par Léa" disponibles + +La séquence "Léa fais ça → maintenant Léa, rejoue ça" n'a pas de surface API exposée — elle passe **implicitement** par VWB (qui lit les workflows en DB et orchestre). + +**Impact produit** : la voie nominale "capture → replay direct" n'a pas de point d'entrée client. C'est cohérent avec le blocage #2 (`build_workflow_replay` orphelin) : le pont produit n'existe pas non plus côté API. + +**Gravité** : P1 +**Catégorie** : branche non branchée (au niveau orchestration) + +--- + +## P2 — Bruit et observabilité + +### Blocage #9 — Deux boucles heartbeat parallèles, persistance non scoped + +**Fichier** : `agent_v0/agent_v1/main.py:131, 349-393` + +**Constat** : 2 boucles tournent : +- `_heartbeat_loop` (ligne 434) — actif seulement si `self.session_id` (= recording actif) +- `_background_heartbeat_loop` (ligne 349) — actif **en permanence**, pousse sous `bg_` toutes les 5s même sans session + +Le serveur persiste ces sessions `bg_*` dans `data/streaming_sessions/`. Pas de purge automatique scoped (la purge générale tourne sur les sessions finalisées > 24h, mais `bg_*` ne se finalise jamais). + +**Impact produit** : pollution disque indépendante de l'usage. Croissance non maîtrisée. Bruit dans toute analyse a posteriori des sessions Léa réelles. + +**Gravité** : P2 +**Catégorie** : dette + +--- + +### Blocage #10 — Aucune métrique runtime sur la mémoire + +**Fichiers** : +- `agent_v0/server_v1/replay_memory.py` (`memory_lookup`, `memory_record_*`) +- `core/learning/target_memory_store.py` (`get_stats`) + +**Constat** : +- Les hits/misses sont seulement `logger.info` (`replay_memory.py:191-196`). +- `TargetMemoryStore.get_stats()` (`target_memory_store.py:440-479`) renvoie `total_entries, total_successes, total_failures, overall_confidence, jsonl_files_count, jsonl_total_size_mb` — **mais n'est branché à aucune route API**. +- Pas de compteur Prometheus, pas d'endpoint `/api/v1/memory/stats`, pas de surface dashboard. + +**Impact produit** : impossible de répondre en runtime à "la mémoire travaille-t-elle aujourd'hui ?" ou "combien d'entrées sur ce workflow ?" sans grepper les logs ou ouvrir la DB SQLite à la main. Debugging et validation Léa-first **à l'aveugle**. + +**Gravité** : P2 +**Catégorie** : dette (observabilité) + +--- + +## Tableau récapitulatif + +| # | Sévérité | Catégorie | Fichier:fonction | 1-line | +|---|---|---|---|---| +| 1 | P0 | bug | `replay_learner.py:210` `record_human_correction` | Import inexistant + signature obsolète, apprentissage humain mort | +| 2 | P0 | branche non branchée | `workflow_replay.py:29` `build_workflow_replay` | Orphelin, pas de pont capture→replay direct | +| 3 | P0 | dette | `resolve_engine.py:1541` + `api_stream.py:3634` | Memory gated sur `window_title` souvent absent, silencieusement morte | +| 4 | P0 | bug | `vision/capturer.py:107,150,247` | `mss.monitors[1]` aveugle, base mémoire empoisonnée | +| 5 | P1 | dette | `executor.py:2895` (7 sites) | Captures 800×500 par défaut, store bruité | +| 6 | P1 | bug | `main.py:319-345` `_replay_poll_loop` | `_replay_active` mal géré pendant pause, état UI désynchro | +| 7 | P1 | branche non branchée | `core/learning/*` | Phase 7 non branchée au runtime serveur | +| 8 | P1 | branche non branchée | `api_stream.py:1479` `/finalize` | Pas d'API "rejoue ce que tu viens d'enregistrer" | +| 9 | P2 | dette | `main.py:131,349` | Heartbeat background pollue la persistance | +| 10 | P2 | dette | `replay_memory.py` + `target_memory_store.py:440` | Aucune métrique runtime mémoire exposée | + +--- + +## Recommandation de séquencement (si on devait choisir 4 fixes) + +Pour rendre la voie nominale `capture → replay direct → memory` opérationnelle avec un effort minimal : + +1. **#4** d'abord — fixer `mss.monitors[1]` aveugle. Sinon tout ce qu'on stocke après est faux. +2. **#3** ensuite — exiger ou dériver `window_title` dans le `target_spec` à l'enregistrement Léa (la capture client a déjà cette info via `window_capture.title`, à propager). Sans ça, la mémoire reste vide. +3. **#1** — corriger `record_human_correction` (import + signature). Ouvre la boucle d'apprentissage supervisé. +4. **#2** + **#8** ensemble — soit rebrancher `build_workflow_replay` au worker VLM et exposer un endpoint client, soit assumer que VWB reste l'orchestrateur intermédiaire. Décision produit à arbitrer. + +**Pas dans le périmètre de cette mission** : proposer le design des fixes (la mission demandait l'audit, pas la refonte). + +--- + +## Méthode d'audit + +- Lectures intégrales : `core/learning/__init__.py`, `core/learning/target_memory_store.py`, `replay_memory.py`, `replay_learner.py`, `live_session_manager.py`, `workflow_replay.py`, `core/captor.py`, `vision/capturer.py`, `network/streamer.py`, `main.py` +- Lectures ciblées : `api_stream.py:1479-1525, 3600-3690`, `stream_processor.py:1700-1745, 2285-2345`, `resolve_engine.py:1525-1565` +- Grep consumers : `build_workflow_replay`, `memory_lookup`, `memory_record_*`, `ReplayLearner`, `record_human_correction`, `to_raw_session`, `TargetMemoryStore(`, `ShadowLearningHook`, `from core.learning` +- Croisement avec : handoffs 12-19 mai, `DETTE_TECHNIQUE.md`, `AUDIT_CONTROLES_DEBRANCHES_2026-05-08.md` diff --git a/docs/AUDIT_WINDOW_TITLE_MEMORY_PATH_2026-05-19.md b/docs/AUDIT_WINDOW_TITLE_MEMORY_PATH_2026-05-19.md new file mode 100644 index 000000000..8f2c40fef --- /dev/null +++ b/docs/AUDIT_WINDOW_TITLE_MEMORY_PATH_2026-05-19.md @@ -0,0 +1,109 @@ +# Audit — Perte de `window_title` dans le pipeline mémoire + +**Date** : 2026-05-19 +**Mission** : Claude 3 (lecture seule) +**Périmètre** : `agent_v0/server_v1/stream_processor.py`, `api_stream.py`, `replay_engine.py`, `workflow_replay.py`, `replay_memory.py` +**Branche** : `backup/post-demo-2026-05-19` (HEAD `5ea4960e6`) +**Référence** : blocage P0 #3 de `docs/AUDIT_LEA_FIRST_RUNTIME_2026-05-19.md` + +## Constat + +La mémoire persistante (`TargetMemoryStore`) repose sur une signature d'écran `sha256(window_title)` (cf. `replay_memory.py:94-103`). Sans `window_title`, la signature est vide → ni `memory_lookup` ni `memory_record_*` ne se déclenchent. Le code utilise `try/except` permissif et un `if _window_title` silencieux — aucun signal n'est émis quand la mémoire est skip. + +L'audit identifie **deux problèmes simultanés** : + +1. **Asymétrie écriture/lecture** : plusieurs chemins de production écrivent `window_title` sur l'action *top-level* (`action["window_title"]` ou `action["expected_window_before"]`), mais la lecture mémoire (`api_stream.py:3634-3639`) cherche **uniquement dans `target_spec`**. Conséquence : la fallback `or _mem_target_spec.get("expected_window_before", "")` ne peut jamais réussir car ce champ n'est posé que sur l'action top-level. + +2. **Producteurs incomplets** : plusieurs constructeurs de `target_spec` n'injectent pas `window_title` même quand l'information est disponible dans le contexte. + +Résultat net : sur le **chemin Léa-first natif** (capture → workflow construit par `build_replay_from_raw_events`), la mémoire ne se déclenche jamais bien que `window_title` soit présent sur l'action. + +## Tableau — cartographie des chemins + +### Producteurs `target_spec` / actions click + +| Fichier | Fonction / site | Chemin | `window_title` dans target_spec | Impact mémoire | +|---|---|---|---|---| +| `stream_processor.py:1532-1601` | `build_replay_from_raw_events` (raw events Léa → actions) | **Léa-first natif** | **JAMAIS posé** dans target_spec — écrit top-level ligne 1545 | 🔴 lookup + record **toujours skip** | +| `stream_processor.py:1590-1601` | branche enrich post-Léa (anchor + window_capture) | Léa-first natif | non — propage `enrichment` (by_text, anchor) + `window_capture.rect`, jamais `window_title` | 🔴 même chemin que 1545 | +| `stream_processor.py:4396-4443` | `_create_edge_action` (worker VLM offline, GraphBuilder edges) | **workflow construit hors session** | OK ligne 4402 : `if window_title: target_spec['window_title'] = window_title` | 🟢 mémoire active si node metadata contient `window_title` | +| `replay_engine.py:534-548` | `_generate_setup_env_actions` clic Démarrer | **replay-session bootstrap** | **AUCUN window_title** posé (légitime : fenêtre Bureau Windows) | 🟡 dette assumée — clic système | +| `replay_engine.py:563-578` | `_generate_setup_env_actions` clic Rechercher | replay-session bootstrap | **AUCUN window_title** posé (légitime : menu Démarrer) | 🟡 dette assumée | +| `replay_engine.py:611-625` | `_generate_setup_env_actions` clic résultat app | replay-session bootstrap | **AUCUN window_title** posé (résultats recherche volatils) | 🟡 dette assumée | +| `replay_engine.py:966-979` | `_normalize_action` (action depuis objet Target) | normalisation chemin Target API | by_role, by_text, context_hints posés — **PAS window_title** | 🔴 lookup + record skip | +| `replay_engine.py:1804-1807` | `_create_replay_state` slim copy | tous chemins (post-construction) | conserve si présent ; strip uniquement `anchor_image_base64` | 🟢 transparent | +| `workflow_replay.py:119-128` | `build_workflow_replay` (orphelin) | **branche non branchée** | OK ligne 123 : `"window_title": node_title` posé correctement | ⚫ code mort, 0 caller runtime | + +### Lecteurs `window_title` (sites memory) + +| Fichier | Fonction / site | Cherche dans | Statut | +|---|---|---|---| +| `replay_memory.py:142-206` | `memory_lookup` | `target_spec.get("window_title", "")` | 🔴 silencieusement skip si vide | +| `api_stream.py:3634-3639` | memory_record_success/failure source | `_mem_target_spec.get("window_title", "")` puis `_mem_target_spec.get("expected_window_before", "")` | 🔴 deuxième fallback **inopérante** — `expected_window_before` n'est jamais dans `target_spec` | +| `resolve_engine.py:1541` | déclenche memory_lookup | `target_spec.get("window_title", "")` | 🔴 propagation du silence | +| `api_stream.py:3278-3281` | log REPLAY (observabilité) | `action.get("expected_window_before") or _tspec.get("window_title", "")` | 🟢 **correct** — preuve que les 2 endroits existent et qu'au moins ce code le sait | +| `api_stream.py:3599` | audit_trail `target_app` | `_target_spec.get("window_title", "")` | 🔴 audit incomplet sur chemins où window_title est top-level | +| `stream_processor.py:1149, 1176` | `_enrich_actions_with_intentions` (user-facing) | `action.get("target_spec", {}).get("window_title", "")` | 🟡 affiche `'?'` ou `'inconnue'` quand absent | + +### Producteurs top-level `action["window_title"]` ou `expected_window_before` + +| Fichier | Site | Champ posé | Présent dans target_spec ? | +|---|---|---|---| +| `stream_processor.py:1545` | `build_replay_from_raw_events` | `action["window_title"] = window["title"]` | **non** | +| `replay_engine.py:1797-1798` | `_create_replay_state` slim copy | `a_copy["expected_window_before"]`, `a_copy["expected_window_title"]` | **non** | + +## Cas critiques (chemins où la mémoire skip silencieusement) + +### Cas A — Session Léa fraîchement enregistrée, workflow direct +**Trigger** : utilisateur enregistre une session, `finalize` enqueue, `build_replay_from_raw_events` produit les actions. +**Chemin** : `stream_processor.py:1532-1601`. +**Symptôme** : `action["window_title"]` est posé au top-level (ligne 1545), mais le `target_spec` (lignes 1590-1601) ne contient que `enrichment + window_capture`. Au replay, `memory_record_success` lit `_mem_target_spec.get("window_title", "")` → vide → `compute_screen_sig` → `""` → skip silencieux. +**Conséquence produit** : la voie nominale Léa-first n'alimente jamais la mémoire. Aucune leçon stockée. + +### Cas B — Action via API Target normalisé +**Trigger** : un client appelle une API qui passe par `_normalize_action` (replay_engine.py:966). +**Chemin** : `replay_engine.py:966-979`. +**Symptôme** : target_spec construit avec `by_role`, `by_text`, `context_hints` uniquement. `window_title` jamais posé même si disponible dans le contexte appelant. +**Conséquence produit** : tout client qui passe par cette API perd la mémoire. + +### Cas C — Workflow `setup_env` autogénéré (ouvrir une app via Démarrer) +**Trigger** : un workflow démarre par ouvrir une app, génération automatique de 3 clics (Démarrer → Recherche → Résultat). +**Chemin** : `replay_engine.py:534-625`. +**Symptôme** : aucun des 3 clics n'a `window_title` dans target_spec. C'est intentionnel (fenêtres système volatiles), mais la mémoire ne s'active pas non plus sur ces clics. +**Conséquence produit** : ces 3 clics ne bénéficieront jamais de l'apprentissage par répétition, alors qu'ils sont parmi les plus stables visuellement (bouton Démarrer toujours en bas-gauche). + +### Cas D — Fallback `expected_window_before` codée mais inopérante +**Trigger** : action a `expected_window_before` posé top-level (par `_create_replay_state` ou un workflow VWB qui le renseigne). +**Chemin** : `api_stream.py:3634-3639`. +**Symptôme** : le code tente le fallback `_mem_target_spec.get("expected_window_before", "")`. Mais `_mem_target_spec` est l'objet `target_spec` ; `expected_window_before` n'est jamais dans target_spec, il est sur action top-level (cf. `replay_engine.py:1797`). La fallback est **toujours vide**. +**Conséquence produit** : même quand l'information existe au top-level de l'action, le memory_record ne la voit pas. +**Preuve qu'il existait une intention de cohérence** : `api_stream.py:3278-3281` (log REPLAY) lit correctement `action.get("expected_window_before") or _tspec.get("window_title")`. Le code mémoire a copié la mauvaise moitié de l'expression. + +### Cas E — Workflow VWB édité à la main sans `window_title` +**Trigger** : workflow construit/édité dans VWB (table `steps`), `target_spec.window_title` souvent omis. +**Chemin** : consommé tel quel par `_create_replay_state`. +**Symptôme** : aucun warning, aucune erreur, aucun signal. La mémoire reste vide pour ce workflow. +**Conséquence produit** : sur Demo_urgence_3_db (46 steps), à vérifier combien de steps ont effectivement `target_spec.window_title` non vide — probablement minoritaire. + +## Conclusion + +**Bug réel (P0)** : +- **Cas D** — `api_stream.py:3634-3639` : la fallback `or _mem_target_spec.get("expected_window_before", "")` cherche dans `target_spec` au lieu de l'action top-level. Code mort par erreur de copy-paste depuis `api_stream.py:3278-3281` qui faisait correctement `action.get(...) or _tspec.get(...)`. Une ligne à corriger une fois la décision produit prise. +- **Cas A** — `stream_processor.py:1545` vs `:1590-1601` : asymétrie de contrat dans le même fichier, sur le chemin Léa-first nominal. `window_title` est connu (posé top-level) mais non propagé dans `target_spec` où la mémoire le cherche. + +**Dette de contrat** (P1) : +- **Cas B** — `replay_engine.py:966-979` : `_normalize_action` n'inclut pas `window_title` dans `target_spec`. Contrat implicite "tous les producteurs de `target_spec` doivent injecter `window_title` quand disponible" non documenté ni appliqué. +- **Cas E** — workflows VWB sans `window_title` : pas de validation côté serveur, pas de warning. La forme du contrat n'est jamais vérifiée à la création. +- **Cas C** — clics `setup_env` : exclusion légitime mais devrait être documentée. Une mémoire "setup_env" pourrait utiliser une signature d'écran différente (cf. `replay_learner.py:213` qui utilise `"human_correction"` comme signature constante — pattern réutilisable). + +**Branche non branchée** : +- `workflow_replay.py:119-128` : code mort qui pose pourtant `window_title` correctement. Cohérent avec son orphelinat global (blocage P0 #2 de l'audit Léa-first). Pas de valeur tant qu'il n'est pas câblé. + +**Synthèse 1-ligne** : la mémoire est branchée mais le contrat `window_title in target_spec` n'est respecté que par le chemin GraphBuilder (worker VLM offline) ; le chemin Léa-first nominal et la normalisation API perdent l'info, et la fallback prévue pour rattraper est inopérante par bug de lecture. Le résultat observable : `TargetMemoryStore` reste vide sur les sessions Léa réelles. + +## Méthode d'audit + +- Grep cibles : `target_spec.*=`, `"window_title"`, `window_title=`, `"type": "click"` sur les 5 fichiers du périmètre. +- Lectures ciblées : `stream_processor.py:1140-1180, 1530-1605, 4380-4445`, `replay_engine.py:525-625, 955-980, 1780-1810`, `api_stream.py:3270-3290, 3540-3670`, `workflow_replay.py` (intégral), `replay_memory.py` (intégral). +- Croisement écritures/lectures pour identifier les asymétries. +- **Pas de modification de code, pas d'exploration VWB, pas de proposition de refonte** — conformément aux interdits de la mission. diff --git a/docs/CR_AUDIT_PAUSED_RESUME_BUS_2026-05-22.md b/docs/CR_AUDIT_PAUSED_RESUME_BUS_2026-05-22.md new file mode 100644 index 000000000..b4f8e3fb0 --- /dev/null +++ b/docs/CR_AUDIT_PAUSED_RESUME_BUS_2026-05-22.md @@ -0,0 +1,132 @@ +# CR — Audit `paused_bubble: bus déconnecté, resume non émis` + fallback HTTP + +**Date** : 2026-05-22 +**Branche** : `backup/post-demo-2026-05-19` +**Périmètre** : agent-side uniquement (`agent_v0/agent_v1/**` + `agent_v0/lea_ui/**`). `agent_v0/server_v1/replay_engine.py` non touché. +**Statut** : patch + tests implémentés et verts (19 tests neufs + 1 test intégration trim). + +--- + +## 1. Cause exacte la plus probable + +Le bouton **Continuer** de la bulle paused suit un chemin **unique**, sans fallback : + +1. `ChatWindow._on_paused_resume(replay_id)` (`agent_v0/agent_v1/ui/chat_window.py:1016`) teste `self._bus is not None and self._bus.connected`. +2. Si vrai → `self._bus.resume_replay(replay_id)` → `FeedbackBusClient._safe_emit("lea:replay_resume", …)` (`agent_v0/agent_v1/network/feedback_bus.py:135`). +3. `_safe_emit` re-vérifie `self._sio.connected`, sinon retourne `False` (`feedback_bus.py:141-149`). +4. Côté serveur, c'est `agent_chat` (port 5004, SocketIO) qui relaie en HTTP `POST /api/v1/traces/stream/replay/{id}/resume` vers le serveur streaming (port 5005). + +**Le bug** : si le bus SocketIO est tombé (network blip, `agent_chat` redémarré, `LEA_FEEDBACK_BUS=0`, ou socket cassé entre `connect()` et le `emit`), le clic est *perdu* : +- log `paused_bubble: bus déconnecté, resume non émis` (`chat_window.py:1036`) +- boutons **disabled** (`_disable_paused_buttons`) +- fenêtre **minimisée** 500 ms plus tard (`self._root.after(500, self._do_hide)`) +- UX affiche « ⚠ Bus indisponible — réessayez dans 5 s » mais l'utilisateur **ne peut pas** réessayer (boutons figés + fenêtre cachée) +- côté serveur : le replay reste `paused_need_help` jusqu'à expiration / cancel manuel + +L'endpoint HTTP qui ferait le job existe pourtant déjà côté serveur (`api_stream.py:4333` `POST /replay/{id}/resume` et `:4443` `/cancel`) — il n'est juste pas appelé directement par l'agent quand le bus est down. + +**Confiance haute** : la chaîne du chemin nominal et le défaut de fallback ont été tracés ligne par ligne ; le log exact correspond bien à ce branchement. + +## 2. Fichiers / fonctions concernés + +| Fichier | Fonctions clés | +|---|---| +| `agent_v0/agent_v1/ui/chat_window.py` | `_on_paused_resume:1016`, `_on_paused_abort:1044`, `_disable_paused_buttons:1071` | +| `agent_v0/agent_v1/network/feedback_bus.py` | `FeedbackBusClient.resume_replay:130`, `abort_replay:137`, `_safe_emit:141`, `connected:122` | +| `agent_v0/agent_v1/main.py` | wiring `chat_window._bus` (start/stop dans `_start_chat`, fenêtre `start_session`) | +| `agent_v0/lea_ui/server_client.py` | `_auth_headers:114`, `_stream_url`, base requests existante (resume/abort absents avant ce patch) | +| `agent_v0/server_v1/api_stream.py` (référence, non modifié) | `/replay/{id}/resume:4333`, `/replay/{id}/cancel:4443` | + +## 3. Patch minimal recommandé (implémenté) + +**Choix** : ajouter un **fallback HTTP direct** côté agent vers `/replay/{id}/resume` et `/replay/{id}/cancel`, déclenché quand le bus SocketIO est down ou que l'emit échoue. En cas d'échec sur les deux canaux, ne PAS désactiver les boutons et ne PAS auto-hide la fenêtre → l'utilisateur peut réessayer. + +Pas de queue persistante, pas de retry automatique : minimum viable, déterministe, traçable dans les logs (`channel=bus` vs `channel=http` vs aucun). + +### Changements de code + +**`agent_v0/lea_ui/server_client.py`** — ajout de deux méthodes HTTP symétriques au flux SocketIO : + +- `resume_replay(replay_id) -> bool` : POST `/traces/stream/replay/{id}/resume`, retourne `resp.ok`. +- `abort_replay(replay_id) -> bool` : POST `/traces/stream/replay/{id}/cancel`, retourne `resp.ok`. +- Toutes deux : guard `replay_id` vide, lazy import `requests`, try/except → False sur exception, `_auth_headers()` pour le Bearer. + +**`agent_v0/agent_v1/ui/chat_window.py`** — refactor de la décision d'envoi : + +- Nouveau helper `_dispatch_paused_action(replay_id, bus_method, client_method) -> (emitted, channel)` qui essaie bus puis HTTP fallback. Retourne le canal utilisé pour le log (`"bus"` / `"http"` / `""`). +- `_on_paused_resume` et `_on_paused_abort` utilisent ce helper. En cas d'échec sur les deux canaux : + - feedback UI : « ⚠ Serveur injoignable — réessayez » + - `_enable_paused_buttons()` (nouveau) réactive les deux boutons + - **pas** de `_root.after(500, self._do_hide)` (pas d'auto-hide) + - log warning `paused_bubble: bus et HTTP indisponibles, resume non émis pour ` +- En cas de succès : feedback « → Reprise demandée… » avec mention du canal dans le log (`replay_resume émis pour via bus|http`). + +Aucun changement de signature publique ; aucun touchage côté `agent_v0/server_v1/`. + +## 4. Tests ajoutés + +| Fichier | Tests | Bilan | +|---|---|---| +| `tests/unit/test_server_client_replay_controls.py` | 10 tests (`resume_replay` × 5 + `abort_replay` × 5) : succès, échec serveur, replay_id vide, exception réseau, URL & header auth | ✅ 10/10 | +| `tests/unit/test_chat_window_paused_dispatch.py` | 9 tests sur `_dispatch_paused_action` en isolation Tkinter (bus OK, bus down, bus emit False, bus raise, no bus, all-fail, no-client, méthode absente, abort symétrique) | ✅ 9/9 | +| `tests/integration/test_replay_session_trim_neutral.py` | 1 test bout-en-bout `_extract_required_apps → _generate_setup_actions → _trim_redundant_setup_events → build_replay_from_raw_events` sur fixture reproduisant `sess_20260520T102916_066851` — vérifie que la première action utile post-setup est `type 'test'`, pas un click `by_text="Sans titre"` | ✅ 1/1 | + +Total **20 tests neufs**, **79 tests verts** sur le périmètre (les 59 existants `test_env_setup.py` n'ont pas régressé). + +### Commandes de validation + +```bash +cd /home/dom/ai/rpa_vision_v3 +source .venv/bin/activate +set -a && source .env.local && set +a + +python -m pytest tests/unit/test_server_client_replay_controls.py -v +python -m pytest tests/unit/test_chat_window_paused_dispatch.py -v +python -m pytest tests/integration/test_replay_session_trim_neutral.py -v +python -m pytest tests/unit/test_env_setup.py tests/unit/test_server_client_replay_controls.py tests/unit/test_chat_window_paused_dispatch.py tests/integration/test_replay_session_trim_neutral.py +``` + +## 5. Fichiers modifiés + +| Fichier | Nature | SCP Windows requis | +|---|---|---| +| `agent_v0/lea_ui/server_client.py` | Ajout `resume_replay` + `abort_replay` (~45 lignes) | Oui → `dom@192.168.1.11:C:/rpa_vision/lea_ui/server_client.py` | +| `agent_v0/agent_v1/ui/chat_window.py` | Refactor `_on_paused_resume`, `_on_paused_abort` ; ajout `_dispatch_paused_action`, `_enable_paused_buttons` (~110 lignes touchées) | Oui → `dom@192.168.1.11:C:/rpa_vision/agent_v1/ui/chat_window.py` | +| `tests/unit/test_server_client_replay_controls.py` | NEW (109 lignes) | Non | +| `tests/unit/test_chat_window_paused_dispatch.py` | NEW (115 lignes) | Non | +| `tests/integration/test_replay_session_trim_neutral.py` | NEW (130 lignes) | Non | + +⚠️ Le miroir `agent_v0/deploy/windows_client/lea_ui/server_client.py` est obsolète (setup initial, pas l'incrémental — cf. handoff 2026-05-20). Le canal réel reste le SCP manuel direct vers `C:/rpa_vision/`. + +## 6. Risques et limites + +- **Pas de queue persistante** : si l'utilisateur clique Continuer pendant un blackout réseau total (bus + HTTP indisponibles), le clic n'est pas mis en attente. Le patch garantit juste qu'il pourra réessayer (boutons restent actifs, pas d'auto-hide). Une vraie queue serait une refacto, hors scope « minimal ». +- **Pas d'invalidation du bus** : si l'attribut `self._sio.connected` est `True` mais le socket est en fait mort (cas rare), le bus émettra et retournera `True` au niveau client — le serveur ne recevra rien et le replay restera figé. Mitigation indirecte : `_safe_emit` re-vérifie `connected` juste avant le `emit`, et le pattern try/except attrape les erreurs réelles. Pas de fix supplémentaire, hors scope. +- **Endpoint `/cancel` côté serveur** : utilisé par `abort_replay`. Hypothèse : il fonctionne comme attendu (idempotent, accepte un replay déjà annulé). Référence `api_stream.py:4443` — pas re-vérifié dans cet audit. +- **`LEA_FEEDBACK_BUS=0`** : si le flag d'env désactive le bus côté Windows, `self._bus` reste `None`. Le patch couvre ce cas : HTTP est appelé direct. À garder à l'esprit pour la doc de déploiement Windows. +- **`server_client` non câblé après instanciation de ChatWindow** : `update_server_client()` existe (`chat_window.py:255`), donc le wiring tardif est OK. Si `server_client` reste `None` pendant le clic, le patch tombe en `(False, "")` proprement. + +## 7. Bonus — test d'intégration de non-régression pour le trim + +Le test demandé en second choix est livré dans `tests/integration/test_replay_session_trim_neutral.py`. Il exécute la chaîne **complète** `replay-session` côté serveur sur une fixture synthétique reproduisant le pattern de `sess_20260520T102916_066851` : + +- focus initial Notepad sur un titre non-neutre (`http…txt – Bloc-notes`) +- clic intra-Notepad à rel_y ≈ 40 sur la barre d'onglets +- focus_change vers `Sans titre – Bloc-notes` (titre neutre = état setup auto) +- saisie `test` + +Le test vérifie trois invariants stricts : + +1. `_generate_setup_actions` produit bien les actions Notepad (`act_setup_sess_click_start`, `click_search`, `click_result`). +2. Après `_trim_redundant_setup_events`, aucun event mouse_click ne porte un `window.title` contenant l'URL `http192.168.1.40` (le clic redondant a été coupé). +3. Après `build_replay_from_raw_events`, la première action utile est `type "test"` — pas un click `by_text="Sans titre"` que `_infer_tab_switch_target` aurait pu produire si le clic redondant avait survécu au trim. + +Si la régression du bug du 20 mai revient (par exemple un revert silencieux du patch `_NEUTRAL_TITLE_TOKENS`), ce test échoue immédiatement avec un message clair. + +## 8. Synthèse pour décision + +- **Cause** : pas de fallback HTTP, UI bloque l'utilisateur dès qu'un emit SocketIO échoue → replay paused figé. +- **Patch** : `ServerClient.resume_replay/abort_replay` (HTTP direct) + `ChatWindow._dispatch_paused_action` (bus → HTTP) + ré-activation boutons + skip auto-hide sur échec total. +- **Scope** : 2 fichiers prod (≤ 160 lignes touchées), 3 fichiers test (354 lignes). Pas de refacto. +- **Validation** : 79/79 tests verts. À valider en condition réelle : kill agent_chat (port 5004) pendant un replay paused, cliquer Continuer → côté Léa log `replay_resume émis pour … via http` + replay redémarre. +- **SCP requis** : 2 fichiers vers Windows avant relance Léa (`lea_ui/server_client.py`, `agent_v1/ui/chat_window.py`). diff --git a/docs/CR_AUDIT_SETUP_VISUAL_GUARDS_2026-05-22.md b/docs/CR_AUDIT_SETUP_VISUAL_GUARDS_2026-05-22.md new file mode 100644 index 000000000..1a9de3bad --- /dev/null +++ b/docs/CR_AUDIT_SETUP_VISUAL_GUARDS_2026-05-22.md @@ -0,0 +1,154 @@ +# CR — Durcissement du setup auto Windows (gardes visuelles + skip pixel-change) + +**Date** : 2026-05-22 +**Branche** : `backup/post-demo-2026-05-19` +**Périmètre** : `agent_v0/server_v1/replay_engine.py`, `agent_v0/agent_v1/core/executor.py`, `agent_v0/agent_v1/ui/chat_window.py` +**Statut** : patch + tests implémentés et verts (104/104 sur le périmètre). + +--- + +## 1. Constat live (run du 22 mai 2026 — `replay_sess_76b7d067`) + +Sur `sess_20260520T102916_066851`, le trim `neutral=True` passe correctement (patch 20 mai). En revanche, le setup auto Windows enchaîne ses étapes sans aucune garde visuelle intermédiaire : + +- `act_setup_sess_click_start` finit en `position_fallback` (clic blind, sans résolution VLM). +- Succès jugé sur **simple changement d'écran** (`_wait_for_screen_change` dans `executor.py:1313`). +- `type_search` et `wait_results` partent sans garde de fenêtre. +- `click_app_result` découvre **trop tard** que la fenêtre attendue n'est pas `Rechercher` mais `Fenêtre de dépassement de capacité de la barre d'état système.`. +- Pendant ce temps, `bloc-notes` a déjà été tapé dans la mauvaise surface (le popup overflow), polluant l'état. + +## 2. Cause racine + +Deux trous combinés : + +| # | Trou | Détail | +|---|---|---| +| A | **Pas de pré/post-conditions visuelles** entre les étapes du setup | `verify_screen` côté agent (`executor.py:1196`) ne faisait qu'un `time.sleep` et déléguait toute vérification au serveur — qui n'a pas de node CLIP pour ces étapes intermédiaires | +| B | **Validation par simple pixel-change** sur `click_start` | `executor.py:1313` considère un click `_setup_phase` valide dès qu'un seul pixel change. Or l'overflow popup change l'écran sans pour autant ouvrir le bon menu | + +## 3. Patch minimal — nouveau contrat de contrôle visuel + +### 3.1 Nouvelle chaîne setup (12 actions, 3 gardes intermédiaires + 1 finale) + +``` +1. click_start_menu clic visuel, fallback x/y +2. wait_start_menu 1000 ms +3. verify_start_menu_open GARDE 1 — titre ∈ {Rechercher, Search, + Cortana, Démarrer, + Start, SearchHost, + StartMenuExperienceHost} +4. click_search_box clic visuel (uniquement si search_mode = click_then_type) +5. wait_search_ready 500 ms +6. verify_search_box_active GARDE 2 — titre ∈ {, + Rechercher, Search} +7. type_app_name frappe « Bloc-notes » +8. wait_search_results 1200 ms +9. verify_search_results_visible GARDE 3 — titre ∈ {Rechercher, Search, + Cortana, SearchHost, + StartMenuExperienceHost} +10. click_app_result clic visuel + expected_window_before +11. wait_app_launch 2000 ms (3000 pour Office) +12. verify_screen final CLIP node setup_initial (pré-existant) +``` + +### 3.2 Mécanisme des gardes (nouveau champ sur `verify_screen`) + +Champ ajouté sur le contrat `verify_screen` : `expected_window_title_contains: List[str]`. Côté agent : + +1. `time.sleep` d'attente (comportement legacy). +2. Si patterns présents : `get_active_window_info()` → comparaison substring case-insensitive avec les patterns. +3. Match positif → succès, on continue. +4. Match négatif → bascule en **mode apprentissage humain** (`_capture_human_correction`, 120 s). + - Si l'utilisateur agit : warning `setup_guard_window_mismatch`, success=True, la correction remonte au serveur. + - Si timeout : success=False, `needs_human=True`, pause supervisée. + +Pas de changement côté serveur. Le node CLIP final (`setup_initial`) reste pour le verify post-launch. + +### 3.3 Skip pixel-change pour `_setup_phase` + +Dans `executor.py`, juste avant la branche `_wait_for_screen_change` : + +``` +is_setup_action = bool(action.get("_setup_phase")) +if needs_screen_check and hash_before and is_setup_action: + # Setup phase : pixel-change neutralisé, la garde verify_screen tranche + time.sleep(0.5) +elif needs_screen_check and hash_before: + # Comportement legacy pour les actions utilisateur + ... +``` + +Conséquences : +- `click_start_menu` ne peut plus être validé sur la seule ouverture du systray overflow popup. +- Le verify_screen suivant détecte le mauvais titre fenêtre et déclenche immédiatement le mode apprentissage. +- Les actions utilisateur **hors setup** conservent strictement le comportement précédent (non-régression vérifiée). + +### 3.4 Fix troncature bulle pause supervisée (livré au tour précédent) + +Pour mémoire — déjà appliqué : + +- `chat_window._compute_paused_bubble_height(reason_str)` : helper statique testable. +- Calcul : `max(wrapped_lines, explicit_lines)` avec cap à 12 lignes (vs 8 avant). +- Scrollbar activée dès que **cap atteint OU contenu ≥ 200 chars** (vs > 280 chars avant). +- Les longs `reason` serveur listant plusieurs candidats (avec `\n`) ne sont plus tronqués silencieusement. + +## 4. Fichiers modifiés + +| Fichier | Modification | SCP Windows | +|---|---|---| +| `agent_v0/server_v1/replay_engine.py` | `_generate_setup_actions` : insertion de 3 actions `verify_screen` (`verify_start_menu_open`, `verify_search_box_active`, `verify_search_results_visible`) | Non | +| `agent_v0/agent_v1/core/executor.py` | Helper statique `_window_title_matches_any` ; branche `verify_screen` étendue avec garde titre fenêtre + mode apprentissage ; skip `_wait_for_screen_change` pour `_setup_phase=True` | **Oui** → `C:/rpa_vision/agent_v1/core/executor.py` | +| `agent_v0/agent_v1/ui/chat_window.py` | Helper statique `_compute_paused_bubble_height` ; cap relevé à 12 lignes, scrollbar dès cap atteint ou ≥ 200 chars (tour précédent) | **Oui** → `C:/rpa_vision/agent_v1/ui/chat_window.py` | + +⚠️ Le miroir `agent_v0/deploy/windows_client/` est obsolète (setup initial uniquement). Canal d'incrémental réel = SCP manuel direct vers `C:/rpa_vision/`. + +## 5. Tests ajoutés ou adaptés + +| Fichier | Nature | Tests | +|---|---|---| +| `tests/unit/test_env_setup.py` | NEW classe `TestSetupVisualGuards` | 6 tests : insertion `verify_start_menu_open`, `verify_search_box_active` (mode `click_then_type`), absence en `direct_typing`, `verify_search_results_visible` toujours présent (les 2 modes), timeout ≤ 2 s sur toutes les gardes | +| `tests/unit/test_env_setup.py` | Adaptation de 5 tests existants | `test_notepad_setup_visual` (12 actions), `test_skips_search_click_for_direct_typing`, `test_verify_screen_final_present_with_title`, `test_no_final_verify_without_title`, `test_full_pipeline_from_events` (séquence canonique mise à jour) | +| `tests/unit/test_executor_verify_window_guard.py` | NEW fichier | 13 tests : helper `_window_title_matches_any` (7 cas) + routage garde (4 cas : match, mismatch+correction, mismatch+timeout, neutre sans patterns) + skip pixel-change `_setup_phase` (2 cas : setup skippe, hors-setup garde le comportement) | +| `tests/unit/test_chat_window_paused_dispatch.py` | Ajout classe `TestPausedBubbleHeight` (tour précédent) | 6 tests : empty, court, long single line, multi-lignes `\n`, cap atteint, seuil 200 chars | +| `tests/integration/test_replay_session_trim_neutral.py` | Inchangé (tour précédent) | 1 test bout-en-bout — toujours vert avec le nouveau setup | + +**Bilan tests sur le périmètre** : **104 / 104 verts**. + +```bash +cd /home/dom/ai/rpa_vision_v3 +source .venv/bin/activate +set -a && source .env.local && set +a + +python -m pytest \ + tests/unit/test_env_setup.py \ + tests/unit/test_executor_verify_window_guard.py \ + tests/unit/test_chat_window_paused_dispatch.py \ + tests/unit/test_server_client_replay_controls.py \ + tests/integration/test_replay_session_trim_neutral.py -v +``` + +## 6. Comportement attendu en live + +Après SCP `executor.py` + `chat_window.py` et redémarrage Léa, sur un nouveau `/replay-session` de `sess_20260520T102916_066851` : + +| Scénario | Log Léa attendu | Issue | +|---|---|---| +| `click_start` touche le vrai bouton Windows | `[LEA] verify_screen garde OK : 'Recherche' matche [...]` | Setup avance, frappe protégée | +| `click_start` ouvre systray overflow popup | Pixel-change observé MAIS log explicite `Setup action … : validation pixel-change skippée (garde verify_screen ultérieure)` puis `[LEA] verify_screen garde KO : attendu un titre contenant [...], actuel 'Fenêtre de dépassement…'` | Mode apprentissage humain immédiat, aucune frappe à l'aveugle | +| Focus perdu pendant `wait_search_results` (notification surgit) | `[LEA] verify_screen garde KO` sur `verify_search_results_visible` | Apprentissage humain avant `click_app_result` | +| Bulle de pause avec un long `reason` | Scrollbar visible | Plus de troncature | + +## 7. Risques / limites + +- **Patterns FR+EN uniquement** : couverture Windows 10/11 FR et EN. Sur OS exotique (DE, ES, ZH), il faudra étendre `expected_window_title_contains`. Localisé dans `_generate_setup_actions`, extension triviale. +- **Skip pixel-change conditionné à `_setup_phase`** : seules les actions marquées `_setup_phase=True` perdent la validation pixel-change. Si une future contribution ajoute une action setup sans garde verify_screen derrière, on perdrait le filet. À surveiller / documenter dans la convention de génération. +- **Mode `direct_typing`** : couverture par `verify_start_menu_open` (avant frappe) + `verify_search_results_visible` (avant clic résultat). Pas de `verify_search_box_active` car pas de `click_search_box` à valider — testé explicitement. +- **Helper `_compute_paused_bubble_height`** : prend en compte les `\n` explicites et la longueur ; cap 12 lignes. Compromis volontairement conservateur — afficher une scrollbar légèrement trop tôt vaut mieux que tronquer du contenu critique de pause. + +## 8. Synthèse pour décision + +- **Avant ce patch** : setup auto enchaînait click → wait → type → click_result sans contrôle entre, et un seul changement de pixel suffisait à valider la première étape. Constat live = `bloc` tapé dans `Fenêtre de dépassement…`, click_result en erreur tardive, `paused_need_help`. +- **Après ce patch** : 3 gardes verify_screen titre fenêtre + skip pixel-change setup → chaque transition critique est verrouillée. Mode apprentissage humain immédiat à la première dérive. Pixel-change ne décide plus de la validité d'une étape setup. +- **Scope** : 2 fichiers prod modifiés (≈ 90 lignes ajoutées dans `replay_engine.py`, ≈ 75 dans `executor.py`), 2 fichiers test (≈ 350 lignes neuves + adaptations). Aucun changement côté serveur ni protocole. +- **SCP** : `executor.py` et `chat_window.py` à pousser vers `C:/rpa_vision/agent_v1/…` avant relance Léa. `replay_engine.py` reste côté serveur Linux. +- **Validation live à faire** : lancer un `/replay-session` sur `sess_20260520T102916_066851`, vérifier la présence des 3 logs `verify_screen garde OK` (ou un mode apprentissage propre en cas de dérive). diff --git a/docs/HEALTHCHECK_LEA_STACK_2026-05-25.md b/docs/HEALTHCHECK_LEA_STACK_2026-05-25.md new file mode 100644 index 000000000..a6bc0989b --- /dev/null +++ b/docs/HEALTHCHECK_LEA_STACK_2026-05-25.md @@ -0,0 +1,76 @@ +# Healthcheck Lea stack — preuve initiale + +Date : 2026-05-25 12:53 Europe/Paris +Script : `tools/lea_healthcheck.py` +Mode : lecture seule, aucun restart, aucune restauration, aucune suppression. + +## Commandes + +Local Linux seul : + +```bash +.venv/bin/python tools/lea_healthcheck.py +``` + +Linux + Windows via SSH, sans stocker le mot de passe dans le script : + +```bash +SSHPASS='***' LEA_SSH_COMMAND='sshpass -e ssh' \ + .venv/bin/python tools/lea_healthcheck.py --windows-host 192.168.1.11 +``` + +Sortie JSON : + +```bash +.venv/bin/python tools/lea_healthcheck.py --json +``` + +## Resultat initial + +Statut global : **WARN**. + +OK : + +- `rpa-streaming.service` actif. +- Port `5005` ouvert. +- `/health` streaming healthy. +- Ollama API `11434` ouverte. +- Tags critiques presents : + - `qwen2.5vl:7b-rpa` + - `t2a-gemma3-27b:latest` + - `t2a-gemma3-27b-q4:latest` + - `thiagomoraes/medgemma-27b-it:Q4_K_S` +- `qwen2.5vl:7b-rpa` resident dans Ollama avec `context_length=2048`. +- Store Ollama : 38 manifests, 106 blobs. +- 3 blobs critiques 27B presents. +- Windows SSH joignable. +- Tache Windows `LeaInteractive` : `Running`. +- 2 processus `run_agent_v1.py` observes, conforme au wrapper venv + Python reel. + +Etat initial avant C1 : + +- `rpa-agent-chat.service` inactif. +- Port `5004` ferme. +- FeedbackBus non joignable. +- Variable utilisateur Windows `LEA_FEEDBACK_BUS='1'`, donc Lea tente le bus 5004 alors qu'il est down. + +Etat apres C1 / restart controle du 2026-05-25 13:26 : + +- `rpa-agent-chat.service` actif. +- Port `5004` ouvert. +- SocketIO polling OK avec origins `http://192.168.1.40:5004` et `http://192.168.1.11:5004`. +- `GET /api/status` FeedbackBus retourne `status=online`. +- Healthcheck Linux + Windows : **OK**. + +Point restant : `agent_chat` tente encore de charger OWL-v2 sur CUDA au boot et garde environ 602 MiB VRAM apres OOM. Cela n'empeche pas 5004, mais doit etre traite dans le chantier performance/VRAM. + +## Interpretation + +Le chemin critique replay/pause/resume reste couvert par `rpa-streaming` port 5005 et par le fallback HTTP. + +Le chantier propre avant le 1 juin est de choisir entre : + +1. reparer et rallumer FeedbackBus 5004 pour la narration temps reel ; +2. ou desactiver explicitement `LEA_FEEDBACK_BUS` cote Windows si la narration n'est pas retenue. + +Avec le report de la demo au 1 juin, l'option privilegiee est de reparer proprement 5004 au lieu de masquer le warning. diff --git a/docs/LESSONS_LEARNED_GHT_2026-05.md b/docs/LESSONS_LEARNED_GHT_2026-05.md new file mode 100644 index 000000000..20215a97b --- /dev/null +++ b/docs/LESSONS_LEARNED_GHT_2026-05.md @@ -0,0 +1,233 @@ +# Lessons Learned — Sprint démo GHT 5→19 mai 2026 + +**Objectif du document** : inventaire factuel des 15 jours de bug-chasing pré-démo GHT. À relire AVANT d'attaquer ARES et Anouste pour ne pas refaire les mêmes diagnostics. + +**Pas de prose, pas de plan d'action ici** — juste « ce qui marche » / « ce qui ne marche pas » + références. + +## Périmètre + +- **Période** : 2026-05-05 → 2026-05-19 (15 jours) +- **Branches** : `feature/qw-suite-mai` (travail) → `backup/post-demo-2026-05-19` (commit `5ea4960e6`) +- **Référence "ça marchait" antérieure** : tag `demo-stable-2026-05-12` (commit `2eeaa806b`), branche `demo/ght-2026-05-08` (commit `56e869c46`) +- **Démo livrée** : vidéo `Demo_urgence_3_db` (`wf_483910cdd851_1778750587`, 46 steps), patiente MOREL Catherine, décision UHCD, 1750 € + +--- + +## ✅ Ce qui marche (validé empiriquement pendant le sprint) + +### Pipeline visuel / résolution UI + +| Élément | Statut | Référence | +|---|---|---| +| Template matching multi-scale étendu (0.25 → 2.0) | scores ≥ 0.9 retrouvés sur capture downscalée 800×500 | `resolve_engine.py:130`, handoff 18 mai §1.2 | +| `hybrid_text_direct` rebranché dans cascade strict (mode legacy) | actif depuis commit `1cbec2806` | audit F2.4.4 | +| Exemption drift > 0.20 si `template_matching ≥ 0.95` ou `hybrid_text_direct ≥ 0.80` | actif, évite faux rejets sur high-confidence | `resolve_engine.py:2367-2390`, audit F2.2.1, DETTE-002 | +| Fallback heartbeat full-screen si capture client tronquée < 1200×800 | actif, élargi le 7 mai par `7233df2bb` | `api_stream.py:4422`, audit F2.2.6 | +| Anchors `by_text` (LINUX_demo, Tables, demo_95) ciblage textuel anti-faux-positif | validé sur Demo_urgence_3_db ord 29/35/36 | handoff 16 mai §4 | +| LoopDetector composite (screen_static + action_repeat + retry) | actif par défaut | `loop_detector.py`, commit `2a51a844b` | +| SafetyChecksProvider hybride (déclaratif + LLM contextuel) | actif sur `safety_level == medical_critical` | commit `7c6945171`, audit F2.3.4 | +| MonitorRouter (résolution écran cible multi-monitor) | actif, enrichissement heartbeat `monitor_index + monitors_geometry` | commits `6582a69d3`, `b1a3aa16f`, `2d71e2a24` | + +### Workflow `linux_db` (NoMachine + DBeaver VM Ubuntu) + +| Élément | Statut | +|---|---| +| Clics simples traversent NoMachine vers la VM | OK (pynput → SendInput → NoMachine → VM) | +| Bypass Ctrl+V/Ctrl+Enter via `ydotool` directement dans la VM | fix retenu (NoMachine en passive grab mange Ctrl) | +| `ydotoold` daemon persistant via service systemd | installé 18 mai, redémarre au boot VM | +| Gardien clipboard `prepare_clipboard_linuxdb.sh` (wl-copy + xsel boucle 0.5s) | actif, recharge VM clipboard | +| Workflow `linux_db` (7-9 steps) E2E sur VM Ubuntu en ~30s | validé 18 mai | +| Hook libvirt `/etc/libvirt/hooks/network` injecte `LIBVIRT_FWI ACCEPT 4000` | installé 18 mai, à valider au reboot | +| DNAT 4001→4000 via `nomachine-vm-forward.service` | persistant | +| UFW `route allow 4000 → 192.168.122.132` | persistant | + +### LLM / Modèles + +| Élément | Statut | +|---|---| +| `gemma4:latest` retenu pour `safety_checks` (bench rigoureux) | commit `0a02a6ec9`, BENCH_SAFETY_CHECKS_2026-05-06.md | +| `gemma4:31b-cloud` pour `t2a_decision` MOREL | qualité clinique propre observée (run 8 du 12 mai) | +| `qwen3-next:80b-cloud` testé qualité OK | switch ponctuel, pas durable | +| `qwen2.5vl:7b` pour VLM | configuré via `.env.local`, déborde CPU sur RTX 5070 12GB (acceptable car fallback) | +| `InfiGUI-G1-3B` Transformers grounding | 3.9 GB VRAM, permanent depuis 7 jours, principal | +| Bypass LLM via `static_result` / `static_text` (`replay_engine.py`) | court-circuit Ollama pour MOREL UHCD 1750 € — utilisé en démo | +| Module `smart_resize` officiel Qwen3-VL (commit `0d7bcd18a`) | commité mais ⚠ calé sur mauvaise référence patch_size — voir DETTE-014 | +| Bench `bench_t2a_dryrun.py` + `t2a_mappings.py` (commit `f2212e77e`) | outillage standalone 11 dossiers POC | +| `build_dpi_enriched` extraction déterministe horaires/classifications (commit `9872f4510`) | 41/41 tests verts | + +### Infra & déploiement + +| Élément | Statut | +|---|---| +| Service systemd `rpa-streaming` | actif, restart propre | +| Backup tarball post-démo `_archives/rpa_vision_v3_post_demo_20260519_142940.tar.gz` (8.9 GB) | SHA256 vérifié | +| Backup git `backup/post-demo-2026-05-19` poussé sur gitea (627 fichiers, 468 anchors) | commit `5ea4960e6` | +| 12+ backups DB `workflows.db.bak.*` jalonnant la session | présents dans `visual_workflow_builder/backend/instance/` | +| Registre `docs/DETTE_TECHNIQUE.md` créé (14 entrées) | actif depuis 9 mai | + +### Méthodes de travail (sanctuariser) + +| Méthode | Origine | +|---|---| +| `git status --short` SYSTÉMATIQUE en début de session | incident commit composite 12 mai (4 backend + 2 frontend stagés non liés) | +| Sauvegarde + fork AVANT chantiers parallèles | appliqué 12 mai, a payé | +| Instrumenter AVANT optimiser | corrigé 2 fois le cap (baseline run 4 + mesure parallélisme v2) | +| Test Ollama direct 30s AVANT pari sur connaissance LLM | `gemma3:27b` aurait été retenu par erreur sinon | +| Mesurer 2 conditions COMPARABLES, jamais cold-start vs warm | parallélisme ratio 2.7× → 1.27× corrigé | +| Diff PNG anchor avant/après recapture | aurait économisé 15 jours de bug-chasing (cause racine bug P0 #1) | + +--- + +## ❌ Ce qui ne marche pas (cause connue ou hypothèse, contournement noté) + +### 🔴 Bugs P0 racines (NON résolus — démo a tourné grâce aux contournements) + +| Bug | Cause connue | Contournement | Fix réel à faire | +|---|---|---|---| +| **VWB recapture anchor ne régénère pas le PNG** | inconnue — `capture.py` réutilise PNG existant ou écrit avant screenshot ; 2 anchors capturés à 8j d'intervalle bit-à-bit identiques | recapture inutile, accepter régressions mystérieuses | audit `visual_workflow_builder/backend/api_v3/capture.py` | +| **Stop VWB ne purge pas la queue serveur** | VWB n'appelle pas `POST /api/v1/traces/stream/replay//cancel` au clic Stop | script `./scripts/cancel-replays.sh` manuel | brancher Stop → cancel côté frontend | +| **Coord client Léa Y cassé (÷ ~27)** | `mss.monitors[1]` retourne intermittemment `2560×60` au lieu de `2560×1600` → `y_pct × 60 = 16 px` (clic en haut écran) | aucun — bug intermittent | `agent_v0/agent_v1/core/executor.py:606-617`, ajouter fallback `if height < 200: reject` | +| **Bug skip ord 13 orchestration** (intermittent, run 8 du 12 mai) | non identifiée — transition serveur → visuel → serveur (mécanisme server-side action) | aucun, NOT REPRO 100% | trace `replay_fb0c9882_state.json` ; investiguer `replay_engine.py` + `api_stream.py` | +| **Bug échelle pixel grounding Ollama** (smart_resize non-déterministe) | DETTE-006 + DETTE-010 + DETTE-014 — checkpoint Qwen3-VL utilise `Qwen2VLImageProcessorFast` avec `patch_size=16` (factor=32, non 28) ; module `smart_resize.py` calé sur mauvaise référence | non posé | réaligner après lecture `image_processing_qwen2_vl_fast` | + +### ⚠ Bugs P1 (workaround dispo) + +| Bug | Cause | Workaround | +|---|---|---| +| Léa état mémoire dégradé (bulle paused n'apparaît plus après plusieurs replays) | `_last_pause_msg_shown` + `_chat_window_ref` jamais reset | restart Léa Windows | +| `delay_before` / `delay_after` ignorés au runtime | non lus par executor.py | fix partiel `dag_execute.py` pour `duration_ms` ; généraliser à `delay_before/after` | +| Léa interprète `action=null + replay_paused=true` comme "fin du replay" | `main.py` désactive `_replay_active` à tort | fix proposé `executor.py:1875` retourner `True` (non appliqué — nécessite SCP + restart Léa) | +| VWB frontend cache après modif DB | pas d'invalidation cache React | Ctrl+Shift+R obligatoire | +| `paste:true` ne fonctionne pas Windows → VM Ubuntu via NoMachine | NoMachine ne propage pas clipboard (ou `win32clipboard.SetClipboardText` plante silencieusement) | bypass via `ydotool` dans la VM (voir ✅) | +| Léa client envoie captures **800×500** au serveur | défaut `max_width=800` dans `executor.py:2895`, 7 sites d'appel sans override | compensé côté serveur par multi-scale étendu (✅) — fix client à poser : `max_width=0` sur 7 sites + SCP | +| `RPA_VLM_MODEL=gemma4:e4b` hardcoded dans Léa Windows (tag inexistant) | `executor.py` lignes 1569, 1700, 2248 | exporter `RPA_VLM_MODEL=qwen2.5vl:7b` env Windows | +| NoMachine viewer Windows freeze (clics avalés après quelques minutes) | NoMachine 9.5.7, pattern intermittent | restart NoMachine + plein écran obligatoire | +| Bug `'int' object has no attribute 'get'` VLM Quick Find | exception Python, non bloquant | DETTE B handoff 18 mai | +| Bug `get_target_memory_store` import dans `replay_memory.py` | import cassé | non bloquant mais empêche apprentissage corrections humaines | +| Démarrage Léa très lent (3-6 min au lancement) | chargement modèles ML | investiguer | +| Léa peut crasher silencieusement sous Windows | non identifié (mémoire ? exception ?) | quick-restart avant démo | +| Bouton "Stop" disparaît côté VWB UI alors que replay actif serveur | désynchro UI/serveur | confondant, à fixer | +| DAG edges visuelles VWB ne se sauvegardent pas | seul `steps.order` fait foi | confondant, à fixer | +| Capture VWB fallback `mss` Linux échoue sur Wayland natif | `XGetImage failed` | dépendance Léa Windows OBLIGATOIRE pour capturer | + +### 🚫 Code orphelin / débranché (audit 8 mai) + +| Code | Statut | Référence | +|---|---|---| +| `_resolve_by_yolo` défini, importé, **jamais appelé** | cascade OmniParser/YOLO neutralisée | DETTE-004, F2.4.1 | +| `_fuzzy_match` importé `api_stream.py:4372` mais jamais appelé | import mort | F2.5.2 | +| `VisualEmbeddingManager` + `ScreenshotValidationManager` (`core/visual/*`) définis mais jamais instanciés | mémoire visuelle orpheline | DETTE-005 | +| `ShadowLearningHook` (`core/grounding/shadow_learning_hook.py`) défini mais jamais instancié | Phase 6 FAST→SMART→THINK non câblée à Shadow | DETTE-009 | +| `_handle_possible_popup` (client) défini, **0 site d'appel** | fonction morte côté Léa, remplacée par `_handle_popup_vlm` | F5.5.1 | +| Pre-check VLM par-clic désactivé par `if False:` (`observe_reason_act.py:1704-1713`) | actif depuis 25 avril, commentaire « pipeline FAST→SMART→THINK a déjà validé » | DETTE-008, F6.1.1 | +| Pre-check OCR sémantique gardé par flag `RPA_ENABLE_TEXT_PRECHECK=false` par défaut | extinction explicite 8 mai pour démo GHT | DETTE-001, F2.3.1, F2.6.2 | +| Self-healing Win+D au retry 1 reverté (`22c0a2ba6`) | cercle vicieux observé | DETTE-003, F2.2.3 | +| Trois implémentations `smart_resize` coexistent (server.py, infigui_worker.py, module officiel) | unification post-démo Kerella | DETTE-007 | +| `pause_for_human` ignorée silencieusement en mode autonome (sans `safety_checks`) | actif `api_stream.py:3011-3017` | F2.6.5 | + +### Anti-méthode observée (autocritique) + +| Erreur | Conséquence | +|---|---| +| Modifs locales empilées sans validation à chaque étape | 15 jours de dérive depuis `demo-stable-2026-05-12` | +| Pas de smoke-test reproductible entre démos | régressions silencieuses découvertes par hasard | +| Bug VWB recapture anchor non détecté pendant 15 jours | cause racine n°1 des régressions, restée invisible | +| Workarounds empilés (cancel-replays.sh, bypass LLM static, pause humaine NoMachine) | dette technique non remboursée | +| Recapture en aveugle (Dom a recapturé des anchors en pensant fixer, le bug était VWB) | effort gaspillé | +| Apprentissage workflow via VWB-recording au lieu de mode Shadow Léa | divergence VWB ↔ Léa, 5 bugs P0 | + +--- + +## ⚠ Contournements ACTIFS à connaître absolument + +À sortir avant chaque nouveau run / déploiement client. Ces hacks **ne survivront pas** à un environnement propre. + +| Contournement | Localisation | Risque | +|---|---|---| +| Bypass auth NPM `/aiva-urgence/` | `proxy_host/10.conf` (hors git) | écrasé si UI NPM touchée | +| Bypass LLM `static_result/static_text` MOREL | `replay_engine.py` steps 12-14 Demo_urgence_3_db | démo seulement, pas réutilisable client | +| Script `scripts/cancel-replays.sh` | manuel après chaque Stop VWB | oublié → replay zombie | +| `prepare_clipboard_linuxdb.sh` à relancer après reboot VM | non auto | clipboard vide → paste vide | +| `xhost +local:` à refaire après reboot VM | non auto | xsel échoue | +| Bypass Ctrl+V via `ydotool` au lieu de NoMachine clipboard | architectural, OK | dépend de la VM, pas Windows pur | +| Mot de passe `loli` en clair dans les scripts SSH/sudo | DETTE 5/16 mai | à remplacer par clé SSH + sudoers NOPASSWD | +| `RPA_VLM_MODEL=gemma4:e4b` hardcoded Léa | env var Windows à exporter | popup VLM 404 sinon | +| Flag pré-check OCR off par défaut | `RPA_ENABLE_TEXT_PRECHECK=false` | clics au pif si VLM/template échouent | +| Drift exemption template ≥ 0.95 / hybrid ≥ 0.80 | `resolve_engine.py:2367-2390` | accepte position visuelle même hors zone enregistrée | +| Fallback heartbeat sur capture < 1200×800 | `api_stream.py:4422` | risque image stale si heartbeat ancien | + +--- + +## 🧠 Constats produit (à intégrer dans le pivot post-démo) + +1. **Erreur stratégique identifiée par Dom (19 mai)** : apprendre via VWB-recording au lieu de Shadow Léa = deux représentations divergentes (anchors capturés à un moment T, replay à T+N). Origine des 5 bugs P0. +2. **VWB et Léa non unifiés** : VWB édite un script explicite que Léa rejoue. Pas de réinterprétation au runtime. Unification réelle = 4-6 semaines (refonte paradigme, hors fenêtre 15j POC). +3. **gemma3:27b CONFABULE sur PMSI français** (invente acronymes GEMSA, présente avec assurance maximale) — ne JAMAIS l'envisager comme fallback `gemma4` sur T2A. +4. **gemma4:31b** a conscience d'incertitude (bloc *Thinking*) — mais confond CCMU/GEMSA avec logique GHM. Libellé complet PMSI obligatoire dans FAITS_CALCULÉS. +5. **Ollama Cloud 503** vécue 12 mai → robustesse non couverte pour démo. Pas de fallback local équivalent qualité testé. +6. **Communication Dom ↔ Claude Code × 2 ↔ Claude session principale** : déperdition observée (3 arbitrages décision retransmis 2 fois). Pointer Claude Code vers fichiers de référence rédigés en session principale, pas paraphraser en chat. + +--- + +## Références + +### Commits clés (5 → 19 mai) + +- `5ea4960e6` (19 mai) — backup snapshot post-démo GHT +- `f2212e77e` (12 mai) — bench_t2a_dryrun.py + t2a_mappings.py +- `9872f4510` (12 mai) — build_dpi_enriched extraction déterministe +- `2eeaa806b` (9 mai) — **tag `demo-stable-2026-05-12` — référence "ça marchait"** +- `bfbf0f9c3` (9 mai) — refactor parser bbox_2d centralisé +- `0d7bcd18a` (9 mai) — module smart_resize officiel (⚠ DETTE-014) +- `88ed103de` (9 mai) — création registre DETTE_TECHNIQUE.md +- `731b5bcae` (8 mai) — réactivation pré-check OCR calibrage chirurgical +- `56e869c46` (8 mai) — flag pré-check OCR off par défaut **(branche `demo/ght-2026-05-08`)** +- `7847a0e82` (7 mai) — toast paused supervisée + threshold FIND-TEXT 0.75 +- `40440f1ca` (7 mai) — cure régression b584bbabc fallback aveugle +- `f62fda575` / `7233df2bb` (7 mai) — fallback heartbeat image tronquée + execution_mode supervised +- `1cbec2806` (6 mai) — rebrancher hybrid_text_direct +- `22c0a2ba6` (6 mai) — **revert** self-healing Win+D auto (cercle vicieux) +- `c969f93a2` (6 mai) — self-healing Win+D auto retry 1 (reverté) +- `864530c85` (6 mai) — `_async_replay_lock` helper + 17 endpoints async non-bloquants +- `0a02a6ec9` (6 mai) — bench rigoureux LLM safety_checks → `gemma4:latest` +- `2a51a844b` (5 mai) — LoopDetector composite +- `7c6945171` (5 mai) — SafetyChecksProvider hybride + +### Handoffs détaillés + +- `docs/handoffs/2026-05-19_handoff_post_demo_GHT.md` — bilan démo + 5 bugs P0 +- `docs/handoffs/2026-05-18_handoff_consolidation.md` — UFW/LIBVIRT, template multi-scale, ydotoold systemd +- `docs/handoffs/2026-05-17_handoff_session_nomachine.md` — NoMachine freeze + `gemma4:e4b` +- `docs/handoffs/2026-05-16_handoff_ydotool_clipboard.md` — bypass Ctrl+V via ydotool +- `docs/handoffs/2026-05-16_handoff_demo3_workflow.md` — création Demo_urgence_3_db + linux_db +- `docs/handoffs/2026-05-13_inventaire_anchors_interop.md` — inventaire anchors +- `docs/handoffs/2026-05-12_handoff_fin_journee.md` — bug skip ord 13, bench A.1 paste, 8 arbitrages décision +- `docs/handoffs/2026-05-12_brief_S1_build_dpi_enriched.md` — brief V3 décision +- `docs/handoffs/2026-05-12_audit_complet_decision_t2a.md` — audit t2a_decision + +### Audits & rapports + +- `docs/DETTE_TECHNIQUE.md` — 14 entrées +- `docs/AUDIT_CONTROLES_DEBRANCHES_2026-05-08.md` — audit serveur+client 50+ findings +- `docs/AUDIT_DIM_TIM_DEMO_GHT_2026-05-08.md` — audit médecin DIM + TIM +- `docs/AUDIT_MEMOIRE_CLAUDE_2026-05-08.md` — santé index mémoire +- `docs/AUDIT_BDD_WORKFLOW_2026-05-10.md` — audit BDD workflows +- `docs/BUG_PRECHECK_SPATIAL_BLINDNESS_2026-05-08.md` — DETTE-001 +- `docs/BENCH_SAFETY_CHECKS_2026-05-06.md` — bench LLM safety_checks +- `docs/BENCH_T2A_DECISION_11DOSSIERS.md` — bench décision +- `docs/MIGRATION_VLM_PLAN_2026-05-09.md` — plan migration VLM (DETTE-006, DETTE-010, DETTE-014) +- `docs/INVESTIGATION_MEMOIRE_VISUELLE_ORPHELINE_2026-05-09.md` — DETTE-005, DETTE-009 + +### Backups + +| Type | Localisation | +|---|---| +| Tarball post-démo | `/home/dom/ai/_archives/rpa_vision_v3_post_demo_20260519_142940.tar.gz` (8.9 GB, SHA256 `7ab84f22d5a4...`) | +| Branche git backup | `backup/post-demo-2026-05-19` sur gitea (commit `5ea4960e6`) | +| Tag stable référence | `demo-stable-2026-05-12` (commit `2eeaa806b`) | +| Branche démo référence | `demo/ght-2026-05-08` (commit `56e869c46`) | +| Backups DB workflows | `visual_workflow_builder/backend/instance/workflows.db.bak.*` (12+ jalons) | + +--- + +*Document maintenu par Dom. Toute nouvelle leçon (succès ou échec) à ajouter dans la section appropriée. Pas de remplissage — uniquement faits sourcés.* diff --git a/docs/OLLAMA_MODEL_STORE_INCIDENT_2026-05-25.md b/docs/OLLAMA_MODEL_STORE_INCIDENT_2026-05-25.md new file mode 100644 index 000000000..f261dffaf --- /dev/null +++ b/docs/OLLAMA_MODEL_STORE_INCIDENT_2026-05-25.md @@ -0,0 +1,93 @@ +# Incident Ollama model store — diagnostic lecture seule + +Date : 2026-05-25 12:55 Europe/Paris +Auteur : Codex +Contexte : Dom observe que des modeles semblent avoir disparu de `ollama list`, notamment des 27B medicaux / T2A. + +## Conclusion provisoire + +Les deux modeles T2A 27B critiques ne sont pas perdus : + +- `t2a-gemma3-27b:latest` est present dans Ollama et son blob est identique au GGUF local Q8. +- `t2a-gemma3-27b-q4:latest` est present dans Ollama et son blob est identique au GGUF local Q4. +- Les deux GGUF existent aussi dans `/mnt/backup/projects/t2a_v2/models/` avec les memes checksums. + +Le modele `thiagomoraes/medgemma-27b-it:Q4_K_S` est aussi present dans le store actif Ollama, avec un blob de 16G. + +Ce diagnostic ne prouve pas qu'aucun autre modele n'a disparu : il prouve que les 27B identifies pendant l'incident sont encore presents et reconstruisibles. + +## Etat Ollama actif + +- Un seul serveur `ollama serve` observe. +- Processus principal : `/usr/local/bin/ollama serve`, user `ollama`. +- Store actif : `/var/lib/ollama/.ollama/models`. +- `OLLAMA_MODELS` shell : vide. +- Service systemd : pas d'override explicite `OLLAMA_MODELS`, mais le journal indique le store actif ci-dessus. +- Store actif : 38 manifests, 106 blobs, 178G. +- `/usr/share/ollama` : absent. +- `/home/dom/.ollama/models` : absent. + +Historique notable : + +```text +/home/dom/.bash_history:33:sudo rm -rf /usr/share/ollama +/home/dom/.bash_history:34:sudo rm -rf ~/.ollama +``` + +Le fichier bash history date de 2025-10-23, donc ce n'est pas une preuve d'action recente. C'est compatible avec une ancienne migration/reinstallation Ollama ayant supprime d'anciens stores non actifs. + +## Modeles critiques verifies + +| Tag Ollama | Taille | Quant | Blob actif | Source locale | Backup | Statut | +|---|---:|---|---|---|---|---| +| `t2a-gemma3-27b:latest` | 28G | Q8_0 | `sha256-2f2509e30b0d07db517b82e62404194ef355846f08ac287775ff363693086818` | `/home/dom/ai/t2a_v2/models/t2a-gemma3-27b-q8_0.gguf` | `/mnt/backup/projects/t2a_v2/models/t2a-gemma3-27b-q8_0.gguf` | Present + reconstructible | +| `t2a-gemma3-27b-q4:latest` | 16G | Q4_K_M | `sha256-0139f42273d53348fa0d24daae016b7231e1310258bbbaa7e38a1af703217c1a` | `/home/dom/ai/t2a_v2/models/t2a-gemma3-27b-q4_k_m.gguf` | `/mnt/backup/projects/t2a_v2/models/t2a-gemma3-27b-q4_k_m.gguf` | Present + reconstructible | +| `thiagomoraes/medgemma-27b-it:Q4_K_S` | 16G | Q4_K_S | `sha256-7cb6ff10942c8ccf370e274daafaf56da3fff318f40a355df331d8783c6c11f3` | Store Ollama actif | non identifie hors Ollama | Present | + +Checksums confirmes : + +```text +2f2509e30b0d07db517b82e62404194ef355846f08ac287775ff363693086818 /var/lib/ollama/.ollama/models/blobs/sha256-2f2509e30b0d07db517b82e62404194ef355846f08ac287775ff363693086818 +2f2509e30b0d07db517b82e62404194ef355846f08ac287775ff363693086818 /home/dom/ai/t2a_v2/models/t2a-gemma3-27b-q8_0.gguf +2f2509e30b0d07db517b82e62404194ef355846f08ac287775ff363693086818 /mnt/backup/projects/t2a_v2/models/t2a-gemma3-27b-q8_0.gguf + +0139f42273d53348fa0d24daae016b7231e1310258bbbaa7e38a1af703217c1a /var/lib/ollama/.ollama/models/blobs/sha256-0139f42273d53348fa0d24daae016b7231e1310258bbbaa7e38a1af703217c1a +0139f42273d53348fa0d24daae016b7231e1310258bbbaa7e38a1af703217c1a /home/dom/ai/t2a_v2/models/t2a-gemma3-27b-q4_k_m.gguf +0139f42273d53348fa0d24daae016b7231e1310258bbbaa7e38a1af703217c1a /mnt/backup/projects/t2a_v2/models/t2a-gemma3-27b-q4_k_m.gguf + +7cb6ff10942c8ccf370e274daafaf56da3fff318f40a355df331d8783c6c11f3 /var/lib/ollama/.ollama/models/blobs/sha256-7cb6ff10942c8ccf370e274daafaf56da3fff318f40a355df331d8783c6c11f3 +``` + +## Modelfiles de reconstruction T2A + +Les Modelfiles existent en local et backup : + +```text +/home/dom/ai/t2a_v2/models/Modelfile-t2a-gemma3-27b +/home/dom/ai/t2a_v2/models/Modelfile-t2a-gemma3-27b-q4 +/mnt/backup/projects/t2a_v2/models/Modelfile-t2a-gemma3-27b +/mnt/backup/projects/t2a_v2/models/Modelfile-t2a-gemma3-27b-q4 +``` + +Reconstruction possible, si necessaire et apres accord : + +```bash +ollama create t2a-gemma3-27b -f /home/dom/ai/t2a_v2/models/Modelfile-t2a-gemma3-27b +ollama create t2a-gemma3-27b-q4 -f /home/dom/ai/t2a_v2/models/Modelfile-t2a-gemma3-27b-q4 +``` + +Ne pas executer tant que les tags sont deja presents. + +## Elements a clarifier + +1. La liste attendue par Dom avant incident : noms exacts des modeles absents. +2. Si des modeles etaient dans `/usr/share/ollama` ou `~/.ollama/models`, ils ne sont pas dans le store actif actuel. +3. Timeshift contient des repertoires `var/lib/ollama` vides : pas de restauration model store via Timeshift observee. +4. Aucun `/api/delete` recent observe apres le 28 avril dans les extraits consultes ; les logs complets peuvent etre re-parcourus si une date suspecte est fournie. + +## Decision immediate + +- Ne rien supprimer. +- Ne rien restaurer a chaud. +- Garder ce fichier comme inventaire initial. +- Demander a Gemini de completer la table et chercher une eventuelle liste historique des tags attendus. diff --git a/docs/POC/AUDIT_CHAINE_APPRENTISSAGE_2026-06-01.md b/docs/POC/AUDIT_CHAINE_APPRENTISSAGE_2026-06-01.md new file mode 100644 index 000000000..2bb6cfa58 --- /dev/null +++ b/docs/POC/AUDIT_CHAINE_APPRENTISSAGE_2026-06-01.md @@ -0,0 +1,268 @@ +# Audit chaîne apprentissage modèle IA — 2026-06-01 + +> **DRAFT audit factuel — lecture seule, pas encore appliqué.** +> +> Date : 2026-06-01 22:00 Europe/Paris +> Auteurs : agent Explore Claude (audit primaire) + Claude (synthèse + matérialisation fichier) +> Statut : DRAFT — relecture Dom/Codex/Qwen attendue +> Origine demande : Dom 2026-06-01 ~21:40 — « tu pourrais lancer un agent explorateur pour nous remonter la chaîne exact d'apprentissage du modèle d'IA sur lequel j'ai travaillé. Le code existe, mais je pense qu'il a été débranché... » + +## TL;DR — Constat fort + +**L'intuition de Dom était JUSTE.** La chaîne d'apprentissage est **partiellement débranchée** depuis plusieurs semaines/mois. Les composants nécessaires pour implémenter ce que Dom a explicité dans ses 5 messages du 2026-06-01 20:46-21:27 (auto-évaluation par répétition, fusion/regroupement compétences immuables, versioning adaptateurs UI, portabilité du modèle appris) **existent déjà** dans le repo : + +- `core/learning/continuous_learner.py` — **644 lignes** +- `core/learning/feedback_processor.py` — **176 lignes** +- `PrototypeVersionManager` (support de ContinuousLearner) +- `TargetMemoryStore`, `VersionedStore` (supports) + +**Et ils sont tous orphelins** : ils ne sont pas importés par les points d'entrée actifs (`api_stream.py`, `run_worker.py`, `agent_chat/app.py`, `web_dashboard/app.py`, etc.). + +**En plus** : le **worker VLM** (le composant qui retraite les sessions finalisées avec ScreenAnalyzer/CLIP/FAISS/GraphBuilder) **n'a traité aucune session depuis 5 jours** (queue vide). Sessions accumulées non passées par le pipeline d'enrichissement profond. + +## Section meta — Constat de méthode (à dire franchement) + +> Dom 2026-06-01 ~21:55 : « on vient de passer presque 7 jours à refaire ce que j'avais déjà fait. Il faut arrêter de réinventer la roue. » + +C'est un constat factuel et juste. Cet audit (le seul à avoir cartographié l'existant) aurait dû être fait **avant** : +- de spécifier P1-SEMANTIQUE comme une nouvelle Phase 2.5 ; +- de proposer un `LoopDetector` proactif comme « bonus » alors que `ContinuousLearner` couvre ce besoin ; +- de discuter de « désapprentissage » (notion que Dom a explicitement rejetée 21:27) alors que `PrototypeVersionManager` gère déjà le versioning des prototypes ; +- de proposer de nouveaux mécanismes de fusion de compétences alors que `FeedbackProcessor` est conçu pour ça. + +**Pourquoi cet oubli ?** Trois facteurs cumulés : +1. `docs/POC/` (5 docs Dom déposés du 28-29/05 et 01/06) a été lue **après** rédaction du plan POC Claude du jour (mémorisé comme erreur de méthode dans `feedback_lire_docs_poc_avant_depot.md`). +2. **Aucun agent n'a été missionné en début de journée pour cartographier l'existant** dans `core/learning/`, `core/healing/`, `core/cognition/`. Mon audit Explore de 17:00 a flagué `ContinuousLearner` et `RecoveryLogger` comme orphelins mais sans alarmer sur le fait que ces orphelins **étaient précisément ce qui était demandé**. +3. Codex a été en mode urgence patch (P0 régression, dashboard test, etc.) et Claude en mode livraison agressive (5 livraisons P1 dans la journée) **sans pause cartographie**. + +**Décision opérationnelle à acter** : **avant tout nouveau module Léa learning, lancer un agent Explore qui vérifie ce qui existe dans `core/learning/`, `core/cognition/`, `core/healing/`, `core/training/`**. Ne pas spec/coder un module avant d'avoir confirmé qu'il n'a pas déjà été codé et débranché par Dom dans une session antérieure. + +## §1 — Schéma de la chaîne attendue + +``` +[Phase 1 — Capture] + PC Windows agent_v1 → push frames + actions + events + ↓ + data/training/live_sessions/// + ├ shots/*.png + ├ actions.jsonl + └ events.jsonl + ↓ + finalize() côté api_stream.py + ↓ + enqueue → data/training/_worker_queue.txt ← ⚠️ EXISTE, vide depuis 5 jours + +[Phase 2 — Enrichissement post-session (worker VLM)] + run_worker.py poll _worker_queue.txt (10s interval) + ↓ + StreamProcessor.reprocess_session() + ├ ScreenAnalyzer (VLM lecture sémantique) + ├ CLIPEmbedder (embeddings UI) + ├ FAISS index update + ├ _enrich_actions_with_intentions (Ollama gemma4 → intention/avant/après) + └ GraphBuilder (transitions états) + ↓ + data/training/.../enriched_*.jsonl + index.faiss + +[Phase 3 — Construction WorkflowIR] + build_replay_from_raw_events() → WorkflowIR + ↓ + data/workflows/.json OU data/competences/candidate/.yaml + selon le chemin (legacy workflow vs nouveau cycle Léa-first 01/06) + +[Phase 4 — Apprentissage continu (CŒUR DU DÉBAT)] + ┌──────────────────────────────────────────────────────┐ + │ ContinuousLearner (644 lignes, ORPHELIN) │ + │ ├ EMA online sur prototypes │ + │ ├ Détection dérive UI (variance temporelle) │ + │ ├ Variantes de prototypes (clustering) │ + │ ├ TargetMemoryStore : mémoire des éléments cibles │ + │ └ PrototypeVersionManager : versioning rollback │ + │ │ + │ FeedbackProcessor (176 lignes, ORPHELIN) │ + │ ├ Boucle feedback humain → ajustement prototype │ + │ └ Fusion observations multiples → compétence │ + └──────────────────────────────────────────────────────┘ + ↑ + | Doit être déclenché par : nouvelle session retraitée + | Doit produire : score confiance, variantes, dérive + ↓ + data/learning/prototypes_.json (présent ? à vérifier disque) + data/learning/feedback_log.jsonl (présent ? à vérifier disque) + +[Phase 5 — Boucle retour healing] + ┌──────────────────────────────────────────────────────┐ + │ RecoveryLogger (ORPHELIN runtime hors VWB) │ + │ SelfHealingIntegration (VWB seulement) │ + └──────────────────────────────────────────────────────┘ + +[Phase 6 — Utilisation au replay (HOT PATH)] + resolve_engine.py : + cascade OCR → template matching → VLM grounding + (PAS de consultation des prototypes ContinuousLearner) ← rupture + (PAS de consultation FAISS index appris) ← rupture + + Si compétence avec .semantic.yaml (Phase 2.5 nouveau 01/06) : + Phase25Analyzer.match_screen() → annotations sémantiques + +[Phase 7 — Fine-tuning VLM (HORS rpa_vision_v3)] + ~/ai/t2a-finetune/, ~/ai/t2a/, ~/ai/t2a_v2/ + Modèle custom Dom : qwen2.5vl:7b-rpa + Dataset alimenté manuellement (probablement) ← non-vérifié +``` + +## §2 — État par phase + +| Phase | Code existe | Wired runtime | Dernière utilisation effective | Verdict | +|---|---|---|---|---| +| 1. Capture live | ✅ | ✅ | aujourd'hui (sessions live actives) | OK | +| 1bis. Enqueue worker | ✅ (`finalize()` `api_stream.py:2253+`) | ⚠️ **à diagnostiquer** | Probablement débranché — queue vide 5j alors que sessions live continuent | ⚠️ R6 critique | +| 2. Worker VLM post-session | ✅ (`run_worker.py`) | ✅ (réveillé aujourd'hui 18:54 PID 4054092) | **0 session traitée depuis 5 jours** | ⚠️ tourne à vide | +| 2bis. Enrichissement actions | ✅ (`stream_processor.py:1643`) | ✅ (au build) | continu, mais perd valeur sans ContinuousLearner | OK partiel | +| 3. Construction WorkflowIR | ✅ | ✅ (au moment du build) | aujourd'hui (P1-LEA-SHADOW livré) | OK nouveau cycle | +| **4. ContinuousLearner** | ✅ **644 lignes** | ❌ **ORPHELIN** | Jamais appelé en runtime | 🔴 **DÉBRANCHÉ** | +| **4bis. FeedbackProcessor** | ✅ **176 lignes** | ❌ **ORPHELIN** | Jamais appelé | 🔴 **DÉBRANCHÉ** | +| **4ter. PrototypeVersionManager** | ✅ | ❌ ORPHELIN (dép ContinuousLearner) | Jamais | 🔴 DÉBRANCHÉ | +| 5. RecoveryLogger | ✅ | ❌ ORPHELIN hors VWB | VWB seulement | 🔴 DÉBRANCHÉ runtime agent_v1 | +| 6. Replay hot path | ✅ | ✅ | actif | OK fonctionnel mais déconnecté de Phase 4 | +| 6bis. Phase 2.5 sémantique | ✅ (livré 20:15) | ✅ (endpoint dispo) | aujourd'hui | OK nouveau | +| 7. Fine-tuning VLM | ✅ hors repo (siblings) | n/a (asynchrone manuel) | inconnu | hors scope audit interne | + +## §3 — Ruptures identifiées + +| ID | Rupture | Sévérité | Détail | Conséquence POC | +|---|---|---|---|---| +| **R1** | ContinuousLearner orphelin | 🟡 MOYENNE | EMA online, dérive UI, variantes prototypes : **tous existent mais non câblés**. Couvre exactement le besoin "auto-évaluation par répétition" exprimé par Dom 2026-06-01 20:46. | Pas d'apprentissage incrémental cross-session. Léa ne s'améliore pas avec l'usage. | +| **R2** | PrototypeVersionManager orphelin | 🟡 MOYENNE | Dépend de R1. Versioning prototypes + rollback. Couvre "versioning des adaptateurs UI" demandé par Dom 21:27. | Pas de rollback prototype dégradé. Pas de gestion versions UI. | +| **R3** | FeedbackProcessor orphelin | 🟠 LOURDE | Boucle feedback humain → ajustement prototype. **Cœur de la fusion/regroupement vers compétence immuable** demandée par Dom 21:27. | Léa Phase 4 humaine (corrections) ne nourrit pas le modèle. Apprentissage repart de zéro à chaque session. | +| **R4** | RecoveryLogger orphelin runtime | 🟢 FAIBLE | Healing limité à VWB. Pas de retour boucle sur sessions agent_v1 ratées. | Workflows en échec récurrent ne génèrent pas d'insights actionnables. | +| **R5** | Phase 2.5 sémantique (livrée aujourd'hui) → utilisation au replay incertaine | 🟢 FAIBLE | `.semantic.yaml` produit mais utilisé seulement si Phase25Analyzer.match_screen() est consultée. Wiring à confirmer. | Annotations sémantiques apprises peut-être pas exploitées au replay. | +| **R6** | **Worker queue vide depuis 5 jours malgré sessions live actives** | 🔴 **CRITIQUE** | Le worker tourne (PID 4054092 actif) mais `data/training/_worker_queue.txt` est vide. **Soit `finalize()` n'enqueue plus, soit toutes les sessions échouent silencieusement à se finaliser, soit la queue est purgée ailleurs.** À diagnostiquer URGENT. | **0 enrichissement profond depuis 5 jours**. Toutes les sessions live actuelles sont stockées brutes sans ScreenAnalyzer/CLIP/FAISS/GraphBuilder. POC Wallerstein impossible en l'état. | + +## §4 — Modules orphelins inventaire + +### O1 — `core/learning/continuous_learner.py` (644 lignes) + +- **Rôle** : adaptation incrémentale des prototypes UI par EMA online, détection de dérive temporelle, génération de variantes par clustering +- **Importé par** : 0 point d'entrée actif (vérifié par grep) +- **Dernière modification git** : à confirmer +- **Pourquoi débranché** : inconnu — pas de commit `disable` ou `remove` visible. Probablement n'a jamais été câblé en runtime depuis sa création (intention de wiring jamais finalisée). +- **Effort rebranchement** : **MOYEN** (2-3 j-h). Nécessite : + - Hook dans `run_worker.py` après `reprocess_session()` pour appeler `learner.update(prototypes, new_observations)` + - Chargement initial des prototypes au démarrage worker + - Persistance prototypes mis à jour : `data/learning/prototypes_.json` + - Tests intégration : sessions répétées sur même UI → vérification EMA progresse + +### O2 — `core/learning/feedback_processor.py` (176 lignes) + +- **Rôle** : intègre les feedbacks humains (validate/correct/undo Phase 4 Léa-first) dans le modèle prototype +- **Importé par** : 0 point d'entrée actif +- **Effort rebranchement** : **LOURD** (3-5 j-h). Nécessite : + - Hook dans `agent_chat/handlers/learn_action.py` (livré aujourd'hui) à chaque `POST /shadow/feedback` + - Routage : feedback → FeedbackProcessor → ContinuousLearner + - Persistance log : `data/learning/feedback_log.jsonl` + - Tests : validation step → prototype renforcé ; correction → prototype variante créée + +### O3 — `PrototypeVersionManager` + +- **Rôle** : versionner les prototypes successifs, permettre rollback si nouvelle version dégrade +- **Dépendance** : ContinuousLearner (utilise pour stocker versions) +- **Effort rebranchement** : **FAIBLE** (1 j-h) une fois O1 rebranché +- **Couvre la décision Dom 21:27** : « Ce qu'il faut versionner/invalider, ce sont plutôt mappings UI propres à une application/version, sélecteurs/positions/labels OCR, hypothèses fragiles ou obsolètes, compétence mal validée » + +### O4 — `TargetMemoryStore`, `VersionedStore` + +- **Rôle** : supports de persistance pour les prototypes versionnés +- **Effort** : couvert par O1+O3 + +### O5 — `RecoveryLogger` / `SelfHealingIntegration` + +- **Statut** : utilisé par VWB seulement, pas par agent_v1 runtime +- **Effort rebranchement runtime agent_v1** : MOYEN (2 j-h) +- **Priorité** : P2 (post-MVP), pas critique POC + +## §5 — Worker queue R6 — diagnostic urgent + +**Constat** : worker actif PID 4054092 depuis 18:54, log indique poll toutes les 10s sur `_worker_queue.txt`. Mais **0 traitement** depuis le réveil. + +**Hypothèses à vérifier (par ordre de probabilité)** : + +1. **`finalize()` côté `api_stream.py:2253+` n'enqueue plus** (commit qui a cassé le pipeline) +2. Les sessions live actuelles ne sont **jamais finalisées** (problème côté agent_v1 Windows qui ne push pas la fin de session) +3. La queue est **purgée par un autre processus** (cron ? cleanup ?) +4. Path resolution : worker poll un fichier inexistant ou un chemin différent de celui où `finalize()` écrit + +**Action recommandée** : lancer un agent Explore ciblé sur : +- `git log -p agent_v0/server_v1/api_stream.py | grep -A 20 "finalize\|_worker_queue"` pour voir les commits récents touchant à la queue +- Tester manuellement : finaliser une session test, vérifier que le fichier `_worker_queue.txt` est touché, vérifier que le worker la dépile +- Identifier où la rupture est exacte + +## §6 — Effort global de rebranchement + +| Composant | Effort | Priorité POC Wallerstein | +|---|---|---| +| **R6 — Diagnostic worker queue** | Faible (1-2 j-h) | 🔴 **P0 ABSOLU** (sinon POC impossible) | +| O1+O3+O4 — ContinuousLearner + Versioning | Moyen (2-3 j-h) | 🟠 P1 (apprentissage incrémental) | +| O2 — FeedbackProcessor | Lourd (3-5 j-h) | 🟠 P1 (fusion compétences) | +| O5 — RecoveryLogger runtime | Moyen (2 j-h) | 🟢 P2 (post-MVP) | +| **Total rebranchement (P0+P1)** | **6-10 j-h** | À comparer aux ~15 j-h de spécifications/impl P1 d'aujourd'hui qui les reconstruisaient partiellement de zéro | + +## §7 — Conséquences POC Wallerstein + +### En l'état actuel (rien rebranché) + +- ❌ **Aucune session traitée par le pipeline d'enrichissement profond depuis 5 jours** (worker tourne à vide). Cumulé des sessions live brutes accumulées : ScreenAnalyzer, CLIP, FAISS, GraphBuilder pas appliqués. +- ❌ **Pas d'apprentissage incrémental** : chaque démo Léa = repart de zéro. Pas d'auto-évaluation par répétition (alors que Dom le demande explicitement 20:46). +- ❌ **Pas de versioning prototypes** : si Easily/DPI change d'interface, pas de mécanisme de rollback. (Alors que Dom le demande 21:27.) +- ❌ **Pas de portabilité du modèle appris** : pas de paquet portable de réflexes/compétences/schémas/détecteurs/mappings, car ce paquet est produit par la chaîne d'apprentissage qui est débranchée. (Alors que Dom dit « point essentiel » 21:27.) +- ✅ Restitution Option C livrée aujourd'hui (P1-LEA-SHADOW) mais **trop longue** pour sessions 1-2h (recadrage Dom 20:46) +- ✅ Phase 2.5 sémantique livrée mais **ne produit pas encore les signaux de confiance/regroupement** demandés (recadrage Dom 20:46) + +### Avec rebranchement P0+P1 (6-10 j-h) + +- ✅ Worker pipeline actif → toutes les sessions live enrichies (ScreenAnalyzer/CLIP/FAISS/GraphBuilder) +- ✅ ContinuousLearner alimenté → apprentissage par répétition automatique +- ✅ FeedbackProcessor branché → fusion progressive vers compétences immuables +- ✅ PrototypeVersionManager actif → versioning mappings UI, rollback si dégradation +- ⚠️ Reste à ajouter : enrichir P1-LEA-SHADOW et P1-SEMANTIQUE avec champs `confidence`, `uncertainties[]`, `repetition_count`, distinctions `hypothesis`/`candidate`/`validated` (≈2-3 j-h additionnels) +- ⚠️ Reste à concevoir : paquet portable séparé de la mémoire patient (décision Dom 21:27 — pas encore couvert par aucune impl, ni avant aujourd'hui ni dans mes livraisons P1) (≈3-5 j-h) + +## §8 — Plan d'action recommandé + +### Étape A — IMMÉDIAT (avant tout nouveau dev) + +1. **Diagnostic R6 worker queue** (1-2 j-h) : pourquoi vide depuis 5 jours malgré sessions live actives +2. **Audit factuel modules orphelins** : confirmer le bon état du code de `ContinuousLearner`, `FeedbackProcessor`, `PrototypeVersionManager` (pas de bug bloquant, signatures à jour vis-à-vis du reste du codebase) +3. **Lecture par Dom** des modules orphelins pour confirmer qu'ils correspondent bien à son intention historique + +### Étape B — REBRANCHEMENT (P0+P1) + +4. Rebrancher worker queue (R6) — code probablement minimal, action chirurgicale +5. Rebrancher ContinuousLearner + supports (O1+O3+O4) avec tests intégration +6. Rebrancher FeedbackProcessor (O2) + hook dans `agent_chat/handlers/learn_action.py` (livré aujourd'hui) à chaque `POST /shadow/feedback` + +### Étape C — AJUSTEMENTS LIVRAISONS P1 + +7. Ajouter `confidence`, `uncertainties[]`, `repetition_count`, `hypothesis/candidate/validated` aux SessionState + payload persist +8. Phase 2.5 sémantique : enrichir pour produire signaux confiance + regroupement (actions stables vs parasites, invariants vs variables, blocs récurrents) +9. Option C restitution : raccourcir à « centré incertitudes uniquement », jamais relecture complète + +### Étape D — PORTABILITÉ (objectif Dom essentiel) + +10. Concevoir paquet portable : export réflexes/compétences/schémas/détecteurs/mappings/plans d'action/métriques, **sans** mémoire patient ni captures brutes +11. Mécanisme d'import sur poste tiers +12. Validation : aucune trace patient dans le paquet exporté + +## §9 — Sources de l'audit + +- Agent Explore Claude — 2026-06-01 21:50 (audit primaire) +- Audit Claude antérieur 17:00 (`feedback_lea_principes_techniques.md`) qui avait flagué `ContinuousLearner` et `RecoveryLogger` comme orphelins (mais sans alarme suffisante) +- Audit Explore worker VLM 17:30 (Claude) qui avait confirmé que worker traite sessions finalisées +- 5 messages Codex 2026-06-01 20:46-21:37 relayant 5 décisions/clarifications Dom +- Code source `core/learning/`, `core/healing/`, `agent_v0/server_v1/`, `agent_chat/` + +--- + +*Fin DRAFT — relecture Dom/Codex/Qwen attendue avant action.* + +**Décision opérationnelle proposée à Dom** : suspendre tout nouveau dev de modules d'apprentissage Léa tant que (a) R6 worker queue n'est pas diagnostiqué + corrigé et (b) Dom n'a pas confirmé que les modules orphelins identifiés correspondent à son intention historique. diff --git a/docs/POC/AUDIT_TOKEN_PAR_POSTE_2026-06-01.md b/docs/POC/AUDIT_TOKEN_PAR_POSTE_2026-06-01.md new file mode 100644 index 000000000..65db9c1e4 --- /dev/null +++ b/docs/POC/AUDIT_TOKEN_PAR_POSTE_2026-06-01.md @@ -0,0 +1,729 @@ +# AUDIT — Migration Token Global → Token Par-Poste +## rpa_vision_v3 — POC Clinique Wallerstein + +**Status** : DRAFT lecture seule — audit factuel — pas encore appliqué +**Date** : 2026-06-01 +**Scope** : Identification des modifications pour passer d'un token API global (partagé entre tous les postes) à un token unique par-poste permettant la révocation chirurgicale indépendante +**Contexte** : POC sur ≤5 TIM pendant plusieurs mois ; passage de 3 mois (doc initiale) à durée longue → risque amplifié de compromission d'un poste + +--- + +## 1. Source du Token Actuel — Facteurs Identifiés + +### 1.1 Génération et Stockage Côté Serveur + +**Fichier** : `agent_v0/server_v1/api_stream.py` (lignes 285-363) + +| Point | Facteur | +|-------|---------| +| **Variable env** | `RPA_API_TOKEN` (obligatoire en production) | +| **Mode génération** | Lecture depuis l'env OBLIGATOIRE (`os.getenv("RPA_API_TOKEN", "").strip()`) | +| **Tolérance dev** | Génération aléatoire via `secrets.token_hex(32)` si `RPA_AUTH_DISABLED=true` | +| **Fail-closed** | Production : token manquant → `sys.exit(1)` immédiat, pas de génération silencieuse | +| **Scope du token** | **GLOBAL** : un seul token pour tous les postes clients (tous les TIM partagent la même valeur) | +| **Type de valeur** | Hex string de 32 caractères (e.g. `a1b2c3d4e5f6...`) | +| **Lifecycle** | Aucune rotation built-in ; révocation → rotation manuelle de la var env + redémarrage du serveur | + +### 1.2 Distinction Admin/ReadOnly + +**Constat** : **N'existe pas actuellement.** + +- Un seul niveau de droit : droits R/W complets +- Tous les postes ont capacité à : capturer screenshot, invoquer VLM, lire/exécuter workflows +- Aucune notion de scope ou permission granulaire par token + +### 1.3 Lecture au Démarrage du Serveur + +**Code** (api_stream.py:301-328) : + +```python +_API_TOKEN_ENV = os.environ.get("RPA_API_TOKEN", "").strip() + +if _AUTH_DISABLED: + API_TOKEN = _API_TOKEN_ENV or secrets.token_hex(32) +elif not _API_TOKEN_ENV: + logger.critical("[SÉCURITÉ] FATAL — RPA_API_TOKEN est absent ou vide. ...") + sys.exit(1) +else: + API_TOKEN = _API_TOKEN_ENV +``` + +**Impact** : Token chargé une seule fois au boot, inchangé pendant toute la session du serveur. + +--- + +## 2. Fabrication du ZIP Installeur Léa — Flux Token + +### 2.1 Pipeline de Téléchargement du ZIP + +**Endpoint** : `web_dashboard/app.py` → `POST /api/fleet/download/` (ligne 2156) + +**Flux** : +``` +1. Dashboard Frontend (ui) demande au Dashboard Backend (5001) + ↓ +2. Backend lit la liste des agents enregistrés (AgentRegistry SQLite) + ↓ +3. Requête POST vers serveur streaming (5005) : /api/v1/agents/fleet + ↓ +4. Streaming renvoie liste agents (enrolled_agents table) + ↓ +5. Backend construit config.txt avec : + - RPA_SERVER_URL (toujours conforme, finit par /api/v1) + - RPA_API_TOKEN = valeur globale unique (tous les postes) + - RPA_MACHINE_ID = fourni par l'UI + - RPA_USER_LABEL = nom du collaborateur +``` + +### 2.2 Source du Token Injecté dans le ZIP + +**Fichier** : `web_dashboard/app.py` (ligne 2183) + +```python +api_token = os.environ.get('RPA_API_TOKEN', '') # <- MÊME VALEUR pour tous les postes +custom_config = _build_custom_config(machine_id, user_name, api_token) +``` + +**Constat** : +- Le token global de l'environnement du Dashboard (5001) est utilisé +- **Tous les ZIPs téléchargés contiennent le même token** (pas de diversification par poste) +- Aucune génération de token unique lors du POST /download + +### 2.3 Machine ID — Source et Validation + +**Provenance du machine_id** : +- Côté **client Windows** (Léa.bat / install.bat) : Inno Setup génère un UUID via la page custom d'enrollment +- Côté **serveur** : Enrôlement possible sans vérification du machine_id source (endpoint `/api/v1/agents/enroll` accepte n'importe quel machine_id du client) + +**Validation** : +- Pas de vérification cryptographique du machine_id (e.g. pas de signature pour prouver l'authenticité) +- Le machine_id est traité comme **identifiant de confiance** après enrôlement (logging, last_seen_at) + +### 2.4 Endpoint d'Enrôlement — État Actuel + +**Endpoint** : `agent_v0/server_v1/api_stream.py` → `POST /api/v1/agents/enroll` (ligne 6638) + +| Aspect | État Actuel | +|--------|-------------| +| **Qui génère le token ?** | Dashboard Backend (lit env `RPA_API_TOKEN`) | +| **Le client enrôlé reçoit quoi ?** | Le token global dans la réponse `/api/v1/agents/enroll` (ligne 6696) | +| **Existe-t-il un endpoint qui génère token unique ?** | Non. Phase 2 signalée dans le commentaire (ligne 6647) | +| **Stockage des tokens par-poste ?** | Non. Table `enrolled_agents` enregistre le poste mais pas le token | + +**Code** (api_stream.py:6688-6698) : + +```python +return { + "status": "enrolled", + "created": result["created"], + "reactivated": result["reactivated"], + "machine_id": machine_id, + # Phase 1 : on renvoie le token global pour que le client puisse + # verifier qu'il est bien aligne avec le serveur. Phase 2 pourra + # emettre un token par poste (issued_token != API_TOKEN global). + "api_token": API_TOKEN, # <- GLOBAL + "agent": agent, +} +``` + +--- + +## 3. Validation du Token Côté Serveur + +### 3.1 Middleware de Vérification + +**Fichier** : `agent_v0/server_v1/api_stream.py` (ligne 351) + +```python +async def _verify_token(request: Request): + """Middleware de vérification du token API Bearer.""" + if _AUTH_DISABLED: + return + if request.url.path in _PUBLIC_PATHS: + return + auth = request.headers.get("Authorization", "") + if not auth.startswith("Bearer ") or auth[7:] != API_TOKEN: + raise HTTPException(status_code=401, detail="Token API invalide") +``` + +**Constat** : +- **Comparaison à une constante** unique (pas de lookup table) +- Tous les endpoints protégés (sauf 5 publics) nécessitent ce token exact +- Le token est envoyé dans chaque requête HTTP : `Authorization: Bearer ` + +### 3.2 Granularité des Droits + +**Constat** : **Un seul niveau : R/W complet** + +- Pas de distinction admin/user/readonly +- Pas de scope granulaire (e.g. "capture seulement" vs "exécution workflow") +- Tous les agents ayant le bon token peuvent accéder à toutes les fonctions + +### 3.3 Machine ID et Audit + +**Machine_id dans les requêtes** : + +Le client (Léa) envoie le machine_id dans les headers ou dans le body de certaines requêtes. Côté serveur : + +- `audit_trail.record(AuditEntry(..., machine_id=client_provided_machine_id, ...))` (enregistré, pas vérifié) +- `agent_registry.touch_last_seen(machine_id)` (mise à jour timestamp) + +**Risque** : Aucune vérification que le machine_id envoyé par le client correspond au token utilisé. Un client pourrait usurper l'identité d'une autre machine en envoyant un autre machine_id + le token global. + +--- + +## 4. Points d'Appel Côté Agent Windows + +### 4.1 Lecture de config.txt au Démarrage + +**Fichier** : `agent_v0/agent_v1/config.py` (ligne 57) + +```python +API_TOKEN = os.environ.get("RPA_API_TOKEN", "") +``` + +Le token vient de trois sources (ordre de priorité) : +1. Variable env `RPA_API_TOKEN` (posée par Lea.bat après parsing de config.txt) +2. Fichier `config.txt` dans le ZIP (contient le token global) +3. Défaut : chaîne vide (agent lancé sans token) + +**Fichier** : `deploy/lea_package/Lea.bat` (ligne 40-43) + +```batch +if exist "config.txt" ( + for /f "usebackq eol=# tokens=1,* delims==" %%a in ("config.txt") do ( + if not "%%a"=="" if not "%%b"=="" set "%%a=%%b" + ) +) +``` + +Lea.bat parse config.txt ligne par ligne et pose les variables d'environnement (y compris `RPA_API_TOKEN`). + +### 4.2 Envoi du Token dans les Requêtes + +**Streaming WebSocket** : `agent_v0/agent_v1/network/streamer.py` (ligne ~80-90) + +```python +from ..config import API_TOKEN, STREAMING_ENDPOINT + +# Dans la fonction d'auth : +if API_TOKEN: + return {"Authorization": f"Bearer {API_TOKEN}"} +``` + +**Requêtes REST (executor)** : `agent_v0/agent_v1/core/executor.py` + +```python +if API_TOKEN: + headers["Authorization"] = f"Bearer {API_TOKEN}" +``` + +**Constat** : +- Token présent dans **chaque** requête (REST + WebSocket streaming) +- Format standard Bearer : `Authorization: Bearer ` +- Aucune logique de refresh ou renouvellement + +### 4.3 Renouvellement / Refresh du Token + +**Constat** : **N'existe pas.** + +- Token lu une seule fois au démarrage de l'agent +- Aucun mécanisme pour redemander un token au serveur sans redémarrer l'agent +- Révocation d'un token global = tous les agents existants deviennent invalides immédiatement (pas de transition gracieuse) + +--- + +## 5. Architecture Cible : Token Par-Poste + +### 5.1 Schéma de Stockage Côté Serveur + +**Nouvelle table SQLite** dans `data/databases/rpa_data.db` : + +```sql +CREATE TABLE IF NOT EXISTS postes_tokens ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + machine_id TEXT NOT NULL UNIQUE, + token_hash TEXT NOT NULL, -- SHA256(token) + token_readable TEXT NOT NULL, -- token lisible, jamais loggé + status TEXT DEFAULT 'active', -- 'active', 'revoked' + created_at TEXT NOT NULL, -- ISO 8601 UTC + revoked_at TEXT, -- ISO 8601 UTC si révoqué + rotated_at TEXT, -- ISO 8601 UTC si régénéré + last_seen_at TEXT, -- Dernière utilisation + rotation_reason TEXT -- 'manual_revoke', 'rotation_request', etc. +); + +CREATE INDEX idx_postes_tokens_machine_id ON postes_tokens(machine_id); +CREATE INDEX idx_postes_tokens_status ON postes_tokens(status); +``` + +**Co-habitation** : Table dans la même DB que `enrolled_agents` (même `data/databases/rpa_data.db`). + +### 5.2 Endpoint d'Enrôlement Amélioré + +**Route modifiée** : `POST /api/v1/agents/register` (nouvelle, plutôt que d'écraser `/agents/enroll`) + +```python +@app.post("/api/v1/agents/register", status_code=201) +async def agents_register(request: AgentRegisterRequest): + """ + Enrôlement du poste + génération du token unique. + + Comportement : + - Enregistre le poste (ou le réactive) dans enrolled_agents + - Génère un token unique via secrets.token_urlsafe(32) + - Stocke le hash SHA256 du token dans postes_tokens + - Retourne le token UNE SEULE FOIS dans la réponse + + Client doit sauvegarder ce token dans config.txt immédiatement. + Aucune autre route ne renvoie le token en clair. + """ + machine_id = (request.machine_id or "").strip() + if not machine_id: + raise HTTPException(status_code=400, detail="machine_id obligatoire") + + # 1. Enregistrer le poste (ou le réactiver) + try: + result = agent_registry.enroll(machine_id=machine_id, ...) + except AgentAlreadyEnrolledError: + # Poste déjà actif : générer un nouveau token pour rotation + pass + + # 2. Générer token unique + token_value = secrets.token_urlsafe(32) # e.g. "aB1_cD2-eF3...XyZ" + token_hash = hashlib.sha256(token_value.encode()).hexdigest() + + # 3. Insérer ou mettre à jour dans postes_tokens + postes_registry.create_or_rotate( + machine_id=machine_id, + token_hash=token_hash, + token_readable=token_value, # Stocké temporairement + rotation_reason="enrollment" + ) + + # 4. Retourner le token UNE SEULE FOIS + return { + "status": "registered", + "machine_id": machine_id, + "token": token_value, # <- À SAUVEGARDER IMMÉDIATEMENT + "expires_in": "never", # Sans expiration, mais révocable à tout moment + "agent": {...} + } +``` + +### 5.3 Modification du Middleware d'Auth + +**Fichier** : `agent_v0/server_v1/api_stream.py` (remplacer `_verify_token`) + +```python +async def _verify_token(request: Request): + """Middleware de vérification du token par-poste. + + Lookup du token dans postes_tokens au lieu de comparaison constante. + Cache mémoire pour éviter hammer SQLite à chaque requête. + """ + if _AUTH_DISABLED: + return + if request.url.path in _PUBLIC_PATHS: + return + + auth = request.headers.get("Authorization", "") + if not auth.startswith("Bearer "): + raise HTTPException(status_code=401, detail="Token invalide") + + token_value = auth[7:] + token_hash = hashlib.sha256(token_value.encode()).hexdigest() + + # 1. Chercher dans le cache (dict en mémoire) + if token_hash in _token_cache: + cached = _token_cache[token_hash] + if cached["status"] != "active": + raise HTTPException(status_code=401, detail="Token révoqué") + cached["last_used"] = time.time() + request.scope["machine_id"] = cached["machine_id"] # Injecter pour l'audit + return + + # 2. Chercher en DB + row = postes_registry.get_by_hash(token_hash) + if row is None or row["status"] != "active": + raise HTTPException(status_code=401, detail="Token invalide") + + # 3. Mettre en cache + _token_cache[token_hash] = { + "machine_id": row["machine_id"], + "status": row["status"], + "last_used": time.time(), + } + # Limiter la taille du cache (LRU, max 1000 tokens) + if len(_token_cache) > 1000: + oldest = min(_token_cache.items(), key=lambda x: x[1]["last_used"]) + del _token_cache[oldest[0]] + + # Injecter machine_id pour l'audit + request.scope["machine_id"] = row["machine_id"] + return +``` + +### 5.4 Endpoint de Révocation + +**Route** : `POST /api/v1/postes//revoke` (sécurisé par auth Bearer) + +```python +@app.post("/api/v1/postes/{machine_id}/revoke") +async def revoke_poste(machine_id: str): + """ + Révoque le token du poste (soft delete, audit conservé). + + Dashboard ou administrateur : révoque un poste compromis. + """ + # Vérifier que le token Bearer utilisé a le droit de révoquer + # (optionnel : admin-only, ou même machine_id peut révoquer son propre token) + + row = postes_registry.revoke(machine_id=machine_id, reason="admin_revoke") + if row is None: + raise HTTPException(status_code=404, detail="Machine not found") + + # Invalider le cache + if row["token_hash"] in _token_cache: + del _token_cache[row["token_hash"]] + + logger.info(f"[SECURITY] Poste révoqué : {machine_id}") + return {"status": "revoked", "machine_id": machine_id} +``` + +### 5.5 Endpoint de Régénération + +**Route** : `POST /api/v1/postes//rotate` (pour l'utilisateur du poste) + +```python +@app.post("/api/v1/postes/{machine_id}/rotate") +async def rotate_poste_token(machine_id: str): + """ + L'utilisateur du poste demande un nouveau token (le vieil est invalidé). + + Retourne le nouveau token (à injecter dans config.txt). + """ + # Vérifier que le token actuel appartient à ce machine_id + # (via injection dans request.scope par le middleware) + + new_token = secrets.token_urlsafe(32) + new_hash = hashlib.sha256(new_token.encode()).hexdigest() + + postes_registry.rotate( + machine_id=machine_id, + new_token_hash=new_hash, + rotation_reason="user_request" + ) + + # Invalider l'ancien token du cache + old_row = postes_registry.get_by_machine_id(machine_id) + if old_row and old_row["token_hash"] in _token_cache: + del _token_cache[old_row["token_hash"]] + + return { + "status": "rotated", + "machine_id": machine_id, + "new_token": new_token, + "hint": "Mettez à jour config.txt avec ce nouveau token" + } +``` + +### 5.6 UI Dashboard — Onglet Postes + +**Nouvelle page** : `/templates/fleet_management.html` + +Fonctionnalités : +- Liste des postes actifs (machine_id, user_name, last_seen_at, enrolled_at) +- Liste des postes révoqués (avec date révocation + raison) +- Boutons par poste : + - **Révoquer** : POST `/api/v1/postes/{machine_id}/revoke` → impossibilité pour le poste de se connecter + - **Régénérer token** : POST `/api/v1/postes/{machine_id}/rotate` → nouveau token à faire entrer sur le poste + - **Afficher détails** : historique de révocation/rotation +- Filtres : statut (actif/révoqué), plage de dates, collaborateur + +--- + +## 6. Chiffrage Qualitatif de la Migration + +### 6.1 Fichiers à Modifier + +| Fichier | Modification | Effort | Risque | +|---------|--------------|--------|--------| +| `agent_v0/server_v1/api_stream.py` | Remplacer `_verify_token`, ajouter `_token_cache`, endpoints `/api/v1/postes/*/revoke` et `/rotate` | **Moyen** (~200-250 lignes) | Moyen : tous les endpoints dépendent du middleware | +| `agent_v0/server_v1/agent_registry.py` | Ajouter classe `PostesTokenRegistry` (CRUD postes_tokens table) | **Moyen** (~150-200 lignes) | Faible : isolé, pas d'impact existant | +| `agent_v0/server_v1/` (nouveau) | `postes_registry.py` (classe PostesTokenRegistry) | **Moyen** (~150-200 lignes) | Faible : nouveau module | +| `web_dashboard/app.py` | Remplacer `_build_custom_config` + endpoint `/api/fleet/download` pour générer token unique | **Moyen** (~100-150 lignes) | Moyen : impact sur téléchargement ZIP | +| `web_dashboard/templates/` | Ajouter `fleet_management.html` (onglet postes) | **Moyen** (~300-400 lignes HTML/JS) | Faible : interface nouvelle, pas de régression | +| `agent_v0/agent_v1/config.py` | Pas de modification (lecture env inchangée) | **Aucun** | Aucun | +| `deploy/installer/Lea.iss` | Pas de modification (Inno Setup pose la var env) | **Aucun** | Aucun | + +**Total effort** : ~1000-1300 lignes de code nouveau / modifié + +### 6.2 Migration Progressive en 3 Étapes + +#### Étape A : Fondations (1-2 jours) + +**Objectif** : Ajouter la table + le registre, mais le serveur accepte les 2 modes. + +1. Créer table `postes_tokens` + index dans `data/databases/rpa_data.db` +2. Implémenter classe `PostesTokenRegistry` (CRUD simple) +3. Modifier `_verify_token` pour : + - D'abord chercher le token en par-poste (lookup table) + - Si non trouvé, faire un fallback sur le mode global (API_TOKEN) +4. Logger chaque validation en clair : `[AUTH] Token trouvé : par-poste | global` + +**Pre-requis** : Aucun + +**Durée estimée** : 1-2 jours +**Point de non-retour** : Non (le fallback permet de revenir en arrière) +**Impact utilisateurs** : Nul (transparent, les agents existants continuent avec le global) + +**Code exemple (middleware)**: +```python +async def _verify_token(request: Request): + auth = request.headers.get("Authorization", "") + if not auth.startswith("Bearer "): + raise HTTPException(status_code=401) + + token_value = auth[7:] + token_hash = hashlib.sha256(token_value.encode()).hexdigest() + + # Mode 1 : Par-poste + row = postes_registry.get_by_hash(token_hash) + if row and row["status"] == "active": + logger.info(f"[AUTH] Token par-poste validé : {row['machine_id']}") + request.scope["machine_id"] = row["machine_id"] + return + + # Mode 2 : Global (fallback, phase 1) + if token_value == API_TOKEN: + logger.info(f"[AUTH] Token global validé (fallback phase 1)") + return + + raise HTTPException(status_code=401, detail="Token invalide") +``` + +#### Étape B : Redéploiement sur Postes Wallerstein (3-5 jours) + +**Objectif** : Migrer les 5 postes du POC en par-poste (ZIPs avec tokens uniques). + +1. Nouvelle version du Dashboard : endpoint `/api/v1/agents/register` qui génère tokens uniques +2. Modifier `/api/fleet/download/` pour injecter le token unique (pas le global) +3. Sur chaque poste Wallerstein : + - Télécharger le nouveau ZIP via le Dashboard + - Extraire et remplacer config.txt + - Redémarrer Léa + +**Pre-requis** : Étape A complète et testée + +**Durée estimée** : 3-5 jours (déploiement sur le terrain + tests) +**Point de non-retour** : OUI — une fois que tous les postes ont le par-poste, l'admin peut supprimer API_TOKEN global +**Impact utilisateurs** : Très faible (ZIPs pré-générés, transparents pour les utilisateurs) + +**Checklist de déploiement** : +- [ ] Tester endpoint `/api/v1/agents/register` en lab +- [ ] Générer 5 ZIPs avec tokens uniques (une clé par poste) +- [ ] Vérifier que config.txt contient le bon token pour chaque machine_id +- [ ] Déployer sur poste 1, vérifier dernière utilisation dans Dashboard +- [ ] Idem postes 2-5 +- [ ] Vérifier historique audit : chaque poste tracé séparément + +#### Étape C : Durcissement — Suppression du Global (1 jour) + +**Objectif** : Désactiver le mode global, garantir que seule l'authentification par-poste fonctionne. + +1. Retirer le fallback global de `_verify_token` +2. Supprimer variable env `RPA_API_TOKEN` du bootstrap +3. Logger un FATAL si la variable est trouvée (debug) +4. Mettre à jour la doc : "Mode global dépréciéé depuis 2026-06-XX" + +**Pre-requis** : Tous les postes en par-poste (Étape B complète) + +**Durée estimée** : 1 jour +**Point de non-retour** : OUI — après ce point, les anciens agents (mode global) ne peuvent plus se connecter +**Impact utilisateurs** : ÉLEVÉ si Étape B incomplète (agents bloqués) + +--- + +## 7. Risques Non Couverts Par la Migration + +### 7.1 Vol Physique du PC TIM avec config.txt en Clair + +**Risque** : Token stocké en clair dans le fichier `config.txt` (Windows, pas chiffré par défaut) + +**Mitigations possibles** : +- DPAPI Windows : Chiffrer config.txt via `CryptProtectData` (nécessite clé utilisateur Windows) +- Stockage sécurisé : Léa peut demander token à la première utilisation (sans le persister) +- JWT court + refresh : Token courte durée de vie (~30 min) + refresh token distinct (mais complexe) +- Jeton hardware : Clé USB ou smartcard (hors scope POC) + +**Recommandation** : Pour Wallerstein, utiliser DPAPI Windows (effort faible, sécurité augmentée). + +### 7.2 Replay Attacks sur le Streaming WebSocket + +**Risque** : Sans TLS strict (HTTPS/WSS), un attaquant en MITM peut rejouer les requêtes vidées du token Bearer. + +**Mitigations actuelles** : +- Reverse proxy NPM (déjà en place) expose lea.labs.laurinebazin.design en HTTPS +- WSS (WebSocket Secure) utilisé en production (non en lab) + +**Recommandation** : Vérifier que tous les endpoints streaming utilisent WSS en production (config NPM). + +### 7.3 Token Enregistré dans les Logs + +**Risque** : Si Flask ou Uvicorn log les en-têtes HTTP en DEBUG, le token Bearer s'affiche en clair. + +**Prévention** : +- Mode production (défaut) : `docs_url=None, redoc_url=None` (api_stream.py:469-471) +- Vérifier que les logs ne contiennent jamais `Authorization: Bearer ...` + +**Commande de vérification** : +```bash +grep -r "Authorization.*Bearer" /path/to/logs/ # Ne doit retourner rien +grep -r "RPA_API_TOKEN" /path/to/logs/ # Ne doit retourner rien +``` + +### 7.4 Usurpation d'Identité : Machine_ID + +**Risque** : Actuellement, aucune vérification que le machine_id envoyé par le client correspond au token utilisé. + +**Scénario** : Client A possède token de poste A. Poste A envoie requête avec `machine_id=poste_B` → audit imputé à poste B. + +**Mitigation dans l'architecture cible** : +- Dès que le token est validé, extraire automatiquement le machine_id depuis la table postes_tokens +- Rejecter toute tentative du client d'envoyer un machine_id différent + +```python +async def _verify_token(request: Request): + # ... validation token ... + row = postes_registry.get_by_hash(token_hash) + + # Injecter l'identité vraie dans la requête + request.scope["machine_id"] = row["machine_id"] # <- Vrai machine_id + request.scope["user_id"] = row["user_id"] # <- Vrai user_id + + # Ne pas faire confiance au client pour l'identité +``` + +### 7.5 Absence de Rotation Forcée + +**Risque** : Aucune rotation obligatoire (ex. tous les 90 jours). Un token compromis et non détecté reste valide indéfiniment. + +**Mitigation pour Wallerstein** : +- Ajouter `last_rotation_at` et `rotation_period_days` à la table postes_tokens +- Endpoint `/api/v1/postes/{machine_id}/status` retourne un warning si rotation > 90 jours +- Dashboard affiche visuel "Token à renouveler" (jaune : 60j, rouge : >90j) + +**Effort** : Moyen (~50-100 lignes) + +--- + +## 8. Verdict et Recommandations + +### Effort Global + +**Catégorie** : **MOYEN** (4-8 jours-homme, 1000-1300 lignes, pas d'architecte externe requis) + +- Fondations (Étape A) : 1-2 jours +- Déploiement terrain (Étape B) : 3-5 jours (nécessite accès aux postes Wallerstein) +- Durcissement (Étape C) : 1 jour + +### Durée Estimée + +| Phase | Durée | Bloqué Par | +|-------|-------|-----------| +| A (Fondations) | 1-2 j | Aucun | +| B (Déploiement) | 3-5 j | A complète | +| C (Durcissement) | 1 j | B complète | +| **TOTAL** | **5-8 j** | — | + +### Dépendances Connues + +**Aucune dépendance blocage avec autres chantiers actuels** : +- Worktree guard Qwen : Isolé, pas d'impact (module core/) +- Gap propagation verdict : Isolé (migration replay) +- Mismatch captures persistées : Isolé (API /traces/upload) + +**Dépendance suggérée** : Implémenter mitigations § 7.1-7.5 après la migration (durcissement sécurité, effort marginal). + +### Recommandation + +**GO pour le POC Wallerstein** (approche par étapes) : + +1. **Étape A maintenant** : Ajouter table + registre + middleware 2 modes (1-2 jours) +2. **Étape B début Wallerstein** : Redéployer les 5 postes avec tokens uniques (3-5 jours, pendant POC) +3. **Étape C post-POC** : Durcir si compromise d'un poste détectée, ou en fin de POC + +Cette approche offre la **révocation chirurgicale** dès l'Étape B (capacité à révoquer un poste sans affecter les autres), tout en gardant une marche arrière jusqu'à la fin de l'Étape B. + +--- + +## Annexe A : Schéma de Synthèse + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Système Actuel (Phase 1 — Token Global) │ +├─────────────────────────────────────────────────────────────┤ +│ • RPA_API_TOKEN = "a1b2c3d4...xyz" (1 seul, dans .env) │ +│ • Tous les postes envoient le MÊME token │ +│ • Aucune révocation fine (révoque tout ou rien) │ +│ • Audit : machine_id non vérifiable (peut être usurpé) │ +└─────────────────────────────────────────────────────────────┘ + ↓ + (Étape A : 1-2j) + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ État Intermédiaire (Phase 1.5 — Dual Mode) │ +├─────────────────────────────────────────────────────────────┤ +│ • Table postes_tokens ajoutée │ +│ • Middleware accepte tokens par-poste OU global │ +│ • Anciens agents (global) : continuent à fonctionner │ +│ • Nouveaux agents (par-poste) : peuvent être enrôlés │ +└─────────────────────────────────────────────────────────────┘ + ↓ + (Étape B : 3-5j, terrain) + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Cible (Phase 2 — Token Par-Poste) │ +├─────────────────────────────────────────────────────────────┤ +│ • Poste A : token_A (unique) → machine_id_A │ +│ • Poste B : token_B (unique) → machine_id_B │ +│ • Poste C : token_C (unique) → machine_id_C │ +│ • ... │ +│ • Révocation poste A : ne touche pas B, C, D, E │ +│ • Audit : machine_id garanti (extrait du token validé) │ +└─────────────────────────────────────────────────────────────┘ + ↓ + (Étape C : 1j) + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Durcissement (Phase 3 — Suppression Global) │ +├─────────────────────────────────────────────────────────────┤ +│ • RPA_API_TOKEN supprimé du bootstrap │ +│ • Middleware accepte UNIQUEMENT tokens par-poste │ +│ • Anciens agents (global) : rejetés (404) │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Annexe B : Checklist de Vérification Pré-Déploiement + +- [ ] **DB** : Table postes_tokens créée et indexée, migration de la DB testée +- [ ] **Code** : Middleware dual-mode (par-poste + fallback global) compilé +- [ ] **Cache** : Vérifier que le cache mémoire ne fuit pas (taille bornée à 1000 tokens) +- [ ] **Audit** : Chaque validation loggée avec indication du mode (par-poste ou global) +- [ ] **Endpoint register** : Génère token unique, hash stocké en DB, token retourné UNE FOIS +- [ ] **Endpoint revoke** : Invalidation du token en DB + vidage du cache +- [ ] **Endpoint rotate** : Génère nouveau token, ancien invalidé +- [ ] **UI Dashboard** : Affichage liste postes (actifs + révoqués), boutons revoke/rotate +- [ ] **Tests unitaires** : Vérifier que token global + par-poste fonctionnent (Étape A) +- [ ] **Tests intégration** : Workflow complet (enroll → register → streaming → audit) +- [ ] **Logs de sécurité** : Aucun token en clair, aucune variable d'env loggée +- [ ] **Documentation** : Mise à jour du PLAYBOOK_DSI_RSSI avec procédure par-poste + +--- + +**Document généré** : 2026-06-01 +**Auteur** : Audit factuel (sans modifications de code) +**Validation requise avant implémentation** : Dom (chef projet) diff --git a/docs/POC/REQUIREMENTS_DGX_AARCH64_DRAFT_2026-06-01.md b/docs/POC/REQUIREMENTS_DGX_AARCH64_DRAFT_2026-06-01.md new file mode 100644 index 000000000..7c8a1e422 --- /dev/null +++ b/docs/POC/REQUIREMENTS_DGX_AARCH64_DRAFT_2026-06-01.md @@ -0,0 +1,313 @@ +# requirements-dgx-aarch64.txt — DRAFT pour relecture Codex/Dom + +**Statut** : DRAFT — pas encore appliqué. Aucun changement au `requirements.txt` réel. +**Date** : 2026-06-01 +**Cible** : DGX Spark, Ubuntu 24.04 ARM64, CUDA 13 système, GB10 (sm_121), container Docker headless serveur. +**Source de vérité** : `docs/POC/PORTAGE_DGX_SPARK_2026-05-28.md` §5. +**Base** : `requirements.txt` actuel = 180 paquets — bench x86 `venv_v3/`. + +--- + +## Règles appliquées + +- PyTorch : `torch`, `torchvision` désépinglés, installés via `--index-url https://download.pytorch.org/whl/cu128` (binaire sm_120 compat sm_121). +- Wheels GPU NVIDIA (`nvidia-*-cu12`, 15 paquets) : **supprimés**, tirés transitivement par torch cu128 ARM. +- Capture / GUI / X11 : **supprimés** — container serveur headless ; l'agent capture tourne sur poste Linux client séparé. +- `onnx==1.20.1` : **supprimé** (audit dépendances : fantôme transitif). +- Reste : wheels neutres ARM/CPU **gardés tels quels** ; wheels torch-dependent **désépinglés** pour laisser pip résoudre. + +--- + +## 1. GARDÉ TEL QUEL (paquets neutres ARM/CPU) + +Wheels pur Python ou wheels manylinux2014_aarch64 / musllinux_aarch64 publiés sur PyPI. §5 RAS pour les binaires explicitement listés. + +- **Web/API** : `fastapi`, `Flask`, `Flask-Caching`, `Flask-Cors`, `Flask-Migrate`, `Flask-SocketIO`, `Flask-SQLAlchemy`, `starlette`, `uvicorn`, `uvloop` (§5), `httptools`, `httpcore`, `httpx`, `h11`, `websockets`, `wsproto`, `simple-websocket`, `python-engineio`, `python-socketio`, `python-multipart`, `Werkzeug`, `Jinja2`, `itsdangerous`, `blinker`, `click`, `MarkupSafe`, `cachelib`, `watchfiles`, `anyio`, `bidict` +- **DB/migrations** : `SQLAlchemy`, `alembic`, `Mako`, `greenlet`, `redis` +- **ML/vision CPU** : `faiss-cpu` (§5), `opencv-python` (§5), `pillow`, `numpy`, `scipy`, `scikit-learn`, `matplotlib`, `contourpy`, `fonttools`, `kiwisolver`, `cycler`, `pyparsing`, `ml_dtypes`, `networkx`, `sympy`, `mpmath`, `joblib`, `threadpoolctl`, `RapidFuzz`, `shapely` (§5), `pyclipper` (§5), `h5py` (§5) +- **PDF/docs** : `pypdfium2` (§5), `lxml` (§5), `python-docx`, `openpyxl`, `et_xmlfile`, `defusedxml`, `anyascii`, `langdetect`, `ftfy`, `wcwidth`, `regex` +- **Ollama** : `ollama` (client HTTP pur Python) +- **Validation/config** : `pydantic`, `pydantic_core`, `annotated-doc`, `annotated-types`, `typing-inspection`, `typing_extensions`, `validators`, `marshmallow`, `jsonschema`, `jsonschema-specifications`, `referencing`, `rpds-py`, `attrs`, `PyYAML`, `python-dotenv` +- **HTTP/réseau** : `aiohttp`, `aiohappyeyeballs`, `aiosignal`, `frozenlist`, `multidict`, `propcache`, `yarl`, `requests`, `urllib3`, `charset-normalizer`, `idna`, `certifi` +- **Crypto/utils** : `cryptography`, `cffi`, `pycparser`, `filelock`, `fsspec`, `packaging`, `platformdirs`, `pathspec`, `six`, `python-dateutil`, `sortedcontainers`, `tqdm`, `Pygments` +- **Monitoring** : `prometheus_client`, `psutil`, `pynvml`, `nvidia-ml-py` (bindings NVML pur Python, pas un wheel CUDA toolkit) +- **Protobuf** : `protobuf` (§5) +- **Dev/tests** : `pytest`, `pytest-asyncio`, `pytest-cov`, `pytest-flask`, `pytest-mock`, `hypothesis`, `coverage`, `iniconfig`, `pluggy`, `black`, `flake8`, `mypy`, `mypy_extensions`, `mccabe`, `pycodestyle`, `pyflakes` + +## 2. GARDÉ MAIS DÉSÉPINGLÉ (laisser pip résoudre avec torch cu128 ARM) + +| Paquet | Version actuelle x86 | Raison | +|--------|----------------------|--------| +| `torch` | `2.9.1` | installé via index PyTorch cu128 ARM (étape 1 ci-dessous) | +| `torchvision` | `0.24.1` | idem, doit matcher torch cu128 ARM | +| `transformers` | `4.57.3` | dépend de torch — laisser résoudre | +| `accelerate` | `1.13.0` | dépend de torch | +| `timm` | `1.0.24` | dépend de torch | +| `open_clip_torch` | `3.2.0` | dépend de torch | +| `python-doctr` | `1.0.1` | dépend de torch + torchvision (§5 RAS) | +| `safetensors` | `0.7.0` | binding Rust, couplé à transformers/torch | +| `tokenizers` | `0.22.2` | binding Rust, couplé à transformers | +| `huggingface-hub` | `0.36.0` | couplé à transformers | +| `hf-xet` | `1.2.0` | binding Rust, wheel aarch64 à vérifier (cf. §4) | + +## 3. SUPPRIMÉ + +### 3a. Capture / GUI / X11 (serveur DGX headless) + +`PyQt5`, `PyQt5-Qt5`, `PyQt5_sip`, `mss`, `PyAutoGUI`, `pynput`, `evdev`, `python-xlib`, `python3-xlib`, `pystray`, `PyGetWindow`, `PyMsgBox`, `PyScreeze`, `MouseInfo`, `pyperclip`, `pytweening`, `PyRect` — tous hors scope container serveur (§5 ligne 81 : non bloquant si non chargés). Restent sur le poste Linux client de l'agent capture. + +### 3b. Wheels GPU NVIDIA épinglées x86 (15 paquets) + +`nvidia-cublas-cu12`, `nvidia-cuda-cupti-cu12`, `nvidia-cuda-nvrtc-cu12`, `nvidia-cuda-runtime-cu12`, `nvidia-cudnn-cu12`, `nvidia-cufft-cu12`, `nvidia-cufile-cu12`, `nvidia-curand-cu12`, `nvidia-cusolver-cu12`, `nvidia-cusparse-cu12`, `nvidia-cusparselt-cu12`, `nvidia-nccl-cu12`, `nvidia-nvjitlink-cu12`, `nvidia-nvshmem-cu12`, `nvidia-nvtx-cu12` — §5 ligne 78 : désépinglés, retirés du requirements, tirés par torch cu128 ARM. + +### 3c. Fantôme + +`onnx==1.20.1` — audit runtime : non importé par le code server. `onnxruntime` déjà confirmé non utilisé (cf. brief Dom). Suppression nette. + +## 4. À VÉRIFIER (validation Codex/Dom requise) + +| Paquet | Question | Action proposée | +|--------|----------|-----------------| +| `triton` | §5 risque ligne 79 : PTXAS embarqué peut crasher avec CUDA 13 | Non listé (tiré par torch cu128) ; si KO au runtime : `export TRITON_PTXAS_PATH=/usr/local/cuda/bin/ptxas` | +| `hf-xet` | Wheel aarch64 publié au moment du build ? | Désépingler ; si non dispo, retirer (dep optionnelle de `huggingface-hub`) | +| `accelerate` | Version compatible torch cu128 ARM ? | Désépinglé (cat. 2) — flag pour test post-install | +| `onnx` (suppression) | Confirmé sur scope server/ + core/grounding/ + core/detection/ ? | `grep -r "^import onnx\|^from onnx" --include='*.py' server/ core/` avant build | +| `nvidia-ml-py` vs `pynvml` | Doublon (officiel vs fork tiers) | Probable retrait de `pynvml`, à arbitrer | + +--- + +## 5. Bloc `requirements-dgx-aarch64.txt` proposé + +Installation en 2 étapes (cu128 ARM **avant** le reste, sinon pip tire les wheels x86 du cache) : + +```bash +# Étape 1 : PyTorch ARM cu128 +pip install --no-cache-dir torch torchvision \ + --index-url https://download.pytorch.org/whl/cu128 + +# Étape 2 : reste +pip install --no-cache-dir -r requirements-dgx-aarch64.txt +``` + +```txt +# requirements-dgx-aarch64.txt +# Cible : DGX Spark / Ubuntu 24.04 ARM64 / CUDA 13 / GB10 sm_121 +# Pré-requis : torch+torchvision installés via index cu128 ARM AVANT ce fichier. + +# --- Stack torch-dependent (désépinglé) --- +transformers +accelerate +timm +open_clip_torch +python-doctr +safetensors +tokenizers +huggingface-hub +hf-xet + +# --- Web / API --- +fastapi==0.128.0 +Flask==3.0.0 +Flask-Caching==2.1.0 +Flask-Cors==4.0.0 +Flask-Migrate==4.1.0 +Flask-SocketIO==5.3.5 +Flask-SQLAlchemy==3.1.1 +starlette==0.50.0 +uvicorn==0.40.0 +uvloop==0.22.1 +httptools==0.7.1 +httpcore==1.0.9 +httpx==0.28.1 +h11==0.16.0 +websockets==16.0 +wsproto==1.3.2 +simple-websocket==1.1.0 +python-engineio==4.8.0 +python-socketio==5.10.0 +python-multipart==0.0.21 +Werkzeug==3.1.5 +Jinja2==3.1.6 +itsdangerous==2.2.0 +blinker==1.9.0 +click==8.3.1 +MarkupSafe==3.0.3 +cachelib==0.9.0 +watchfiles==1.1.1 +anyio==4.12.1 +bidict==0.23.1 + +# --- DB / migrations --- +SQLAlchemy==2.0.23 +alembic==1.18.4 +Mako==1.3.10 +greenlet==3.3.0 +redis==5.0.1 + +# --- ML / vision CPU --- +faiss-cpu==1.13.2 +opencv-python==4.12.0.88 +pillow==12.1.0 +numpy==2.2.6 +scipy==1.17.0 +scikit-learn==1.8.0 +matplotlib==3.10.8 +contourpy==1.3.3 +fonttools==4.62.1 +kiwisolver==1.5.0 +cycler==0.12.1 +pyparsing==3.3.2 +ml_dtypes==0.5.4 +networkx==3.6.1 +sympy==1.14.0 +mpmath==1.3.0 +joblib==1.5.3 +threadpoolctl==3.6.0 +RapidFuzz==3.14.3 +shapely==2.1.2 +pyclipper==1.4.0 +h5py==3.16.0 + +# --- PDF / docs --- +pypdfium2==5.6.0 +lxml==6.0.2 +python-docx==1.2.0 +openpyxl==3.1.5 +et_xmlfile==2.0.0 +defusedxml==0.7.1 +anyascii==0.3.3 +langdetect==1.0.9 +ftfy==6.3.1 +wcwidth==0.2.14 +regex==2025.11.3 + +# --- Ollama --- +ollama==0.6.1 + +# --- Validation / config --- +pydantic==2.12.5 +pydantic_core==2.41.5 +annotated-doc==0.0.4 +annotated-types==0.7.0 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +validators==0.35.0 +marshmallow==3.20.1 +jsonschema==4.20.0 +jsonschema-specifications==2025.9.1 +referencing==0.37.0 +rpds-py==0.30.0 +attrs==25.4.0 +PyYAML==6.0.1 +python-dotenv==1.0.0 + +# --- HTTP / réseau --- +aiohttp==3.13.3 +aiohappyeyeballs==2.6.1 +aiosignal==1.4.0 +frozenlist==1.8.0 +multidict==6.7.0 +propcache==0.4.1 +yarl==1.22.0 +requests==2.32.5 +urllib3==2.6.3 +charset-normalizer==3.4.4 +idna==3.11 +certifi==2026.1.4 + +# --- Crypto / utils --- +cryptography==46.0.3 +cffi==2.0.0 +pycparser==2.23 +filelock==3.20.3 +fsspec==2026.1.0 +packaging==25.0 +platformdirs==4.5.1 +pathspec==1.0.3 +six==1.17.0 +python-dateutil==2.8.2 +sortedcontainers==2.4.0 +tqdm==4.67.1 +Pygments==2.19.2 + +# --- Monitoring --- +prometheus_client==0.23.1 +psutil==7.2.1 +pynvml==13.0.1 +nvidia-ml-py==13.590.48 + +# --- Protobuf --- +protobuf==7.34.0 + +# --- Dev / tests (à isoler dans requirements-dev.txt à terme) --- +pytest==9.0.2 +pytest-asyncio==1.3.0 +pytest-cov==4.1.0 +pytest-flask==1.3.0 +pytest-mock==3.12.0 +hypothesis==6.92.1 +coverage==7.13.1 +iniconfig==2.3.0 +pluggy==1.6.0 +black==23.12.1 +flake8==6.1.0 +mypy==1.7.1 +mypy_extensions==1.1.0 +mccabe==0.7.0 +pycodestyle==2.11.1 +pyflakes==3.1.0 +``` + +**Bilan** : 180 → ~125 lignes. Retraits = 17 capture/GUI + 15 nvidia-cu12 + 1 onnx + 11 remontés cat. 2 désépinglée (torch/torchvision/transformers/accelerate/timm/open_clip_torch/python-doctr/safetensors/tokenizers/huggingface-hub/hf-xet) = 44 lignes en moins, 9 désépinglées remontées en tête. + +--- + +## 6. Vérifications post-install sur DGX + +```bash +# 1. PyTorch + GPU sm_121 +python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_capability(0))" +# attendu : (True, (12, 1)) + +python -c "import torch; print(torch.__version__, torch.version.cuda)" +# attendu : 2.9.x+ / 12.8 + +# 2. FAISS +python -c "import faiss; print(faiss.__version__)" +# attendu : 1.13.x + +# 3. Transformers stack +python -c "import transformers; print(transformers.__version__)" +python -c "import accelerate, timm, open_clip, doctr; print(accelerate.__version__, timm.__version__, open_clip.__version__, doctr.__version__)" + +# 4. Bindings Rust aarch64 +python -c "import tokenizers, safetensors; print(tokenizers.__version__, safetensors.__version__)" +python -c "import hf_xet; print(hf_xet.__version__)" # point de vérif cat. 4 + +# 5. Confirmation onnx absent et runtime OK +python -c "import onnx" 2>&1 | grep -q ModuleNotFoundError && echo "OK : onnx absent" + +# 6. Points d'entrée applicatifs +python -c "from web_dashboard.app import app; print('dashboard OK')" +python -c "from visual_workflow_builder.backend.app import app; print('VWB backend OK')" +# adapter le module server selon le wiring réel + +# 7. Sanity GPU +python -c "import torch; x=torch.randn(8,8,device='cuda'); print('matmul GPU OK', (x@x).device)" + +# 8. Ollama client → daemon +python -c "import ollama; print(ollama.list())" +``` + +**Garde-fous §5** : +- Si kernels Triton crashent : `export TRITON_PTXAS_PATH=/usr/local/cuda/bin/ptxas`. +- Si pip tire encore des `nvidia-*-cu12` x86 : confirmer que l'étape 1 (torch cu128 ARM) a été exécutée **avant**, et que `--no-cache-dir` est bien passé. + +--- + +## 7. Points ouverts pour Codex/Dom + +1. Confirmer suppression `onnx` : `grep -r "^import onnx\|^from onnx" --include='*.py' server/ core/` avant build image Docker. +2. Splitter dès le POC `requirements-dev.txt` (pytest/black/mypy/flake8/coverage/hypothesis) pour ne pas polluer l'image server ? À trancher. +3. `hf-xet` : si wheel aarch64 indispo au build, retirer (dep optionnelle `huggingface-hub`). +4. `nvidia-ml-py` vs `pynvml` : doublon. Garder uniquement `nvidia-ml-py` (officiel) ? diff --git a/docs/POC/SPECS_AGENT_CHAT_LEARN_ACTION_2026-06-01.md b/docs/POC/SPECS_AGENT_CHAT_LEARN_ACTION_2026-06-01.md new file mode 100644 index 000000000..42d589ae1 --- /dev/null +++ b/docs/POC/SPECS_AGENT_CHAT_LEARN_ACTION_2026-06-01.md @@ -0,0 +1,240 @@ +# SPECS_AGENT_CHAT_LEARN_ACTION — 2026-06-01 + +> **DRAFT specs pour implémentation — pas encore appliqué.** +> Nouveau module `agent_chat/handlers/learn_action.py` orchestrant le dialogue Léa-first conversationnel et le raccordement au cycle Shadow existant (streaming server port 5005). +> Référence d'archi : Claude 2026-06-01 16:20 + addendum 17:45. Audit 17:00 : pas de nouveau code dashboard pour l'apprentissage — tout passe par agent-chat (port 5004). + +## Sommaire + +1. Architecture du module +2. Intent recognition (3 approches, reco hybride) +3. Mapping intents → appels Shadow +4. Formateur Option C (restitution texte + libellés OCR) +5. Itération correction (phase 4) +6. Phase 5 — nomination + scope (paramètres vs constantes) +7. Hooks mode proactif (LoopDetector) +8. Persistance d'état (session_id.json) +9. Tests à prévoir +10. Estimation effort implémentation + +--- + +## 1. Architecture du module + +**Fichier cible** : `agent_chat/handlers/learn_action.py` (nouveau, dossier `handlers/` à créer). + +**Classe principale** : `LearnActionOrchestrator` +- Instanciée une fois par `agent_chat/app.py` au démarrage (singleton via `get_learn_action_orchestrator()` cohérent avec `intent_parser`, `confirmation`, `conversation_manager`). +- Injection : `StreamingClient` (wrapper httpx sync vers `RPA_STREAMING_URL` port 5005), `IntentParser` (réutilisé), `ResponseGenerator` (formuler phrases Léa), callback `emit_dual` (réutilise `_emit_dual` de `app.py` pour socket.io). + +**Machine d'état finie** (Enum `LearnState`) : + +``` +IDLE + → LISTENING (déclencheur : bouton 🎓 ou phrase magique → POST /shadow/start) + → WAITING_USER_STOP (Léa observe en silence ; ping périodique notifications) + → ANALYZING (utilisateur a dit "stop" → POST /shadow/stop) + → PRESENTING (Léa restitue Option C dans le chat) + → ITERATING_FEEDBACK (boucle correction step-par-step) + → NAMING (Léa demande nom + scope paramètres) + → PERSISTING (POST /api/v1/lea/competences/candidate/persist) + → DONE | ABORTED +``` + +Transitions illégales rejetées + log. Chaque transition réussie : (a) sauvegarde d'état §8, (b) émission socket.io `lea:learn_state_changed`. + +**Persistance d'état par session** : `agent_chat/state/.json` (dossier créé au démarrage). Format §8. + +**Interface streaming server (5005)** : +- Client httpx sync (cohérence avec `requests` déjà utilisé dans `app.py:31`). +- Wrapper `StreamingClient` (~50 lignes) : `shadow_start`, `shadow_stop`, `shadow_understanding`, `shadow_feedback`, `shadow_build`, `competence_persist`. +- Timeout 5s par appel, retry x2 sur `ConnectionError`, logging structuré. +- Auth via `RPA_API_TOKEN` env var (réutilise `_streaming_headers()`). + +**Découplage** : le module ne touche **jamais** directement à `app.py` ni aux templates HTML. Il expose API Python (`start_session`, `handle_chat_message`, `handle_proactive_signal`, `resume_session`). C'est `app.py` qui route les messages du chat vers ce handler quand `LearnState != IDLE`. + +## 2. Intent recognition + +Pendant une session, messages utilisateur en français libre. Léa doit comprendre : start, stop, validate_step, correct_step, name_competence, mark_parameter, cancel. + +**3 approches** : + +- **(a) Phrases magiques regex** — déterministe, 0 latence, fragile sur variantes. +- **(b) NLU léger `qwen2.5:0.5b` Ollama** — robuste paraphrases, latence ~200-500ms, format=json mode. +- **(c) Hybride a → b — RECOMMANDÉ** : table regex sur ~80% des cas évidents, fallback `qwen2.5:0.5b` si aucun match avec confidence ≥ 0.9. Si LLM `confidence < 0.7` → Léa demande clarification. + +**Implémentation** : sous-classe `LearnIntentParser` qui *étend* `IntentParser` existant en ajoutant enum `LearnIntent` (distinct de `IntentType`). + +## 3. Mapping intents → appels Shadow + +| Intent (`LearnIntent`) | Phase | Appel HTTP (5005) | Payload clé | Effet | +|---|---|---|---|---| +| `START_OBSERVE` | IDLE → LISTENING | `POST /api/v1/shadow/start` | `{session_id}` | démarre observation Windows | +| `USER_STOP_OBSERVE` | WAITING_USER_STOP → ANALYZING | `POST /shadow/stop` + `GET /shadow//understanding` | `{session_id}` | récupère steps compris | +| `VALIDATE_STEP` | ITERATING_FEEDBACK | `POST /shadow/feedback` | `{session_id, action:"validate", step_index}` | marque validé | +| `CORRECT_STEP` | ITERATING_FEEDBACK | `POST /shadow/feedback` | `{session_id, action:"correct", step_index, new_intent}` | corrige intent | +| `UNDO_STEP` | ITERATING_FEEDBACK | `POST /shadow/feedback` | `{session_id, action:"undo", step_index}` | retire step | +| `MERGE_NEXT` | ITERATING_FEEDBACK | `POST /shadow/feedback` | `{session_id, action:"merge_next", step_index}` | fusionne | +| `SPLIT_STEP` | ITERATING_FEEDBACK | `POST /shadow/feedback` | `{session_id, action:"split", step_index, at_event_index}` | coupe | +| `NAME_COMPETENCE` | NAMING (interne) | aucun | — | prépare payload persist | +| `MARK_PARAMETER` | NAMING (interne) | aucun | — | annote step (`is_parameter: true`) | +| `PERSIST` | NAMING → PERSISTING | `POST /api/v1/lea/competences/candidate/persist` | `{session_id, name, slug, parameters[]}` | enregistre | +| `CANCEL` | n'importe → ABORTED | `POST /shadow/stop` + journaling sortie d'urgence si partial | `{session_id, reason}` | nettoyage | + +**Note** : payload final de `/persist` non figé tant que les specs parallèles ne sont pas mergées. Couche `PersistPayloadBuilder` isolée pour réagir aux évolutions. + +**Préalable shadow_build** : avant `/persist`, appeler `POST /shadow/build` pour matérialiser WorkflowIR final (incluant feedbacks rejoués). + +## 4. Formateur Option C + +**But** : transformer `understanding[]` retourné par `/shadow//understanding` en texte naturel français + libellés OCR entre « ». + +**Format attendu** : + +``` +1. Fenêtre « Patient » → ouverte +2. Bouton « Nouvelle facture » → cliqué +3. Champ « IPP » → saisi : « 25003284 » +4. Bouton « Valider » → cliqué (à confirmer) +``` + +**Règles** : +- 1 ligne par step. +- Libellés OCR entre « » (U+00AB / U+00BB). +- Verbe passé composé (`cliqué`, `saisi`, `ouverte`, `validé`). +- Si `confidence_ocr < 0.6` → suffixe `(à confirmer)`. +- Numérotation à partir de 1. + +**Implémentation** : classe `OptionCFormatter` (~80 lignes), méthode `format(understanding: list) -> str`. Table mapping `action_type → verbe`. Fallback générique « action sur "X" → effectuée » si type inconnu. + +**Inclure aussi** : question fermée Léa après la liste : « C'est bien ça ou je me suis trompée quelque part ? ». Transition vers ITERATING_FEEDBACK. + +## 5. Itération correction (phase 4) + +**Parser** doit détecter : +- Numéro de step : regex `\b(?:étape|numéro|step|ligne|le|la)?\s*([1-9]\d?)\b` + fallback contextuel. +- Action correction : type de feedback shadow (validate / correct / undo / merge_next / split). +- Contenu correction : si `correct`, extraire `new_intent` (reste de phrase après verbe correctif). Mini appel `qwen2.5:0.5b` pour nettoyer. + +**Boucle** : +1. Appel `/shadow/feedback`. +2. Récupération nouveau `understanding[]` via `/shadow//understanding`. +3. Reformulation Léa avec **recap complet** (Option C re-formatée). Pas de diff partiel. +4. Question fermée : « C'est bon ou il reste à corriger ? ». + +**Garde-fous** : +- **Limite 3 corrections sur même step** (compteur par `step_index`). Au 4ᵉ → sortie d'urgence : « Je n'arrive pas à comprendre l'étape N°X. Je préfère qu'on reprenne plus tard. Je garde tout. » → ABORTED avec `partial=true`. +- **Timeout silencieux** : 5 min sans message → Léa relance « Tu es toujours là ? Si tu veux on s'arrête, je garde tout. » +- **Détection boucle de doute** : alternance 2 fois `correct`/`undo` sur même step → Léa propose de relancer l'enregistrement de cette étape seulement. + +## 6. Phase 5 — nomination + scope + +**5.1 Nomination** : +- Léa : « Comment on appelle cette tâche ? Tu peux la nommer simplement, en français. » +- Validation : non vide, ≤ 80 chars, pas de doublon strict (lookup `GET /api/v1/lea/competences?name=…`). +- **Slugification côté serveur** : client envoie `name` brut, serveur dérive `slug`. + +**5.2 Scope paramètres** : +- Pour chaque step avec valeur saisie, Léa pose la question : « La valeur "25003284" pour le champ "IPP" — c'est l'exemple du jour ou ça doit toujours être ça ? » +- Choix : + - « toujours » / « constante » → `is_parameter=False` + - « ça change » / « exemple » → `is_parameter=True`, nom proposé (default = nom champ OCR slugifié), confirmation +- Résumé final : « Donc je retiens : tâche `` avec les paramètres `ipp`, `montant`. C'est OK ? » + +## 7. Hooks mode proactif (LoopDetector) + +**Contexte** : `agent_v0/server_v1/loop_detector.py` détecte 3 signaux : `screen_static`, `action_repeat`, `retry_threshold`. + +**Mécanisme** : +- Souscription WebSocket (à exposer côté streaming) ou polling `GET /api/v1/shadow/loop_signals?since_ts=…` toutes les 30s. +- Sur `action_repeat` → message proactif : « J'ai remarqué que tu fais souvent la même séquence. Tu veux m'apprendre à la faire pour toi ? » (bouton « Apprenez-moi » mis en évidence). +- Sur `retry_threshold` (pendant replay) → « Je n'arrive pas à reproduire l'étape N°X. Tu peux me re-montrer ? » → mini-apprentissage ciblé. + +**Cooldown / mémoire** : +- Cooldown global 5 min entre 2 messages proactifs. +- Mémoire courte des **refus** (« non merci » → blocage 24h sur signal précis, persisté `agent_chat/state/proactive_memory.json`). +- **Report unique** : « pas maintenant » → re-proposition une fois 30 min plus tard. + +**Pas d'auto-déclenchement** : un signal proactif n'appelle **jamais** `/shadow/start` sans validation utilisateur explicite. + +## 8. Persistance d'état + +**Fichier** : `agent_chat/state/.json` (un par session active). + +**Contenu** : +```json +{ + "session_id": "...", + "user_id": "...", + "trigger_source": "button" | "magic_phrase" | "proactive", + "state": "ITERATING_FEEDBACK", + "created_at": "...", + "last_transition_at": "...", + "shadow_understanding": [...], + "pending_feedbacks": [...], + "correction_counters": {"3": 2, ...}, + "competence_name": null | "...", + "parameters_marked": [...], + "abort_reason": null | "..." +} +``` + +**Écriture** : à chaque transition d'état (atomique `tmp` + `os.replace`), à chaque feedback appliqué, à chaque marquage paramètre. + +**Reprise** : au démarrage `app.py`, scanner `state/*.json`. Pour chaque non-DONE/ABORTED : +- Vérifier session shadow côté streaming. +- Si trouvée → restaurer + notifier user : « On était en train d'apprendre "". Tu veux qu'on reprenne ou qu'on jette ? » +- Si shadow plus en mémoire → `state=ABORTED, abort_reason="streaming_lost"`. + +**Nettoyage** : DONE archivées dans `state/archive/` après 7 jours ; ABORTED conservées 30 jours. + +## 9. Tests à prévoir + +**Unit (`tests/agent_chat/test_learn_action.py`)** : +- `LearnIntentParser` : 30+ cas regex + 5 cas LLM mocké. +- `OptionCFormatter` : 10 cas understanding → texte attendu, dont confidence basse. +- Gestionnaire corrections : 3 corrections → sortie d'urgence, boucle correct/undo → propose re-record. +- `StateStore` : write atomique, reprise après crash simulé. + +**Integration (`tests/agent_chat/test_learn_action_integration.py`)** : +- Flux complet mocké bout-en-bout. +- Cas redémarrage agent-chat pendant `ITERATING_FEEDBACK` → reprise propre. +- Cas streaming down sur `/shadow/stop` → retry x2 puis message d'erreur explicite. +- Cas signal proactif `action_repeat` → message + cooldown. + +**Robustesse** : +- Timeout streaming sur `/persist` → état `PERSISTING`, retry manuel possible. +- Agent Windows down pendant LISTENING → détection > 60s → « Je n'arrive pas à observer, on réessaie ? ». +- Concurrence 2 sessions parallèles → pas d'interférence. + +**Couverture cible** : ≥ 80% sur `learn_action.py`, ≥ 70% global. + +## 10. Estimation effort implémentation + +| Composant | Lignes Python | Lignes tests | Effort | +|---|---:|---:|---| +| `LearnActionOrchestrator` + machine d'état | ~180 | ~80 | moyen | +| `LearnIntentParser` (hybride) | ~120 | ~60 | moyen | +| `OptionCFormatter` | ~80 | ~40 | léger | +| `StreamingClient` (httpx wrapper) | ~70 | ~30 | léger | +| `StateStore` (persistance + reprise) | ~80 | ~30 | léger | +| `ProactiveHook` (LoopDetector) | ~70 | ~10 | moyen (dépend WS server) | +| **Total** | **~600** | **~250** | **moyen-lourd** | + +**Dépendances bloquantes** : +- Specs `/persist` parallèles à figer avant `PersistPayloadBuilder`. +- Endpoint WebSocket / polling LoopDetector côté streaming. + +**Hors scope** : +- Bouton 🎓 Windows : déjà fonctionnel, on s'aligne sur trigger existant. +- UI dashboard 5001 : aucune intervention. +- Modifs `app.py` : injection singleton + routage messages quand `state != IDLE` (~40 lignes additionnelles). + +**Risques** : +- Latence cumulée NLU + HTTP : viser ≤ 1s par tour utilisateur. +- Cohérence `correction_counters` local et historique `validator` côté shadow : source de vérité = observer (relecture via `/understanding`). +- Reprise après crash : éviter `/shadow/feedback` déjà appliqué (idempotence à valider). + +--- + +*Fin DRAFT — relecture Codex/Dom attendue avant implémentation. Aucune modification de fichier de prod.* diff --git a/docs/POC/SPECS_ENDPOINT_PERSIST_2026-06-01.md b/docs/POC/SPECS_ENDPOINT_PERSIST_2026-06-01.md new file mode 100644 index 000000000..9557de62e --- /dev/null +++ b/docs/POC/SPECS_ENDPOINT_PERSIST_2026-06-01.md @@ -0,0 +1,210 @@ +# SPECS — POST /api/v1/lea/competences/candidate/persist + +> **DRAFT specs pour implémentation — pas encore appliqué — relecture Codex/Dom attendue** +> +> Date : 2026-06-01 +> Auteur : Claude (agent Plan) +> Statut : DRAFT — aucun code écrit, aucun fichier de prod modifié +> Cible : `agent_v0/server_v1/api_stream.py` + +## Contexte + +L'audit Claude du 2026-06-01 17h00 confirme l'absence de ce endpoint. Le cycle Shadow apprentissage actuel s'arrête à `/api/v1/shadow/build` (renvoie le `workflow_ir` en mémoire, ne le persiste pas). + +Ce endpoint clôt le cycle Léa-first (6 phases) : +`start → stop → understanding → feedback (N×) → build → **persist**` + +Sources : +- `docs/coordination/inbox_codex/2026-06-01_1620_claude-to-codex_ARCHI-apprentissage-Lea-first-validee-Dom.md` +- `docs/coordination/inbox_codex/2026-06-01_1745_claude-to-codex_ADDENDUM-archi-Lea-lecture-semantique-agent-externe.md` +- `docs/coordination/inbox_codex/2026-06-01_1800_claude-to-codex_PRECISIONS-desapprentissage-regle-or-HDS.md` + +Schéma YAML de référence : `data/competences/candidate/key_win_r_wait_explorer_exe.yaml` + +## Sommaire + +1. Contrat de l'endpoint +2. Validation pré-écriture +3. Génération YAML +4. Journalisation +5. Cas particuliers +6. Sécurité +7. Tests à prévoir +8. Estimation effort implémentation + +--- + +## 1. Contrat de l'endpoint + +**Méthode / URL** : `POST /api/v1/lea/competences/candidate/persist` + +**Headers requis** : +- `Authorization: Bearer ` (cohérent avec patch P0 révocation token Codex) +- `Content-Type: application/json` + +**Payload entrée (JSON)** : +```json +{ + "name": "Saisir texte Word", + "machine_id": "DESKTOP-58D5CAC_windows", + "session_id": "sess_20260601T180000_abc123", + "workflow_ir": { "steps": [...], "preconditions": [...], "success_marker": {...} }, + "parameters": [ { "name": "texte", "type": "string", "required": true } ], + "external_decision": { + "agent_id": "t2a_v2_codeur", + "decision_contract": "..." + }, + "annotations_semantiques": { + "intent_fr": "saisir le texte transmis dans Word" + }, + "learning_metadata": { + "persist_id": "uuid-v4-client", + "partial": false, + "dropout_reason": null, + "source_phase": "shadow_build" + } +} +``` + +**Codes HTTP** : +- `201 Created` — YAML écrit, audit journalisé +- `400 Bad Request` — payload invalide (schema, slug, agent externe inconnu) +- `401 Unauthorized` — token absent / invalide / révoqué +- `403 Forbidden` — `machine_id` du payload ≠ machine du token +- `409 Conflict` — slug existe déjà dans `data/competences/candidate/` +- `429 Too Many Requests` — rate limit dépassé +- `500 Internal Server Error` — échec atomic write ou roundtrip + +**Payload sortie (201)** : +```json +{ + "competence_id": "saisir_texte_word", + "yaml_path": "data/competences/candidate/saisir_texte_word.yaml", + "learning_state": "candidate", + "persist_id": "uuid-v4-serveur", + "audit_entry_id": 4271 +} +``` + +## 2. Validation pré-écriture + +**Slugification** (`name` → `competence_id`) : +- Conversion kebab-case ASCII (translittération accents) +- Lowercase, espaces → `_`, chars non `[a-z0-9_]` retirés +- Longueur ≤ 80 caractères, ≥ 3 caractères +- Pattern regex final : `^[a-z][a-z0-9_]{2,79}$` +- Si collision avec un fichier existant en `candidate/` : **409 sans suffixage automatique** + +**Schema YAML cohérent** — champs obligatoires : `schema_version, id, name, version, learning_state, intent, parameters, preconditions, methods, success_marker, failure_message_template, promotion, generalisation, failure_log, created_at, last_updated_at, methods_execution` + +**WorkflowIR** : +- `workflow_ir.steps` non vide **sauf si** `learning_metadata.partial == true` +- Chaque step possède `kind` ∈ primitives connues (réutiliser registry primitives existante) + +**Idempotence** : +- `persist_id` (UUID v4 client) déduit la clé d'idempotence +- Si `persist_id` déjà présent dans `persist_audit.jsonl` → renvoyer `200 OK` avec le payload de l'entrée précédente (pas 409) + +## 3. Génération YAML + +**Chemin cible** : `data/competences/candidate/.yaml` + +**Atomic write** : +1. Écrire dans `data/competences/candidate/..yaml.tmp.` +2. `os.rename()` vers `.yaml` (atomique POSIX) +3. Si collision détectée entre validation et rename → 409, supprimer le `.tmp` + +**Post-write roundtrip** : +- Recharger via `core.competences.catalog.load_competence(slug)` +- Vérifier que `competence.to_dict()` est cohérent +- Si KO → 500, supprimer le fichier, log critique + +## 4. Journalisation + +**Audit principal** — append-only `data/competences/persist_audit.jsonl` : +```json +{ + "persist_id": "uuid-v4", + "timestamp": "2026-06-01T18:05:32+02:00", + "machine_id": "...", + "session_id": "...", + "competence_name": "Saisir texte Word", + "competence_id": "saisir_texte_word", + "yaml_path": "data/competences/candidate/saisir_texte_word.yaml", + "learning_state": "candidate", + "partial": false, + "dropout_reason": null, + "external_agent_id": null, + "audit_entry_id": 4271 +} +``` + +**Apprentissages incomplets** — si `partial: true`, entrée *en plus* dans `data/competences/incomplete_learnings.jsonl` avec `dropout_reason` obligatoire. + +**Append-only strict** : ouvrir en mode `a`, jamais réécrire. Verrou `fcntl.flock` par fichier. + +## 5. Cas particuliers + +| Cas | Comportement | +|---|---| +| `learning_metadata.partial == true` | accepté, `learning_state` forcé `incomplete`, entrée double dans `incomplete_learnings.jsonl`, `dropout_reason` obligatoire (sinon 400) | +| Payload demande `learning_state: stable` | **rejet silencieux** — forcer `candidate` (jamais stable par persist direct — règle d'or HDS) | +| Aucun `learning_state` fourni | défaut `candidate` | +| `external_decision.agent_id` inconnu de `ExternalDecisionClient` registry | **400** avec liste agents valides | +| `workflow_ir.steps` vide et `partial == false` | **400** | +| YAML déjà présent en `data/competences/stable/` ou `supervised/` avec même slug | **409** (collision cross-states) | +| Désapprentissage en cours sur le slug | **409** `detail: "désapprentissage actif"` | + +## 6. Sécurité + +- **Token Bearer obligatoire** — réutiliser middleware d'auth du patch P0 révocation token. Pas de fallback "agent local". +- **Couplage machine_id ↔ token** : token référence `machine_id` ; si `payload.machine_id != token.machine_id` → **403**. +- **Rate limit** : 10 persists/min/`machine_id` (in-memory token-bucket). Au-delà → **429** avec `Retry-After`. +- **Pas d'écriture hors `data/competences/candidate/`** : path traversal interdit (slug strict, jamais lu d'un champ user). +- **Logs** : ne jamais journaliser le token. `persist_id` et `machine_id` only. +- **Règle d'or HDS** : aucune donnée patient ne doit transiter — refuser le payload si `workflow_ir` ou `annotations_semantiques` contient pattern PII détecté. Détection MVP via regex paramétrable. + +## 7. Tests à prévoir + +**Unit (`tests/unit/test_competence_persist.py`)** : +- `test_slug_generation_normal` / `_with_accents` / `_too_short` / `_collision_409` +- `test_yaml_schema_required_fields_present` +- `test_atomic_write_then_rename` +- `test_post_write_roundtrip_ok` / `_corrupt_yaml_500` +- `test_idempotence_same_persist_id_returns_previous` +- `test_partial_true_forces_incomplete_state` +- `test_payload_stable_forced_to_candidate` +- `test_external_agent_unknown_400` +- `test_pii_detected_rejected` + +**Integration (`tests/integration/test_shadow_full_cycle.py`)** : +- Cycle complet : `shadow/start → POST événements → shadow/stop → shadow/build → persist` +- Vérifier YAML présent, `persist_audit.jsonl` enrichi +- Cas partial : double entrée `incomplete_learnings.jsonl` + +**Sécurité (`tests/security/test_persist_auth.py`)** : +- `test_no_token_401` +- `test_token_revoked_401` +- `test_machine_id_mismatch_403` +- `test_rate_limit_11th_call_429` +- `test_path_traversal_in_name_blocked` + +## 8. Estimation effort implémentation + +| Item | Volume | +|---|---| +| Handler FastAPI + Pydantic models | ~80-120 lignes (`api_stream.py`) | +| Helpers (slugify, atomic_write, audit_append) | ~40 lignes (nouveau module `core/competences/persist.py`) | +| Tests unit + integration + sécu | ~150 lignes | + +**Effort total** : faible-moyen — **0.5 à 1 jour**. Pas de dépendance externe nouvelle, réutilise `core.competences.catalog` et le pattern atomic-write de C-γ. + +**Points d'attention pour la review** : +- Confirmer comportement collision (409 strict vs suffixage `_v2`) +- Valider mapping `workflow_ir.steps[*].kind` → `yaml.methods[*].kind` avec registry primitives à jour +- Décider si rate limit par `machine_id` ou par token +- Statuer sur format `audit_entry_id` (auto-incrément vs hash) + +--- + +*Fin DRAFT — relecture Codex/Dom attendue avant kick-off implémentation.* diff --git a/docs/POC/SPECS_PHASE_25_SEMANTIQUE_2026-06-01.md b/docs/POC/SPECS_PHASE_25_SEMANTIQUE_2026-06-01.md new file mode 100644 index 000000000..46be78f8c --- /dev/null +++ b/docs/POC/SPECS_PHASE_25_SEMANTIQUE_2026-06-01.md @@ -0,0 +1,169 @@ +# SPECS Phase 2.5 Sémantique — POC RPA Vision v3 + +**DRAFT specs pour implémentation — pas encore appliqué — alignement avec arbitrage Plato 2026-06-01 18:18** + +Date : 2026-06-01 +Auteur : Claude (agent Plan), arbitrage Plato/Codex +Statut : DRAFT — à valider par Dom avant implémentation +Référence arbitrage : `docs/coordination/inbox_claude/2026-06-01_1818_codex-to-claude_ADDENDUM-agent-plato-archi-semantique.md` +Référence addendum initial Claude : 17:45 (OmniParser systématique dans hot path replay) — **révoqué par Plato** + +## Principes d'arbitrage retenus + +1. Garder replay/click/OCR existant comme chemin principal (hot path inchangé) +2. Phase 2.5 = post-apprentissage uniquement, jamais en hot path replay +3. Demander à l'humain seulement les ambiguïtés utiles +4. Sémantique = contexte, pas prérequis de chaque clic +5. `ExternalDecisionClient` autour de `t2a_decision`, puis adaptation AIVA +6. OCR critique renforcé par régions annotées + escalade humaine + +## Sommaire + +1. Périmètre Phase 2.5 MVP +2. Endpoint à créer +3. Identification des écrans distincts +4. Stockage des annotations sémantiques +5. Réutilisation au replay (autopilote / autonome) +6. Compatibilité compétences existantes +7. Garde-fous anti-fragilité OmniParser +8. Coûts perf attendus +9. Tests à prévoir +10. Estimation effort implémentation +11. Hors scope MVP Phase 2.5 + +--- + +## 1. Périmètre Phase 2.5 MVP + +- **Quand** : déclenchement à la fin de la phase d'observation, après `POST /api/v1/shadow/stop`, **avant** restitution Option C à l'humain. +- **Sur quoi** : screenshots clés capturés (1 par écran distinct détecté), **pas tous les frames**. +- **Avec quoi** : OmniParser réutilisé (déjà présent, adaptateur fragile mais isolé), encapsulé derrière garde-fou anti-fragilité §7. +- **Pour quoi** : produire un payload structuré `{tables[], forms[], buttons[], text_blocks[]}` par écran clé, stocké dans un YAML sémantique séparé du YAML compétence principal. +- **Non-objectif** : Phase 2.5 ne touche pas le chemin replay. Elle enrichit uniquement la compétence apprise. + +## 2. Endpoint à créer + +`POST /api/v1/lea/screen/analyze` côté streaming server (port 5005). + +- **Entrée** : `{session_id: str, screenshot_indexes: int[]}` — index des frames capturées en mode léger pendant l'observation. +- **Sortie** : `{screens: [{index, hash, structure: {tables, forms, buttons, text_blocks}}]}`. +- **Idempotence** : cache disque sous `data/cache/omniparser//.json`. +- **Timeout** : 30s par screenshot. Dépassement → fallback OCR-seul (docTR text_blocks seuls), flag `degraded: true`. +- **Erreur OmniParser** (exception, modèle KO) : fallback OCR-seul + log `logs/omniparser_errors.log`. L'endpoint ne renvoie **jamais** 500 — toujours 200 avec flag dégradé. + +## 3. Identification des écrans distincts + +Heuristique de regroupement, exécutée avant l'appel `/api/v1/lea/screen/analyze` : + +- Calcul `imagehash.phash` pour chaque frame capturée. +- Grouping par similarité : Hamming distance ≤ 8 ⇒ même écran logique. +- Sélection d'un frame représentatif par groupe (premier dans l'ordre temporel, ou celui avec le plus de détections OCR). +- **Limite POC** : maximum **10 écrans distincts par session**. Au-delà, session marquée `too_complex` et l'humain est invité à scinder la compétence en sous-compétences (message explicite dans restitution Option C). + +## 4. Stockage des annotations sémantiques + +Fichier séparé du YAML compétence principal, suffixe `.semantic.yaml`. + +Chemin : `data/competences/candidate/.semantic.yaml` + +Structure : + +```yaml +competence_id: facturation_urgence_simple +semantic_version: 1 +generated_at: 2026-06-01T18:30:00Z +omniparser_version: +degraded: false +screens: + - screen_id: screen_001 + phash: "abc123..." + representative_frame_index: 42 + annotations: + - region_id: region_3 + bbox: [120, 80, 400, 150] + semantic_label: motif_arrivee + confidence: human_verified + structure: + tables: [...] + forms: [...] + buttons: [...] + text_blocks: [...] +``` + +- Séparation stricte : YAML compétence principal reste lisible et minimal. `.semantic.yaml` est optionnel. +- YAML compétence référence le fichier sémantique par clé `semantic_ref: .semantic.yaml` si présent. + +## 5. Réutilisation au replay (autopilote / autonome) + +OmniParser au replay **uniquement** si la compétence référence un `.semantic.yaml` (présence de `semantic_ref`). + +Algorithme : + +1. Au démarrage du replay d'un écran, calcul `phash` du screen courant. +2. Si Hamming distance ≤ 8 avec un `screen_id` connu OU même nombre de tables/buttons/forms structurellement → match, application des annotations. +3. Sinon → divergence : + - **Autopilote** : escalade humaine via Option C runtime (« écran inattendu, valider ou annoter »). + - **Autonome** : log incident `logs/replay_incidents.log`, **stop** avec code `semantic_mismatch`. + +Compétences sans `semantic_ref` → on saute totalement cette étape (rétrocompat §6). + +## 6. Compatibilité compétences existantes + +- Compétences sans `.semantic.yaml` ⇒ pas de Phase 2.5 au replay, pas d'OmniParser, **chemin OCR + template + click inchangé**. +- Phase 2.5 = **opt-in par compétence** : seule une compétence apprise après l'activation de Phase 2.5 possédera le fichier sémantique. +- Aucune migration auto des compétences anciennes. Rétrocompat garantie par l'absence de `semantic_ref`. +- Pas de breaking change sur le format YAML compétence principal — seulement une clé optionnelle. + +## 7. Garde-fous anti-fragilité OmniParser + +Adaptateur OmniParser fragile (chemin absolu, dépendances lourdes). Mesures : + +- **Wrapper try/except global** autour de chaque appel OmniParser, fallback OCR-seul (docTR) systématique en cas d'exception. +- **Log dédié** : toute erreur → `logs/omniparser_errors.log` avec stack trace, session_id, frame_index. +- **Healthcheck au démarrage Phase 2.5** : appel test sur image bidon. Si échec → bascule auto en mode dégradé OCR-seul, log warning, restitution `degraded: true`. +- **Test unitaire obligatoire** : mock OmniParser qui lève exception, vérifier que la chaîne aboutit à un `.semantic.yaml` dégradé valide. +- **Isolation chemin absolu** : config OmniParser (chemin modèle, version) centralisée dans `config/omniparser.yaml`, jamais en dur. + +## 8. Coûts perf attendus + +- **Dev (RTX 5070, OmniParser CPU)** : 2 à 5s par screen. Acceptable (principe 5 Dom, hors hot path). +- **DGX Spark (cible prod)** : < 500ms par screen. +- **Coût session typique** : 10 screens × 2-5s = 20-50s ajoutés à la fin d'une session d'apprentissage sur dev. Tolérable, asynchrone vis-à-vis humain (restitution Option C peut afficher spinner). +- Pas d'impact sur replay (hot path) tant que pas de `semantic_ref`. Avec `semantic_ref`, surcoût = `phash` (négligeable) + OmniParser uniquement en cas de mismatch structure. + +## 9. Tests à prévoir + +**Unitaires** : +- Hash perceptuel + grouping : frames similaires/différentes, vérif Hamming threshold. +- Fallback OCR-seul si OmniParser KO (mock exception, timeout, healthcheck KO). +- Génération `.semantic.yaml` : structure valide, `degraded` correctement positionné. +- Cap 10 écrans : session avec 15 → marquage `too_complex` propre. + +**Intégration** : +- Flux complet : session shadow → stop → identification écrans → analyse → `.semantic.yaml` écrit → Option C. +- Replay compétence avec `.semantic.yaml` : match nominal, mismatch déclenche escalade autopilote / stop autonome. +- Replay compétence sans `.semantic.yaml` : chemin legacy intact, aucun appel OmniParser. + +**Sécurité** : +- Injection chemins relatifs/absolus dans `session_id` ou `slug` refusée (regex strict `^[a-z0-9_]+$`). +- Pas d'écriture hors `data/competences/candidate/` et `data/cache/omniparser/`. + +## 10. Estimation effort implémentation + +- ~200-300 lignes Python (endpoint, grouping phash, wrapper OmniParser, écriture YAML, lecture replay). +- ~150 lignes tests. +- Effort **moyen**, étalable sur 1-2 jours. +- **Risque régression faible** : ne touche pas hot path replay, opt-in par compétence, rétrocompat totale. + +## 11. Hors scope MVP Phase 2.5 + +- Détection visuelle de changements d'écran en temps réel pendant le replay (= hot path, révoqué par Plato). +- Annotations sémantiques automatiques par VLM (prérequis trop ambitieux POC). +- Versioning sémantique multi-versions Easily (post-POC). +- Recalcul auto annotations si interface change (post-POC, lié mode rééducation). +- Annotations multi-langues (POC français uniquement). +- Synchronisation `.semantic.yaml` entre agents distribués (post-POC). + +--- + +**Fin DRAFT — à valider par Dom avant implémentation. Aucune modification de fichier de prod. Aucun commit.** diff --git a/docs/SMOKE_TEST_FINALIZE_REPLAY_2026-05-20.md b/docs/SMOKE_TEST_FINALIZE_REPLAY_2026-05-20.md new file mode 100644 index 000000000..f942fdc38 --- /dev/null +++ b/docs/SMOKE_TEST_FINALIZE_REPLAY_2026-05-20.md @@ -0,0 +1,158 @@ +# Préparation validation réelle — flux `finalize → proposition utilisateur → replay-session` + +**Date** : 2026-05-20 +**Mission** : Claude 5 (lecture seule) +**Périmètre** : `agent_v0/deploy/windows_client/**`, `agent_v0/agent_v1/**`, scripts SCP, docs déploiement +**Branche** : `backup/post-demo-2026-05-19` (HEAD `5ea4960e6`, 5 fichiers uncommitted + 1 untracked) + +## Constat + +Le câblage agent-side du contrat enrichi `/finalize` est **déjà fait et présent dans la branche locale** : + +- `streamer.py:96-103, 627-635` — attribut `_on_finalize_result`, setter `set_on_finalize_result`, invocation après `resp.ok` (catch d'erreur du callback en log warning). +- `main.py:31` — import `dispatch_finalize_result`. +- `main.py:222` — wire `self.streamer.set_on_finalize_result(self._on_finalize_result)` dans `start_session`. +- `main.py:445-448` — `_on_finalize_result(payload)` dispatche via `dispatch_finalize_result(self.ui, payload, replay_name)` avec `replay_name = self._last_recording_name`. +- `finalize_contract.py` (untracked) — module pur, `dispatch_finalize_result(ui, payload, replay_name)` route selon `replay_launch.status` (`started`/`failed`) et `replay_ready + replay_request`. +- `smart_tray.py:577-599` — `offer_finalize_replay(replay_request, replay_name)` : notification toast + dialog `_ask_consent` Article 14, puis `_launch_replay_request(replay_request, replay_name)` si accepté. + +Le contrat attendu côté serveur s'appelle **`replay_launch`** (status `started`/`failed`), pas `launch_replay` comme supposé en mission 4. Les 3 clés consommées : `replay_ready: bool`, `replay_request: dict` (doit contenir `session_id`), `replay_launch: dict {status}`. + +**Synchro vers Léa Windows** : aucun script SCP automatique présent dans le repo. Le canal réel est un `sshpass scp` manuel fichier par fichier vers `dom@192.168.1.11:C:/rpa_vision/agent_v1/` (cf. `memory/feedback_scp_auto_modif_client_windows.md` + handoff 2026-05-16). Le dossier `agent_v0/deploy/windows_client/` existe mais sert au **setup initial** (setup.bat), pas au déploiement incrémental — ses fichiers miroirs sont **obsolètes de 2 à 7 semaines** par rapport à la source. + +Le `BUILD_DEPLOY_GUIDE.md` (24 novembre 2025) parle de l'ancien `agent_v0/` mode upload chiffré — **obsolète** pour le flux agent_v1 streaming actuel. + +## Fichiers à déployer + +### Tableau fichier / déploiement / motif + +| Fichier source | Doit être déployé ? | Pourquoi | +|---|---|---| +| `agent_v0/agent_v1/main.py` | ✅ **OUI** | Lignes 31 (import), 222 (wire), 445-448 (`_on_finalize_result`) ajoutées — sans ce fichier, le callback n'est pas wired | +| `agent_v0/agent_v1/network/streamer.py` | ✅ **OUI** | Lignes 96-103 + 627-635 ajoutées — sans ce fichier, le payload `/finalize` est jeté dans un log comme avant | +| `agent_v0/agent_v1/ui/smart_tray.py` | ✅ **OUI** | Méthode `offer_finalize_replay` (577-599) ajoutée — sans elle, `finalize_contract.py` log "UI indisponible" et abandonne | +| `agent_v0/agent_v1/finalize_contract.py` | ✅ **OUI (NOUVEAU FICHIER)** | Untracked en git, n'existe nulle part sur Léa Windows → **`ImportError` au démarrage de l'agent** si oublié (import ligne `main.py:31`) | +| `agent_v0/agent_v1/vision/capturer.py` | ✅ **OUI** | Fix monitor missions 2/2b (garde dims aberrantes + fallback) — pertinent pour la fiabilité de la session enregistrée qui alimente le `replay_request` | +| `agent_v0/agent_v1/core/executor.py` | 🟡 Si modifié séparément | Apparaît dans `git status` (modifié dans session antérieure), à vérifier avec `git diff` avant SCP pour ne pas pousser un changement non validé | +| `agent_v0/agent_v1/ui/smart_tray.py` (méthode `_launch_replay_request`) | ✅ Inclus dans smart_tray.py | Référencée par `offer_finalize_replay`, ligne 507 | +| `agent_v0/agent_v1/config.py` | ⚫ Non | Pas modifié par les missions récentes | +| `agent_v0/agent_v1/ui/chat_window.py` | ⚫ Non | Pas modifié par mission 4 | +| `agent_v0/agent_v1/ui/shared_state.py` | ⚫ Non | Pas modifié | +| `agent_v0/agent_v1/network/persistent_buffer.py` | ⚫ Non | Pas modifié | +| `agent_v0/agent_v1/network/feedback_bus.py` | ⚫ Non | Pas modifié | +| `agent_v0/lea_ui/server_client.py` | ⚫ Non | Pas modifié, et probablement non utilisé au runtime actuel (main a son propre poll loop) | +| `agent_v0/deploy/windows_client/**` | ⚫ Non | Miroir obsolète, sert au setup initial, ne fait pas partie du canal de déploiement incrémental | + +### La synchro actuelle couvre-t-elle ? + +| Fichier | Couvert par synchro auto ? | Statut | +|---|---|---| +| `agent_v0/agent_v1/network/streamer.py` | ❌ NON | Aucun script auto — SCP manuel obligatoire | +| `agent_v0/agent_v1/ui/smart_tray.py` | ❌ NON | Aucun script auto — SCP manuel obligatoire | +| `agent_v0/agent_v1/main.py` | ❌ NON | Aucun script auto — SCP manuel obligatoire | +| `agent_v0/agent_v1/finalize_contract.py` | ❌ NON (cas critique) | Fichier NEUF, untracked git, **absent partout sur Léa** → ImportError garanti si oublié | + +**Conclusion synchro** : il faut SCP les 4 fichiers manuellement. Le risque principal est d'oublier `finalize_contract.py` (nouveau) car il n'apparaîtra pas dans un diff incrémental classique — le réflexe "je SCP les fichiers modifiés" ne le couvre pas. **Doit être SCP en premier.** + +### Commandes SCP de référence + +```bash +SSHPASS='loli' sshpass -e scp -o StrictHostKeyChecking=no \ + /home/dom/ai/rpa_vision_v3/agent_v0/agent_v1/finalize_contract.py \ + dom@192.168.1.11:C:/rpa_vision/agent_v1/finalize_contract.py + +SSHPASS='loli' sshpass -e scp -o StrictHostKeyChecking=no \ + /home/dom/ai/rpa_vision_v3/agent_v0/agent_v1/network/streamer.py \ + dom@192.168.1.11:C:/rpa_vision/agent_v1/network/streamer.py + +SSHPASS='loli' sshpass -e scp -o StrictHostKeyChecking=no \ + /home/dom/ai/rpa_vision_v3/agent_v0/agent_v1/ui/smart_tray.py \ + dom@192.168.1.11:C:/rpa_vision/agent_v1/ui/smart_tray.py + +SSHPASS='loli' sshpass -e scp -o StrictHostKeyChecking=no \ + /home/dom/ai/rpa_vision_v3/agent_v0/agent_v1/main.py \ + dom@192.168.1.11:C:/rpa_vision/agent_v1/main.py + +SSHPASS='loli' sshpass -e scp -o StrictHostKeyChecking=no \ + /home/dom/ai/rpa_vision_v3/agent_v0/agent_v1/vision/capturer.py \ + dom@192.168.1.11:C:/rpa_vision/agent_v1/vision/capturer.py +``` + +## Checklist smoke + +### Pré-requis machine (à valider une fois) + +| Item | Vérification | +|---|---| +| Léa Windows installée à `C:\rpa_vision\` | `dir C:\rpa_vision\agent_v1\main.py` doit répondre | +| Venv `.venv` ou `.venv_v1_win` présent avec dépendances | `C:\rpa_vision\.venv\Scripts\python.exe -c "import pystray, pynput, mss, plyer; print('ok')"` | +| Token API exporté côté Windows | `echo %RPA_API_TOKEN%` (ou variable session) — sinon `set RPA_API_TOKEN=` | +| Serveur Linux accessible | depuis Windows : `curl -H "Authorization: Bearer %RPA_API_TOKEN%" http://192.168.1.40:5005/health` | +| `rpa-streaming` actif côté Linux | `systemctl --user status rpa-streaming` | +| VWB backend actif (port 5002) | optionnel — la mission 5 ne le requiert pas | +| Léa Windows actuellement quittée | tray icône absente | + +### Déploiement (≈ 1 min) + +1. **SCP `finalize_contract.py` EN PREMIER** (fichier neuf, sinon ImportError au prochain `python run_agent_v1.py`). +2. SCP `streamer.py`, `smart_tray.py`, `main.py`, `capturer.py` (ordre indifférent). +3. Côté Léa Windows : ouvrir le dossier `C:\rpa_vision\agent_v1\` et confirmer les 5 fichiers ont la date du jour. +4. Lancer Léa : double-clic raccourci ou `C:\rpa_vision\.venv\Scripts\pythonw.exe run_agent_v1.py`. +5. Vérifier qu'aucune fenêtre console n'apparaît avec une stack trace `ImportError: finalize_contract`. Si oui → relancer avec `python.exe` (pas `pythonw.exe`) pour voir l'erreur, ou re-SCP `finalize_contract.py`. +6. Vérifier l'icône systray apparaît (cercle gris/orange selon connexion serveur). + +### Test nominal (≈ 2 min) + +| # | Action utilisateur | Logs Léa Windows attendus | Logs serveur Linux attendus | Comportement attendu | +|---|---|---|---|---| +| 1 | Clic droit tray → "Apprenez-moi une tâche" | `agent_v0.agent_v1.ui.shared_state Enregistrement demarre : ...` + `agent_v1.network.streamer Streamer pour sess_... démarré` | `[REGISTER] session sess_... machine=desktop-58d5cac` | Notif "C'est parti !" | +| 2 | Saisir nom "test_finalize_replay", confirmer | idem | idem | Tray icône passe rouge | +| 3 | Faire 3-5 clics dans une app simple (calculatrice, notepad) | `Action capturée : mouse_click` × N | événements + screenshots reçus dans `/event` `/image` | Compteur "X étapes mémorisées" augmente dans le menu tray | +| 4 | Clic droit tray → "C'est terminé" | `Enregistrement arrete : test_finalize_replay (N actions)` puis `Streamer ... arrêté` puis **`Session finalisée: {...replay_ready: true, replay_request: {session_id: sess_..., ...}, replay_launch: {status: ...}}`** | `[FINALIZE] sess_... → workflow construit, replay_request retourné` | Notif "Merci ! J'ai bien mémorisé vos N actions." | +| 5 | (auto) | `agent_v1.ui.smart_tray.offer_finalize_replay : proposition test 'test_finalize_replay'` | rien | Notif toast "J'ai compris la tâche 'test_finalize_replay'. Voulez-vous la tester ?" | +| 6 | Dialog tkinter Yes/No s'ouvre, cliquer **Oui** | `agent_v1.ui.smart_tray Replay demarre pour workflow ...` | `[REPLAY_START] replay_id=replay_..., workflow=...` puis actions enqueues | Tray icône passe bleu (replay actif), notification Article 50 "Le système d'intelligence artificielle exécute la tâche..." | +| 7 | Observer Léa rejouer les clics | `Replay action received : click ...` × N puis `Replay terminé — retour en mode capture` | actions dispatchées via `/replay/next`, résultats via `/replay/result` | Les clics sont rejoués à l'écran. Tray icône revient vert. Notif "C'est fait !" | + +**Critère succès global** : 7 étapes en ≤ 5 min, sans intervention manuelle entre 4 et 7 autre que le dialog `_ask_consent`. + +### Test d'échec (variantes à essayer après le nominal) + +| Variante | Action | Comportement attendu | +|---|---|---| +| Refus utilisateur | Étape 6 : cliquer **Non** | `_launch_replay_request` non appelé, aucun POST `/replay/start`, log Léa silencieux après `offer_finalize_replay`, retour mode capture sans replay | +| Serveur ne renvoie pas `replay_ready` | Couper le câblage côté serveur pour qu'il renvoie juste `{}` | Léa log `Session finalisée: {}`, **aucune notif**, pas de dialog. Comportement = backward-compat parfaite | +| Serveur renvoie `replay_launch.status=started` | Test serveur déjà lance le replay | Log Léa `Replay direct déjà lancé par le serveur après finalize`, dialog **NON affiché** (logique `dispatch_finalize_result:20-22`) | +| Serveur renvoie `replay_launch.status=failed` | Idem | Log warning `Auto-replay serveur échoué après finalize, proposition manuelle`, dialog **affiché** (fallback) | +| `finalize_contract.py` absent | Ne pas SCP ce fichier, relancer Léa | `ImportError: cannot import name 'dispatch_finalize_result'` au démarrage, agent ne démarre pas | +| `offer_finalize_replay` absent (smart_tray ancien) | Ne pas SCP smart_tray.py, relancer | Log `UI indisponible pour proposer un test immédiat`, pas de dialog, replay non lancé | +| Network coupé pendant finalize | Débrancher Wi-Fi pile au "C'est terminé" | Log Léa `Finalisation échouée: `, callback non invoqué, aucune notif. Session reste localement, retry implicite au prochain run (buffer SQLite) | +| Replay impossible (workflow vide ou bug) | Faire 0 clic pendant l'enregistrement avant Stop | Selon serveur : si `replay_ready=false`, dispatch silencieux ligne 24. Pas de dialog, comportement attendu = ne pas proposer un test inutile | + +### Signes d'échec à surveiller + +- **Stack trace au démarrage Léa** → `finalize_contract.py` manquant ou syntax error +- **Aucun log `Session finalisée:` après "C'est terminé"** → streamer n'arrive pas à appeler `/finalize` (réseau ? auth ? port ?) +- **Log `Session finalisée: {...}` MAIS pas de notif Léa** → soit le payload ne contient pas `replay_ready/replay_request`, soit `offer_finalize_replay` n'est pas sur smart_tray (ancien fichier) +- **Notif affichée mais pas de dialog Yes/No** → bug tkinter (rare) ou thread bloqué (vérifier `_ask_consent` qui crée son propre Tk()) +- **Dialog accepté mais aucun POST `/replay/start` côté serveur** → `_launch_replay_request` mal câblé ou `replay_request` invalide (session_id absent) + +## Risques / points ouverts + +1. **`finalize_contract.py` jamais committé** — `git status` le marque untracked. Risque : disparition silencieuse au prochain `git stash` ou `git checkout`. À committer avant la session de test. +2. **Le SCP des 5 fichiers n'est pas atomique** — si Léa est relancée entre le SCP de 2 fichiers, état incohérent possible. Mitigation : SCP les 5 fichiers d'abord, **puis** relancer Léa, jamais l'inverse. +3. **Le miroir `agent_v0/deploy/windows_client/agent_v1/` est obsolète de 7 semaines** — risque de confusion si quelqu'un essaie de redéployer un poste neuf via `setup.bat`. Hors scope mission 5 mais à signaler pour le prochain client. +4. **`_last_recording_name` peut être vide si `start_session` est appelé avec une chaîne vide** — `_on_finalize_result` fallback sur "la tâche que vous venez d'enregistrer", testé dans `finalize_contract.py:38`. OK mais l'UX dégrade. +5. **Le contrat serveur n'est pas encore validé** — la mission concerne le côté agent. Vérifier côté serveur que `/finalize` retourne bien `{replay_ready, replay_request, replay_launch}` AVANT de smoke-tester. Si le serveur retourne encore l'ancien payload, le test nominal s'arrêtera après étape 4 sans crash mais sans proposition (variante 2 du test d'échec). +6. **Token API `RPA_API_TOKEN` côté Windows** — si la variable n'est pas définie dans la session Windows actuelle, le `register` et le `finalize` retourneront 401 silencieusement (cf. `streamer.py:573-600`). Sympôme : log Léa `Enregistrement session échoué: 401`. +7. **Pas de test d'idempotence** — si l'utilisateur enchaîne 2 enregistrements rapidement et accepte le test du premier, le `_replay_active` pourrait masquer un second `offer_finalize_replay`. À surveiller mais hors scope ici. +8. **Article 14 / Article 50** — le dialog `_ask_consent` couvre l'Article 14 (contrôle humain avant lancement auto). La notification Article 50 (transparence "le système IA exécute...") est déjà câblée dans `_launch_replay_request` réutilisé. Conforme. + +## Méthode d'audit + +- Lecture intégrale : `finalize_contract.py` (40 lignes), `LISEZMOI.txt` deploy, `setup.bat`. +- Lecture déjà réalisée (audits précédents 2026-05-19/20) : `streamer.py`, `main.py`, `smart_tray.py` complets, `vision/capturer.py`. +- Lecture ciblée : `BUILD_DEPLOY_GUIDE.md` (constate obsolescence), `feedback_scp_auto_modif_client_windows.md`, handoff 2026-05-16 (commandes SCP de référence). +- Grep : consumers `dispatch_finalize_result`, `offer_finalize_replay`, `_launch_replay_request`, `_last_recording_name`. +- Diff dates source vs miroir deploy/windows_client pour valider obsolescence. +- `git diff agent_v0/agent_v1/network/streamer.py` pour confirmer la nature exacte des modifs. +- **Aucune modification de code, aucune action VWB, aucune modif de script** — conformément aux interdits. diff --git a/docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md b/docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md new file mode 100644 index 000000000..da3947227 --- /dev/null +++ b/docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md @@ -0,0 +1,393 @@ +# Synthèse croisée — Études techno RPA vision & méthodes de replay + +**Date :** 2026-05-23 +**Auteur :** Claude (session principale) à partir des `docs/` du projet +**Périmètre :** consolider, en un seul document, tout ce que `rpa_vision_v3/docs/` contient sur (a) les techno externes étudiées pour faire avancer le RPA visuel et (b) les méthodes de replay testées/diagnostiquées. Destiné à être lu par un agent comme brief de mise à niveau. + +**Statut :** lecture seule, pas d'action. Pas de proposition d'implémentation : on consolide ce qui est déjà documenté et on signale les liens. + +--- + +## 0. Comment lire ce document + +Le projet a accumulé sur mars–mai 2026 : +- Des **explorations externes** (5+4 frameworks computer-use / RPA visuel). +- Des **benchmarks internes** (VLM grounding, LLM décision T2A, LLM safety checks). +- Des **diagnostics replay post-démo GHT** très précis, qui révèlent que la cause des échecs récents n'est PAS la cascade de résolution UI, mais la couche transport et un bug OCR latent. + +Les sections 1 à 4 sont organisées par thème (frameworks, VLM/grounding, LLM décisionnels, replay). La section 5 croise tout et signale les liens forts. La section 6 est la carte des documents de référence. + +--- + +## 1. Frameworks externes étudiés (RPA visuel, GUI agents, computer-use) + +Deux vagues d'exploration distinctes, à 5 jours d'écart, par 2 canaux différents — leur convergence est elle-même un signal. + +### 1.1. Vague QW Suite Mai — 5 frameworks computer-use (2026-05-05) + +Source : `docs/superpowers/specs/2026-05-05-qw-suite-mai-design.md` §1 + `docs/QW_SUITE_MAI.md` en-tête. + +| Framework | Apport repéré | Quick win extrait | +|---|---|---| +| **Simular Agent-S** | architecture computer-use mature | (pas de QW direct) | +| **browser-use** | détection de boucle de stagnation | **QW2 LoopDetector composite** (CLIP screen_static + action_repeat + retry_threshold) | +| **OpenAI CUA (sample)** | pattern de validations humaines avant action | **QW4 Safety checks hybrides** (declaratif + LLM contextuel local) | +| **Coasty (open-cu)** | computer-use générique | (pas de QW direct) | +| **Showlab OOTB** | capture/grounding propre par moniteur | **QW1 Multi-écrans** (`monitor_index` + fallback focus actif puis composite) | + +**Livrables réels (commit en branche `feature/qw-suite-mai`)** : +- QW1 multi-écrans + `MonitorRouter` (`screeninfo>=0.8` ajouté) — `loop_detector.py`, commits `6582a69d3`, `b1a3aa16f`, `2d71e2a24`, fix `fc01afa59`. +- QW2 LoopDetector — commit `2a51a844b`. +- QW4 SafetyChecksProvider + endpoint `/api/v3/replay/resume` + `` VWB — commit `7c6945171`, `0a02a6ec9` (sélection du LLM). +- 24 tests QW + 89 baseline = 113 verts. +- Kill-switches env vars : `RPA_LOOP_DETECTOR_ENABLED`, `RPA_SAFETY_CHECKS_LLM_ENABLED`, `RPA_LOOP_SCREEN_STATIC_THRESHOLD`, etc. + +### 1.2. Vague Inspiration Frameworks — 4 projets RPA visuel (2026-05-10) + +Source : `docs/INSPIRATION_FRAMEWORKS_2026-05-10.md`. + +| Framework | Stars (mai 2026) | Apport conceptuel | +|---|---|---| +| **OpenAdapt** (OpenAdaptAI) | ~7k | RPA générative VLM, "success traces become new training data" (Evaluation-Driven Feedback) | +| **Skyvern** (Skyvern-AI) | ~12k | browser RPA vision, **Planner-Actor-Validator loop**, **Visual Workflow Builder**, prompt caching, WebVoyager 85.85% | +| **OmniParser** (Microsoft) | ~22k | tokenisation d'écran (interactable elements + captions sémantiques) AVANT le VLM principal, OmniTool VM Windows | +| **TagUI** (AI Singapore) | — | RPA cross-OS multilangue, moins LLM-first | + +**Convergences fortes mises en évidence dans le doc** : + +1. **Policy / Grounding Separation** — chez nous VWB = Policy, cascade `_resolve_target` (OCR → template → VLM) = Grounding. Existe en pratique, à formaliser dans la doc. +2. **Safety Gate** — Léa vérifie l'ancre visuelle avant de cliquer. Vocabulaire reconnu → atout pitch healthtech. +3. **Abstraction Ladder** — replay déclaratif VWB (low) ↔ ORA `observe_reason_act` (high). À ne PAS opposer. +4. **Planner-Actor-Validator** (Skyvern) — VWB ≈ Planner statique, Léa ≈ Actor + Validator partiel. Le bug du step 10 démontre que **notre Validator est laxiste** (pHash global au lieu de vérification sémantique). +5. **Tokenizing UI screenshots** (OmniParser) — pas en place chez nous. Suggestion modeste : logger la liste des candidats à chaque appel `_resolve_target` (vue parsée implicite). +6. **OmniTool VM Windows + agent côté serveur** — convergence indépendante avec notre archi Léa Windows + agent Linux. + +**Repères benchmarks externes mentionnés** : WindowsAgentArena, ScreenSpot, ScreenSpot-Pro, WebVoyager. + +### 1.3. Convergences et différentiels nets + +| Ce qu'ils ont en plus | Ce qu'on a en plus | +|---|---| +| Formalisation explicite des composants | Spécialisation healthtech (moat) | +| Communication mainstream | Validation visuelle (Safety Gate) côté agent Windows | +| Pre-built templates / use cases | VWB visuel = différenciateur UX | +| | Stack on-premise complète (RGPD / souveraineté) | + +--- + +## 2. VLM et grounding — historique implémentations + benchmarks + +Source maître : `docs/HISTORIQUE_VLM_IMPLEMENTATIONS_2026-05-08.md` (audit branche `feature/qw-suite-mai` HEAD `731b5bcae`). + +### 2.1. Modèles VLM passés ou en place + +| Modèle | Période | Backend(s) | Statut | +|---|---|---|---| +| `qwen2.5-vl:7b` | 2026-03 → encore présent | Ollama + vLLM | actif (mais déborde VRAM, voir 2.3) | +| `qwen3-vl:8b` | 2026-04 → | Ollama | fallback dans `vlm_config.FALLBACK_VLM_MODELS` | +| `UI-TARS-1.5-7B` | 2026-04-25 (commit `9da589c8c`) | Transformers Flask `server.py` 4-bit NF4 | **remplacé** par InfiGUI (commit `77faa03ec`) | +| **`InfiGUI-G1-3B`** | 2026-04-26 → | Transformers (Flask `server.py` + worker subprocess + daemon Unix socket) | **principal grounding aujourd'hui** (3.9 GB VRAM, `_smart_resize` complet) | +| `SeeClick` (Qwen-VL) | 2026-01 → ? | HuggingFace direct | exporté mais "cassé" — commit `d1b556b6c` retire de l'executor | +| `OWL-v2` (Google) | — | Transformers direct (`OwlDetector`) | détecteur open-vocabulary, câblé dans `ui_detector.py` mais aucun bench récent | +| `medgemma:4b` | — | Ollama | retenu QW4 par défaut puis **évincé** au bench (cf. 2.4) | +| `gemma4:e4b` / `gemma4:latest` | — | Ollama | usage VLM et safety_checks | +| Cloud opt-in OpenAI/Gemini/Anthropic | — | HTTP | `VLM_ALLOW_CLOUD=true` dans `visual_workflow_builder/backend/vlm_provider.py` | + +3 entry-points distincts coexistent pour la même logique Transformers : `core/grounding/server.py` (Flask :8200), `core/grounding/infigui_server.py` (Unix socket, service systemd `rpa-grounding.service`), `core/grounding/infigui_worker.py` (subprocess one-shot). Le pipeline FAST→SMART→THINK + ThinkArbiter + ShadowLearningHook a été câblé en avril (commits Phase 1→6 entre `ea36bba5c` et `73cea2385`). + +### 2.2. Backends testés + +`Ollama HTTP`, `vLLM HTTP OpenAI-compat`, `Transformers in-process` (3 entry-points), `HuggingFace direct` (SeeClick, OWL-v2), `Cloud opt-in`. + +vLLM a été ajouté en mars (commit `394342be7` du 2026-03-31), positionné comme essai principal AVANT fallback Ollama dans `resolve_engine.py:785-816`. Modèle hardcodé par défaut `Qwen/Qwen2.5-VL-7B-Instruct-AWQ`, switchable via env `VLLM_MODEL`. + +### 2.3. Bench grounding bbox_2d (2026-05-08) + +Source : `docs/MIGRATION_VLM_PLAN_2026-05-09.md` §2. + +Screenshot fixture : `data/training/live_sessions/bg_DESKTOP-58D5CAC_windows/shots/heartbeat_1773792436.png` (2560×1600, dialog OK/Cancel). + +| Modèle / config | Latence | bbox_2d | Parse regex prod | Coords cohérentes | +|---|---|---|---|---| +| `qwen2.5vl:7b` Ollama (prod) | 11.0 s | `{"bbox_2d":[422,604,462,624]}` | oui | **non** (cx ≈ 0.17, top-left) | +| `qwen3-vl:8b` Ollama (prod strict) | 8.0 s | vide (thinking absorbe les 50 tokens) | non | n/a | +| `qwen3-vl:8b` Ollama (`think:false`, num_predict=256) | 1.7 s | liste nue `[332,487,362,507]` | non (regex attend objet) | n/a | +| `qwen3-vl:8b` Ollama (prompt JSON explicite) | 1.8 s | `{"bbox_2d":[...]}` | oui | **non** (même bug d'échelle) | + +**Deux problèmes structurels relevés** : + +1. **VRAM saturée** : qwen2.5vl:7b = 14 GB en mémoire totale, RTX 5070 = 12 GB. Ollama bascule split CPU/GPU 42/58 → 11s par appel. qwen3-vl:8b (6 GB) tient full GPU à 1.7 s. +2. **Bug d'échelle bbox_2d** : Qwen2.5-VL retourne les coords dans la résolution **post-smart_resize**, pas l'orig. Ollama applique son propre smart_resize sans exposer la taille → prod divise par `orig_w` au lieu de `resized_w` → coords toutes shiftées top-left. Présent dans 4 occurrences de `resolve_engine.py` + `_locate_popup_button` (L:2576). Source : HF discussion #13 Qwen2.5-VL-7B-Instruct. Cité mainteneur : « *bbox_2d coordinates will be relative to your resized image size* » + « *resized dimensions parameter is not supported in OLLAMA* ». + +**Conclusion plan migration** : tant qu'on passe par Ollama le fix n'est qu'une rustine. Cible = vLLM ou Transformers direct avec passage explicite de `resized_width`/`resized_height`. Modèle pressenti `qwen3-vl:8b`. Module `smart_resize` officiel commité (`0d7bcd18a`) mais **DETTE-014** : calé sur mauvaise référence (`patch_size=16` Qwen3-VL → factor 32, pas 28). + +### 2.4. Bench safety_checks (2026-05-06) + +Source : `docs/BENCH_SAFETY_CHECKS_2026-05-06.md`. Méthodo : 5 scénarios anomalies × 5 modèles, cold + 3 runs warm, métriques (JSON valide, détection, latence). + +| Modèle | Cold (s) | Warm avg (s) | JSON | Détection | +|---|---:|---:|---:|---:| +| **`gemma4:latest`** | 10.6 | **2.9** | 92% | **46%** ✅ retenu | +| `qwen3-vl:8b` | 5.6 | — | **0%** | 0% (ignore `format=json` Ollama) | +| `qwen2.5vl:7b` | 9.4 | 6.6 | 100% | 23% | +| `qwen2.5vl:3b` | 6.0 | 2.0 | 100% | 8% (vérifie pour vérifier) | +| `medgemma:4b` | 2.0 | 0.5 | 100% | **0%** (renvoie systématiquement `[]`) | + +Enseignement : `medgemma:4b` malgré son nom est **mauvais choix par défaut** (trop obéissant au "rien à signaler"). `gemma4:latest` gagne sur cohérence motif/diagnostic. `qwen3-vl:8b` à écarter tant qu'il ignore `format=json` Ollama. + +Aucun modèle ne détecte les 5 scénarios — IPP corrompu et forfait incohérent ratés par tous. + +### 2.5. Code potentiellement utilisable pour la migration + +Repris de `HISTORIQUE_VLM_IMPLEMENTATIONS_2026-05-08.md` §6 : + +- **Transformers clé en main** : `core/grounding/server.py` (loader + `_smart_resize` complet MIN/MAX_PIXELS) et `core/grounding/infigui_worker.py` (`load_model`, `infer`). Il suffit de changer `MODEL_ID` (env `GROUNDING_MODEL` déjà supporté). +- **vLLM clé en main** : `resolve_engine.py:785-816` contient déjà l'appel HTTP OpenAI-compat avec `image_url: data:image/jpeg;base64`. Il manque le passage de `resized_width/resized_height` (extension OpenAI vLLM). +- **Socket persistant + fallback subprocess** : `infigui_server.py` + `ui_tars_grounder.py` réutilisables tels quels. +- **Prompt UI-TARS officiel** récupérable via `git show 9da589c8c:core/grounding/server.py`. + +--- + +## 3. LLM décisionnels (T2A) — bench 18 modèles + +Source : `docs/BENCH_T2A_DECISION_11DOSSIERS.md` (2026-05-05). 18 modèles × 11 DPI GHT Sud 95 (5 UHCD / 6 Forfait, vérité-terrain corrigée le 2026-05-05). + +Top 5 : + +| # | Modèle | Acc | p50 | Verdict | +|---|---|---:|---:|---| +| 1 | `gemma3:27b-cloud` | 8/11 (73%) | 10.6s | 🟢 retenu démo | +| 2 | `qwen3:8b` | 7/11 (64%) | 7.6s | 🟡 backup local (5 GB) | +| 3 | `qwen2.5:7b` | 7/11 (64%) | 10.0s | 🟡 | +| 4 | `qwen3-vl:235b-instruct-cloud` | 7/11 (64%) | 20.3s | 🟡 | +| 5 | `qwen3.5:9b` | 7/11 (64%) | 25.8s | 🟡 | + +Échecs notables : +- `gemma4:latest` 6/11 (55%) — **insuffisant** sur décision T2A (mais OK sur safety_checks, cf. §2.4). +- `medgemma:4b` 4/11 (36%) — JSON cassé via Ollama, **PAS le LLM médical magique** que son nom suggère. +- `gpt-oss:20b-cloud` 0% — format JSON cassé. +- 3 cas universellement ratés (Pneumo VRS, Aura migr., Salpingite) → soit DPI à enrichir, soit vérité-terrain à rediscuter avec Pauline. + +**Pendant la démo réelle** (cf. `LESSONS_LEARNED_GHT_2026-05.md`) : `gemma4:31b-cloud` retenu pour T2A MOREL (qualité clinique propre, run 8 du 12 mai). Test ponctuel de `qwen3-next:80b-cloud` OK aussi mais pas durable. Ollama Cloud 503 vécue le 12 mai → robustesse non couverte. **`gemma3:27b` confabule sur PMSI français** (invente GEMSA), à NE PAS utiliser en fallback. + +--- + +## 4. Replay — méthodes, blocages observés, options + +### 4.1. Architecture replay actuelle (rappel) + +VWB Flask + SQLite édite un workflow déclaratif. Le serveur (`api_stream.py`) dispatche step par step à un client Léa V1 sur Windows distant, qui exécute via la cascade de résolution UI (OCR → template → VLM/grounding). Transport = **HTTP pull / long-poll** `GET /replay/next`. + +Trois engines pertinents : +- `agent_v0/server_v1/replay_engine.py` — pilotage côté serveur +- `agent_v0/server_v1/replay_verifier.py` — vérification post-action +- `agent_v0/server_v1/replay_learner.py` + `replay_memory.py` — apprentissage (import cassé sur `get_target_memory_store`) + +### 4.2. Diagnostic phare — Replay bloqué 8 mai 2026 + +Source : `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md`. Replay `replay_free_68ca51ab` sur `Urgence_aiva_demo` cancellé à 10:34. + +**Deux causes simultanées :** + +1. **Cause primaire — désync HTTP** : + Client Léa V1 utilise `read_timeout=5 s` sur `GET /replay/next` (`agent_v1/core/executor.py:1786`). Le serveur exécute parfois `extract_text` (5–7 s) PUIS dispatch un `click` dans le même appel HTTP. Le client coupe avant la réponse. L'action `click` est déjà *poppée* de la queue serveur et stockée dans `_retry_pending` mais **jamais re-dispatchée** (pas de watchdog). 9 actions sautées en 33 s sur les steps 10/12/14/17. + +2. **Cause aggravante — OCR-DIRECT center-of-line** : + `_resolve_by_ocr_text` (`resolve_engine.py:1447-1527`) retourne le centre de la **ligne docTR entière** quand le `target_text` est un sous-fragment (score 0.8). La barre de tabs Easily est détectée comme UNE ligne → `Imagerie` / `Notes médicales` / `Synthèse Urgences` renvoient tous (0.23, 0.28). Confirmé par e2e_singleshot du même jour. + +### 4.3. Hypothèses systématiquement testées (≠ rustinées) + +Cf. §2 du diagnostic. Sur 6 hypothèses : +- Hyp #1 (cascade serveur foire) : **infirmée** — la cascade n'est même jamais invoquée pour ces tabs. +- Hyp #2 (cascade locale Léa) : **infirmée** — client n'a rien reçu. +- Hyp #3 (coords brutes obsolètes) : **infirmée** — strict mode bypass la bbox. Anomalie cosmétique restante : ancres `anchor_0438bd2d9bdd_1778161174` et `anchor_6a2591e7c51c_1778229076` ont des bboxes décalées d'un cran à gauche. +- Hyp #4 (offset écran live vs record) : **partiellement vraie** — offset ±10-30 px, gérable, dégradé par le bug OCR-DIRECT. +- Hyp #5 (event onclick JS de la maquette) : **infirmée** — `addEventListener('click')` propre sur ``, aucun overlay. +- Hyp #6 (cache client/serveur) : **infirmée** — aucun `from_memory=True`, TargetMemoryStore pas hit. + +### 4.4. Quatre correctifs proposés (gradués) + +Repris du diagnostic §5 : + +| Correctif | Effort | Risque | Effet | +|---|---|---|---| +| **Quick fix 1** — `timeout=5 → 30` côté client | 5–10 min | très bas | Plus aucun click perdu sur extract_text/t2a_decision lents | +| **Quick fix 2** — OCR-DIRECT center-of-span | 10–15 min | moyen | Chaque tab résolu à son propre centre. À NE PAS appliquer à chaud démo | +| **Fix moyen terme** — watchdog `_retry_pending` côté serveur | 30–60 min | moyen | Toute action dispatchée sans REPORT depuis > 30s repush en tête de queue, `lea:dispatch_orphan_resent` | +| **Fix structurel** — SSE ou WebSocket | 1–2 j | — | Push ack-based, suppression du bug timeout pour de bon, détection immédiate de déconnexion | + +### 4.5. État post-démo (19 mai) — 5 bugs racines P0 non résolus + +Source : `docs/LESSONS_LEARNED_GHT_2026-05.md` §🔴. + +| Bug | Cause | Contournement actuel | +|---|---|---| +| VWB recapture anchor ne régénère pas le PNG | `capture.py` réutilise PNG existant ou écrit avant screenshot | recapture inutile | +| Stop VWB ne purge pas la queue serveur | pas d'appel `POST /api/v1/traces/stream/replay//cancel` au clic Stop | `./scripts/cancel-replays.sh` manuel | +| Coord client Léa Y cassé (÷ ~27) | `mss.monitors[1]` retourne intermittemment `2560×60` | aucun | +| Bug skip ord 13 orchestration | non identifiée, transition serveur→visuel→serveur | aucun, NOT REPRO 100% | +| Bug échelle pixel grounding Ollama | DETTE-006/010/014, `Qwen2VLImageProcessorFast` `patch_size=16` factor 32 ≠ 28 | non posé | + +Démo livrée grâce à `Demo_urgence_3_db` (46 steps, MOREL Catherine, UHCD 1750 €) et **bypass LLM static_result/static_text** (`replay_engine.py` steps 12-14). Pas réutilisable client. + +### 4.6. Contournements actifs côté replay (à survoler avant tout déploiement) + +Repris de `LESSONS_LEARNED_GHT_2026-05.md` (§⚠ Contournements actifs) : + +- Drift exemption template ≥ 0.95 / hybrid ≥ 0.80 (`resolve_engine.py:2367-2390`) +- Fallback heartbeat sur capture < 1200×800 (`api_stream.py:4422`) +- Flag pré-check OCR off par défaut (`RPA_ENABLE_TEXT_PRECHECK=false`) +- `RPA_VLM_MODEL=gemma4:e4b` hardcodé Léa (tag inexistant) — exporter `qwen2.5vl:7b` +- Mot de passe `loli` en clair dans scripts SSH/sudo +- Bypass Ctrl+V via `ydotool` au lieu de NoMachine clipboard + +### 4.7. Code orphelin/débranché côté replay (audit 8 mai) + +- `_resolve_by_yolo` défini, importé, jamais appelé (DETTE-004) +- `_fuzzy_match` import mort `api_stream.py:4372` +- `VisualEmbeddingManager` + `ScreenshotValidationManager` définis non instanciés (DETTE-005) +- `ShadowLearningHook` défini non instancié (DETTE-009) +- `_handle_possible_popup` côté client, 0 site d'appel +- Pre-check VLM par-clic désactivé par `if False:` dans `observe_reason_act.py:1704-1713` (DETTE-008) +- Trois implémentations `smart_resize` coexistent (DETTE-007) +- `pause_for_human` ignorée silencieusement en mode autonome (`api_stream.py:3011-3017`) + +### 4.8. Smoke test finalize → replay (2026-05-20) + +Source : `docs/SMOKE_TEST_FINALIZE_REPLAY_2026-05-20.md`. + +Le flux `finalize → proposition utilisateur → replay-session` est **déjà câblé** côté agent : +- `streamer.py:96-103, 627-635` — `set_on_finalize_result` + invocation +- `main.py:31, 222, 445-448` — wire callback dans `start_session` +- `finalize_contract.py` (untracked) — `dispatch_finalize_result(ui, payload, replay_name)` +- `smart_tray.py:577-599` — `offer_finalize_replay` + dialog consent Article 14 + +Contrat serveur attendu : `replay_launch.status` (`started`/`failed`), `replay_ready`, `replay_request`. + +**4 fichiers à SCP manuellement** vers Léa Windows (`finalize_contract.py` en premier, c'est un nouveau fichier, `ImportError` garanti sinon). + +--- + +## 5. Croisements et liens forts + +### 5.1. Le replay actuel échoue avant même que le grounding entre en scène + +Le diagnostic 8 mai est explicite : le bug primaire est **transport** (HTTP pull/long-poll, timeout client trop court, pas de watchdog), pas **vision**. Tous les efforts d'amélioration VLM (vLLM, InfiGUI, Qwen3-VL, smart_resize) sont nécessaires mais ne corrigeront RIEN tant que les actions sont perdues entre serveur et client. + +→ **Lien fort** : QW2 LoopDetector + QW4 SafetyChecksProvider (sprint mai) traitent les symptômes (UI bloquée, validation humaine) mais pas la cause racine (HTTP fragile). Une refonte SSE/WebSocket est citée comme "fix structurel" par le diagnostic 8 mai et n'apparaît dans aucun plan post-démo. + +### 5.2. Le pattern Planner-Actor-Validator de Skyvern décrit littéralement notre dette + +- VWB = Planner statique → mais **inflexible**, pas de re-planification au runtime. +- Léa = Actor → fonctionnel. +- Validator = absent ou laxiste → le bug step 10 (Imagerie cliqué dans bandeau Edge, REPORT success=True) en est l'illustration. + +Le pHash global utilisé pour VERIFY post-action est connu insuffisant (`feedback_phash_vs_dialog_in_vm.md`). Un Validator-as-component avec vérification sémantique (texte attendu présent dans la zone visée, par exemple) éliminerait toute la classe de bugs "j'ai cliqué quelque part mais pas où je voulais". + +### 5.3. OmniParser ↔ notre cascade + +OmniParser tokenise l'écran en éléments structurés AVANT le VLM principal. UI-DETR-1 fait quelque chose d'analogue mais SEULEMENT côté VWB recording, pas en replay runtime — asymétrie connue (cf. CLAUDE.md). Logger systématiquement la liste des candidats à chaque appel `_resolve_target` (suggestion §4.1 du doc inspiration) donnerait une "vue parsée" implicite sans refonte. + +### 5.4. medgemma:4b — l'illusion du modèle médical + +Apparaît avec un nom prometteur dans 2 benchs distincts (`BENCH_SAFETY_CHECKS_2026-05-06.md` §résultats, `BENCH_T2A_DECISION_11DOSSIERS.md` #12). **Mauvais aux deux** : `[]` systématique en safety_checks, 4/11 (36%) en T2A avec JSON cassé. Le nom suggère plus que la performance. + +### 5.5. Le bug d'échelle bbox_2d est la racine documentée d'un problème encore actif + +`MIGRATION_VLM_PLAN_2026-05-09.md` documente précisément la cause (smart_resize Ollama opaque). Le module officiel a été commité (`0d7bcd18a`) mais DETTE-014 signale qu'il est mal calé. Trois implémentations coexistent (DETTE-007). Le bench bbox cible (§5 du plan) n'a **pas été refait** post-migration — checkpoint manquant pour valider la trajectoire. + +### 5.6. Les frameworks externes valident notre direction + +OpenAdapt + Skyvern + OmniParser + TagUI = 4 projets matures qui formalisent ce qu'on fait déjà en pratique. Convergence indépendante = signal fort. + +Mais ce qu'ils ont en plus n'est pas trivial à rattraper : +- Vocabulaire normé (Policy/Grounding/Safety Gate/Validator) → adoptable sans refonte, gain pitch. +- Pre-built templates / use cases → maturité de marché, on n'a que la démo GHT. +- Evaluation-Driven Feedback (OpenAdapt) — on a `TargetMemoryStore` (Phase 1 apprentissage Léa) mais sans pipeline d'entraînement. + +--- + +## 6. Carte des documents de référence (à charger dans le contexte selon le sujet) + +### Étude techno externe + +- **`docs/INSPIRATION_FRAMEWORKS_2026-05-10.md`** — OpenAdapt / Skyvern / OmniParser / TagUI, patterns à adopter +- **`docs/QW_SUITE_MAI.md`** — synthèse livraison QW1+QW2+QW4 (5 frameworks computer-use) +- **`docs/superpowers/specs/2026-05-05-qw-suite-mai-design.md`** — spec design détaillée, décisions architecturales +- **`docs/superpowers/plans/2026-05-05-qw-suite-mai.md`** — plan d'exécution + +### VLM, grounding, modèles + +- **`docs/HISTORIQUE_VLM_IMPLEMENTATIONS_2026-05-08.md`** — audit complet implémentations, commits, code potentiellement perdu (rien d'irrécupérable) +- **`docs/MIGRATION_VLM_PLAN_2026-05-09.md`** — plan migration `qwen2.5vl Ollama → vLLM/Transformers Qwen3-VL`, bench bbox_2d +- `docs/VLM_DETECTION_IMPLEMENTATION.md` — implémentation initiale (22 nov 2025) +- `docs/OLLAMA_INTEGRATION.md` — intégration Ollama +- `docs/reference/QWEN3_VL_CONFIGURATION.md` — config Qwen3-VL + +### Benchmarks + +- **`docs/BENCH_T2A_DECISION_11DOSSIERS.md`** — 18 modèles × 11 DPI, gemma3:27b-cloud retenu +- **`docs/BENCH_SAFETY_CHECKS_2026-05-06.md`** — 5 modèles × 5 scénarios, gemma4:latest retenu +- `docs/BENCH_MEDGEMMA.md` + `.json` — medgemma:4b CIM-10 (écarté pour safety/T2A) +- `docs/BENCH_MINI_LLM_NLP.md` — 14 commandes NLP fr pour Léa +- `docs/QW_SMOKE_TESTS_2026-05-06.md` — smoke tests QW Suite + +### Replay — diagnostic et méthodes + +- **`docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md`** — diagnostic complet replay_free_68ca51ab, 4 correctifs proposés, le doc le plus instructif sur les méthodes +- **`docs/LESSONS_LEARNED_GHT_2026-05.md`** — 15 jours de bug-chasing, 5 bugs P0, contournements actifs +- `docs/SMOKE_TEST_FINALIZE_REPLAY_2026-05-20.md` — flux finalize → replay câblé, fichiers à SCP +- `docs/AUDIT_FINALIZE_CONTRACT_INTEGRATION_2026-05-20.md` — audit contrat finalize +- `docs/CR_AUDIT_PAUSED_RESUME_BUS_2026-05-22.md` — audit pause/resume bus de replay +- `docs/CR_AUDIT_SETUP_VISUAL_GUARDS_2026-05-22.md` — audit guards visuels au replay +- `docs/AUDIT_LEA_FIRST_RUNTIME_2026-05-19.md` — audit runtime Léa-first post-démo +- `docs/EXECUTION_LOOP_FLAGS.md` — flags boucle d'exécution +- `docs/DETTE_TECHNIQUE.md` — 14 entrées (DETTE-001 à DETTE-014) + +### Audits transversaux 8-10 mai + +- `docs/AUDIT_CONTROLES_DEBRANCHES_2026-05-08.md` — 50+ findings serveur+client +- `docs/AUDIT_DIM_TIM_DEMO_GHT_2026-05-08.md` — audit médecin DIM + TIM +- `docs/AUDIT_BDD_WORKFLOW_2026-05-10.md` — audit BDD workflows +- `docs/BUG_PRECHECK_SPATIAL_BLINDNESS_2026-05-08.md` — DETTE-001 +- `docs/INVESTIGATION_MEMOIRE_VISUELLE_ORPHELINE_2026-05-09.md` — DETTE-005, DETTE-009 +- `docs/CARTE_FONCTIONNELLE_2026-05-08.md` — carte fonctionnelle système + +### Vision/architecture de fond + +- `docs/VISION_RPA_INTELLIGENT.md` — vision produit (CLAUDE.md `feedback_follow_spec` : à lire AVANT toute proposition) +- `docs/ROADMAP_RPA_100_VISION.md` — roadmap 100% vision +- `docs/REFERENCE_VISION_RPA.md` — référence VWB Vision RPA (jan 2026) +- `docs/CARTOGRAPHY.md` — cartographie d'exécution +- `docs/PLAN_ACTEUR_V1.md` / `docs/PLAN_APPRENTISSAGE_LEA.md` / `docs/PLAN_AGENT_RUST.md` — plans cibles + +--- + +## 7. Projets et benchmarks externes nommément cités + +À reprendre dans un brief ou un mail (sourcing déjà fait dans `INSPIRATION_FRAMEWORKS_2026-05-10.md` §8) : + +- **OpenAdapt v1.0** — https://github.com/OpenAdaptAI/OpenAdapt +- **OmniParser V2** — https://github.com/microsoft/OmniParser +- **Skyvern** — https://github.com/Skyvern-AI/skyvern +- **TagUI** — AI Singapore +- **Simular Agent-S** — Simular (5 framework QW Suite) +- **browser-use** — open source, source LoopDetector +- **OpenAI CUA sample** — pattern Safety Checks +- **Coasty open-cu** — open source +- **Showlab OOTB** — source multi-écrans +- Benchmarks externes : **WindowsAgentArena**, **ScreenSpot**, **ScreenSpot-Pro**, **WebVoyager** (Skyvern 85.85%) + +--- + +## 8. Ce que cette synthèse n'aborde pas (à demander à Dom si besoin) + +- Estimation chiffrée du coût d'une refonte SSE/WebSocket dans `api_stream.py`. +- Comparaison vLLM vs Transformers direct pour le grounding (le plan migration laisse la question ouverte). +- État d'OpenAdapt Evaluation-Driven Feedback vis-à-vis de notre `TargetMemoryStore` actuel. +- Tests des modèles candidats à un fine-tuning GUI (GUI-R1, OS-Atlas, ShowUI, SE-GUI) cités dans `memory/project_finetuning_vlm_plan.md` mais hors périmètre des bench mai. +- Bench de UI-DETR-1 standalone (utilisé au recording VWB, pas évalué isolément). + +--- + +*Document destiné à être consommé en lecture seule par un agent ou un humain qui doit se mettre à niveau sur l'état des études techno et du replay au 23 mai 2026. Toute action à prendre est hors-périmètre de ce doc et nécessite une décision explicite de Dom.* diff --git a/docs/architecture/CARTOGRAPHIE_BRIQUES_MANDAT_PROTOCOLS_2026-05-25.md b/docs/architecture/CARTOGRAPHIE_BRIQUES_MANDAT_PROTOCOLS_2026-05-25.md new file mode 100644 index 000000000..f9fb5b4c8 --- /dev/null +++ b/docs/architecture/CARTOGRAPHIE_BRIQUES_MANDAT_PROTOCOLS_2026-05-25.md @@ -0,0 +1,474 @@ +# Cartographie des briques existantes — modèle Mandat / Protocoles + +Date : 2026-05-25 +Statut : lecture structurelle, pas une spec d'implémentation + +## Synthèse + +Le projet contient déjà la plupart des briques nécessaires au modèle : + +```text +intention / mandat -> agent_chat + working_memory + ORA +protocoles / gestes -> gesture_catalog + autonomous_planner + workflows VWB +observation visuelle -> capturers + grounding + SomEngine + OCR/VLM +scènes / dialogues -> DialogResolver + window guards + UI patterns +action -> agent_v1 executor + replay engine +vérification retour -> ReplayVerifier + Validator V2 + shadow validator +apprentissage -> replay_learner + target_memory + correction packs + coaching +évaluation -> LeaBench + tests replay + démo DPI +supervision -> pause supervisée + UI Léa + confirmation loop +``` + +La faiblesse principale n'est pas l'absence de composants. C'est leur absence de **contrat unificateur** autour de : + +```text +mandat -> intention -> scène -> affordance -> geste -> retour -> preuve apprenable +``` + +## Briques runtime live + +### Agent Windows live + +Fichiers : + +```text +agent_v0/agent_v1/core/executor.py +agent_v0/agent_v1/core/grounding.py +agent_v0/agent_v1/core/policy.py +agent_v0/agent_v1/core/recovery.py +agent_v0/agent_v1/core/system_dialog_guard.py +agent_v0/agent_v1/ui/* +``` + +Rôle actuel : + +```text +exécuter les actions ; +résoudre les cibles visuelles ; +faire les fallbacks ; +pauser/demander de l'aide ; +gérer certains dialogues ; +remonter les résultats au serveur. +``` + +Lien avec le modèle : + +```text +geste +observation locale +doute de localisation +doute de scène +retour action +pause supervisée +``` + +Manque conceptuel : + +```text +pas encore de trace causale complète ; +pas de mandat/intention actif porté explicitement ; +fallbacks encore trop locaux si non encadrés par le contrat ; +pas de typage explicite du doute dans l'API résultat. +``` + +### Serveur replay / coordination + +Fichiers : + +```text +agent_v0/server_v1/api_stream.py +agent_v0/server_v1/stream_processor.py +agent_v0/server_v1/replay_engine.py +agent_v0/server_v1/replay_watchdog.py +agent_v0/server_v1/replay_verifier.py +agent_v0/server_v1/replay_learner.py +agent_v0/server_v1/replay_memory.py +agent_v0/server_v1/resolve_engine.py +``` + +Rôle actuel : + +```text +construire/rejouer les actions ; +distribuer les actions à l'agent ; +résoudre les cibles côté serveur ; +vérifier les retours ; +planifier retries ou pauses ; +enregistrer résultats et apprentissages. +``` + +Lien avec le modèle : + +```text +protocole candidat sous forme workflow/replay ; +résolution scène/affordance via resolve_engine ; +retour via ReplayVerifier ; +preuve potentielle via replay_learner/replay_memory ; +pause supervisée ; +temporalité via watchdog/retry. +``` + +Manque conceptuel : + +```text +le workflow reste la structure dominante ; +les expected_result existent mais ne forment pas encore une trace causale ; +la vérification ne sait pas toujours quelle intention elle valide ; +les retries ne sont pas encore arbitrés par doute typé. +``` + +## Briques cognitives déjà présentes + +### Mémoire de travail + +Fichier : + +```text +core/cognition/working_memory.py +``` + +Points forts : + +```text +objective ; +current_step ; +observation courante ; +historique d'actions ; +faits appris ; +expected_screen ; +confidence ; +needs_help ; +timing. +``` + +Correspondance modèle : + +```text +mandat/intention embryonnaire ; +trace courte ; +précondition attendue via expected_screen ; +temporalité ; +doute via confidence/needs_help. +``` + +Manque : + +```text +pas encore de scène d'intention typée ; +pas de trace causale formelle geste/scène/intention/hypothèse/retour ; +pas de distinction correction/démonstration/validation. +``` + +### Boucle ORA + +Fichier : + +```text +core/execution/observe_reason_act.py +``` + +Points forts : + +```text +observe -> reason -> act -> verify ; +Decision avec expected_after ; +VerificationResult ; +mode instruction via VLM ; +erreurs typées de base. +``` + +Correspondance modèle : + +```text +base naturelle pour le contrat d'action ; +hypothèse de retour via expected_after ; +vérification post-geste. +``` + +Manque : + +```text +encore orienté step/workflow ; +pas de protocole ouvert ; +pas de preuve apprenable structurée ; +pas d'opérateur d'extension de grammaire. +``` + +## Briques intention / gestes / risque + +### Agent chat et planification autonome + +Fichiers : + +```text +agent_chat/intent_parser.py +agent_chat/autonomous_planner.py +agent_chat/gesture_catalog.py +agent_chat/confirmation.py +agent_chat/urgences_orchestrator.py +``` + +Points forts : + +```text +parseur d'intentions utilisateur ; +planner libre sans workflow pré-enregistré ; +catalogue de gestes universels Windows/navigateur/édition ; +évaluation de risque ; +confirmation utilisateur ; +orchestrateur urgence avec intentions nommées. +``` + +Correspondance modèle : + +```text +mandat ; +protocoles universels ; +gestes types ; +niveau de risque ; +protocole métier urgence. +``` + +Manque : + +```text +cette couche semble parallèle au runtime live, pas intégrée au replay agent_v1 ; +le risque est lexical et statique, pas encore tutoré par niveau de délégation ; +les gestes ne sont pas encore reliés à scènes/affordances/retours qualifiés. +``` + +## Briques scènes / dialogues + +### DialogResolver + +Fichiers : + +```text +agent_v0/server_v1/core/dialog/catalog.py +agent_v0/server_v1/core/dialog/resolver.py +tests/unit/test_dialog_resolver.py +tests/integration/test_dialog_resolver_endpoint.py +``` + +Points forts : + +```text +catalogue de dialogues connus ; +policy auto/pause/skip ; +protection modaux système ; +pause conservative sans match ; +evidence title + OCR/UIA. +``` + +Correspondance modèle : + +```text +scènes d'intention simples ; +affordances compatibles par dialogue ; +affordances anti-intention potentielles ; +rupture de scène. +``` + +Manque : + +```text +pas encore branché sur une intention active ; +un dialogue est connu en absolu, pas encore "compatible avec ce mandat" ; +les scènes composées métier restent hors catalogue. +``` + +## Briques vérification / preuve + +### ReplayVerifier et Validator V2 + +Fichiers : + +```text +agent_v0/server_v1/replay_verifier.py +core/validation/orchestrator.py +core/workflow/shadow_validator.py +tests/unit/test_replay_critic.py +tests/integration/test_validator_step10.py +``` + +Points forts : + +```text +vérification pixel ; +critic sémantique avec expected_result ; +verdicts agrégés ; +shadow validation. +``` + +Correspondance modèle : + +```text +qualification du retour ; +preuve apprenable potentielle ; +doute d'effet ; +temporalité post-action. +``` + +Manque : + +```text +le retour n'est pas toujours rattaché à intention/scène/affordance ; +le "rien ne change" n'est pas encore qualifié par protocole ; +la preuve apprenable n'est pas encore un objet explicite. +``` + +## Briques apprentissage / correction + +### Mémoire et corrections + +Fichiers : + +```text +core/learning/target_memory_store.py +agent_v0/server_v1/replay_learner.py +agent_v0/server_v1/replay_memory.py +core/corrections/* +core/coaching/* +``` + +Points forts : + +```text +mémoire de cible persistante JSONL + SQLite ; +résultats replay enregistrés ; +correction packs versionnés ; +statistiques succès/échec ; +sessions de coaching avec accept/reject/correct/manual/skip. +``` + +Correspondance modèle : + +```text +preuve apprenable ; +correction ; +démonstration ; +validation ; +désapprentissage par baisse confiance / failure_count ; +niveau de délégation tutoré. +``` + +Manque : + +```text +les preuves actuelles sont très orientées cible UI ; +il manque intention/scène/affordance/retour dans la clé d'apprentissage ; +correction et démonstration sont proches mais pas encore distinguées au niveau modèle ; +le niveau de délégation par protocole/environnement n'est pas centralisé. +``` + +## Briques évaluation / terrains d'essai + +### LeaBench + +Fichiers : + +```text +core/evaluation/computer_use_bench.py +core/evaluation/ollama_lea_bench_adapter.py +tools/lea_bench.py +tools/lea_bench_ollama.py +benchmarks/computer_use/cases/*.jsonl +``` + +Rôle : + +```text +mesurer décisions computer-use ; +cas Bloc-notes/replay ; +adapter modèles locaux ; +scorer abstention/danger. +``` + +Usage modèle : + +```text +tester scène/intention/affordance ; +mesurer action fiable vs abstention ; +convertir échecs réels en cas reproductibles. +``` + +### Maquette DPI / urgence + +Fichiers : + +```text +demo/facturation_urgences/* +tests/e2e/test_urgence_aiva_demo.py +tests/e2e/fixtures/urgence_aiva_demo/* +``` + +Rôle : + +```text +terrain métier ; +cas urgences synthétiques ; +workflow démo existant ; +stress test scènes composées et protocole métier. +``` + +Usage modèle : + +```text +protocole métier générique ; +adaptateur applicatif ; +apprentissage sans contenu sensible réel ; +validation de la généralisation hors Bloc-notes. +``` + +## Diagnostic structurel + +Le projet a déjà presque toutes les pièces, mais elles ne sont pas au même étage : + +```text +agent_chat sait raisonner intention/risque mais n'est pas le runtime live principal ; +agent_v1 sait exécuter live mais raisonne encore action/cible ; +server_v1 sait vérifier/apprendre mais pas sous trace causale complète ; +core/cognition a la mémoire de travail mais elle n'est pas encore le fil commun ; +core/learning sait mémoriser mais pas encore apprendre une grammaire d'action ; +LeaBench sait mesurer mais doit recevoir des cas "mandat/protocole/scène". +``` + +## Assemblage conceptuel recommandé + +Sans parler encore de code, la bonne direction est d'utiliser : + +```text +core/cognition/working_memory.py +comme fil de mandat/intention/trace causale ; + +agent_chat/gesture_catalog.py +comme base des protocoles universels ; + +agent_v0/server_v1/core/dialog/* +comme catalogue initial de scènes/dialogues ; + +agent_v0/server_v1/replay_verifier.py + core/validation/* +comme qualification du retour ; + +core/learning + core/corrections + core/coaching +comme apprentissage tutoré et désapprentissage ; + +LeaBench + Bloc-notes + maquette DPI +comme banc de validation progressif. +``` + +## Point d'attention immédiat + +Une correction technique est déjà en attente dans : + +```text +agent_v0/agent_v1/core/grounding.py +``` + +Elle bloque le fallback texte local après rejet sémantique serveur. Elle correspond directement à la règle : + +```text +un rejet sémantique doit dominer les fallbacks opportunistes. +``` + +Elle devra être testée et intégrée quand on repassera en exécution. diff --git a/docs/architecture/CARTOGRAPHIE_MICRO_APPRENTISSAGE_LEA_2026-05-27.md b/docs/architecture/CARTOGRAPHIE_MICRO_APPRENTISSAGE_LEA_2026-05-27.md new file mode 100644 index 000000000..6c68dd667 --- /dev/null +++ b/docs/architecture/CARTOGRAPHIE_MICRO_APPRENTISSAGE_LEA_2026-05-27.md @@ -0,0 +1,99 @@ +# Cartographie micro-apprentissage Lea - 2026-05-27 + +## But + +Passer du replay metier fragile a une boucle courte ou Lea observe des gestes simples, les interprete, verifie leur effet, puis memorise ce qui marche. + +On reutilise l'existant. On ne fabrique pas une boite a clic. + +Premiere competence retenue: `ouvrir le menu Demarrer`. C'est plus atomique que "ouvrir Chrome" ou "ouvrir Word" et sert de base aux competences composees suivantes. + +## Briques a reutiliser maintenant + +| Besoin | Brique existante | Chemin | +|---|---|---| +| Demarrer une demonstration humaine | Bouton `Apprenez-moi` + `SharedState.start_recording()` | `agent_v0/agent_v1/ui/chat_window.py`, `agent_v0/agent_v1/ui/shared_state.py` | +| Capturer clics/frappes/ecran | `AgentV1.start_session()` + `TraceStreamer` | `agent_v0/agent_v1/main.py`, `agent_v0/agent_v1/network/streamer.py` | +| Recevoir les evenements | endpoint `/event` + `StreamProcessor.process_event()` | `agent_v0/server_v1/api_stream.py`, `agent_v0/server_v1/stream_processor.py` | +| Construire une trace exploitable | `StreamProcessor.finalize_session()` + `GraphBuilder` | `agent_v0/server_v1/stream_processor.py`, `core/graph/graph_builder.py` | +| Apprendre des corrections humaines | `ReplayLearner.record_human_correction()` | `agent_v0/server_v1/replay_learner.py` | +| Memoriser les cibles fiables | `memory_record_success()` / `memory_lookup()` | `agent_v0/server_v1/replay_memory.py`, `core/learning/target_memory_store.py` | +| Verifier qu'une action a eu un effet | `ReplayVerifier.verify_action()` | `agent_v0/server_v1/replay_verifier.py` | +| Parler clairement a l'humain | contrat 4 champs | `agent_v0/agent_v1/ui/message_contract.py` | +| Verifier le socle technique | preflight read-only | `tools/lea_micro_preflight.py` | + +## Briques a eviter pour la V1 micro-learning + +- Replay VWB/DAG comme moteur principal. +- Coordonnees brutes comme verite finale. +- Clipboard global Linux/Windows/NoMachine comme transport de donnees. +- Scenarios metier longs. +- Corrections injectees a la main dans un workflow pour faire passer une demo. + +## Flux minimal + +1. Observation + Dom lance `Apprenez-moi`, nomme une micro-tache, puis montre 1 geste court. + +2. Interpretation + Le serveur conserve les evenements, les screenshots, les titres de fenetre, les indices UIA/OCR/SoM disponibles. + +3. Generalisation prudente + Lea derive une competence courte: intention, contexte d'ecran attendu, signaux visuels, postcondition observable. + +4. Tentative encadree + Lea ne tente en autonomie que si la cible est suffisamment decrite et verifiable. + +5. Verification + Pixel diff seul ne suffit pas. Il faut verifier un effet observable: fenetre ouverte, champ rempli, titre change, application fermee, texte present. + +6. Memoire + Les succes repetes alimentent `TargetMemoryStore`. Les echecs et corrections humaines alimentent `ReplayLearner`. + +7. Demande humaine + Si Lea ne sait pas, elle doit afficher: + + ```text + J'essaie de : + J'attendais : + Je vois : + Peux-tu : + ``` + +## Etats d'une competence + +| Etat | Sens | +|---|---| +| OBSERVATION | Lea a seulement vu le geste humain. | +| COACHING | Lea peut proposer une tentative, mais demande confirmation/correction. | +| AUTO_CANDIDATE | Lea a reussi plusieurs fois avec verification. | +| AUTO | Lea peut executer seule avec garde-fous. | + +## Fichiers a lire/modifier ensuite + +Priorite 1: + +- `agent_v0/agent_v1/ui/chat_window.py` : texte de lancement/fin d'apprentissage. +- `agent_v0/agent_v1/ui/shared_state.py` : metadata de session micro-learning. +- `agent_v0/agent_v1/main.py` : nommage session + mode capture. +- `agent_v0/server_v1/stream_processor.py` : sortie de finalisation orientee competence courte. +- `agent_v0/server_v1/replay_learner.py` : persistance des corrections et resultats. + +Priorite 2: + +- `agent_v0/server_v1/replay_memory.py` : fiabilite et collisions sur boutons generiques. +- `agent_v0/server_v1/replay_verifier.py` : verifier des postconditions simples. +- `agent_v0/agent_v1/core/executor.py` : mode correction humaine et messages visibles. + +## Risques prioritaires + +1. Confondre trace de demonstration et competence generalisable. +2. Valider une action seulement parce que les pixels bougent. +3. Apprendre une coordonnee dependante DPI/ecran. +4. Produire un message conforme au contrat mais pauvre en contexte. +5. Recharger le VLM au mauvais moment et ajouter une latence cold start. +6. Melanger session de capture et replay actif. + +## Regle de travail + +Chaque micro-tache doit tenir en moins de deux minutes, avec une postcondition observable. Si on ne peut pas formuler la postcondition, on ne l'apprend pas encore. diff --git a/docs/architecture/MODELE_MANDAT_PROTOCOLS_LEA_2026-05-25.md b/docs/architecture/MODELE_MANDAT_PROTOCOLS_LEA_2026-05-25.md new file mode 100644 index 000000000..fdc83c753 --- /dev/null +++ b/docs/architecture/MODELE_MANDAT_PROTOCOLS_LEA_2026-05-25.md @@ -0,0 +1,609 @@ +# Modèle Mandat / Protocoles / Scènes pour Léa + +Version : 0.1 brainstorming +Date : 2026-05-25 +Statut : modèle conceptuel, pas une spec technique + +## Thèse + +Léa n'est pas une boîte à clics et ne doit pas rejouer un workflow. Léa est une collaboratrice visuelle mandatée. + +Elle reçoit une fin à atteindre, choisit un chemin connu ou apprend un chemin nouveau, observe le retour de ses actions, qualifie réussite/échec/doute, puis adapte son comportement. + +Formule centrale : + +```text +Un protocole est une grammaire d'action autour d'une intention. +``` + +Un logiciel, un OS ou un DPI peut changer les pixels, les titres, les boutons et les DPI. Le processus métier reste souvent stable : chercher, ouvrir, saisir, valider, enregistrer, corriger, transmettre, facturer, archiver. Léa doit apprendre ces grammaires d'action, pas mémoriser des coordonnées. + +## Vocabulaire + +### Mandat + +Un mandat est une fin déléguée à Léa. + +Exemples : + +```text +Ecris ce texte et enregistre-le. +Trouve une vidéo de jazz et lance-la. +Ouvre le dossier patient de Mme X. +Saisis cette information dans le logiciel métier. +``` + +Le mandat n'est pas une suite de clics. Il donne le but, le contexte et éventuellement les limites. + +### Intention active + +L'intention active est le sous-but courant qui sert le mandat. + +Exemple pour `Ecris ce texte et enregistre-le` : + +```text +ouvrir un outil de saisie +écrire le texte +déclencher la sauvegarde +choisir ou confirmer le nom +vérifier que le fichier existe +``` + +### Protocole d'usage + +Un protocole d'usage est un chemin connu, adaptable, pour accomplir une intention. + +Exemples universels : + +```text +ouvrir une application +chercher sur le web +saisir du texte +sauvegarder un fichier +confirmer une boîte de dialogue +fermer une fenêtre +copier-coller +``` + +Exemples métier : + +```text +ouvrir une fiche patient +créer une ligne de facturation +valider une commande de stock +rapprocher une écriture comptable +``` + +Un protocole n'est pas un workflow figé. Il contient des scènes attendues, des affordances compatibles, des variantes autorisées et des conditions d'arrêt. + +### Scène + +Une scène est la situation visuelle pertinente pour l'intention courante. + +La scène active pertinente n'est pas forcément la fenêtre au focus OS. C'est la zone ou la boîte qui sert le mandat maintenant. + +Exemples : + +```text +Bloc-notes prêt à recevoir du texte. +Fenêtre Enregistrer sous. +Dialogue "Voulez-vous enregistrer les modifications ?". +Page de résultats Google. +Page vidéo YouTube. +Fiche patient ouverte. +``` + +### Affordance + +Une affordance est une action proposée par la scène. + +Exemples : + +```text +bouton Enregistrer +bouton Annuler +champ Nom du fichier +barre d'adresse +champ de recherche +bouton Lecture +onglet Patient +bouton Valider +``` + +Léa ne doit pas seulement reconnaître une affordance. Elle doit comprendre son rôle dans la scène et sa compatibilité avec l'intention. + +### Geste + +Un geste est l'action concrète décidée dans une scène. + +Exemples : + +```text +cliquer sur Enregistrer +taper le nom du fichier +appuyer sur Ctrl+S +sélectionner un résultat de recherche +cliquer sur Lecture +``` + +### Retour + +Un retour est tout changement ou non-changement observé après un geste. + +```text +Résultat attendu obtenu -> réussite. +Résultat contraire -> échec. +Rien ne change -> attente, latence ou échec à qualifier. +Nouvelle fenêtre -> événement à interpréter. +Erreur -> rupture ou branche prévue selon le protocole. +``` + +### Doute + +Le doute est un signal utile, pas une faiblesse. + +Léa doute quand : + +```text +les sources visuelles divergent ; +la scène observée ne correspond pas à l'intention ; +aucun protocole connu ne s'applique ; +un retour attendu n'arrive pas ; +une action répétée ne produit aucun effet ; +la scène est sensible ou irréversible sans mandat explicite. +``` + +Le doute peut mener à une variante, une demande d'aide, ou une pause. + +## Boucle cognitive minimale + +```text +1. Recevoir le mandat. +2. Déduire l'intention active. +3. Choisir le protocole connu le plus simple. +4. Observer la scène active pertinente. +5. Identifier les affordances disponibles. +6. Choisir le geste compatible avec l'intention. +7. Agir. +8. Observer le retour. +9. Qualifier : réussite, échec, attente, rupture, doute. +10. Continuer, essayer une variante, demander de l'aide, ou apprendre. +``` + +Le point essentiel : une action n'est pas justifiée par le fait qu'un bouton existe. Elle est justifiée parce que ce bouton, dans cette scène, sert l'intention active. + +## Contrat d'action + +Avant d'agir, Léa doit pouvoir répondre implicitement à cinq questions : + +```text +Quelle intention est-ce que je sers ? +Dans quelle scène suis-je ? +Quelle affordance est-ce que j'utilise ? +Pourquoi cette affordance est-elle compatible avec mon intention ? +Quel retour est-ce que j'attends ? +``` + +Si Léa ne peut pas produire cette justification, elle ne doit pas transformer l'action en clic opportuniste. + +Ce contrat n'impose pas de demander à l'humain à chaque doute. Il impose que toute tentative ait une hypothèse vérifiable. + +## Autonomie + +L'autonomie de Léa est une autonomie d'initiative, pas une autonomie d'entêtement. + +Léa peut : + +```text +choisir le chemin le plus simple ; +changer de chemin si le premier échoue ; +essayer une variante cohérente ; +interpréter un retour ; +demander de l'aide ; +apprendre après aide ou résultat qualifié. +``` + +Léa ne doit pas : + +```text +agir coûte que coûte ; +inventer une réussite ; +apprendre un échec comme une réussite ; +continuer un fallback après rejet sémantique ; +sortir d'un protocole sans raison explicable. +``` + +Le risque n'est pas interdit. Il doit être exploitable : + +```text +observable +attribuable à une intention +réversible si possible +évaluable après coup +transformable en apprentissage +``` + +## Structure conceptuelle d'un protocole + +Un protocole peut se décrire sur un coin de papier avec les éléments suivants : + +```text +Nom +Intention servie +Préconditions plausibles +Scènes attendues +Affordances compatibles par scène +Gestes possibles +Variantes autorisées +Retours attendus +Branches normales +Ruptures connues +Conditions de réussite +Conditions d'abstention ou demande d'aide +Preuves apprenables +``` + +Ce n'est pas une séquence figée. C'est une grammaire : elle autorise plusieurs phrases correctes pour atteindre la même intention. + +## Exemple 1 : ouvrir un logiciel + +Mandat : + +```text +Ouvre Bloc-notes. +``` + +Intention : + +```text +rendre disponible un outil de saisie texte simple +``` + +Protocoles possibles : + +```text +menu Démarrer / recherche Windows +raccourci existant +commande Exécuter +barre de recherche système +``` + +Scènes attendues : + +```text +bureau ou environnement de départ +menu/recherche d'application +résultat "Bloc-notes" +fenêtre Bloc-notes ouverte +``` + +Affordances compatibles : + +```text +champ de recherche +résultat d'application Bloc-notes +zone de texte vide +``` + +Retours attendus : + +```text +une fenêtre de saisie texte apparaît +elle accepte le focus +elle permet de taper du texte +``` + +Variantes : + +```text +si la recherche Windows échoue, essayer Exécuter/notepad +si un autre éditeur texte est disponible et accepté par le mandat, l'utiliser +si aucune scène d'édition texte n'apparaît, demander ou apprendre +``` + +## Exemple 2 : saisir un texte + +Mandat : + +```text +Saisis "testtesttest". +``` + +Intention : + +```text +placer le contenu texte dans une zone éditable +``` + +Scènes attendues : + +```text +éditeur texte ouvert +champ texte actif +curseur visible ou zone éditable détectée +``` + +Affordances compatibles : + +```text +zone de saisie +document vide ou modifiable +``` + +Gestes : + +```text +focus zone éditable si nécessaire +taper le texte +``` + +Retours attendus : + +```text +le texte apparaît +le contenu correspond au mandat +``` + +Ruptures : + +```text +zone non éditable +fenêtre inattendue +texte non apparu +application fermée +``` + +## Exemple 3 : enregistrer un fichier + +Mandat : + +```text +Enregistre le texte saisi. +``` + +Intention : + +```text +persister le contenu courant dans un fichier +``` + +Protocoles possibles : + +```text +Ctrl+S +menu Fichier > Enregistrer +bouton Enregistrer si visible +Enregistrer sous si le fichier n'a pas encore de nom +``` + +Scènes attendues : + +```text +éditeur texte avec contenu non sauvegardé +dialogue Enregistrer sous +dialogue "voulez-vous enregistrer les modifications ?" +dialogue "le fichier existe déjà, voulez-vous le remplacer ?" +retour à l'éditeur avec état sauvegardé +``` + +Affordances compatibles : + +```text +Enregistrer +Oui +Remplacer, si le mandat autorise l'écrasement +champ Nom du fichier +``` + +Affordances contraires : + +```text +Annuler +Ne pas enregistrer +Non, si la question porte sur la sauvegarde souhaitée +``` + +Retours attendus : + +```text +la boîte de sauvegarde se ferme +le fichier existe à l'emplacement choisi +le contenu est présent +le document n'est plus en état non sauvegardé +``` + +Règle clé : + +```text +Voir un bouton "Enregistrer" ne suffit pas. +Il faut que ce bouton soit dans une scène compatible avec l'intention de sauvegarde. +``` + +## Exemple 4 : regarder une vidéo de jazz + +Mandat : + +```text +Trouve et lance une vidéo de jazz. +``` + +Intention : + +```text +obtenir une vidéo en lecture correspondant au thème jazz +``` + +Protocoles possibles : + +```text +ouvrir navigateur -> YouTube -> chercher jazz -> lancer une vidéo +ouvrir navigateur -> moteur de recherche -> "video jazz" -> ouvrir un résultat vidéo +utiliser une application ou un favori connu si disponible +``` + +Scènes attendues : + +```text +navigateur ouvert +barre d'adresse ou champ de recherche +page de résultats +page vidéo +lecteur avec bouton Lecture ou vidéo déjà en lecture +``` + +Affordances compatibles : + +```text +barre d'adresse +champ de recherche +résultat vidéo pertinent +bouton Lecture +``` + +Retours attendus : + +```text +une vidéo démarre +le contenu semble lié au jazz +le son ou la lecture est active si observable +``` + +Variantes : + +```text +si YouTube est inaccessible, utiliser le moteur de recherche +si le premier résultat n'est pas pertinent, revenir et choisir un autre résultat +si un consentement cookie bloque la scène, traiter le dialogue seulement s'il est compatible avec la navigation +``` + +## Généralisation multi-environnements + +Léa doit apprendre à plusieurs niveaux. + +### Niveau 1 : mémoire locale + +```text +Dans cet écran précis, ce bouton sauvegarde. +Dans ce DPI, cette scène ressemble à Save As. +Dans ce logiciel, ce champ est le champ de recherche patient. +``` + +### Niveau 2 : protocole applicatif + +```text +Dans ce logiciel DPI, ouvrir un dossier patient passe par recherche -> liste -> fiche. +Dans ce logiciel comptable, valider une écriture passe par saisie -> contrôle -> validation. +``` + +### Niveau 3 : protocole métier + +```text +Tous les DPI ont une façon de chercher un patient, ouvrir sa fiche, saisir une information, valider et tracer. +Tous les logiciels de stock ont une façon de rechercher un article, ajuster une quantité, valider un mouvement. +Tous les logiciels comptables ont une façon de saisir, contrôler, rapprocher, valider. +``` + +### Niveau 4 : protocole universel + +```text +chercher +ouvrir +saisir +valider +annuler +enregistrer +confirmer +revenir +fermer +``` + +La généralisation consiste à relier les preuves locales à ces niveaux supérieurs. + +## Apprentissage + +Léa apprend seulement à partir d'un résultat qualifié. + +Apprentissage valide : + +```text +L'action a produit le retour attendu. +L'humain a confirmé ou corrigé. +La scène et l'intention sont connues. +Le geste est attribuable au résultat. +``` + +Apprentissage interdit : + +```text +clic opportuniste sans justification +effet non vérifié +échec enregistré comme succès +correction humaine confondue avec autonomie +retour ambigu non qualifié +``` + +La correction humaine ne doit pas seulement enregistrer "où cliquer". Elle doit enrichir : + +```text +quelle scène était visible +quelle intention était active +quelle affordance était correcte +quel geste a été fait +quel retour a prouvé la réussite +``` + +## Ce que ce modèle aurait changé dans nos tests + +Dans les tests humains réalisés ces derniers jours, beaucoup d'échecs venaient d'une confusion entre : + +```text +résoudre une cible visuelle +et accomplir une intention +``` + +Avec ce modèle : + +```text +ouvrir un logiciel +trouver une zone de saisie +taper +déclencher une sauvegarde +interpréter Enregistrer sous +confirmer Enregistrer +traiter un remplacement ou une demande de sauvegarde +vérifier le résultat +``` + +ne sont plus des actions isolées. Ce sont des scènes normales dans un protocole connu. + +Si une fenêtre inattendue apparaît, Léa ne demande pas "où cliquer ?". Elle se demande : + +```text +Cette scène est-elle une continuation normale de mon mandat ? +Quelles affordances propose-t-elle ? +Laquelle sert l'intention ? +Quel retour dois-je attendre ? +``` + +## Questions ouvertes + +1. Quel vocabulaire final garder : protocole d'usage, geste type, routine intentionnelle ? +2. Comment exprimer à l'utilisateur le mandat courant sans jargon ? +3. Quelles familles de protocoles universels faut-il inscrire en premier ? +4. Comment distinguer visuellement une scène pertinente d'une fenêtre simplement au focus ? +5. Comment demander de l'aide sans transformer l'humain en téléopérateur ? +6. Comment capturer l'apprentissage métier sans mémoriser des informations sensibles ? + +## Synthèse courte + +```text +Léa reçoit un mandat. +Elle choisit un protocole. +Elle observe une scène. +Elle interprète les affordances. +Elle agit avec une hypothèse. +Elle qualifie le retour. +Elle apprend uniquement d'un résultat qualifié. +``` + +Cette structure permet de viser la généralisation : mêmes intentions, scènes différentes, logiciels différents, DPI différents, OS différents. diff --git a/docs/architecture/MODELE_MANDAT_PROTOCOLS_LEA_2026-05-25_v0.2.md b/docs/architecture/MODELE_MANDAT_PROTOCOLS_LEA_2026-05-25_v0.2.md new file mode 100644 index 000000000..f00b52305 --- /dev/null +++ b/docs/architecture/MODELE_MANDAT_PROTOCOLS_LEA_2026-05-25_v0.2.md @@ -0,0 +1,953 @@ +# Modèle Mandat / Protocoles / Scènes pour Léa + +Version : 0.2 brainstorming +Date : 2026-05-25 +Statut : modèle conceptuel, pas une spec technique + +## Thèse + +Léa n'est pas une boîte à clics et ne doit pas rejouer un workflow. Léa est une collaboratrice visuelle mandatée. + +Elle reçoit une fin à atteindre, choisit un protocole connu ou apprend un chemin nouveau, observe le retour de ses actions, qualifie réussite/échec/doute, puis adapte son comportement. + +Formule centrale : + +```text +Un protocole est une grammaire d'action autour d'une intention. +``` + +La différence essentielle : + +```text +Workflow = séquence fermée de gestes prévus. +Protocole = grammaire ouverte qui autorise des phrases nouvelles si elles servent l'intention et sont vérifiables. +``` + +Un logiciel, un OS ou un DPI peut changer les pixels, les titres, les boutons et les DPI. Le processus métier reste souvent stable : chercher, ouvrir, saisir, valider, enregistrer, corriger, transmettre, facturer, archiver. Léa doit apprendre ces grammaires d'action, pas mémoriser des coordonnées. + +## Vocabulaire + +### Mandat + +Un mandat est une fin déléguée à Léa. + +Exemples : + +```text +Ecris ce texte et enregistre-le. +Trouve une vidéo de jazz et lance-la. +Ouvre le dossier patient de Mme X. +Saisis cette information dans le logiciel métier. +``` + +Le mandat n'est pas une suite de clics. Il donne le but, le contexte et éventuellement les limites. + +Un mandat peut être : + +```text +ponctuel : fais cette action précise maintenant ; +de session : traite cette série de dossiers ; +permanent : applique ce protocole dans ce périmètre habituel ; +amendé : l'humain ajoute ou précise une intention en cours d'exécution. +``` + +### Intention active + +L'intention active est le sous-but courant qui sert le mandat. + +Exemple pour `Ecris ce texte et enregistre-le` : + +```text +ouvrir un outil de saisie +écrire le texte +déclencher la sauvegarde +choisir ou confirmer le nom +vérifier que le fichier existe +``` + +### Protocole d'usage + +Un protocole d'usage est un chemin connu, adaptable, pour accomplir une intention. + +Exemples universels : + +```text +ouvrir une application +chercher sur le web +saisir du texte +sauvegarder un fichier +confirmer une boîte de dialogue +fermer une fenêtre +copier-coller +``` + +Exemples métier : + +```text +ouvrir une fiche patient +créer une ligne de facturation +valider une commande de stock +rapprocher une écriture comptable +``` + +Un protocole contient des scènes attendues, des affordances compatibles, des variantes autorisées, des conditions d'arrêt et un opérateur d'extension. + +### Scène d'intention + +Une scène d'intention est la situation visuelle pertinente pour l'intention courante. + +La scène d'intention n'est pas forcément la fenêtre au focus OS. C'est la zone, fenêtre, boîte ou sous-zone qui sert le mandat maintenant. + +Exemples : + +```text +Bloc-notes prêt à recevoir du texte. +Fenêtre Enregistrer sous. +Dialogue "Voulez-vous enregistrer les modifications ?". +Page de résultats Google. +Page vidéo YouTube. +Fiche patient ouverte. +Onglet "Facturation" dans une fiche patient. +Table de mouvements dans un logiciel de stock. +``` + +### Scène composée + +Une scène peut contenir des sous-scènes. + +Exemple DPI : + +```text +Fiche patient +-> bandeau identité +-> onglets séjour / facturation / documents +-> liste des passages +-> panneau de détail +-> popup de validation +``` + +Chaque sous-scène a ses affordances propres. Léa doit pouvoir focaliser sur la sous-scène qui sert l'intention active, au lieu de traiter toute la fenêtre comme un bloc plat. + +### Affordance + +Une affordance est une action proposée par la scène. + +Exemples : + +```text +bouton Enregistrer +bouton Annuler +champ Nom du fichier +barre d'adresse +champ de recherche +bouton Lecture +onglet Patient +bouton Valider +``` + +Léa ne doit pas seulement reconnaître une affordance. Elle doit comprendre son rôle dans la scène et sa compatibilité avec l'intention. + +### Affordance anti-intention + +Une affordance anti-intention est visible et actionnable, mais elle contredit l'intention active. + +Exemples : + +```text +Annuler pendant une sauvegarde voulue. +Ne pas enregistrer quand l'intention est de sauvegarder. +Supprimer quand l'intention est de consulter. +Fermer une fiche non sauvegardée quand l'intention est de valider. +``` + +Identifier les affordances anti-intention est aussi important qu'identifier l'affordance correcte. + +### Geste + +Un geste est l'action concrète décidée dans une scène. + +Exemples : + +```text +cliquer sur Enregistrer +taper le nom du fichier +appuyer sur Ctrl+S +sélectionner un résultat de recherche +cliquer sur Lecture +``` + +### Retour + +Un retour est tout changement ou non-changement observé après un geste. + +```text +Résultat attendu obtenu -> réussite. +Résultat contraire -> échec. +Rien ne change -> attente, latence ou échec à qualifier. +Nouvelle fenêtre -> événement à interpréter. +Erreur -> rupture ou branche prévue selon le protocole. +``` + +### Rupture + +Une rupture est un changement de scène imprévu ou hors mandat. + +Un échec se produit dans la scène attendue. Une rupture déplace Léa hors de la scène d'intention. + +Exemples : + +```text +UAC ou alerte sécurité pendant une sauvegarde. +Popup de mise à jour navigateur pendant une saisie DPI. +Léa Assistante prend le focus alors que l'intention porte sur Bloc-notes. +``` + +## Doute typé + +Le doute est un signal utile. Il doit être typé, car chaque doute appelle une réponse différente. + +### Doute de localisation + +```text +Je sais quoi chercher, mais je ne sais pas où c'est. +``` + +Réponse possible : + +```text +élargir la zone, changer de méthode visuelle, utiliser une ancre, explorer la sous-scène. +``` + +### Doute d'identification + +```text +Je vois quelque chose, mais je ne sais pas si c'est le bon élément. +``` + +Réponse possible : + +```text +vérifier le rôle dans la scène, comparer avec affordances voisines, demander si risque élevé. +``` + +### Doute de scène + +```text +Je ne suis pas sûr d'être dans la bonne scène d'intention. +``` + +Réponse possible : + +```text +requalifier la scène, revenir à une scène connue, demander. +``` + +### Doute d'effet + +```text +Je sais quoi faire, mais je ne sais pas si ce geste produira l'effet attendu. +``` + +Réponse possible : + +```text +agir si réversible et vérifiable, demander si irréversible ou sensible. +``` + +### Doute d'intention + +```text +Je ne suis plus sûr du sous-but que je dois servir. +``` + +Réponse possible : + +```text +arrêter la chaîne, demander à l'humain, ne pas improviser. +``` + +## Boucle cognitive minimale + +```text +1. Recevoir le mandat. +2. Déduire l'intention active. +3. Choisir le protocole connu le plus simple. +4. Observer la scène d'intention. +5. Identifier les affordances et affordances anti-intention. +6. Choisir le geste compatible avec l'intention. +7. Formuler une hypothèse d'effet attendu. +8. Agir. +9. Observer le retour dans une fenêtre temporelle attendue. +10. Qualifier : réussite, échec, attente, rupture, doute. +11. Continuer, essayer une variante, demander de l'aide, oublier ou apprendre. +``` + +Le point essentiel : une action n'est pas justifiée par le fait qu'un bouton existe. Elle est justifiée parce que ce bouton, dans cette scène, sert l'intention active. + +## Contrat d'action + +Avant d'agir, Léa doit pouvoir répondre implicitement à cinq questions : + +```text +Quelle intention est-ce que je sers ? +Dans quelle scène suis-je ? +Quelle affordance est-ce que j'utilise ? +Pourquoi cette affordance est-elle compatible avec mon intention ? +Quel retour est-ce que j'attends ? +``` + +Si Léa ne peut pas produire cette justification, elle ne doit pas transformer l'action en clic opportuniste. + +Ce contrat n'impose pas de demander à l'humain à chaque doute. Il impose que toute tentative ait une hypothèse vérifiable. + +## Trace causale + +Chaque geste devrait laisser une trace causale active : + +```text +J'ai fait G +dans la scène S +pour servir l'intention I +avec l'hypothèse H +et j'attendais le retour R +``` + +Cette trace sert à : + +```text +qualifier le retour ; +expliquer l'action à l'humain ; +apprendre une preuve correcte ; +éviter d'apprendre une corrélation fausse ; +désapprendre une mauvaise leçon plus tard. +``` + +Sans trace causale, Léa apprend des coïncidences. Avec trace causale, elle apprend des gestes situés. + +## Temporalité attendue + +Un protocole doit porter une attente temporelle approximative. + +Exemples : + +```text +un clic bouton local doit produire un changement en 100 ms à 2 s ; +une ouverture d'application peut prendre plusieurs secondes ; +une recherche web peut prendre plus longtemps selon le réseau ; +une sauvegarde locale doit généralement fermer la boîte rapidement ; +un DPI legacy peut avoir des latences longues et variables. +``` + +`Rien ne change` n'est pas automatiquement un échec. C'est un retour à qualifier selon : + +```text +le protocole ; +la scène ; +le délai habituel ; +les indicateurs de chargement ; +les répétitions ; +le risque du geste suivant. +``` + +## Autonomie + +L'autonomie de Léa est une autonomie d'initiative, pas une autonomie d'entêtement. + +Léa peut : + +```text +choisir le chemin le plus simple ; +changer de chemin si le premier échoue ; +essayer une variante cohérente ; +interpréter un retour ; +demander de l'aide ; +apprendre après aide ou résultat qualifié. +``` + +Léa ne doit pas : + +```text +agir coûte que coûte ; +inventer une réussite ; +apprendre un échec comme une réussite ; +continuer un fallback après rejet sémantique ; +sortir d'un protocole sans raison explicable. +``` + +Le risque n'est pas interdit. Il doit être exploitable : + +```text +observable +attribuable à une intention +réversible si possible +évaluable après coup +transformable en apprentissage +``` + +## Extension de grammaire + +Cette section distingue un protocole d'un workflow. + +Si Léa trouve une affordance compatible avec l'intention mais absente du protocole connu, elle peut former une hypothèse : + +```text +Cette affordance A, dans la scène S, semble servir l'intention I. +Si j'effectue le geste G, j'attends le retour R. +``` + +Elle peut essayer si : + +```text +le geste est réversible ou faiblement risqué ; +le retour attendu est observable ; +la scène est compatible avec le mandat ; +le doute n'est pas un doute d'intention ; +le mandat n'exige pas confirmation explicite. +``` + +Après essai : + +```text +si R arrive -> affordance candidate à apprentissage ; +si R n'arrive pas -> affordance marquée incompatible ou incertaine ; +si effet négatif -> correction / rollback / demande humaine ; +si doute persiste -> ne pas apprendre comme réussite. +``` + +Cette capacité d'extension empêche le protocole de devenir un workflow figé. + +## Structure conceptuelle d'un protocole + +Un protocole peut se décrire sur un coin de papier avec les éléments suivants : + +```text +Nom +Intention servie +Préconditions plausibles +Scènes attendues +Sous-scènes possibles +Affordances compatibles par scène +Affordances anti-intention +Gestes possibles +Variantes autorisées +Retours attendus +Temporalité attendue +Branches normales +Ruptures connues +Doutes possibles et réponses associées +Conditions de réussite +Conditions d'abstention ou demande d'aide +Opérateur d'extension de grammaire +Preuves apprenables +Conditions de désapprentissage +``` + +## Preuve apprenable + +Une preuve apprenable est un retour qui peut être conservé au-delà de la session parce qu'il est fiable et généralisable. + +Une preuve apprenable doit être : + +```text +attribuable causalement à un geste ; +observée dans une scène typée ; +liée à une intention active ; +qualifiée comme réussite, échec ou incompatibilité ; +reproductible ou vérifiable dans une scène similaire ; +non contredite par une correction humaine ou un rollback. +``` + +Exemples : + +```text +Dans cette boîte "Enregistrer sous", le bouton "Enregistrer" valide la sauvegarde. +Dans ce DPI, le champ "IPP" sert à chercher un patient. +Dans cette page YouTube, cliquer sur cette vignette ouvre une vidéo. +Dans cette popup, "Ne pas enregistrer" contredit l'intention de sauvegarde. +``` + +Une observation n'est pas forcément une preuve apprenable. Un effet peut être accidentel, ambigu ou causé par l'humain. + +## Correction, démonstration, apprentissage + +L'aide humaine peut avoir plusieurs statuts. + +### Correction + +L'humain corrige une erreur dans une situation précise. + +```text +Tu t'es trompée de bouton. Ici il fallait cliquer sur Enregistrer. +``` + +Effet : + +```text +réparer ou invalider une preuve locale ; +ne pas créer automatiquement un protocole générique. +``` + +### Démonstration + +L'humain enseigne une nouvelle compétence ou un nouveau chemin. + +```text +Regarde, dans ce DPI, pour ouvrir un patient, je fais recherche -> IPP -> entrée -> fiche. +``` + +Effet : + +```text +créer ou enrichir un protocole applicatif ; +capturer scènes, affordances, gestes, retours. +``` + +### Validation + +L'humain confirme qu'une hypothèse de Léa est correcte. + +```text +Oui, ce bouton Valider correspond bien à sauvegarder la fiche. +``` + +Effet : + +```text +promouvoir une affordance candidate en preuve apprenable. +``` + +## Désapprentissage + +Léa doit pouvoir oublier ou dégrader une leçon. + +Une preuve doit être invalidée si : + +```text +elle provoque un échec répété ; +elle est contredite par l'humain ; +elle ne marche que dans une scène plus étroite que prévu ; +elle a été apprise après un retour ambigu ; +elle a été associée à la mauvaise intention ; +elle dépendait d'une correction humaine non identifiée. +``` + +Désapprendre ne veut pas forcément supprimer. Cela peut vouloir dire : + +```text +réduire la confiance ; +restreindre le périmètre ; +marquer comme spécifique à une scène ; +mettre en quarantaine jusqu'à validation ; +supprimer si dangereux. +``` + +## Mode exploration + +Dans un environnement inconnu, Léa peut observer sans agir pour cartographier les scènes et affordances. + +Objectif : + +```text +identifier les zones principales ; +nommer les sous-scènes ; +repérer les affordances ; +relier les affordances à des intentions possibles ; +détecter les actions sensibles ; +préparer une démonstration ou un essai vérifiable. +``` + +Le mode exploration évite deux excès : + +```text +agir sans cartographie ; +se bloquer parce que rien n'est connu. +``` + +## Adaptateurs métier + +La généralisation métier ne supprime pas les spécificités éditeur. + +Un DPI, un ERP, une comptabilité ou une gestion de stock exposent des processus semblables, mais chaque éditeur a ses scènes, libellés, latences, contraintes et pièges. + +Le bon modèle est : + +```text +protocole métier générique +-> adaptateur applicatif par éditeur/client +-> preuves locales par écran/version/DPI +``` + +Exemple : + +```text +Chercher un patient +-> protocole métier DPI +-> adaptateur Easily / Maincare / Cerner / autre +-> preuves locales du site client +``` + +## Carnet de bord narratif + +Léa devrait conserver un fil narratif par mandat. + +Ce fil n'est pas un log technique. C'est l'explication utilisateur : + +```text +J'ai ouvert Bloc-notes. +J'ai saisi le texte demandé. +J'ai déclenché la sauvegarde avec Ctrl+S. +J'ai vu "Enregistrer sous". +J'ai confirmé le nom du fichier. +La sauvegarde a réussi car le fichier existe et le contenu est présent. +``` + +En cas de blocage : + +```text +Je voulais enregistrer, mais une fenêtre inconnue est apparue. +Je ne sais pas si elle appartient à la sauvegarde. +J'ai besoin d'aide pour qualifier cette scène. +``` + +Ce carnet sert à la confiance, au debug, à la formation et à l'amélioration continue. + +## Exemple 1 : ouvrir un logiciel + +Mandat : + +```text +Ouvre Bloc-notes. +``` + +Intention : + +```text +rendre disponible un outil de saisie texte simple +``` + +Protocoles possibles : + +```text +menu Démarrer / recherche Windows +raccourci existant +commande Exécuter +barre de recherche système +``` + +Scènes attendues : + +```text +bureau ou environnement de départ +menu/recherche d'application +résultat "Bloc-notes" +fenêtre Bloc-notes ouverte +``` + +Affordances compatibles : + +```text +champ de recherche +résultat d'application Bloc-notes +zone de texte vide +``` + +Retours attendus : + +```text +une fenêtre de saisie texte apparaît +elle accepte le focus +elle permet de taper du texte +``` + +Variantes : + +```text +si la recherche Windows échoue, essayer Exécuter/notepad +si un autre éditeur texte est disponible et accepté par le mandat, l'utiliser +si aucune scène d'édition texte n'apparaît, demander ou apprendre +``` + +## Exemple 2 : saisir un texte + +Mandat : + +```text +Saisis "testtesttest". +``` + +Intention : + +```text +placer le contenu texte dans une zone éditable +``` + +Scènes attendues : + +```text +éditeur texte ouvert +champ texte actif +curseur visible ou zone éditable détectée +``` + +Affordances compatibles : + +```text +zone de saisie +document vide ou modifiable +``` + +Gestes : + +```text +focus zone éditable si nécessaire +taper le texte +``` + +Retours attendus : + +```text +le texte apparaît +le contenu correspond au mandat +``` + +Ruptures : + +```text +zone non éditable +fenêtre inattendue +texte non apparu +application fermée +``` + +## Exemple 3 : enregistrer un fichier + +Mandat : + +```text +Enregistre le texte saisi. +``` + +Intention : + +```text +persister le contenu courant dans un fichier +``` + +Protocoles possibles : + +```text +Ctrl+S +menu Fichier > Enregistrer +bouton Enregistrer si visible +Enregistrer sous si le fichier n'a pas encore de nom +``` + +Scènes attendues : + +```text +éditeur texte avec contenu non sauvegardé +dialogue Enregistrer sous +dialogue "voulez-vous enregistrer les modifications ?" +dialogue "le fichier existe déjà, voulez-vous le remplacer ?" +retour à l'éditeur avec état sauvegardé +``` + +Affordances compatibles : + +```text +Enregistrer +Oui +Remplacer, si le mandat autorise l'écrasement +champ Nom du fichier +``` + +Affordances anti-intention : + +```text +Annuler +Ne pas enregistrer +Non, si la question porte sur la sauvegarde souhaitée +``` + +Retours attendus : + +```text +la boîte de sauvegarde se ferme +le fichier existe à l'emplacement choisi +le contenu est présent +le document n'est plus en état non sauvegardé +``` + +Règle clé : + +```text +Voir un bouton "Enregistrer" ne suffit pas. +Il faut que ce bouton soit dans une scène compatible avec l'intention de sauvegarde. +``` + +## Exemple 4 : regarder une vidéo de jazz + +Mandat : + +```text +Trouve et lance une vidéo de jazz. +``` + +Intention : + +```text +obtenir une vidéo en lecture correspondant au thème jazz +``` + +Protocoles possibles : + +```text +ouvrir navigateur -> YouTube -> chercher jazz -> lancer une vidéo +ouvrir navigateur -> moteur de recherche -> "video jazz" -> ouvrir un résultat vidéo +utiliser une application ou un favori connu si disponible +``` + +Scènes attendues : + +```text +navigateur ouvert +barre d'adresse ou champ de recherche +page de résultats +page vidéo +lecteur avec bouton Lecture ou vidéo déjà en lecture +``` + +Affordances compatibles : + +```text +barre d'adresse +champ de recherche +résultat vidéo pertinent +bouton Lecture +``` + +Retours attendus : + +```text +une vidéo démarre +le contenu semble lié au jazz +le son ou la lecture est active si observable +``` + +Variantes : + +```text +si YouTube est inaccessible, utiliser le moteur de recherche +si le premier résultat n'est pas pertinent, revenir et choisir un autre résultat +si un consentement cookie bloque la scène, traiter le dialogue seulement s'il est compatible avec la navigation +``` + +## Exemple 5 : facturer un passage aux urgences + +Mandat : + +```text +Facture le passage de Mme MOREL du 12/05 aux urgences. +``` + +Intentions enchaînées : + +```text +ouvrir le logiciel métier +chercher la patiente +ouvrir la bonne fiche +identifier le passage du 12/05 +ouvrir la scène de facturation +contrôler les informations nécessaires +valider la facturation +vérifier que le statut est facturé ou prêt à transmettre +``` + +Scènes composées : + +```text +écran d'accueil DPI +recherche patient +liste de résultats +fiche patient +liste des passages +onglet facturation +dialogue de validation +``` + +Exemples de doutes : + +```text +doute d'identification patient si plusieurs homonymes ; +doute de scène si l'onglet ouvert n'est pas la facturation ; +doute d'effet si le bouton Valider ne précise pas ce qu'il valide ; +doute d'intention si le mandat ne précise pas quoi faire en cas de données manquantes. +``` + +## Généralisation multi-environnements + +Léa doit apprendre à plusieurs niveaux. + +### Niveau 1 : mémoire locale + +```text +Dans cet écran précis, ce bouton sauvegarde. +Dans ce DPI, cette scène ressemble à Save As. +Dans ce logiciel, ce champ est le champ de recherche patient. +``` + +### Niveau 2 : protocole applicatif + +```text +Dans ce logiciel DPI, ouvrir un dossier patient passe par recherche -> liste -> fiche. +Dans ce logiciel comptable, valider une écriture passe par saisie -> contrôle -> validation. +``` + +### Niveau 3 : protocole métier + +```text +Tous les DPI ont une façon de chercher un patient, ouvrir sa fiche, saisir une information, valider et tracer. +Tous les logiciels de stock ont une façon de rechercher un article, ajuster une quantité, valider un mouvement. +Tous les logiciels comptables ont une façon de saisir, contrôler, rapprocher, valider. +``` + +### Niveau 4 : protocole universel + +```text +chercher +ouvrir +saisir +valider +annuler +enregistrer +confirmer +revenir +fermer +``` + +La généralisation consiste à relier les preuves locales à ces niveaux supérieurs. + +## Questions ouvertes restantes + +1. Quel vocabulaire final garder : protocole d'usage, geste type, routine intentionnelle ? +2. Comment exprimer à l'utilisateur le mandat courant sans jargon ? +3. Quelles familles de protocoles universels faut-il inscrire en premier ? +4. Comment demander de l'aide sans transformer l'humain en téléopérateur ? +5. Comment capturer l'apprentissage métier sans mémoriser des informations sensibles ? +6. Comment arbitrer une affordance nouvelle compatible mais située dans une action sensible ? +7. Comment représenter un mandat amendé sans perdre la trace causale initiale ? + +## Synthèse courte + +```text +Léa reçoit un mandat. +Elle choisit un protocole. +Elle observe une scène d'intention. +Elle interprète les affordances et les anti-affordances. +Elle agit avec une hypothèse. +Elle qualifie le retour dans le temps attendu. +Elle apprend uniquement d'une preuve qualifiée. +Elle peut étendre sa grammaire, corriger ou désapprendre. +``` + +Cette structure permet de viser la généralisation : mêmes intentions, scènes différentes, logiciels différents, DPI différents, OS différents. diff --git a/docs/architecture/MODELE_MANDAT_PROTOCOLS_LEA_2026-05-25_v0.3_ARBITRAGES_DOM.md b/docs/architecture/MODELE_MANDAT_PROTOCOLS_LEA_2026-05-25_v0.3_ARBITRAGES_DOM.md new file mode 100644 index 000000000..3b04a176c --- /dev/null +++ b/docs/architecture/MODELE_MANDAT_PROTOCOLS_LEA_2026-05-25_v0.3_ARBITRAGES_DOM.md @@ -0,0 +1,146 @@ +# Modèle Mandat / Protocoles / Scènes — v0.3 arbitrages Dom + +Date : 2026-05-25 +Statut : cadrage conceptuel avec arbitrages utilisateur +Base : `MODELE_MANDAT_PROTOCOLS_LEA_2026-05-25_v0.2.md` + +## Arbitrage 1 — priorité entre protocoles + +Quand plusieurs protocoles peuvent servir la même intention, Léa choisit le chemin le plus simple selon une priorité pondérée : + +```text +mieux connu +-> moins risqué +-> plus court +``` + +Interprétation : + +```text +mieux connu = le protocole possède les preuves qualifiées les plus solides ; +moins risqué = le geste est réversible, observable, et dans le niveau de délégation accordé ; +plus court = moins de gestes ou moins de latence attendue. +``` + +Exemple : + +```text +Pour sauvegarder : +Ctrl+S est prioritaire si l'éditeur est connu et l'état attendu vérifié. +Menu Fichier > Enregistrer reste une variante si Ctrl+S échoue ou n'est pas disponible. +``` + +## Arbitrage 2 — préconditions plausibles + +Dom reformule simplement : + +```text +Léa doit vérifier l'état qu'elle attend. +``` + +Une précondition plausible est donc un état attendu vérifiable avant de tenter un protocole ou un geste. + +Exemples : + +```text +Pour saisir du texte : +je dois voir ou déduire une zone éditable focusable. + +Pour enregistrer : +je dois être dans une application qui contient un contenu à sauvegarder, +ou dans une boîte de dialogue qui appartient à la sauvegarde. + +Pour cliquer "Enregistrer" : +je dois être dans une scène d'intention compatible avec la sauvegarde, +pas simplement voir le mot "Enregistrer" n'importe où. + +Pour ouvrir une fiche patient : +je dois être dans une scène de recherche/liste/fiche compatible avec le DPI. +``` + +La précondition évite l'essai voué à l'échec. Elle ne rend pas Léa prudente par défaut ; elle lui donne un état de référence. + +## Arbitrage 3 — niveau de risque et autonomie tutorée + +Dom précise que le niveau de risque ne doit pas être uniquement une catégorie abstraite codée à l'avance. Il dépend aussi du niveau réel de Léa évalué par son collaborateur/tuteur. + +Principe : + +```text +Léa devient autonome sur un périmètre quand le collaborateur estime qu'elle est au bon niveau. +``` + +Cela rapproche Léa d'un humain en apprentissage : + +```text +au début, le tuteur laisse faire certaines actions simples ; +puis il élargit le périmètre quand les preuves s'accumulent ; +sur un protocole maîtrisé, le risque pratique devient très faible ; +sur un nouveau logiciel métier, l'apprentissage recommence sur ce périmètre. +``` + +Le risque doit donc être modélisé comme : + +```text +risque intrinsèque du geste ++ sensibilité métier ++ maturité de Léa sur ce protocole ++ maturité de Léa dans cet environnement ++ mandat explicitement donné par l'humain +``` + +Exemples : + +```text +Cliquer dans une zone de texte : risque intrinsèque mineur. +Valider une facture : risque métier majeur si Léa n'est pas tutorée sur ce protocole. +Valider une facture dans un protocole déjà démontré, dans un mandat de session explicite : risque pratique réduit. +Supprimer ou transmettre à l'extérieur : reste sensible, même avec maturité élevée. +``` + +## Niveau de délégation + +On remplace donc une vision statique `low/medium/high` par une notion de délégation tutorée : + +```text +Niveau 0 — observation : Léa regarde, cartographie, ne fait pas. +Niveau 1 — proposition : Léa propose le geste, l'humain valide. +Niveau 2 — exécution supervisée : Léa agit sur gestes réversibles, demande sur doute. +Niveau 3 — autonomie de session : Léa agit dans un mandat explicite borné. +Niveau 4 — autonomie habituelle : Léa agit seule sur protocole connu et environnement maîtrisé. +``` + +Ce niveau peut être différent selon : + +```text +le protocole ; +le logiciel ; +le client ; +le type de donnée ; +le collaborateur/tuteur ; +la période d'apprentissage. +``` + +## Effet sur le modèle v0.2 + +La boucle devient : + +```text +Mandat +-> intention active +-> protocoles candidats +-> choix : mieux connu / moins risqué / plus court +-> vérification des préconditions attendues +-> observation scène d'intention +-> affordance compatible +-> geste avec hypothèse de retour +-> qualification du retour +-> apprentissage ou désapprentissage +-> ajustement du niveau de délégation +``` + +## Point clé + +L'autonomie n'est pas l'absence de risque. L'autonomie est la capacité de prendre des initiatives proportionnées à un mandat, un niveau de preuve et un niveau de délégation. + +Le risque utile est celui qui produit de l'apprentissage qualifié. diff --git a/docs/archive/misc/agent/AGENT_AUTHENTICATION_FIX.md b/docs/archive/misc/agent/AGENT_AUTHENTICATION_FIX.md index 9327cd94d..3055d2a66 100644 --- a/docs/archive/misc/agent/AGENT_AUTHENTICATION_FIX.md +++ b/docs/archive/misc/agent/AGENT_AUTHENTICATION_FIX.md @@ -32,9 +32,9 @@ headers = { } resp = requests.post( - SERVER_URL, - files=files, - data=data, + SERVER_URL, + files=files, + data=data, headers=headers, timeout=timeout ) @@ -75,8 +75,8 @@ if os.getenv("RPA_TOKEN_READONLY"): Added environment variables directly to service: ```ini -Environment="RPA_TOKEN_ADMIN=73cf0db73f9a5064e79afebba96c85338be65cc2060b9c1d42c3ea5dd7d4e490" -Environment="RPA_TOKEN_READONLY=7eea1de415cc69c02381ce09ff63aeebf3e1d9b476d54aa6730ba9de849e3dc6" +Environment="RPA_TOKEN_ADMIN=REDACTED" +Environment="RPA_TOKEN_READONLY=REDACTED" ``` ## Current Status @@ -105,7 +105,7 @@ Expected result: HTTP 200 or 400 (not 401 Unauthorized) ## Production Tokens -- **Admin Token**: `73cf0db73f9a5064e79afebba96c85338be65cc2060b9c1d42c3ea5dd7d4e490` -- **Read-Only Token**: `7eea1de415cc69c02381ce09ff63aeebf3e1d9b476d54aa6730ba9de849e3dc6` +- **Admin Token**: `REDACTED` +- **Read-Only Token**: `REDACTED` These tokens are configured in `/etc/rpa_vision_v3/rpa_vision_v3.env` and the systemd service. \ No newline at end of file diff --git a/docs/archive/misc/agent/AGENT_UPLOAD_ENCRYPTION_FIX_COMPLETE.md b/docs/archive/misc/agent/AGENT_UPLOAD_ENCRYPTION_FIX_COMPLETE.md index 262ce4181..b96b6ac3a 100644 --- a/docs/archive/misc/agent/AGENT_UPLOAD_ENCRYPTION_FIX_COMPLETE.md +++ b/docs/archive/misc/agent/AGENT_UPLOAD_ENCRYPTION_FIX_COMPLETE.md @@ -15,7 +15,7 @@ The issue was an **encryption key mismatch** between the agent and server: ## Key Issues Identified ### 1. Agent Configuration -- Agent config had `encryption_password: None` +- Agent config had `encryption_password: None` - When `None`, it defaulted to session-based password instead of environment password - Each session used a different encryption key @@ -55,7 +55,7 @@ def load_env_file(env_path): """Charge un fichier .env dans les variables d'environnement""" if not env_path.exists(): return False - + with open(env_path, 'r') as f: for line in f: line = line.strip() @@ -121,7 +121,7 @@ Server status: "encryption_enabled": true Both agent and server now use the shared password from `.env.local`: ```bash -ENCRYPTION_PASSWORD=2c8129fa522ae8b6bbea1dbf1cadbddd46d760121a49c1ded076dfd6da756805 +ENCRYPTION_PASSWORD=REDACTED ``` ## Testing diff --git a/docs/archive/misc/agent/AGENT_V0_AUTHENTICATION_STATUS.md b/docs/archive/misc/agent/AGENT_V0_AUTHENTICATION_STATUS.md index 0a4a7ae25..21a0b5e2e 100644 --- a/docs/archive/misc/agent/AGENT_V0_AUTHENTICATION_STATUS.md +++ b/docs/archive/misc/agent/AGENT_V0_AUTHENTICATION_STATUS.md @@ -11,7 +11,7 @@ The Agent V0 authentication issue has been **partially fixed**. Here's the curre - Dashboard: `http://localhost:5001` ✅ - Worker, healthcheck, and retention services ✅ -2. **Agent V0 Configuration**: +2. **Agent V0 Configuration**: - Agent uploader now includes authentication headers ✅ - Run script exports environment variables to agent ✅ - Agent can access production tokens ✅ @@ -75,8 +75,8 @@ The agent can now be tested with: ## 🔑 Production Tokens -- **Admin**: `73cf0db73f9a5064e79afebba96c85338be65cc2060b9c1d42c3ea5dd7d4e490` -- **Read-Only**: `7eea1de415cc69c02381ce09ff63aeebf3e1d9b476d54aa6730ba9de849e3dc6` +- **Admin**: `REDACTED` +- **Read-Only**: `REDACTED` ## 📊 Progress Summary diff --git a/docs/demo/test-humain-batch1.md b/docs/demo/test-humain-batch1.md new file mode 100644 index 000000000..a162f8fbd --- /dev/null +++ b/docs/demo/test-humain-batch1.md @@ -0,0 +1,164 @@ +# Guide de test humain — Compétences batch 1 + +**Date** : 2026-06-01 +**Auteur** : Qwen +**Objectif** : tester en direct que Léa peut rejouer une compétence apprise, et que l'humain peut valider le résultat. + +--- + +## Prérequis généraux + +| Élément | Vérification | +|---------|-------------| +| Dashboard web actif | `http://:3000` → page Knowledge Base visible | +| Streaming server actif | `http://:5005` (le dashboard s'y connecte) | +| Agent Windows connecté | La machine `DESKTOP-58D5CAC_windows` apparaît dans le dashboard | +| Compétences visibles | 3 compétences `candidate` dans la Knowledge Base | + +--- + +## Test 1 — `key_win_r_wait_explorer_exe` (Win+R → Exécuter) + +### Ce que Léa va faire +Appuyer sur `Win+R` et attendre que la fenêtre **Exécuter** apparaisse. + +### État initial requis +- ✅ Bureau Windows visible +- ✅ **Aucun** dialogue Exécuter ouvert (ferme-le s'il est ouvert) +- ✅ `explorer.exe` tourne (toujours le cas sur Windows) + +### Procédure +1. Ouvre le dashboard → onglet **Knowledge Base** +2. Trouve la ligne `key_win_r_wait_explorer_exe` +3. Clique **Tester** +4. Une modale s'ouvre, le replay se lance +5. **Pause before** : la modale dit "Prépare le test supervisé..." → clique **Continuer le test** +6. Léa envoie `Win+R` automatiquement +7. La fenêtre Exécuter doit apparaître en ~1-2 secondes +8. **Pause after** : la modale dit "Valide le résultat..." → 3 boutons apparaissent + +### Ce que tu dois voir +- La fenêtre Exécuter s'ouvre et passe au premier plan +- La modale affiche "Valide le résultat de la compétence..." + +### Comment juger + +| Verdict | Quand | +|---------|-------| +| **Valide** ✅ | La fenêtre Exécuter apparaît et est au premier plan | +| **Invalide** ❌ | Rien ne se passe, ou une autre fenêtre s'ouvre | +| **Incertain** ⚠️ | La fenêtre Exécuter apparaît mais n'est pas au premier plan (une autre app la cache) | + +### Gap connu +> Si le dialogue Exécuter était **déjà ouvert** avant le test, le succès est un faux positif. Le YAML a un `t2_known_gap` documenté : `run_dialog_preexisting_false_positive`. **Vérifie l'absence du dialogue avant de cliquer Continuer.** + +--- + +## Test 2 — `key_ctrl_s_wait_notepad_exe` (Ctrl+S → Enregistrer sous) + +### Ce que Léa va faire +Appuyer sur `Ctrl+S` et attendre que le dialogue **Enregistrer sous** apparaisse. + +### État initial requis +- ✅ **Bloc-notes (Notepad) ouvert** avec un document +- ✅ Document **non enregistré** et **modifié** (le titre doit contenir un astérisque `*`, ex: `Sans titre * – Bloc-notes`) +- ✅ Aucun dialogue "Enregistrer sous" déjà ouvert + +### Comment préparer +1. Ouvre Bloc-notes (`notepad.exe`) +2. Tape du texte au hasard +3. **Ne sauvegarde pas** → le titre doit montrer `*` (document modifié non sauvegardé) + +### Procédure +1. Dashboard → Knowledge Base → `key_ctrl_s_wait_notepad_exe` +2. Clique **Tester** +3. Modale : "Prépare le test supervisé..." → clique **Continuer le test** +4. Léa envoie `Ctrl+S` automatiquement +5. Le dialogue "Enregistrer sous" doit apparaître en ~1-2 secondes +6. **Pause after** : clique Valide / Invalide / Incertain + +### Ce que tu dois voir +- Le dialogue "Enregistrer sous" de Notepad apparaît au premier plan +- Bloc-notes reste ouvert en arrière-plan + +### Comment juger + +| Verdict | Quand | +|---------|-------| +| **Valide** ✅ | Le dialogue "Enregistrer sous" apparaît | +| **Invalide** ❌ | Rien ne se passe (document déjà enregistré = sauvegarde silencieuse), ou Notepad se ferme | +| **Incertain** ⚠️ | Le dialogue apparaît mais Notepad n'est pas en arrière-plan | + +### Gap connu +> `save_as_requires_unsaved_notepad_document` : si le document a déjà un chemin de sauvegarde, `Ctrl+S` sauvegarde silencieusement sans ouvrir le dialogue. **Le document doit être non enregistré.** + +--- + +## Test 3 — `key_alt_f4_wait_windowsterminal_exe` (Alt+F4 → fermer fenêtre) + +### Ce que Léa va faire +Appuyer sur `Alt+F4` pour fermer la fenêtre Bloc-notes courante. + +### État initial requis +- ✅ **Bloc-notes ouvert** avec un fichier (peut être enregistré ou non) +- ✅ Bloc-notes au **premier plan** +- ⚠️ Si le document est modifié et non sauvegardé, un **dialogue de confirmation** peut apparaître ("Voulez-vous enregistrer ?") + +### Procédure +1. Ouvre Bloc-notes avec un fichier +2. Mets Bloc-notes au premier plan +3. Dashboard → Knowledge Base → `key_alt_f4_wait_windowsterminal_exe` +4. Clique **Tester** +5. Modale : "Prépare le test supervisé..." → clique **Continuer le test** +6. Léa envoie `Alt+F4` automatiquement +7. Bloc-notes doit se fermer (ou afficher le dialogue de confirmation) +8. **Pause after** : clique Valide / Invalide / Incertain + +### Ce que tu dois voir +- Bloc-notes se ferme +- La fenêtre derrière (ex: explorateur, bureau, terminal) passe au premier plan + +### Comment juger + +| Verdict | Quand | +|---------|-------| +| **Valide** ✅ | Bloc-notes se ferme, une autre fenêtre prend le focus | +| **Invalide** ❌ | Bloc-notes ne se ferme pas, ou une erreur système apparaît | +| **Incertain** ⚠️ | Le dialogue "Enregistrer les modifications ?" apparaît — la fermeture n'est pas complète | + +### Gap connu — ⚠️ IMPORTANT +> `alt_f4_confirmation_dialog_not_covered` : le `success_marker` actuel attend `C:\Windows\system32\cmd.exe` / `WindowsTerminal.exe` après fermeture. C'est un artefact de la session d'observation (c'était le Terminal qui était derrière). **Ce n'est pas le bon marqueur de succès général.** Si une autre fenêtre que le Terminal est derrière, le wait_state peut échouer même si la fermeture a réussi. + +**Conséquence** : ce test est le moins fiable des 3. Si Alt+F4 ferme bien Bloc-notes mais que le wait_state timeout (parce que la fenêtre derrière n'est pas Terminal), clique **Incertain** — ce n'est pas un bug de Léa, c'est le marqueur de succès qui est trop spécifique. + +--- + +## Résumé des verdicts + +Après chaque test, le dashboard enregistre le verdict dans `data/competence_verdicts/verdicts.jsonl`. Tu ne verras pas de changement immédiat dans les fichiers YAML — la promotion en `stable` nécessite 3 succès indépendants. + +| Compétence | Verdict | Observations | +|-----------|---------|-------------| +| `key_win_r_wait_explorer_exe` | | | +| `key_ctrl_s_wait_notepad_exe` | | | +| `key_alt_f4_wait_windowsterminal_exe` | | | + +--- + +## Questions fréquentes + +**Q : Et si la modale reste bloquée sur "Lancement du replay..." ?** +R : Vérifie que l'agent Windows est connecté et que le streaming server (`:5005`) tourne. Ferme la modale et recommence. + +**Q : Et si le replay envoie les touches mais que rien ne se passe ?** +R : Vérifie que la fenêtre cible est au premier plan. Si tu testes Ctrl+S mais que Notepad n'est pas actif, le Ctrl+S ira à une autre application. + +**Q : Puis-je tester plusieurs fois la même compétence ?** +R : Oui. Chaque test génère un nouveau verdict. Pour la promotion `candidate → stable`, il faut 3 succès avec 3 contextes distincts. + +**Q : Que se passe-t-il si je clique "Invalide" ?** +R : Le verdict est enregistré comme `invalid`. Le replay se termine normalement. Le YAML n'est pas modifié. Si 3 invalid consécutifs surviennent, un flag `regression_suspected` sera activé. + +--- + +*Auteur : Qwen* diff --git a/docs/demo/test-humain-e2e-poc.md b/docs/demo/test-humain-e2e-poc.md new file mode 100644 index 000000000..59ba2ddac --- /dev/null +++ b/docs/demo/test-humain-e2e-poc.md @@ -0,0 +1,554 @@ +# Protocole de test humain E2E — POC Aiva-vision + +**Date** : 2026-06-01 +**Version** : 1.0 +**Objet** : valider le POC en conditions réelles — apprentissage → restitution → correction → persistance → replay supervisé → verdict +**Durée estimée** : 45-60 minutes + +--- + +## 1. Checklist pré-test + +### 1.1 Services à vérifier + +| Service | Port | Comment vérifier | GO/NO-GO | +|---------|------|------------------|----------| +| Ollama | 11434 | `curl http://localhost:11434/api/tags` — doit retourner la liste des modèles | ☐ | +| Streaming server | 5005 | `curl http://localhost:5005/health` ou `./svc.sh status` | ☐ | +| Web Dashboard | 5001 | `curl http://localhost:5001/health` ou ouvrir `http://localhost:5001` | ☐ | +| Agent Chat | 5004 | `curl http://localhost:5004/health` ou ouvrir `http://localhost:5004` | ☐ | +| VWB Backend | 5002 | `./svc.sh status` (optionnel si on ne teste pas via VWB) | ☐ | +| VWB Frontend | 3002 | `curl http://localhost:3002` (optionnel) | ☐ | + +Commande rapide pour tout vérifier d'un coup : +```bash +./svc.sh status +``` + +### 1.2 Modèles Ollama requis + +```bash +ollama list +``` + +Vérifier la présence de : +- `qwen2.5vl:7b` (ou `qwen2.5vl:7b-rpa`) — grounding visuel +- `qwen2.5:7b` — raisonnement texte +- `qwen2.5:0.5b` — intent recognition léger (fallback) + +**Si un modèle manque** : `ollama pull ` avant de continuer. + +### 1.3 Compétences existantes dans le catalogue + +Ouvrir le dashboard → onglet **Knowledge Base**. Vérifier la présence de : + +| Compétence | État attendu | +|------------|-------------| +| `key_win_r_wait_explorer_exe` | `candidate` | +| `key_ctrl_s_wait_notepad_exe` | `candidate` | +| `key_alt_f4_wait_windowsterminal_exe` | `candidate` | + +**Si le catalogue est vide** : les fichiers YAML doivent exister dans `data/competences/candidate/`. Vérifier : +```bash +ls -la data/competences/candidate/ +``` +Si les fichiers existent mais n'apparaissent pas dans le dashboard, redémarrer le streaming server (`./svc.sh restart streaming`). + +### 1.4 Agent Windows connecté + +- Dans le dashboard, vérifier que la machine cible apparaît (ex: `DESKTOP-XXXXX_windows`). +- Si l'agent n'est pas connecté : le lancer depuis le poste Windows (`Lea_v*.zip` → `lea_agent.exe`). +- **Machine cible** : noter le `machine_id` exact (affiché dans le dashboard ou dans `data/agents/`). + +### 1.5 État initial de la machine Windows + +Avant toute manipulation : + +- [ ] **Fermer tous les dialogues parasites** : boîtes de dialogue "Enregistrer", "Exécuter", confirmations, pop-up Windows Update +- [ ] **Fermer les applications non nécessaires** : tout garder minimal (bureau + applications de test uniquement) +- [ ] **Vérifier que le bureau est visible** : pas d'application plein écran +- [ ] **Couper les notifications Windows** : mode "Ne pas déranger" activé (Win+A → activer) +- [ ] **Noter la résolution écran** : `Win+I → Système → Écran` (utile si un problème de positionnement survient) + +### 1.6 Dossier de collecte des preuves + +Créer un dossier de travail pour cette session de test : +```bash +mkdir -p /home/dom/ai/rpa_vision_v3/docs/demo/e2e-poc-$(date +%Y%m%d) +``` + +Ce dossier recevra les screenshots, logs, et verdicts collectés pendant le test. + +--- + +## 2. Protocole pas-à-pas + +### Phase 1 — Démarrer Léa / agent Windows + +**Ce que l'opérateur fait** : +1. Vérifier que l'agent Windows est connecté (dashboard → page principale) +2. Noter le `machine_id` affiché +3. Ouvrir l'agent chat (`http://localhost:5004`) dans un navigateur + +**Ce que Léa fait** : Rien (état IDLE) + +**Ce qu'on doit voir** : +- Dashboard : machine connectée, statut "online" +- Agent chat : interface de conversation vide, bouton "Apprendre" visible + +**Critère GO/NO-GO** : +- GO : agent chat accessible, machine connectée +- NO-GO : relancer l'agent Windows, vérifier le réseau, vérifier que le streaming server tourne + +**Preuve à collecter** : +- Screenshot du dashboard montrant la machine connectée +- Copier le `machine_id` dans un fichier `preuves/machine_id.txt` + +--- + +### Phase 2 — Déclencher l'apprentissage depuis agent-chat + +**Ce que l'opérateur fait** : +1. Dans l'agent chat, taper : `lea apprends-moi` ou cliquer le bouton "Apprendre" si disponible +2. Observer la réponse de Léa + +**Ce que Léa fait** : +- Transition IDLE → LISTENING → WAITING_USER_STOP +- Répond : *"Je te regarde. Fais ce que tu veux m'apprendre, et dis-moi « stop » ou « j'ai fini » quand c'est terminé."* +- Appelle `shadow_start` côté streaming server + +**Ce qu'on doit voir** : +- Message de Léa confirmant qu'elle observe +- Side shadow actif côté streaming server (vérifiable via `curl http://localhost:5005/api/v1/shadow/sessions` si endpoint existe) + +**Critère GO/NO-GO** : +- GO : Léa a répondu avec le message d'observation +- NO-GO : si `shadow_start` échoue → vérifier streaming server, token API (`RPA_API_TOKEN`), relancer + +**Preuve à collecter** : +- Screenshot de la conversation agent-chat +- Copier le `session_id` (affiché dans la réponse ou dans les logs `agent_chat/state/`) + +--- + +### Phase 3 — Observer une tâche courte + +**Tâche cible** : `Win+R` → ouverture de la boîte "Exécuter" + +**Ce que l'opérateur fait** (sur le poste Windows) : +1. **Basculer sur la session Windows** (via RDP ou directement) +2. Appuyer sur `Win+R` +3. Vérifier que la boîte "Exécuter" s'ouvre +4. **Ne pas fermer** la boîte "Exécuter" (elle servira de preuve visuelle) +5. Revenir à l'agent chat + +**Ce que Léa fait** : +- Capture les événements clavier (`key_press` pour `win` + `r`) +- Capture les screenshots avant/après +- Enregistre la session brute + +**Ce qu'on doit voir** : +- La boîte "Exécuter" apparaît à l'écran Windows +- Dans les logs du streaming server, des événements `key_press` et des screenshots sont reçus + +**Critère GO/NO-GO** : +- GO : la boîte "Exécuter" est visible à l'écran +- NO-GO : si `Win+R` ne fonctionne pas → vérifier que le clavier Windows n'est pas bloqué, que RDP ne capture pas les touches + +**Preuve à collecter** : +- Screenshot de la boîte "Exécuter" ouverte (depuis le poste Windows) +- Noter l'heure exacte de l'action + +--- + +### Phase 4 — Arrêter l'observation + +**Ce que l'opérateur fait** : +1. Dans l'agent chat, taper : `stop` ou `j'ai fini` + +**Ce que Léa fait** : +- Reconnaît l'intent `USER_STOP_OBSERVE` (regex, confiance > 0.9) +- Transition WAITING_USER_STOP → ANALYZING → PRESENTING +- Appelle `shadow_stop` + `shadow_understanding` côté streaming server +- Formate la restitution Option C + +**Ce qu'on doit voir** : +- Léa répond avec un résumé de ce qu'elle a compris, par exemple : + ``` + Voilà ce que j'ai compris : + + 1. Touche → raccourci utilisé : « win + r » + 2. Fenêtre « Exécuter » → ouverte + + C'est bien ça ou je me suis trompée quelque part ? + ``` + +**Critère GO/NO-GO** : +- GO : Léa a restitué au moins 1 étape correcte +- NO-GO : si Léa dit "aucune étape comprise" → vérifier que les screenshots ont bien été capturés, que le shadow était actif + +**Preuve à collecter** : +- Screenshot de la restitution Léa dans l'agent chat +- Copier le contenu JSON de `understanding` depuis `curl http://localhost:5005/api/v1/shadow//understanding` + +--- + +### Phase 5 — Restitution et correction (Option C — formateur) + +**Ce que l'opérateur fait** : +1. **Lire attentivement** la restitution de Léa +2. Si c'est correct : taper `oui` ou `c'est bon` ou `parfait` +3. Si c'est incorrect : taper `corrige l'étape 1 : [description correcte]` + +**Ce que Léa fait** : +- Si validation : transition ITERATING_FEEDBACK → NAMING +- Si correction : envoie le feedback au streaming server (`shadow_feedback`), reçoit la nouvelle compréhension, reformule +- Compte les corrections par étape (max 3 → sortie d'urgence) + +**Ce qu'on doit voir** : +- Si corrigée : Léa reformule avec la correction appliquée +- Si validée : Léa demande le nom de la compétence + +**Critère GO/NO-GO** : +- GO : Léa a soit validé soit corrigé avec reformulation +- NO-GO : si Léa ne comprend pas la correction → vérifier l'intent parser (logs), le regex, le fallback Ollama + +**Preuve à collecter** : +- Screenshot de l'échange de correction (si applicable) +- Noter le nombre de corrections apportées + +--- + +### Phase 6 — Nommer la compétence et marquer les paramètres + +**Ce que l'opérateur fait** : +1. Donner un nom : `ouverture executer` ou `lancer executer` +2. Si Léa demande si une valeur est un paramètre : répondre "toujours" ou "c'est l'exemple du jour" +3. Pour `Win+R`, il n'y a pas de valeur saisie → aucun paramètre à marquer + +**Ce que Léa fait** : +- Enregistre le nom de la compétence +- Cherche les steps avec des valeurs saisies → demande si ce sont des paramètres +- Transition NAMING → PERSISTING quand tout est marqué + +**Ce qu'on doit voir** : +- Léa confirme : *"C'est enregistré sous « ouverture executer » (slug `ouverture_executer`). Paramètres : aucun."* + +**Critère GO/NO-GO** : +- GO : Léa a confirmé la persistance +- NO-GO : si `shadow_build` ou `competence_persist` échoue → vérifier logs streaming server, endpoint `/api/v1/lea/competences/candidate/persist` + +**Preuve à collecter** : +- Screenshot du message de confirmation +- Vérifier que le YAML a été créé : `ls -la data/competences/candidate/` +- Copier le contenu du YAML dans `preuves/competence_.yaml` + +--- + +### Phase 7 — Tester via le dashboard (replay supervisé) + +**Ce que l'opérateur fait** : +1. Ouvrir le dashboard → onglet **Knowledge Base** +2. Trouver la compétence fraîchement créée +3. Cliquer **Tester** +4. Une modale s'ouvre : "Prépare le test supervisé..." +5. **Avant de cliquer "Continuer le test"** : + - Fermer la boîte "Exécuter" si elle est encore ouverte (étape critique — voir section 5 sur les faux succès) + - Vérifier que le bureau Windows est visible, aucune application parasite +6. Cliquer **Continuer le test** + +**Ce que Léa fait** : +- Envoie `Win+R` automatiquement sur le poste Windows via l'agent +- Attend que la boîte "Exécuter" apparaisse (wait_state) +- Se met en pause après l'action + +**Ce qu'on doit voir** : +- La boîte "Exécuter" s'ouvre sur le poste Windows (~1-2 secondes après le clic "Continuer") +- La modale affiche "Valide le résultat de la compétence..." avec 3 boutons : **Valide**, **Invalide**, **Incertain** + +**Critère GO/NO-GO** : +- GO : la boîte "Exécuter" apparaît et la modale de verdict est affichée +- NO-GO : si rien ne se passe → vérifier que l'agent Windows est connecté, que le replay a bien été lancé (logs streaming server) + +**Preuve à collecter** : +- Screenshot de la boîte "Exécuter" ouverte (depuis Windows) +- Screenshot de la modale de verdict dans le dashboard + +--- + +### Phase 8 — Enregistrer le verdict + +**Ce que l'opérateur fait** : +1. Évaluer le résultat selon les critères : + - **Valide** ✅ : la boîte "Exécuter" apparaît, au premier plan, correcte + - **Invalide** ❌ : rien ne se passe, ou une autre fenêtre s'ouvre + - **Incertain** ⚠️ : la boîte apparaît mais n'est pas au premier plan +2. Cliquer le bouton correspondant +3. La modale se ferme, le dashboard se rafraîchit + +**Ce que Léa fait** : +- Persiste le verdict dans `data/competence_verdicts/verdicts.jsonl` +- Le replay reprend puis se termine + +**Ce qu'on doit voir** : +- Le dashboard revient à la liste Knowledge Base +- Un nouveau verdict est enregistré (vérifiable dans le fichier JSONL) + +**Critère GO/NO-GO** : +- GO : le verdict est enregistré, le fichier JSONL contient une nouvelle entrée +- NO-GO : si le verdict n'est pas persisté → vérifier logs dashboard, endpoint `/api/v1/lea/competences//verdict` + +**Preuve à collecter** : +- Copier la dernière ligne du fichier `data/competence_verdicts/verdicts.jsonl` dans `preuves/verdict.jsonl` +- Screenshot du dashboard après verdict + +--- + +### Phase 9 — Nettoyer et recommencer (itération) + +Pour un test robuste, **répéter les phases 3-8 au moins 3 fois** avec des contextes différents : + +| Itération | Contexte | Tâche | +|-----------|----------|-------| +| 1 | Bureau propre | `Win+R` → Exécuter | +| 2 | Avec Notepad ouvert + texte non sauvegardé | `Ctrl+S` → Enregistrer sous | +| 3 | Avec Notepad ouvert au premier plan | `Alt+F4` → Fermer | + +**Ou** répéter 3 fois la même tâche avec des contextes légèrement différents (fenêtres ouvertes différentes, résolution modifiée, etc.) pour valider la robustesse. + +--- + +## 3. Critères GO/NO-GO globaux + +### 3.1 Qu'est-ce qui valide le POC ? + +| Critère | Seuil | +|---------|-------| +| **Apprentissage par observation** | ✅ Léa doit restituer correctement au moins **1 tâche sur 3** tentées | +| **Restitution compréhensible** | ✅ Le texte Option C doit être lisible par un humain non-technique (phrases françaises, pas JSON) | +| **Correction acceptée** | ✅ Léa doit accepter et appliquer au moins **1 correction** | +| **Persistance** | ✅ Le YAML doit être créé dans `data/competences/candidate/` | +| **Replay supervisé** | ✅ Au moins **2 replays réussis sur 3** (la tâche est rejouée automatiquement) | +| **Verdict humain** | ✅ Les verdicts doivent être persistés dans le JSONL | + +**Le POC est validé si** : au moins **2 des 3 compétences** du batch 1 passent le cycle complet (apprentissage → restitution → persistance → replay → verdict) avec un verdict **Valide**. + +### 3.2 Échecs acceptables + +| Échec | Acceptable ? | Condition | +|-------|-------------|-----------| +| `Alt+F4` → verdict Incertain | ✅ Oui | Le gap `alt_f4_confirmation_dialog_not_covered` est documenté | +| Ollama fallback timeout | ✅ Oui | Si les regex passent quand même (confiance > 0.9) | +| 1 correction sur 3 nécessaires | ✅ Oui | C'est dans le design (max 3 corrections/step) | +| Wait_state timeout sur Alt+F4 | ✅ Oui | Le success_marker est trop spécifique (artefact de session) | + +### 3.3 Échecs bloquants + +| Échec | Bloquant ? | Action | +|-------|-----------|--------| +| `shadow_start` échoue systématiquement | 🔴 BLOQUANT | L'agent Windows ne communique pas avec le streaming server | +| Aucune restitution (understanding vide) | 🔴 BLOQUANT | Le pipeline shadow ne reconstruit pas les étapes | +| `competence_persist` échoue | 🔴 BLOQUANT | La persistance ne fonctionne pas | +| Replay ne déclenche aucune action | 🔴 BLOQUANT | L'agent Windows ne reçoit pas les ordres | +| Verdict non persisté | 🔴 BLOQUANT | Le contrat de verdict n'est pas implémenté | + +--- + +## 4. Preuves à collecter + +### 4.1 Fichiers à vérifier après le test + +| Fichier | Contenu | Comment vérifier | +|---------|---------|------------------| +| `data/competences/candidate/*.yaml` | Compétences persistées | `ls -la` + `cat` pour vérifier le contenu | +| `data/competence_verdicts/verdicts.jsonl` | Verdicts humains | `tail -n 5` pour voir les derniers verdicts | +| `agent_chat/state/learn_*.json` | États des sessions d'apprentissage | `ls -la agent_chat/state/` | +| `data/raw_sessions//` | Sessions brutes (screenshots + événements) | Vérifier la présence de screenshots | +| Logs streaming server | `journalctl -u rpa-streaming` ou stdout du process | Chercher les erreurs HTTP 5xx | +| Logs agent chat | stdout du process ou `logs/agent_chat.log` | Vérifier les transitions d'état | +| Logs dashboard | stdout du process ou `logs/dashboard.log` | Vérifier les appels replay/verdict | + +### 4.2 Comment documenter un succès + +Créer un fichier `preuves/succes_.md` avec : + +```markdown +# Succès — + +**Date** : 2026-06-01 +**Session ID** : learn_xxx +**Machine** : DESKTOP-XXXXX_windows + +## Preuves +- Screenshot restitution : `preuves/screenshot_restitution_.png` +- Screenshot replay : `preuves/screenshot_replay_.png` +- YAML : `data/competences/candidate/.yaml` +- Verdict : dernière ligne de `data/competence_verdicts/verdicts.jsonl` + +## Observations +- Nombre de corrections : X +- Temps d'apprentissage : ~X minutes +- Temps de replay : ~X secondes +``` + +### 4.3 Comment documenter un échec + +Créer un fichier `preuces/echec_.md` avec : + +```markdown +# Échec — + +**Date** : 2026-06-01 +**Session ID** : learn_xxx +**Phase d'échec** : apprentissage / restitution / persistance / replay / verdict + +## Symptôme + + +## Logs pertinents + + +## Cause probable + + +## Reproduire +<étapes pour reproduire> +``` + +--- + +## 5. Risques de faux succès + +### 5.1 Comment distinguer un vrai succès d'un faux positif + +| Risque | Comment le détecter | Mitigation | +|--------|-------------------|------------| +| **Dialogue déjà ouvert** avant le replay | Vérifier visuellement **avant** de cliquer "Continuer le test" | Fermer tous les dialogues parasites (checklist phase 1.5) | +| **machine_id spoofé** ou wrong target | Vérifier que le `machine_id` dans le verdict correspond à la machine de test | Noter le `machine_id` en phase 1, le comparer dans le verdict JSONL | +| **Léa envoie les touches mais une autre app les reçoit** | Vérifier que la fenêtre cible est **au premier plan** avant le replay | Mettre la fenêtre cible au premier plan avant de cliquer "Continuer" | +| **Wait_state passe par chance** (la fenêtre était déjà là) | Le wait_state doit détecter un **changement d'état**, pas un état statique | Vérifier que le wait_state est bien un "appear" et pas un "already present" | +| **Verdict enregistré mais replay non exécuté** | Vérifier que `step_results[]` dans le verdict contient des steps avec `status: success` | Lire le JSONL et vérifier le contenu de `step_results` | + +### 5.2 Pièges spécifiques à éviter + +1. **RDP et les touches Windows** : en session RDP, `Win+R` peut être capturé par la machine locale au lieu de la machine distante. + - **Solution** : dans le client RDP, options → Clavier local → "Appliquer les combinaisons de touches Windows : Sur le serveur distant" + +2. **Notifications Windows** : une notification pop-up peut voler le focus pendant le replay. + - **Solution** : activer le mode "Ne pas distrub" avant le test + +3. **Windows Update** : un redémarrage intempestif pendant le test. + - **Solution** : suspendre Windows Update temporairement + +4. **Agent Windows déconnecté** : le replay est envoyé mais l'agent ne le reçoit pas. + - **Solution** : vérifier la connexion avant chaque replay (dashboard → statut machine) + +5. **Session shadow non fermée** : si `shadow_stop` n'a pas été appelé correctement, la session reste ouverte et peut interférer. + - **Solution** : vérifier `agent_chat/state/` — aucune session ne doit être dans un état non-terminal avant de recommencer + +--- + +## 6. Bonus — Learning pack portable + +### 6.1 Structurer les artefacts POC + +Le risque : se retrouver avec des milliers de fichiers éparpillés entre `data/raw_sessions/`, `data/competences/`, `data/competence_verdicts/`, `agent_chat/state/`, et les screenshots. + +**Structure recommandée** pour un POC exportable : + +``` +learning-poc-/ +├── manifest.json # Métadonnées : date, machine, opérateur, version du code +├── competences/ +│ ├── .yaml # Compétences persistées +│ └── .yaml +├── verdicts/ +│ └── verdicts.jsonl # Verdicts humains (copie du JSONL) +├── sessions/ +│ ├── / +│ │ ├── events.jsonl # Événements bruts +│ │ └── screenshots/ # Screenshots (compressés) +│ └── / +├── audit/ +│ ├── promotions.jsonl # Audit de promotion (si applicable) +│ └── test-log.md # Journal du test humain +└── proofs/ + ├── screenshot_restitution_*.png + ├── screenshot_replay_*.png + └── succes_*.md / echec_*.md +``` + +### 6.2 Script d'export rapide + +```bash +#!/bin/bash +# export_learning_poc.sh — créer un zip portable du POC +DATE=$(date +%Y%m%d) +DEST="learning-poc-${DATE}" + +mkdir -p "$DEST"/{competences,verdicts,sessions,audit,proofs} + +# Compétences candidate +cp data/competences/candidate/*.yaml "$DEST/competences/" 2>/dev/null + +# Verdicts +cp data/competence_verdicts/verdicts.jsonl "$DEST/verdicts/" 2>/dev/null + +# Audit +cp data/competences/promotions.jsonl "$DEST/audit/" 2>/dev/null + +# Sessions récentes (dernières 24h) +find data/raw_sessions/ -maxdepth 1 -mtime -1 -type d -exec cp -r {} "$DEST/sessions/" \; 2>/dev/null + +# States agent chat +cp agent_chat/state/learn_*.json "$DEST/audit/" 2>/dev/null + +# Manifest +cat > "$DEST/manifest.json" </dev/null || echo 'unknown')", + "competences_count": $(ls "$DEST/competences/"*.yaml 2>/dev/null | wc -l), + "verdicts_count": $(wc -l < "$DEST/verdicts/verdicts.jsonl" 2>/dev/null || echo 0) +} +EOF + +# Zip +zip -r "${DEST}.zip" "$DEST/" +echo "Pack créé : ${DEST}.zip" +``` + +### 6.3 Export vers DGX Spark + +Le learning pack ainsi structuré est **autonome** : + +1. Transférer le zip vers le DGX Spark : `scp learning-poc-*.zip user@dgx-spark:/path/to/imports/` +2. Importer côté DGX : le script d'import lit le `manifest.json`, place les YAML dans `data/competences/candidate/`, les verdicts dans `data/competence_verdicts/`, et les sessions dans `data/raw_sessions/` +3. Vérifier l'import : `ls data/competences/candidate/` + `tail data/competence_verdicts/verdicts.jsonl` + +**Invariant** : le learning pack ne contient **pas** de modèles Ollama, pas de dépendances Python, pas de fichiers système. Uniquement des artefacts métier (YAML, JSONL, screenshots, events). + +--- + +## Annexe — Résumé visuel du flux + +``` +[IDLE] ──(apprends-moi)──> [LISTENING] ──> [WAITING_USER_STOP] + │ + (stop / j'ai fini) + │ + v +[DONE] <── [PERSISTING] <── [NAMING] <── [ITERATING_FEEDBACK] <── [ANALYZING] + │ │ + │ (oui / corrige étape N) + │ │ + +──> YAML + verdict JSONL <── Dashboard: Tester ──> Replay supervisé + │ + Valide/Invalide/Incertain +``` + +--- + +*Document créé le 2026-06-01. À suivre pas-à-pas pendant la démo.* diff --git a/docs/guides/GUIDE_REPARATION_RAPIDE.md b/docs/guides/GUIDE_REPARATION_RAPIDE.md index aa9b7d328..c94cc3c47 100644 --- a/docs/guides/GUIDE_REPARATION_RAPIDE.md +++ b/docs/guides/GUIDE_REPARATION_RAPIDE.md @@ -171,7 +171,7 @@ Une fois la correction appliquée et validée : --- **Tokens de Production (pour référence)** : -- Admin: `73cf0db73f9a5064e79afebba96c85338be65cc2060b9c1d42c3ea5dd7d4e490` -- ReadOnly: `7eea1de415cc69c02381ce09ff63aeebf3e1d9b476d54aa6730ba9de849e3dc6` +- Admin: `REDACTED` +- ReadOnly: `REDACTED` ⚠️ **IMPORTANT** : Ces tokens doivent rester confidentiels et seront à changer avant la production réelle. diff --git a/docs/handoffs/2026-05-19_handoff_fix_capture_monitor_guard.md b/docs/handoffs/2026-05-19_handoff_fix_capture_monitor_guard.md new file mode 100644 index 000000000..064f53466 --- /dev/null +++ b/docs/handoffs/2026-05-19_handoff_fix_capture_monitor_guard.md @@ -0,0 +1,120 @@ +# Handoff — Fix capture monitor aberrante + sécurisation fallback + +**Date** : 2026-05-19 +**Branche** : `backup/post-demo-2026-05-19` (HEAD `5ea4960e6`, non commité) +**Missions** : Claude 2 (fix initial) + Claude 2b (sécurisation fallback) +**Bug source** : audit Léa-first runtime, blocage P0 #4 — `agent_v0/agent_v1/vision/capturer.py` accède aveuglément à `mss.monitors[1]` sans valider ses dimensions. Cas observé en démo GHT 19 mai : `mss` renvoie intermittemment `2560×60` au lieu de `2560×1600`, coords Y divisées ~27×, mémoire persistante empoisonnée. + +## Contexte produit + +La voie nominale `capture → replay direct → memory` repose sur des coordonnées normalisées via les dimensions de l'écran principal. Une capture acceptant des dimensions aberrantes propage des `(x_pct, y_pct)` faux jusqu'au `TargetMemoryStore` (`core/learning/target_memory_store.py`), qui stocke des fingerprints inexploitables et incrémente `fail_count` sans signal sur la cause. Le bug originel s'auto-amplifie : chaque réutilisation de la mémoire empoisonnée déclenche d'autres échecs silencieux. + +## Fichier modifié + +- `agent_v0/agent_v1/vision/capturer.py` (309 → 401 lignes) + +## Fichier créé + +- `tests/unit/test_capturer_monitor_guard.py` (10 tests) + +## Périmètre strict + +**Non touchés** : `executor.py`, `main.py`, `api_stream.py`, replay engine, `resolve_engine.py`, `replay_memory.py`. Le fix est intégralement contenu côté capture client. Pas de SCP vers Léa Windows réalisé. + +## Changements code + +### Constantes module-level +```python +MIN_MONITOR_WIDTH = 200 +MIN_MONITOR_HEIGHT = 200 +MONITOR_MAX_ATTEMPTS = 2 +MONITOR_RETRY_DELAY_S = 0.05 +``` + +Seuil 200 px : largement au-dessus du cas observé (60 px) et largement sous toute résolution physique légitime. Marge confortable pour ne pas rejeter une fenêtre RDP/NoMachine ré-dimensionnée. Retry 2× avec 50 ms : couvre le pattern "mss cache stale au premier appel" sans pénaliser le chemin nominal (worst-case 100 ms avant abandon). + +### Helpers privés +- `_is_monitor_sane(monitor) -> bool` — prédicat plausibilité dims (None → False, dict avec width/height ≥ seuils → True) +- `_dim_str(monitor) -> str` — formatage compact `WxH` pour les logs (gère None) +- `_acquire_safe_grab(max_attempts, retry_delay_s, allow_secondary_fallback) -> (monitor_dict | None, PIL.Image | None)` — ouvre `mss.mss()`, valide les dims, retry, fallback contrôlé, retour `(None, None)` en cas d'abandon. Logs WARNING par tentative aberrante, ERROR explicite à l'abandon avec distinction des causes. + +### Méthodes publiques refactorées +| Méthode | Comportement | Justification | +|---|---|---| +| `capture_full_context` | `_acquire_safe_grab()` → fallback secondaire **autorisé** | Heartbeat ne porte pas de coords client, un écran sain quelconque suffit | +| `capture_dual` | `_acquire_safe_grab(allow_secondary_fallback=False)` → fail-closed | Reçoit `(x, y)` en coords composite, cropper depuis monitor secondaire produirait une image saine mais décalée | +| `capture_active_window` (path standalone) | `_acquire_safe_grab(allow_secondary_fallback=False)` → fail-closed | `win_rect` en coords globales, même risque | + +Signatures publiques inchangées : `""`, `{}`, `None` selon le contrat existant en cas d'erreur. + +## Décision clé — fail-closed vs recalcul d'offsets + +**Choix : fail-closed** sur fallback secondaire pour les méthodes coord-bearing. Pas de translation `(x - monitor.left, y - monitor.top)` car : +1. Translation impose ensuite de vérifier inclusion du clic et de la fenêtre dans le monitor choisi, gérer les fenêtres à cheval, valider les coords positives — surface de bug supérieure à celle qu'on essaie d'éliminer. +2. Le bug est intermittent ; refuser une capture pendant ~100 ms est acceptable, Léa reprend au prochain événement. +3. Aligne avec `feedback_failure_is_learning.md` — un échec explicite vaut mieux qu'une réussite fausse. + +## Tests + +```bash +source /home/dom/ai/rpa_vision_v3/.venv/bin/activate && \ + python -m pytest tests/unit/test_capturer_monitor_guard.py -v +``` + +**Résultat** : `10 passed in 0.25s` + +| # | Test | Mission | Couverture | +|---|---|---|---| +| 1 | `test_capture_full_context_returns_empty_when_monitor_height_aberrant` | 2 | refus dim aberrante height=60 | +| 2 | `test_aberrant_monitor_logs_warning_with_observed_dimensions` | 2 | log WARN contient "2560" et "60" | +| 3 | `test_capture_retries_when_first_monitor_query_is_aberrant` | 2 | retry réussi après 1er appel aberrant | +| 4 | `test_capture_falls_back_to_secondary_monitor_when_primary_aberrant` | 2 | multi-écrans : `capture_full_context` utilise monitors[2] | +| 5 | `test_capture_dual_returns_empty_dict_when_monitor_aberrant` | 2 | `capture_dual` retourne `{}` sur aberration | +| 6 | `test_capture_active_window_returns_none_when_monitor_aberrant` | 2 | `capture_active_window` retourne `None` sur aberration | +| 7 | `test_capture_full_context_succeeds_on_normal_dimensions` | 2 | non-régression chemin nominal | +| 8 | `test_capture_dual_fails_closed_when_only_secondary_monitor_sane` | 2b | `capture_dual` refuse fallback secondaire | +| 9 | `test_capture_active_window_fails_closed_when_only_secondary_monitor_sane` | 2b | `capture_active_window` refuse fallback secondaire | +| 10 | `test_capture_full_context_still_uses_secondary_fallback` | 2b | non-régression : heartbeat accepte toujours le fallback | + +Cycle TDD respecté pour tous les RED (1, 3, 5, 8) : test rouge vu avant implémentation. Tests 2, 4, 6, 7, 9, 10 ont validé une propriété exercée par l'implémentation déjà en place mais distincte du test précédent. + +## Logs runtime attendus + +**Cas nominal** : aucun log de cette garde (silencieux). + +**Cas dim aberrante avec retry réussi** : +``` +WARNING agent_v0.agent_v1.vision.capturer Monitor[1] dims aberrantes (2560x60, seuil 200x200) — attempt 1/2 +WARNING agent_v0.agent_v1.vision.capturer Capture fallback : monitor[1] dim=2560x1600, attempt=2 +``` + +**Cas fallback secondaire (heartbeat)** : +``` +WARNING ... Monitor[1] dims aberrantes (2560x60, seuil 200x200) — attempt 1/2 +WARNING ... Capture fallback : monitor[2] dim=1920x1080, attempt=1 +``` + +**Cas fail-closed (capture_dual / capture_active_window avec secondaire dispo)** : +``` +WARNING ... Monitor[1] dims aberrantes (2560x60, seuil 200x200) — attempt 1/2 +WARNING ... Monitor[2] sain (1920x1080) mais fallback secondaire refusé (allow_secondary_fallback=False) — capture cohérente des coords impossible +ERROR ... Capture abandonnée : monitor[1] aberrant après 2 tentatives (dernier vu 2560x60) et fallback secondaire désactivé pour préserver la cohérence des coordonnées +``` + +**Cas abandon total** : +``` +ERROR ... Aucun monitor avec dims plausibles trouvé après 2 tentatives (dernier vu : 2560x60, seuil 200x200) — capture abandonnée +``` + +## Points ouverts (à tracer post-déploiement) + +1. **Seuil 200 px choisi par jugement** — pas par mesure empirique sur l'éventail des dims légitimes (RDP, NoMachine windowed, multi-écran portrait). À relever si un faux rejet apparaît en prod. +2. **Reproduction du bug réel non testée** — les 10 tests utilisent des mocks `mss`. La cause exacte côté mss reste non identifiée. Cette garde **protège** sans **expliquer**. À confirmer en redéployant sur la machine Windows Léa. +3. **SCP vers Léa Windows non fait** — conformément à `feedback_scp_auto_modif_client_windows.md`, le SCP vers `dom@192.168.1.11` est attendu après toute modif `agent_v0/agent_v1/**`. À faire avant le prochain run réel. +4. **Suivi côté serveur potentiellement nécessaire** — si `capture_full_context` (heartbeat) commence à retourner une image issue d'un monitor secondaire en cas de panne intermittente du primary, et si le serveur réutilise ce heartbeat pour pré-check sur des coords (`/resolve_target` fallback heartbeat), la même incohérence coord/image pourrait apparaître côté serveur. **Hors périmètre** missions 2/2b. À tracer comme follow-up éventuel. +5. **Pas de translation x/y dans `capture_dual`/`capture_active_window`** — si un jour on voulait supporter le fallback secondaire proprement, ce serait un chantier séparé (calcul `(x - monitor.left, y - monitor.top)`, validation inclusion, gestion fenêtres à cheval). +6. **Couverture `capture_active_window` quand `full_img` est fourni par l'appelant** — la garde s'applique uniquement au path standalone (sans `full_img`). Le cas le plus fréquent (appel depuis `capture_dual` avec `full_img=img`) est implicitement gardé via la garde de `capture_dual`. Mais un autre appelant qui fournirait un `full_img` issu d'une capture non gardée court-circuiterait la défense. Pas d'appelant identifié actuellement. + +## Référence audit + +Ce fix résout le blocage **P0 #4** de `docs/AUDIT_LEA_FIRST_RUNTIME_2026-05-19.md`. Les blocages P0 #1 (`record_human_correction` cassé), #2 (`build_workflow_replay` orphelin), #3 (memory gated sur `window_title`) restent ouverts. diff --git a/docs/handoffs/2026-05-19_handoff_post_demo_GHT.md b/docs/handoffs/2026-05-19_handoff_post_demo_GHT.md new file mode 100644 index 000000000..1cc9b9921 --- /dev/null +++ b/docs/handoffs/2026-05-19_handoff_post_demo_GHT.md @@ -0,0 +1,113 @@ +# Handoff — Post-démo GHT 2026-05-19 + +## TL;DR + +✅ **Démo vidéo enregistrée en bout en bout** après une session de 14h (18-19 mai) et **15 jours de bug-chasing** depuis le 5 mai. + +⚠️ **Retour d'expérience amer** : on est partis d'un système moins mature mais qui **fonctionnait** (état tag `demo-stable-2026-05-12`, branche `demo/ght-2026-05-08`) et on a dérivé via une cascade de modifs locales jusqu'à un système instable. La démo enregistrée a fonctionné **grâce à des contournements**, pas grâce à une vraie résolution des bugs. + +🎯 **Objectif post-démo** : consolider, identifier les vrais bugs racines, restaurer un état stable utilisable pour les prochaines démos sans devoir refaire 14h de bricolage. + +## État final livré + +- **Vidéo démo** : enregistrée et utilisable +- **Workflow** : `Demo_urgence_3_db` (`wf_483910cdd851_1778750587`) — 46 steps +- **Branche backup git** : `backup/post-demo-2026-05-19` → commit `5ea4960e6` (poussée sur gitea) +- **Tarball complet** : `/home/dom/ai/_archives/rpa_vision_v3_post_demo_20260519_142940.tar.gz` (8.9 Go, sha256 `7ab84f22d5a45b7880cad4efb4466f9e320f3e1e33218ceee267fc93fe7631af`) + +## Chronologie résumée de la session (18-19 mai) + +### 18 mai +1. Création **page AIVA-URGENCE** autonome (`docs/clients/ght_sud_95/aiva_urgence/`) — interface médicale pour la démo, design dark +2. Intégration AIVA dans le workflow : 6 nouveaux steps insérés (ord 15-20) entre `llm_generate` et `Win+D` +3. Bug **NPM Basic Auth** sur `urgence.labs.laurinebazin.design` → bypass via `location ^~ /aiva-urgence/ auth_basic off;` directement dans `proxy_host/10.conf` (modif hors git, sera écrasée si UI NPM touchée) +4. Bug **`pause_for_human` skip silencieux en mode autonome** → fix `safety_checks` avec `required: false` sur step 1 +5. Bug **frontend cache** : Ctrl+Shift+R obligatoire après chaque modif DB pour que VWB envoie le workflow à jour +6. Fix **`api_stream.py:3013`** — enrichir le payload `replay_paused/pause_message/replay_id` dès le premier polling `/replay/next` (sinon gap 1-2s) +7. Création `scripts/cancel-replays.sh` — workaround pour purger les queues serveur (bug Stop VWB) + +### 19 mai +8. **Merge workflow `linux_db`** dans Demo_urgence_3_db (suppression ord 36-46, insertion 9 steps de linux_db) via agent +9. **Bypass LLM** : `_handle_t2a_decision_action` + `_handle_llm_generate_action` acceptent maintenant `static_result` / `static_text` → décision déterministe UHCD pour MOREL Catherine (court-circuit Ollama) +10. **Bug `delay_before/delay_after` jamais lus** au runtime → ajout step `wait` explicite + lift `duration_ms` dans `dag_execute.py` +11. **Bug coord côté Léa** : `actual_position Y` divisé par ~27 sur certains clics (mapping client utilise dim écran tronquée `2560×60` au lieu de `2560×1600`) +12. **Bug VWB recapture anchor** : "Recapture" via UI ne régénère PAS le PNG (les 2 anchors `anchor_d467a5411722` et `anchor_bfbffbb47be7` sont **bit-à-bit identiques** alors que capturés à 8 jours d'intervalle) +13. **Bug Léa état mémoire** : la bulle paused n'apparaît plus dans le chat après plusieurs replays consécutifs → résolu par restart Léa Windows +14. Solution pour NoMachine : remplacé le double-clic auto par `pause_for_human` → Dom clique manuellement pendant l'enregistrement + +## Bugs racines identifiés (à traiter post-démo, par ordre de gravité) + +### 🔴 P0 — Bug "recapture anchor ne régénère pas le PNG" (VWB) +**Symptôme** : à chaque modif, "régression mystérieuse". L'anchor recapturé pointe sur la mauvaise icône car le PNG reste l'ancien. +**Audit** : `visual_workflow_builder/backend/api_v3/capture.py` — chercher si `image_path` est défini AVANT le screenshot, ou si la fonction réutilise un fichier existant. +**Impact** : explique 80% des "ça marchait hier" et des régressions silencieuses entre démos. + +### 🔴 P0 — Bug "Stop VWB ne purge pas la queue serveur" +**Symptôme** : relance d'un replay = celui-ci hérite des actions en attente du précédent → Léa reprend au milieu. +**Workaround actuel** : `./scripts/cancel-replays.sh` manuel. +**Vrai fix** : VWB doit appeler `POST /api/v1/traces/stream/replay//cancel` quand on clique Stop. + +### 🔴 P0 — Bug coord côté Léa client (mapping Y cassé sur capture tronquée) +**Symptôme** : `actual_position Y = 0.0099` au lieu de `0.265` → clic en haut de l'écran au lieu de la cible. +**Cause** : `mss.monitors[1]` retourne intermittemment `2560×60` au lieu de `2560×1600` → Léa map `y_pct * 60 = 16 px`. +**Audit** : `agent_v0/agent_v1/core/executor.py:606-617` — ajouter fallback dim minimale (rejeter si `height < 200`). + +### ⚠️ P1 — Léa client accumule un état mémoire dégradé +**Symptôme** : après plusieurs pauses consécutives, la bulle paused n'apparaît plus (chat vide alors que le serveur envoie bien `replay_paused: true`). +**Workaround** : restart Léa Windows. +**Vrai fix** : reset propre de `_last_pause_msg_shown`, `_chat_window_ref`, état Tkinter à chaque fin de replay (côté `main.py`). + +### ⚠️ P1 — `delay_before` / `delay_after` ignorés au runtime +**Symptôme** : on peut configurer ces champs en DB mais Léa ne les lit pas. +**Fix partiel appliqué** : `dag_execute.py` lift `duration_ms` pour les actions `wait`/`wait_for_anchor`. +**Vrai fix** : faire de même pour `delay_before` et `delay_after` côté executor.py (généraliser). + +### ⚠️ P1 — Léa client interprète `action=null + replay_paused=true` comme "fin du replay" +**Symptôme** : main.py désactive `_replay_active` à tort quand le replay est en pause → cleanup UI + bulle invisible. +**Fix proposé** : `executor.py:1875` retourner `True` (au lieu de `False`) quand `replay_paused` est traité. +**Status** : non appliqué (nécessite SCP + restart Léa Windows). + +### ⚠️ P2 — Bug VWB frontend cache +**Symptôme** : après modif DB, le frontend continue à envoyer l'ancienne version. Ctrl+Shift+R obligatoire. +**Vrai fix** : invalidation cache automatique côté React quand workflow modifié serveur-side. + +## Leçons de méthode (autocritique honnête) + +1. **15 jours de dérive** : on a accumulé des modifs locales sans valider à chaque étape qu'on ne dégradait pas l'existant. CLAUDE.md disait "chirurgie itérative supervisée", on a fait l'inverse à plusieurs reprises. +2. **Pas de tests de non-régression entre démos** : à chaque "ça marchait hier", on découvrait par hasard qu'un truc avait changé. Manque de smoke-test reproductible. +3. **Bug VWB recapture anchor non détecté pendant 15 jours** : la cause racine n°1 des régressions silencieuses était invisible. Aurait dû être trouvée plus tôt par un audit "diff PNG anchor avant/après recapture". +4. **Workarounds empilés** : chaque bug a généré un workaround (script cancel-replays.sh, bypass LLM static, pause humaine NoMachine, etc.) au lieu de fixer la cause. Dette technique accumulée. +5. **Recapture en aveugle** : Dom a recapturé plusieurs anchors en pensant fixer le bug, alors que le bug était VWB côté capture (pas Dom). + +## État technique en sortie de session + +- **Branche actuelle** : `feature/qw-suite-mai` (avec toutes les modifs uncommitted de la session) +- **Branche backup** : `backup/post-demo-2026-05-19` (commit groupé, poussé sur gitea) +- **Référence "ça marchait"** : tag `demo-stable-2026-05-12` (commit `2eeaa806b`), branche `demo/ght-2026-05-08` (commit `56e869c46`) +- **Services actifs** : streaming (5005), VWB backend (5002), VWB frontend (3002), dashboard (5001), worker (5099), agent-chat (5004) +- **Bypass LLM actif** dans `replay_engine.py` : les steps 12/13/14 du workflow utilisent `static_result`/`static_text` → décision UHCD MOREL hardcodée +- **NPM bypass auth** actif dans `proxy_host/10.conf` (sera écrasé si UI NPM touchée) + +## Sauvegardes + +| Type | Localisation | Détails | +|------|--------------|---------| +| Tarball | `/home/dom/ai/_archives/rpa_vision_v3_post_demo_20260519_142940.tar.gz` | 8.9 Go, sha256 vérifié | +| Branche git | `backup/post-demo-2026-05-19` sur gitea | commit `5ea4960e6`, 627 fichiers incluant 468 anchors | +| Backups DB | `visual_workflow_builder/backend/instance/workflows.db.bak.*` | ~12 .bak DB de la session (chaque modif majeure) | + +## Prochaine session — priorités proposées + +1. **Comparer avec `demo/ght-2026-05-08`** : identifier ce qui a vraiment régressé depuis l'état "ça marchait" (mais qui était moins mature) +2. **Fixer le bug VWB recapture anchor** (P0) — c'est le boss final, il explique la majorité des régressions silencieuses +3. **Fixer le bug Stop VWB ne purge pas la queue** (P0) — supprime un workaround manuel +4. **Réintégrer le fix `executor.py:1875` (return True sur replay_paused) côté Léa** + SCP + restart Léa Windows +5. **Smoke-test reproductible** : script qui rejoue Demo_urgence_3_db de bout en bout et compare l'état attendu à chaque step (au lieu de tester manuellement) + +## Pointeurs utiles + +- Mémoire projet : `/home/dom/.claude/projects/-home-dom-ai-rpa-vision-v3/memory/MEMORY.md` +- CLAUDE.md projet : `/home/dom/ai/rpa_vision_v3/CLAUDE.md` (règles de méthode) +- Reset replays : `./scripts/cancel-replays.sh` +- Mockup AIVA-URGENCE : `docs/clients/ght_sud_95/aiva_urgence/` (port 8765, exposé via `https://urgence.labs.laurinebazin.design/aiva-urgence/`) +- Workflow ID démo : `wf_483910cdd851_1778750587` diff --git a/docs/handoffs/2026-05-20_handoff_claude_horizon1_lea_first.md b/docs/handoffs/2026-05-20_handoff_claude_horizon1_lea_first.md new file mode 100644 index 000000000..0c30a87e6 --- /dev/null +++ b/docs/handoffs/2026-05-20_handoff_claude_horizon1_lea_first.md @@ -0,0 +1,138 @@ +# Handoff Claude — Horizon 1 Léa-first + +**Date** : 2026-05-20 +**Branche** : `backup/post-demo-2026-05-19` (HEAD `5ea4960e6`) +**Session** : sessions du 19-20 mai post-démo GHT + +## Contexte + +Horizon 1 = pivot Léa-first post-démo GHT. Mission globale comprise : auditer l'état réel du runtime `capture → replay direct → memory`, identifier et corriger ce qui empêche cette voie d'être nominale, préparer la validation client. Cadre : pas de refonte large, pas de retour sur VWB comme outil central, chirurgie supervisée. + +Le travail a été découpé par Dom en missions numérotées (1, 2, 2b, 3, 4, 5) avec périmètres serrés et interdits explicites. Ce handoff couvre uniquement ma contribution sur ces missions. + +## Travaux réalisés + +### Audits (lecture seule, sans modification de code) + +| Mission | Objectif | Livrable | +|---|---|---| +| Préliminaire | Inventaire factuel des 15 jours pré-démo GHT (ce qui marche / ce qui casse) | `docs/LESSONS_LEARNED_GHT_2026-05.md` | +| 1 | 10 blocages runtime Léa-first (capture / replay direct / memory) | `docs/AUDIT_LEA_FIRST_RUNTIME_2026-05-19.md` | +| 3 | Cartographie de la perte de `window_title` dans le pipeline mémoire | `docs/AUDIT_WINDOW_TITLE_MEMORY_PATH_2026-05-19.md` | +| 4 | Point d'intégration agent le plus petit pour le contrat `/finalize` enrichi | `docs/AUDIT_FINALIZE_CONTRACT_INTEGRATION_2026-05-20.md` | +| 5 | Préparation validation réelle Windows (fichiers à déployer + smoke test) | `docs/SMOKE_TEST_FINALIZE_REPLAY_2026-05-20.md` | + +### Modifications de code + +**Faites par moi (missions 2 + 2b)** : +- `agent_v0/agent_v1/vision/capturer.py` — ajout garde dimensions monitor : + - constantes module-level (`MIN_MONITOR_WIDTH=200`, `MIN_MONITOR_HEIGHT=200`, `MONITOR_MAX_ATTEMPTS=2`, `MONITOR_RETRY_DELAY_S=0.05`) + - helpers `_is_monitor_sane`, `_dim_str`, `_acquire_safe_grab(allow_secondary_fallback)` + - refactor `capture_full_context` (fallback secondaire autorisé) + - refactor `capture_dual` (fail-closed sur fallback secondaire) + - refactor `capture_active_window` path standalone (fail-closed) +- `tests/unit/test_capturer_monitor_guard.py` — CRÉÉ, 10 tests TDD + +Handoff de ce fix : `docs/handoffs/2026-05-19_handoff_fix_capture_monitor_guard.md`. + +**Constatées dans la branche, NON faites par moi** (apparues entre mes missions, probablement faites dans une session parallèle) : +- `agent_v0/agent_v1/main.py` — câblage `set_on_finalize_result` + `_on_finalize_result` + import `dispatch_finalize_result` +- `agent_v0/agent_v1/network/streamer.py` — attribut + setter + invocation `_on_finalize_result` dans `_finalize_session` +- `agent_v0/agent_v1/ui/smart_tray.py` — méthode `offer_finalize_replay` + `_launch_replay_request` +- `agent_v0/agent_v1/finalize_contract.py` — NOUVEAU fichier (untracked) +- `agent_v0/agent_v1/core/executor.py` — modifié (origine antérieure, hors scope mes missions) + +### Validations / tests + +| Test | Commande | Résultat | +|---|---|---| +| Garde monitor (10 tests TDD) | `source .venv/bin/activate && python -m pytest tests/unit/test_capturer_monitor_guard.py -v` | `10 passed in 0.25s` | +| Import sanity du module modifié | `python -c "from agent_v0.agent_v1.vision.capturer import VisionCapturer, _acquire_safe_grab, _is_monitor_sane, MIN_MONITOR_WIDTH, MIN_MONITOR_HEIGHT, MONITOR_MAX_ATTEMPTS, MONITOR_RETRY_DELAY_S"` | `Import OK ; seuils=200x200, retries=2, delay=0.05s` | + +Aucun autre test exécuté (suite complète projet, tests intégration, tests Windows réel). + +## Fichiers touchés (effectivement modifiés par moi) + +``` +agent_v0/agent_v1/vision/capturer.py (modifié — garde monitor) +tests/unit/test_capturer_monitor_guard.py (créé — 10 tests) +``` + +Plus les 6 documents listés en section Audits ci-dessus. + +**Aucune autre modification de code de ma part.** Les autres modifs visibles dans `git status` (main.py, streamer.py, smart_tray.py, finalize_contract.py, executor.py) ne sont pas de mon fait. + +## Tests exécutés + +```bash +source /home/dom/ai/rpa_vision_v3/.venv/bin/activate && \ + python -m pytest tests/unit/test_capturer_monitor_guard.py -v +``` + +Résultat : **10 passed in 0.25s** (rejet dims aberrantes, log WARNING avec dims observées, retry après aberration, fallback monitor secondaire pour `capture_full_context`, fail-closed sur fallback secondaire pour `capture_dual` et `capture_active_window`, non-régression dim normale). + +Cycle TDD respecté : RED observé avant chaque GREEN sur les 4 tests qui ont fait l'objet d'une implémentation neuve (tests 1, 3, 5, 8). + +## Points ouverts + +### Bugs P0 identifiés mais NON fixés + +| # | Origine | Statut | +|---|---|---| +| P0 #1 audit Léa-first | `record_human_correction` double bug (import inexistant + signature obsolète) | **non fixé** | +| P0 #2 audit Léa-first | `build_workflow_replay` orphelin (0 caller runtime) | **non fixé** — note : pertinence à reconsidérer maintenant que le contrat `/finalize` enrichi semble couvrir le besoin | +| P0 #3 audit Léa-first / Mission 3 | `window_title` perdu : asymétrie écriture top-level vs lecture target_spec (`stream_processor.py:1545` vs `:1590-1601`) | **non fixé** | +| P0 #3 audit Léa-first / Mission 3 | Fallback `expected_window_before` morte dans `api_stream.py:3636` (cherche dans target_spec, n'est jamais là) | **non fixé** | +| P0 #4 audit Léa-first | `mss.monitors[1]` aveugle aux dims aberrantes | ✅ **FIXÉ** missions 2 + 2b | + +### Validations Windows en attente + +| Item | Statut | +|---|---| +| SCP des 5 fichiers vers `dom@192.168.1.11:C:/rpa_vision/agent_v1/` (capturer.py, finalize_contract.py, streamer.py, smart_tray.py, main.py) | **non fait** | +| Reproduction réelle du bug mss monitors aberrant | **non vérifié** (la garde protège sans expliquer la cause exacte) | +| Smoke test 5 minutes `enregistrement → finalize → proposition → replay-session` sur Léa Windows | **non fait** | +| Contrat serveur `/finalize` enrichi renvoie bien `{replay_ready, replay_request, replay_launch}` | **non vérifié** (audité côté agent uniquement, jamais déclenché en condition réelle) | + +### État git + +- 5 fichiers modifiés non committés (`capturer.py` par moi ; `main.py`, `streamer.py`, `smart_tray.py`, `executor.py` non par moi) +- 1 fichier untracked (`finalize_contract.py`, non par moi) +- 6 documents créés dans `docs/` et `docs/handoffs/` (par moi, non committés) + +## Risques / hypothèses + +### Sur le déploiement et la synchro client Windows + +- **Hypothèse** : le canal de déploiement Windows reste le `sshpass scp` manuel fichier par fichier vers `C:/rpa_vision/agent_v1/`, conforme à `memory/feedback_scp_auto_modif_client_windows.md`. **Non vérifié** : aucun script SCP automatique n'est présent dans le repo, et le dossier `agent_v0/deploy/windows_client/` est obsolète de 2 à 7 semaines selon les fichiers (sert au setup initial, pas à l'incrémental). +- **Risque critique** : `finalize_contract.py` est un fichier neuf untracked. Le réflexe "je SCP les fichiers modifiés" ne le couvre pas. **Si oublié → ImportError au démarrage Léa, agent ne démarre pas**. +- **Risque** : SCP non atomique. Si Léa est relancée entre 2 SCP, état incohérent possible. Mitigation : SCP les 5 fichiers d'abord, **puis** relancer Léa. +- **Hypothèse** : token `RPA_API_TOKEN` est exporté côté session Windows. **Non vérifié** depuis cette session. Sans token → `register` et `finalize` retournent 401 silencieusement. + +### Sur le contrat `/finalize` enrichi + +- **Observé** : le câblage agent-side consomme un contrat `{replay_ready, replay_request, replay_launch}` (avec `replay_launch.status` ∈ {`started`, `failed`}). **Non vérifié** : le serveur Linux retourne effectivement ce contrat. Si le serveur renvoie encore l'ancien payload, le smoke test s'arrêtera silencieusement après l'étape "Session finalisée" sans crash ni proposition. + +### Sur le fix monitor + +- **Fait** : la garde protège contre dim aberrante via mocks. **Non vérifié** sur Léa Windows réelle (pas de SCP). La cause exacte côté `mss` reste non identifiée — cette garde **protège sans expliquer**. +- **Hypothèse** : seuil 200 px choisi par jugement, pas par mesure empirique sur l'éventail des dims légitimes. À relever si un faux rejet apparaît en prod. + +## Ce que je recommande pour la suite + +1. **Committer `finalize_contract.py` et les 5 fichiers modifiés** avant tout SCP. Un fichier untracked qui disparaît dans un `git stash` ou `git checkout` casserait silencieusement Léa au prochain démarrage. Un seul commit groupé `feat(agent): câblage contrat /finalize enrichi + garde dims monitor` aligne aussi les deux chantiers Horizon 1. + +2. **Smoke test 5 min sur Léa Windows en suivant `docs/SMOKE_TEST_FINALIZE_REPLAY_2026-05-20.md`** avant tout autre développement. Risque P0 : tant qu'on n'a pas vu le flux complet `enregistrement → finalize → proposition → replay-session` tourner sur Léa réelle, on ne sait pas si le contrat serveur est bien aligné avec le câblage agent. + +3. **Fixer les bugs P0 #1 et #3 (Mission 3) dans une mission courte dédiée** : + - `record_human_correction` (2 lignes : import + signature) + - `expected_window_before` fallback (1 ligne dans `api_stream.py:3636` : `action.get(...) or _tspec.get(...)` au lieu de `_tspec.get(...) or _tspec.get(...)`) + Ces 3 lignes débloquent l'apprentissage supervisé et la fallback mémoire, sans dépendre du run Windows. + +4. **Reconsidérer le statut de P0 #2 (`build_workflow_replay` orphelin)** maintenant que le contrat `/finalize` enrichi expose `replay_request`. Hypothèse : le besoin du pont capture→replay-direct est résolu par le nouveau contrat. Si vrai → supprimer `workflow_replay.py` (code mort) ou le marquer explicitement obsolète. À trancher avant prochaine session sur le sujet. + +5. **Ne pas attaquer le bug P0 #3 racine (window_title posé top-level au lieu de target_spec dans `stream_processor.py:1545`) sans confirmation produit du contrat mémoire**. La correction "rapide" (propager dans target_spec) marche mais n'élimine pas le doublon top-level / target_spec présent dans tout le code. Mieux : décider d'abord si la signature d'écran reste `sha256(window_title)` ou évolue (référence `last_window_info` au niveau session, par exemple). + +--- + +**Conformité aux contraintes** : pas de réécriture historique projet, VWB mentionné une seule fois (recommandation 4, comparaison de pont), pas de proposition de refonte, factuel, court. Aucune modification de code dans cette mission handoff. diff --git a/docs/handoffs/2026-05-20_handoff_horizon1_lea_first.md b/docs/handoffs/2026-05-20_handoff_horizon1_lea_first.md new file mode 100644 index 000000000..54eeeff33 --- /dev/null +++ b/docs/handoffs/2026-05-20_handoff_horizon1_lea_first.md @@ -0,0 +1,142 @@ +# Handoff Horizon 1 — Lea-first + +Date : 2026-05-20 +Owner : Codex +Contexte : remise en ordre du coeur produit `Lea-first` en excluant `VWB` du chemin nominal court terme. + +## 1. Ligne produit retenue + +- Coeur produit court terme : `capture -> replay direct -> verification -> memory` +- `VWB` n'est pas la reference produit +- `TargetMemoryStore` est la boucle d'apprentissage utile pour Horizon 1 +- La voie `shadow -> WorkflowIR -> ExecutionPlan` reste une direction moyen terme, pas la voie nominale actuelle + +## 2. Tickets traites + +### T1 — correction humaine -> mémoire persistante + +Fichiers : +- `agent_v0/server_v1/replay_learner.py` +- `tests/unit/test_policy_grounding_recovery_learning.py` + +Effet : +- `record_human_correction()` repersiste bien dans la mémoire cible +- fallback `window_title` amélioré + +### T2 — pause/reprise replay côté agent + +Fichiers : +- `agent_v0/agent_v1/core/executor.py` +- `agent_v0/agent_v1/main.py` +- `tests/unit/test_agent_v1_replay_pause_state.py` + +Effet : +- `replay_paused=true` ne coupe plus prématurément le mode replay + +### T3 — contrat `window_title` pour la mémoire + +Fichiers : +- `agent_v0/server_v1/stream_processor.py` +- `agent_v0/server_v1/api_stream.py` +- `tests/unit/test_window_title_memory_path.py` + +Effet : +- le flux Lea-first propage `window_title` jusque dans les chemins mémoire importants + +### T4 — chaînage produit `finalize -> replay-session` + +Fichiers : +- `agent_v0/server_v1/api_stream.py` +- `tests/integration/test_finalize_replay_chain.py` + +Effet : +- `POST /api/v1/traces/stream/finalize` reste compatible +- expose maintenant `replay_ready` et `replay_request` +- peut lancer un replay direct avec `launch_replay=true` +- si le lancement échoue, la finalisation reste un succès exploitable + +### T5 — intégration agent du contrat `finalize` + +Fichiers : +- `agent_v0/agent_v1/network/streamer.py` +- `agent_v0/agent_v1/ui/smart_tray.py` +- `agent_v0/agent_v1/main.py` +- `agent_v0/agent_v1/finalize_contract.py` +- `tests/unit/test_agent_finalize_replay_contract.py` +- `tests/integration/test_client_server_compat.py` + +Effet : +- `TraceStreamer` remonte le payload JSON de `/finalize` +- `AgentV1` route ce payload vers une logique légère dédiée +- le systray propose un test immédiat avec consentement humain +- le lancement de test passe par `POST /api/v1/traces/stream/replay-session` +- on ne réutilise pas `_launch_replay()` car cette méthode cible `/replay/start` avec `workflow_id` + +## 3. Tests verts + +- `pytest -q tests/unit/test_agent_v1_replay_pause_state.py` +- `pytest -q tests/unit/test_window_title_memory_path.py` +- `pytest -q tests/unit/test_agent_finalize_replay_contract.py` +- `pytest -q tests/integration/test_client_server_compat.py -k finalize` +- `pytest -q tests/integration/test_finalize_replay_chain.py` +- `pytest -q tests/unit/test_capturer_monitor_guard.py` +- `pytest -q tests/unit/test_target_memory_store.py` +- `pytest -q tests/unit/test_policy_grounding_recovery_learning.py -k 'ReplayLearner and human_correction'` + +Note : +- le fichier complet `tests/unit/test_policy_grounding_recovery_learning.py` reste non portable localement à cause de `pynput` + +## 4. Etat du worktree + +Modifs runtime principales en cours : +- `agent_v0/agent_v1/core/executor.py` +- `agent_v0/agent_v1/main.py` +- `agent_v0/agent_v1/network/streamer.py` +- `agent_v0/agent_v1/ui/smart_tray.py` +- `agent_v0/agent_v1/vision/capturer.py` +- `agent_v0/server_v1/api_stream.py` +- `agent_v0/server_v1/replay_learner.py` +- `agent_v0/server_v1/stream_processor.py` +- `tests/integration/test_client_server_compat.py` +- `tests/unit/test_policy_grounding_recovery_learning.py` + +Nouveaux fichiers runtime/tests : +- `agent_v0/agent_v1/finalize_contract.py` +- `tests/integration/test_finalize_replay_chain.py` +- `tests/unit/test_agent_finalize_replay_contract.py` +- `tests/unit/test_agent_v1_replay_pause_state.py` +- `tests/unit/test_capturer_monitor_guard.py` +- `tests/unit/test_window_title_memory_path.py` + +Fichiers hors-scope a ne pas embarquer par reflexe : +- `visual_workflow_builder/backend/instance/workflows.db` +- docs d'audit/handoff/plan si on veut un commit runtime pur +- `.remember/*` + +## 5. Recommandation de commit + +Point de commit pertinent : +- un commit runtime/tests cohérent `Horizon 1 Lea-first` + +Message suggéré : +- `lea-first: stabilize direct replay and finalize contract` + +## 6. Prochain objectif + +Ne pas ouvrir un nouveau chantier code tout de suite. + +Priorité suivante : +- validation réelle du flux : + `fin enregistrement -> finalize -> proposition utilisateur -> replay-session` + +Mission de lecture utile à déléguer : +- vérifier le déploiement Windows effectif des fichiers modifiés +- produire une checklist de smoke test réel de 5 minutes + +## 7. Décision de reprise + +Si nouvelle session : +- repartir à partir de ce handoff +- considérer le bloc `serveur + agent + tests finalize` comme base de référence +- ne pas revenir sur `VWB` +- passer en mode `validation réelle / déploiement / smoke test` diff --git a/docs/handoffs/2026-05-23_handoff_codex_lea_replay_resume.md b/docs/handoffs/2026-05-23_handoff_codex_lea_replay_resume.md new file mode 100644 index 000000000..b3281e2c4 --- /dev/null +++ b/docs/handoffs/2026-05-23_handoff_codex_lea_replay_resume.md @@ -0,0 +1,100 @@ +Contexte +- Projet : `rpa_vision_v3` +- Poste Windows de démo : `dom@192.168.1.11` +- Léa Windows poll le serveur Linux `192.168.1.40:5005` +- Coordination Claude/Codex via `docs/coordination/` + +Direction produit +- Ne pas dériver vers une simple “boîte à clic”. +- Cible produit : `capture brute -> post-traitement -> workflow différé compilé -> exécution supervisable`. +- Le `replay-session` direct reste utile pour smoke/debug, mais n’est pas la voie produit finale. +- Les “réflexes” (`Enregistrer`, `Ouvrir`, `Copier`, etc.) doivent idéalement devenir des primitives robustes, pas des chorégraphies visuelles fragiles. + +État code important déjà en place +- `finalize -> replay-session` validé. +- setup Windows durci par `verify_screen`. +- propagation `expected_window_before` côté serveur. +- relaxation ciblée du seuil pour `switch_tab`. +- garde drift `anchor-template`. +- handler runtime popup `Confirmer l'enregistrement -> Oui` ajouté côté agent dans `executor.py`. +- nouveau patch grounding côté agent : + - rejet des rects système / taskbar + - validation visuelle du crop fenêtre avant usage + - fallback plein écran si le crop ne matche pas visuellement + +Derniers fichiers touchés +- `agent_v0/agent_v1/core/executor.py` +- `agent_v0/agent_v1/core/grounding.py` +- `tests/unit/test_executor_verify_window_guard.py` +- `tests/unit/test_policy_grounding_recovery_learning.py` + +Validation locale récente +- `./.venv/bin/pytest -q tests/unit/test_executor_verify_window_guard.py` +- `./.venv/bin/pytest -q tests/unit/test_policy_grounding_recovery_learning.py -k GroundingEngine` +- `python3 -m py_compile agent_v0/agent_v1/core/executor.py` +- `python3 -m py_compile agent_v0/agent_v1/core/grounding.py tests/unit/test_policy_grounding_recovery_learning.py` + +Déploiement Windows récent +- `C:\\rpa_vision\\agent_v1\\core\\executor.py` mis à jour +- `C:\\rpa_vision\\agent_v1\\core\\grounding.py` mis à jour +- `py_compile` Windows OK pour ces fichiers + +Problème infrastructure découvert +- Le serveur Linux n’utilise pas réellement le GPU actuellement. +- Symptômes : + - `nvidia-smi` échoue avec `Driver/library version mismatch` + - module noyau NVIDIA `580.126.09` + - libs userspace `580.159.03` + - PyTorch `.venv` : `cuda_available=False` +- Conséquence : + - une partie des résolutions vision reste CPU-bound + - les `Server resolve timeout` à 30s sont fortement amplifiés +- Correctif probable infra : + - reboot Linux pour réaligner driver chargé / libs + - sinon réparation driver NVIDIA + +Replays live récents +- `replay_sess_ebb4d998` + - a servi à valider le besoin du handler popup overwrite +- `replay_sess_3c56b8f2` + - lancé avec mauvais `machine_id=bg_DESKTOP-58D5CAC_windows` + - non consommé par Léa +- `replay_sess_595c4947` + - lancé avec le bon `machine_id=DESKTOP-58D5CAC_windows` + - bien consommé par Léa + +État exact du replay courant `replay_sess_595c4947` +- `status=running` +- `completed_actions=11/17` +- setup passé +- saisie `test` passée +- nouveau grounding visuel actif, preuve log : + - `Grounding fenêtre validé visuellement via 'test'` +- point de casse courant : + - action `act_raw_74e4e5ec` = clic onglet `Enregistrer sous` + - retry puis `som_anchor_match score=0.74` + - clic exécuté + - `POST-VÉRIF TIMEOUT : 'rpa_vision : Explorateur de fichiers' ≠ '*test - Bloc-notes'` + - action suivante `act_raw_022cb97c` se bloque immédiatement car fenêtre actuelle = `rpa_vision : Explorateur de fichiers` + - Léa repasse en apprentissage supervisé + +Important +- Le handler popup overwrite `Oui/Non` n’a pas encore été réellement exercé sur ce run. +- Le blocage actuel est plus tôt : dérive `Enregistrer sous -> Explorateur`. +- Je n’ai pas de trace d’un `Ctrl+C` dans ce replay courant. + +Coordination Claude +- Brief ouvert : + - `docs/coordination/inbox_claude/2026-05-23_1024_codex-to-claude_notepad-saveas-explorer-drift.md` +- Réponse attendue dans : + - `docs/coordination/inbox_codex/` + +Actions recommandées pour reprise +1. Lire le brief Claude ci-dessus et sa réponse si elle existe. +2. Vérifier si la machine Windows a bien redémarré Léa proprement. +3. Vérifier l’état du replay courant `replay_sess_595c4947`. +4. Si le run est abandonné après reboot, relancer un nouveau `POST /replay-session` avec : + - `session_id=sess_20260520T102916_066851` + - `machine_id=DESKTOP-58D5CAC_windows` +5. Continuer le diagnostic uniquement sur la dérive `Enregistrer sous -> Explorateur`. +6. Garder en tête le problème infra GPU, mais ne pas le mélanger avec le bug fonctionnel de ce replay. diff --git a/docs/handoffs/2026-05-24_handoff_codex_leabench_grounding_supervision.md b/docs/handoffs/2026-05-24_handoff_codex_leabench_grounding_supervision.md new file mode 100644 index 000000000..ac7934e86 --- /dev/null +++ b/docs/handoffs/2026-05-24_handoff_codex_leabench_grounding_supervision.md @@ -0,0 +1,142 @@ +# Handoff Codex — LeaBench / Grounding / Supervision equipe + +Date : 2026-05-24 22:06 Europe/Paris +Auteur : Codex +Statut memoire Codex : `OK`, mais handoff ecrit pour reprise propre ou nouvelle session. + +## Position projet + +Lea ne doit pas etre une boite a clics. Direction maintenue : + +`observe -> verifier preconditions -> agir -> stabiliser -> juger -> apprendre uniquement sur preuve saine` + +Decisions non negociables : +- cloud models = analyse offline uniquement, apres anonymisation explicite ; +- runtime Lea reste controle par notre code ; +- aucun modele externe ne prend le controle direct de Windows ; +- corrections humaines et memory ne sont apprises que si coordonnees et contexte sont sains ; +- pas de branchement runtime experimental sans tests et rollback. + +## Commits recents importants + +- `debd7b423 feat(evaluation): add local Ollama LeaBench adapter` +- `6544ebe3f feat(evaluation): add 16 LeaBench cases from replay failures` +- `10136f0ee feat(agent): add standalone anchor-relative resolver` +- `054279feb feat(evaluation): add LeaBench model prompt packs` +- `ea1f57afb feat(evaluation): add LeaBench computer-use scorer` +- `345762330 fix(agent): respect server visual reject before text fallback` +- `b1b32187b fix(agent): P0.6 guard human corrections` +- `ad24d16d8 fix(executor): P0.9 double-check stabilité post-transition fenêtre` + +## Etat code + +LeaBench : +- `core/evaluation/computer_use_bench.py` +- `tools/lea_bench.py` +- `benchmarks/computer_use/cases/notepad_replay_failures_2026-05-24.jsonl` +- `benchmarks/computer_use/cases/leabench_extended_2026-05-24.jsonl` +- 4 cas Notepad initiaux + 16 cas etendus = 20 cas total. +- Les 16 cas etendus ont `screenshot_path` reels et sont valides. + +Prompt packs : +- `--write-prompt-pack` ajoute a LeaBench. +- Le prompt pack ne fuite pas `expectation` ni `click_region`. + +Adaptateur Ollama local : +- `core/evaluation/ollama_lea_bench_adapter.py` +- `tools/lea_bench_ollama.py` +- tests mockes dans `tests/unit/test_ollama_lea_bench_adapter.py` +- aucun run live Ollama n'a ete lance apres implementation, pour eviter contention GPU/Ollama pendant Lea. + +Anchor relative : +- `agent_v0/agent_v1/core/anchor_relative.py` +- `agent_v0/agent_v1/core/anchor_catalog.py` +- `tests/unit/test_anchor_relative.py` +- Phase 1 standalone acceptee et committee. +- Pas de branchement `executor.py`. +- Garde Codex ajoute : `target_out_of_bounds`. + +## Verifications passees + +Commandes importantes deja passees : + +```bash +python3 -m pytest -q tests/unit/test_computer_use_bench.py tests/unit/test_ollama_lea_bench_adapter.py tests/unit/test_anchor_relative.py +python3 -m py_compile core/evaluation/ollama_lea_bench_adapter.py tools/lea_bench_ollama.py +python3 tools/lea_bench.py --cases benchmarks/computer_use/cases/notepad_replay_failures_2026-05-24.jsonl --repo-root . --json +python3 tools/lea_bench.py --cases benchmarks/computer_use/cases/leabench_extended_2026-05-24.jsonl --repo-root . --json +``` + +Resultats : +- 25 tests verts sur les 3 modules. +- 4 cas Notepad valides. +- 16 cas etendus valides. +- baseline all-abstain sur 16 cas : 10/16 correct, 0 dangereux. + +## Coordination lue + +Claude : +- `2026-05-24_2230_claude-to-codex_qwen-leabench-prompt-spec.md` +- `2026-05-24_2230_claude-to-codex_anchor-relative-phase1-result.md` +- `2026-05-24_2230_claude-to-codex_leabench-cases-enrichis.md` + +Gemini : +- `2026-05-24_2315_gemini-to-codex_leabench-adapters-and-cases-v1.md` +- `2026-05-24_2345_gemini-to-codex_cloud-adapters-privacy-spec.md` +- `2026-05-24_2355_gemini-to-codex_continual-learning-alignment.md` + +Messages envoyes : +- `docs/coordination/inbox_claude/2026-05-24_2206_codex-to-claude_memory-health-check-and-handoff.md` +- `docs/coordination/outbox_gemini/2026-05-24_2206_codex-to-gemini_memory-health-check-and-handoff.md` +- `docs/coordination/outbox_gemini/2026-05-24_2208_codex-to-gemini_quality-frame-source-discipline.md` + +## Points de vigilance + +Deploiement Léa Windows : +- Verifie le 2026-05-24 vers 22:18 : `C:\rpa_vision\agent_v1\core\executor.py` date du 24/05 20:24, taille 176068 bytes. +- Marqueurs absents cote Windows : `skip_text_fallback_after_server_reject`, `drain_guard_s`, `below_threshold`. +- Conclusion : `b1b32187b` P0.6 et `345762330` R1 ne sont pas deployes cote client Windows. +- Ne pas relancer de replay live avant backup + SCP du `agent_v0/agent_v1/core/executor.py` local actuel vers Windows, puis relance Lea. + +Gemini cloud/privacy : +- Ne pas accepter comme verite les IDs API `gpt-5.5`, `claude-opus-4.7`, etc. sans verification officielle. +- Distinguer vision offline vs computer-use natif vs runtime Lea. +- Ne pas coder d'adaptateur cloud maintenant. +- Regle imposee a Gemini : chaque claim doit etre classe `SOURCE_OFFICIELLE_VERIFIEE`, `SOURCE_PRIMAIRE_NON_OFFICIELLE`, `HYPOTHESE`, `PROPOSITION_PROJET` ou `A_VERIFIER`. +- Toute mention de modele/API/benchmark sans source officielle reste non decision-ready. + +GPU/Ollama : +- Ne pas lancer `tools/lea_bench_ollama.py` si Lea est active. +- Lancer uniquement quand Dom confirme que Lea est idle. + +Git : +- Beaucoup de docs non suivis dans `docs/`, `docs/coordination/`, `docs/recherche/`. +- Ne pas nettoyer/revert. +- Les commits techniques recents sont propres ; les docs coordination restent souvent untracked volontairement. + +## Prochaine action recommandee + +Si Dom confirme que Lea est idle : + +```bash +python3 tools/lea_bench_ollama.py \ + --cases benchmarks/computer_use/cases/leabench_extended_2026-05-24.jsonl \ + --repo-root . \ + --model qwen2.5vl:7b-rpa \ + --output benchmarks/computer_use/predictions/qwen25vl_extended_2026-05-24.jsonl + +python3 tools/lea_bench.py \ + --cases benchmarks/computer_use/cases/leabench_extended_2026-05-24.jsonl \ + --repo-root . \ + --predictions benchmarks/computer_use/predictions/qwen25vl_extended_2026-05-24.jsonl \ + --json +``` + +Sinon : +- attendre les reponses memory-health de Claude et Gemini ; +- preparer Phase 2 `GroundingGuard` sur design/test plan uniquement ; +- ne pas brancher runtime avant arbitrage. + +## Resume court pour reprise + +On a stabilise l'axe evaluation avant de continuer a corriger au feeling. LeaBench existe, il a 20 cas reels, un prompt pack neutre, et un adaptateur Ollama local teste sans GPU. `anchor_relative` existe en Phase 1 standalone. Prochaine decision : lancer Qwen local quand Lea est idle, puis utiliser le score pour decider quoi brancher dans `GroundingGuard`. diff --git a/docs/handoffs/2026-05-25_handoff_claude_phase2_notepad_success.md b/docs/handoffs/2026-05-25_handoff_claude_phase2_notepad_success.md new file mode 100644 index 000000000..6781ded16 --- /dev/null +++ b/docs/handoffs/2026-05-25_handoff_claude_phase2_notepad_success.md @@ -0,0 +1,127 @@ +# Handoff Claude — Phase 2 Notepad live réussi, passage session fraîche + +- **Date** : 2026-05-25 09:10 +- **Auteur** : Claude (session 18h→09h, ~15h continue) +- **Contexte** : live Bloc-notes `replay_sess_e96e5822` réussi 18/18, dialog handler runtime validé en prod. Dom demande session fraîche avant Phase 2 wiring. + +## État mémoire honnête (au moment du handoff) + +**Solide** : +- Modèle Mandat/Protocoles/Scènes v0.3 (vocabulaire complet, boucle 8 étapes, contrat d'action) +- 4 découvertes structurantes (D1 source/deploy, D2 expected_state mort, D3 single in-flight, D4 LearningManager dormant) +- 3 dataclasses créées hier (Trace, SceneExpected, Precondition+Recovery) — commit `7bb8d543a` +- Discipline rollback (tags, .bak Windows, sshpass `'loli'`) +- Contraintes Codex (4 fichiers en lecture seule) +- Pipeline de coordination (inbox_claude/inbox_codex) + +**Friction qui commence** : +- Précision sur les `file:line` exacts (re-grep nécessaire) +- Détail diff par diff des 14 commits +- Détails fins des 7 tests manquants WP4 + +**Recommandation Claude** : session fraîche maintenant = la bonne décision. Confiance baisse sur les détails fins après ~15h continue. + +## Décisions conceptuelles à conserver + +1. **Léa est mandatée, pas commandée** : reçoit une fin, choisit le chemin, qualifie le retour. +2. **Un protocole est une grammaire d'action autour d'une intention** (≠ workflow scripté). +3. **Autonomie d'initiative, pas d'entêtement** + délégation tutorée 5 niveaux (N0 observation → N4 autonomie habituelle, contextuel selon protocole/app/client/tuteur/période). +4. **Précondition = état attendu vérifiable avant l'action** (formulation Dom). +5. **Priorité protocoles** : mieux connu → moins risqué → plus court. +6. **Apprentissage uniquement sur résultat qualifié** ; désapprentissage gradué (réduire confiance / restreindre périmètre / quarantaine / supprimer). +7. **Méta-cognition minimale** : 4 critères agrégés (score / ancre / scène / prédiction) → AGIR / DEMANDER / ABSTENTION. +8. **Rejet sémantique domine fallbacks opportunistes** (règle d'or — déjà gagnée pour close_tab via commit `345762330`). + +## Workpacks produits ce matin (à intégrer) + +| ID | Sujet | Livrable inbox_codex | Conclusion clé | +|---|---|---|---| +| WP1 (0905) | Inventaire source vs deploy | `2026-05-25_0905_WP1-inventaire-source-deploy.md` | **Risque CRITIQUE** : 9 fichiers divergents, 50% codebase deploy obsolète de 20j. NE PAS RESYNC avant démo. Porter d'abord `monitor_resolution` deploy→source. | +| WP2 (0905) | SceneExpected wiring | `2026-05-25_0905_WP2-scene-expected-wiring.md` | Point injection : `_attach_scene_expected()` dans stream_processor.py l.1368. Plan B (hors zone interdite Codex) recommandé passe 1. Flag `RPA_SCENE_EXPECTED` OFF. | +| WP3 (0905) | expected_state → Precondition | `2026-05-25_0905_WP3-expected-state-precondition.md` | Mapping Notepad : `Precondition(window_title_must_contain=["Sans titre","Untitled"])` + `Recovery(Ctrl+N, wait 400ms)`. Nouveau module isolé `precondition_inference.py`. Flag `RPA_PRECONDITION_INFER`. | +| WP4 (0905) | Single in-flight tests + factorisation | `2026-05-25_0905_WP4-single-inflight-tests-factorisation.md` | Helper `_find_in_flight_action()` remplace blocs api_stream.py:3074-3095 et 3138-3159. 3 tests critiques (1 vert, 1 xfail attendu, 1 limitation Q7). | + +## Documents stratégiques à relire au redémarrage + +Dans cet ordre : + +1. `docs/architecture/MODELE_MANDAT_PROTOCOLS_LEA_2026-05-25_v0.3_ARBITRAGES_DOM.md` — modèle de base +2. `docs/architecture/CARTOGRAPHIE_BRIQUES_MANDAT_PROTOCOLS_2026-05-25.md` — carto initiale Codex +3. `docs/plans/PLAN_PHASE2_TRACE_MANDAT_PROTOCOLS_2026-05-25.md` — plan d'exécution Phase 2 +4. `docs/coordination/inbox_codex/2026-05-25_0626_claude-to-codex_rapport-complet-phase-attente.md` — vue d'ensemble pour reprise +5. `docs/coordination/inbox_codex/2026-05-25_0905_claude-to-codex_WP{1,2,3,4}*.md` — les 4 derniers WP +6. `docs/coordination/inbox_claude/2026-05-25_0855_codex-to-claude_live-notepad-success-speed-followup.md` — preuve live + 3 axes follow-up + +## Commits Phase 2 / cognition (état branche `backup/post-demo-2026-05-19`) + +``` +7bb8d543a feat(cognition): dataclasses Trace + SceneExpected + Precondition (Phase 2.1) ← Claude +[commits Codex Phase 2.0 grounding + précondition Notepad + single in-flight] +debd7b423 feat(evaluation): add local Ollama LeaBench adapter ← Codex +6544ebe3f feat(evaluation): add 16 LeaBench cases from replay failures ← Claude +10136f0ee feat(agent): add standalone anchor-relative resolver ← Claude +054279feb feat(evaluation): add LeaBench model prompt packs ← Codex +ea1f57afb feat(evaluation): add LeaBench computer-use scorer ← Codex +345762330 fix(agent): respect server visual reject before text fallback ← Codex +b1b32187b fix(agent): P0.6 guard human corrections ← Codex +ad24d16d8 fix(executor): P0.9 double-check stabilité post-transition fenêtre ← Claude +a76f3db68 feat(executor): P1 DialogResolver serveur en fallback ← Claude +9a029a221 fix(executor): timeout 120→30s ← Claude +5ed1810ef fix(memory): rejeter coords (0,0) et hors [0,1] ← Claude +c9878f0a7 fix(validator-v2): override success=False uniquement sur TERMINATE +``` + +Tags rollback récents : `rollback/pre-cognition-dataclasses-2026-05-25_0610`, et tous les tags P0.x/P1 d'hier. + +## Risques restants + +1. **Désynchronisation source/deploy** (D1) : critique, ne pas resync avant démo. Porter `monitor_resolution` deploy→source en priorité quand Codex libère executor.py. +2. **Race lock 3349/3470** (D3) : commentaire "race bénigne" accepte le double dispatch. Faible risque démo (Léa unique), à traiter post-démo. +3. **`expected_state` mort** (D2) : produit par gemma4 mais jamais lu. WP3 propose le wiring sans gros refactor. +4. **autonomous_planner orphelin** (D4) : 1019 LOC, endpoints HTTP 410. À archiver post-démo. +5. **Fixtures `replay_failures/` non versionnées** : si purgé, LeaBench casse. Décision Dom : versionner post-démo. + +## Ce que Claude doit faire dans la prochaine session + +1. **Relire les 6 documents stratégiques ci-dessus** (15 min) +2. **Vérifier l'état d'avancement Codex** sur le test live `replay_sess_e96e5822` + 3 axes follow-up (perf, trimming, test offline) +3. **Attendre arbitrage Codex** sur l'ordre d'intégration des 4 WP avant tout patch +4. **Respecter les 4 fichiers en lecture seule** : `executor.py`, `api_stream.py`, `replay_engine.py`, `grounding.py` (tant que Codex n'a pas explicitement libéré) +5. **Continuer la discipline rollback** : tag avant chaque modif, commit atomique, tests verts avant +6. Si patch validé par Codex : utiliser sshpass `SSHPASS='loli' sshpass -e scp/ssh dom@192.168.1.11` pour SCP Windows + +## Ce que Claude ne doit PAS faire + +- ❌ Modifier runtime sans accord explicite Codex +- ❌ Toucher aux 4 fichiers interdits tant que Codex pilote +- ❌ Réveiller `autonomous_planner`, `ORALoop`, `LearningManager` (modules dormants identifiés) +- ❌ Refactor `report_action_result` (700 lignes, 4 écritures, fragile) +- ❌ Refactor `resolve_engine._resolve_target_sync` (cascade VLM) +- ❌ Resync brutale source/deploy avant démo +- ❌ Activer `DialogResolver` serveur en live alors qu'il est déjà OK côté Léa +- ❌ Modifier schéma `target_memory.db` (5 backups en prod) +- ❌ Spawner d'agents pour du brainstorming si question est ciblée (overkill — réservé aux explorations larges et workpacks parallèles) + +## Infos opérationnelles + +- **Démo DPI** : `http://192.168.1.40:8765/` (Easily, pour tests futurs post-Bloc-notes) +- **Léa Windows** : 192.168.1.11, SSH password `loli`, exec via sshpass +- **rpa-streaming** : systemd user sur Linux, port 5005 +- **Branche** : `backup/post-demo-2026-05-19` +- **DB target_memory** : `/home/dom/ai/rpa_vision_v3/data/learning/target_memory.db` (25 entrées valides, 0 poison (0,0)) + +## Inventaire complet des messages coordination 2026-05-24 → 2026-05-25 + +**inbox_claude/** (de Codex à Claude) : 25 messages depuis 2026-05-23 (le vieux `notepad-saveas-explorer-drift` reste open en option B, classé non-actionable). + +**inbox_codex/** (de Claude à Codex) : ~30 messages depuis 2026-05-23. + +Trop pour les lister, mais les plus récents (24h) : +- 13 messages de 2026-05-24 18h → 22h (P0.6 à P1, R1, anchor_relative, LeaBench cases, Qwen spec, GroundingGuard, observations Dom) +- 12 messages de 2026-05-25 02h → 09h (brainstorming Mandat/Protocoles v0.2/v0.3, cartographies A1-A4 + carto complémentaire, dataclasses cognitives, 4 WP, rapport phase attente) + +--- + +**Statut handoff** : prêt pour redémarrage session fraîche. Bonne reprise. + +Auteur : Claude diff --git a/docs/handoffs/2026-05-25_handoff_codex_notepad_success_phase2.md b/docs/handoffs/2026-05-25_handoff_codex_notepad_success_phase2.md new file mode 100644 index 000000000..a92a6a09d --- /dev/null +++ b/docs/handoffs/2026-05-25_handoff_codex_notepad_success_phase2.md @@ -0,0 +1,261 @@ +# Handoff Codex — Notepad success / Phase 2 cognitive + +Date : 2026-05-25 09:04 Europe/Paris +Auteur : Codex +Statut memoire Codex : OK pour supervision, mais session fraiche recommandee avant nouvelle phase de programmation lourde. + +## TL;DR + +Le test live Bloc-notes est passe de bout en bout. + +- Replay : `replay_sess_e96e5822` +- Source : `sess_20260520T102916_066851` +- Machine : `DESKTOP-58D5CAC_windows` +- Resultat live : `18/18`, `0` echec, `0` retry, `0` non-verifie +- Point critique valide : dialogue Windows `Confirmer l'enregistrement` absorbe sans aide humaine +- Signal cle : `warning='runtime_dialog_handled_post_verify'` + +Conclusion produit : le scenario n'est plus seulement une boite a clic. Lea a reussi a poursuivre son intention d'enregistrement malgre un dialogue systeme connu. + +## Position projet a conserver + +Lea doit etre pilotee comme un collaborateur visuel : + +`mandat -> intention -> scene attendue -> precondition -> action -> retour observe -> apprentissage sain` + +Principes non negociables : + +- Ne pas traiter Lea comme un replay de pixels. +- Verifier l'etat reel avant les gestes sensibles. +- Ne pas apprendre depuis un succes douteux ou une correction humaine parasite. +- Les dialogues connus doivent etre absorbes dans la fenetre active, avec verification que le dialogue disparait. +- Les dialogues inconnus doivent produire du doute type et une demande d'aide, pas un clic opportuniste. +- Claude doit etre delegue largement avant toute phase longue ; Codex garde supervision, arbitrage, integration et live. + +Directive persistante ajoutee : + +- `docs/coordination/CODEX_MEMO_STRATEGIE_SUPERVISION_2026-05-24.md` + +## Travail realise depuis le precedent handoff + +### Correctifs runtime / tests locaux + +Fichiers modifies cote Codex : + +- `agent_v0/agent_v1/core/executor.py` +- `agent_v0/agent_v1/core/grounding.py` +- `agent_v0/server_v1/api_stream.py` +- `agent_v0/server_v1/replay_engine.py` +- `tests/integration/test_replay_session_trim_neutral.py` +- `tests/unit/test_env_setup.py` +- `tests/unit/test_executor_verify_window_guard.py` +- `tests/unit/test_grounding_engine.py` + +Points fonctionnels : + +- Grounding : respecte les rejets serveur explicites, notamment les rejets `close_tab_*`, `drift_*`, `below_threshold`. +- Replay setup Notepad : ouverture d'un document vierge avec `Ctrl+N` et verification de scene Bloc-notes. +- API replay : garde single in-flight partielle pour eviter plusieurs actions concurrentes. +- Executor Windows : handler de dialogue runtime connu revu pour travailler dans la fenetre active, cliquer le bouton attendu, puis verifier que la fenetre a change. + +Tests passes : + +```bash +.venv/bin/python -m py_compile agent_v0/agent_v1/core/executor.py tests/unit/test_executor_verify_window_guard.py +.venv/bin/python -m pytest -q tests/unit/test_executor_verify_window_guard.py +``` + +Resultat : tests `test_executor_verify_window_guard.py` verts. + +Note : `python3 -m pytest` systeme echoue car `pynput` absent ; utiliser `.venv/bin/python`. + +### Deploiement Windows + +Deploiements effectues vers Lea Windows : + +- `C:\rpa_vision\agent_v1\core\grounding.py` +- `C:\rpa_vision\agent_v1\core\executor.py` + +Verifications : + +- `python -m py_compile C:\rpa_vision\agent_v1\core\executor.py` OK cote Windows. +- `grounding.py` egalement compile OK apres SCP precedent. +- Lea a ete relancee par Dom avant le live test. + +Attention : SSH a ensuite echoue avec `Too many authentication failures`; ne pas supposer que SSH est disponible sans corriger l'auth. + +## Resultat live important + +Ancien replay annule proprement : + +- `replay_sess_37dd7cdf` -> `cancelled` +- verification : `active=0` + +Nouveau replay propre : + +- `replay_sess_e96e5822` +- lance apres redemarrage Lea et focus ecran 1 +- termine avec succes selon logs : + - `Replay replay_sess_e96e5822 termine avec succes : 18/18 actions` + - metriques : `4 resolves [anchor_template=1, grounding_vlm=1, semantic_close_tab_hotkey=1, som_anchor_match=1] score_moy=0.94 temps_moy=10755ms` + +Point critique : + +- action : `act_raw_a8dbaaac` +- intention : enregistrer le document dans `Enregistrer sous` +- dialogue apparu : `Confirmer l'enregistrement` +- serveur OCR n'a pas trouve `Oui` via OCR-only +- agent/runtime a quand meme gere le dialogue +- report : + - `success=True` + - `warning='runtime_dialog_handled_post_verify'` + - `resolution_method='anchor_template'` + +Interprétation : + +- La brique "dialogue connu dans fenetre active + verification post-clic" fonctionne sur le cas Notepad. +- Le comportement doit maintenant devenir un test offline, pas rester une anecdote live. + +## Problemes observes pendant le live + +### Performance + +Le demarrage et l'execution sont trop lents. + +Observations : + +- Environ 60 s entre preparation et demarrage effectif. +- Premier dispatch double : + - deux `DISPATCH act_setup_sess_open_run` quasiment simultanes. + - puis `dispatch_orphan_resent` a 50 s. +- Resolution moyenne loggee : environ `10755ms` par resolve. + +Priorite prochaine : mesurer separement build replay, dispatch, attente report agent, resolve VLM/OCR/template. + +### Single in-flight incomplet + +Le live confirme les objections Claude : + +- double dispatch initial encore visible ; +- la garde single in-flight ne couvre pas toute la fenetre de race ; +- ajouter tests avant refactor. + +WP4 Claude recommande : + +- tests d'abord ; +- helper `_find_in_flight_action(session_id, machine_id, replay_id)`; +- test concurrent probablement `xfail` tant que la race n'est pas corrigee proprement. + +### Trimming hors objectif + +Le replay a termine une derniere action `ouvrir le lien vers le dossier specifie`. + +Ce n'est pas un echec du live, mais c'est hors mandat "saisir et enregistrer un texte". + +Conclusion : le trimming doit etre pilote par objectif/intention, pas seulement par fin brute du recording. + +### Source/deploy a clarifier + +Claude WP1 signale une divergence massive entre : + +- `agent_v0/agent_v1/` +- `agent_v0/deploy/windows_client/agent_v1/` + +Mais le live valide semble utiliser `C:\rpa_vision\agent_v1\...`. + +Ne pas lancer de resync massif avant verification du chemin reel d'execution Windows. + +Action prochaine : + +- confirmer le `CommandLine` du process Windows ; +- exposer ou logger un `AGENT_VERSION` avec hash/timestamp ; +- documenter une politique unique de deploiement. + +## Coordination lue / envoyee + +Retours Claude lus : + +- `docs/coordination/inbox_codex/2026-05-25_0626_claude-to-codex_rapport-complet-phase-attente.md` +- `docs/coordination/inbox_codex/2026-05-25_0640_claude-to-codex_carto-complementaire-agent_chat-WM-ORA.md` +- `docs/coordination/inbox_codex/2026-05-25_0610_claude-to-codex_synthese-workpacks-A-B-C.md` +- `docs/coordination/inbox_codex/2026-05-25_0905_claude-to-codex_WP1-inventaire-source-deploy.md` +- `docs/coordination/inbox_codex/2026-05-25_0905_claude-to-codex_WP2-scene-expected-wiring.md` +- `docs/coordination/inbox_codex/2026-05-25_0905_claude-to-codex_WP3-expected-state-precondition.md` +- `docs/coordination/inbox_codex/2026-05-25_0905_claude-to-codex_WP4-single-inflight-tests-factorisation.md` + +Messages Codex envoyes a Claude : + +- `docs/coordination/inbox_claude/2026-05-25_0845_codex-to-claude_delegation-max-phase2-supervision.md` +- `docs/coordination/inbox_claude/2026-05-25_0855_codex-to-claude_live-notepad-success-speed-followup.md` +- `docs/coordination/inbox_claude/2026-05-25_0904_codex-to-claude_handoff-session-fraiche.md` + +Claude doit produire son handoff miroir dans `docs/handoffs/`. + +## Arbitrage courant + +Ordre recommande pour prochaine session : + +1. Lire ce handoff puis lire le handoff Claude miroir. +2. Verifier `git status` et ne rien revert. +3. Figer le succes live par un test offline minimal sur `runtime_dialog_handled_post_verify`. +4. Traiter performance/dispatch : + - ajouter les tests WP4 critiques ; + - factoriser seulement apres tests ; + - mesurer build/dispatch/resolve. +5. Ensuite seulement brancher transport inerte `SceneExpected` / `Precondition` cote serveur, flag OFF par defaut. +6. Ne pas activer de blocage runtime `scene_expected.required=true` tant que les tests et le chemin Windows reel ne sont pas clarifies. + +Ce qu'il ne faut pas faire au redemarrage : + +- Pas de resync massif `deploy/windows_client` avant verification. +- Pas de refactor large `api_stream.report_action_result`. +- Pas de reveil `autonomous_planner`, `ORALoop`, `LearningManager`. +- Pas de nouveau replay live avant d'avoir decide si on mesure performance ou si on teste une correction. +- Pas de commit composite melangeant runtime, docs et experiences. + +## Git / etat worktree + +Worktree sale attendu. Ne pas nettoyer automatiquement. + +Modifies techniques connus : + +- `agent_v0/agent_v1/core/executor.py` +- `agent_v0/agent_v1/core/grounding.py` +- `agent_v0/server_v1/api_stream.py` +- `agent_v0/server_v1/replay_engine.py` +- `tests/integration/test_replay_session_trim_neutral.py` +- `tests/unit/test_env_setup.py` +- `tests/unit/test_executor_verify_window_guard.py` +- `tests/unit/test_grounding_engine.py` + +Nombreux docs non suivis dans `docs/`, `docs/coordination/`, `docs/recherche/`, `docs/plans/`, `docs/architecture/`. + +Ne pas les supprimer ; ce sont des artefacts de coordination et recherche. + +## Commandes utiles + +Lister replays actifs : + +```bash +TOKEN=$(grep '^RPA_API_TOKEN=' .env.local | cut -d= -f2- | tr -d "'\"") +curl -sS -H "Authorization: Bearer $TOKEN" \ + http://localhost:5005/api/v1/traces/stream/replays | + python3 -c "import sys,json; d=json.load(sys.stdin); active=[r for r in d.get('replays', []) if r.get('status') in ('running','paused_need_help','busy')]; print('active=', len(active)); [print(r.get('replay_id'), r.get('status'), r.get('completed_actions'), '/', r.get('total_actions')) for r in active]" +``` + +Logs live Notepad : + +```bash +journalctl --user -u rpa-streaming --since '2026-05-25 08:50:00' --no-pager | + rg "e96e5822|runtime_dialog|Confirmer|REPORT|VERIFY|dispatch_orphan|termine" +``` + +Tests unitaires executor : + +```bash +.venv/bin/python -m pytest -q tests/unit/test_executor_verify_window_guard.py +``` + +## Resume court pour reprise + +Le live Bloc-notes a reussi. Le dialogue `Confirmer l'enregistrement` est absorbe sans humain, ce qui valide la correction fenetre active/dialogue connu. Les deux vrais chantiers suivants sont maintenant la fiabilite du dispatch et la vitesse. Le modele cognitif reste : objectif explicite, scene attendue, precondition, action, retour. Ne pas repartir dans une rustine de clic ; transformer le succes en test, puis stabiliser la chaine. diff --git a/docs/handoffs/2026-05-26_handoff_codex_demo_aiva_urgence_repetition_humaine.md b/docs/handoffs/2026-05-26_handoff_codex_demo_aiva_urgence_repetition_humaine.md new file mode 100644 index 000000000..3e8896a4a --- /dev/null +++ b/docs/handoffs/2026-05-26_handoff_codex_demo_aiva_urgence_repetition_humaine.md @@ -0,0 +1,262 @@ +# Handoff Codex — Demo Aiva-urgence v2 / repetition humaine + +- `Auteur`: Codex +- `Date`: 2026-05-26, fin de journee +- `Reprise visee`: 2026-05-27 +- `Contexte`: preparation demo client Aiva-vision / Aiva-urgence, repetition avec Dom comme humain challenge + +## 1. Decision produit du jour + +Produit clarifie : + +- `Aiva-vision` est le socle universel qui apprend les interfaces. +- `Léa` est le collaborateur agent qui observe, lit, agit, demande et apprend. +- `Aiva-urgence` est le plugin metier de la demo sante. + +Principe Dom acte : + +- qualite d'abord ; +- le client sait que nous sommes en POC ; +- en contexte hospitalier, un arret propre vaut mieux qu'une action fausse ; +- Léa doit savoir dire "je ne sais pas" ou "montrez-moi". + +## 2. Scenario demo retenu + +Scenario cible : + +1. Léa lit la liste des passages aux urgences. +2. Elle decrit le tableau sans enumerer fragilement tous les IPP. +3. Elle propose : traiter tous les dossiers ou un dossier precis. +4. Dom choisit `MOREL Catherine / IPP 25003284`. +5. Léa ouvre le dossier et verifie le bandeau : `25003284`, `MOREL`, `Catherine`. +6. Léa collecte les 5 onglets : + - `Motif d'admission` + - `Examens cliniques` + - `Imagerie` + - `Notes medicales` + - `Synthese Urgences` +7. `Synthese Urgences` reste dans le perimetre live : lecture haut + bas, avec validation par marqueurs. +8. Léa demande ou consigner les informations. +9. Sortie prioritaire : tableur OnlyOffice visible. +10. Validation finale humaine obligatoire. + +## 3. Scroll : arbitrage corrige + +Dom a rappele que VWB n'avait pas eu de probleme de scroll. + +Verification code : + +- VWB et replay utilisent le contrat `extract_text_scroll` : + - OCR haut ; + - `ctrl+end` ; + - wait ; + - OCR bas ; + - concat ; + - retour `ctrl+home`. + +Correction d'arbitrage : + +- ne plus retirer `Synthese Urgences` par prudence abstraite ; +- cible live = 5 onglets ; +- degrade 4 onglets uniquement si echec concret non recupere. + +Marqueurs obligatoires bas synthese : + +- `CCMU` +- `GEMSA` +- `J12.1` +- `Consultation externe` + +Principe qualite : + +> Scroll reussi = geste envoye + changement visuel constate + donnees attendues relues. + +Docs : + +- `docs/coordination/active/2026-05-26_arbitrage-scroll-vwb-reference.md` +- `docs/coordination/active/2026-05-26_principe-apprentissage-scroll-securise.md` + +## 4. Repetition humaine du 2026-05-27 + +Dom sera "l'humain challenge". + +Il peut : + +- interrompre ; +- refuser une lecture ; +- demander une preuve ; +- demander une reprise depuis un onglet ; +- demander OnlyOffice ; +- verifier que Léa ne conclut pas sans validation. + +Regle orale Claude a retenir : + +> Une reponse Léa doit toujours contenir une preuve, une question ou un arret. Jamais une affirmation seule. + +Docs a relire avant repetition : + +- `docs/coordination/active/2026-05-26_runbook-repetition-humain-challenge-demo-v2.md` +- `docs/coordination/inbox_codex/2026-05-26_2230_claude-to-codex_SCRIPT-oral-lea-humain-challenge.md` +- `docs/coordination/active/2026-05-26_etat-preparation-repetition-2026-05-27.md` + +## 5. OCR IPP/chiffres + +Benchmark local : + +- Tesseract lit les 11 IPP exacts sur `landing_wide.png`. +- EasyOCR reste bon pour le texte continu, mais moins fiable sur les IPP secondaires. +- Preprocessing OpenCV global rejete : regressions marqueurs et latence. +- docTR utile pour structure/bboxes, pas meilleur que Tesseract pour les chiffres. + +Patch implemente : + +- `core/llm/ocr_extractor.py` + - ajout `extract_digits_tesseract_from_image(...)` ; + - ajout `engine` sur `extract_table_from_image(...)`. +- `core/llm/__init__.py` + - export de la nouvelle fonction. +- `agent_v0/server_v1/replay_engine.py` + - `extract_table` transmet maintenant `engine` ; + - le normaliseur accepte `variable_name` pour `extract_table`. +- `tests/unit/test_ocr_extractor_tesseract.py` + - tests unitaires nouveaux. +- `tests/integration/test_t2a_extract.py` + - test integration `variable_name` + `engine="tesseract"`. + +Workflow raccorde : + +- BDD : `visual_workflow_builder/backend/instance/workflows.db` +- Workflow : `Demo_urgence_3_db` +- ID : `wf_483910cdd851_1778750587` +- Step : `step_79c40f5a8342_1778750587` +- Modification : `parameters.engine = "tesseract"` sur `extract_table`. + +Backup BDD : + +- `visual_workflow_builder/backend/instance/workflows.db.backup_2026-05-26_ocr_tesseract_demo3` + +Doc : + +- `docs/coordination/active/2026-05-26_patch-ocr-tesseract-ipp.md` + +## 6. Verifications passees + +Tests : + +```bash +pytest -q tests/unit/test_ocr_extractor_tesseract.py tests/integration/test_t2a_extract.py +``` + +Resultat : + +- OK +- 40 tests passes + +Compile : + +```bash +python3 -m compileall -q core/llm/ocr_extractor.py core/llm/__init__.py agent_v0/server_v1/replay_engine.py tests/unit/test_ocr_extractor_tesseract.py tests/integration/test_t2a_extract.py +``` + +Resultat : OK. + +Verification capture : + +```bash +python3 -c "from core.llm.ocr_extractor import extract_digits_tesseract_from_image; print(extract_digits_tesseract_from_image('output/playwright/easily_dryrun_2026-05-26/landing_wide.png', pattern=r'^25\\d{6}$'))" +``` + +Resultat : 11/11 IPP exacts. + +Etat services au dernier check : + +- `http://127.0.0.1:8765/` accessible. +- `http://127.0.0.1:5005/health` OK. +- `http://127.0.0.1:5004/api/status` OK. +- OnlyOffice : `/snap/bin/onlyoffice-desktopeditors`. +- Tesseract : `/usr/bin/tesseract`, langues `eng`, `fra`, `osd`. + +## 7. Coordination + +Qwen : + +- a valide le principe scroll securise ; +- a valide l'architecture OCR multi-moteur par zone ; +- a recu les infos de patch et raccord workflow. + +Claude : + +- a retracte la recommandation 4 onglets ; +- cible 5 onglets confirmee ; +- a livre le script oral "humain challenge". + +Derniers messages lus : + +- `docs/coordination/inbox_codex/2026-05-26_2149_qwen-to-codex_ACK-apprentissage-scroll-securise.md` +- `docs/coordination/inbox_codex/2026-05-26_2215_claude-to-codex_ACK-scroll-vwb-reformulation-discours.md` +- `docs/coordination/inbox_codex/2026-05-26_2230_claude-to-codex_SCRIPT-oral-lea-humain-challenge.md` + +## 8. Points d'attention demain + +1. Commencer par lire les nouveaux retours dans `docs/coordination/inbox_codex/`. +2. Verifier les services avant repetition : + - maquette `8765` ; + - streaming `5005` ; + - agent chat `5004` ; + - OnlyOffice ; + - Tesseract. +3. Rejouer le scenario avec Dom comme challengeur, pas comme assistant de demo. +4. Logger les moments ou Léa : + - cite une preuve ; + - demande confirmation ; + - s'arrete ; + - reprend apres correction. +5. Verifier que `Synthese Urgences` est tentee en 5e onglet. +6. Si scroll incomplet : + - ne pas exploiter l'onglet ; + - annoncer la limite ; + - demander confirmation humaine. +7. Si divergence OCR sur IPP : + - ne pas trancher ; + - demander confirmation. +8. OnlyOffice doit etre visible avant validation finale. + +## 9. Worktree + +Le worktree est deja tres modifie par plusieurs chantiers/collaborateurs. + +Ne pas revert les changements non lies. + +Changements Codex de cette fin de journee a surveiller : + +- `core/llm/ocr_extractor.py` +- `core/llm/__init__.py` +- `agent_v0/server_v1/replay_engine.py` +- `tests/unit/test_ocr_extractor_tesseract.py` +- `tests/integration/test_t2a_extract.py` +- `visual_workflow_builder/backend/instance/workflows.db` +- `visual_workflow_builder/backend/instance/workflows.db.backup_2026-05-26_ocr_tesseract_demo3` +- docs sous `docs/coordination/active/` et inbox Claude/Qwen crees le 2026-05-26 soir. + +Attention : `agent_v0/server_v1/replay_engine.py` contenait deja d'autres modifications hors patch OCR dans le diff global. Ne pas les attribuer automatiquement au patch OCR. + +## 10. Premiere action conseillee demain + +Lire : + +```bash +find docs/coordination/inbox_codex -maxdepth 1 -type f -printf '%TY-%Tm-%Td %TH:%TM %f\n' | sort | tail -20 +``` + +Puis lancer un smoke : + +```bash +curl -fsS http://127.0.0.1:8765/ >/tmp/aiva_easily_health.html +curl -fsS http://127.0.0.1:5005/health +curl -fsS http://127.0.0.1:5004/api/status +pytest -q tests/unit/test_ocr_extractor_tesseract.py tests/integration/test_t2a_extract.py +``` + +Ensuite repetition humaine avec Dom. + +Auteur : Codex diff --git a/docs/handoffs/2026-05-27_handoff_codex_micro_apprentissage_lea_p0_reprise_2026-05-28.md b/docs/handoffs/2026-05-27_handoff_codex_micro_apprentissage_lea_p0_reprise_2026-05-28.md new file mode 100644 index 000000000..d98b15a07 --- /dev/null +++ b/docs/handoffs/2026-05-27_handoff_codex_micro_apprentissage_lea_p0_reprise_2026-05-28.md @@ -0,0 +1,141 @@ +# Handoff Codex - Reprise micro-apprentissage Lea P0 + +Date: 2026-05-27 21:35 +Reprise prevue: 2026-05-28 matin +Pilote: Codex +Participants: Dom, Qwen, Claude + +## Etat de fin de soiree + +La demo metier / VWB n'est plus l'axe principal immediat. Le travail est recentre sur le coeur d'apprentissage de Lea. + +Decision centrale: + +- Unite de travail: **competence courte verifiee**. +- Cette competence est une couche de classification / promotion au-dessus de la chaine existante Graph / FAISS / Shadow / Replay / Memoire. +- On ne cree pas une nouvelle chaine d'apprentissage. + +Chaine existante a reutiliser: + +- `core/pipeline/workflow_pipeline.py` +- `core/graph/graph_builder.py` +- `core/graph/node_matcher.py` +- `core/embedding/faiss_manager.py` +- `core/embedding/state_embedding_builder.py` +- `core/workflow/shadow_observer.py` +- `core/workflow/shadow_validator.py` +- `core/learning/target_memory_store.py` +- `agent_v0/server_v1/replay_memory.py` +- `agent_v0/server_v1/replay_learner.py` +- `agent_v0/server_v1/replay_verifier.py` +- `tools/session_cleaner.py` +- `agent_chat/gesture_catalog.py` +- `core/knowledge/ui_patterns.py` + +## Fait aujourd'hui + +- Capture clavier systeme corrigee pour `Win+S` / `key_combo`. +- Consolidation preserve maintenant `key_combo`. +- Session cleaner corrige: plus d'affichage ``. +- Health technique OK: RAM/VRAM/swap/Ollama acceptables; seul risque residuel = VLM cold start. +- Dashboard Base de connaissances identifie comme point d'inventaire: + - 13 666 vecteurs FAISS, + - 63 sessions, + - 29 workflows, + - 28 reflexes natifs, + - 3 machines. +- Qwen et Claude ont ete remis dans la boucle via leurs inbox fichiers. + +## Retours agents lus + +Claude: + +- `docs/coordination/inbox_codex/2026-05-27_1959_claude-to-codex_CONTRAT-competence-courte-verifiee-P0.md` +- `docs/coordination/inbox_codex/2026-05-27_2040_claude-to-codex_PLAN-P1-contrat-p0-message-warning.md` +- `docs/coordination/inbox_codex/2026-05-27_2123_claude-to-codex_ACK-correction-session-wins-existe.md` + +Qwen: + +- `docs/coordination/inbox_codex/2026-05-27_2010_qwen-to-codex_AVIS-corrige-reuse-lea-core-complete.md` +- `docs/coordination/inbox_codex/2026-05-27_2044_qwen-to-codex_INVENTAIRE-offline-competences-existantes.md` +- `docs/coordination/inbox_codex/2026-05-27_2055_qwen-to-codex_INVENTAIRE-offline-competences-existantes.md` + +Correction importante: + +- Le retour Qwen 20:55 disait que `sess_20260527T185155_98ad9a` n'existait pas. +- Verification Codex: elle existe bien. +- Corrections envoyees: + - `docs/coordination/inbox_qwen/2026-05-27_2122_codex-to-qwen_CORRECTION-session-wins-existe.md` + - `docs/coordination/inbox_claude/2026-05-27_2122_codex-to-claude_CORRECTION-session-wins-existe.md` + +## Session P0 confirmee + +Ne pas recapturer Win+S pour P0. + +Chemins confirmes: + +- `data/training/live_sessions/streaming_sessions/sess_20260527T185155_98ad9a.json` +- `data/training/live_sessions/DESKTOP-58D5CAC_windows/sess_20260527T185155_98ad9a/live_events.jsonl` +- `data/training/live_sessions/DESKTOP-58D5CAC_windows/sess_20260527T185155_98ad9a/shots/` + +Objectif P0: + +- extraire le segment propre: etat initial -> `Win+S` -> `Rechercher/SearchHost.exe`, +- couper les evenements apres postcondition, +- ignorer les clics systray/pythonw, +- creer `data/competences/observed/open_windows_search.yaml`, +- verifier sans promouvoir `stable`. + +## Objectifs demain matin + +1. Figer le segment propre P0. +2. Creer le premier YAML `open_windows_search.yaml` en `observed` ou `candidate`. +3. Ajouter un validateur leger du schema competence, sans runtime lourd. +4. Brancher `message_contract.py` en mode warning sur le premier chemin de pause. +5. Lancer les tests de non-regression: + - capture clavier / key_combo, + - session cleaner, + - message contract, + - validateur YAML si ajoute. +6. Rejouer supervise seulement si Dom donne GO runtime. + +## Regles a ne pas casser + +- Pas de coordonnees comme savoir durable. +- Pas de nouvelle chaine parallele. +- Pas de promotion stable depuis dashboard / FAISS seul. +- Pas d'inference methode depuis postcondition seule. +- `SearchHost.exe` prouve l'etat, pas la methode. +- Messages Lea visibles: toujours dire intention, attendu, vu, demande. +- VLM en fallback seulement si fenetre/process/OCR simple ne suffisent pas. + +## Repartition demain + +Codex: + +- implementation minimale, +- tests, +- verification disque/runtime, +- integration des retours Qwen/Claude. + +Qwen: + +- finaliser l'inventaire offline exploitable, +- corriger son inventaire avec la session P0 confirmee, +- identifier les prochains candidats sans nouvelle capture. + +Claude: + +- verrouiller le contrat YAML minimal, +- guider l'integration `message_contract.py` en warning, +- verifier les invariants de promotion. + +## Premiere action recommande demain + +Commencer par une lecture rapide de ce handoff, puis ouvrir: + +- `docs/coordination/inbox_codex/2026-05-27_2040_claude-to-codex_PLAN-P1-contrat-p0-message-warning.md` +- `docs/coordination/inbox_codex/2026-05-27_2044_qwen-to-codex_INVENTAIRE-offline-competences-existantes.md` +- `data/training/live_sessions/streaming_sessions/sess_20260527T185155_98ad9a.json` + +Ensuite seulement: patch minimal. diff --git a/docs/handoffs/2026-06-01_handoff_codex_fin-soir-poc-bi-turbo.md b/docs/handoffs/2026-06-01_handoff_codex_fin-soir-poc-bi-turbo.md new file mode 100644 index 000000000..70c771d41 --- /dev/null +++ b/docs/handoffs/2026-06-01_handoff_codex_fin-soir-poc-bi-turbo.md @@ -0,0 +1,468 @@ +# Handoff Codex - fin de soiree 2026-06-01 - reprise POC bi-turbo + +Date: 2026-06-01 soir, Europe/Paris. +Auteur: Codex. +Statut: handoff de reprise. Aucun commit. + +Ce handoff complete et remplace pour la reprise le handoff plus ancien : +`docs/handoffs/2026-06-01_handoff_codex_p0-p1-lea-session-propre.md`. + +## Message court pour la prochaine session + +Dom arrete ce soir pour se reposer. Demain, reprise "bi-turbo", mais l'objectif reste court terme : + +- POC presentable/testable rapidement. +- Consolider ce qui existe. +- Arreter de recoder des briques deja implementees. +- Utiliser Claude et Qwen activement. +- Codex ne code pas seul. +- Qwen reste quality gate anti-doublon. +- Claude execute les correctifs courts. +- Codex orchestre et valide. + +## Decisions Dom a ne pas perdre + +1. Ne pas repartir sur VWB comme produit. +2. POC court terme d'abord : capture humaine fiable, replay precis, apprentissage consolide, test humain. +3. L'audit global ne doit pas devenir une phase longue. Il sert a aller plus vite. +4. Regle anti-reimplementation globale projet, pas seulement apprentissage : + - chercher l'existant, + - raccorder/consolider si possible, + - remplacer seulement si l'existant est clairement insuffisant. +5. Apprentissage Lea : + - pas de validation exhaustive de longues sessions, + - auto-evaluation par repetition, + - convergence sur repetitions, + - questions uniquement en mode copilote / prise de main / apprentissage supervise, + - aucune question pendant observation passive. +6. Les humains sont imparfaits : + - hesitations, + - erreurs de clic, + - retours arriere, + - corrections, + - pauses, + - bruit ecran. + Lea doit distinguer action utile, parasite, correction, hesitation. +7. Confidentialite DPI : + - donnees DPI exploitables localement sur DGX / hopital car necessaires pour trancher, + - aucune information ne sort du milieu hospitalier, + - artefacts portables sans memoire patient, captures DPI brutes, identifiants patients, + - attention au risque de memorisation par modele/adapters/embeddings si export naif. +8. Portabilite importante : + - exporter/importer les reflexes, competences, schemas, detecteurs, mappings, plans d'action, metriques, + - reduire duree d'apprentissage sur nouvelles machines, + - ne pas exporter la memoire patient. +9. Ne pas laisser de "a surveiller plus tard" flou : + - si un risque peut couter 10x plus tard, mettre maintenant un garde-fou minimal, + - acceptable POC seulement si le risque est contenu et documente. + +## Etat coordination au moment du handoff + +Derniers messages importants a lire dans cet ordre demain : + +1. `docs/coordination/inbox_codex/2026-06-01_qwen-to-codex-claude_LEVEE-GO-P1-SEMANTIQUE.md` + - Qwen confirme GO P1-SEMANTIQUE, conditionnel leve. +2. `docs/coordination/inbox_codex/2026-06-01_2240_claude-to-codex_LEVEE-GO-conditionnel-Qwen-P1-SEMANTIQUE.md` + - Claude livre executor + timeout OmniParser + tests. +3. `docs/coordination/inbox_codex/2026-06-01_qwen-to-codex_REVUE-GLOBALE-ANTI-DOUBLON-REBRANCHEMENT.md` + - Qwen confirme R6 worker queue, FAISS, graph, learning orphelins, plan P0/P1/P2. +4. `docs/coordination/inbox_codex/2026-06-01_2200_claude-to-codex_ACK-5-decisions-Dom-+-audit-chaine-apprentissage-debranchee.md` + - Claude confirme chaine apprentissage partiellement debranchee. +5. `docs/POC/AUDIT_CHAINE_APPRENTISSAGE_2026-06-01.md` + - Audit detaille de la chaine apprentissage. +6. `docs/coordination/inbox_claude/2026-06-01_2222_codex-to-claude_GO-EXECUTION-POC-court-terme-R6-semantique-learning.md` + - GO Claude sur P0/P1 court terme. +7. `docs/coordination/inbox_qwen/2026-06-01_2222_codex-to-qwen_ACK-verdict-global-et-role-quality-gate.md` + - Qwen confirme comme quality gate attendu. + +Important : apres le dernier recadrage Dom sur "a surveiller plus tard", Qwen a confirme que P1-SEMANTIQUE contient maintenant les 5 garde-fous demandes. + +## Statut par lot + +### P0 revocation / agent registry + +Statut: probablement OK POC, non commite. + +Travail applique dans le worktree : + +- `agent_v0/server_v1/agent_registry.py` +- `agent_v0/server_v1/api_stream.py` +- tests P0 autour revocation/replay/enroll. + +Qwen a livre et confirme : + +- `/register`, `/stream/event`, `/replay/next`, `/replay/result`, `/finalize`, `/replay-session` gardes/testes. +- Strict default/unknown quand registry non vide. + +Tests annonces/constates plus tot : + +- `30 passed, 1 xfailed`, puis Qwen a annonce `32 passed, 1 xfailed`. + +Attention : + +- `api_stream.py` avait deja un tres gros diff avant Codex/Qwen/Claude. +- Toute modification dans ce fichier doit etre chirurgicale. + +### P1-PERSIST + +Statut: GO conditionnel POC, pas prod. + +Claude a livre : + +- `core/competences/persist.py` +- endpoint `/api/v1/lea/competences/candidate/persist` en fin de `api_stream.py` +- tests unit/integration/security. + +Qwen a classe : + +- acceptable POC, +- prod a durcir. + +Risques/restes : + +- couplage token <-> machine_id pas implemente, +- rate limiter in-memory, +- idempotence 201 vs 200, +- audit failure rollback YAML recommande. + +### P1-LEA-SHADOW + +Statut: GO Qwen. + +Claude a livre : + +- `agent_chat/handlers/learn_action.py` +- `agent_chat/handlers/__init__.py` +- `agent_chat/state/` +- modifications `agent_chat/app.py` +- route `POST /api/learn/start` +- rebranchement bouton Windows vers `/api/learn/start` +- `agent_v0/agent_v1/network/lea_orchestrator_client.py` +- modifications UI `chat_window.py`, `smart_tray.py`. + +Qwen avait mis NO-GO puis a leve : + +- `machine_id` ajoute dans `SessionState` et payload persist, +- `datetime.now(timezone.utc)`, +- guard anti-CONFIRM sans nom, +- route `/api/learn/start`. + +Point important Dom : + +- questions seulement en prise de main / supervision, +- pas pendant observation passive. + +### P1-SEMANTIQUE + +Statut: GO Qwen confirme, conditionnel leve. + +Claude a livre puis corrige : + +- `core/semantic/phase25_analyzer.py` +- `core/semantic/__init__.py` +- endpoint `/api/v1/lea/screen/analyze` dans `api_stream.py` +- tests `tests/unit/test_phase25_semantic.py` +- tests `tests/integration/test_phase25_semantic_integration.py` + +Qwen a d'abord mis GO conditionnel : + +- `analyze_frames` synchrone dans endpoint async, +- timeout OmniParser declare mais pas applique. + +Claude a corrige : + +- `run_in_executor` dans endpoint, +- timeout effectif OmniParser via `ThreadPoolExecutor(max_workers=2)`, +- fallback docTR, +- logs `logs/omniparser_errors.log`, +- tests annonces `35 passed / 0 failed`. + +Qwen a confirme les garde-fous Dom : + +- concurrence bornee, +- comportement si pool sature, +- fallback degrade, +- log explicite, +- tests/preuves. + +Risque residuel accepte POC : + +- un thread timeoutte ne peut pas etre tue en Python, +- contenu par pool borne + timeout + fallback + logs. + +Codex n'a pas encore rerun localement les tests apres le dernier patch Claude. A faire demain si besoin : + +```bash +.venv/bin/python -m py_compile core/semantic/phase25_analyzer.py agent_v0/server_v1/api_stream.py +.venv/bin/python -m pytest tests/unit/test_phase25_semantic.py tests/integration/test_phase25_semantic_integration.py -q +``` + +Note : les tests timeout peuvent durer environ 30s. + +### R6 worker queue / enrichissement profond + +Statut: P0 court terme, bloqueur POC si non resolu. + +Qwen confirme : + +- `data/training/_worker_queue.txt` existe, 0 octets, cree le 29 mai. +- Worker actif mais tourne a vide. +- Dernier traitement worker il y a plusieurs jours. +- Artefacts enriched/FAISS/graph stales. +- Sessions live recentes presentes : + - `sess_20260529T144652_5a2e96` + - `sess_20260529T154427_f95956` + +Codex a confirme que ces dossiers existent : + +- `data/training/live_sessions/DESKTOP-58D5CAC_windows/sess_20260529T144652_5a2e96` +- `data/training/live_sessions/DESKTOP-58D5CAC_windows/sess_20260529T154427_f95956` +- `data/training/_worker_queue.txt` = 0 octets. + +Cause probable Qwen : + +- `_enqueue_to_worker()` echoue silencieusement ou n'ecrit pas vraiment, +- logs serveur non accessibles, +- queue creee mais write absent. + +GO donne a Claude a 22:22 : + +1. Ajouter log critique avec `exc_info=True` dans `_enqueue_to_worker`. +2. Re-enqueue les deux sessions du 29 mai si elles existent. +3. Prouver queue -> worker -> artefacts ou logs exploitables. + +Pas encore de livraison Claude R6 au moment du handoff. + +Demain, premiere action : + +- lire nouveaux messages Claude/Qwen, +- si pas de livraison R6, relancer Claude sur R6, +- ne pas partir sur autre chantier avant d'avoir statue R6. + +### Existing learning modules + +Statut: a rebrancher apres R6, pas a recoder. + +Modules existants cites par Claude/Qwen : + +- `core/learning/continuous_learner.py` +- `core/learning/feedback_processor.py` +- `core/learning/target_memory_store.py` +- `core/learning/versioned_store.py` +- `core/learning/learning_manager.py` +- `core/training/training_data_collector.py` +- `core/training/offline_trainer.py` +- `core/training/model_validator.py` +- `core/training/session_analyzer.py` +- `core/healing/learning_repository.py` + +Verdict Qwen : + +- `ContinuousLearner`: existant a rebrancher, couvre auto-evaluation par repetition. +- `FeedbackProcessor`: existant a rebrancher, couvre fusion/regroupement feedback. +- `target_memory_store` / `versioned_store`: existants a rebrancher. +- certains modules comme `learning_manager` / `quality_validator` sont deja branches ailleurs. + +Ordre conseille apres R6 : + +1. verifier signatures reelles, +2. hook minimal apres session enrichie, +3. hook feedback shadow -> FeedbackProcessor, +4. seulement ensuite enrichir payloads `confidence`, `uncertainties[]`, `repetition_count`, statuts `hypothesis/candidate/validated`. + +### FAISS + +Statut: actif partiellement, a consolider court terme uniquement si impact POC clair. + +Qwen : + +- FAISS actif localement dans plusieurs branches runtime. +- FAISS alimente par `reindex()`, `add_embedding()`, import pack. +- GlobalFAISS jamais consulte. +- save/load existent mais jamais appeles : index perdus au redemarrage. +- `replay_memory.py` n'utilise pas FAISS. + +Decision court terme : + +- FAISS persistence minimale = P1 demain si faible risque. +- GlobalFAISS consultation = P2 apres tests humains. +- Ne pas refondre FAISS maintenant. + +Composants a garder en tete : + +- `core/embedding/faiss_manager.py` +- `core/federation/faiss_global.py` +- `core/embedding/clip_embedder.py` +- `core/embedding/state_embedding_builder.py` +- `core/visual/visual_embedding_manager.py` +- `agent_v0/server_v1/replay_memory.py` +- tests `tests/unit/test_faiss_*`, `tests/unit/test_learning_pack.py`. + +### Graph + +Statut: actif, mais a reconnecter mieux avec Phase25 plus tard. + +Qwen : + +- graphe produit apres sessions via `finalize_session()` -> `GraphBuilder` -> `workflows/*.json`. +- utilise par WorkflowRunner, NodeMatcher, ExecutionLoop. +- compat apprentissage par repetition via DBSCAN / LearningState. +- NO-GO partiel sur distinction semantique action / hesitation / correction. +- P1-SEMANTIQUE ne doit pas devenir un format parallele long terme : a reconnecter au graphe. + +Decision court terme : + +- pas de refonte graphe maintenant. +- R6 d'abord pour que sessions repassent par enrichissement. +- Phase25 -> graphe = P1/P2 apres R6 si faible risque. + +## Worktree / hygiene + +Le worktree est tres sale. Ne pas revert ce que l'on n'a pas fait. + +Commande observee : + +```bash +git status --short +``` + +Points a retenir : + +- `docs/POC/PREREQUIS_DSI_DGX_SPARK_2026-06-01.docx` est modifie par Dom : ne pas toucher. +- `api_stream.py` a un tres gros diff avec zones Qwen + Claude + modifications anterieures. +- Beaucoup de fichiers modifies/untracked predatent la session. +- Pas de commit sans GO explicite Dom. +- Si commit demain, probablement split logique : + 1. P0 revocation Qwen, + 2. P1 persist Claude, + 3. P1 shadow + learn/start + bouton Windows, + 4. P1 semantic, + 5. R6 worker queue, + mais seulement apres revue et validation. + +## Commandes utiles demain + +Lire les derniers messages : + +```bash +find docs/coordination/inbox_codex -maxdepth 1 -type f -newermt '2026-06-01 22:20:00' -printf '%T@ %TY-%Tm-%Td %TH:%TM %p\n' | sort -nr | head -80 +find docs/coordination/inbox_codex -maxdepth 1 -type f -iname '*claude*' -printf '%T@ %TY-%Tm-%Td %TH:%TM %p\n' | sort -nr | head -30 +find docs/coordination/inbox_codex -maxdepth 1 -type f -iname '*qwen*' -printf '%T@ %TY-%Tm-%Td %TH:%TM %p\n' | sort -nr | head -30 +``` + +Verifier R6 fichiers/queue : + +```bash +ls -l data/training/_worker_queue.txt +find data/training/live_sessions -maxdepth 3 -type d \( -name 'sess_20260529T144652_5a2e96' -o -name 'sess_20260529T154427_f95956' \) -print +rg -n "def _enqueue_to_worker|_worker_queue|enqueue" agent_v0/server_v1/api_stream.py agent_v0/server_v1/run_worker.py agent_v0/server_v1/stream_processor.py +``` + +Verifier P1-SEMANTIQUE localement : + +```bash +.venv/bin/python -m py_compile core/semantic/phase25_analyzer.py agent_v0/server_v1/api_stream.py +.venv/bin/python -m pytest tests/unit/test_phase25_semantic.py tests/integration/test_phase25_semantic_integration.py -q +``` + +Tests P0 revocation a rerun si besoin : + +```bash +.venv/bin/python -m py_compile agent_v0/server_v1/api_stream.py agent_v0/server_v1/agent_registry.py +.venv/bin/python -m pytest tests/unit/test_api_stream_revocation_gaps.py tests/integration/test_replay_single_inflight.py tests/integration/test_agents_enroll_api.py -q +``` + +## Plan de reprise recommande demain + +### Etape 0 - Lire coordination + +1. Lire ce handoff. +2. Lire les derniers `inbox_codex` apres 22:20. +3. Si Claude a livre R6, lire sa livraison. +4. Si Qwen a donne un nouveau verdict, le prioriser. + +### Etape 1 - Clore P1-SEMANTIQUE + +Statut deja GO Qwen. + +Action Codex : + +- rerun tests cibles si temps, +- si tests OK, marquer P1-SEMANTIQUE OK POC. + +Ne pas ajouter de nouveau correctif sauf echec test reel. + +### Etape 2 - R6 worker queue + +Priorite P0. + +Si Claude a livre : + +1. Lire livraison. +2. Demander ou lire revue Qwen. +3. Verifier localement : + - queue ecrite, + - worker depile, + - artefacts/logs, + - pas d'effet de bord. + +Si Claude n'a pas livre : + +- relancer Claude avec le GO 22:22. +- garder Qwen en copie/review. + +Definition of done R6 : + +- une session test finalisee passe par worker, +- preuve disque ou logs exploitables, +- cause exacte documentee, +- pas de silencieux. + +### Etape 3 - Rebrancher l'existant learning + +Uniquement apres R6 prouve. + +Ordre : + +1. `ContinuousLearner` hook minimal apres session enrichie. +2. `FeedbackProcessor` hook minimal depuis feedback shadow. +3. `VersionedStore` / `TargetMemoryStore` si deja requis par les hooks. +4. Tests integration courts. + +Ne pas creer nouveau module learning. + +### Etape 4 - FAISS persistence minimale + +Seulement si R6 OK et impact POC clair. + +Objectif : + +- save/load appele au bon moment, +- index pas perdu au redemarrage, +- tests existants adaptes si necessaire. + +Ne pas traiter GlobalFAISS multi-postes maintenant. + +### Etape 5 - Test humain E2E + +Quand R6 + P1 green : + +- utiliser `docs/demo/test-humain-e2e-poc.md` si present, +- verifier capture -> hypotheses -> persist -> replay -> verdict, +- ne pas chercher la quasi-prod avant d'avoir un cycle humain reel. + +## Points a ne pas oublier + +- Dom veut aller vite, mais solide. +- Dom est inquiet que l'on recode deja fait : toujours chercher l'existant. +- Dom veut Claude et Qwen utilises. +- Ne pas coder seul sans coordination. +- Les YAML sont checkpoints transitoires, pas memoire finale. +- Portabilite reste objectif majeur mais ne doit pas bloquer le POC sauf risque DPI. +- Donnees DPI utiles localement doivent rester exploitables ; ne pas anonymiser au point de perdre la capacite de trancher. +- Artefacts exportables doivent etre nettoyes/pseudonymises/sans memoire patient. +- Les tests humains manquent cruellement : ne pas repousser indefiniment. + +## Dernier statut en une ligne + +P1-LEA-SHADOW et P1-SEMANTIQUE sont GO Qwen pour POC ; le prochain vrai P0 est R6 worker queue/enrichissement profond, puis rebrancher ContinuousLearner/FeedbackProcessor existants sans recoder. diff --git a/docs/handoffs/2026-06-01_handoff_codex_p0-p1-lea-session-propre.md b/docs/handoffs/2026-06-01_handoff_codex_p0-p1-lea-session-propre.md new file mode 100644 index 000000000..87b301aa8 --- /dev/null +++ b/docs/handoffs/2026-06-01_handoff_codex_p0-p1-lea-session-propre.md @@ -0,0 +1,128 @@ +# Handoff Codex — P0/P1 Léa, session propre + +- `Date`: 2026-06-01 18:15 Europe/Paris +- `Contexte`: Dom demande qualité top, pas de régression, travail coordonné avec Claude/Qwen et agents. +- `Contrat produit`: Léa apprend par démonstration depuis Léa/agent-chat. Dashboard = admin/supervision/QA/promotion. VWB = outil admin/récupération uniquement. + +## Décisions actives + +| Sujet | Décision | +|---|---| +| Apprentissage | Départ depuis Léa (`agent-chat`, bouton/tray existant), pas bouton dashboard | +| Artefact durable | YAML `candidate/`, pas `stable` sans promotion admin | +| Runtime | Shadow existe mais est orphelin ; à raccorder | +| Lecture sémantique | Remarques Claude retenues : OmniParser runtime, Phase 2.5, agents externes, OCR qualité | +| Démo/POC | Pas de CLI opérateur ; CLI seulement dev/test | +| VLM | Pas de mock VLM en démo/POC | + +## Messages lus + +| Auteur | Fichier | Résumé | +|---|---|---| +| Qwen | `docs/coordination/inbox_codex/2026-06-01_qwen-to-codex_DIAGNOSTIC-P0-SINGLE-INFLIGHT.md` | Root cause P0 confirmé : early return `paused_need_help` renvoyait `status: ok` | +| Claude | `docs/coordination/inbox_codex/2026-06-01_1745_claude-to-codex_ADDENDUM-archi-Lea-lecture-semantique-agent-externe.md` | Ajout essentiel : lecture sémantique, OmniParser runtime, `ExternalDecisionClient`, OCR qualité | +| Qwen | `docs/coordination/inbox_codex/2026-06-01_qwen-to-codex_SYNTHESE-Q1-Q4-AGENTS-PARALLELES.md` | Shadow orphelin, `persist` absent, révocation non effective, micro-warnings | + +## Dispatchs déposés + +| Destinataire | Fichier | +|---|---| +| Claude | `docs/coordination/inbox_claude/2026-06-01_1812_codex-to-claude_GO-MAX-AGENTS-P0-P1-lea-quality-no-regression.md` | +| Qwen | `docs/coordination/inbox_qwen/2026-06-01_1812_codex-to-qwen_GO-MAX-AGENTS-P0-P1-lea-quality-no-regression.md` | + +## Changements locaux effectués + +### P0 replay single-inflight + +Fichier touché : `agent_v0/server_v1/api_stream.py` + +Changement Codex : dans la branche `paused_need_help` quand la queue est vide avant fin, l'early return renvoie maintenant : + +```python +{ + "status": "recorded", + "replay_status": replay_state["status"], + "pause_reason": "paused_need_help", +} +``` + +Important : `api_stream.py` avait déjà un énorme diff local avant ce changement. Ne pas revert. Le changement Codex est uniquement ce retour P0. + +### Warnings dashboard Tester + +Fichiers touchés par agent interne `Huygens` : + +- `web_dashboard/templates/knowledge_base.html` +- `tests/unit/test_dashboard_routes.py` + +Changements : + +- Confirmation avant lancement d'une compétence qui ressemble à Win+R / Exécuter. +- Blocage/alerte du verdict `Valide` si aucune `step_results` ni evidence exploitable. +- Test HTML ciblé ajouté. + +## Tests exécutés + +| Commande | Résultat | +|---|---| +| `.venv/bin/python -m py_compile agent_v0/server_v1/api_stream.py` | OK | +| `.venv/bin/python -m pytest tests/integration/test_replay_single_inflight.py::test_concurrent_dispatch_and_result_no_double_increment -q` | OK | +| `.venv/bin/python -m pytest tests/integration/test_replay_single_inflight.py -q` | `10 passed, 1 xfailed` | +| `.venv/bin/python -m pytest tests/unit/test_dashboard_routes.py -q` | `30 passed` | +| `.venv/bin/python -m pytest tests/integration/test_replay_watchdog.py tests/integration/test_replay_resume_preserves_original_action.py::TestReplayResumePreservesOriginalAction::test_resume_dispatch_backfills_retry_pending_for_watchdog -q` | `11 passed` | +| `.venv/bin/python -m pytest tests/unit/test_dashboard_routes.py tests/unit/test_competence_verdicts.py tests/unit/test_competence_promotions.py tests/unit/test_competence_to_vwb_preview.py tests/unit/test_competence_catalog_loader.py tests/unit/test_vwb_supervised_pause_runtime.py tests/unit/test_lea_competence_verdict_api.py tests/integration/test_replay_single_inflight.py -q` | `75 passed, 1 xfailed` | +| `git diff --check -- agent_v0/server_v1/api_stream.py web_dashboard/templates/knowledge_base.html tests/unit/test_dashboard_routes.py ...` | OK | + +Warnings attendus : `RequestsDependencyWarning` et `pynvml FutureWarning`. + +## Agents internes Codex + +| Agent | ID | Statut | Résumé | +|---|---|---|---| +| Lorentz | `019e83f4-6f94-77f2-aab4-82395ca62562` | Terminé | Confirme P0 Qwen ; risque faible ; follow-up : harmoniser payload complet de l'early return | +| Huygens | `019e83f4-b4b4-76b1-a9bc-49a3ec7b93fc` | Terminé | Patch warnings dashboard appliqué ; tests ciblés OK | +| Descartes | `019e83f4-85db-7a73-ba37-6b68938dd725` | Terminé | Révocation runtime non effective ; `/replay/next` public ; `touch_last_seen()` mort ; ré-enrollment admin_revoke à bloquer | +| Plato | `019e83f4-9b9f-7b63-904e-25befda0354a` | Terminé | Confirme faisabilité, mais recommande MVP prudent : Phase 2.5 post-apprentissage, pas OmniParser partout dans le hot path replay | + +## Résultat agent Plato — architecture sémantique + +Constats : + +- OmniParser existe (`core/detection/omniparser_adapter.py`) mais reste fragile : chemin absolu, fallback souvent silencieux. +- ScreenAnalyzer existe et est conceptuellement propre, mais le streaming court-circuite l'initialisation lourde en mode léger. +- `t2a_decision` est réel et utilisable comme premier agent métier interne. +- `ExternalDecisionClient` n'existe pas encore. +- Confiance OCR actuelle insuffisante pour autonomie métier : certaines confiances sont approximatives. +- Des bypass `static_result` / `static_text` existent et doivent être interdits en démo/POC hors tests. + +MVP recommandé : + +1. Garder le P0 replay/click/OCR existant comme chemin principal. +2. Ajouter une Phase 2.5 post-apprentissage uniquement : snapshots sémantiques `{screen_id, window_title, screenshot_path, elements[]}`. +3. Demander à l'humain seulement les ambiguïtés utiles. +4. Brancher les résultats comme contexte, pas comme prérequis de chaque clic replay. +5. Démarrer `ExternalDecisionClient` autour de `t2a_decision`, puis adapter HTTP AIVA. +6. Prioriser OCR critique par régions annotées + escalade humaine si confiance basse. + +## Prochains P0/P1 + +| Priorité | Sujet | Action recommandée | +|---|---|---| +| P0 | Revue/commit P0 replay + warnings | Relire diff final, décider commit | +| P0 POC | Révocation effective minimale | Ajouter garde registre côté serveur, retirer `/replay/next` des publics, appeler `touch_last_seen`, empêcher ré-enroll `admin_revoke` | +| P1 | `candidate/persist` | Créer endpoint `POST /api/v1/lea/competences/candidate/persist` + tests YAML | +| P1 | `agent-chat` -> Shadow | Raccorder bouton/chat Léa au cycle `start/stop/understanding/feedback/build/persist` | +| P1 | Lecture sémantique | D'abord Phase 2.5 post-apprentissage + snapshots, puis adapter OmniParser runtime, puis `ExternalDecisionClient` | +| P1 | Worker VLM | Vérifier/remettre green avant test humain sérieux | + +## Points de vigilance + +- Worktree très sale : ne pas revert les changements non identifiés. +- `docs/POC/PREREQUIS_DSI_DGX_SPARK_2026-06-01.docx` a été modifié par Dom : ne pas toucher. +- `api_stream.py` contient des changements préexistants massifs ; isoler les futurs patches. +- Token global : limitation encore forte. Le patch révocation minimale ne protège pas contre spoof d'un autre `machine_id` actif avec token global. +- Ne pas mettre OmniParser/VLM dans le hot path replay sans mesure perf/VRAM. +- Interdire les bypass `static_result` / `static_text` dans les workflows démo/POC. +- Session propre recommandée maintenant : oui, après lecture de ce handoff. + +— Codex diff --git a/docs/handoffs/2026-06-01_handoff_qwen_fin_session_reprise_2026-06-02.md b/docs/handoffs/2026-06-01_handoff_qwen_fin_session_reprise_2026-06-02.md new file mode 100644 index 000000000..685065474 --- /dev/null +++ b/docs/handoffs/2026-06-01_handoff_qwen_fin_session_reprise_2026-06-02.md @@ -0,0 +1,160 @@ +# HANDOFF Qwen — Fin de session 2026-06-01 + +- `Auteur`: Qwen +- `Date`: 2026-06-01 ~23:00 Europe/Paris +- `Prochaine reprise`: 2026-06-02 matin (bi-turbo) +- `Dom`: malade, en repos + +--- + +## Bilan de la journée + +**19 messages Codex lus, 12 ACK/revues déposés, 5 agents parallèles lancés, 1 revue globale 5 missions.** + +| Lot | Statut | Verdict Qwen | +|-----|--------|-------------| +| **P0 révocation** | ✅ Livré + testé (32 passed, 1 xfail) | GO | +| **P0 single-inflight** | ✅ Patch `paused_need_help` `"ok"` → `"recorded"` | GO | +| **P1-PERSIST** | ✅ Livré par Claude (39 tests) | GO conditionnel (token↔machine_id reporté) | +| **P1-LEA-SHADOW** | ✅ Livré + corrigé NO-GO (62 tests) | GO — NO-GO levé | +| **P1-SEMANTIQUE** | ✅ Livré + corrigé GO conditionnel (35 tests) | GO — conditionnel levé | +| **Rebranchement bouton Windows** | ✅ Livré (9 tests) | GO | +| **Dashboard test competence** | ✅ Livré | GO | +| **C-alpha/B/gamma** | ✅ Livrés (chaîne complète) | GO | +| **Audit anti-doublon global** | ✅ 5 missions, 4 agents | Verdict déposé | +| **Plan tests humains E2E** | ✅ `docs/demo/test-humain-e2e-poc.md` | Prêt | +| **Guide test humain batch 1** | ✅ `docs/demo/test-humain-batch1.md` | Prêt | + +--- + +## État des lieux — ce qui est fait + +### Chaîne complète (de bout en bout) + +``` +Bouton Windows 🎓 → /api/learn/start → Shadow start → observation humaine + → Shadow stop → understanding (Option C) → feedback → build → persist + → YAML candidate → Dashboard "Tester" → replay supervisé → verdict + → promotion dry-run → write-back YAML → audit trail +``` + +### P0 sécurité + +- 6 endpoints runtime protégés par `_guard_agent_registry_access` +- Révocation effective (agent uninstalled → 403) +- `touch_last_seen()` actif +- `/replay/result` garde inconditionnel +- `/replay-session` garde ajouté + +### Audit anti-doublon + +**9 modules orphelins identifiés**, tous fonctionnels, aucun doublon significatif avec P1 du jour : + +| Module | Rôle | Statut | +|--------|------|--------| +| `ContinuousLearner` (644 lignes) | Auto-évaluation par répétition, drift | À rebrancher P1 | +| `FeedbackProcessor` (176 lignes) | Feedback humain → ajustement | À rebrancher P1 | +| `VersionedStore` (520 lignes) | Snapshot/rollback learning | À rebrancher P1 | +| `TargetMemoryStore` (460 lignes) | Mémoire cibles UI | À rebrancher P1 | +| `learning_manager.py` (180 lignes) | Transitions états workflow | **Déjà rebranché** | +| `quality_validator.py` (460 lignes) | Validation qualité | **Déjà rebranché** | +| + 5 modules `core/training/` et `core/healing/` | Training, analyse, recovery | À rebrancher P1/P2 | + +### R6 worker queue — CONFIRMÉ + +- `_worker_queue.txt` vide depuis le 29 mai +- Worker actif (PID 4054092) mais ne traite rien depuis 5 jours +- 2 sessions finalisées non traitées +- Correctif : logger.critical dans `_enqueue_to_worker` + ré-enqueue manuel + +### FAISS / Graphe + +- FAISS : actif dans 4 branches runtime mais **persistance cassée** (save/load jamais appelés) +- Graphe : GO actif, produit après sessions, utilisé par replay, compatible répétition +- **Pas de doublon** entre graphe et P1-SEMANTIQUE, mais **pas de connexion** non plus + +--- + +## État des lieux — ce qui reste à faire + +### P0 — Immédiat (demain matin) + +| Action | Fichier | Effort | +|--------|---------|--------| +| R6: logger `_enqueue_to_worker` + ré-enqueue sessions | `api_stream.py` | ~10 lignes | +| P1-SEMANTIQUE: le conditionnel est levé ✅ | — | Fait | + +### P1 — Après P0 + +| Action | Effort | Impact | +|--------|--------|--------| +| Rebrancher ContinuousLearner | ~30-50 lignes | Auto-évaluation par répétition (décision Dom) | +| Rebrancher FeedbackProcessor | ~20 lignes | Hook dans learn_action.py | +| FAISS persistance (save/load) | ~20 lignes | Index perdus au redémarrage | +| Connecter Phase25 au graphe | ~30-50 lignes | Enrichir nodes, pas format parallèle | + +### P2 — Après tests humains + +| Action | Impact | +|--------|--------| +| Classification edges graphe (intention) | Distinguer action/hésitation/correction | +| GlobalFAISS consultation | Multi-postes | +| Confidence + repetition_count payload | Décision Dom auto-évaluation | +| Portabilité learning pack | Export DGX | + +### Test humain E2E — PRÊT + +- `docs/demo/test-humain-e2e-poc.md` — protocole 9 phases +- `docs/demo/test-humain-batch1.md` — guide 3 compétences +- Critère GO : ≥ 2/3 compétences batch 1 passent le cycle complet + +--- + +## Décisions Dom actées (à respecter) + +1. **Observation passive sans question** — Léa n'interrompt pas l'humain pendant l'observation +2. **Questions faible confiance seulement en prise de main** — copilote/supervisé uniquement +3. **Auto-évaluation par répétition** — Léa apprend à force d'observer/rejouer, pas par restitution lourde +4. **Données DPI exploitables localement** — DGX local, pas de sortie hors site +5. **Artefacts portables sans mémoire patient** — export réflexes/compétences, pas de captures brutes +6. **Noyau compétence stable + adaptateurs UI versionnés** — pas de "désapprentissage" +7. **Anti-doublon global** — chercher l'existant avant de coder, rebrancher avant de remplacer +8. **Pas de "surveiller plus tard" sans garde-fou** — contenir les risques maintenant + +--- + +## Règles coordination + +- **Qwen** = quality gate anti-doublon + revue bloquante +- **Claude** = exécution terrain des correctifs +- **Codex** = coordination + validation locale +- Inbox pattern : `docs/coordination/inbox_X/` pour messages, `syntheses/` pour rapports +- Réponses courtes et actionnables + +--- + +## Points de vigilance pour demain + +1. **Dom est malade** — il revient en bi-turbo mais peut être fatigué. Privilégier les tests humains courts. +2. **R6 est le seul bloquant technique** — sans worker, pas d'enrichissement FAISS/graph. Le correctif est trivial (~10 lignes). +3. **Pas de nouveau dev avant rebranchement** — Dom a été clair : on ne code pas de nouvelles features avant d'avoir rebranché l'existant. +4. **Test humain avant tout** — le protocole est prêt. Le plus important est de lancer un test réel avec Dom opérateur. +5. **P1-SEMANTIQUE GO** — le conditionnel est levé, mais c'est post-R6 en priorité. + +--- + +## Fichiers clés à connaître + +| Fichier | Rôle | +|---------|------| +| `docs/demo/test-humain-e2e-poc.md` | Protocole test humain E2E | +| `docs/demo/test-humain-batch1.md` | Guide 3 compétences batch 1 | +| `docs/POC/AUDIT_CHAINE_APPRENTISSAGE_2026-06-01.md` | Audit chaîne apprentissage (Claude) | +| `inbox_codex/...REVUE-GLOBALE-ANTI-DOUBLON-REBRANCHEMENT.md` | Revue globale Qwen 5 missions | +| `data/competences/candidate/*.yaml` | 6 compétences candidates | +| `data/competence_verdicts/verdicts.jsonl` | Verdicts supervisés | +| `data/training/_worker_queue.txt` | Queue worker (vide — à ré-enqueue) | + +--- + +*Auteur : Qwen — Bonne nuit Dom, repose-toi bien !* diff --git a/docs/handoffs/PROMPT_REPRISE_CLAUDE_2026-05-23.md b/docs/handoffs/PROMPT_REPRISE_CLAUDE_2026-05-23.md new file mode 100644 index 000000000..38a469e62 --- /dev/null +++ b/docs/handoffs/PROMPT_REPRISE_CLAUDE_2026-05-23.md @@ -0,0 +1,16 @@ +Lis `docs/coordination/README.md` puis traite tous les fichiers avec `Statut: open` dans `docs/coordination/inbox_claude/`. + +Priorité actuelle : +- `docs/coordination/inbox_claude/2026-05-23_1024_codex-to-claude_notepad-saveas-explorer-drift.md` + +Contraintes : +- répondre dans `docs/coordination/inbox_codex/` +- respecter la convention de nommage +- ne pas modifier le fichier source +- patch minimal et tests si tu implémentes +- ne pas mélanger ce bug avec le handler popup overwrite déjà traité + +Point de contexte utile : +- le grounding visuel agent a déjà été durci et validé +- le blocage courant n’est plus la popup `Oui/Non` +- la cause à analyser est la dérive après clic `Enregistrer sous` vers `rpa_vision : Explorateur de fichiers` diff --git a/docs/handoffs/PROMPT_REPRISE_CLAUDE_2026-05-25_SOIR.md b/docs/handoffs/PROMPT_REPRISE_CLAUDE_2026-05-25_SOIR.md new file mode 100644 index 000000000..1dd4acc44 --- /dev/null +++ b/docs/handoffs/PROMPT_REPRISE_CLAUDE_2026-05-25_SOIR.md @@ -0,0 +1,56 @@ +# Handoff Claude — 2026-05-25 soir + +## État validé + +- Smoke live Bloc-notes `replay_sess_1c0bfb42` : 16/16, 0 pause, score 0.94, temps moyen 4793 ms. +- Lot tests : `88 passed, 1 xfailed` (race concurrent polls documentée). +- VRAM agent_chat : 0 (libéré par C1+C1b+C1c+C1d). +- Build skip : 271 ms mesuré Codex (vs 91s historique). +- Profil démo actif sur Linux : `RPA_SKIP_INTENTION_ENRICHMENT=true`, `RPA_SKIP_BUILD_VISION=true`, `RPA_EASYOCR_GPU=0`, `AGENT_CHAT_ENABLE_OWL=0`, `AGENT_CHAT_ENABLE_UI_DETECTION=0`. + +## Décisions actées + +- Démo cible **Easily** (pas Bloc-notes) — Dom referra la capture à la main. +- D5-v2 = API préparatoire (qwen3.5 JSON), non consommée runtime. +- D5-v3a mini-fix : `num_ctx=4096` posé sur 3 sites bbox `resolve_engine.py`. +- D5-v3b helper bbox : reporté post-démo. +- D5-v3c Windows `num_ctx=8192` : NOGO avant freeze (fallbacks non triggered en smoke). +- C-P1 OCR tolérance préfixe : patch posé (`_text_match_fuzzy` + 14 tests). +- C-P3 bulle Léa : OK pour démo sans patch. + +## Travaux en attente + +- Capture réelle Easily par Dom selon protocole `inbox_codex/2026-05-25_1735_claude-to-codex_protocole-capture-easily-lea.md` (6 arbitrages à trancher avec Codex). +- Codex doit ACK : rapport unifié C-P1/C-P2/C-P3 (1720) + protocole Easily (1735). +- Commit/freeze du lot stabilisation par Codex (pas par Claude). +- Smoke équivalent Easily à faire (R7 ouvert). + +## Risques bloquants seulement + +- **R1** `RPA_GROUNDING_MODEL` ambigu (D5-v2 qwen3.5 vs legacy qwen2.5vl bbox) — NE PAS set globalement. D5-v3b résout post-démo. +- **R7** pas de smoke Easily équivalent (démo cible Easily). À faire avant freeze. +- Worktree large/sale — Codex groupera commits avant freeze. + +## Fichiers importants + +- `docs/coordination/active/2026-05-25_runbook-profil-demo-smoke.md` — runbook démo +- `docs/coordination/active/2026-05-25_etat-courant.md` — file active +- `docs/coordination/syntheses/2026-05-25_synthese-direction.md` — synthèse jour +- `docs/coordination/inbox_codex/2026-05-25_1720_claude-to-codex_REPONSE-taches-projet-ocr-d5v3c-lea.md` — C-P1/C-P2/C-P3 +- `docs/coordination/inbox_codex/2026-05-25_1735_claude-to-codex_protocole-capture-easily-lea.md` — C-P4 Easily + +## Prochaine action recommandée + +Attendre ACK Codex sur C-P1+C-P2+C-P3 (rapport 1720) et sur le protocole Easily (1735). Ensuite : assister Dom pour la capture Easily selon protocole. + +## Ce qu'il ne faut pas faire demain + +- NE PAS set `RPA_GROUNDING_MODEL=qwen3.5:9b` globalement (casse les 3 sites bbox legacy). +- NE PAS toucher `agent_v0/agent_v1/core/executor.py` (Windows, reporté D5-v3c post-démo). +- NE PAS redéployer Windows sans GO Codex. +- NE PAS lancer de live replay sans GO Dom explicite. +- NE PAS commit (Codex groupe). +- NE PAS rouvrir D5-v3b helper bbox tant que lot perf/stabilité pas figé. +- NE PAS mettre de secret en clair dans `docs/coordination/` (placeholder). +- NE PAS copier mécaniquement le YAML `urgence_aiva_demo_expected.yaml` comme scénario (Dom recapture à la main). +- NE PAS spawner d'agent pour brainstorming ciblé (overkill, déjà cadré matin). diff --git a/docs/handoffs/PROMPT_REPRISE_CLAUDE_2026-05-26_SOIR.md b/docs/handoffs/PROMPT_REPRISE_CLAUDE_2026-05-26_SOIR.md new file mode 100644 index 000000000..1080d99a3 --- /dev/null +++ b/docs/handoffs/PROMPT_REPRISE_CLAUDE_2026-05-26_SOIR.md @@ -0,0 +1,105 @@ +# Handoff Claude — 2026-05-26 soir + +## Rôle réassigné aujourd'hui + +Dom m'a réassigné le rôle de **chef de projet** (priorisation + pilotage Codex), avec arbitrages stratégiques validés par Dom. Pas exécutant supervisé. + +Feedback durable sauvé en mémoire : `feedback_depose_par_defaut.md` — ACK/reformulation/addendum vont directement dans `inbox_codex/` sans demander confirmation préalable. + +## Pivot majeur de la journée + +Démo Paris **du 21 mai → annulée/reportée**. Nouvelle démo cible **2026-06-01** (J-5 au matin du 27/05). + +Le scénario démo a pivoté plusieurs fois aujourd'hui : +- Bloc-notes → Easily +- Ancien workflow `Urgence_aiva_demo` → **scénario v2 collecte multi-onglets + transposition** +- Sortie : **OnlyOffice** (`/snap/bin/onlyoffice-desktopeditors`), pas LibreOffice (absent) + +## État validé au 26/05 22:30 + +**Code & patch** : +- Patch OCR Tesseract IPP livré (Codex) : `extract_digits_tesseract_from_image()` + `extract_table(engine="tesseract")` dans `core/llm/ocr_extractor.py`. 40 tests OK. **11/11 IPP exacts** sur `landing_wide.png` (vs 8/11 EasyOCR brut). +- Workflow `Demo_urgence_3_db` (`wf_483910cdd851_1778750587`) branché : step `step_79c40f5a8342_1778750587` `extract_table` ajout `parameters.engine = "tesseract"`. Sauvegarde DB : `workflows.db.backup_2026-05-26_ocr_tesseract_demo3`. +- Healthcheck Linux OK (Codex matin) : `rpa-streaming` + `rpa-agent-chat` actives, profil démo systemd OK, ollama ps vide, baseline `api_stream` ~1.1 Go VRAM noté. +- Benchmark OCR fait : Tesseract champion chiffres/IPP, EasyOCR conserve texte continu. docTR utile pour bboxes/zonage si besoin futur. Preprocessing OpenCV global = régression, **pas activé par défaut**. +- Dry-run Easily v2 fait (Codex) : MOREL Catherine / IPP 25003284 lisible. Synthèse Urgences nécessite scroll obligatoire (CCMU/GEMSA/J12.1 en bas). + +**Infra démo prête** : +- Maquette Easily : http://127.0.0.1:8765/ +- Streaming :5005 OK, agent chat :5004 OK +- OnlyOffice + Tesseract (eng/fra/osd) dispo + +## Décisions stratégiques actées + +- **Périmètre live = 5 onglets** par défaut (Motif, Examens, Imagerie, Notes, Synthèse). Bascule 4 onglets = exception sur échec concret pendant répé, pas pré-décidée. +- **Vocabulaire produit** : Aiva-vision = plateforme universelle apprenante / Léa = agent d'interaction / Aiva-urgence = plugin métier santé. +- **Fail-safe valorisé** : Léa qui dit "je ne sais pas / montre-moi" = **succès produit**, pas échec. NOGO = actions dangereuses, pas demandes de confirmation. +- **Scroll = compétence apprise** : geste + changement visuel + marqueurs. Pas un raccourci fixe. +- **Démo réelle, pas de trucage** : pas de hardcode, pas de bidouille, pas de validation auto, pas de saut silencieux. +- **PII levée** : c'est une maquette, tout est fictif. MOREL Catherine / 25003284 OK. + +## Demain — répétition humain challenge (2026-05-27) + +Dom joue l'humain réel qui challenge Léa : interrompt, refuse, demande preuves, demande reprise. + +Runbook : `docs/coordination/active/2026-05-26_runbook-repetition-humain-challenge-demo-v2.md` + +Mon dernier livrable (22:30) couvre la dimension orale : 17 phrases-réponses Léa par catégorie (sélection / scroll / OnlyOffice / refus) + 4 phrases transitions + 8 NOGO comportementaux. Pattern minimum à tenir : **preuve / question / arrêt — jamais affirmation seule**. + +## Mes 7 livrables déposés aujourd'hui (inbox_codex/) + +1. `0920_claude-to-codex_reprise-session-plan-j6-demo.md` — reprise session +2. `1030_claude-to-codex_CHECKLIST-easily-capture-trace.md` — checklist capture +3. `2130_claude-to-codex_DEMO-v2-script-failsafe-onlyoffice.md` — script démo v2 +4. `2145_claude-to-codex_ADDENDUM-demo-v2-dryrun-integration.md` — addendum dry-run +5. `2155_claude-to-codex_ACK-arbitrage-onglets-bascule-discours.md` — ACK 5/4/3 onglets +6. `2215_claude-to-codex_ACK-scroll-vwb-reformulation-discours.md` — ACK scroll VWB +7. `2230_claude-to-codex_SCRIPT-oral-lea-humain-challenge.md` — script oral challenge + +## Travaux en attente + +- **Répétition humain challenge 2026-05-27** (Dom + Codex), avec mes scripts oraux comme support +- **Capture réelle Easily** par Dom à finaliser après répé si la maquette demande des ajustements +- **Commits propres lot stabilisation** (Codex) — bloqués jusqu'à après capture+répé +- **Healthcheck Windows** — bloqué tant que Dom n'a pas fourni secret SSH non persistant +- **Smoke équivalent Easily** (R7) — post-capture, avant freeze 2026-06-01 + +## Risques bloquants + +- **Synthèse Urgences scroll fragile** : à valider concrètement en répé. Si marqueurs CCMU/GEMSA/J12.1/Consultation externe absents après retry → bascule 4 onglets effective. +- **Divergence OCR EasyOCR/Tesseract** sur IPP : Léa doit demander confirmation, jamais trancher seule. +- **Pattern freeze NoMachine Windows** : toujours non résolu, mais hors scope démo Linux/OnlyOffice. + +## Sources de vérité actives (ordre de lecture Codex) + +1. `active/2026-05-26_cadrage-produit-aiva-vision.md` +2. `active/2026-05-26_arbitrage-dom-demo-reelle-poc.md` +3. `active/2026-05-26_principe-dom-apprentissage-fail-safe.md` +4. `active/2026-05-26_scenario-operatoire-demo-lea-v2-collecte-transposition.md` +5. `active/2026-05-26_arbitrage-scroll-vwb-reference.md` +6. `active/2026-05-26_principe-apprentissage-scroll-securise.md` +7. `active/2026-05-26_arbitrage-sortie-transposition-onlyoffice.md` +8. `active/2026-05-26_benchmark-ocr-local-captures-easily.md` +9. `active/2026-05-26_patch-ocr-tesseract-ipp.md` +10. `active/2026-05-26_runbook-repetition-humain-challenge-demo-v2.md` +11. `active/2026-05-26_etat-preparation-repetition-2026-05-27.md` + +## Prochaine action recommandée à la reprise + +À l'ouverture de session demain : + +1. Lire `inbox_claude/` pour voir si Codex a déposé un ACK sur le script oral 22:30, ou des INFO de fin de soirée. +2. Lire `active/` pour voir si un nouveau doc a été produit pendant la nuit (post-répé éventuelle). +3. Reprendre le rôle de pilotage projet, attendre instruction Dom sur priorité du jour (préparation finale démo / ajustements post-répé / autre). + +## Ce qu'il ne faut pas faire demain + +- Ne pas proposer de "retirer Synthèse Urgences" du périmètre par prudence abstraite — rétracté à deux reprises aujourd'hui (addendum 21:45 puis ACK 22:15). Bascule 4 onglets = exception sur échec concret. +- Ne pas mentionner LibreOffice (absent côté Linux). +- Ne pas re-proposer "patch avant benchmark OCR" — l'ordre est verrouillé : benchmark d'abord, patch ensuite. +- Ne pas commiter (Codex groupe les commits). +- Ne pas lancer de live replay sans GO Dom explicite. +- Ne pas mettre de secret en clair dans `docs/coordination/`. +- Ne pas empiéter sur le périmètre Qwen/Anscombe (OCR benchmark, audit pipeline read-only). +- Ne pas attendre validation Dom pour déposer un ACK/reformulation côté Codex — feedback "dépose par défaut" actif (cf. `feedback_depose_par_defaut.md`). +- Ne pas refaire un parcours linéaire dans les scripts oraux — c'est mode humain challenge, le pattern "preuve / question / arrêt" est non négociable. diff --git a/docs/handoffs/PROMPT_REPRISE_CLAUDE_2026-05-28_MATIN.md b/docs/handoffs/PROMPT_REPRISE_CLAUDE_2026-05-28_MATIN.md new file mode 100644 index 000000000..c533d87ba --- /dev/null +++ b/docs/handoffs/PROMPT_REPRISE_CLAUDE_2026-05-28_MATIN.md @@ -0,0 +1,70 @@ +# Prompt de reprise Claude — 2026-05-28 matin + +> A coller (ou referencer) dans une nouvelle session Claude Code pour repartir propre. + +## Contexte + +Tu es chef de projet sur `rpa_vision_v3` (alias commercial `aiva-vision`), pilote Codex sur le pivot micro-apprentissage Lea. Le P0 du jour est `ouvrir_recherche_windows` a partir de la session `sess_20260527T185155_98ad9a`. + +Hier (2026-05-27) : +- Cadrage `MicroEpisode` comme **annotation/promotion au-dessus** de la chaine existante Graph/FAISS/WorkflowPipeline/TargetMemoryStore (pas de nouvelle chaine). +- Brique `message_contract.py` ecrite par Codex (35 tests verts). +- Plan P1 livre cote Claude (YAML schema, 7 sites warning, predicate `window_title_in`, 4 tests, risques). + +## A lire d'abord (ordre important) + +1. `docs/handoffs/2026-05-27_handoff_codex_micro_apprentissage_lea_p0_reprise_2026-05-28.md` — handoff Codex +2. `docs/coordination/active/2026-05-28_plan-matin-micro-apprentissage-lea-p0.md` — plan matin actif +3. `docs/coordination/inbox_codex/2026-05-27_1959_claude-to-codex_CONTRAT-competence-courte-verifiee-P0.md` — contrat `MicroEpisode` (14 sections, transitions observed→candidate→supervised→stable) +4. `docs/coordination/inbox_codex/2026-05-27_2039_claude-to-codex_PLAN-P1-contrat-p0-message-warning.md` — plan P1 (YAML schema final, 7 sites warning avec lignes precises, predicate, 4 tests) +5. `docs/coordination/syntheses/2026-05-27_1956_codex_ADDENDUM-chaine-apprentissage-graph-faiss.md` — recadrage Dom : ne pas reinventer la chaine + +## Priorites du jour (cf. PREP Codex 21:35) + +1. Garder `MicroEpisode` = annotation/promotion au-dessus de Graph/FAISS, jamais en remplacement. +2. Finaliser `data/competences/observed/open_windows_search.yaml` (schema dans le PLAN P1 §1). +3. Guider le branchement `message_contract.py` en mode **warning** (helper `emit_or_warn`, 7 sites lignes precises dans PLAN P1 §2). +4. Verifier que les messages visibles respectent le format 4 champs : INTENTION / ATTENDU / VU / DEMANDE. +5. Refuser toute derive boite a clic ou chaine parallele. + +## 5 decisions §13 a acter avec Dom + +1. Validateur YAML chez Codex ou Claude ? +2. Helper `emit_or_warn` dans `message_contract.py` ou nouveau module ? +3. Branchement warning : 1 site d'abord ou 6 en bloc ? +4. Demotion stable→supervised : N=2 ou N=3 echecs ? +5. Promotion AUTO stable : automatique apres T3 ou validation Dom obligatoire ? + +## Invariants + +- Pas de patch sans GO Dom matin. +- Pas de recapture Win+S (session P0 existe : 3 chemins confirmes). +- Pas de bypass Graph / FAISS / TargetMemoryStore. +- Pas de coordonnees codees en dur ; jamais de boite a clic. +- Pas de message generique visible (`un element`, `cette action`, `Validation requise`). +- Ne pas modifier `agent_v0/agent_v1/core/executor.py` ni `core/grounding.py`. +- Pas de live replay / restart service / redeploiement Windows sans GO. +- Pas de commit (Codex groupe les commits). +- Secrets : pas en clair dans `docs/coordination/` (placeholder OK). + +## Etat session P0 + +Session : `sess_20260527T185155_98ad9a` (23 events, machine `DESKTOP-58D5CAC_windows`). +Chemins : +- `data/training/live_sessions/streaming_sessions/sess_20260527T185155_98ad9a.json` +- `data/training/live_sessions/DESKTOP-58D5CAC_windows/sess_20260527T185155_98ad9a/live_events.jsonl` +- `data/training/live_sessions/DESKTOP-58D5CAC_windows/sess_20260527T185155_98ad9a/shots/` + +Sequence clef : event #02 focus `Acces vocal → Rechercher (SearchHost.exe)` precede event #03 `key_combo ["win","s"]` (effet release-only). Heartbeats #07/#11/#13 confirment `active='Rechercher'`. Parasites a filtrer : #00 (Acces vocal), #15-22 (systray + pythonw). + +Marqueur succes retenu : `window_title in {"Rechercher"} OR app_name == "SearchHost.exe"`. + +## Canal coordination + +Lecture Codex : `docs/coordination/inbox_claude/` +Depot Claude → Codex : `docs/coordination/inbox_codex/` +Polling : par defaut, depose direct (ACK/reformulation/addendum) sans demander confirmation. + +## Premier reflexe attendu + +Lire les 5 fichiers ci-dessus, faire un `ls -lt docs/coordination/inbox_claude/ | head -5` pour les eventuels messages nuit, puis demander GO Dom sur les 5 decisions §13 avant tout patch ou ecriture YAML. diff --git a/docs/handoffs/PROMPT_REPRISE_CODEX_2026-05-23.md b/docs/handoffs/PROMPT_REPRISE_CODEX_2026-05-23.md new file mode 100644 index 000000000..8dd25277f --- /dev/null +++ b/docs/handoffs/PROMPT_REPRISE_CODEX_2026-05-23.md @@ -0,0 +1,23 @@ +Lis d’abord `docs/handoffs/2026-05-23_handoff_codex_lea_replay_resume.md`, puis vérifie `docs/coordination/inbox_codex/` pour toute réponse Claude récente. + +Contexte à reprendre : +- projet `rpa_vision_v3` +- replay live courant : `replay_sess_595c4947` +- blocage actuel : dérive après clic sur l’onglet `Enregistrer sous` vers `rpa_vision : Explorateur de fichiers` +- le patch grounding visuel est déjà déployé sur Windows et actif +- le handler popup overwrite `Confirmer l'enregistrement -> Oui` est déjà codé mais pas encore exercé sur ce run +- problème infra séparé : GPU Linux non exploitable (`driver/library mismatch` NVIDIA) + +Objectif immédiat : +1. vérifier si la machine a redémarré et si Léa est revenue +2. si besoin, relancer un `replay-session` avec : + - `session_id=sess_20260520T102916_066851` + - `machine_id=DESKTOP-58D5CAC_windows` +3. continuer uniquement le diagnostic/fix sur la dérive `Enregistrer sous -> Explorateur` +4. lire et intégrer la réponse Claude si elle existe +5. ne pas repartir de zéro ni rouvrir les sujets déjà clos + +Rappels importants : +- utiliser `docs/coordination/` pour les briefs Claude +- ne pas mélanger le bug fonctionnel courant avec le problème infra GPU +- ne pas retirer le nouveau grounding visuel sans preuve contraire diff --git a/docs/handoffs/PROMPT_REPRISE_CODEX_2026-05-25.md b/docs/handoffs/PROMPT_REPRISE_CODEX_2026-05-25.md new file mode 100644 index 000000000..531c2ac21 --- /dev/null +++ b/docs/handoffs/PROMPT_REPRISE_CODEX_2026-05-25.md @@ -0,0 +1,19 @@ +Lis d'abord : + +1. `docs/handoffs/2026-05-25_handoff_codex_notepad_success_phase2.md` +2. le handoff Claude miroir du 2026-05-25 s'il existe dans `docs/handoffs/` +3. `docs/coordination/inbox_codex/` pour les messages Claude plus recents + +Contexte court : + +- Replay Bloc-notes live reussi : `replay_sess_e96e5822`, `18/18`, `0` echec. +- Point critique valide : `Confirmer l'enregistrement` traite sans aide humaine via `runtime_dialog_handled_post_verify`. +- Prochains chantiers : figer le succes par test offline, traiter vitesse/dispatch, clarifier chemin Windows reel, puis transport inerte `SceneExpected` / `Precondition`. + +Regles de reprise : + +- Ne pas faire de gros code sans deleguer a Claude. +- Ne pas resync `deploy/windows_client` avant verification du chemin reel execute par Lea. +- Ne pas relancer de replay live tant que l'objectif de test n'est pas clair. +- Utiliser `.venv/bin/python` pour les tests Python. +- Ne rien revert dans le worktree sale sans demande explicite Dom. diff --git a/docs/handoffs/PROMPT_REPRISE_CODEX_2026-05-25_SOIR.md b/docs/handoffs/PROMPT_REPRISE_CODEX_2026-05-25_SOIR.md new file mode 100644 index 000000000..422cc6c68 --- /dev/null +++ b/docs/handoffs/PROMPT_REPRISE_CODEX_2026-05-25_SOIR.md @@ -0,0 +1,140 @@ +# Handoff Codex — reprise soir 2026-05-25 + +- `Auteur`: Codex +- `Date`: 2026-05-25 soir Europe/Paris +- `Contexte`: stabilisation Lea / demo cible 2026-06-01 +- `Role Codex`: direction technique, arbitrage, integration, tests finaux + +## Etat valide + +1. Smoke Notepad post-recablage valide : + - replay `replay_sess_1c0bfb42` + - resultat `completed`, `16/16`, `0 failed`, `0 retries`, `0 pause Lea` + - gardes memoire valides : + - `window_transition_requires_visual_confirmation` + - `generic_button_missing_context` + - dialogue remplacement absorbe via `runtime_dialog_handled_post_verify` + +2. Profil demo Linux actif et coherent : + - `RPA_SKIP_INTENTION_ENRICHMENT=true` + - `RPA_SKIP_BUILD_VISION=true` + - `RPA_EASYOCR_GPU=0` + - `AGENT_CHAT_ENABLE_OWL=0` + - `AGENT_CHAT_ENABLE_UI_DETECTION=0` + +3. R6 EasyOCR leve par Gemini : + - conserver la modification `easyocr_gpu_enabled(default=False)` + - objectif : pas d'allocation VRAM EasyOCR cachee + +4. Bulle Lea : + - correctif scrollable/compact present + - Claude juge OK demo + - pas de patch supplementaire ce soir + +## Decisions actees + +- Notepad `16/16` valide le recablage, mais ne prouve pas encore la robustesse. Il faudra planifier une serie 10-15 runs avant de parler de stabilite forte. +- Pas de migration globale `qwen3.5` avant D5-v3b/D5-v3c. +- Runtime demo reste `qwen2.5vl` bbox legacy avec `num_ctx=4096`. +- `qwen3.5` = benchmark isole / API preparatoire JSON, pas runtime global. +- D5-v3c Windows `num_ctx=8192` = report post-demo. +- Easily avec Lea reel = chantier reporte apres-demain, pas demain. +- Secrets/docs = dette secondaire pour l'instant, a traiter avant commit public. + +## Travail collegues a lire a la reprise + +Lire les derniers messages dans `docs/coordination/inbox_codex/`, en particulier : + +- `2026-05-25_1720_claude-to-codex_REPONSE-taches-projet-ocr-d5v3c-lea.md` +- `2026-05-25_1735_claude-to-codex_protocole-capture-easily-lea.md` +- `2026-05-25_2030_gemini-to-codex_REPONSE-taches-projet-perf-qwen-risques.md` +- `2026-05-25_2045_gemini-to-codex_GRILLE-demo-intelligence-facilite.md` +- `2026-05-25_2100_gemini-to-codex_PROPOSITION-demo-metier-avancee.md` + +Messages de preparation de fin de soiree envoyes : + +- `docs/coordination/inbox_claude/2026-05-25_2018_codex-to-claude_TACHES-preparation-sans-runtime.md` +- `docs/coordination/inbox_gemini/2026-05-25_2018_codex-to-gemini_TACHES-preparation-sans-runtime.md` + +Dom demande aussi a Claude et Gemini de produire leurs handoffs soir. + +## Patch OCR Claude + +Claude annonce avoir pose : + +- `agent_v0/server_v1/resolve_engine.py` +- `tests/unit/test_text_match_fuzzy_prefix.py` + +Objet : accepter un OCR prefixe partiel, ex. `Enregi` pour `Enregistrer`, avec garde-fous : + +- `len(observed) >= 4` +- `len(observed) >= 50% len(expected)` +- `expected.startswith(observed)` + +A faire a la reprise : + +1. verifier le diff soi-meme ; +2. lancer le test cible ; +3. arbitrer `ACK/NACK` a Claude ; +4. decider si seuil 50% ou 60%. + +Commande cible : + +```bash +.venv/bin/python -m pytest tests/unit/test_text_match_fuzzy_prefix.py -q +``` + +## Prochaines actions recommandees + +Priorite courte : + +1. `git status --short` +2. Lire nouveaux handoffs Claude/Gemini s'ils existent. +3. Verifier patch OCR Claude + test cible. +4. Repondre ACK/NACK a Claude. +5. Planifier protocole smoke Notepad 10-15 runs, sans le lancer sans GO Dom. + +Priorite moyenne : + +1. Decoupage worktree en commits propres. +2. Ne pas committer docs avant scan/sanitisation minimal. +3. Preparer, plus tard, capture Easily avec Lea reelle. + +## A ne pas faire demain + +- Ne pas lancer chantier Easily. +- Ne pas migrer `qwen3.5` en global. +- Ne pas set `RPA_GROUNDING_MODEL=qwen3.5:9b`. +- Ne pas redeployer Windows pour D5-v3c. +- Ne pas lancer 10-15 smokes sans fenetre de controle et GO Dom. +- Ne pas faire de grand nettoyage worktree non supervise. +- Ne pas archiver/supprimer messages coordination sans synthese. + +## Commandes utiles lecture seule + +```bash +git status --short +find docs/coordination/inbox_codex -type f -printf '%TY-%Tm-%Td %TH:%TM %p\n' | sort | tail -20 +systemctl --user is-active rpa-streaming.service +curl -fsS http://127.0.0.1:5005/health +ollama ps +``` + +Pour les endpoints proteges, charger `.env.local` sans afficher le token : + +```bash +set -a; . ./.env.local; set +a +curl -fsS -H "Authorization: Bearer $RPA_API_TOKEN" http://127.0.0.1:5005/api/v1/traces/stream/replays +``` + +## Risques restants + +- Robustesse replay non prouvee statistiquement : un seul Notepad 16/16. +- Worktree tres sale, nombreux fichiers modifiés/non suivis. +- Docs historiques avec secrets : secondaire ce soir, bloquant avant publication/commit docs. +- Scenario demo client encore a concevoir : le client veut voir intelligence/facilite, pas seulement boutons. +- Capture Easily reelle devra utiliser patient fictif et eviter PII. + +## Dernier mot + +Le socle a enfin passe un vrai smoke propre. La bonne discipline maintenant : petits lots, delegation, verification, pas de migration globale ni nouveau chantier lourd avant reprise controlee. diff --git a/docs/handoffs/PROMPT_REPRISE_GEMINI_2026-05-25.md b/docs/handoffs/PROMPT_REPRISE_GEMINI_2026-05-25.md new file mode 100644 index 000000000..42dc6c2f3 --- /dev/null +++ b/docs/handoffs/PROMPT_REPRISE_GEMINI_2026-05-25.md @@ -0,0 +1,114 @@ +# Prompt reprise Gemini — 2026-05-25 + +Tu es Gemini, collègue de revue indépendante sur le projet Léa/RPA Vision. + +Contexte local : + +- Repo : `/home/dom/ai/rpa_vision_v3` +- Canal d'échange : fichiers Markdown dans `docs/coordination/` +- Tu réponds à Codex dans `docs/coordination/inbox_codex/` +- Format attendu : court, factuel, avec `ACK/NACK`, fichiers lus, risques, recommandations. + +## Rôle attendu + +Tu n'es pas le pilote principal. Ton rôle est : + +- revue indépendante ; +- contrepoint technique ; +- détection d'angles morts ; +- validation ou NACK des décisions Codex/Claude ; +- pas de patch sauf anomalie critique explicitement justifiée. + +Codex garde l'arbitrage. Claude exécute/analyse. Dom tranche les choix produit/demo. + +## À lire en priorité + +Lis dans cet ordre : + +1. `docs/coordination/README.md` +2. `docs/coordination/active/2026-05-25_etat-courant.md` +3. `docs/coordination/syntheses/2026-05-25_synthese-direction.md` +4. `docs/coordination/registre/2026-05-25_decisions.md` +5. `docs/coordination/registre/2026-05-25_arbitrages-runbook-demo.md` +6. `docs/coordination/active/2026-05-25_execution-profil-demo-linux.md` +7. `docs/coordination/active/2026-05-25_runbook-profil-demo-smoke.md` +8. `docs/coordination/index/2026-05-25_messages-cles.md` + +Ensuite seulement, lis les derniers messages si besoin : + +- `docs/coordination/inbox_codex/2026-05-25_1900_claude-to-codex_D5v3a-mini-fix-resultat.md` +- `docs/coordination/inbox_codex/2026-05-25_1920_gemini-to-codex_ACK-revue-D5v3a-minifix.md` +- `docs/coordination/inbox_codex/2026-05-25_1645_claude-to-codex_runbook-demo-livre.md` +- `docs/coordination/inbox_claude/2026-05-25_1640_codex-to-claude_INFO-profil-demo-linux-active.md` + +## État courant résumé + +La démo client est reportée au 2026-06-01. On stabilise proprement. + +Ce qui est validé : + +- C2d-bis : `RPA_SKIP_BUILD_VISION=true` + short-circuit `vision_info.text`. +- Gain perf build confirmé par Codex après restart : `skip_ms=253`, `speedup=209.7x`. +- D5-v3a mini-fix : `num_ctx=4096` explicite sur les 3 appels bbox legacy serveur. +- Agent_chat ne charge plus OWL/UI detection et n'occupe plus la VRAM. +- Profil démo Linux actif via drop-ins systemd. +- Smoke offline OK. + +Ce qui n'est PAS fait : + +- Pas de smoke live. +- Pas de healthcheck Windows avec secret. +- Pas de commit. +- Pas de D5-v3b. +- Pas de modification Windows executor. + +## Flags actifs côté Linux + +`rpa-streaming.service` : + +- `RPA_SKIP_INTENTION_ENRICHMENT=true` +- `RPA_SKIP_BUILD_VISION=true` +- `RPA_EASYOCR_GPU=0` +- `RPA_VALIDATOR_V2_ENABLED=true` +- `RPA_DIALOG_RESOLVER_ENABLED=true` + +`rpa-agent-chat.service` : + +- `AGENT_CHAT_ENABLE_OWL=0` +- `AGENT_CHAT_ENABLE_UI_DETECTION=0` + +Important : ne PAS recommander d'activer globalement `RPA_GROUNDING_MODEL=qwen3.5:9b`. +Raison : conflit avec les callers bbox legacy qui attendent qwen2.5vl/bbox_2d. + +## Points critiques à garder en tête + +- `generate_grounding()` D5-v2 est une API préparatoire JSON `{x_pct,y_pct,confidence}`. Elle n'est pas encore consommée par les callers runtime. +- Les chemins bbox legacy restent qwen2.5vl, avec `num_ctx=4096` explicite depuis D5-v3a. +- Le healthcheck Linux peut afficher WARN si `qwen2.5vl` n'est pas résident à froid ; ce WARN est acceptable avant smoke live. +- Le test perf peut réchauffer qwen2.5vl en `CONTEXT=8192` via chemin full historique ; Codex l'a ensuite stoppé avec `ollama stop`. +- Windows executor contient encore des `num_ctx=8192`, report D5-v3c. +- Le worktree est large et sale : pas de commit/freeze sans tri Codex. +- Ne jamais stocker de secret réel dans les docs ; utiliser placeholders. + +## Ce que Codex attend de toi maintenant + +Mission de reprise : + +1. ACK que tu as repris le contexte. +2. Vérifie que la structure de coordination est compréhensible. +3. Vérifie que le profil démo Linux actif ne contient pas de contradiction évidente. +4. Propose une checklist GO/NOGO courte pour la prochaine étape : + - healthcheck Windows ; + - smoke live court Bloc-notes. +5. Signale tout NACK bloquant avant smoke live. + +Contraintes : + +- Pas de patch code. +- Pas de restart service. +- Pas de live replay. +- Pas de D5-v3b. +- Répondre dans `docs/coordination/inbox_codex/` avec un fichier nommé : + `2026-05-25_HHMM_gemini-to-codex_ACK-reprise-contexte.md` + +Auteur du prompt : Codex diff --git a/docs/handoffs/PROMPT_REPRISE_GEMINI_2026-05-25_SOIR.md b/docs/handoffs/PROMPT_REPRISE_GEMINI_2026-05-25_SOIR.md new file mode 100644 index 000000000..d201f0ce9 --- /dev/null +++ b/docs/handoffs/PROMPT_REPRISE_GEMINI_2026-05-25_SOIR.md @@ -0,0 +1,33 @@ +# Handoff Gemini — 2026-05-25 SOIR + +## 1. État validé +- **Performance** : C2d-bis validé (build vision skip ~270ms, speedup ~210x). +- **Stabilisation VRAM** : D5-v3a (num_ctx=4096) colmaté sur les appels bbox legacy. +- **Profil Démo Linux** : Actif (drop-ins systemd), validé par smoke live Bloc-notes (`16/16`). + +## 2. Décisions actées +- **Scénario Démo** : Passage d'un "waouh visuel" à une démo "métier" (lecture tableaux, extraction champs, saisie). +- **Architecture** : Maintien strict de `qwen2.5vl` pour le runtime démo ; `qwen3.5` réservé au benchmark/audit. +- **R6 EasyOCR** : Validé et conservé (respect de `RPA_EASYOCR_GPU=0`). + +## 3. Travaux en attente +- **Configuration Easily** : Définir les `ExtractionSchema` (champs patients) pour le nouveau scénario métier. +- **Sanitisation Docs** : Nettoyage des secrets identifiés (G-P3). +- **Benchmark VLM** : Exécution du protocole isolé qwen2.5 vs qwen3.5. + +## 4. Risques bloquants seulement +- **Secrets Git** : `docs/AUDIT_20260404.md` contient des clés API réelles. **Interdiction de push/publier sans purge.** + +## 5. Fichiers importants +- `core/extraction/field_extractor.py` (Cœur de la démo métier demain). +- `agent_v0/server_v1/resolve_engine.py` (Contient le mini-fix VRAM). +- `docs/coordination/registre/2026-05-25_decisions.md` (Source de vérité). + +## 6. Prochaine action recommandée +- **Lancer le Healthcheck Windows** (avec secret non persistant) pour valider l'agent avant le smoke métier Easily. + +## 7. Ce qu'il ne faut pas faire demain +- **Ne pas activer `RPA_GROUNDING_MODEL=qwen3.5:9b` globalement.** +- **Ne pas modifier le code de `agent_v1` (Windows)** sans plan de déploiement validé. + +Auteur : Gemini diff --git a/docs/handoffs/PROMPT_REPRISE_GEMINI_2026-05-26_MATIN.md b/docs/handoffs/PROMPT_REPRISE_GEMINI_2026-05-26_MATIN.md new file mode 100644 index 000000000..e85965a50 --- /dev/null +++ b/docs/handoffs/PROMPT_REPRISE_GEMINI_2026-05-26_MATIN.md @@ -0,0 +1,85 @@ +# Prompt reprise Gemini — 2026-05-26 matin + +- `Auteur`: Codex +- `Date`: 2026-05-26 10:09 Europe/Paris +- `Contexte`: reprise J-6 démo Easily 2026-06-01 +- `But`: permettre à Gemini de redémarrer vite sans relire tout l'historique + +## État court + +Le socle technique a été stabilisé hier soir : + +- Smoke Bloc-notes `replay_sess_1c0bfb42` validé `16/16`, `0 failed`, `0 retries`, `0 pause Léa`. +- Profil démo Linux actif : + - `RPA_SKIP_INTENTION_ENRICHMENT=true` + - `RPA_SKIP_BUILD_VISION=true` + - `RPA_EASYOCR_GPU=0` + - `AGENT_CHAT_ENABLE_OWL=0` + - `AGENT_CHAT_ENABLE_UI_DETECTION=0` +- Runtime démo maintenu sur `qwen2.5vl` bbox legacy. +- `qwen3.5` reste benchmark/API préparatoire, pas runtime global. +- Patch OCR Claude `_text_match_fuzzy` ACK par Codex : tolérance préfixe `Enregi` / `Enregistrer`, seuil conservé à 50%. +- D5-v3c Windows `num_ctx=8192` reporté post-démo. + +## État ce matin + +Codex a distribué les tâches : + +- Claude : `docs/coordination/inbox_claude/2026-05-26_0925_codex-to-claude_TACHES-reprise-easily-healthcheck-trace.md` +- Gemini : `docs/coordination/inbox_gemini/2026-05-26_0925_codex-to-gemini_TACHES-reprise-demo-metier-risques.md` + +Healthcheck local Codex : + +- `rpa-streaming.service`: actif +- `rpa-agent-chat.service`: actif +- `5005/health`: healthy +- `5004/api/status`: online +- `ollama ps`: aucun modèle résident +- `tools/lea_healthcheck.py`: WARN uniquement car `qwen2.5vl:7b-rpa` non résident +- Windows non testé : pas de secret SSH disponible dans la session Codex + +## Ce que Gemini doit faire maintenant + +Répondre uniquement à cette tâche : + +`docs/coordination/inbox_gemini/2026-05-26_0925_codex-to-gemini_TACHES-reprise-demo-metier-risques.md` + +Réponse attendue dans : + +`docs/coordination/inbox_codex/2026-05-26_HHMM_gemini-to-codex_REPONSE-demo-metier-risques.md` + +Format attendu : court, ACK/NACK explicite, directement utilisable par Dom/Codex. + +## Contenu attendu + +1. **Scénario métier minimal Easily** + - 6 à 10 actions maximum. + - Montrer intelligence + facilité, pas performance technique. + - Inclure au moins une lecture métier et une saisie contrôlée. + - Ne pas reprendre mécaniquement un workflow historique VWB. + +2. **Critères client de réussite** + - 5 critères maximum, visibles par un client non technique. + +3. **Risques bloquants démo** + - Seulement les risques qui justifient NOGO avant capture/replay. + - Distinguer technique, métier, conformité. + +4. **Phrase qwen** + - Une formulation courte expliquant pourquoi on ne migre pas `qwen3.5` globalement avant la démo. + +## Contraintes non négociables + +- Pas de runtime. +- Pas de benchmark. +- Pas de migration modèle. +- Pas de replay live. +- Pas de redéploiement Windows. +- Pas de données patient réelles. +- Ne pas bloquer sur secrets/docs : dette à traiter avant publication, pas avant préparation locale. + +## Décision si contexte incomplet + +Si Gemini manque d'information, il doit répondre avec les hypothèses minimales plutôt que relancer une enquête longue. + +Auteur : Codex diff --git a/docs/handoffs/PROMPT_REPRISE_QWEN_2026-05-26_HANDOFF.md b/docs/handoffs/PROMPT_REPRISE_QWEN_2026-05-26_HANDOFF.md new file mode 100644 index 000000000..605a8f940 --- /dev/null +++ b/docs/handoffs/PROMPT_REPRISE_QWEN_2026-05-26_HANDOFF.md @@ -0,0 +1,161 @@ +# Handoff Qwen — Session 2026-05-26 soir → 2026-05-27 + +- `De`: Qwen +- `Date`: 2026-05-26 22:30 Europe/Paris +- `Contexte`: fin de session soir, reprise demain +- `Équipe`: Dom (arbitre), Codex (coordinateur), Claude/Claudettes (collègues), Qwen (moi) + +--- + +## État du projet — ce qui a été fait ce soir + +### Session de reprise (20h44 → 22h30) + +1. **Lecture du prompt de reprise** (`PROMPT_REPRISE_QWEN_2026-05-26_SOIR.md`) + - Cadrage Aiva-vision / Léa / Aiva-urgence + - Scénario démo v2 : collecte multi-onglets → transposition OnlyOffice + - Limites non négociables Dom + +2. **Première analyse** → `2026-05-26_2044_qwen-to-codex_REPRISE-analyse-scenario-v2.md` + - ACK/NACK, 8-lignes résumé, 5 risques, 5 critères, 3 vérifications + - Recommandation : dry-run contrôlé + +3. **Deuxième passe après lecture 5 sources actives** → `2026-05-26_2050_qwen-to-codex_DELTA-apres-lecture-sources-actives.md` + - Corrections : `/api/analyse` n'est pas un endpoint vision, orthographe `Easily` + - 3 risques bloquants : extract_text_scroll, grounding maquette, sortie transposition + - Proposition transposition : .xlsx via openpyxl, fallback .txt + +4. **Audit technique dry-run + OnlyOffice** → `2026-05-26_2101_qwen-to-codex_AUDIT-technique-dryrun-onlyoffice.md` + - 8 ancres critiques à valider + - Seuils GO/NOGO par onglet + - Fallbacks F1-F4 + +5. **Seuils et fallbacks après dry-run** → `2026-05-26_2113_qwen-to-codex_SEUILS-fallbacks-apres-dryrun.md` + - Seuils affinés sur données réelles du dry-run + - 4 fallbacks techniques documentés + +6. **Rapport P0 OCR écran** → `2026-05-26_2117_qwen-to-codex_RAPPORT-P0-ocr-ecran.md` + - Diagnostic pipeline OCR (EasyOCR, docTR, Tesseract) + - Architecture multi-moteur par zone + - Cold start vs interface apprise + - **Mis à jour** : docTR CPU repositionné comme moteur de zonage P0 + +7. **Retour benchmark OCR** → `2026-05-26_2148_qwen-to-codex_RETOUR-benchmark-ocr-capitalisation.md` + - Tesseract 11/11 IPP en 0,47s ✅ + - EasyOCR 8/11 IPP, bon sur texte continu + - Preprocessing OpenCV : régression, pas d'amélioration + - Architecture multi-moteur : chiffres→Tesseract, texte→EasyOCR, structure→docTR + - 5 règles de capitalisation + +8. **ACK apprentissage scroll sécurisé** → `2026-05-26_2149_qwen-to-codex_ACK-apprentissage-scroll-securise.md` + - GO/NOGO sur marqueurs après scroll (CCMU, GEMSA, J12.1, Consultation externe) + - Scroll réussi = geste + changement visuel + données relues + +### Exploration web (solutions similaires) +- Agent-S : réflexion in-context, Best-of-N sampling, grounding dédié +- UI-TARS : grounding GUI par coordonnées, reinforcement learning +- Claude Computer Use : 22% OSWorld, scroll/drag difficiles +- OpenAI Operator : **abandonné** (août 2025) +- Différentiateur Aiva-vision : "l'agent qui sait s'arrêter" — défendable en domaine réglementé + +### Exploration codebase +- Agent explorateur a scanné 880 fichiers Python, 39 sous-modules core/ +- Pipeline complet compris : capture → streaming → analyse → grounding → execution → replay + +--- + +## État technique connu — décisions actives + +### OCR +- **EasyOCR brut** : moteur par défaut pour texte continu (inchangé) +- **Tesseract** : patch appliqué pour IPP/chiffres (`extract_digits_tesseract_from_image()`, `extract_table(engine="tesseract")`) +- **docTR CPU** : moteur de zonage pour band patient, synthèse, bboxes +- **Preprocessing OpenCV** : ❌ reporté (régression mesurée) +- **PaddleOCR** : ❌ post-démo +- **VLM OCR texte** : ❌ exclu J-6 + +### Workflow +- `Demo_urgence_3_db` / `wf_483910cdd851_1778750587` : step `extract_table` → Tesseract +- BDD backupée : `workflows.db.backup_2026-05-26_ocr_tesseract_demo3` +- 5 onglets préparés, live prudent possible en 4 si scroll échoue + +### Démo +- Cible : 2026-06-01 +- Répétition humaine : demain (Dom challengeur) +- Dossier cible : `MOREL Catherine / IPP 25003284` +- Sortie : `.xlsx` ouvert dans OnlyOffice (`/snap/bin/onlyoffice-desktopeditors`) +- Profil démo Linux actif (flags skip vision, EasyOCR CPU) + +--- + +## Documents actifs à connaître + +### Sources actives prioritaires +1. `docs/coordination/active/2026-05-26_cadrage-produit-aiva-vision.md` +2. `docs/coordination/active/2026-05-26_arbitrage-dom-demo-reelle-poc.md` +3. `docs/coordination/active/2026-05-26_principe-dom-apprentissage-fail-safe.md` +4. `docs/coordination/active/2026-05-26_scenario-operatoire-demo-lea-v2-collecte-transposition.md` +5. `docs/coordination/active/2026-05-26_audit-ancien-workflow-urgence-aiva.md` + +### Documents ajoutés ce soir +- `docs/coordination/active/2026-05-26_benchmark-ocr-local-captures-easily.md` +- `docs/coordination/active/2026-05-26_arbitrage-scroll-vwb-reference.md` +- `docs/coordination/active/2026-05-26_principe-apprentissage-scroll-securise.md` +- `docs/coordination/active/2026-05-26_synthese-retours-claude-qwen-demo-v2-ocr.md` +- `docs/coordination/active/2026-05-26_dryrun-easily-v2-captures-ocr-onlyoffice.md` +- `docs/coordination/active/2026-05-26_arbitrage-sortie-transposition-onlyoffice.md` +- `docs/coordination/active/2026-05-26_mission-p0-ocr-ecran-lea.md` +- `docs/coordination/active/2026-05-26_mission-P0-ocr-ecran-qwen.md` + +### Runbook +- `docs/coordination/active/2026-05-26_runbook-repetition-humain-challenge-demo-v2.md` + +--- + +## Fichiers produits ce soir (inbox_codex) + +| Fichier | Type | +|---------|------| +| `2026-05-26_2044_qwen-to-codex_REPRISE-analyse-scenario-v2.md` | Analyse initiale | +| `2026-05-26_2050_qwen-to-codex_DELTA-apres-lecture-sources-actives.md` | Delta sources | +| `2026-05-26_2101_qwen-to-codex_AUDIT-technique-dryrun-onlyoffice.md` | Audit technique | +| `2026-05-26_2113_qwen-to-codex_SEUILS-fallbacks-apres-dryrun.md` | Seuils/fallbacks | +| `2026-05-26_2117_qwen-to-codex_RAPPORT-P0-ocr-ecran.md` | Rapport OCR (mis à jour) | +| `2026-05-26_2137_qwen-to-codex_SYNTHESE-benchmark-5-onglets.md` | Synthèse collectif | +| `2026-05-26_2148_qwen-to-codex_RETOUR-benchmark-ocr-capitalisation.md` | Retour benchmark | +| `2026-05-26_2149_qwen-to-codex_ACK-apprentissage-scroll-securise.md` | ACK scroll | + +--- + +## Ce qui reste à faire / à surveiller demain + +1. **Répétition humaine** — Dom challengeur, critères GO/NOGO stricts +2. **Résultats de la répétition** — ajuster si nécessaire +3. **Patchs potentiels** — selon résultats répétition (scroll, grounding) +4. **Préparation démo 2026-06-01** — J-5 après demain + +--- + +## Mémoire construite ce soir + +- `user/dom_constraints.md` — Limites non négociables Dom +- `project/aiva_vision_demo.md` — Contexte démo Aiva-vision +- `feedback/qwen_avoidances.md` — Ce que Qwen doit éviter +- `feedback/dom_doctr_preference.md` — DocTR puissant pour zonage +- `feedback/qwen_proactive_improvements.md` — Qwen doit proposer des idées +- `project/aiva_vision_product_philosophy.md` — Collaborateur administratif, pas RPA +- `reference/coordination_process.md` — Coordination par fichiers Markdown + +--- + +## Notes personnelles Qwen + +- Le positionnement produit est **collaborateur administratif supervisé**, pas RPA "boîte à clic" +- Notre avantage : "l'agent qui sait s'arrêter" — pas un bug, une feature en domaine réglementé +- Architecture : Aiva-vision (socle universel) + plugins métier (accélérateurs d'apprentissage) +- Cycle Léa : apprendre → essayer → se planter → humain rattrape → consolide → indépendant +- L'exploration web montre que personne n'a résolu le computer use fiable (Claude 22%, OpenAI a abandonné) + +--- + +*Auteur : Qwen* diff --git a/docs/handoffs/PROMPT_REPRISE_QWEN_2026-05-26_SOIR.md b/docs/handoffs/PROMPT_REPRISE_QWEN_2026-05-26_SOIR.md new file mode 100644 index 000000000..83525183c --- /dev/null +++ b/docs/handoffs/PROMPT_REPRISE_QWEN_2026-05-26_SOIR.md @@ -0,0 +1,208 @@ +# Prompt reprise Qwen — Aiva-vision / Léa / Aiva-urgence + +- `Auteur`: Codex +- `Date`: 2026-05-26 20:41 Europe/Paris +- `Contexte`: remplacement de Gemini comme collègue d'analyse +- `Objectif`: permettre à Qwen de contribuer efficacement à la préparation démo 2026-06-01 + +## Ton rôle + +Tu rejoins une équipe de coordination autour du projet **Aiva-vision**. + +Tu ne pilotes pas le runtime. Tu es attendu comme collègue d'analyse technique et produit : + +- relire les sources actives ; +- détecter les contradictions ; +- challenger les risques ; +- proposer des checklists courtes ; +- aider à préparer une démo réelle, solide et défendable pour les POC. + +Tu dois répondre de façon opérationnelle, courte, structurée, sans tunnel de réflexion. + +## Produit + +### Aiva-vision + +Aiva-vision est la plateforme générique. + +Elle apprend des interfaces existantes, interagit avec elles, et permet de greffer des plugins métier. + +Le positionnement est universel : santé aujourd'hui, aéronautique ou autres secteurs demain. + +### Léa + +Léa est l'agent d'interaction d'Aiva-vision. + +Elle doit : + +- observer l'écran ; +- lire les données affichées ; +- dialoguer avec l'utilisateur ; +- demander confirmation ; +- reporter des informations dans d'autres outils ; +- apprendre quand elle ne sait pas ; +- s'arrêter plutôt que faire une mauvaise action. + +### Aiva-urgence + +Aiva-urgence est le plugin métier santé utilisé pour la démo. + +Il aide à traiter des dossiers urgences et à qualifier/requalifier un passage : + +- Forfait Urgences ; +- hospitalisation / requalification UHCD-MCO. + +## Cadrage démo + +Le client a déjà vu un vrai scénario filmé. Le 2026-06-01, il veut voir en vrai. + +Dom impose : + +- pas de trucage ; +- pas de bidouillage ; +- pas de succès simulé ; +- pas de hardcode pour faire illusion ; +- si Léa ne sait pas, elle s'arrête et demande de l'aide. + +Dans le monde hospitalier, un arrêt sûr est préférable à une action dangereuse. + +La démo est courte, mais elle prépare les POC. + +## Scénario cible actuel + +La base opérationnelle est : + +`docs/coordination/active/2026-05-26_scenario-operatoire-demo-lea-v2-collecte-transposition.md` + +Résumé : + +1. Léa observe la liste des passages aux urgences. +2. Léa décrit le tableau : colonnes, dossiers, statuts. +3. Léa propose : traiter tous les dossiers ou un dossier en particulier. +4. Dom choisit un dossier, probablement `MOREL Catherine / 25003284`. +5. Léa ouvre le dossier. +6. Léa parcourt les onglets : + - `Motif d'admission` + - `Examens cliniques` + - `Imagerie` + - `Notes médicales` + - `Synthèse Urgences` +7. Léa collecte les informations, y compris avec scroll/ascenseur si nécessaire. +8. Léa demande où consigner les informations : + - Excel ; + - Word ; + - base de données ; + - autre plateforme/environnement. +9. Léa reporte les informations collectées. +10. Léa s'arrête pour validation humaine. + +Le coeur de la démonstration : **lecture écran précise -> collecte multi-onglets -> choix utilisateur -> transposition dans un autre outil**. + +## État technique connu + +Validé : + +- Smoke Bloc-notes `replay_sess_1c0bfb42` : `16/16`, `0 failed`, `0 retries`, `0 pause Léa`. +- Profil démo Linux actif : + - `RPA_SKIP_INTENTION_ENRICHMENT=true` + - `RPA_SKIP_BUILD_VISION=true` + - `RPA_EASYOCR_GPU=0` + - `AGENT_CHAT_ENABLE_OWL=0` + - `AGENT_CHAT_ENABLE_UI_DETECTION=0` +- Runtime démo visuel : `qwen2.5vl` bbox legacy. +- Ne pas migrer globalement vers `qwen3.5`. +- `qwen3.5` reste benchmark/API préparatoire. +- Patch OCR préfixe `Enregi` / `Enregistrer` ACK, seuil 50%. +- EasyOCR GPU désactivé par défaut. +- D5-v3c Windows `num_ctx=8192` reporté post-démo. + +Maquette : + +- service `rpa-mockup-easily.service` actif ; +- endpoint `http://127.0.0.1:8765/healthz` OK ; +- maquette Windows attendue : `http://192.168.1.40:8765/index.html`. + +Important : + +- `/api/analyse` utilise le modèle texte `qwen2.5:7b`, pas `qwen2.5vl`. +- `qwen2.5vl:7b-rpa` sert au grounding visuel/replay. +- l'ancien workflow `Urgence_aiva_demo` contient des briques utiles mais ne doit pas être rejoué tel quel. + +## Documents à lire dans cet ordre + +1. `docs/coordination/active/2026-05-26_cadrage-produit-aiva-vision.md` +2. `docs/coordination/active/2026-05-26_arbitrage-dom-demo-reelle-poc.md` +3. `docs/coordination/active/2026-05-26_principe-dom-apprentissage-fail-safe.md` +4. `docs/coordination/active/2026-05-26_scenario-operatoire-demo-lea-v2-collecte-transposition.md` +5. `docs/coordination/active/2026-05-26_audit-ancien-workflow-urgence-aiva.md` + +Compléments utiles : + +- `docs/coordination/inbox_codex/2026-05-26_1030_claude-to-codex_CHECKLIST-easily-capture-trace.md` +- `docs/coordination/inbox_codex/2026-05-26_1145_gemini-to-codex_REPONSE-demo-metier-risques.md` +- `docs/handoffs/PROMPT_REPRISE_CODEX_2026-05-25_SOIR.md` + +## Méthode de travail + +Le repo utilise une coordination par fichiers Markdown. + +Pour écrire à Codex : + +`docs/coordination/inbox_codex/YYYY-MM-DD_HHMM_qwen-to-codex_SUJET.md` + +Format attendu : + +- `De`: Qwen +- `A`: Codex +- `Date`: horodatage Europe/Paris +- `Statut`: ACK / NACK / proposition +- réponse courte et actionnable. + +Ne pas écrire de rapport long sauf demande explicite. + +Toujours distinguer : + +- faits vérifiés ; +- hypothèses ; +- recommandations ; +- risques bloquants. + +## Limites non négociables + +- Pas de replay live sans GO explicite Dom. +- Pas de redéploiement Windows. +- Pas de restart service sans demande Codex/Dom. +- Pas de migration globale modèle. +- Pas de `RPA_GROUNDING_MODEL=qwen3.5:9b`. +- Pas de patch runtime sans demande. +- Pas de secret dans les docs. +- Pas de patient réel : la maquette est fictive. +- Pas de proposition cosmétique ou truquée. + +## Ce que tu dois éviter + +- Ne pas vendre une autonomie totale immédiate. +- Ne pas réduire Aiva-vision à la maquette Easily. +- Ne pas présenter Aiva-urgence comme tout le produit. +- Ne pas proposer l'ancien `Urgence_aiva_demo` comme scénario tel quel. +- Ne pas recommander une action risquée pour "faire passer la démo". +- Ne pas inventer des capacités absentes du code. + +## Première tâche attendue + +Après lecture des documents prioritaires, produis un fichier : + +`docs/coordination/inbox_codex/YYYY-MM-DD_HHMM_qwen-to-codex_REPRISE-analyse-scenario-v2.md` + +Contenu demandé : + +1. ACK/NACK du cadrage produit. +2. Résumé du scénario v2 en 8 lignes maximum. +3. 5 risques NOGO maximum. +4. 5 critères de réussite client maximum. +5. Les 3 points techniques à vérifier avant capture. +6. Une seule recommandation prioritaire pour rendre Léa "super solide et agile" sur cette démo. + +Ne propose pas encore de patch. + +Auteur : Codex diff --git a/docs/plans/BACKLOG_HORIZON1_LEA_FIRST_2026-05-19.md b/docs/plans/BACKLOG_HORIZON1_LEA_FIRST_2026-05-19.md new file mode 100644 index 000000000..ffd1002c7 --- /dev/null +++ b/docs/plans/BACKLOG_HORIZON1_LEA_FIRST_2026-05-19.md @@ -0,0 +1,257 @@ +# Backlog Horizon 1 Lea-first -- 2026-05-19 + +**Objet** +- Transformer `Horizon 1` en backlog d'execution concret. +- Se limiter au coeur produit `Lea-first`. +- Ne pas reintroduire `VWB` ou la demo comme reference produit. + +## 1. Definition de done + +Horizon 1 est termine quand les conditions suivantes sont vraies : + +- un workflow Lea-first canonique peut etre capture puis rejoue sans passer par `VWB` +- `pause -> intervention humaine -> resume` est fiable et comprehensible +- les succes, echecs et corrections humaines alimentent vraiment la memoire persistante +- on dispose d'un gate minimal reproductible avant toute session terrain +- on dispose d'un runbook operatoire court pour lancer, observer et reprendre un replay + +## 2. Baseline technique au 2026-05-19 + +### Pipeline reel retenu + +Le chemin nominal reste : + +`capture Lea -> stream serveur -> replay direct -> verification -> memory` + +Fichiers pivots : + +- `agent_v0/agent_v1/main.py` +- `agent_v0/agent_v1/core/executor.py` +- `agent_v0/server_v1/api_stream.py` +- `agent_v0/server_v1/stream_processor.py` +- `agent_v0/server_v1/resolve_engine.py` +- `agent_v0/server_v1/replay_learner.py` +- `agent_v0/server_v1/replay_memory.py` +- `core/learning/target_memory_store.py` + +### Gate minimal deja vert + +Tests executes localement le 2026-05-19 : + +- `pytest -q tests/integration/test_replay_resume_acknowledgments.py tests/integration/test_loop_detector_replay.py tests/unit/test_target_memory_store.py` +- `pytest -q tests/integration/test_stream_processor.py tests/smoke/test_smoke_e2e_minimal.py` +- `pytest -q tests/integration/test_pause_for_human.py tests/unit/test_replay_critic.py` + +Conclusion : + +- les briques `resume`, `loop detector`, `target memory`, `stream processor` et le smoke critique offline sont deja exploitables comme premier garde-fou +- `pause_for_human` et `replay_critic` peuvent rejoindre ce garde-fou immediatement +- il reste un warning `pytest-asyncio` de configuration de boucle, non bloquant pour Horizon 1 +- `tests/unit/test_policy_grounding_recovery_learning.py` n'est pas encore un gate portable : 4 tests `RecoveryEngine` tombent localement sur `ModuleNotFoundError: pynput` +- ce gate ne prouve pas encore le replay Windows reel de bout en bout + +## 3. Ordre d'attaque + +### Chantier H1-A -- Stabiliser le replay nominal + +**But** +- faire du replay direct le chemin assume et observable + +**Fichiers cibles** +- `agent_v0/server_v1/stream_processor.py` +- `agent_v0/server_v1/api_stream.py` +- `agent_v0/agent_v1/main.py` +- `agent_v0/agent_v1/core/executor.py` + +**Taches** +- figer un workflow Lea-first canonique de reference +- verifier que `build_replay_from_raw_events(...)` produit des actions propres sur ce workflow +- verifier le couplage `session_id / machine_id / replay queue` pour eviter les confusions inter-machines +- tracer explicitement le passage `capture finalisee -> replay injecte -> action poll -> resultat recu` + +**Acceptation** +- a partir d'une capture Lea, le serveur injecte une queue de replay exploitable sans bridge VWB +- l'agent recupere les actions avec le bon `machine_id` +- l'index courant du replay et le nombre d'actions restantes restent coherents jusqu'a completion ou pause + +**Gate associe** +- `tests/integration/test_stream_processor.py` +- `tests/smoke/test_smoke_e2e_minimal.py` + +### Chantier H1-B -- Fiabiliser la pause supervisee + +**But** +- rendre deterministe et lisible la boucle `pause / aide humaine / reprise` + +**Fichiers cibles** +- `agent_v0/server_v1/api_stream.py` +- `agent_v0/server_v1/loop_detector.py` +- `agent_v0/agent_v1/network/feedback_bus.py` +- `agent_v0/agent_v1/ui/chat_window.py` + +**Taches** +- formaliser la matrice des causes de `paused_need_help` +- distinguer les pauses qui doivent reinjecter l'action de celles qui ne doivent pas la reinjecter +- verifier l'alignement entre payload serveur, bulle Lea et endpoint `/replay/{id}/resume` +- verifier que les acquittements de `safety_checks` restent obligatoires quand requis + +**Causes a couvrir explicitement** +- `pause_for_human` +- `target_not_found` +- `wrong_window` +- `no_screen_change_strict` +- `system_dialog` +- `loop_detected` + +**Acceptation** +- chaque pause expose une raison, un message et un comportement de reprise clairs +- `resume` ne relance pas une action qui etait une pause intentionnelle +- `cancel` vide proprement la queue +- aucun cas de pause ne laisse le replay dans un etat ambigu + +**Gate associe** +- `tests/integration/test_replay_resume_acknowledgments.py` +- `tests/integration/test_loop_detector_replay.py` +- `tests/integration/test_pause_for_human.py` + +### Chantier H1-C -- Faire apprendre la memoire de replay + +**But** +- s'assurer que la boucle d'apprentissage est branchee sur le runtime reel, pas seulement documentee + +**Fichiers cibles** +- `agent_v0/server_v1/api_stream.py` +- `agent_v0/server_v1/replay_learner.py` +- `agent_v0/server_v1/replay_memory.py` +- `agent_v0/server_v1/resolve_engine.py` +- `core/learning/target_memory_store.py` + +**Taches** +- verifier le contrat `actual_position` entre agent et serveur +- verifier que `memory_record_success()` ne se declenche qu'apres succes verifie +- verifier que `memory_record_failure()` decremente bien la fiabilite utile +- verifier que `record_human_correction()` nourrit bien `target_memory.db` +- tracer un cas de `memory_lookup HIT` jusqu'a la resolution suivante + +**Acceptation** +- au moins un cas reel montre la sequence : + `target_not_found ou no_screen_change_strict -> correction humaine -> stockage -> hit memoire sur replay suivant` +- les fichiers `data/learning/replay_results/*.jsonl` et `data/learning/events/*/resolution_events.jsonl` sont utiles et lisibles +- `target_memory.db` contient des entrees correspondant a des cibles reelles du workflow canonique + +**Gate associe** +- `tests/unit/test_target_memory_store.py` +- `tests/unit/test_policy_grounding_recovery_learning.py` +- `tests/unit/test_replay_critic.py` + +### Chantier H1-D -- Runbook et gate terrain + +**But** +- disposer d'un protocole d'execution simple avant toute session reelle + +**Fichiers cibles** +- `agent_v0/deploy/test_replay_diag.py` +- `tests/smoke/test_smoke_e2e_minimal.py` +- nouvelle doc de runbook Lea-first + +**Taches** +- definir un gate local minimum avant session terrain +- definir un workflow canonique a jouer a blanc avant toute session utilisateur +- ecrire une checklist courte `avant capture / avant replay / pendant pause / apres correction` +- rendre explicites les artefacts a conserver apres un incident + +**Acceptation** +- on sait dire en moins de 5 minutes si la machine est "prete a rejouer" +- on sait quoi collecter en cas d'echec sans improviser +- on sait faire une reprise propre apres une pause supervisee + +**Gate associe** +- `python agent_v0/deploy/test_replay_diag.py` +- `pytest -q tests/smoke/test_smoke_e2e_minimal.py` + +## 4. Sequence immediate + +### Etape 1 -- Geler le gate Horizon 1 + +- conserver les 5 fichiers de test deja verts comme premier noyau +- ajouter maintenant `tests/integration/test_pause_for_human.py` +- ajouter maintenant `tests/unit/test_replay_critic.py` +- garder `tests/unit/test_policy_grounding_recovery_learning.py` en seconde couronne tant que la dependance `pynput` n'est pas geree proprement pour le contexte de test + +### Etape 2 -- Choisir le workflow canonique + +- un seul workflow Lea-first +- court +- peu de fenetres +- au moins un clic, une saisie, une verification, une pause possible + +### Etape 3 -- Prioriser les corrections + +Ordre de traitement : + +1. ambiguite de replay `session_id / machine_id / queue` +2. cas de pause/reprise incoherents +3. apprentissage non persiste ou non reutilise +4. runbook et collecte d'artefacts + +## 5. Integration de l'audit Claude + +Reference : + +- [AUDIT_LEA_FIRST_RUNTIME_2026-05-19.md](/home/dom/ai/rpa_vision_v3/docs/AUDIT_LEA_FIRST_RUNTIME_2026-05-19.md) + +Important : + +- dans ce repo, `agent_v0/` reste le **conteneur de chemin** du code courant +- l'audit qui parle de `agent_v0/agent_v1` et `agent_v0/server_v1` parle donc bien du runtime actuel + +### Points valides + +- `record_human_correction()` est reellement casse +- la capture via `mss.monitors[1]` n'a pas de garde contre des dimensions aberrantes +- `_replay_active` est mal gere cote client pendant `replay_paused=true` + +### Points a corriger dans la formulation + +- le replay direct **existe bien** via `POST /api/v1/traces/stream/replay-session` +- ce endpoint charge `live_events.jsonl`, appelle `build_replay_from_raw_events(...)`, puis injecte la queue de replay +- en revanche, `finalize` n'enchaine pas ce chemin automatiquement et la surface produit reste morcelee +- `build_workflow_replay()` est bien orphelin, mais cela ne prouve pas que le replay direct n'existe pas + +- le gating memoire sur `window_title` est reel +- mais sur le flux Lea-first natif, `build_replay_from_raw_events(...)` propage deja `window_title` dans les clics enrichis +- le risque est donc surtout : actions non enrichies, cas degradés, et workflows non Lea-first ou incompletés + +- `core/learning/*` non branche au runtime serveur est un constat juste +- mais pour Horizon 1, ce n'est pas un blocage du coeur produit si on assume que `TargetMemoryStore` est la seule boucle d'apprentissage necessaire court terme + +### Reclassement pratique pour Horizon 1 + +- `P0 reel` : correction humaine cassee +- `P0 reel` : corruption possible des captures moniteur +- `P1 reel` : pause/reprise desynchronisee cote client +- `P1 structurant` : absence de chainage produit propre `finalize -> replay-session` +- `P1 risque` : memoire trop dependante de `window_title` +- `P2 assume` : branches `core/learning/*` hors chemin nominal court terme + +## 6. Ce que je retiens pour la suite immediate + +Ordre de traitement mis a jour : + +1. corriger `record_human_correction()` pour rouvrir l'apprentissage supervise +2. blinder la capture moniteur contre les dimensions aberrantes +3. corriger `_replay_active` sur `action=null + replay_paused=true` +4. definir si `finalize` doit proposer ou lancer explicitement `replay-session` +5. renforcer le fallback `window_title` pour le record / lookup memoire + +## 7. Premier ticket d'execution + +Le premier ticket que je lancerais sans attendre est : + +**"Corriger `record_human_correction()` et valider un vrai cycle correction -> target_memory.db -> hit memoire."** + +Pourquoi : + +- c'est le bug le plus net et le plus rentable de l'audit +- il touche directement la promesse produit Lea-first +- il fournit ensuite un cas de validation concret pour Horizon 1 diff --git a/docs/plans/CADRAGE_COEUR_PRODUIT_LEA_2026-05-19.md b/docs/plans/CADRAGE_COEUR_PRODUIT_LEA_2026-05-19.md new file mode 100644 index 000000000..0a9c7a68b --- /dev/null +++ b/docs/plans/CADRAGE_COEUR_PRODUIT_LEA_2026-05-19.md @@ -0,0 +1,313 @@ +# Cadrage coeur produit Lea -- 2026-05-19 + +**But** +- Repartir du vrai produit. +- Ne pas confondre le coeur `Lea-first` avec la demo recente ni avec le VWB. +- Distinguer ce qui est **branche au runtime**, ce qui est **partiellement branche**, et ce qui releve surtout de `VWB/demo/veille`. + +## 1. Definition du coeur produit + +Le coeur produit n'est pas : +- un canvas de creation manuelle de workflows +- un assembleur de demo +- un bench de modeles + +Le coeur produit est : +- **un agent local Lea** qui observe un humain, +- **un serveur de streaming/replay** qui reconstruit une procedure exploitable, +- **une boucle d'apprentissage** qui s'ameliore avec les replays, les verifications et les corrections humaines. + +En une ligne : + +`capture Lea -> streaming serveur -> reconstruction procedure -> replay supervise -> apprentissage persistant` + +## 2. Ce qui est reellement branche aujourd'hui + +### A. Capture Lea -> serveur : OUI, c'est le vrai chemin de base + +Cote client : +- `agent_v0/agent_v1/main.py` +- `agent_v0/agent_v1/core/captor.py` +- `agent_v0/agent_v1/network/streamer.py` + +Ce que fait Lea aujourd'hui : +- capture evenements clavier/souris +- capture screenshots/crops +- ajoute du contexte UIA et fenetre +- streame vers le serveur via : + - `/register` + - `/event` + - `/image` + - `/finalize` + +Conclusion : +- **la capture et le streaming Lea sont bien le socle produit deja branche** + +### B. Reconstruction immediate d'un replay depuis la capture : OUI + +Cote serveur : +- `agent_v0/server_v1/stream_processor.py` +- fonction `build_replay_from_raw_events(...)` + +Ce chemin prend `live_events.jsonl` et produit directement : +- des actions rejouables +- des clics enrichis visuellement +- des intentions et screenshots attendus +- une consolidation avec l'historique d'apprentissage + +Conclusion : +- **il existe deja un chemin capture -> replay exploitable sans passer par le VWB** +- c'est un candidat fort pour le vrai noyau produit court terme + +### C. Replay d'execution Lea : OUI + +Cote serveur : +- `agent_v0/server_v1/api_stream.py` +- `agent_v0/server_v1/replay_engine.py` +- `agent_v0/server_v1/resolve_engine.py` + +Cote client : +- `agent_v0/agent_v1/main.py` +- `agent_v0/agent_v1/core/executor.py` + +Ce qui est branche : +- le serveur alimente une queue de replay +- Lea poll `/replay/next` +- Lea execute +- le serveur resout les cibles, verifie, enregistre les resultats + +Conclusion : +- **le replay Lea pilote par serveur est bien un axe central du produit** + +### D. Apprentissage persistant sur le replay : OUI, mais partiel + +Modules reels : +- `agent_v0/server_v1/replay_learner.py` +- `agent_v0/server_v1/replay_memory.py` +- `core/learning/target_memory_store.py` + +Ce qui est reellement branche : +- historisation des resultats d'actions de replay +- stockage des corrections humaines +- lookup memoire avant cascade de resolution couteuse +- enregistrement des succes verifies apres action + +Conclusion : +- **la boucle d'apprentissage "apres replay" existe vraiment** +- **c'est aujourd'hui le morceau le plus concret de l'apprentissage Lea** + +## 3. Ce qui existe mais n'est pas encore le coeur runtime + +### A. ShadowObserver / ShadowValidator : presents, mais pas au centre + +Modules : +- `core/workflow/shadow_observer.py` +- `core/workflow/shadow_validator.py` +- endpoints `api_stream.py` : `/api/v1/shadow/*` + +Constat important : +- le client Lea ne les appelle pas nativement +- aucun appelant clair n'apparait dans `agent_v0/agent_v1/` +- aucun appelant clair n'apparait non plus dans `visual_workflow_builder/` ou `agent_chat/` + +Conclusion : +- **le mode shadow est une brique prometteuse, mais pas encore un flux produit dominant** + +### B. WorkflowIR -> ExecutionPlan : presents, mais pas encore la voie nominale + +Modules : +- `core/workflow/ir_builder.py` +- `core/workflow/execution_compiler.py` +- `agent_v0/server_v1/execution_plan_runner.py` +- endpoints `api_stream.py` : + - `/api/v1/traces/stream/workflow/compile` + - `/api/v1/traces/stream/replay/plan` + +Constat : +- ce pipeline est bien code +- il compile une session en artefacts plus propres et plus deterministes +- mais il n'est pas pilote naturellement par le client Lea +- il ne semble pas etre le chemin principal utilise au quotidien + +Conclusion : +- **c'est probablement la meilleure direction architecturale** +- **ce n'est pas encore le coeur produit effectivement adopte** + +### C. WorkflowPipeline / GraphBuilder / LearningManager : reels, mais plutot couche moteur + +Modules : +- `core/pipeline/workflow_pipeline.py` +- `core/graph/graph_builder.py` +- `core/learning/learning_manager.py` + +Constat : +- ce socle exprime tres bien l'ambition du produit +- il construit des workflows depuis les sessions +- il gere des etats d'apprentissage + +Mais : +- `LearningManager` est en memoire, pas le centre d'un flux de promotion produit visible +- cette couche n'est pas encore le protocole operatoire dominant Lea + +Conclusion : +- **c'est le moteur conceptuel** +- **pas encore la boucle produit la plus nette pour les 15 prochains jours** + +## 4. Ce qui releve du VWB, pas du coeur produit + +Le VWB sert aujourd'hui surtout a : +- creer ou editer manuellement des workflows +- importer des workflows appris +- exporter un workflow vers Lea +- lancer une execution via proxy vers le streaming server + +Indices forts dans le repo : +- `visual_workflow_builder/backend/api_v3/learned_workflows.py` +- `visual_workflow_builder/backend/api_v3/execute.py` +- `visual_workflow_builder/frontend_v4/src/services/api.ts` + +Le VWB n'est donc pas : +- la source unique de l'apprentissage Lea +- le protocole naturel de capture du savoir-faire +- la definition du produit coeur + +Conclusion : +- **le VWB est un outil satellite d'authoring, d'inspection et de bridge** +- **il ne doit pas definir la direction du produit Lea-first** + +## 5. Ce qui releve surtout de la demo ou de la veille + +Hors coeur produit immediat : +- workflows reconstruits manuellement pour la demo +- corrections specifiques a la maquette de demo +- benches de modeles et comparatifs +- experiments UX ou UX debug non relies a la capture/replay Lea +- documentation strategique ou speculative non branchee + +Cela peut avoir de la valeur : +- comme preuve +- comme R&D +- comme materiau commercial + +Mais ce n'est pas le socle a partir duquel il faut juger l'etat du produit. + +## 6. Matrice simple + +### A. Coeur produit branche aujourd'hui +- capture client Lea +- streaming temps reel +- replay serveur -> client Lea +- resolution visuelle au runtime +- apprentissage persistant post-replay + +### B. Coeur produit partiellement branche +- observation shadow pendant l'enregistrement +- validation/correction structurée du savoir-faire +- compilation systematique en WorkflowIR/ExecutionPlan +- promotion explicite observation -> coaching -> autonomie + +### C. Satellite produit +- VWB +- dashboard de supervision +- agent_chat + +### D. Hors coeur / exploration +- benches modeles +- comparatifs frameworks +- documents de vision non encore materialises dans le chemin runtime + +## 7. Ce qui manque pour que Lea "apprenne vraiment" + +### 1. Un chemin unique assume + +Aujourd'hui, plusieurs chemins coexistent : +- capture -> replay direct +- capture -> GraphBuilder/workflow +- capture -> shadow -> WorkflowIR +- WorkflowIR -> ExecutionPlan -> replay + +Probleme : +- tant qu'un chemin nominal n'est pas assume, l'energie se disperse + +Ce qu'il faut : +- choisir un pipeline principal +- releguer les autres en support, migration ou R&D + +### 2. Une validation utilisateur native sur le chemin Lea + +Le mode shadow et la validation structurée existent, mais ne semblent pas au centre de l'usage client. + +Ce qu'il faut : +- que Lea sache naturellement : + - observer + - montrer ce qu'elle a compris + - recevoir correction/validation + - cristalliser un artefact rejouable + +### 3. Une vraie politique de promotion de l'apprentissage + +Le repo contient des concepts d'etat d'apprentissage, mais pas encore un protocole operatoire simple du style : +- observe N fois +- replaye sous supervision +- apprend des corrections +- passe en autonomie sur critere + +Ce qu'il faut : +- des seuils, des traces, et un etat visible par workflow + +### 4. Une alimentation effective de la memoire + +La memoire persistante est branchee, mais sa valeur depend de : +- replays repetes +- validations reelles +- corrections humaines capturees proprement + +Ce qu'il faut : +- 1 ou 2 cas terrain qui nourrissent vraiment la memoire +- une verification que la base d'apprentissage grossit utilement + +## 8. Consequence pratique pour la remise en ordre + +Si on parle du vrai produit, il faut travailler dans cet ordre : + +1. **Cartographier et assumer le pipeline nominal Lea-first** +2. **Verifier le flux capture -> replay -> apprentissage** +3. **Decider ce qui, dans shadow/IR/ExecutionPlan, doit devenir le prochain cran officiel** +4. **Sortir le VWB du role de reference produit** +5. **Ranger la veille et la demo comme materiaux satellites** + +## 9. Point de depart recommande + +Le bon point de depart n'est pas : +- la demo recente +- le VWB +- la doc speculative + +Le bon point de depart est : +- `agent_v0/agent_v1/` +- `agent_v0/server_v1/` +- `core/learning/` +- `core/workflow/` uniquement pour distinguer le branche du promis + +## 10. Decision simple + +**Formulation recommandee** + +"Le produit coeur est Lea qui observe, rejoue et apprend. +Le VWB est un outil satellite. +Le pipeline nominal doit etre pense depuis Lea, pas depuis le canvas." + +## 11. Sources principales + +- [agent_v0/agent_v1/main.py](/home/dom/ai/rpa_vision_v3/agent_v0/agent_v1/main.py) +- [agent_v0/agent_v1/network/streamer.py](/home/dom/ai/rpa_vision_v3/agent_v0/agent_v1/network/streamer.py) +- [agent_v0/server_v1/stream_processor.py](/home/dom/ai/rpa_vision_v3/agent_v0/server_v1/stream_processor.py) +- [agent_v0/server_v1/api_stream.py](/home/dom/ai/rpa_vision_v3/agent_v0/server_v1/api_stream.py) +- [agent_v0/server_v1/replay_learner.py](/home/dom/ai/rpa_vision_v3/agent_v0/server_v1/replay_learner.py) +- [agent_v0/server_v1/replay_memory.py](/home/dom/ai/rpa_vision_v3/agent_v0/server_v1/replay_memory.py) +- [core/learning/target_memory_store.py](/home/dom/ai/rpa_vision_v3/core/learning/target_memory_store.py) +- [core/workflow/shadow_observer.py](/home/dom/ai/rpa_vision_v3/core/workflow/shadow_observer.py) +- [core/workflow/ir_builder.py](/home/dom/ai/rpa_vision_v3/core/workflow/ir_builder.py) +- [core/workflow/execution_compiler.py](/home/dom/ai/rpa_vision_v3/core/workflow/execution_compiler.py) +- [docs/VISION_RPA_INTELLIGENT.md](/home/dom/ai/rpa_vision_v3/docs/VISION_RPA_INTELLIGENT.md) +- [docs/CARTE_FONCTIONNELLE_2026-05-08.md](/home/dom/ai/rpa_vision_v3/docs/CARTE_FONCTIONNELLE_2026-05-08.md) diff --git a/docs/plans/CARTOGRAPHIE_EXECUTABLE_PIPELINE_LEA_FIRST_2026-05-19.md b/docs/plans/CARTOGRAPHIE_EXECUTABLE_PIPELINE_LEA_FIRST_2026-05-19.md new file mode 100644 index 000000000..92c8a8ee5 --- /dev/null +++ b/docs/plans/CARTOGRAPHIE_EXECUTABLE_PIPELINE_LEA_FIRST_2026-05-19.md @@ -0,0 +1,403 @@ +# Cartographie executable du pipeline Lea-first -- 2026-05-19 + +**Objet** +- Decrire le pipeline reel du produit a partir du code branche. +- Montrer les artefacts, endpoints, workers et bifurcations. +- Distinguer clairement : + - le **nominal actuel** + - les **branches optionnelles** + - les **outils satellites** (`VWB`) + +## 1. Vue d'ensemble + +Le pipeline reel se lit aujourd'hui comme ceci : + +```text +Lea (capture locale) + -> stream register/event/image/finalize + -> live session serveur + -> deux suites possibles + +Suite A: replay direct + live_events.jsonl + -> build_replay_from_raw_events + -> queue de replay + -> Lea execute + -> verify + learning + +Suite B: apprentissage structure + session finalisee + -> worker VLM + -> ScreenStates + embeddings + -> GraphBuilder + -> workflow JSON appris + +Suite C: compilation V4 (optionnelle) + live_events.jsonl + -> IRBuilder + -> WorkflowIR + -> ExecutionCompiler + -> ExecutionPlan + -> replay/plan +``` + +## 2. Pipeline nominal actuel + +### Etape 1 -- Capture locale par Lea + +Fichiers principaux : +- `agent_v0/agent_v1/main.py` +- `agent_v0/agent_v1/core/captor.py` +- `agent_v0/agent_v1/network/streamer.py` + +Flux : +1. `start_session()` cree `session_id`, `VisionCapturer`, `TraceStreamer`, `EventCaptorV1`. +2. `_on_event_bridge()` enrichit chaque action avec : + - `machine_id` + - `window` + - `screenshot_id` + - `vision_info` + - `window_capture` +3. Lea pousse : + - les evenements via `push_event` + - les screenshots via `push_image` +4. un `action_result` est capture 1s apres l'action +5. des heartbeats sont envoyes pendant l'enregistrement et hors enregistrement + +Sorties locales utiles : +- session de capture locale Lea +- crops et screenshots associes +- stream HTTP vers le serveur + +### Etape 2 -- Reception et persistance serveur + +Fichiers principaux : +- `agent_v0/server_v1/api_stream.py` +- `agent_v0/server_v1/live_session_manager.py` + +Endpoints principaux : +- `POST /api/v1/traces/stream/register` +- `POST /api/v1/traces/stream/event` +- `POST /api/v1/traces/stream/image` +- `POST /api/v1/traces/stream/finalize` + +Ce qui est persiste : +- `data/training/live_sessions/{machine_id}/{session_id}/live_events.jsonl` +- `data/training/live_sessions/{machine_id}/{session_id}/shots/*.png` +- etat session en memoire via `LiveSessionManager` + +Remarques importantes : +- les evenements sont journalises en JSONL +- les screenshots `shot_*_full` sont stockes sans analyse GPU immediate +- les heartbeats et screenshots de resultat sont gardes, mais pas tous analyses + +### Etape 3 -- Generation immediate d'un replay direct + +Fichiers principaux : +- `agent_v0/server_v1/stream_processor.py` +- `agent_v0/server_v1/api_stream.py` + +Fonction centrale : +- `build_replay_from_raw_events(...)` + +Ce qu'elle produit a partir de `live_events.jsonl` : +- actions normalisees +- fusion de texte saisi +- sanitization clavier +- `visual_mode` pour les clics +- `target_spec` enrichi +- `expected_screenshot_b64` +- `expected_window_title` +- `intention` +- consolidation avec l'historique du `ReplayLearner` + +Conclusion : +- **c'est le chemin le plus direct entre observation Lea et replay exploitable** + +### Etape 4 -- Replay serveur vers Lea + +Fichiers principaux : +- `agent_v0/server_v1/api_stream.py` +- `agent_v0/server_v1/replay_engine.py` +- `agent_v0/server_v1/resolve_engine.py` +- `agent_v0/agent_v1/core/executor.py` +- `agent_v0/agent_v1/main.py` + +Endpoints : +- `GET /api/v1/traces/stream/replay/next` +- `POST /api/v1/traces/stream/replay/result` +- `POST /api/v1/traces/stream/replay/{replay_id}/resume` +- `POST /api/v1/traces/stream/replay/{replay_id}/cancel` + +Flux : +1. le serveur alimente une queue de replay +2. Lea poll `replay/next` +3. le serveur execute au besoin les actions serveur (`extract_text`, `t2a_decision`, etc.) avant de renvoyer une action visuelle +4. Lea execute l'action +5. Lea renvoie le resultat +6. le serveur verifie et met a jour l'etat du replay + +Conclusion : +- **c'est le runtime produit actif** + +### Etape 5 -- Verification et apprentissage post-replay + +Fichiers principaux : +- `agent_v0/server_v1/replay_learner.py` +- `agent_v0/server_v1/replay_memory.py` +- `core/learning/target_memory_store.py` + +Ce qui se passe apres chaque action : +- enregistrement du resultat d'action +- stockage des corrections humaines +- `memory_record_success()` apres succes verifie +- `memory_record_failure()` sur echec +- `memory_lookup()` avant la cascade de resolution au prochain replay + +Artefacts d'apprentissage : +- `data/learning/replay_results/*.jsonl` +- `data/learning/events/YYYY-MM-DD/resolution_events.jsonl` +- `data/learning/target_memory.db` + +Conclusion : +- **la memoire de replay est le bloc d'apprentissage le plus concret aujourd'hui** + +## 3. Branche d'apprentissage structurel via worker + +### Objectif + +Construire un workflow appris a partir d'une session finalisee. + +### Fichiers principaux + +- `agent_v0/server_v1/run_worker.py` +- `agent_v0/server_v1/stream_processor.py` +- `core/pipeline/workflow_pipeline.py` +- `core/graph/graph_builder.py` + +### Flux + +1. `finalize` marque la session et l'ajoute a `data/training/_worker_queue.txt` +2. `run_worker.py` lit la queue +3. `reprocess_session()` recharge les screenshots `shot_*_full.png` +4. `process_screenshot()` produit : + - `ScreenState` + - embedding + - indexation FAISS +5. `finalize_session()` convertit vers `RawSession` +6. `GraphBuilder.build_from_session(...)` construit le workflow +7. `_persist_workflow()` sauvegarde le workflow appris + +Artefacts : +- `data/training/_worker_queue.txt` +- `data/training/workflows/{machine_id}/wf_*.json` + +Conclusion : +- **ce chemin construit du savoir-faire structure, mais en arriere-plan** +- **il n'est pas le replay nominal court terme** + +## 4. Branche V4 compilee + +### Objectif + +Transformer une session en artefacts plus propres : +- `WorkflowIR` +- `ExecutionPlan` + +### Fichiers principaux + +- `core/workflow/ir_builder.py` +- `core/workflow/execution_compiler.py` +- `agent_v0/server_v1/execution_plan_runner.py` +- `agent_v0/server_v1/api_stream.py` + +### Endpoints + +- `POST /api/v1/traces/stream/workflow/compile` +- `POST /api/v1/traces/stream/replay/plan` + +### Flux + +1. lecture de `live_events.jsonl` +2. `IRBuilder.build(...)` +3. sauvegarde en `data/workflows_ir/*.json` +4. `ExecutionCompiler.compile(...)` +5. sauvegarde en `data/plans/*.json` +6. `execution_plan_to_actions(...)` +7. injection dans la queue de replay + +Artefacts : +- `data/workflows_ir/*.json` +- `data/plans/*.json` + +Conclusion : +- **c'est la branche architecturale la plus propre** +- **mais elle n'est pas encore pilotee naturellement par Lea** + +## 5. Branche shadow + +### Fichiers principaux + +- `core/workflow/shadow_observer.py` +- `core/workflow/shadow_validator.py` +- `agent_v0/server_v1/api_stream.py` + +### Endpoints + +- `POST /api/v1/shadow/start` +- `POST /api/v1/shadow/stop` +- `POST /api/v1/shadow/feedback` +- `GET /api/v1/shadow/{session_id}/understanding` +- `POST /api/v1/shadow/build` + +### Role + +- observer la capture en temps reel +- construire une comprehension incrementale +- laisser l'utilisateur valider/corriger +- produire un `WorkflowIR` + +### Constat + +- le code est present +- le serveur l'accepte +- mais le client Lea ne semble pas le piloter nativement + +Conclusion : +- **shadow est branche cote serveur** +- **shadow n'est pas encore la voie nominale Lea-first** + +## 6. Role reel du VWB + +### Ce qu'il fait + +Fichiers principaux : +- `visual_workflow_builder/backend/api_v3/learned_workflows.py` +- `visual_workflow_builder/backend/api_v3/execute.py` +- `visual_workflow_builder/frontend_v4/src/services/api.ts` + +Role : +- lister les workflows appris +- les importer dans le VWB +- les exporter vers Lea +- lancer une execution via proxy + +Conclusion : +- **le VWB est un bridge et un outil d'authoring** +- **il ne doit pas etre lu comme le pipeline coeur du produit** + +## 7. Artefacts de reference + +### Capture live +- `data/training/live_sessions/{machine_id}/{session_id}/live_events.jsonl` +- `data/training/live_sessions/{machine_id}/{session_id}/shots/*.png` + +### Sessions serveur en memoire / persistance +- `data/training/live_sessions/` +- `data/training/live_sessions/...` + +### Queue worker +- `data/training/_worker_queue.txt` +- `data/training/_replay_active.lock` + +### Workflows appris +- `data/training/workflows/{machine_id}/wf_*.json` + +### Pipeline V4 +- `data/workflows_ir/*.json` +- `data/plans/*.json` + +### Apprentissage replay +- `data/learning/replay_results/*.jsonl` +- `data/learning/events/YYYY-MM-DD/resolution_events.jsonl` +- `data/learning/target_memory.db` + +## 8. Ce qui est nominal, ce qui ne l'est pas + +### Nominal actuel +- capture Lea +- streaming serveur +- replay direct +- verification +- memoire post-replay + +### Non nominal mais reel +- worker qui fabrique un workflow appris +- compile V4 vers `ExecutionPlan` +- shadow avec validation + +### Satellite +- import/export VWB +- edition manuelle VWB +- lancement VWB via proxy + +## 9. Point de decision architectural + +Le repo contient aujourd'hui **trois chemins serieux** : + +### Option A -- assumer le direct replay comme v1 produit + +Chemin : +- capture Lea +- `build_replay_from_raw_events` +- replay +- apprentissage memoire + +Avantage : +- c'est le plus branche +- c'est le plus immediat + +Limite : +- moins propre conceptuellement +- moins explicite sur la validation du savoir-faire + +### Option B -- promouvoir `shadow -> WorkflowIR -> ExecutionPlan` + +Chemin : +- capture Lea +- observation shadow +- correction/validation +- compilation +- replay de plan + +Avantage : +- pipeline plus propre +- meilleure explicitation du savoir-faire + +Limite : +- pas encore le flux naturel pilote par le client + +### Option C -- continuer a passer par VWB + +Ce n'est pas recommande comme coeur produit, car cela deplace la reference +du produit vers un outil satellite. + +## 10. Recommandation de lecture + +Si l'objectif est de remettre le produit en ordre, lire dans cet ordre : + +1. `agent_v0/agent_v1/main.py` +2. `agent_v0/agent_v1/network/streamer.py` +3. `agent_v0/server_v1/api_stream.py` +4. `agent_v0/server_v1/stream_processor.py` +5. `agent_v0/server_v1/replay_learner.py` +6. `agent_v0/server_v1/replay_memory.py` +7. `core/workflow/ir_builder.py` +8. `core/workflow/execution_compiler.py` +9. `visual_workflow_builder/backend/api_v3/learned_workflows.py` + +## 11. Conclusion courte + +Le produit reel aujourd'hui n'est pas : +- `VWB -> workflow manuel -> replay` + +Le produit reel aujourd'hui est plutot : +- `Lea observe -> le serveur reconstruit -> Lea rejoue -> le systeme apprend` + +La vraie question n'est donc pas "faut-il garder le VWB au centre ?" + +La vraie question est : + +**quel pipeline Lea-first on officialise comme voie nominale :** +- le replay direct deja branche, +- ou la voie `shadow/IR/ExecutionPlan` qu'il faut maintenant faire monter en puissance ? diff --git a/docs/plans/FEUILLE_DE_ROUTE_PRODUIT_LEA_FIRST_2026-05-19.md b/docs/plans/FEUILLE_DE_ROUTE_PRODUIT_LEA_FIRST_2026-05-19.md new file mode 100644 index 000000000..18d318d73 --- /dev/null +++ b/docs/plans/FEUILLE_DE_ROUTE_PRODUIT_LEA_FIRST_2026-05-19.md @@ -0,0 +1,307 @@ +# Feuille de route produit Lea-first -- 2026-05-19 + +**But** +- Transformer le recadrage technique en arbitrages produit. +- Dire clairement ce qui reste au centre, ce qui sort du centre, et ce qu'on fait monter en puissance. +- Donner une sequence d'execution lisible pour les prochaines semaines. + +## 1. These produit + +Le produit n'est pas un editeur de workflows. + +Le produit est **Lea**, un agent local qui : +- observe un humain, +- rejoue une procedure, +- demande de l'aide quand elle ne sait pas, +- memorise les corrections, +- devient progressivement plus autonome. + +La phrase de reference devrait etre : + +**"Lea apprend par observation et se fiabilise par replay supervise."** + +## 2. Decision structurante + +### Ce qu'on garde au centre + +- `agent_v0/agent_v1/` +- `agent_v0/server_v1/` +- `replay direct` depuis la capture +- `verification post-action` +- `memoire persistante` de replay +- `pause_for_human` et correction humaine + +### Ce qu'on sort du centre + +- `visual_workflow_builder/` comme chemin principal de creation +- la demo GHT comme reference produit +- les benches modeles comme moteur de roadmap +- `agent_chat/` comme facade prioritaire +- les analytics non necessaires a la boucle observation -> replay -> apprentissage + +### Ce qu'on garde mais en satellite + +- `VWB` pour inspection, import/export, correction manuelle ponctuelle +- dashboard pour supervision +- worker `GraphBuilder` +- pipeline `WorkflowIR -> ExecutionPlan` + +## 3. Regle d'arbitrage + +Une fonctionnalite est prioritaire si elle renforce au moins un de ces 4 axes : + +1. Lea observe mieux +2. Lea rejoue plus surement +3. Lea apprend vraiment des corrections +4. Lea sait mieux quand s'arreter et demander de l'aide + +Si une fonctionnalite ne renforce aucun de ces 4 axes, elle ne doit pas etre sur le chemin critique. + +## 4. Ce qu'on garde + +### A. Socle produit a conserver + +- capture Lea enrichie (`captor`, `vision`, `streamer`) +- streaming serveur +- replay pilote par serveur +- `resolve_engine` +- `ReplayLearner` +- `replay_memory` +- `TargetMemoryStore` +- worker de retraitement session -> workflow appris + +### B. Briques d'avenir a conserver + +- `ShadowObserver` +- `ShadowValidator` +- `IRBuilder` +- `ExecutionCompiler` +- `ExecutionPlanRunner` + +Pourquoi les garder : +- elles portent la meilleure cible architecturale moyen terme +- mais ne doivent pas dicter le court terme tant qu'elles ne sont pas sur le chemin nominal + +## 5. Ce qu'on coupe du centre + +### A. Couper du centre ne veut pas dire supprimer + +On parle ici de **deprioriser**, pas de detruire. + +### B. A sortir du role de reference produit + +- `VWB` comme voie principale de creation de procedures +- workflows de demo recables a la main +- chasse aux bugs VWB sans impact sur Lea +- ajout de nouveaux blocs VWB non necessaires au produit +- travaux de design/UI autour d'un canvas qui n'est pas le coeur + +### C. A stopper temporairement + +- benchmarker de nouveaux modeles sans lien avec un echec produit concret +- etendre `agent_chat` tant que Lea n'est pas stabilisee +- pousser des analytics riches avant d'avoir une boucle d'apprentissage fiable +- melanger preuves demo, veille et architecture produit dans la meme priorite + +## 6. Ce qu'on gele + +### Gel court terme + +- nouvelles features VWB +- nouvelles experiences demo-centriques +- nouvelles couches d'orchestration conversationales +- nouvelle dette de docs "vision" sans branchement runtime + +### Autorise pendant le gel + +- correctifs minimaux si un satellite bloque le coeur Lea-first +- documentation de clarification +- import/export necessaire pour debloquer un usage reel + +## 7. Ce qu'on promeut + +### Promotion immediate + +**Voie nominale v1 produit** + +`capture Lea -> replay direct -> verify -> memory` + +C'est le pipeline a assumer officiellement maintenant. + +### Promotion a court terme + +- correction humaine explicite comme mecanisme produit central +- memoire persistante comme actif principal +- supervision humaine native +- runbook operatoire autour de Lea + +### Promotion a moyen terme + +**Voie nominale v2 cible** + +`capture Lea -> shadow/validation -> WorkflowIR -> ExecutionPlan -> replay` + +Cette voie doit etre promue seulement quand elle devient : +- plus simple a operer que le direct replay, +- et vraiment pilotee par Lea, pas par un outillage annexe. + +## 8. Produit cible en 3 etages + +### Etage 1 -- Lea Rejoue + +Promesse : +- Lea peut rejouer de facon supervisee une procedure observee + +Definition of done : +- 1 a 2 workflows reels reproductibles +- pause propre sur incertitude +- reprise fiable +- apprentissage des corrections + +### Etage 2 -- Lea Apprend + +Promesse : +- Lea rejoue mieux la 2e fois que la 1ere + +Definition of done : +- `target_memory.db` utile et alimentee +- hits memoire visibles +- baisse des resolutions couteuses sur cas repetes +- historique de corrections exploitable + +### Etage 3 -- Lea Comprend + +Promesse : +- Lea sait expliciter ce qu'elle a compris pendant l'observation + +Definition of done : +- flux shadow/validation utilisable sans bricolage externe +- generation d'un artefact rejouable propre +- promotion vers `ExecutionPlan` + +## 9. Roadmap pratique + +## Horizon 1 -- Stabiliser le produit reel + +### Objectif + +Faire de `capture -> replay direct -> apprentissage` un vrai produit POC. + +### Priorites + +1. Stabiliser le replay Lea sur 1 ou 2 cas reels +2. Rendre la pause supervisee propre et fiable +3. Aligner la verification et la memorisation +4. S'assurer que les corrections humaines nourrissent vraiment la memoire +5. Ecrire le runbook produit Lea + +### Livrables + +- pipeline nominal documente +- checklist de pre-replay +- 1 workflow canonique Lea-first +- 1 base de memoire non vide et utile + +## Horizon 2 -- Native learning loop + +### Objectif + +Faire de la correction humaine un chemin naturel, pas un mecanisme cache. + +### Priorites + +1. rendre visible l'etat "je sais / je ne sais pas" +2. exposer clairement les corrections memorisees +3. definir les criteres de promotion d'un workflow +4. connecter proprement observation, replay et apprentissage + +### Livrables + +- etat d'apprentissage lisible par workflow +- journal des corrections humaines +- mesure simple du gain de memoire + +## Horizon 3 -- Promotion de la voie compilee + +### Objectif + +Faire monter `shadow -> WorkflowIR -> ExecutionPlan` jusqu'a pouvoir remplacer la voie directe sur certains cas. + +### Priorites + +1. brancher un vrai appelant Lea vers `shadow` +2. rendre la validation utilisateur exploitable +3. compiler automatiquement une session validee +4. comparer replay direct vs replay compile sur un meme cas + +### Livrables + +- 1 flux shadow complet de bout en bout +- 1 `ExecutionPlan` reel lance sans bricolage annexe +- decision go/no-go sur la promotion en voie nominale + +## 10. Non-objectifs explicites + +Ce qu'on ne cherche pas a faire maintenant : + +- faire du VWB le centre du produit +- multiplier les demos au prix du coeur technique +- construire un editeur BPMN generaliste +- benchmarker tout l'ecosysteme VLM +- transformer `agent_chat` en facade principale avant stabilisation Lea + +## 11. Backlog dirigeant + +### Maintenant + +- replay direct fiable +- correction humaine exploitable +- memoire persistante alimentee +- runbook Lea-first + +### Ensuite + +- etat d'apprentissage visible +- shadow utile +- compile vers `ExecutionPlan` + +### Plus tard + +- VWB comme outil de correction avancée +- agent_chat comme surcouche +- analytics et reporting riches + +## 12. Formulation de pilotage + +Si tu dois recadrer une session de travail, la phrase a utiliser est : + +**"Est-ce que ce qu'on fait aide Lea a mieux observer, rejouer, apprendre ou demander de l'aide ?"** + +Si la reponse est non : +- ce n'est pas sur le chemin critique produit + +## 13. Decision finale recommandee + +### Voie nominale officielle a assumer maintenant + +`Lea capture -> serveur construit un replay direct -> Lea execute -> le systeme apprend` + +### Voie nominale cible a construire + +`Lea capture -> Lea explicite ce qu'elle a compris -> validation -> ExecutionPlan -> replay` + +### Statut du VWB + +`outil satellite` + +Pas : +- coeur produit +- reference d'architecture +- source de verite sur ce qu'est Lea + +## 14. Documents lies + +- [CADRAGE_COEUR_PRODUIT_LEA_2026-05-19.md](./CADRAGE_COEUR_PRODUIT_LEA_2026-05-19.md) +- [CARTOGRAPHIE_EXECUTABLE_PIPELINE_LEA_FIRST_2026-05-19.md](./CARTOGRAPHIE_EXECUTABLE_PIPELINE_LEA_FIRST_2026-05-19.md) +- [PLAN_REMISE_EN_ORDRE_POCS_2026-05-19.md](./PLAN_REMISE_EN_ORDRE_POCS_2026-05-19.md) +- [STATUS.md](/home/dom/ai/rpa_vision_v3/docs/STATUS.md) diff --git a/docs/plans/PLAN_ACTION_COLLEGES_2026-05-25_APRES_C1_G2.md b/docs/plans/PLAN_ACTION_COLLEGES_2026-05-25_APRES_C1_G2.md new file mode 100644 index 000000000..35f376fe8 --- /dev/null +++ b/docs/plans/PLAN_ACTION_COLLEGES_2026-05-25_APRES_C1_G2.md @@ -0,0 +1,111 @@ +# Plan d'action collegues — apres C1/G2 + +Date : 2026-05-25 13:41 Europe/Paris +Pilotage : Codex +Cible demo : lundi 1 juin 2026 + +## Etat factuel + +- `rpa-streaming` 5005 : actif. +- `rpa-agent-chat` 5004 : actif apres C1, SocketIO/CORS OK. +- Windows `LeaInteractive` : running, `LEA_FEEDBACK_BUS=1`. +- Inventaire Ollama v2 : 38 tags, reference actuelle. +- `qwen3.5:9b` : calibre avec prefill, resident en `CONTEXT=2048`, `100% GPU`, ~8.6 GB. +- `qwen2.5vl:7b-rpa` : point chaud precedent, peut revenir en `CONTEXT=8192` via appels hardcodes. +- C2 build replay : baseline mesuree 88-94s -> 22-24s avec skip enrichment, speedup ~4x. + +## Probleme restant prioritaire + +C1b a montre que le flag `AutonomousPlanner` ne suffit pas : `agent_chat` charge encore OWL-v2 via `WorkflowPipeline -> UIDetector` au boot. + +Log observe : + +```text +core.detection.owl_detector:Chargement OWL-v2 sur cuda... +core.detection.owl_detector:OWL-v2 chargé +core.detection.ui_detector:✓ OWL-v2 initialized +agent_chat.autonomous_planner:OWL-v2 visual detector skipped at boot +``` + +VRAM observee apres C1b restart : + +```text +agent_chat python3 : 1478 MiB +ollama qwen3.5:9b : 7794-8566 MiB selon releve +``` + +Donc le chantier VRAM n'est pas termine. + +## Repartition + +### Claude — C1c runtime 5004 sans OWL GPU au boot + +Objectif : `rpa-agent-chat` doit servir SocketIO 5004 sans charger OWL-v2/CUDA au boot. + +Actions : + +- Identifier le meilleur point de coupure : `agent_chat/app.py` instancie `WorkflowPipeline()` ; `WorkflowPipeline` instancie `UIDetector` avec `use_owl_detection=True`. +- Proposer patch minimal : + - soit `WorkflowPipeline(enable_ui_detection=False, enable_vlm=False)` dans `agent_chat` si ces fonctions sont hors chemin narration ; + - soit `DetectionConfig.use_owl_detection` pilote par env ; + - soit `AGENT_CHAT_ENABLE_OWL=0` applique aussi a `UIDetector`. +- Tests unitaires. +- Redemarrage seulement apres GO Codex. +- Critere : healthcheck OK, SocketIO OK, `agent_chat` VRAM nettement reduite, pas de log `Chargement OWL-v2 sur cuda`. + +### Claude — C2b instrumentation des 22s restantes + +Objectif : savoir ou part le build skip ~22-24s. + +Actions : + +- Ajouter des spans `[PERF]` autour de crops d'ancrage, cleaning, waits, ReplayLearner. +- Garder harnais `performance` manuel. +- Critere : tableau temps par segment, pas de replay live. + +### Gemini — G2b benchmark qwen3.5 comparatif + +Objectif : dire si `qwen3.5:9b` remplace `qwen2.5vl:7b-rpa`, devient fallback, ou est rejete. + +Actions : + +- Attendre que C1c stabilise VRAM si possible. +- Benchmark court puis complet : + - qwen3.5 avec prefill obligatoire ; + - `num_ctx=2048`, puis 4096 seulement avec GO ; + - 5 screenshots reels ; + - JSON validity, p50/p95, precision coordonnées, VRAM, offload. +- Comparer a `qwen2.5vl:7b-rpa` et `qwen2.5vl:3b` si cela ne perturbe pas un service actif. +- Critere : matrice de decision claire. + +### Gemini — G3c patch proposal politique contexte + +Objectif : fermer les retours accidentels en `CONTEXT=8192`. + +Actions : + +- Proposer patch minimal couvrant : + - executor Windows hardcodes 8192 ; + - `resolve_engine.py` appels sans `num_ctx` ; + - constante/env unique `RPA_VLM_DEFAULT_CTX=2048`. +- Ne pas appliquer. +- Inclure tests attendus et risques. + +### Codex — arbitrage/integration + +Actions : + +- Maintenir healthcheck comme source de verite. +- Autoriser ou refuser les restarts. +- Integrer uniquement les patchs testables. +- Garder le store Ollama en lecture seule. +- Composer les decisions modele/runtime dans un plan demo. + +## Ordre d'execution + +1. C1c Claude : enlever le dernier chargement OWL GPU du 5004. +2. Healthcheck + VRAM. +3. G2b Gemini : benchmark qwen3.5 dans etat VRAM propre. +4. G3c Gemini + implementation Codex/Claude : politique contexte 2048. +5. C2b Claude : instrumentation 22s restantes. +6. Smoke replay controle, seulement apres stabilisation. diff --git a/docs/plans/PLAN_PHASE2_TRACE_MANDAT_PROTOCOLS_2026-05-25.md b/docs/plans/PLAN_PHASE2_TRACE_MANDAT_PROTOCOLS_2026-05-25.md new file mode 100644 index 000000000..90066996e --- /dev/null +++ b/docs/plans/PLAN_PHASE2_TRACE_MANDAT_PROTOCOLS_2026-05-25.md @@ -0,0 +1,274 @@ +# Plan Phase 2 — Trace mandat / protocoles + +Date : 2026-05-25 +Statut : plan d'exécution chirurgical, issu du modèle v0.3 +Principe : additif, testable, sans refactor des gros blocs fragiles + +## Objectif + +Faire circuler un contrat commun dans Léa : + +```text +mandat -> intention -> scène -> affordance -> geste -> retour -> preuve +``` + +Le but n'est pas de remplacer le replay actuel. Le but est de l'enrichir pour que les décisions, vérifications et apprentissages parlent la même langue. + +## Règles de conduite + +```text +Pas de replay live tant que les correctifs locaux ne sont pas testés. +Pas de refactor de api_stream.report_action_result. +Pas de refactor de resolve_engine._resolve_target_sync. +Pas de réveil brutal de LearningManager / ContinuousLearner. +Champs additifs uniquement. +Fallback comportement actuel si trace absente. +LeaBench et tests unitaires avant runtime live. +``` + +## Phase 2.0 — sécuriser le bug live Bloc-notes + +But : + +```text +empêcher qu'un rejet sémantique serveur soit contourné par hybrid_text_direct local +``` + +Actions : + +```text +tester GroundingEngine après rejet serveur explicite ; +vérifier qu'un simple non-trouvé serveur autorise encore le fallback ; +déployer sur Windows seulement après tests ; +ne retenter le replay Bloc-notes qu'après relance Léa. +``` + +Critère de réussite : + +```text +le cas close_tab_out_of_recorded_zone ne produit plus de clic local ; +le report remonte un échec honnête ou une pause, pas success=True. +``` + +## Phase 2.1 — objet trace additif + +But : + +```text +créer un objet commun transportable sans changer le comportement runtime +``` + +Trace minimale : + +```text +mandate_id +intention_id +scene_id +affordance_signature +expected_retour +level_of_delegation +``` + +Emplacement recommandé : + +```text +core/cognition/ +``` + +Critère de réussite : + +```text +tests unitaires de sérialisation ; +aucun appel runtime obligatoire ; +aucune régression si trace absente. +``` + +## Phase 2.2 — enrichissement build-time + +But : + +```text +produire la trace au moment où le serveur compile/enrichit les actions +``` + +Point d'entrée : + +```text +agent_v0/server_v1/stream_processor.py +``` + +Principe : + +```text +étendre les champs déjà produits par l'enrichissement intention/expected_result ; +ne pas rendre gemma4 obligatoire ; +si enrichissement indisponible, garder l'action actuelle. +``` + +Critère de réussite : + +```text +actions compilées contiennent trace quand disponible ; +les anciens workflows restent valides. +``` + +## Phase 2.3 — transport agent + +But : + +```text +faire traverser la trace jusqu'à agent_v1 sans logique métier côté agent +``` + +Principe : + +```text +l'agent transporte et rapporte ; +le serveur reste responsable du protocole ; +l'agent peut utiliser scene_expected pour pré-vérif simple, mais pas choisir un mandat. +``` + +Critère de réussite : + +```text +report contient causal_trace ; +comportement identique si trace absente ; +pause_message peut inclure intention courante. +``` + +## Phase 2.4 — qualification retour + +But : + +```text +rattacher la vérification à l'intention et à la scène +``` + +Briques : + +```text +ReplayVerifier +Validator V2 +AuditTrail +LoopDetector +``` + +Sorties attendues : + +```text +réussite +échec +attente +rupture +doute_localisation +doute_identification +doute_scène +doute_effet +doute_intention +``` + +Critère de réussite : + +```text +un "rien ne change" n'est plus seulement un retry mécanique ; +il est qualifié selon l'intention, la scène et la temporalité attendue. +``` + +## Phase 2.5 — preuve apprenable + +But : + +```text +promouvoir uniquement les retours qualifiés en preuves +``` + +Briques à enrichir : + +```text +replay_learner.ActionOutcome +replay_memory +target_memory_store via shim additif +audit_trail +``` + +Règle : + +```text +pas de changement destructeur de hash mémoire existant sans migration ; +pas de succès appris si correction humaine non qualifiée ; +pas de promotion si semantic_verified est faux ou absent sur action sensible. +``` + +Critère de réussite : + +```text +la mémoire sait distinguer "Enregistrer pour sauvegarder" de "Enregistrer dans une autre intention/scène". +``` + +## Phase 2.6 — délégation tutorée + +But : + +```text +modéliser le niveau d'autonomie par protocole, environnement et tuteur +``` + +Niveaux : + +```text +N0 observation +N1 proposition +N2 exécution supervisée +N3 autonomie de session +N4 autonomie habituelle +``` + +Principe MVP : + +```text +ne pas réveiller LearningManager ; +utiliser l'étage live ; +ajouter un DelegationResolver simple et non bloquant plus tard ; +fallback restrictif si aucune délégation connue. +``` + +## Bancs de validation + +Ordre : + +```text +LeaBench Bloc-notes enrichi +-> tests unitaires Grounding/Dialog/Verifier +-> replay Bloc-notes live +-> LeaBench Easily dérivés des fixtures +-> adaptateur T2A urgences +-> smoke E2E Easily live +``` + +## Décisions déjà prises + +```text +priorité protocoles : mieux connu -> moins risqué -> plus court +précondition plausible : vérifier l'état attendu +risque : dépend du geste, du métier, du mandat et du niveau de Léa validé par tuteur +trace : générée au build, amendable au runtime par intervention humaine explicite +apprentissage : seulement sur résultat qualifié +``` + +## Décisions à ne pas prendre maintenant + +```text +archiver workflow_replay.py +activer DialogResolver serveur en live +réveiller LearningManager / ContinuousLearner +modifier le schéma target_memory.db +refactorer api_stream.py +``` + +## Prochaine action concrète + +```text +Terminer Phase 2.0 : +tester et déployer le verrou GroundingEngine anti-fallback opportuniste, +puis retenter le replay Bloc-notes. +``` diff --git a/docs/plans/PLAN_REMISE_EN_ORDRE_POCS_2026-05-19.md b/docs/plans/PLAN_REMISE_EN_ORDRE_POCS_2026-05-19.md new file mode 100644 index 000000000..2fa09a7d3 --- /dev/null +++ b/docs/plans/PLAN_REMISE_EN_ORDRE_POCS_2026-05-19.md @@ -0,0 +1,217 @@ +# Plan de remise en ordre POCs -- 2026-05-19 + +> Note : ce document a ete ecrit avant le recadrage explicite `Lea-first`. +> Il doit etre relu a la lumiere de +> [CADRAGE_COEUR_PRODUIT_LEA_2026-05-19.md](./CADRAGE_COEUR_PRODUIT_LEA_2026-05-19.md), +> qui distingue proprement coeur produit, VWB/demo et veille. + +**Contexte** +- Les 15 derniers jours ont servi a produire une demo GHT sous forte pression. +- La demo a ete enregistree, mais avec des contournements empiles et une derive de perimetre documentee dans [docs/handoffs/2026-05-19_handoff_post_demo_GHT.md](../handoffs/2026-05-19_handoff_post_demo_GHT.md). +- Les premiers POCs demarrent dans 15 jours. L'objectif n'est pas de "tout reparer", mais de remettre le projet en ordre autour du chemin qui a le plus de valeur terrain. + +## 1. Decision de pilotage + +**Chemin nominal POC** +- `agent_v0/agent_v1/` (Lea) pour la capture et l'execution +- `agent_v0/server_v1/` pour le streaming, le replay et l'orchestration runtime +- `core/` pour la detection, l'extraction, le raisonnement et la supervision + +**Chemin non critique a court terme** +- `visual_workflow_builder/` reste utile pour inspecter, exporter ou bricoler une demo +- `visual_workflow_builder/` ne doit plus etre la source principale de creation des workflows POC tant que ses bugs structurants ne sont pas fermes + +**Raison** +- L'etat reel du projet decrit deja `agent_v1` + streaming + replay comme le socle le plus mature, alors que le VWB reste "en cours" avec bugs runtime connus. +- Le retour d'experience post-demo confirme que la derive VWB a coute du temps et masque des causes racines. + +## 2. Objectif dans 15 jours + +Arriver au debut des POCs avec : +- un chemin `Lea-first` assume et documente +- un socle de replay reproductible sur 1 a 2 cas terrain +- un smoke test canonique de pre-demo / pre-POC +- un registre clair de ce qui est stable, de ce qui est experimental, et des workarounds encore actifs +- une capitalisation utile de la veille, des benches et des lecons apprises + +## 3. Ce qu'on garde, ce qu'on gele, ce qu'on sort du chemin critique + +### A. A garder comme socle produit +- `agent_v0/agent_v1/` +- `agent_v0/server_v1/` +- `core/detection/`, `core/execution/`, `core/llm/`, `core/graph/`, `core/security/` +- la maquette et les assets de demo quand ils servent de terrain d'essai reproductible +- les docs de bench et d'audit qui documentent des decisions reversibles ou des limites prouvees + +### B. A geler temporairement +- creation de workflows complexes par VWB pour les POCs +- nouvelles features UI non necessaires a l'execution terrain +- experimentation libre sur des variantes de modeles ou des sous-systemes non relies au replay + +### C. A sortir du chemin critique +- corrections cosmetiques du VWB +- generalisation de `agent_chat/` tant qu'il n'est pas requis pour le protocole POC +- analytics avancées, reporting, experimentation produit non necessaire au go-live POC + +## 4. Priorites reelles + +### P0 -- cette semaine +1. **Fixer la ligne produit** + - Ecrire noir sur blanc que le chemin POC est `Lea -> streaming -> core`. + - Interdire les nouveaux workflows critiques crees d'abord dans le VWB. + +2. **Geler une baseline de reference** + - Comparer `demo-stable-2026-05-12`, `demo/ght-2026-05-08` et `backup/post-demo-2026-05-19`. + - Sortie attendue : une baseline "reference POC" et une liste des ecarts volontaires ou accidentels. + +3. **Lister les contournements actifs** + - `static_result` / `static_text` dans le replay + - bypass NPM auth hors git + - `cancel-replays.sh` + - pauses humaines remplaçant des automatisations defectueuses + - tout workaround client Lea ou VM encore necessaire + +4. **Construire un smoke test canonique** + - 1 workflow de reference + - 1 environnement de reference + - 1 checklist avant execution + - 1 resultat attendu par etape critique + +5. **Traiter les bugs qui cassent Lea en execution reelle** + - mapping coordonnees Y cote client + - gestion correcte de `replay_paused` + - reset d'etat propre apres replay + - captures runtime utiles au matching + +### P1 -- avant les POCs +1. **Durcir le replay Lea-first** + - harmoniser les comportements strict / legacy la ou ils degradent la supervision + - verifier les delais runtime effectivement appliques + - s'assurer que les annulations et reprises sont propres + +2. **Mettre au propre le protocole d'enregistrement** + - comment on capture un workflow de reference avec Lea + - ce qui est interdit pendant l'enregistrement + - quelles preuves garder quand un replay echoue + +3. **Remettre a niveau la documentation de pilotage** + - `STATUS` + - un runbook POC + - un registre de decisions + - un registre de limites connues + +### P2 -- apres stabilisation du socle +1. **Reprendre le VWB comme chantier separe** + - bug recapture d'anchor + - bug Stop qui ne purge pas la queue + - invalidation cache frontend + - coherence BDD / export / execution + +2. **Reprendre les chantiers exploratoires** + - benchmarks modeles + - agent_chat + - analytics + - autres pistes produit + +## 5. Sprint 15 jours + +### Jours 1 a 3 -- Cadrage et gel +- Choisir la baseline de reference POC. +- Produire une matrice `stable / workaround / casse / hors-scope`. +- Lister les workflows de demo existants et en designer un seul comme workflow canonique. +- Geler les developpements hors chemin critique. + +**Livrable** +- un dossier de reference POC +- une baseline git clairement nommee +- une checklist de reprise + +### Jours 4 a 7 -- Stabilisation du runtime Lea +- Corriger les bugs client / replay qui cassent l'execution reelle. +- Retester sur l'environnement de demo le plus proche du terrain. +- Documenter le protocole de capture et de replay. + +**Livrable** +- 1 smoke test manuel reproductible +- 1 replay de reference stable +- 1 liste limitee de workarounds encore toleres + +### Jours 8 a 10 -- Tesorisation utile +- Trier la doc recente en trois familles : + - preuve exploitable + - decision durable + - archive de session +- Extraire la substance des benches, audits et handoffs dans des documents courts. +- Nettoyer la memoire projet pour que les vraies lecons remontent. + +**Livrable** +- un registre de decisions +- un registre de preuves / benches +- un registre des limites et des dettes actives + +### Jours 11 a 13 -- Preparation POC +- Rejouer le workflow canonique plusieurs fois. +- Tester les preconditions machine, VM, reseau, clipboard, captures, modele VLM. +- Verifier ce qui doit etre industrialise ou ritualise en procedure. + +**Livrable** +- runbook operatoire POC +- checklist pre-demo / pre-POC +- journal d'incidents restant + +### Jours 14 a 15 -- Verrouillage +- Dernier passage sur la doc critique. +- Validation humaine du chemin nominal. +- Gel des modifs non indispensables. + +**Livrable** +- branche de stabilisation POC +- etat de confiance clair +- liste "ne pas toucher" avant demarrage + +## 6. Tesorisation : structure cible + +L'objectif n'est pas de conserver toutes les notes telles quelles. Il faut les condenser. + +### Documents a avoir +- `docs/STATUS.md` + - etat reel par sous-systeme +- `docs/POC_RUNBOOK.md` + - comment lancer, verifier, relancer, diagnostiquer +- `docs/DECISIONS_LOG.md` + - decisions structurantes, datees, avec raison +- `docs/KNOWN_LIMITS.md` + - limites connues, contournements toleres, date de revision +- `docs/BENCHMARKS_INDEX.md` + - index court vers les benches qui comptent vraiment + +### Regle de tri +- un handoff brut ne doit pas rester la seule source d'une decision importante +- une conclusion durable doit etre recopier dans un document stable +- une veille sans impact concret doit sortir du chemin critique documentaire + +## 7. Ce que je ferais en premier, des demain + +1. Choisir une baseline POC de reference. +2. Ecrire un court document "chemin nominal POC". +3. Construire le smoke test canonique. +4. Ouvrir la liste des workarounds actifs et decider lesquels sont acceptables 15 jours, lesquels doivent disparaitre. +5. Ne traiter le VWB que s'il bloque encore le chemin Lea-first. + +## 8. Risques si on ne fait pas ce pivot + +- Repartir dans des corrections VWB qui ne servent pas le demarrage POC +- Continuer a melanger demo, produit, veille et bricolage runtime +- Garder une documentation abondante mais peu actionnable +- Rejouer les memes erreurs de session, faute d'avoir remonte les vraies lecons au bon niveau + +## 9. Sources de reference + +- [docs/handoffs/2026-05-19_handoff_post_demo_GHT.md](../handoffs/2026-05-19_handoff_post_demo_GHT.md) +- [docs/handoffs/2026-05-18_handoff_consolidation.md](../handoffs/2026-05-18_handoff_consolidation.md) +- [docs/STATUS.md](../STATUS.md) +- [docs/CARTE_FONCTIONNELLE_2026-05-08.md](../CARTE_FONCTIONNELLE_2026-05-08.md) +- [docs/AUDIT_BDD_WORKFLOW_2026-05-10.md](../AUDIT_BDD_WORKFLOW_2026-05-10.md) +- [docs/QW_SUITE_MAI.md](../QW_SUITE_MAI.md) +- [docs/BENCH_T2A_DECISION_11DOSSIERS.md](../BENCH_T2A_DECISION_11DOSSIERS.md) +- [docs/AUDIT_MEMOIRE_CLAUDE_2026-05-08.md](../AUDIT_MEMOIRE_CLAUDE_2026-05-08.md) diff --git a/docs/plans/PLAN_STABILISATION_DEMO_2026-06-01.md b/docs/plans/PLAN_STABILISATION_DEMO_2026-06-01.md new file mode 100644 index 000000000..4fa9d0a72 --- /dev/null +++ b/docs/plans/PLAN_STABILISATION_DEMO_2026-06-01.md @@ -0,0 +1,67 @@ +# Plan stabilisation demo Lea — cible lundi 1 juin 2026 + +Date de cadrage : 2026-05-25 12:44 Europe/Paris +Pilotage : Codex +Contexte : la demo client est reportee au lundi 1 juin 2026. On sort du mode rustine J-4 et on vise un systeme propre, mesurable, restaurable. + +## Principes + +1. Pas de restauration destructive ni suppression de modele sans inventaire et accord explicite. +2. Pas de replay live tant que les chemins pause/resume, FeedbackBus et perf ne sont pas instrumentes. +3. Les changements doivent avoir une preuve : test, commande de healthcheck, log cible, ou bench. +4. `C:\rpa_vision` reste le runtime Windows reel ; ne pas resynchroniser `agent_v0/deploy/windows_client`. +5. Les collegues repondent dans `docs/coordination/inbox_codex/` avec ACK/NACK explicite. + +## P0 — Inventaire et protection Ollama + +Objectif : garantir que les modeles critiques existent, sont referencables, et peuvent etre reconstruits si un tag disparait. + +- Figer `ollama list`, manifests, gros blobs, `ollama show --modelfile` des modeles critiques. +- Verifier les artefacts locaux et backup : `t2a-gemma3-27b-q8_0.gguf`, `t2a-gemma3-27b-q4_k_m.gguf`, merged safetensors. +- Produire une table : tag Ollama, digest/blob, source GGUF/HF, backup, statut de reconstruction. +- Identifier les vrais manquants avec Dom si sa liste attendue depasse les 38 tags actuels. + +## P0 — FeedbackBus 5004 propre + +Objectif : garder la narration temps reel si elle est utile, mais sans bruit log ni service fragile. + +- Corriger la cause `rpa-agent-chat.service inactive`. +- Corriger ou isoler le warning CLIP/torch au boot. +- Corriger CORS/SocketIO pour la ChatWindow Windows. +- Conserver le fallback HTTP 5005 pour `resume` / `abort`. +- Decider apres test si `LEA_FEEDBACK_BUS=1` reste actif cote Windows. + +## P0 — Performance mesurable + +Objectif : remplacer les intuitions par des mesures reproductibles. + +- Harness build replay sans live replay : mesure avec/sans `RPA_SKIP_INTENTION_ENRICHMENT`. +- Mesure des appels VLM : modele, `num_ctx`, layers CPU/GPU, p50/p95, taux JSON valide. +- Politique de residence Ollama : `MAX_LOADED_MODELS=1`, modele VLM prechauffe, eviter les swaps texte/VLM. +- Decision documentee : `qwen2.5vl:7b-rpa` vs `qwen2.5vl:7b` vs `qwen2.5vl:3b` vs autre backend. + +## P0 — Replay pause/resume robuste + +Objectif : zero confusion visible dans Lea. + +- La bulle supervisee ne doit plus tronquer le message. +- La bulle doit se fermer a la reprise serveur (`server_cleared`). +- Le compteur et le statut doivent refleter l'etape reelle. +- Smoke Windows obligatoire apres patch deploye. + +## P1 — Hygiene runtime/deploiement + +Objectif : rendre le systeme re-demarrable sans memoire orale. + +- Runbook Linux : `rpa-streaming`, `ollama`, `rpa-agent-chat`. +- Runbook Windows : tache `LeaInteractive`, lock, logs, hash des fichiers deployes. +- Separateur clair : source repo vs runtime `C:\rpa_vision`. + +## P1 — Pack de preuve demo + +Objectif : arriver lundi avec une preuve concrete, pas seulement du code. + +- Healthcheck global : Linux 5005/5004/Ollama + Windows agent. +- Bench perf avant/apres. +- Smoke replay controle, sans improvisation. +- Notes de risques restantes avec mitigation. diff --git a/docs/plans/PROTOCOLE_SEANCE1_MICRO_APPRENTISSAGE_LEA_2026-05-27.md b/docs/plans/PROTOCOLE_SEANCE1_MICRO_APPRENTISSAGE_LEA_2026-05-27.md new file mode 100644 index 000000000..ab2a977a9 --- /dev/null +++ b/docs/plans/PROTOCOLE_SEANCE1_MICRO_APPRENTISSAGE_LEA_2026-05-27.md @@ -0,0 +1,200 @@ +# Protocole seance 1 micro-apprentissage Lea - 2026-05-27 + +## Competence cible + +Premiere competence atomique: + +```text +ouvrir le menu Demarrer +``` + +Raison: ouvrir Chrome, Word ou une recherche depend souvent de cette brique. Si Lea apprend d'abord ce geste simple, observable et verifiable, on pourra composer ensuite: + +```text +ouvrir le menu Demarrer -> rechercher Chrome -> ouvrir Chrome +ouvrir le menu Demarrer -> rechercher Word -> ouvrir Word +``` + +On ne demarre donc pas par un scenario "ouvrir navigateur" complet. On commence plus petit. + +## Duree + +25 minutes maximum: + +- 9 min: 3 demonstrations humaines, +- 5 min: 1 essai supervise de Lea, +- 5 min: 1 variante, +- 6 min: debrief et decision. + +## Avant de commencer + +1. Verifier le socle: + + ```bash + python3 tools/lea_micro_preflight.py + ``` + + Verdict acceptable: tout OK, warning VLM non resident accepte. + +2. Mettre Windows dans un etat simple: + + - bureau visible, + - pas d'application plein ecran, + - NoMachine stable, + - presse-papiers non utilise, + - Lea connectee. + +3. Ne pas ouvrir VWB, Aiva ou un workflow metier. + +## Demonstrations humaines + +Chaque demonstration doit etre lancee avec `Apprenez-moi`, puis arretee avec `Arreter`. + +Nom de session: + +```text +ouvrir le menu Demarrer +``` + +### Demo A - clic souris + +Dom dit ce qu'il fait: + +```text +J'ouvre le menu Demarrer en cliquant sur le logo Windows. +``` + +Actions: + +1. cliquer le logo Windows, +2. attendre que le menu apparaisse, +3. verifier visuellement le champ `Rechercher`, +4. fermer avec `Echap`, +5. arreter l'enregistrement. + +Postcondition: + +- le champ `Rechercher` est visible. + +### Demo B - touche Windows + +Dom dit: + +```text +J'ouvre le meme menu avec la touche Windows. +``` + +Actions: + +1. appuyer sur `Win`, +2. attendre le menu, +3. verifier `Rechercher`, +4. fermer avec `Echap`, +5. arreter l'enregistrement. + +Postcondition: + +- le champ `Rechercher` est visible. + +### Demo C - Win+S + +Dom dit: + +```text +J'ouvre directement la recherche avec Win+S. +``` + +Actions: + +1. appuyer sur `Win+S`, +2. verifier que la zone de recherche est visible et active, +3. fermer avec `Echap`, +4. arreter l'enregistrement. + +Postcondition: + +- la zone de recherche est visible, idealement avec le focus. + +## Essai supervise de Lea + +Lea tente une methode simple, de preference `Win`. + +Message attendu avant action: + +```text +J'ouvre le menu Demarrer avec la touche Windows. +``` + +Verification attendue: + +```text +Je vois le champ Rechercher, comme attendu. +``` + +Si elle echoue, elle s'arrete et affiche: + +```text +J'essaie de : ouvrir le menu Demarrer +J'attendais : voir le champ Rechercher +Je vois : +Peux-tu : ouvrir le menu Demarrer une fois pour me montrer le geste +``` + +## Variante + +Une seule variante pendant cette seance: + +- Notepad ou une autre fenetre simple au premier plan, +- puis Lea tente d'ouvrir le menu Demarrer. + +Critere de succes: + +- le champ `Rechercher` devient visible malgre la fenetre au premier plan. + +On ne teste pas encore DPI ou ecran secondaire pendant cette seance. + +## Donnees a relever + +Pour chaque demonstration et essai: + +- methode: clic logo, Win, Win+S, +- fenetre active avant action, +- marqueur observe: `Rechercher`, +- resultat: succes, correction, echec, +- duree approximative, +- message affiche si blocage. + +## Criteres de succes + +- Lea formule la competence comme une intention, pas comme une coordonnee. +- Lea connait au moins deux methodes pour ouvrir le menu. +- Lea verifie le marqueur `Rechercher`. +- Aucun message generique ne sort: pas `un element`, pas `cette action`, pas `Validation requise`. +- Aucun replay VWB/metier n'est lance. + +## Criteres d'echec + +- Lea clique une coordonnee sans regarder le resultat. +- Lea dit OK alors que le menu n'est pas ouvert. +- Lea demande une aide incomprehensible. +- Lea confond menu Demarrer, bureau, navigateur ou Word. +- La capture/finalisation de session ne produit aucun artefact exploitable. + +## Promotion + +| Etat | Condition | +|---|---| +| OBSERVATION | au moins une demonstration capturee | +| COACHING | Lea peut tenter avec aide humaine disponible | +| AUTO_CANDIDATE | 3 succes verifies sur au moins 2 methodes | +| AUTO | pas pour cette seance; attendre DPI/ecran secondaire | + +## Suite directe + +Si cette seance est propre: + +1. seance 2: saisir une recherche dans le menu Demarrer, +2. seance 3: ouvrir Chrome depuis le menu, +3. seance 4: ouvrir Word depuis le menu, +4. seance 5: fermer Word proprement, +5. seulement ensuite: DPI et ecran secondaire. diff --git a/docs/recherche/AXE_A1_VLM_GROUNDING_SOTA.md b/docs/recherche/AXE_A1_VLM_GROUNDING_SOTA.md new file mode 100644 index 000000000..781de62ce --- /dev/null +++ b/docs/recherche/AXE_A1_VLM_GROUNDING_SOTA.md @@ -0,0 +1,402 @@ +# Axe A1 — État de l'art VLM Grounding UI (2025-2026) + +**Date :** 2026-05-23 +**Auteur :** Agent recherche dispatché (Claude Opus 4.7 1M) +**Périmètre :** modèles VLM de grounding d'éléments UI graphiques, focus 2025-2026, candidats déployables sur RTX 5070 12 GB VRAM, healthtech (licence permissive). +**Source maître interne :** [`SYNTHESE_TECHNOS_REPLAY_2026-05-23.md`](../SYNTHESE_TECHNOS_REPLAY_2026-05-23.md), [`MIGRATION_VLM_PLAN_2026-05-09.md`](../MIGRATION_VLM_PLAN_2026-05-09.md), [`HISTORIQUE_VLM_IMPLEMENTATIONS_2026-05-08.md`](../HISTORIQUE_VLM_IMPLEMENTATIONS_2026-05-08.md) + +> Recherche documentaire — aucun test runtime. Chaque chiffre vient d'un papier, fiche HF ou leaderboard linké. Les scores ScreenSpot-Pro varient parfois de ±3 points entre sources (papier vs leaderboard tiers vs reproduction utilisateur). On affiche systématiquement le chiffre déclaré par les auteurs ou la fiche HF officielle. + +--- + +## 1. TL;DR + +1. **Le SOTA open-source 7B sur ScreenSpot-Pro a doublé en 12 mois** : 18.9 % (OS-Atlas-7B, oct 2024) → 61.6 % (UI-TARS-1.5-7B, avr 2025) → 51.9 % (InfiGUI-G1-7B, aoû 2025, AAAI 2026 Oral). Les fermés (GPT-5.2 à 86 %) creusent encore l'écart mais inutilisables on-premise. +2. **Notre InfiGUI-G1-3B actuel (45.2 % SSPro / 91.1 % SSv2) reste compétitif** pour 3 GB VRAM 4-bit. Le ratio perf/VRAM est excellent. Migration vers le 7B (51.9 % SSPro) faisable sans changer d'architecture (même `Qwen2_5_VLForConditionalGeneration`). +3. **Qwen3-VL-8B-Instruct (oct 2025, Apache 2.0) ne résout PAS le bug d'échelle bbox seul** : même convention post-resize que Qwen2.5-VL. Le fix est dans le **backend** (vLLM/Transformers in-process expose `resized_height/resized_width`), pas dans le modèle. +4. **Approche coordinate-free montante** (GUI-Actor, MolmoPoint-GUI, InfiGUI-G1) : la cible n'est plus du texte JSON mais un token de patch visuel ou des grounding-tokens. Élimine structurellement le bug d'échelle. Mais demande un fork Transformers ou un head custom. + +**Recommandation top 3 pour notre cas (12 GB VRAM, healthtech, licence commerciale OK) :** + +| # | Modèle | Pourquoi | +|---|---|---| +| 1 | **`InfiX-ai/InfiGUI-G1-7B`** ([HF](https://huggingface.co/InfiX-ai/InfiGUI-G1-7B), Apache 2.0) | Continuité totale avec notre stack `core/grounding/`, +5 pts SSPro vs G1-3B, tient en 4-bit NF4 (~6 GB), même format point post-resize que G1-3B donc le bug d'échelle est déjà géré côté `_smart_resize` | +| 2 | **`Hcompany/Holo1.5-7B`** ([HF](https://huggingface.co/Hcompany/Holo1.5-7B), Apache 2.0) | Qwen2.5-VL-7B-Instruct base, **57.9 % SSPro / 93.3 % SSv2**, natif 3840×2160 (utile fenêtres 2560×1600 Easily), entraîné GRPO sur UI réelles | +| 3 | **`ByteDance-Seed/UI-TARS-1.5-7B`** ([HF](https://huggingface.co/ByteDance-Seed/UI-TARS-1.5-7B), Apache 2.0) | **61.6 % SSPro / 94.2 % SSv2** déclarés (mais reproduction utilisateur à ~40-48 % selon [issue #215](https://github.com/bytedance/UI-TARS/issues/215)) — fallback si InfiGUI déçoit en réel | + +--- + +## 2. Table comparative complète + +Légende : +- VRAM : approximation pour inférence single-batch, base BF16 sans optim. `(4-bit ≈ /3)` pour quantif NF4 type bitsandbytes. +- SS = ScreenSpot v1 (1200+ instructions, multi-OS), SSv2 = re-annoté par OS-Atlas (11% corrections), SSPro = professional high-res (1581 instructions, 23 apps, 5 industries, 3 OS, papier ICLR 2025). +- Conv. coord : `0-1000` = normalisé 0-1000 indép. taille image (Qwen2-VL natif). `post-resize` = bbox dans la résolution **après smart_resize** côté modèle (Qwen2.5-VL). `point-token` = grounding via attention sur tokens visuels, pas de texte coord. `abs` = pixel image originale. +- "non trouvé" = pas de chiffre publié dans les sources consultées. + +| Modèle | Params | VRAM BF16 | SS | SSv2 | SSPro | Sortie | Conv. coord | vLLM | Transformers | Licence | Release | HF | +|---|---:|---:|---:|---:|---:|---|---|:-:|:-:|---|---|---| +| **InfiGUI-G1-3B** ⭐ *(actuel)* | 3B | ~6 GB (~3 GB 4-bit) | 90.3 | 91.1 | 45.2 | point JSON | post-resize | ✅ | ✅ | Apache 2.0 | 2025-08-11 | [InfiX-ai/InfiGUI-G1-3B](https://huggingface.co/InfiX-ai/InfiGUI-G1-3B) | +| **InfiGUI-G1-7B** | 7B | ~14 GB (~6 GB 4-bit) | non trouvé | 93.5 | 51.9 | point JSON | post-resize | ✅ | ✅ | Apache 2.0 | 2025-08-11 | [InfiX-ai/InfiGUI-G1-7B](https://huggingface.co/InfiX-ai/InfiGUI-G1-7B) | +| **InfiGUI-R1-3B** *(prédécesseur)* | 3B | ~6 GB | 87.5 | non trouvé | 35.7 | point JSON | post-resize | ✅ | ✅ | Apache 2.0 | 2025-04-20 | [InfiX-ai/InfiGUI-R1-3B](https://huggingface.co/InfiX-ai/InfiGUI-R1-3B) | +| **UI-TARS-1.5-7B** | 7B | ~14 GB | non trouvé | 94.2 | 61.6 *(48 reprod.)* | action DSL `click(x,y)` | abs px | ✅ | ✅ | Apache 2.0 | 2025-04-16 | [ByteDance-Seed/UI-TARS-1.5-7B](https://huggingface.co/ByteDance-Seed/UI-TARS-1.5-7B) | +| **Qwen3-VL-8B-Instruct** | 9B | ~18 GB (~6 GB 4-bit) | ~94 (déclaré) | non trouvé | 54.6 *(leaderboard llm-stats)* / 61.8 *(papier)* | bbox_2d ou point JSON | post-resize (multiples 32) | ✅ (vllm≥0.11) | ✅ | Apache 2.0 | 2025-10-15 | [Qwen/Qwen3-VL-8B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-8B-Instruct) | +| **Qwen3-VL-4B-Instruct** | 4B | ~8 GB | non trouvé | non trouvé | 59.5 *(leaderboard)* | bbox_2d ou point JSON | post-resize | ✅ | ✅ | Apache 2.0 | 2025-10-15 | [Qwen/Qwen3-VL-4B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct) | +| **Qwen2.5-VL-7B-Instruct** *(legacy)* | 7B | ~14 GB | 88.8 | 88.8 | 26.8 | bbox_2d JSON | post-resize (multiples 28) | ✅ | ✅ | Apache 2.0 | 2025-01 | [Qwen/Qwen2.5-VL-7B-Instruct](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct) | +| **Holo1.5-7B** ⭐ | 7B | ~14 GB | non trouvé | 93.31 | 57.94 | non documenté (probable point) | non documenté | ✅ | ✅ | Apache 2.0 | 2025-09 | [Hcompany/Holo1.5-7B](https://huggingface.co/Hcompany/Holo1.5-7B) | +| **Holo1.5-3B** | 3B | ~6 GB | non trouvé | non trouvé | non trouvé | idem | idem | ✅ | ✅ | Apache 2.0 | 2025-09 | [Hcompany/Holo1.5-3B](https://huggingface.co/Hcompany/Holo1.5-3B) | +| **Holo1-7B** *(v1)* | 7B | ~14 GB | non trouvé (avg UI 76.2) | non trouvé | non trouvé | non documenté | non documenté | ✅ | ✅ | Apache 2.0 | 2025-06 | [Hcompany/Holo1-7B](https://huggingface.co/Hcompany/Holo1-7B) | +| **OS-Atlas-Base-7B** | 8B | ~16 GB | 82.5 *(papier)* | 85.1 *(InfiGUI eval)* | 18.9 | bbox + point JSON | 0-1000 normalisé | ✅ | ✅ | Apache 2.0 | 2024-10-30 | [OS-Copilot/OS-Atlas-Base-7B](https://huggingface.co/OS-Copilot/OS-Atlas-Base-7B) | +| **OS-Atlas-Base-4B** | 4B | ~8 GB | non trouvé | non trouvé | non trouvé | bbox + point JSON | 0-1000 normalisé | ✅ | ✅ | Apache 2.0 | 2024-10-30 | [OS-Copilot/OS-Atlas-Base-4B](https://huggingface.co/OS-Copilot/OS-Atlas-Base-4B) | +| **UGround-V1-7B** ⭐ | 7B | ~14 GB | 86.3 | non trouvé | non trouvé (probable ~36 papier) | point `(x,y)` | 0-1000 normalisé | ✅ | ✅ | Apache 2.0 | 2024-10-07 / révisé 2025-01 | [osunlp/UGround-V1-7B](https://huggingface.co/osunlp/UGround-V1-7B) | +| **UGround-V1-2B** | 2B | ~4 GB | non trouvé | non trouvé | non trouvé | point `(x,y)` | 0-1000 | ✅ | ✅ | Apache 2.0 | 2025-01 | [osunlp/UGround-V1-2B](https://huggingface.co/osunlp/UGround-V1-2B) | +| **UGround-V1-72B** | 72B | ~144 GB | non trouvé | non trouvé | 34.5 *(papier SSPro orig.)* | point `(x,y)` | 0-1000 | ✅ | ✅ | Apache 2.0 | 2025-01 | [osunlp/UGround-V1-72B](https://huggingface.co/osunlp/UGround-V1-72B) | +| **Magma-8B** | 9B | ~18 GB | mobile 59.5 / desktop 64.1 / web 60.6 | non trouvé | non trouvé | Set-of-Mark + bbox | non documenté | ⚠️ fork transfo | ✅ (fork) | **MIT** | 2025-02-18 | [microsoft/Magma-8B](https://huggingface.co/microsoft/Magma-8B) | +| **GUI-Actor-7B-Qwen2.5-VL** | 8B | ~16 GB | non trouvé | 92.1 | 44.6 | **special token attention head** → `topk_points` normalisés | normalisé 0-1 (sans texte coord) | ⚠️ pas mentionné | ✅ (fork) | **MIT** | 2025-06-03 | [microsoft/GUI-Actor-7B-Qwen2.5-VL](https://huggingface.co/microsoft/GUI-Actor-7B-Qwen2.5-VL) | +| **GUI-Actor-7B-Qwen2-VL** | 8B | ~16 GB | non trouvé | 89.5 | 40.7 | idem | idem | ⚠️ | ✅ (fork) | MIT | 2025-06 | [microsoft/GUI-Actor-7B-Qwen2-VL](https://huggingface.co/microsoft/GUI-Actor-7B-Qwen2-VL) | +| **MolmoPoint-GUI-8B** ⭐ | 9B | ~18 GB | non trouvé | non trouvé | **61.1** (open SOTA) | grounding-tokens `[id,img,x,y]` | abs px | ❌ (logits processor custom) | ✅ | Apache 2.0 | 2026-03-18 | [allenai/MolmoPoint-GUI-8B](https://huggingface.co/allenai/MolmoPoint-GUI-8B) | +| **AGUVIS-7B-720P** | 8B | ~16 GB | 84.4 *(papier)* | non trouvé | 22.9 | bbox + action plan | non documenté (probable post-resize Qwen2-VL) | ✅ | ✅ | non trouvé (probable Apache via base) | 2024-12 | [xlangai/Aguvis-7B-720P](https://huggingface.co/xlangai/Aguvis-7B-720P) | +| **ShowUI-2B** | 2B | ~4 GB | 75.1 | non trouvé | 7.7 | point + action dict | normalisé 0-1 | ✅ | ✅ | **MIT** | 2024-11-26 | [showlab/ShowUI-2B](https://huggingface.co/showlab/ShowUI-2B) | +| **CogAgent-9B-20241220** | 14B (9B lang + 5B vision) | ~28 GB | leader cité, score précis non publié | non trouvé | non trouvé | `CLICK(box=[x1,y1,x2,y2])` action DSL | non documenté (probable abs sur 1120×1120) | ⚠️ partiel | ✅ | **Other** (custom, non-Apache) | 2024-12-20 | [zai-org/cogagent-9b-20241220](https://huggingface.co/zai-org/cogagent-9b-20241220) | +| **SeeClick** *(historique)* | 9.6B | ~19 GB | 53.4 | non trouvé | <10 (papier ScreenSpot-Pro) | bbox via Qwen-VL | non documenté | ❌ | ✅ | Apache 2.0 | 2024-04 (ACL 2024) | [cckevinn/SeeClick](https://huggingface.co/cckevinn/SeeClick) | +| **GUI-G2-7B** | 7B | ~14 GB | SOTA déclaré | SOTA déclaré | SOTA déclaré (Gaussian reward GRPO) | non documenté | non documenté | ✅ | ✅ | non trouvé | 2026-01 (AAAI 2026) | [inclusionAI/GUI-G2-7B](https://huggingface.co/inclusionAI/GUI-G2-7B) | +| **GPT-5.2** *(fermé)* | n/a | n/a (cloud) | — | — | **86.3** | n/a | n/a | n/a | n/a | OpenAI propriétaire | 2026 | n/a | +| **Gemini 3 Pro** *(fermé)* | n/a | n/a (cloud) | — | — | 72.7 | n/a | n/a | n/a | n/a | Google propriétaire | 2026 | n/a | + +**Sources des scores principaux :** [ScreenSpot-Pro leaderboard llm-stats](https://llm-stats.com/benchmarks/screenspot-pro), [papier ScreenSpot-Pro arXiv:2504.07981](https://arxiv.org/abs/2504.07981), [InfiGUI-G1 paper arXiv:2508.05731](https://arxiv.org/abs/2508.05731), fiches HF citées colonne droite. + +--- + +## 3. Fiches détaillées par modèle + +### 3.1. InfiGUI-G1-3B / 7B (notre modèle actuel + upgrade direct) + +- **Repo HF :** [InfiX-ai/InfiGUI-G1-3B](https://huggingface.co/InfiX-ai/InfiGUI-G1-3B), [InfiX-ai/InfiGUI-G1-7B](https://huggingface.co/InfiX-ai/InfiGUI-G1-7B) +- **Papier :** [arXiv:2508.05731](https://arxiv.org/abs/2508.05731) — *InfiGUI-G1: Advancing GUI Grounding with Adaptive Exploration Policy Optimization*, AAAI 2026 Oral +- **GitHub :** [InfiXAI/InfiGUI-G1](https://github.com/InfiXAI/InfiGUI-G1) +- **Release :** 2025-08-11 +- **Licence :** Apache 2.0 +- **Base :** Qwen2.5-VL-3B (resp. 7B), GRPO via AEPO (Adaptive Exploration Policy Optimization) +- **Bench :** ScreenSpot 90.3 (3B) / 92.5+ (7B), SSv2 91.1 / 93.5, **SSPro 45.2 (3B) / 51.9 (7B)**, MMBench-GUI L2 73.4 (3B) / 80.8 (7B) +- **Sortie :** point JSON `[{"point_2d": [x, y]}, …]`, coordonnées **post-resize** (le prompt expose `{new_width}x{new_height}` au modèle, mapping à faire client side) +- **Code grounding minimal :** + ```python + # Sortie typique + # [{"point_2d": [421, 612], "label": "OK button"}] + # Mapping coords: + original_x = int(coords[0] / new_width * original_width) + original_y = int(coords[1] / new_height * original_height) + ``` +- **Pourquoi pertinent chez nous :** déjà câblé (`core/grounding/server.py` + `infigui_worker.py` + `infigui_server.py`), `_smart_resize` factor 28 calibré. Passage 3B → 7B = changement de `MODEL_ID` (env `GROUNDING_MODEL`). VRAM 4-bit ≈ 6 GB, tient sur RTX 5070. + +### 3.2. UI-TARS-1.5-7B (ByteDance) + +- **Repo HF :** [ByteDance-Seed/UI-TARS-1.5-7B](https://huggingface.co/ByteDance-Seed/UI-TARS-1.5-7B) +- **Papier :** [arXiv:2501.12326](https://arxiv.org/abs/2501.12326), UI-TARS-2 technical report [arXiv:2509.02544](https://arxiv.org/html/2509.02544v1) +- **GitHub :** [bytedance/UI-TARS](https://github.com/bytedance/ui-tars), desktop [bytedance/UI-TARS-desktop](https://github.com/bytedance/UI-TARS-desktop) +- **Release :** 2025-04-16 +- **Licence :** Apache 2.0 +- **Bench déclarés :** SSv2 94.2, **SSPro 61.6** — MAIS [issue #215](https://github.com/bytedance/UI-TARS/issues/215) signale reproduction à ~40-48 % selon prompt +- **Sortie :** action DSL natif `click(start_box='[x1,y1,x2,y2]')`, coordonnées **pixels absolues** sur image originale +- **Note :** UI-TARS-2 (sept 2025) existe mais pas open-source à date des sources consultées (technical report only). Continuer sur 1.5. +- **Risque :** asymétrie déclaré/reproduit. Tester localement avant migration. + +### 3.3. Qwen3-VL-8B-Instruct (cible migration plan 9 mai) + +- **Repo HF :** [Qwen/Qwen3-VL-8B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-8B-Instruct), quantif [cpatonn/Qwen3-VL-8B-Instruct-AWQ-8bit](https://huggingface.co/cpatonn/Qwen3-VL-8B-Instruct-AWQ-8bit), [cyankiwi/Qwen3-VL-8B-Instruct-AWQ-4bit](https://huggingface.co/cyankiwi/Qwen3-VL-8B-Instruct-AWQ-4bit) +- **GitHub :** [QwenLM/Qwen3-VL](https://github.com/QwenLM/Qwen3-VL) +- **Release :** 2025-10-15 +- **Licence :** Apache 2.0 +- **Bench :** [llm-stats SSPro 54.6](https://llm-stats.com/benchmarks/screenspot-pro) (rank 16), mais codersera blog cite ~94 % ScreenSpot et 61.8 % SSPro pour la variante computer-use de Qwen3-VL +- **Sortie :** flexible, supporte `bbox_2d` ET `point` selon prompt. Conv. **post-resize multiples de 32** (≠ Qwen2.5-VL qui était multiples de 28 — **DETTE-014 du repo s'aligne désormais sur cette nouvelle factor 32**) +- **Resize :** le modèle expose `resized_width` et `resized_height` en paramètres directs (cf. GitHub Qwen3-VL "Directly set resized_height and resized_width. These values will be rounded to the nearest multiple of 32") +- **Support vLLM :** `vllm>=0.11.0` requis +- **Pourquoi vigilant :** le bench llm-stats positionne Qwen3-VL-8B-Instruct à 54.6 % SSPro, **moins bon que InfiGUI-G1-7B (51.9 %)... attendez non, 54.6 > 51.9**. À 3 pts d'écart, dans la marge d'erreur protocole. Le 4B (59.5 % rank 12) est curieusement meilleur que le 8B (54.6 %), à investiguer. + +### 3.4. Holo1.5-7B (H Company) + +- **Repo HF :** [Hcompany/Holo1.5-7B](https://huggingface.co/Hcompany/Holo1.5-7B), variantes [3B](https://huggingface.co/Hcompany/Holo1.5-3B) et [72B](https://huggingface.co/Hcompany/Holo1.5-72B) +- **Blog :** [HF blog Holo1](https://huggingface.co/blog/Hcompany/holo1), [GRPO for GUI Grounding](https://huggingface.co/blog/HelloKKMe/grounding-r1) +- **Papier (Holo1) :** [arXiv:2506.02865](https://arxiv.org/pdf/2506.02865) *Surfer-H Meets Holo1* +- **Release :** v1 juin 2025, v1.5 septembre 2025 +- **Licence :** Apache 2.0 +- **Base :** Qwen2.5-VL-7B-Instruct +- **Bench :** **SSv2 93.31 %, SSPro 57.94 %**, WebClick 90.24 % — natif 3840×2160 +- **Sortie :** non documenté dans la fiche HF directement (probable point format Qwen2.5-VL-like) +- **Pourquoi pertinent :** entraîné spécifiquement multi-environnements (web + desktop + mobile) avec GRPO, score SSPro très solide pour Apache 2.0. Probable swap drop-in dans `core/grounding/server.py` (même architecture Qwen2_5_VLForConditionalGeneration). + +### 3.5. UGround-V1 (OSU NLP, ICLR'25 Oral) + +- **Repo HF :** [osunlp/UGround-V1-7B](https://huggingface.co/osunlp/UGround-V1-7B), [2B](https://huggingface.co/osunlp/UGround-V1-2B), [72B](https://huggingface.co/osunlp/UGround-V1-72B) +- **Papier :** [arXiv:2410.05243](https://arxiv.org/abs/2410.05243), ICLR 2025 Oral +- **GitHub :** [OSU-NLP-Group/UGround](https://github.com/OSU-NLP-Group/UGround) +- **Licence :** Apache 2.0 +- **Base :** Qwen2-VL-7B-Instruct +- **Bench :** ScreenSpot 86.3 % moyenne (texte/icône desktop/mobile/web 76-93 %), UGround-V1-72B cité 34.5 % sur SSPro (papier original SSPro) +- **Sortie :** point unique `(x, y)` en string, **convention normalisée [0, 1000)** indépendante de l'image (héritage Qwen2-VL) +- **Avantage :** convention 0-1000 = pas de bug d'échelle post-resize. Le modèle a appris à raisonner dans un espace canonique. +- **Inconvénient :** absence d'évaluation SSPro publique pour le 7B (à part le 72B). Compatible bbox = non (point only). + +### 3.6. OS-Atlas (UCSD / Shanghai AI Lab) + +- **Repo HF :** [OS-Copilot/OS-Atlas-Base-7B](https://huggingface.co/OS-Copilot/OS-Atlas-Base-7B), [Base-4B](https://huggingface.co/OS-Copilot/OS-Atlas-Base-4B), [Pro-7B](https://huggingface.co/OS-Copilot/OS-Atlas-Pro-7B), [Pro-4B](https://huggingface.co/OS-Copilot/OS-Atlas-Pro-4B) +- **Papier :** [arXiv:2410.23218](https://arxiv.org/abs/2410.23218), NeurIPS 2024 +- **GitHub :** [OS-Copilot/OS-Atlas](https://github.com/OS-Copilot/OS-Atlas) +- **Release :** 2024-10-30 +- **Licence :** Apache 2.0 +- **Base Base-7B :** Qwen2-VL-7B-Instruct, dataset 13M éléments GUI cross-platform +- **Bench :** ScreenSpot 82.5 % avg, SSv2 85.1 %, **SSPro 18.9 %** *(plus bas que tous les modèles 2025)* +- **Sortie :** bbox + point JSON, **normalisé 0-1000** (Qwen2-VL natif) +- **Statut :** point de référence historique. Surclassé par tous les modèles 2025 sur SSPro. À retenir uniquement pour SSv2 / desktop low-res "tabs simples". + +### 3.7. AGUVIS-7B (Salesforce + HKU) + +- **Repo HF :** [xlangai/Aguvis-7B-720P](https://huggingface.co/xlangai/Aguvis-7B-720P) +- **Papier :** voir [paper list OSU](https://github.com/OSU-NLP-Group/GUI-Agents-Paper-List/blob/main/paper_by_key/paper_visual_grounding.md) +- **Release :** 2024-12 +- **Licence :** non explicite sur fiche HF (probable Apache 2.0 via base) +- **Base :** Qwen2-VL +- **Bench :** ScreenSpot 84.4 % papier, **SSPro 22.9 %** +- **Sortie :** bbox + action plan (training en 2 étapes : grounding puis action) +- **Statut :** intéressant historiquement (pure-vision unified framework Salesforce). Score SSPro faible vs cohorte 2025. Pas prioritaire. + +### 3.8. Magma-8B (Microsoft, CVPR 2025) + +- **Repo HF :** [microsoft/Magma-8B](https://huggingface.co/microsoft/Magma-8B) +- **Papier :** [arXiv:2502.13130](https://arxiv.org/abs/2502.13130), CVPR 2025 +- **GitHub :** [microsoft/Magma](https://github.com/microsoft/Magma) +- **Release :** 2025-02-18 +- **Licence :** **MIT** (très permissive) +- **Bench :** ScreenSpot mobile 59.5 / desktop 64.1 / web 60.6 — **pas d'éval SSPro publiée** +- **Sortie :** Set-of-Mark (marques numérotées sur image) + Trace-of-Mark (vidéo). Hybride GUI + robotique +- **Inconvénient majeur :** nécessite **fork custom de Transformers** (`git+https://github.com/jwyang/transformers.git@dev/jwyang-v4.48.2`), pas de support vLLM standard +- **Pertinent si :** intérêt cross-domaine (GUI + robotique). Pour pure GUI, autres modèles font mieux. + +### 3.9. GUI-Actor-7B-Qwen2.5-VL (Microsoft, NeurIPS 2025) + +- **Repo HF :** [microsoft/GUI-Actor-7B-Qwen2.5-VL](https://huggingface.co/microsoft/GUI-Actor-7B-Qwen2.5-VL), variante [Qwen2-VL](https://huggingface.co/microsoft/GUI-Actor-7B-Qwen2-VL) +- **Papier :** [arXiv:2506.03143](https://arxiv.org/abs/2506.03143), NeurIPS 2025 — *GUI-Actor: Coordinate-Free Visual Grounding for GUI Agents* +- **GitHub :** [microsoft/GUI-Actor](https://github.com/microsoft/GUI-Actor) +- **Release :** 2025-06-03 +- **Licence :** MIT +- **Bench :** SSv2 92.1, **SSPro 44.6** (sans verifier), avec verifier monte +- **Sortie :** **coordinate-free** — attention-based action head qui pointe directement vers les patches visuels. Output décodé en `topk_points` (coordonnées normalisées 0-1, sans génération texte) +- **Avantage théorique majeur :** élimine structurellement le bug d'échelle. Le modèle aligne directement un token spécial avec les patches visuels pertinents. +- **Inconvénient :** demande fork custom (`Qwen2_5_VLForConditionalGenerationWithPointer`), pas de support vLLM standard mentionné. +- **Intérêt R&D :** valider la direction "coordinate-free" comme architecturale pour la v2 grounding. + +### 3.10. MolmoPoint-GUI-8B (Allen AI, mars 2026) + +- **Repo HF :** [allenai/MolmoPoint-GUI-8B](https://huggingface.co/allenai/MolmoPoint-GUI-8B) +- **Blog :** [MolmoPoint blog Ai2](https://allenai.org/blog/molmopoint) +- **Papier :** voir blog (référence papier non explicite dans nos sources) +- **GitHub :** [allenai/molmo2](https://github.com/allenai/molmo2) +- **Release :** mars 2026 +- **Licence :** Apache 2.0 (recherche/éducation, Responsible Use Guidelines Ai2) +- **Base :** Qwen3-8B + MolmoPoint-8B finetuning +- **Bench :** **SSPro 61.1 (SOTA open)**, OSWorldG 70.0 +- **Sortie :** grounding-tokens `[object_id, image_num, x, y]`, **coords pixels absolues** (pas post-resize !) +- **Données training :** MolmoPoint-GUISyn = 36k screenshots synthétiques HR (desktop + web + mobile) +- **Inconvénient :** **pas de support vLLM** (logits processor custom requis), single-image only, pas de support training prod +- **Note pour nous :** **score SSPro le plus élevé parmi les open-source**, et conv. coord absolue = AUCUN bug d'échelle. Mais intégration plus lourde (custom logits processor). + +### 3.11. CogAgent-9B-20241220 (Zhipu / THUDM) + +- **Repo HF :** [zai-org/cogagent-9b-20241220](https://huggingface.co/zai-org/cogagent-9b-20241220) +- **Papier :** [arXiv:2312.08914](https://arxiv.org/abs/2312.08914) (v1), v2 dec 2024 sans papier dédié +- **GitHub :** [zai-org/CogAgent](https://github.com/zai-org/CogAgent) +- **Release :** 2024-12-20 (v2) +- **Licence :** **Other** (Custom Zhipu License, non Apache — vérifier compat commerciale healthtech !) +- **Base :** GLM-4V-9B (14B total : 9B language + 5B vision) +- **Bench :** "leader cité" sur ScreenSpot vs GPT-4o/Claude/SeeClick mais **chiffre SSPro précis non publié dans les sources consultées** +- **Sortie :** action DSL `CLICK(box=[[x1,y1,x2,y2]], element_info='...')`, conv. probablement absolue sur 1120×1120 +- **Risque licence :** "Other" custom, à valider ligne par ligne avant production commerciale. + +### 3.12. ShowUI-2B (Show Lab, CVPR 2025) + +- **Repo HF :** [showlab/ShowUI-2B](https://huggingface.co/showlab/ShowUI-2B) +- **Papier :** [arXiv:2411.17465](https://arxiv.org/abs/2411.17465), CVPR 2025 +- **GitHub :** [showlab/ShowUI](https://github.com/showlab/ShowUI) +- **Release :** 2024-11-26 +- **Licence :** MIT +- **Base :** Qwen2-VL-2B-Instruct +- **Bench :** ScreenSpot 75.1 % (zéro-shot), **SSPro 7.7 %** *(très faible — modèle 2B léger)* +- **Sortie :** point normalisé 0-1 + action dict structuré +- **Pertinent si :** contrainte VRAM extrême (4 GB), workflow simple, fenêtres low-res. Pas pour Easily 2560×1600. +- **Successeur FocusUI** ([CVPR 2026](https://github.com/showlab/FocusUI)) : framework token pruning sur Qwen2.5-VL / Qwen3-VL multi-sizes, outperforme SOTA précédents. + +### 3.13. SeeClick (référence historique, ACL 2024) + +- **Repo HF :** [cckevinn/SeeClick](https://huggingface.co/cckevinn/SeeClick) +- **Papier :** [arXiv:2401.10935](https://arxiv.org/abs/2401.10935), ACL 2024 +- **GitHub :** [njucckevin/SeeClick](https://github.com/njucckevin/SeeClick) +- **Release :** 2024-04 +- **Licence :** Apache 2.0 +- **Base :** Qwen-VL ≈9.6B + LoRA finetune +- **Bench :** ScreenSpot 53.4 % moyenne (Windows text 55.7, Windows icon/widget 32.5), **SSPro <10 % (papier SSPro orig.)** +- **Statut chez nous :** déjà testé, retiré de `intelligent_executor.py` au commit `d1b556b6c` (avril 2026, "cassé"). À NE PAS réutiliser. + +### 3.14. GUI-G2-7B (Zhejiang Univ / inclusionAI, AAAI 2026) + +- **Repo HF :** [inclusionAI/GUI-G2-7B](https://huggingface.co/inclusionAI/GUI-G2-7B) +- **GitHub :** [ZJU-REAL/GUI-G2](https://github.com/ZJU-REAL/GUI-G2) +- **Papier :** AAAI 2026 *GUI-G²: Gaussian Reward Modeling for GUI Grounding* +- **Innovation :** Gaussian reward modeling pour RL — récompense continue scalée selon la taille de l'élément cible (≠ binaire). Pertinent pour icônes petites en haute-res (cas Easily Assure). +- **Bench :** SOTA sur ScreenSpot/SSv2/SSPro déclaré (papier InfiGUI cite GUI-G2-7B à 47.5 % SSPro) +- **Statut :** récent (jan 2026), à surveiller mais pas encore largement reproduit publiquement. + +### 3.15. UI-Venus (inclusionAI, 2025-2026) + +- **GitHub :** [inclusionAI/UI-Venus](https://github.com/inclusionAI/UI-Venus) +- **Statut :** signalé dans recherche comme native UI agent screenshot-only. Pas d'évaluation détaillée trouvée dans nos sources. + +### 3.16. Florence-2 (Microsoft) — hors scope GUI + +- **Note :** modèle 0.27B encodant les coords comme tokens, **non entraîné sur UI** (object/phrase grounding général). Cité pour complétude — **PAS adapté** au cas GUI, à éliminer. + +--- + +## 4. Analyse Qwen3-VL vs InfiGUI-G1 vs OS-Atlas vs Magma sur notre cas usage + +Périmètre concret : Windows desktop Easily Assure, fenêtre 2560×1600 souvent croppée par mss à 2560×60 (bug DETTE séparé), 22+ steps mixant tabs, dropdowns, dialogues modaux, boutons de toolbar, champs de saisie. + +| Critère | InfiGUI-G1-7B (upgrade direct) | Qwen3-VL-8B-Instruct (plan migration) | OS-Atlas-Base-7B (référence 2024) | Magma-8B (Microsoft hybrid) | +|---|---|---|---|---| +| **VRAM 4-bit RTX 5070 12 GB** | ~6 GB ✅ | ~6 GB ✅ | ~6 GB ✅ | ~8 GB ⚠️ (fork transfo) | +| **ScreenSpot-Pro (SSPro)** | 51.9 ✅ | 54.6 ✅ | 18.9 ❌ | non publié SSPro | +| **Convention coords** | post-resize (factor 28) — `_smart_resize` déjà en place | post-resize (factor 32) — DETTE-014 à recaler | 0-1000 normalisé — **pas de bug d'échelle** | SoM/marks, complexe | +| **Bug d'échelle bbox_2d évité ?** | non par construction, mais `_smart_resize` côté serveur OK si bien calibré | non par construction, idem (factor 32 ≠ 28 → recalibration) | **OUI** (0-1000 indépendant) | non documenté | +| **Format sortie** | point JSON `point_2d` | bbox_2d OU point JSON | bbox + point JSON | SoM (numéros sur image) | +| **vLLM support** | ✅ natif | ✅ (vllm≥0.11) | ✅ | ❌ fork custom | +| **Continuité code existant** | **maximale** — même architecture `Qwen2_5_VLForConditionalGeneration`, mêmes prompts, juste `MODEL_ID` à changer | moyenne — Qwen3-VL = nouvelle architecture, factor 32 ≠ 28, prompts à adapter (think:false, num_predict≥128) | bonne (Qwen2-VL base) — mais format coord 0-1000 → tout le parsing à refaire | faible — fork transfo, head SoM, parser custom | +| **Healthtech licence commerciale** | ✅ Apache 2.0 | ✅ Apache 2.0 | ✅ Apache 2.0 | ✅ MIT (encore plus permissive) | +| **Risque démo (Easily 2560×1600)** | bas | moyen (recalage factor 32 + DETTE-014 + nouveaux prompts) | élevé (SSPro 18.9 = grosses erreurs sur dialogues complexes) | élevé (intégration custom) | +| **Effort migration** | ~1 jour | ~3-5 jours | ~2-3 jours (réécrire parser 0-1000) | ~1 semaine + intégration spéciale | + +**Conclusion comparative :** **InfiGUI-G1-7B est l'upgrade le plus rapide et le moins risqué**. Qwen3-VL-8B est techniquement aussi bon mais demande de recalibrer `_smart_resize` (DETTE-014 documente déjà le piège factor 28 vs 32). OS-Atlas perd 30+ pts SSPro vs cohorte 2025 mais offre la convention 0-1000 qui élimine le bug d'échelle. Magma intéressant en R&D, pas en production court terme. + +--- + +## 5. Bug d'échelle bbox_2d : quels modèles l'évitent + +Rappel du bug (cf. [`MIGRATION_VLM_PLAN_2026-05-09.md`](../MIGRATION_VLM_PLAN_2026-05-09.md) §1.2) : les coordonnées renvoyées sont dans la résolution **post-`smart_resize`** appliquée par le modèle, mais le code prod divise par `orig_w` au lieu de `resized_w` → toutes les coords shiftées top-left. Ollama n'expose pas `resized_dimensions`, d'où impossibilité de fixer côté client. + +### Modèles SANS bug d'échelle (par construction) + +1. **UGround-V1 (toutes tailles)** — sortie en `[0, 1000)` normalisé, parser officiel `actual_x = (x / 1000) * image_width`. Le modèle a appris à raisonner dans un espace canonique indépendant de la résolution réelle. +2. **OS-Atlas-Base-7B / Base-4B** — sortie normalisée 0-1000 (héritage Qwen2-VL). Pas d'aller-retour resize → coord. +3. **MolmoPoint-GUI-8B** — sortie en pixels absolus (le grounding-token est décodé en (x, y) image originale). Aucune transformation à faire côté client. +4. **GUI-Actor-7B-Qwen2.5-VL** — sortie en `topk_points` normalisés 0-1, sans texte coordonnées (attention head sur patches visuels). **Architecturalement coordinate-free** = élimination radicale du bug. +5. **UI-TARS-1.5-7B** — sortie en pixels absolus dans le DSL `click(start_box='[x1,y1,x2,y2]')`. Documenté ainsi, mais le modèle a un smart_resize interne dont la cohérence avec son DSL est à vérifier en réel (issue #215 GitHub suggère reproduction inconstante). + +### Modèles AVEC bug d'échelle latent (à gérer côté client) + +6. **InfiGUI-G1-3B / 7B** — `point_2d` post-resize, mais la fiche HF expose **explicitement** `{new_width}x{new_height}` dans le prompt et fournit le mapping. Si on lit la doc, pas de surprise. Notre `core/grounding/server.py` a déjà `_smart_resize` calibré. +7. **Qwen3-VL-8B-Instruct** — `bbox_2d` post-resize (factor **32**, pas 28 !). Avec backend in-process (vLLM ou Transformers), on peut passer `resized_width/resized_height` au modèle. Avec Ollama → impossible (cf. plan migration). +8. **Qwen2.5-VL-7B-Instruct** *(legacy)* — racine du bug actuel chez nous via Ollama. À abandonner. +9. **AGUVIS-7B-720P** — `720P` dans le nom suggère resize fixe vers 720p, mais convention coord non documentée. + +### Recommandation + +Pour éliminer **définitivement** le bug d'échelle : +- **Court terme (continuité code)** : passer en backend Transformers in-process avec exposition explicite de `resized_width/resized_height` (déjà en place dans `core/grounding/server.py` pour InfiGUI). Migrer 3B → 7B. +- **Moyen terme (architecture)** : évaluer GUI-Actor ou MolmoPoint-GUI en R&D pour l'approche coordinate-free / absolue. + +--- + +## 6. Recommandation actionnable + +Si on devait migrer maintenant pour la démo cliente suivante (post-GHT) : + +### Option A — Continuité chirurgicale (recommandée) + +**Modèle :** `InfiX-ai/InfiGUI-G1-7B` +**Backend :** Transformers in-process via `core/grounding/server.py` (déjà en place), changer `MODEL_ID` (env `GROUNDING_MODEL`) +**Effort :** ~1 jour +**Gain attendu :** +6.7 pts SSPro vs InfiGUI-G1-3B actuel (45.2 → 51.9), même format sortie, `_smart_resize` factor 28 inchangé +**Risque :** bas — même architecture, mêmes prompts, juste +3 GB VRAM (~6 GB en 4-bit NF4, tient) + +### Option B — Saut SOTA open (R&D parallèle) + +**Modèle :** `allenai/MolmoPoint-GUI-8B` (SSPro 61.1, open SOTA) +**Backend :** Transformers in-process avec logits processor custom (pas vLLM) +**Effort :** ~3-5 jours (intégration spéciale, training/eval pipelines) +**Gain attendu :** +15 pts SSPro vs actuel, **convention coord absolue → ZÉRO bug d'échelle** +**Risque :** moyen — pas de vLLM, single-image, intégration non standard + +### Option C — Aligner sur le plan migration existant (Qwen3-VL) + +**Modèle :** `Qwen/Qwen3-VL-8B-Instruct` (cible documentée dans [`MIGRATION_VLM_PLAN_2026-05-09.md`](../MIGRATION_VLM_PLAN_2026-05-09.md)) +**Backend :** vLLM ≥0.11 (déjà câblé `resolve_engine.py:785-816`) ou Transformers +**Effort :** ~3-5 jours +**Gain attendu :** +9.4 pts SSPro (54.6 vs 45.2), `resized_width/resized_height` passable explicitement +**Risque :** moyen — factor 32 ≠ 28 (DETTE-014), nouveaux prompts (`think:false`, num_predict≥128), nouvelle architecture Qwen3-VL + +### Choix recommandé : **A maintenant, B en R&D parallèle, C reporté tant que A fonctionne** + +Raisons : +- A minimise le risque court terme et capitalise sur l'infra `core/grounding/` déjà investie depuis 2026-04-26. +- B teste l'hypothèse "coordinate-free / absolue" qui pourrait être le pattern d'avenir. +- C demande de recalibrer le smart_resize sur factor 32 (DETTE-014 explicite), opération à faire UNE fois et qui mérite le timing post-démo. + +**Question ouverte pour Dom :** est-ce que l'écart 45.2 → 51.9 SSPro (option A) suffit pour débloquer les cas Easily où le grounding échoue actuellement ? Si la cause primaire est transport (cf. diagnostic 8 mai, [`REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md`](../REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md)), un modèle SOTA ne corrigera rien. + +--- + +## 7. Sources + +### Benchmarks et leaderboards + +- [ScreenSpot-Pro paper arXiv:2504.07981](https://arxiv.org/abs/2504.07981) — *ScreenSpot-Pro: GUI Grounding for Professional High-Resolution Computer Use* (ICLR 2025) +- [ScreenSpot-Pro leaderboard llm-stats](https://llm-stats.com/benchmarks/screenspot-pro) — leaderboard tiers (21 modèles, GPT-5.2 leader 86.3 %) +- [gui-agent.github.io grounding-leaderboard](https://gui-agent.github.io/grounding-leaderboard/) — infra leaderboard académique +- [ScreenSpot-Pro GitHub likaixin2000](https://github.com/likaixin2000/ScreenSpot-Pro-GUI-Grounding) — repo officiel benchmark +- [HF blog Ziyang ScreenSpot-Pro](https://huggingface.co/blog/Ziyang/screenspot-pro) — annonce HF du benchmark +- [WindowsAgentArena Microsoft](https://microsoft.github.io/WindowsAgentArena/) — environnement Windows benchmark +- [Awesome Agents Computer Use leaderboard](https://awesomeagents.ai/leaderboards/computer-use-leaderboard/) — leaderboard tiers +- [OSU GUI-Agents Paper List](https://github.com/OSU-NLP-Group/GUI-Agents-Paper-List/blob/main/paper_by_key/paper_visual_grounding.md) — recensement papiers + +### Modèles open-source (Apache / MIT) — repos et papiers + +- **InfiGUI-G1** : [HF 3B](https://huggingface.co/InfiX-ai/InfiGUI-G1-3B), [HF 7B](https://huggingface.co/InfiX-ai/InfiGUI-G1-7B), [paper arXiv:2508.05731](https://arxiv.org/abs/2508.05731), [GitHub InfiXAI/InfiGUI-G1](https://github.com/InfiXAI/InfiGUI-G1) +- **InfiGUI-R1** : [paper arXiv:2504.14239](https://arxiv.org/abs/2504.14239), [GitHub InfiXAI/InfiGUI-R1](https://github.com/InfiXAI/InfiGUI-R1) +- **UI-TARS-1.5** : [HF ByteDance-Seed/UI-TARS-1.5-7B](https://huggingface.co/ByteDance-Seed/UI-TARS-1.5-7B), [GitHub bytedance/UI-TARS](https://github.com/bytedance/ui-tars), [GitHub UI-TARS-desktop](https://github.com/bytedance/UI-TARS-desktop), [paper arXiv:2501.12326](https://arxiv.org/abs/2501.12326), [UI-TARS-2 tech report arXiv:2509.02544](https://arxiv.org/html/2509.02544v1) +- **Qwen3-VL** : [HF Qwen/Qwen3-VL-8B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-8B-Instruct), [GitHub QwenLM/Qwen3-VL](https://github.com/QwenLM/Qwen3-VL), [HF Qwen/Qwen3-VL-4B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct) +- **Qwen2.5-VL** : [HF Qwen/Qwen2.5-VL-7B-Instruct](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct), [discussion #13 bbox_2d resize bug](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct/discussions/13) +- **Holo1 / Holo1.5** : [HF Hcompany/Holo1.5-7B](https://huggingface.co/Hcompany/Holo1.5-7B), [3B](https://huggingface.co/Hcompany/Holo1.5-3B), [HF blog Holo1](https://huggingface.co/blog/Hcompany/holo1), [paper Surfer-H arXiv:2506.02865](https://arxiv.org/pdf/2506.02865), [HF blog GRPO grounding-r1](https://huggingface.co/blog/HelloKKMe/grounding-r1) +- **UGround** : [HF osunlp/UGround-V1-7B](https://huggingface.co/osunlp/UGround-V1-7B), [2B](https://huggingface.co/osunlp/UGround-V1-2B), [72B](https://huggingface.co/osunlp/UGround-V1-72B), [paper arXiv:2410.05243](https://arxiv.org/abs/2410.05243), [GitHub OSU-NLP-Group/UGround](https://github.com/OSU-NLP-Group/UGround) +- **OS-Atlas** : [HF OS-Copilot/OS-Atlas-Base-7B](https://huggingface.co/OS-Copilot/OS-Atlas-Base-7B), [Base-4B](https://huggingface.co/OS-Copilot/OS-Atlas-Base-4B), [Pro-7B](https://huggingface.co/OS-Copilot/OS-Atlas-Pro-7B), [paper arXiv:2410.23218](https://arxiv.org/abs/2410.23218), [GitHub OS-Copilot/OS-Atlas](https://github.com/OS-Copilot/OS-Atlas) +- **AGUVIS** : [HF xlangai/Aguvis-7B-720P](https://huggingface.co/xlangai/Aguvis-7B-720P) +- **Magma** : [HF microsoft/Magma-8B](https://huggingface.co/microsoft/Magma-8B), [paper arXiv:2502.13130](https://arxiv.org/abs/2502.13130), [GitHub microsoft/Magma](https://github.com/microsoft/Magma), [Microsoft blog](https://www.microsoft.com/en-us/research/blog/magma-a-foundation-model-for-multimodal-ai-agents-across-digital-and-physical-worlds/) +- **GUI-Actor** : [HF microsoft/GUI-Actor-7B-Qwen2.5-VL](https://huggingface.co/microsoft/GUI-Actor-7B-Qwen2.5-VL), [Qwen2-VL variant](https://huggingface.co/microsoft/GUI-Actor-7B-Qwen2-VL), [paper arXiv:2506.03143](https://arxiv.org/abs/2506.03143), [GitHub microsoft/GUI-Actor](https://github.com/microsoft/GUI-Actor), [project page](https://microsoft.github.io/GUI-Actor/) +- **MolmoPoint-GUI** : [HF allenai/MolmoPoint-GUI-8B](https://huggingface.co/allenai/MolmoPoint-GUI-8B), [blog Ai2 MolmoPoint](https://allenai.org/blog/molmopoint), [GitHub allenai/molmo2](https://github.com/allenai/molmo2), [MolmoWeb blog](https://allenai.org/blog/molmoweb) +- **CogAgent v2** : [HF zai-org/cogagent-9b-20241220](https://huggingface.co/zai-org/cogagent-9b-20241220), [GitHub zai-org/CogAgent](https://github.com/zai-org/CogAgent), [paper v1 arXiv:2312.08914](https://arxiv.org/abs/2312.08914), [MarkTechPost announcement](https://www.marktechpost.com/2024/12/25/tsinghua-university-researchers-just-open-sourced-cogagent-9b-20241220-the-latest-version-of-cogagent/) +- **ShowUI / FocusUI** : [HF showlab/ShowUI-2B](https://huggingface.co/showlab/ShowUI-2B), [paper arXiv:2411.17465](https://arxiv.org/abs/2411.17465), [GitHub showlab/ShowUI](https://github.com/showlab/showui), [GitHub showlab/FocusUI](https://github.com/showlab/FocusUI) +- **SeeClick** : [HF cckevinn/SeeClick](https://huggingface.co/cckevinn/SeeClick), [paper arXiv:2401.10935](https://arxiv.org/abs/2401.10935), [GitHub njucckevin/SeeClick](https://github.com/njucckevin/SeeClick) +- **GUI-G2** : [HF inclusionAI/GUI-G2-7B](https://huggingface.co/inclusionAI/GUI-G2-7B), [GitHub ZJU-REAL/GUI-G2](https://github.com/ZJU-REAL/GUI-G2) +- **OmniParser V2** : [GitHub microsoft/OmniParser](https://github.com/microsoft/omniparser), [Microsoft Research V2 blog](https://www.microsoft.com/en-us/research/articles/omniparser-v2-turning-any-llm-into-a-computer-use-agent/) + +### Annexes + +- [Codersera Qwen3-VL Instruct vs Thinking guide 2026](https://codersera.com/blog/qwen3-vl-8b-instruct-vs-qwen3-vl-8b-thinking-2025-guide/) +- [Skywork blog Qwen3-VL GUI Automation 2025](https://skywork.ai/blog/llm/qwen3-vl-gui-automation-2025-visual-agent-revolution/) +- [BinaryVerse Qwen3-VL benchmarks](https://binaryverseai.com/qwen3-vl-benchmarks-local-installation-guide-use/) +- [The Decoder Qwen3-VL videos](https://the-decoder.com/qwen3-vl-can-scan-two-hour-videos-and-pinpoint-nearly-every-detail/) +- [HF discussion #13 Qwen2.5-VL bbox resize bug](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct/discussions/13) — racine documentée du bug que nous vivons +- [GitHub QwenLM/Qwen3-VL issue #1831 — image zoom factor 32 vs 28](https://github.com/QwenLM/Qwen3-VL/issues/1831) — directement lié à notre DETTE-014 + +--- + +## 8. Liens avec autres axes de recherche du projet + +| Axe | Lien | +|---|---| +| **A2 — smart_resize** | Le choix de modèle conditionne le `factor` à utiliser : Qwen2.5-VL = 28, Qwen3-VL = 32, OS-Atlas/UGround = pas de smart_resize (espace 0-1000). DETTE-014 du repo (`feedback_reread_before_code.md`) doit être recalibrée selon le modèle final retenu. | +| **A3 — Bench grounding bbox cible** | Le test à refaire (`MIGRATION_VLM_PLAN_2026-05-09.md` §5) doit inclure les 3 candidats top : InfiGUI-G1-7B, Qwen3-VL-8B-Instruct, Holo1.5-7B, sur la fixture `heartbeat_1773792436.png` 2560×1600. Critère : OK button à cx ≈ 0.45-0.55. | +| **B2 — Validator (Planner-Actor-Validator)** | GUI-Actor inclut un grounding verifier pour évaluer les candidats. MolmoPoint-GUI retourne topk points. Pattern à intégrer dans notre `replay_verifier.py` actuellement laxiste (cf. synthèse §5.2). | +| **B3 — Coordinate-free architecture** | GUI-Actor (NeurIPS 2025) et MolmoPoint-GUI (Ai2 2026) ouvrent une voie post-coordonnée. À explorer pour v2 grounding, indépendant de l'urgence démo. | +| **Démo GHT post-mortem** | Le bug primaire de la démo 8-19 mai était transport HTTP, pas grounding (cf. [`LESSONS_LEARNED_GHT_2026-05.md`](../LESSONS_LEARNED_GHT_2026-05.md)). Migrer le VLM n'a de sens qu'après stabilisation transport (5 bugs P0 toujours ouverts). | + +--- + +*Document destiné à informer la décision de migration VLM post-démo GHT. Pas de modification de code. La décision opérationnelle (option A / B / C) doit être validée par Dom.* diff --git a/docs/recherche/AXE_A2_SMART_RESIZE_BBOX.md b/docs/recherche/AXE_A2_SMART_RESIZE_BBOX.md new file mode 100644 index 000000000..08086e6a2 --- /dev/null +++ b/docs/recherche/AXE_A2_SMART_RESIZE_BBOX.md @@ -0,0 +1,759 @@ +# Axe A2 — Doctrine smart_resize et convention bbox_2d (RPA Vision V3) + +**Date :** 2026-05-23 +**Auteur :** Claude (sous-agent recherche, contexte mission Dom) +**Périmètre :** clore techniquement DETTE-014, DETTE-010, DETTE-007, DETTE-006 par une doctrine officielle de preprocessing image et de parsing de coordonnées, par famille de modèle VLM grounding. +**Statut :** lecture seule, aucune modification de code. Synthèse de sources officielles HuggingFace, QwenLM, vLLM, Ollama, InfiX-AI, OS-Copilot. + +--- + +## 1. TL;DR + +Trois familles de modèles utilisent les chiffres suivants pour preprocesser l'image et exprimer les coordonnées de sortie : + +| Famille | `patch_size` | `spatial_merge_size` | **Factor image** | Convention output | +|---|---:|---:|---:|---| +| **Qwen2-VL / Qwen2.5-VL** (et tous fine-tunes — InfiGUI-G1-3B, UI-TARS-1.5-7B, OS-Atlas-Base-7B, SeeClick) | **14** | 2 | **28** | bbox/point en **pixel absolu post-smart_resize** (sauf OS-Atlas qui normalise sur 1000) | +| **Qwen3-VL** (Instruct / Thinking, dense + MoE) | **16** | 2 | **32** | bbox/point en pixel absolu post-smart_resize, format `[x1,y1,x2,y2]` ou `point_2d` | +| **OS-Atlas-Base-4B** (backbone InternVL-2) | tiles 448×448 | dynamic_preprocess | n/a (max 6 tiles) | bbox/point **normalisés [0, 1000]** dans des balises ``/`` | + +Trois constantes officielles, communes à toutes les variantes Qwen-VL via `qwen_vl_utils` : +- `IMAGE_MIN_TOKEN_NUM = 4` → `min_pixels = 4 × factor²` +- `IMAGE_MAX_TOKEN_NUM = 16384` → `max_pixels = 16384 × factor²` +- `MAX_RATIO = 200` + +**Le bug d'échelle bbox_2d documenté (cf. `MIGRATION_VLM_PLAN_2026-05-09.md`) a deux racines distinctes** : + +1. **Ollama opaque** : Ollama applique son propre redimensionnement interne (factor 28 pour qwen2.5-vl, factor 32 pour qwen3-vl) sans permettre au client de passer `resized_width` / `resized_height`, ni de récupérer la taille effective post-resize utilisée par le modèle. Conséquence : impossible de re-normaliser les coordonnées correctement. Ce n'est pas un bug réparable côté client — c'est une **limitation de protocole**. La citation mainteneur de la discussion HF #13 est exacte : « *there is no option (at least i don't found any) to set "resized_width": img_width, "resized_height": img_height* ». + +2. **Code prod RPA Vision V3** : `resolve_engine.py:925` (`small_w, small_h = orig_w, orig_h # pas de redimensionnement`) puis `parse_bbox_to_norm(content, small_w, small_h)` divise les coordonnées du modèle par la taille PRE-resize au lieu de la taille POST-resize. C'est documenté dans `core/grounding/bbox_parser.py:10-13` (DETTE-006 ouverte). Côté Ollama, **même corrigé, ça ne marchera pas tant qu'on n'a pas la taille post-resize d'Ollama** ; côté vLLM/Transformers, ça marche dès qu'on impose nos propres `resized_height/resized_width`. + +**Recommandation d'unification (DETTE-007)** : + +- Conserver `core/grounding/smart_resize.py` comme **module de référence** (formule officielle correcte). +- Le corriger pour exposer la famille de modèle : `smart_resize_for_family(model_name, orig_h, orig_w) -> (resized_h, resized_w)` avec dispatch automatique `factor=28` (Qwen2-VL / Qwen2.5-VL / InfiGUI / UI-TARS / OS-Atlas-7B) vs `factor=32` (Qwen3-VL). +- Supprimer les deux copies inline (`core/grounding/server.py:15-26` et `core/grounding/infigui_worker.py:99-101` — cette dernière est tronquée et ne clampe pas MIN/MAX_PIXELS, source probable de désalignement sur petites images). +- Ajouter `parse_bbox_for_family(model_name, raw_output, resized_w, resized_h, orig_w, orig_h) -> (x_pct, y_pct)` qui encapsule la conversion (pixel post-resize → pourcentage de l'image originale envoyée). +- Migrer `resolve_engine.py` (4 sites bbox + `_locate_popup_button`) pour appeler ces deux fonctions au lieu de l'arithmétique inline (corrige DETTE-006). +- **Abandonner Ollama pour le grounding bbox** tant qu'il n'expose pas les dimensions post-resize. Cible vLLM ou Transformers in-process avec passage explicite de `resized_width`/`resized_height` (cf. §4). + +--- + +## 2. Doctrine par famille de modèle + +### 2.1. Qwen2-VL / Qwen2.5-VL (et tous les fine-tunes : InfiGUI-G1-3B, UI-TARS-1.5-7B, OS-Atlas-Base-7B, SeeClick) + +**Architecture vision** (source : Qwen2.5-VL technical report, arXiv 2502.13923) : +- `patch_size = 14` +- `spatial_merge_size = 2` +- → Factor image = `14 × 2 = 28` + +**Pourquoi 28 et pas 14 ?** Le ViT découpe l'image en patches de 14×14 px (stride 14). Puis le projector multimodal fusionne les patches voisins par groupes de `spatial_merge_size × spatial_merge_size = 2×2`. Donc les dimensions H et W doivent être divisibles par `14 × 2 = 28` pour que la fusion soit propre. + +**Constantes officielles** (verbatim depuis `qwen-vl-utils/src/qwen_vl_utils/vision_process.py`, repo Qwen3-VL `main`) : + +```python +MAX_RATIO = 200 +SPATIAL_MERGE_SIZE = 2 +IMAGE_MIN_TOKEN_NUM = 4 +IMAGE_MAX_TOKEN_NUM = 16384 +``` + +Pour Qwen2.5-VL avec `image_patch_size = 14` : +- `factor = 14 × 2 = 28` +- `min_pixels = 4 × 28² = 3136` +- `max_pixels = 16384 × 28² = 12 845 056` + +**Snippet officiel `smart_resize`** (verbatim) : + +```python +import math +from typing import Optional, Tuple + +MAX_RATIO = 200 +IMAGE_MIN_TOKEN_NUM = 4 +IMAGE_MAX_TOKEN_NUM = 16384 + + +def round_by_factor(number: int, factor: int) -> int: + """Returns the closest integer to 'number' that is divisible by 'factor'.""" + return round(number / factor) * factor + + +def ceil_by_factor(number: int, factor: int) -> int: + """Returns the smallest integer greater than or equal to 'number' that is divisible by 'factor'.""" + return math.ceil(number / factor) * factor + + +def floor_by_factor(number: int, factor: int) -> int: + """Returns the largest integer less than or equal to 'number' that is divisible by 'factor'.""" + return math.floor(number / factor) * factor + + +def smart_resize( + height: int, + width: int, + factor: int, + min_pixels: Optional[int] = None, + max_pixels: Optional[int] = None, +) -> Tuple[int, int]: + """Rescales the image so that the following conditions are met: + 1. Both dimensions (height and width) are divisible by 'factor'. + 2. The total number of pixels is within the range ['min_pixels', 'max_pixels']. + 3. The aspect ratio of the image is maintained as closely as possible. + """ + max_pixels = max_pixels if max_pixels is not None else (IMAGE_MAX_TOKEN_NUM * factor ** 2) + min_pixels = min_pixels if min_pixels is not None else (IMAGE_MIN_TOKEN_NUM * factor ** 2) + assert max_pixels >= min_pixels, "The max_pixels of image must be greater than or equal to min_pixels." + if max(height, width) / min(height, width) > MAX_RATIO: + raise ValueError( + f"absolute aspect ratio must be smaller than {MAX_RATIO}, got {max(height, width) / min(height, width)}" + ) + h_bar = max(factor, round_by_factor(height, factor)) + w_bar = max(factor, round_by_factor(width, factor)) + if h_bar * w_bar > max_pixels: + beta = math.sqrt((height * width) / max_pixels) + h_bar = floor_by_factor(height / beta, factor) + w_bar = floor_by_factor(width / beta, factor) + elif h_bar * w_bar < min_pixels: + beta = math.sqrt(min_pixels / (height * width)) + h_bar = ceil_by_factor(height * beta, factor) + w_bar = ceil_by_factor(width * beta, factor) + return h_bar, w_bar +``` + +**Convention coord output Qwen2.5-VL** : pixel absolu dans la résolution **post-smart_resize**. Confirmé verbatim par le mainteneur Qwen sur la discussion HF #13 de Qwen2.5-VL-7B-Instruct : + +> "The bbox_2d coordinates are x1, y1, x2, y2 rather than x,y,w,h. And they will be relative to your resized image size if you are resizing." + +Pour reconvertir en coord originale : +```python +x_orig = x_post_resize * (orig_w / resized_w) +y_orig = y_post_resize * (orig_h / resized_h) +``` + +### 2.2. Qwen3-VL (Instruct / Thinking, dense + MoE) + +**Architecture vision** (source : `transformers/main/model_doc/qwen3_vl`, classe `Qwen3VLVisionConfig`) : +- `patch_size = 16` (au lieu de 14) +- `spatial_merge_size = 2` +- → Factor image = `16 × 2 = 32` + +**Constantes officielles** : identiques à Qwen2.5-VL côté `qwen_vl_utils` (`IMAGE_MIN_TOKEN_NUM=4`, `IMAGE_MAX_TOKEN_NUM=16384`, `MAX_RATIO=200`). Seul le facteur change. + +Pour Qwen3-VL avec `image_patch_size = 16` : +- `factor = 16 × 2 = 32` +- `min_pixels = 4 × 32² = 4096` +- `max_pixels = 16384 × 32² = 16 777 216` + +**API officielle Qwen3-VL** (verbatim README qwen-vl-utils) : + +```python +from qwen_vl_utils import process_vision_info + +images, videos, video_kwargs = process_vision_info( + messages, + image_patch_size=16, # ← Qwen3-VL utilise 16 + return_video_kwargs=True, + return_video_metadata=True, +) +``` + +Le paramètre `image_patch_size` est exposé pour permettre à `process_vision_info` de calculer `factor = image_patch_size * SPATIAL_MERGE_SIZE` dynamiquement. **Si tu utilises Qwen3-VL avec un module `smart_resize` figé sur `factor=28`, l'image envoyée n'est PAS divisible par 32, donc le processor va re-resizer derrière toi à une taille que tu ne connais pas.** C'est exactement la racine documentée de DETTE-014 (`core/grounding/smart_resize.py` est calé sur factor 28). + +**Note importante** : l'issue [QwenLM/Qwen3-VL#1831](https://github.com/QwenLM/Qwen3-VL/issues/1831) signale que **`image_zoom_in_qwen3vl.py` utilise factor=32** alors que le rapport technique Qwen2.5-VL parle de 28. Confusion levée : c'est bien `32` pour Qwen3-VL (`patch_size=16`) et `28` pour Qwen2.5-VL (`patch_size=14`). Le rapport technique 2502.13923 décrit la famille Qwen2.5-VL, pas Qwen3-VL. + +**Convention coord output Qwen3-VL** : pixel absolu post-smart_resize, idem Qwen2.5-VL. Issue [QwenLM/Qwen3-VL#1486](https://github.com/QwenLM/Qwen3-VL/issues/1486) mentionne aussi une variante 0–1000 (`convert_to_qwen2vl_format(bbox, h, w)`), utilisée lors du fine-tuning. **Pour l'inférence prod via process_vision_info, c'est pixel absolu post-resize.** À confirmer empiriquement par le test §8. + +**Différence smart_resize transformers vs qwen-vl-utils** : signalée dans l'issue [QwenLM/Qwen3-VL#2068](https://github.com/QwenLM/Qwen3-VL/issues/2068) — la version transformers ajoute une contrainte `temporal_factor` pour les vidéos. Pour les **images**, les deux sont équivalents. Pour les vidéos, utiliser la version transformers. + +### 2.3. InfiGUI-G1-3B (notre modèle grounding principal en place) + +**Architecture** : fine-tune de Qwen2.5-VL-3B-Instruct (source : carte HF `InfiX-ai/InfiGUI-G1-3B`). Donc **toutes les conventions Qwen2.5-VL s'appliquent** : `patch_size=14`, `factor=28`, output en pixel absolu post-resize. + +**Convention output InfiGUI** (verbatim carte HF) : +``` +Format prompt : The screen's resolution is {width}x{height}. + Locate the UI element(s) for "{instruction}", + output the coordinates using JSON format: [{"point_2d": [x, y]}, ...] + +Format output : ...[{"point_2d": [x, y]}, ...] + où (x, y) est exprimé en pixel post-smart_resize. +``` + +Le `point_2d` est le centre de l'élément (pas une bbox). Conversion en pixel original : +```python +x_orig = x_point * (orig_w / resized_w) +y_orig = y_point * (orig_h / resized_h) +``` + +**MAX_IMAGE_PIXELS officiel InfiGUI** : `5600 * 28 * 28 = 4 390 400` (carte HF). C'est ce qui est utilisé par `core/grounding/server.py:11` et `core/grounding/infigui_worker.py:50` — cohérent avec la doctrine. + +**Bug local DETTE-006/DETTE-007** : `core/grounding/infigui_worker.py:99-101` calcule rH/rW avec un `round_by_factor` simple **sans clamper sur MIN_PIXELS et MAX_PIXELS**. Sur une image en-dessous de 56×56 px (cas heartbeat 2560×60 cité dans `LESSONS_LEARNED_GHT_2026-05.md`), c'est une bombe : le ratio aspect dépasse `MAX_RATIO=200` et toute la logique de smart_resize tombe à plat. À corriger. + +### 2.4. UI-TARS-1.5-7B (ancien modèle grounding, remplacé par InfiGUI) + +**Architecture** : fine-tune de Qwen2.5-VL-7B. Mêmes conventions que Qwen2.5-VL : `patch_size=14`, `factor=28`. + +**Convention output UI-TARS** : format d'action structuré `Thought:/Action: click(start_box='(x1, y1)')`. Coordonnées en **pixel absolu post-resize**, identique à Qwen2.5-VL. Le prompt officiel est récupérable via `git show 9da589c8c:core/grounding/server.py` (commit historique avant remplacement par InfiGUI). + +Sur la doctrine smart_resize : aucune différence opérationnelle avec Qwen2.5-VL. Si on veut le réactiver, c'est interchangeable avec InfiGUI sous la doctrine factor=28. + +### 2.5. OS-Atlas (Base-4B et Base-7B) + +**Architecture** : +- OS-Atlas-Base-4B : fine-tune **InternVL-2** (PAS Qwen-VL). Preprocessing différent : `dynamic_preprocess` avec `max_dynamic_patch=6` tiles 448×448 + thumbnail global. **Pas de smart_resize.** +- OS-Atlas-Base-7B : fine-tune de **Qwen2-VL-7B-Instruct**. Donc `patch_size=14`, `factor=28`, conventions Qwen-VL. + +**Convention output OS-Atlas** (les deux versions) : coordonnées **normalisées dans [0, 1000]**, format structuré entre balises spéciales : +``` +<|object_ref_start|>language switch<|object_ref_end|><|box_start|>(576,12),(592,42)<|box_end|> +``` +ou pour les points : +``` +<|object_ref_start|>...<|object_ref_end|><|point_start|>(x,y)<|point_end|> +``` + +Conversion : +```python +x_orig = (x_normalized / 1000) * orig_w +y_orig = (y_normalized / 1000) * orig_h +``` + +**Pas de bug d'échelle bbox_2d** sur OS-Atlas — la normalisation 0–1000 absorbe le smart_resize côté training. Mais format de parsing complètement différent : il faut un regex séparé sur `<|box_start|>(x1,y1),(x2,y2)<|box_end|>` et non sur `"bbox_2d": [...]`. + +OS-Atlas-Base-7B est intéressant à benchmarker côté ScreenSpot car il bat Qwen2-VL standard sur GUI grounding tout en restant techniquement compatible avec le pipeline Qwen2.5-VL (même backbone, même processor → même smart_resize factor=28). + +--- + +## 3. Convention coord récapitulative et conversion + +| Famille / modèle | Sortie | Range | Parsing regex | Reconversion → pixel orig | +|---|---|---|---|---| +| Qwen2.5-VL / qwen2.5vl:7b | `{"bbox_2d":[x1,y1,x2,y2]}` | [0, resized_w] × [0, resized_h] | `"bbox_2d"\s*:\s*\[([^\]]+)\]` | `x * orig_w / resized_w` | +| Qwen3-VL / qwen3-vl:8b | idem (avec prompt JSON imposé) | [0, resized_w] × [0, resized_h] | idem | idem | +| InfiGUI-G1-3B | `[{"point_2d":[x,y]}]` après `` | [0, resized_w] × [0, resized_h] | `"point_2d"\s*:\s*\[(\d+),\s*(\d+)\]` | idem | +| UI-TARS-1.5-7B | `click(start_box='(x,y)')` | [0, resized_w] × [0, resized_h] | `start_box='\((\d+),\s*(\d+)\)'` | idem | +| OS-Atlas-Base-7B | `<\|box_start\|>(x1,y1),(x2,y2)<\|box_end\|>` | **[0, 1000]** | `<\|box_start\|>\((\d+),(\d+)\),\((\d+),(\d+)\)<\|box_end\|>` | `(x / 1000) * orig_w` | +| OS-Atlas-Base-4B | idem 7B | [0, 1000] | idem | idem | + +**Règle de division universelle** : on divise par la taille **dans laquelle le modèle a vu l'image**, et on multiplie par la taille **de l'image originale envoyée**. Pour les modèles pixel-absolu post-resize, c'est `resized_w/h`. Pour les modèles normalisés 0–1000, c'est `1000`. + +--- + +## 4. Cartographie vLLM — passage de `resized_width` / `resized_height` + +### 4.1. État officiel + +vLLM supporte deux mécanismes pour passer des paramètres au processor multimodal : + +1. **Au démarrage du serveur** : `--mm-processor-kwargs '{"min_pixels": ..., "max_pixels": ...}'` (config globale). +2. **Par requête, via OpenAI client `extra_body`** : depuis le PR [vllm#13533](https://github.com/vllm-project/vllm/pull/13533) (mergé 20 février 2025 par simon-mo). C'est l'extension OpenAI propre à vLLM. + +### 4.2. Méthode A — `extra_body.mm_processor_kwargs` (config globale) + +```python +from openai import OpenAI + +client = OpenAI(base_url="http://localhost:8100/v1", api_key="EMPTY") + +resp = client.chat.completions.create( + model="Qwen/Qwen2.5-VL-7B-Instruct-AWQ", + messages=[ + {"role": "user", "content": [ + {"type": "text", "text": prompt}, + {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{shot_b64}"}}, + ]}, + ], + temperature=0.1, + max_tokens=200, + extra_body={ + "mm_processor_kwargs": { + "min_pixels": 4 * 28 * 28, + "max_pixels": 5600 * 28 * 28, + } + }, +) +``` + +⚠ **Bug ouvert [vllm#15364](https://github.com/vllm-project/vllm/issues/15364)** : `mm_processor_kwargs` passés via `extra_body` au niveau requête sont **parfois ignorés** par le processor Qwen2.5-VL, contrairement à un passage au niveau image. Le warning révélateur : +> "Keyword argument `max_pixels` is not a valid argument for this processor and will be ignored." + +### 4.3. Méthode B — `resized_height` / `resized_width` au niveau image (recommandée) + +C'est la méthode citée par le mainteneur Qwen sur la discussion HF #13 de Qwen2.5-VL-7B-Instruct, et confirmée par les vLLM Recipes Qwen2.5-VL. C'est **la méthode qui contourne le bug #15364** : on impose la taille image par image, et c'est garanti respecté. + +```python +# 1. Côté client : calculer la taille post-resize qu'on VEUT imposer +from core.grounding.smart_resize_unified import smart_resize_for_family +resized_h, resized_w = smart_resize_for_family("Qwen/Qwen2.5-VL-7B-Instruct-AWQ", orig_h, orig_w) + +# 2. Encoder l'image (sans la redimensionner soi-même, ou la redimensionner exactement à +# resized_w x resized_h — les deux marchent, le processor matchera de toute façon) +buf = io.BytesIO() +img.save(buf, format="JPEG", quality=80) +shot_b64 = base64.b64encode(buf.getvalue()).decode() + +# 3. Payload OpenAI vLLM avec resized_height/resized_width INLINE dans l'image +payload = { + "model": "Qwen/Qwen2.5-VL-7B-Instruct-AWQ", + "messages": [{ + "role": "user", + "content": [ + {"type": "text", "text": prompt}, + { + "type": "image_url", + "image_url": {"url": f"data:image/jpeg;base64,{shot_b64}"}, + "resized_height": resized_h, + "resized_width": resized_w, + }, + ], + }], + "temperature": 0.1, + "max_tokens": 200, +} +``` + +⚠ Le format exact `resized_height` / `resized_width` au niveau de l'item `image_url` est l'extension Qwen, supportée par vLLM via le processor Qwen2.5-VL. À valider empiriquement par le test §8 — si vLLM rejette ce format au niveau item, basculer sur méthode A en sachant qu'on est exposé au bug #15364. + +### 4.4. Modèles vLLM supportés pour ce contrat + +- ✅ Qwen2-VL (depuis vLLM 0.6.x) +- ✅ Qwen2.5-VL (depuis vLLM 0.7.x, AWQ et GPTQ depuis 0.8.x) +- ✅ Qwen3-VL (depuis vLLM ~0.10.x avec Qwen3-VL Usage Guide officielle) +- ❌ InfiGUI-G1-3B : pas directement supporté en vLLM (architecture custom QwenForConditionalGeneration mais avec checkpoint InfiX-AI). **Vérifier via test charge réel** que `Qwen2_5_VLForConditionalGeneration` peut loader le checkpoint InfiX-AI sans erreur. + +### 4.5. Statut du commit dans le repo prod + +`agent_v0/server_v1/resolve_engine.py:957-974` appelle déjà vLLM en OpenAI-compat mais **ne passe AUCUN `mm_processor_kwargs` ni `resized_height/resized_width`**. C'est l'écart à combler. Aucune autre modif structurelle nécessaire. + +--- + +## 5. Cartographie Ollama — pourquoi ça casse + +### 5.1. Faits + +- Ollama applique un smart_resize interne (côté `runner.go` / GGUF llama.cpp vision backend) pour Qwen2.5-VL et Qwen3-VL afin de respecter `factor=28` (resp. 32). +- **Aucun champ HTTP `resized_width` / `resized_height` n'est exposé** dans l'API `/api/chat` ni dans `options.*`. La citation mainteneur de la discussion HF Qwen2.5-VL #13 est exacte : « *Could be also a problem with Ollama because there is no option (at least i don't found any) to set "resized_width": img_width, "resized_height": img_height* » (Phreak87, 16 juin 2025). +- L'issue [ollama#11217](https://github.com/ollama/ollama/issues/11217) (close, juin 2025) confirme que le modèle hallucine sur sa propre taille image (il répond « 1000×1000 » à un user demandant la résolution effective) — preuve indirecte que la taille post-resize n'est jamais retournée au client. +- L'issue [ollama#10753](https://github.com/ollama/ollama/issues/10753) (close, mai 2025) montre que Qwen2.5-VL-32b crash sur images ≥ 720p en Ollama — ce qui indique que le preprocessing GGUF n'est ni robuste ni inspecté côté client. +- L'issue [ollama#11297](https://github.com/ollama/ollama/issues/11297) montre que sur les très petites images (< 28 px), le modèle crash directement avec une erreur de factor 28. + +### 5.2. Conséquence directe pour DETTE-006 + +Tant qu'on appelle Qwen2.5-VL via Ollama : + +- On ne sait pas dans quelle résolution Ollama envoie l'image au modèle. +- Le `bbox_2d` retourné est donc dans une référence inconnue, non récupérable. +- **Aucune rustine client n'est correcte**. Pré-resizer l'image avant envoi à une taille qu'on calcule officiellement (`smart_resize(factor=28)`) **peut accidentellement matcher** ce qu'Ollama re-calcule, mais ça reste fragile et version-dépendant. + +### 5.3. Rustine technique possible (à utiliser uniquement si bloqué Ollama) + +Si pour une raison opérationnelle on doit garder Ollama (pas de vLLM disponible, perf qwen3-vl:8b acceptable) : + +1. Redimensionner l'image côté client à une taille **exactement** `factor*round(orig/factor)` clampée à `[min_pixels, max_pixels]`. +2. Envoyer cette image redimensionnée à Ollama, en stockant la taille `(resized_w, resized_h)`. +3. **Forcer le prompt à inclure la résolution** : `"The screen's resolution is {resized_w}x{resized_h}."` — c'est ce que fait InfiGUI dans son prompt officiel. Le modèle est moins susceptible de re-resizer s'il croit que c'est sa résolution naturelle. +4. Diviser `bbox_2d` par `(resized_w, resized_h)`, pas par l'orig. + +C'est une approximation, pas une garantie. Le résultat est probabiliste, à valider empiriquement (le bench du 8 mai a précisément montré que même `qwen3-vl:8b (prompt JSON explicite)` retourne des coords shiftées). + +**Recommandation forte** : abandonner Ollama pour le grounding bbox. Le garder pour les LLM texte (safety_checks, t2a_decision) où le bug d'échelle n'existe pas. + +--- + +## 6. Module unifié recommandé + +### 6.1. Placement + +**Fichier proposé** : `core/grounding/smart_resize.py` (à étendre, ne pas créer de doublon). + +Motifs : +- Module déjà créé (commit `0d7bcd18a`), déjà appelé `smart_resize`. C'est ce que les call-sites futurs auront le réflexe d'importer. +- DETTE-014 actuelle dit qu'il est calé sur la mauvaise référence (factor 28 implicite via `FACTOR_DEFAULT`). On corrige en exposant le dispatch par modèle. +- DETTE-007 demande l'unification : c'est le bon endroit pour la centralisation, à condition d'ajouter `parse_bbox_for_family` à côté (sinon dispersion). + +**Alternative** : créer `core/grounding/preprocessing.py` (smart_resize + parse_bbox + helpers PIL). Plus propre conceptuellement (preprocessing vs parsing séparés peut être confusant pour un newcomer). Tranchable avec Dom. + +### 6.2. Snippet `smart_resize_for_family` + +```python +"""Dispatch smart_resize par famille de modèle VLM grounding. + +Couvre Qwen2-VL / Qwen2.5-VL (factor=28, patch_size=14) et Qwen3-VL +(factor=32, patch_size=16). Pour OS-Atlas-Base-4B (backbone InternVL), +lever une exception explicite : preprocessing différent (dynamic_preprocess). +""" + +import math +from typing import Optional, Tuple + +# Constantes officielles qwen_vl_utils (communes à toutes versions Qwen-VL) +MAX_RATIO = 200 +IMAGE_MIN_TOKEN_NUM = 4 +IMAGE_MAX_TOKEN_NUM = 16384 +SPATIAL_MERGE_SIZE = 2 + +# Patch sizes par famille (source : Qwen3VLVisionConfig, Qwen2VLVisionConfig) +_PATCH_SIZE_BY_FAMILY = { + "qwen2-vl": 14, + "qwen2.5-vl": 14, + "qwen2.5vl": 14, + "qwen3-vl": 16, + "qwen3vl": 16, + "infigui": 14, # fine-tune Qwen2.5-VL-3B + "ui-tars": 14, # fine-tune Qwen2.5-VL-7B + "os-atlas-7b": 14, # fine-tune Qwen2-VL-7B + "seeclick": 14, # fine-tune Qwen-VL (legacy) +} + + +def _detect_family(model_name: str) -> str: + """Retourne la clé de famille à partir d'un model_name humain ou HF. + + Args: + model_name: ex. 'qwen2.5vl:7b', 'Qwen/Qwen3-VL-8B-Instruct', + 'InfiX-ai/InfiGUI-G1-3B', 'ByteDance-Seed/UI-TARS-1.5-7B'. + + Returns: + Clé de _PATCH_SIZE_BY_FAMILY ou raise. + """ + s = model_name.lower() + if "qwen3-vl" in s or "qwen3vl" in s: + return "qwen3-vl" + if "qwen2.5-vl" in s or "qwen2.5vl" in s: + return "qwen2.5-vl" + if "qwen2-vl" in s or "qwen2vl" in s: + return "qwen2-vl" + if "infigui" in s: + return "infigui" + if "ui-tars" in s or "uitars" in s: + return "ui-tars" + if "os-atlas-base-7b" in s: + return "os-atlas-7b" + if "os-atlas-base-4b" in s: + raise ValueError( + "OS-Atlas-Base-4B uses InternVL preprocessing (dynamic_preprocess), " + "not smart_resize. Use dedicated path." + ) + if "seeclick" in s: + return "seeclick" + raise ValueError(f"Unknown VLM family for model {model_name!r}") + + +def _round_by_factor(n: float, factor: int) -> int: + return round(n / factor) * factor + + +def _floor_by_factor(n: float, factor: int) -> int: + return math.floor(n / factor) * factor + + +def _ceil_by_factor(n: float, factor: int) -> int: + return math.ceil(n / factor) * factor + + +def smart_resize_for_family( + model_name: str, + orig_h: int, + orig_w: int, + *, + min_pixels: Optional[int] = None, + max_pixels: Optional[int] = None, +) -> Tuple[int, int]: + """Calcule (resized_h, resized_w) pour un modèle VLM donné. + + Implémentation : formule officielle qwen_vl_utils.smart_resize avec + factor dispatché par famille. + + Args: + model_name: nom du modèle (ex. 'Qwen/Qwen3-VL-8B-Instruct'). + orig_h, orig_w: dimensions de l'image originale en pixels. + min_pixels, max_pixels: bornes optionnelles. Par défaut : + IMAGE_MIN_TOKEN_NUM * factor² et IMAGE_MAX_TOKEN_NUM * factor². + + Returns: + (resized_h, resized_w) divisibles par factor, dans [min_pixels, + max_pixels], aspect ratio préservé au plus près. + + Raises: + ValueError: famille inconnue ou aspect ratio > MAX_RATIO. + """ + family = _detect_family(model_name) + patch_size = _PATCH_SIZE_BY_FAMILY[family] + factor = patch_size * SPATIAL_MERGE_SIZE # 28 ou 32 + + max_pixels = max_pixels if max_pixels is not None else IMAGE_MAX_TOKEN_NUM * factor ** 2 + min_pixels = min_pixels if min_pixels is not None else IMAGE_MIN_TOKEN_NUM * factor ** 2 + + if max(orig_h, orig_w) / max(1, min(orig_h, orig_w)) > MAX_RATIO: + raise ValueError( + f"absolute aspect ratio must be smaller than {MAX_RATIO}, " + f"got {max(orig_h, orig_w) / max(1, min(orig_h, orig_w)):.1f}" + ) + + h_bar = max(factor, _round_by_factor(orig_h, factor)) + w_bar = max(factor, _round_by_factor(orig_w, factor)) + + if h_bar * w_bar > max_pixels: + beta = math.sqrt((orig_h * orig_w) / max_pixels) + h_bar = _floor_by_factor(orig_h / beta, factor) + w_bar = _floor_by_factor(orig_w / beta, factor) + elif h_bar * w_bar < min_pixels: + beta = math.sqrt(min_pixels / (orig_h * orig_w)) + h_bar = _ceil_by_factor(orig_h * beta, factor) + w_bar = _ceil_by_factor(orig_w * beta, factor) + + return h_bar, w_bar +``` + +### 6.3. Snippet `parse_bbox_for_family` + +```python +import json +import re +from typing import Optional, Tuple + +# Convention output par famille (cf. §3 du doc) +_OUTPUT_RANGE_BY_FAMILY = { + "qwen2-vl": "pixel_post_resize", + "qwen2.5-vl": "pixel_post_resize", + "qwen3-vl": "pixel_post_resize", + "infigui": "pixel_post_resize", + "ui-tars": "pixel_post_resize", + "os-atlas-7b": "normalized_1000", + "seeclick": "pixel_post_resize", +} + + +def parse_bbox_for_family( + model_name: str, + raw_output: str, + resized_w: int, + resized_h: int, + orig_w: int, + orig_h: int, +) -> Tuple[Optional[float], Optional[float]]: + """Parse la sortie d'un VLM grounding en (x_pct, y_pct) normalisés sur l'image originale. + + Args: + model_name: nom du modèle, sert au dispatch range/format. + raw_output: texte brut renvoyé par le VLM (peut contenir ...). + resized_w, resized_h: dimensions effectivement utilisées par le modèle. + orig_w, orig_h: dimensions de l'image originale envoyée au client. + + Returns: + (x_pct, y_pct) dans [0, 1] OU (None, None) si non parsable. + """ + family = _detect_family(model_name) + range_kind = _OUTPUT_RANGE_BY_FAMILY[family] + + # Nettoyer le thinking et fences markdown + body = raw_output.split("")[-1] if "" in raw_output else raw_output + body = body.replace("```json", "").replace("```", "").strip() + + cx_px = cy_px = None + + # Cas OS-Atlas : balises <|box_start|>(x1,y1),(x2,y2)<|box_end|> + if range_kind == "normalized_1000": + m = re.search( + r"<\|box_start\|>\((\d+),\s*(\d+)\),\s*\((\d+),\s*(\d+)\)<\|box_end\|>", + body, + ) + if m: + x1, y1, x2, y2 = (int(g) for g in m.groups()) + cx_norm = (x1 + x2) / 2 # dans [0, 1000] + cy_norm = (y1 + y2) / 2 + return cx_norm / 1000.0, cy_norm / 1000.0 + m = re.search( + r"<\|point_start\|>\((\d+),\s*(\d+)\)<\|point_end\|>", + body, + ) + if m: + return int(m.group(1)) / 1000.0, int(m.group(2)) / 1000.0 + return None, None + + # Cas Qwen-VL famille : pixel post-resize, plusieurs formats + # Format 1 : InfiGUI/UI-TARS point_2d + m = re.search(r'"point_2d"\s*:\s*\[\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*\]', body) + if m: + cx_px = float(m.group(1)) + cy_px = float(m.group(2)) + + # Format 2 : Qwen2.5-VL / Qwen3-VL bbox_2d [x1,y1,x2,y2] + if cx_px is None: + m = re.search(r'"bbox_2d"\s*:\s*\[([^\]]+)\]', body) + if m: + coords = [float(v.strip()) for v in m.group(1).split(",")] + if len(coords) == 2: + cx_px, cy_px = coords[0], coords[1] + elif len(coords) >= 4: + cx_px = (coords[0] + coords[2]) / 2 + cy_px = (coords[1] + coords[3]) / 2 + + # Format 3 : UI-TARS click(start_box='(x, y)') + if cx_px is None: + m = re.search(r"start_box\s*=\s*['\"]?\(\s*(\d+)\s*,\s*(\d+)\s*\)", body) + if m: + cx_px = float(m.group(1)) + cy_px = float(m.group(2)) + + # Format 4 : array brut [x, y] ou [x1, y1, x2, y2] (fallback) + if cx_px is None: + m = re.search( + r"\[\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)" + r"(?:\s*,\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?))?\s*\]", + body, + ) + if m: + vals = [float(v) for v in m.groups() if v is not None] + if len(vals) >= 4: + cx_px = (vals[0] + vals[2]) / 2 + cy_px = (vals[1] + vals[3]) / 2 + elif len(vals) == 2: + cx_px = vals[0] + cy_px = vals[1] + + if cx_px is None or cy_px is None: + return None, None + + # IMPORTANT : on divise par resized_w/h (taille POST-resize) et non orig_w/h. + # C'est le fix DETTE-006. + x_pct = cx_px / resized_w + y_pct = cy_px / resized_h + + if not (0.0 <= x_pct <= 1.0 and 0.0 <= y_pct <= 1.0): + return None, None + + return x_pct, y_pct +``` + +--- + +## 7. Recommandation de refactor `resolve_engine.py` + +### 7.1. Sites à modifier + +D'après l'audit déjà réalisé dans le repo (DETTE-006 dans `bbox_parser.py:10-13` et `MIGRATION_VLM_PLAN_2026-05-09.md` §4) : + +| Site | Ligne | Modification | +|---|---|---| +| `_resolve_by_grounding` initialisation | 924–925 | Calculer `resized_h, resized_w = smart_resize_for_family(_grounding_model or _vllm_model, orig_h, orig_w)` au lieu de `small_w, small_h = orig_w, orig_h`. | +| Payload vLLM | 957–974 | Ajouter `"resized_height": resized_h, "resized_width": resized_w` dans l'item image (méthode B §4.3). | +| Payload Ollama | 985–992 | Pré-resize PIL `img.resize((resized_w, resized_h))` avant b64 (rustine §5.3) + même prompt avec résolution annoncée. | +| Parse résultat | 1001 | Remplacer `parse_bbox_to_norm(content, small_w, small_h)` par `parse_bbox_for_family(model_name, content, resized_w, resized_h, orig_w, orig_h)`. | +| Parse retry multi-image | 1027–1029 | Idem. | +| `_locate_popup_button` | 2536–2585 | Mêmes 4 modifs (compute resized, payload, parse) sur cette fonction popup. | + +### 7.2. Fichiers à supprimer / consolider (DETTE-007) + +- `core/grounding/server.py` lignes 10–26 : supprimer la définition locale `_smart_resize` et `MIN_PIXELS`/`MAX_PIXELS`, importer depuis `core/grounding/smart_resize.smart_resize_for_family`. +- `core/grounding/infigui_worker.py` lignes 99–101 : remplacer le calcul `rH, rW` par un appel à `smart_resize_for_family("InfiX-ai/InfiGUI-G1-3B", H, W)`. **Au passage on corrige le bug de non-clamp MIN/MAX_PIXELS.** +- `core/grounding/bbox_parser.py` : peut être conservé comme parser texte générique, ou intégré dans `parse_bbox_for_family`. Décision Dom. + +--- + +## 8. Protocole de test bbox cible (fixture heartbeat 2560×1600) + +Fixture : `data/training/live_sessions/bg_DESKTOP-58D5CAC_windows/shots/heartbeat_1773792436.png` (boîte de dialogue OK/Cancel, bouton OK visuellement centré horizontalement ~ x_pct ≈ 0.48). + +### 8.1. Étapes + +1. **Préparer un script standalone** (pas de modif resolve_engine pour ce test). +2. Charger l'image avec PIL. `orig_w, orig_h = img.size` → attendu `(2560, 1600)`. +3. Appeler `smart_resize_for_family("Qwen/Qwen3-VL-8B-Instruct", orig_h, orig_w)`. Pour `factor=32`, `max_pixels=16384*32²=16 777 216` : `2560*1600=4 096 000` < max_pixels, donc resize attendu = round au plus près de 32 : `resized_w=2560, resized_h=1600`. (Cas où smart_resize est no-op car déjà conforme.) +4. Appeler `smart_resize_for_family("qwen2.5vl:7b", orig_h, orig_w)` (factor=28). `2560/28=91.43`, `round=91*28=2548`. `1600/28=57.14`, `round=57*28=1596`. Donc `(resized_w, resized_h) = (2548, 1596)`. Vérifier `4 067 808 ≤ max_pixels` : oui. +5. Envoyer la requête vLLM (ou Transformers in-process) avec `resized_width=2548, resized_height=1596` (Qwen2.5-VL) ou `(2560, 1600)` (Qwen3-VL) en méthode B. +6. Récupérer la réponse, par exemple `{"bbox_2d": [1175, 935, 1280, 985]}`. +7. Appeler `parse_bbox_for_family(...)`. Attendu : `x_pct = (1175+1280)/2/2548 ≈ 0.482`, `y_pct = (935+985)/2/1596 ≈ 0.602`. +8. **Critère de validation** : `0.45 ≤ x_pct ≤ 0.55` (bouton OK centré horizontalement ±5%) ET overlay visuel sur le screenshot montrant le marker centré sur le bouton OK. + +### 8.2. Critère d'échec (= bug pas corrigé) + +Si on observe `x_pct < 0.20` (toujours top-left, comme le bench du 8 mai), c'est que : +- la taille `resized_w/h` passée n'est PAS celle effectivement utilisée par le modèle (vLLM ignore les kwargs), OU +- on divise par la mauvaise dimension (bug de regression), OU +- le modèle ne respecte pas le contrat de pixel post-resize sur cette taille. + +Faire passer le test **avant** d'attaquer la migration prod. + +### 8.3. Baseline pour comparaison + +Le test du 8 mai (cf. `MIGRATION_VLM_PLAN_2026-05-09.md` §2) a observé `bbox_2d=[422,604,462,624]` retourné par `qwen2.5vl:7b` Ollama, donnant `cx_px = 442`. Divisé par `orig_w=2560` : `0.17` (top-left). Divisé par `resized_w=2548` (notre nouveau dénominateur) : `0.174` — **toujours faux**, parce que le bug n'est pas qu'on divise par 2560 au lieu de 2548 (écart 0.5%), c'est qu'**Ollama a probablement redimensionné l'image à une taille interne inconnue plus petite que (2548, 1596)**, et le modèle a retourné des coords dans cette résolution interne. CQFD : le bug est non-résolvable côté Ollama. + +Sur vLLM avec `resized_width=2548` explicite : prédiction = le bbox passe à `[1150, 920, 1270, 970]` ou approchant, donnant `cx_px=1210`, `x_pct=0.475`. **À valider.** + +--- + +## 9. Sources + +### Officielles QwenLM + +- [Qwen/Qwen2.5-VL-7B-Instruct discussion #13 — Bounding boxes coordinates](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct/discussions/13) — citations mainteneur sur convention post-resize + Ollama. +- [Qwen3-VL — qwen-vl-utils/src/qwen_vl_utils/vision_process.py](https://github.com/QwenLM/Qwen3-VL/blob/main/qwen-vl-utils/src/qwen_vl_utils/vision_process.py) — source officielle smart_resize, constantes. +- [Qwen3-VL — qwen-vl-utils/README.md](https://github.com/QwenLM/Qwen3-VL/blob/main/qwen-vl-utils/README.md) — `image_patch_size=16` pour Qwen3-VL, `resized_height`/`resized_width` au niveau message. +- [Qwen3-VL Issue #1831 — Image zoom tool uses incorrect resize factor 32 vs 28](https://github.com/QwenLM/Qwen3-VL/issues/1831) — confusion factor 32 Qwen3 vs 28 Qwen2.5. +- [Qwen3-VL Issue #1486 — Bounding Box Coordinate Format and Image Resizing for Qwen3-VL Fine-tuning](https://github.com/QwenLM/Qwen3-VL/issues/1486) — variante 0–1000 fine-tuning. +- [Qwen3-VL Issue #1616 — bbox scaling LLaMA-Factory](https://github.com/QwenLM/Qwen3-VL/issues/1616) — question ouverte. +- [Qwen3-VL Issue #1640 — question about smart_resize of qwen3vl](https://github.com/QwenLM/Qwen3-VL/issues/1640) — inconsistance cookbooks. +- [Qwen3-VL Issue #2068 — smart_resize transformers vs qwen-vl-utils](https://github.com/QwenLM/Qwen3-VL/issues/2068) — divergence video. +- [Qwen2.5-VL Technical Report (arXiv:2502.13923)](https://arxiv.org/pdf/2502.13923) — patch_size 14, stride 14, factor 28. + +### Officielles HuggingFace Transformers + +- [HF docs — Qwen3-VL](https://huggingface.co/docs/transformers/main/model_doc/qwen3_vl) — `Qwen3VLVisionConfig.patch_size=16`, `spatial_merge_size=2`. +- [HF docs — Qwen2-VL](https://huggingface.co/docs/transformers/en/model_doc/qwen2_vl) — `Qwen2VLImageProcessor` vs `Qwen2VLImageProcessorFast`. +- [transformers — qwen2_vl/image_processing_qwen2_vl.py](https://github.com/huggingface/transformers/blob/main/src/transformers/models/qwen2_vl/image_processing_qwen2_vl.py) — implémentation smart_resize ref. +- [transformers — qwen2_vl/image_processing_qwen2_vl_fast.py](https://github.com/huggingface/transformers/blob/main/src/transformers/models/qwen2_vl/image_processing_qwen2_vl_fast.py) — processor rapide. + +### vLLM + +- [vLLM PR #13533 — add mm_processor_kwargs to extra_body for Qwen2.5-VL](https://github.com/vllm-project/vllm/pull/13533) (mergé 2025-02-20). +- [vLLM Issue #15364 — Qwen2.5-VL mm_processor_kwargs not respected](https://github.com/vllm-project/vllm/issues/15364) — bug ouvert. +- [vLLM Issue #13143 — Qwen2-VL max_pixels not a valid argument](https://github.com/vllm-project/vllm/issues/13143). +- [vLLM Recipes — Qwen2.5-VL Usage Guide](https://docs.vllm.ai/projects/recipes/en/latest/Qwen/Qwen2.5-VL.html). +- [vLLM Recipes — Qwen3-VL Usage Guide](https://docs.vllm.ai/projects/recipes/en/latest/Qwen/Qwen3-VL.html). +- [vLLM Issue #20855 — Qwen2VLImageProcessorFast vs slow processor](https://github.com/vllm-project/vllm/issues/20855). + +### Ollama + +- [Ollama Issue #11217 — image size of qwen2.5-vl](https://github.com/ollama/ollama/issues/11217) — modèle hallucine sa propre résolution. +- [Ollama Issue #10753 — Qwen2.5-VL 32b crashes 720p+](https://github.com/ollama/ollama/issues/10753). +- [Ollama Issue #11297 — qwen2.5vl crashes small image](https://github.com/ollama/ollama/issues/11297). +- [Ollama Issue #9261 — Support for Qwen2.5-VL Model](https://github.com/ollama/ollama/issues/9261). +- [Ollama Issue #13113 — qwen3-vl small image error](https://github.com/ollama/ollama/issues/13113). +- [Ollama Issue #14388 — Qwen2-VL-2B GGUF fails image recognition](https://github.com/ollama/ollama/issues/14388). +- [llama.cpp Issue #13694 — Qwen2.5-VL-7B-Instruct returns extremely inaccurate bbox coordinates](https://github.com/ggml-org/llama.cpp/issues/13694) — confirme que c'est un problème de toute la stack GGUF, pas que d'Ollama. + +### InfiGUI / UI-TARS / OS-Atlas + +- [InfiX-ai/InfiGUI-G1-3B Hugging Face](https://huggingface.co/InfiX-ai/InfiGUI-G1-3B) — carte modèle, prompt officiel, MAX_PIXELS=5600·28². +- [InfiX-ai/InfiGUI-G1 GitHub](https://github.com/InfiXAI/InfiGUI-G1) — repo officiel AEPO. +- [InfiGUI-G1 paper (arXiv:2508.05731)](https://arxiv.org/html/2508.05731v1) — AEPO, backbone Qwen2.5-VL-3B-Instruct. +- [OS-Copilot/OS-Atlas GitHub](https://github.com/OS-Copilot/OS-Atlas) — README + inférence Base-4B/7B. +- [OS-Atlas paper (arXiv:2410.23218)](https://arxiv.org/html/2410.23218v1) — convention 0–1000 normalisée. +- [OS-Copilot/OS-Atlas-Base-7B HF](https://huggingface.co/OS-Copilot/OS-Atlas-Base-7B) — backbone Qwen2-VL-7B-Instruct. +- [OS-Copilot/OS-Atlas-Base-4B HF](https://huggingface.co/OS-Copilot/OS-Atlas-Base-4B) — backbone InternVL-2. + +### Communauté / blogs + +- [Qwen2.5-VL — A hands on code walkthrough (Towards AI)](https://towardsai.net/p/machine-learning/qwen2-5-vl-a-hands-on-code-walkthrough). +- [Qwen2-VL — A hands-on code walkthrough (Medium)](https://medium.com/data-science-collective/qwen2-vl-a-hands-on-code-walkthrough-c5a4e073e9b3). +- [What means "using a slow image processor" — vLLM Forums](https://discuss.vllm.ai/t/what-means-using-a-slow-image-processor/1607). + +--- + +## 10. Annexe — `Qwen2VLImageProcessor` vs `Qwen2VLImageProcessorFast` + +| Aspect | `Qwen2VLImageProcessor` (slow) | `Qwen2VLImageProcessorFast` | +|---|---|---| +| Default depuis transformers 4.48 | non | **oui** (`use_fast=True` par défaut) | +| Supporte vidéo | oui (deprecated → v5.0) | **non** (utiliser `Qwen2VLVideoProcessor` séparément) | +| Vitesse | référence | sensiblement plus rapide (torchvision/PIL optims) | +| Reproductibilité bit-exact avec ancien checkpoint | oui | légère différence possible sur cas limites | + +**Recommandation** : utiliser `Qwen2VLImageProcessorFast` (default). Sauf besoin de reproductibilité bit-exact avec un ancien checkpoint, auquel cas passer `use_fast=False` au `from_pretrained`. Le projet n'a aucun cas d'usage qui justifie le slow. + +--- + +*Document destiné à clore techniquement DETTE-006/007/010/014. Aucune modification du code n'a été effectuée. Toute migration nécessite une décision explicite de Dom, suivie d'un commit unique par dette.* diff --git a/docs/recherche/AXE_A3_BENCH_PROTOCOL.md b/docs/recherche/AXE_A3_BENCH_PROTOCOL.md new file mode 100644 index 000000000..e3fedfebf --- /dev/null +++ b/docs/recherche/AXE_A3_BENCH_PROTOCOL.md @@ -0,0 +1,895 @@ +# AXE A3 — Protocole de bench grounding VLM (post smart_resize) + +**Date** : 2026-05-23 +**Auteur** : Claude (session dispatchée AXE A3) +**Contexte** : `MIGRATION_VLM_PLAN_2026-05-09.md` §2 a relevé un bug d'échelle `bbox_2d` (cx ≈ 0.17 au lieu de ~0.45-0.55) sur 4 configs Ollama. Le module `core/grounding/smart_resize.py` a été commité (`0d7bcd18a`) mais **jamais reverifié** par un bench end-to-end. DETTE-014 indique qu'il est mal calé (factor 28 vs 32). Ce protocole comble le trou méthodologique. +**Statut** : protocole + script prêts. Aucune exécution réelle réalisée par Claude — Dom décide quand lancer. + +--- + +## 1. TL;DR + +1. **Une fixture** (`heartbeat_1773792436.png` 2560×1600, dialog OK/Cancel) sur laquelle on connaît la vérité-terrain (bouton OK ≈ mid-screen, `cx ≈ 0.50`). +2. **Trois backends** : Ollama (`qwen2.5vl:7b` = baseline buggy, `qwen3-vl:8b` JSON-explicit), vLLM (`Qwen3-VL-8B-Instruct` avec `resized_width/height` natifs), Transformers in-process (`InfiGUI-G1-3B` actuel + 1-2 SOTA optionnels OS-Atlas/Magma). +3. **Pour chaque modèle** : déchargement VRAM → 1 cold → 10 warm → mesure latence, VRAM pic, format brut, parse OK, `cx_pct` mesuré. +4. **Critère go/no-go** : `cx_pct ∈ [0.40, 0.60]` ET `cy_pct ∈ [0.40, 0.60]` (le bouton OK est au centre du dialog, le dialog est centré écran) ET parse regex prod OK ET latence cold < 12 s. +5. **Livrable** : un CSV `/tmp/bench_grounding_2026-05-23.csv` + overlay PNG par modèle pour validation visuelle. + +**Go / no-go pour la migration AXE_A2** : si **aucun** modèle ne passe `cx_pct ∈ [0.40, 0.60]`, le bug n'est PAS uniquement `smart_resize` mais aussi côté preprocessing/prompt → escalader avant de remplacer la prod. + +--- + +## 2. Protocole détaillé + +### 2.1. Preprocessing image (par backend) + +| Backend | Resize | resized_w/h passé au modèle | Coords retournées en | +|---|---|---|---| +| **Ollama** (qwen2.5vl, qwen3-vl) | Implicite côté serveur, opaque | NON (non supporté) | Pixels post-resize OLLAMA (inconnu) ⚠ | +| **vLLM** (qwen3-vl-8b) | Côté client via `smart_resize` officiel | OUI (via `min_pixels`/`max_pixels` extra body) | Pixels post-resize CLIENT (connu) | +| **Transformers** (InfiGUI, OS-Atlas, Magma) | Côté script via `core/grounding/smart_resize.py` | OUI (passé au `processor`) | Pixels post-resize CLIENT (connu) | + +**Côté script** : on calcule `(rH, rW) = smart_resize(H, W)` sur l'image originale en utilisant `core.grounding.smart_resize` (factor 28 par défaut, à challenger après lecture du `probe_qwen3vl_processor.py` qui dump le factor effectif via `patch_size × merge_size`). On envoie l'image **redimensionnée** au backend (sauf Ollama qui re-resize de toute façon). + +### 2.2. Prompt (par famille de modèle) + +- **Qwen2.5-VL** (baseline) : `Detect 'OK button' in this image with a bounding box.` (prompt actuel `resolve_engine.py:942`) +- **Qwen3-VL Instruct** : prompt JSON explicite obligatoire — `Locate the "OK" button. Return ONLY this JSON: {"bbox_2d":[x1,y1,x2,y2],"label":"OK"}.` (Sans cette directive, sortie liste nue cf. MIGRATION_VLM_PLAN §2) +- **InfiGUI-G1-3B** : prompt du worker existant (`infigui_worker.py:130-135`) — `The screen's resolution is {rW}x{rH}. Locate the UI element(s) for "OK button", output the coordinates using JSON format: [{"point_2d": [x, y]}, ...]` + system avec `` +- **OS-Atlas-Base-7B** (Qwen2-VL-7B-FT) : `In the image, please find the bbox of "OK button". Output format: [[x1,y1,x2,y2]] with each value in [0,1000].` +- **Magma-8B** : pas dans le périmètre v0 (Set-of-Mark requiert SomEngine, hors A3) — documenter comme extension. + +### 2.3. Parsing sortie + +Réutiliser `core.grounding.bbox_parser.parse_bbox_to_norm(content, divisor_w, divisor_h)`. **CLEF du bench** : appeler avec `divisor_w = rW` et `divisor_h = rH` (post-resize), pas avec `orig_w`/`orig_h`. C'est le fix qu'AXE_A2 documente comme nécessaire. + +Pour OS-Atlas (sortie 0-1000), parser à part puis convertir : `cx_pct = (x1 + x2) / 2 / 1000`. + +### 2.4. Métriques mesurées (par modèle, en sortie CSV) + +| Colonne | Méthode | +|---|---| +| `model` | nom + backend | +| `cold_s` | 1er appel après `unload_all` (Ollama) ou après reload process (Transformers) | +| `warm_avg_s`, `warm_p50_s`, `warm_p95_s` | sur 10 runs warm (même image, même prompt) | +| `vram_pic_mib` | sample `nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits` toutes 200 ms pendant l'appel, max retenu | +| `raw_output` | premier 250 char de la réponse brute | +| `format_detected` | `bbox_2d` / `point_2d` / `xy_json` / `raw_array` / `unknown` | +| `parse_ok` | bool, parser regex prod renvoie un (x, y) | +| `cx_pct`, `cy_pct` | coords normalisées calculées avec `divisor = (rW, rH)` | +| `validated` | bool, `cx_pct ∈ [0.40, 0.60]` ET `cy_pct ∈ [0.40, 0.60]` | +| `error` | string si exception/timeout | + +### 2.5. Overlay PNG pour validation visuelle + +Pour chaque modèle qui retourne un (cx_pct, cy_pct), générer `bench_grounding_.png` = fixture + croix rouge au point retourné + texte ` cx=0.XX cy=0.YY`. Dom regarde les overlays côte-à-côte. + +--- + +## 3. Script Python autonome + +À créer en `/home/dom/ai/rpa_vision_v3/tools/bench_grounding_2026-05-23.py`. Code prêt à coller : + +```python +#!/usr/bin/env python3 +"""Bench grounding VLM — AXE A3 post smart_resize (2026-05-23). + +Objectif : refaire le bench bbox_2d du 8 mai 2026 après commit du module +`core/grounding/smart_resize.py` (0d7bcd18a) sur la fixture interne du projet. +Mesure latence cold/warm, VRAM pic, format brut, parse OK, cx_pct/cy_pct +mesuré contre vérité-terrain visuelle (bouton OK ≈ centre écran). + +Usage : + .venv/bin/python tools/bench_grounding_2026-05-23.py + [--models qwen25vl_ollama,qwen3vl_ollama,qwen3vl_vllm,infigui_tx] + [--warm 10] + [--out /tmp/bench_grounding_2026-05-23.csv] + [--overlay-dir /tmp/bench_grounding_overlays] + +Pré-requis runtime (à confirmer avant lancement, cf. §4) : + - Ollama tourne sur :11434 avec qwen2.5vl:7b et qwen3-vl:8b pull + - vLLM tourne sur :8100 avec Qwen3-VL-8B-Instruct (cf. §4) + - Transformers : .venv contient transformers + bitsandbytes + - nvidia-smi accessible (mesure VRAM) +""" + +from __future__ import annotations + +import argparse +import base64 +import csv +import io +import json +import math +import os +import re +import statistics +import subprocess +import sys +import threading +import time +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Optional + +import requests +from PIL import Image, ImageDraw, ImageFont + +# Ajout du repo au path pour réutiliser core.grounding.* +REPO_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(REPO_ROOT)) + +from core.grounding.smart_resize import smart_resize # noqa: E402 +from core.grounding.bbox_parser import parse_bbox_to_norm # noqa: E402 + + +# ============================================================================ +# Configuration +# ============================================================================ + +FIXTURE_PATH = REPO_ROOT / "data/training/live_sessions/bg_DESKTOP-58D5CAC_windows/shots/heartbeat_1773792436.png" +TARGET_LABEL = "OK button" +# Vérité-terrain manuelle (cf. §7) : bouton OK approximativement au centre +GT_CX_RANGE = (0.40, 0.60) +GT_CY_RANGE = (0.40, 0.60) + +OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434") +VLLM_URL = os.environ.get("VLLM_URL", "http://localhost:8100") + +# Une entrée = un modèle à bencher. Le champ "runner" identifie l'appel à faire. +MODEL_CATALOG: dict[str, dict[str, Any]] = { + "qwen25vl_ollama": { + "label": "qwen2.5vl:7b (Ollama, baseline buggy)", + "runner": "ollama", + "ollama_model": "qwen2.5vl:7b", + "prompt_style": "qwen25_bbox", + "num_predict": 100, + "think": None, # n/a + }, + "qwen3vl_ollama": { + "label": "qwen3-vl:8b (Ollama, prompt JSON explicite)", + "runner": "ollama", + "ollama_model": "qwen3-vl:8b", + "prompt_style": "qwen3_bbox_explicit", + "num_predict": 128, + "think": False, + }, + "qwen3vl_vllm": { + "label": "Qwen3-VL-8B-Instruct (vLLM, resized_w/h natif)", + "runner": "vllm", + "vllm_model": "Qwen/Qwen3-VL-8B-Instruct", + "prompt_style": "qwen3_bbox_explicit", + "max_tokens": 128, + }, + "infigui_tx": { + "label": "InfiGUI-G1-3B (Transformers, prod)", + "runner": "transformers_infigui", + "model_id": "InfiX-ai/InfiGUI-G1-3B", + "prompt_style": "infigui_point", + }, + # Extensions facultatives (AXE A1) — à activer via --models + "os_atlas_tx": { + "label": "OS-Atlas-Base-7B (Transformers, SOTA grounding)", + "runner": "transformers_qwen2vl", + "model_id": "OS-Copilot/OS-Atlas-Base-7B", + "prompt_style": "os_atlas_bbox_1000", + }, +} + + +# ============================================================================ +# Prompts par style +# ============================================================================ + +def build_prompt(style: str, rW: int, rH: int, label: str) -> tuple[str, str]: + """Retourne (system, user). System "" si pas utile.""" + if style == "qwen25_bbox": + return ( + "You locate UI elements on screenshots. Return coordinates.", + f"Detect '{label}' in this image with a bounding box.", + ) + if style == "qwen3_bbox_explicit": + return ( + "You are a UI element locator. Output raw JSON only. No explanation.", + f'Locate the "{label}" in this {rW}x{rH} screenshot. ' + f'Return ONLY this JSON object: ' + f'{{"bbox_2d":[x1,y1,x2,y2],"label":"{label}"}}', + ) + if style == "infigui_point": + return ( + "You FIRST think about the reasoning process as an internal monologue " + "and then provide the final answer.\n" + "The reasoning process MUST BE enclosed within tags.", + f'The screen\'s resolution is {rW}x{rH}.\n' + f'Locate the UI element(s) for "{label}", ' + f'output the coordinates using JSON format: ' + f'[{{"point_2d": [x, y]}}, ...]', + ) + if style == "os_atlas_bbox_1000": + return ( + "", + f'In the image, please find the bbox of "{label}". ' + f'Output the bounding boxes in this format: [[x1,y1,x2,y2]], ' + f'where each value is normalized in [0, 1000].', + ) + raise ValueError(f"prompt_style inconnu : {style}") + + +# ============================================================================ +# VRAM monitoring (nvidia-smi sampling) +# ============================================================================ + +class VRAMSampler: + """Sample nvidia-smi en thread, expose pic en MiB.""" + + def __init__(self, interval_s: float = 0.2): + self.interval = interval_s + self.peak_mib = 0 + self._stop = threading.Event() + self._thread: Optional[threading.Thread] = None + + def _loop(self) -> None: + while not self._stop.is_set(): + try: + out = subprocess.check_output( + ["nvidia-smi", "--query-gpu=memory.used", + "--format=csv,noheader,nounits", "-i", "0"], + timeout=2, + ).decode().strip() + mib = int(out.splitlines()[0]) + self.peak_mib = max(self.peak_mib, mib) + except Exception: + pass + self._stop.wait(self.interval) + + def start(self) -> None: + self.peak_mib = 0 + self._stop.clear() + self._thread = threading.Thread(target=self._loop, daemon=True) + self._thread.start() + + def stop(self) -> int: + self._stop.set() + if self._thread: + self._thread.join(timeout=2) + return self.peak_mib + + +# ============================================================================ +# Runners par backend +# ============================================================================ + +@dataclass +class CallResult: + elapsed_s: float + raw: str = "" + error: str = "" + vram_pic_mib: int = 0 + + +def _encode_image(img: Image.Image) -> str: + buf = io.BytesIO() + img.save(buf, format="JPEG", quality=85) + return base64.b64encode(buf.getvalue()).decode("ascii") + + +def _ollama_unload_all() -> None: + try: + ps = requests.get(f"{OLLAMA_URL}/api/ps", timeout=5).json() + for m in ps.get("models", []): + requests.post( + f"{OLLAMA_URL}/api/generate", + json={"model": m["name"], "prompt": "", "keep_alive": 0, "stream": False}, + timeout=10, + ) + except Exception: + pass + time.sleep(2) + + +def run_ollama(cfg: dict, img: Image.Image, rW: int, rH: int) -> CallResult: + system, user = build_prompt(cfg["prompt_style"], rW, rH, TARGET_LABEL) + img_resized = img.resize((rW, rH)) + img_b64 = _encode_image(img_resized) + messages = [] + if system: + messages.append({"role": "system", "content": system}) + messages.append({"role": "user", "content": user, "images": [img_b64]}) + options: dict[str, Any] = { + "temperature": 0.1, + "num_predict": cfg.get("num_predict", 128), + } + payload = { + "model": cfg["ollama_model"], + "messages": messages, + "stream": False, + "options": options, + } + if cfg.get("think") is False: + payload["think"] = False + sampler = VRAMSampler() + sampler.start() + t0 = time.perf_counter() + try: + resp = requests.post(f"{OLLAMA_URL}/api/chat", json=payload, timeout=120) + elapsed = time.perf_counter() - t0 + vram = sampler.stop() + if resp.status_code != 200: + return CallResult(elapsed, error=f"HTTP_{resp.status_code}", vram_pic_mib=vram) + content = resp.json().get("message", {}).get("content", "") + return CallResult(elapsed, raw=content, vram_pic_mib=vram) + except Exception as e: + sampler.stop() + return CallResult(time.perf_counter() - t0, error=f"NET:{type(e).__name__}") + + +def run_vllm(cfg: dict, img: Image.Image, rW: int, rH: int) -> CallResult: + system, user = build_prompt(cfg["prompt_style"], rW, rH, TARGET_LABEL) + img_resized = img.resize((rW, rH)) + img_b64 = _encode_image(img_resized) + messages = [] + if system: + messages.append({"role": "system", "content": system}) + messages.append({ + "role": "user", + "content": [ + {"type": "text", "text": user}, + {"type": "image_url", + "image_url": {"url": f"data:image/jpeg;base64,{img_b64}"}}, + ], + }) + payload = { + "model": cfg["vllm_model"], + "messages": messages, + "temperature": 0.1, + "max_tokens": cfg.get("max_tokens", 128), + # vLLM Qwen-VL extension : passer min/max pixels en kwargs + # (cf. github QwenLM/Qwen3-VL issue #1434 — peut être ignoré selon version vllm) + "mm_processor_kwargs": {"min_pixels": rW * rH, "max_pixels": rW * rH}, + } + sampler = VRAMSampler() + sampler.start() + t0 = time.perf_counter() + try: + resp = requests.post( + f"{VLLM_URL}/v1/chat/completions", json=payload, timeout=120, + ) + elapsed = time.perf_counter() - t0 + vram = sampler.stop() + if resp.status_code != 200: + return CallResult(elapsed, error=f"HTTP_{resp.status_code}", + raw=resp.text[:250], vram_pic_mib=vram) + content = (resp.json().get("choices", [{}])[0] + .get("message", {}).get("content", "")) + return CallResult(elapsed, raw=content, vram_pic_mib=vram) + except Exception as e: + sampler.stop() + return CallResult(time.perf_counter() - t0, error=f"NET:{type(e).__name__}") + + +# Pour les runners Transformers, on délègue au worker existant (subprocess) +# pour ne pas surcharger ce script en deps lourdes. Variante one-shot. +def run_transformers_infigui(cfg: dict, img: Image.Image, rW: int, rH: int) -> CallResult: + """Appel one-shot via core/grounding/infigui_worker.py. + + Note : InfiGUI utilise un système Unix-socket persistant en prod + (`core/grounding/infigui_server.py`). Pour le bench cold, on lance + le worker en subprocess one-shot (charge le modèle à chaque cold). + Pour les warms, on bench via le socket si dispo, sinon en subprocess + (et la mesure cold/warm sera identique — à documenter dans le CSV). + """ + req = { + "image_path": str(FIXTURE_PATH), + "target": TARGET_LABEL, + "description": "", + } + sampler = VRAMSampler() + sampler.start() + t0 = time.perf_counter() + try: + proc = subprocess.run( + [sys.executable, "-m", "core.grounding.infigui_worker"], + input=json.dumps(req), + capture_output=True, text=True, + timeout=180, cwd=str(REPO_ROOT), + ) + elapsed = time.perf_counter() - t0 + vram = sampler.stop() + if proc.returncode != 0: + return CallResult(elapsed, error=f"PROC_{proc.returncode}", + raw=(proc.stderr or "")[:250], vram_pic_mib=vram) + # Le worker écrit sur stdout du JSON final + out = proc.stdout.strip().splitlines()[-1] if proc.stdout.strip() else "{}" + return CallResult(elapsed, raw=out, vram_pic_mib=vram) + except Exception as e: + sampler.stop() + return CallResult(time.perf_counter() - t0, error=f"EXC:{type(e).__name__}") + + +def run_transformers_qwen2vl(cfg: dict, img: Image.Image, rW: int, rH: int) -> CallResult: + """Stub pour OS-Atlas / autres Qwen2-VL fine-tunés. + + À étoffer si Dom décide d'inclure OS-Atlas dans la 1ère salve. Procédure : + charger le modèle via Qwen2_5_VLForConditionalGeneration + AutoProcessor, + smart_resize côté script, prompt `os_atlas_bbox_1000`, parser 0-1000. + + Implémentation effective hors périmètre v0 — retourne un CallResult vide. + """ + return CallResult(0.0, error="NOT_IMPLEMENTED_V0") + + +RUNNERS = { + "ollama": run_ollama, + "vllm": run_vllm, + "transformers_infigui": run_transformers_infigui, + "transformers_qwen2vl": run_transformers_qwen2vl, +} + + +# ============================================================================ +# Parsing & validation +# ============================================================================ + +def detect_format(content: str) -> str: + if '"bbox_2d"' in content: + return "bbox_2d" + if '"point_2d"' in content: + return "point_2d" + if '"x_pct"' in content and '"y_pct"' in content: + return "xy_pct" + if re.search(r'"x"\s*:\s*[\d.]+.*?"y"\s*:\s*[\d.]+', content, re.S): + return "xy_json" + if re.search(r'\[\s*\[\s*\d+\s*,', content): + return "raw_2d_array" # OS-Atlas 0-1000 style + if re.search(r'\[\s*[\d.]+\s*,', content): + return "raw_array" + return "unknown" + + +def parse_coords(content: str, rW: int, rH: int, prompt_style: str + ) -> tuple[Optional[float], Optional[float]]: + """Retourne (cx_pct, cy_pct) normalisés ∈ [0, 1].""" + # OS-Atlas-style : 0-1000 + if prompt_style == "os_atlas_bbox_1000": + m = re.search(r'\[\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\]', content) + if m: + x1, y1, x2, y2 = [int(v) for v in m.groups()] + return (x1 + x2) / 2 / 1000.0, (y1 + y2) / 2 / 1000.0 + return None, None + + # InfiGUI point_2d (parser dédié, sortie en pixels post-resize rW/rH) + if prompt_style == "infigui_point": + m = re.search(r'"point_2d"\s*:\s*\[(\d+)\s*,\s*(\d+)\]', content) + if m: + x, y = int(m.group(1)), int(m.group(2)) + return x / rW, y / rH + # Le worker InfiGUI renvoie directement {"x": .., "y": ..} en pixels + # source résolution. On essaie aussi ce format. + m2 = re.search(r'"x"\s*:\s*(\d+).*?"y"\s*:\s*(\d+)', content, re.S) + if m2: + x, y = int(m2.group(1)), int(m2.group(2)) + # ici x/y sont en pixels image source — diviser par fixture size + return x / 2560.0, y / 1600.0 + return None, None + + # Cas général : parser regex prod avec divisor post-resize + return parse_bbox_to_norm(content, rW, rH) + + +def is_validated(cx: Optional[float], cy: Optional[float]) -> bool: + if cx is None or cy is None: + return False + return (GT_CX_RANGE[0] <= cx <= GT_CX_RANGE[1] + and GT_CY_RANGE[0] <= cy <= GT_CY_RANGE[1]) + + +# ============================================================================ +# Overlay PNG pour validation visuelle +# ============================================================================ + +def draw_overlay(img: Image.Image, cx: float, cy: float, label: str, + out_path: Path) -> None: + overlay = img.copy().convert("RGB") + draw = ImageDraw.Draw(overlay) + W, H = overlay.size + px, py = int(cx * W), int(cy * H) + r = 30 + draw.ellipse([px - r, py - r, px + r, py + r], outline="red", width=5) + draw.line([px - r * 2, py, px + r * 2, py], fill="red", width=3) + draw.line([px, py - r * 2, px, py + r * 2], fill="red", width=3) + try: + font = ImageFont.truetype( + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 28) + except OSError: + font = ImageFont.load_default() + txt = f"{label}\ncx={cx:.3f} cy={cy:.3f}" + draw.rectangle([10, 10, 700, 90], fill="white") + draw.text((20, 15), txt, fill="black", font=font) + overlay.save(out_path) + + +# ============================================================================ +# Bench main +# ============================================================================ + +@dataclass +class ModelStats: + key: str + label: str + cold_s: float = 0.0 + warm_times: list[float] = field(default_factory=list) + vram_peaks: list[int] = field(default_factory=list) + raw_first: str = "" + format_detected: str = "unknown" + parse_ok: bool = False + cx: Optional[float] = None + cy: Optional[float] = None + validated: bool = False + error: str = "" + + +def bench_one_model(key: str, cfg: dict, img: Image.Image, rW: int, rH: int, + warm_runs: int, overlay_dir: Optional[Path]) -> ModelStats: + stats = ModelStats(key=key, label=cfg["label"]) + runner = RUNNERS[cfg["runner"]] + + print(f"\n══════ {cfg['label']} ══════") + + # Déchargement VRAM avant cold (Ollama seulement — Transformers/vLLM + # gardent le modèle, c'est attendu) + if cfg["runner"] == "ollama": + _ollama_unload_all() + + # Cold + print(f" [cold]", end=" ", flush=True) + r0 = runner(cfg, img, rW, rH) + stats.cold_s = r0.elapsed_s + if r0.error: + stats.error = r0.error + print(f"❌ {r0.error}") + return stats + stats.raw_first = r0.raw[:250] + stats.format_detected = detect_format(r0.raw) + cx, cy = parse_coords(r0.raw, rW, rH, cfg["prompt_style"]) + stats.parse_ok = (cx is not None and cy is not None) + stats.cx, stats.cy = cx, cy + stats.validated = is_validated(cx, cy) + stats.vram_peaks.append(r0.vram_pic_mib) + print(f"{stats.cold_s:.2f}s | fmt={stats.format_detected} | " + f"cx={cx} cy={cy} | val={stats.validated}") + + # Warm + print(f" [warm × {warm_runs}]", end=" ", flush=True) + for i in range(warm_runs): + rN = runner(cfg, img, rW, rH) + if rN.error: + print(f" run{i}=❌{rN.error}", end="") + continue + stats.warm_times.append(rN.elapsed_s) + stats.vram_peaks.append(rN.vram_pic_mib) + print() + if stats.warm_times: + print(f" warm avg={statistics.mean(stats.warm_times):.2f}s | " + f"p95={sorted(stats.warm_times)[int(len(stats.warm_times)*0.95)-1]:.2f}s") + + # Overlay (si parse OK) + if overlay_dir and cx is not None and cy is not None: + overlay_dir.mkdir(parents=True, exist_ok=True) + draw_overlay(img, cx, cy, cfg["label"], + overlay_dir / f"bench_{key}.png") + + return stats + + +def write_csv(all_stats: list[ModelStats], out_path: Path) -> None: + with out_path.open("w", newline="") as f: + w = csv.writer(f) + w.writerow([ + "key", "label", "cold_s", + "warm_avg_s", "warm_p50_s", "warm_p95_s", + "vram_pic_mib", + "format", "parse_ok", "cx_pct", "cy_pct", "validated", + "raw_first", "error", + ]) + for s in all_stats: + warm_avg = statistics.mean(s.warm_times) if s.warm_times else 0.0 + warm_p50 = statistics.median(s.warm_times) if s.warm_times else 0.0 + warm_p95 = (sorted(s.warm_times)[int(len(s.warm_times)*0.95)-1] + if len(s.warm_times) > 1 else (s.warm_times[0] + if s.warm_times else 0.0)) + vram = max(s.vram_peaks) if s.vram_peaks else 0 + w.writerow([ + s.key, s.label, f"{s.cold_s:.2f}", + f"{warm_avg:.2f}", f"{warm_p50:.2f}", f"{warm_p95:.2f}", + vram, + s.format_detected, s.parse_ok, + f"{s.cx:.4f}" if s.cx is not None else "", + f"{s.cy:.4f}" if s.cy is not None else "", + s.validated, s.raw_first.replace("\n", " "), s.error, + ]) + + +def print_summary(all_stats: list[ModelStats]) -> None: + print("\n\n══════════════════ SYNTHÈSE ══════════════════") + print("| Modèle | Cold (s) | Warm avg | VRAM MiB | Fmt | cx | cy | VAL |") + print("|---|---:|---:|---:|---|---:|---:|:---:|") + for s in all_stats: + warm_avg = statistics.mean(s.warm_times) if s.warm_times else 0.0 + vram = max(s.vram_peaks) if s.vram_peaks else 0 + cx_s = f"{s.cx:.3f}" if s.cx is not None else "—" + cy_s = f"{s.cy:.3f}" if s.cy is not None else "—" + mark = "✅" if s.validated else ("⚠" if s.parse_ok else "❌") + print(f"| {s.label[:40]} | {s.cold_s:.1f} | {warm_avg:.1f} | " + f"{vram} | {s.format_detected} | {cx_s} | {cy_s} | {mark} |") + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--models", default="qwen25vl_ollama,qwen3vl_ollama,qwen3vl_vllm,infigui_tx", + help="liste séparée par virgules (clés dans MODEL_CATALOG)") + ap.add_argument("--warm", type=int, default=10) + ap.add_argument("--out", default="/tmp/bench_grounding_2026-05-23.csv") + ap.add_argument("--overlay-dir", default="/tmp/bench_grounding_overlays") + args = ap.parse_args() + + if not FIXTURE_PATH.exists(): + print(f"ERROR: fixture absente — {FIXTURE_PATH}") + return 2 + + img = Image.open(FIXTURE_PATH).convert("RGB") + W, H = img.size + rH, rW = smart_resize(H, W) + print(f"Fixture : {FIXTURE_PATH}") + print(f" source : {W}×{H}") + print(f" smart_resize() → {rW}×{rH} (factor=28)") + print(f" vérité-terrain : cx ∈ {GT_CX_RANGE}, cy ∈ {GT_CY_RANGE}") + print(f" cible : '{TARGET_LABEL}'") + + keys = [k.strip() for k in args.models.split(",") if k.strip()] + unknown = [k for k in keys if k not in MODEL_CATALOG] + if unknown: + print(f"ERROR: modèles inconnus : {unknown}") + print(f"Catalog : {list(MODEL_CATALOG)}") + return 2 + + overlay_dir = Path(args.overlay_dir) if args.overlay_dir else None + all_stats: list[ModelStats] = [] + for k in keys: + try: + stats = bench_one_model( + k, MODEL_CATALOG[k], img, rW, rH, args.warm, overlay_dir) + all_stats.append(stats) + except KeyboardInterrupt: + print(f"\n⚠ Interrompu pendant {k}") + break + except Exception as e: + print(f"\n❌ Crash {k}: {e}") + all_stats.append(ModelStats(key=k, label=MODEL_CATALOG[k]["label"], + error=f"crash:{e}")) + + out = Path(args.out) + write_csv(all_stats, out) + print(f"\nCSV écrit : {out}") + if overlay_dir: + print(f"Overlays : {overlay_dir}/") + + print_summary(all_stats) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) +``` + +**Vérification syntaxique** : imports cohérents avec `core/grounding/smart_resize.py` (export `smart_resize`) et `core/grounding/bbox_parser.py` (export `parse_bbox_to_norm`). Pas de dépendance externe hors `requests`, `Pillow` (déjà dans `.venv`). `subprocess` pour Transformers worker + nvidia-smi. **À tester par Dom** avec `--models qwen25vl_ollama` seul d'abord pour valider l'I/O. + +--- + +## 4. Procédure d'install pour Dom + +### 4.1. Prérequis Ollama + +```bash +# Vérifier que les 2 modèles sont pull +ollama list | grep -E "qwen2.5vl|qwen3-vl" +# Si manquants : +ollama pull qwen2.5vl:7b # ~8 GB +ollama pull qwen3-vl:8b # ~6 GB +``` + +### 4.2. Prérequis vLLM (option recommandée) + +`vLLM` est listé dans `MIGRATION_VLM_PLAN_2026-05-09.md` §3 comme cible. Démarrage suggéré dans un terminal séparé (ou un service systemd dédié, voir `tools/start_grounding_server.sh` pour le pattern existant) : + +```bash +# Dans venv_v3 (créer pip install vllm si manquant) +cd ~/ai/rpa_vision_v3 && source venv_v3/bin/activate +pip install --upgrade vllm # >= 0.6.5 pour Qwen3-VL + +python -m vllm.entrypoints.openai.api_server \ + --model Qwen/Qwen3-VL-8B-Instruct \ + --host 0.0.0.0 --port 8100 \ + --gpu-memory-utilization 0.55 \ + --max-model-len 8192 \ + --limit-mm-per-prompt image=1 \ + --mm-processor-kwargs '{"min_pixels": 100352, "max_pixels": 1003520}' +``` + +**Note** : le hardware n'a que 12 GB VRAM. Si Ollama tourne en parallèle avec un autre modèle chargé, prévoir `unload_all` Ollama avant lancement vLLM (`for m in $(ollama ps | awk 'NR>1 {print $1}'); do curl -X POST localhost:11434/api/generate -d "{\"model\":\"$m\",\"keep_alive\":0}"; done`). + +### 4.3. Modèles optionnels (HuggingFace) + +```bash +# OS-Atlas (si on l'active dans --models) +huggingface-cli download OS-Copilot/OS-Atlas-Base-7B +# Magma (extension future) +huggingface-cli download microsoft/Magma-8B +``` + +Les modèles HF se cachent automatiquement dans `~/.cache/huggingface/hub/`. Comptez ~15 GB par modèle 7-8B en bf16, ~4 GB en 4-bit NF4 (notre stack quantize à la volée pour InfiGUI). + +### 4.4. Lancement bench + +```bash +cd ~/ai/rpa_vision_v3 && source venv_v3/bin/activate + +# Salve baseline (sans vLLM, sans OS-Atlas) +.venv/bin/python tools/bench_grounding_2026-05-23.py \ + --models qwen25vl_ollama,qwen3vl_ollama,infigui_tx \ + --warm 10 + +# Salve complète (vLLM démarré préalablement) +.venv/bin/python tools/bench_grounding_2026-05-23.py \ + --models qwen25vl_ollama,qwen3vl_ollama,qwen3vl_vllm,infigui_tx \ + --warm 10 + +# Avec OS-Atlas (nécessite implémentation effective de run_transformers_qwen2vl) +.venv/bin/python tools/bench_grounding_2026-05-23.py \ + --models qwen25vl_ollama,qwen3vl_vllm,infigui_tx,os_atlas_tx \ + --warm 10 +``` + +Durée estimée pour la salve baseline : 1 cold + 10 warm × 3 modèles. Si cold ≈ 11s et warm ≈ 2s : ~2 min/modèle, total ~7-10 min. + +--- + +## 5. Modèles candidats avec configs précises + +### 5.1. `qwen2.5vl:7b` (Ollama, baseline buggy attendue) + +- Backend : Ollama HTTP `/api/chat` +- Prompt : `Detect 'OK button' in this image with a bounding box.` (prompt actuel `resolve_engine.py:942`) +- Options : `temperature=0.1`, `num_predict=100` +- **Attendu** : bbox_2d en pixels post-resize Ollama (opaque) → `cx_pct ≈ 0.17` (bug confirmé 8 mai) +- Rôle dans le bench : **témoin du bug**, doit échouer pour confirmer que le bug est reproductible et que le bench discrimine. + +### 5.2. `qwen3-vl:8b` (Ollama, prompt JSON explicite) + +- Backend : Ollama HTTP `/api/chat` +- Prompt système : `You are a UI element locator. Output raw JSON only. No explanation.` +- Prompt user : `Locate the "OK button" in this {rW}x{rH} screenshot. Return ONLY this JSON object: {"bbox_2d":[x1,y1,x2,y2],"label":"OK button"}` +- Options : `temperature=0.1`, `num_predict=128`, `think:false` +- ⚠ Note web search : Ollama issue #14798 — `think:false` est **silencieusement ignoré** pour qwen3-vl:8b (template bare). Vérifier sur les outputs si `...` apparaît. Workaround : préfixer le user prompt par `/no_think`. +- **Attendu** : si `smart_resize` côté script correspond au resize interne Ollama, `cx ≈ 0.50`. Sinon, même bug que qwen2.5vl. + +### 5.3. `Qwen3-VL-8B-Instruct` (vLLM, cible migration) + +- Backend : vLLM OpenAI-compat sur :8100 +- Image envoyée **déjà resize côté client** au `smart_resize`-output → vLLM ne re-resize plus (à confirmer via `mm_processor_kwargs={"min_pixels": rW*rH, "max_pixels": rW*rH}` pour forcer no-op). +- Prompt : idem 5.2 +- `max_tokens=128`, `temperature=0.1` +- **Attendu** : `cx_pct ∈ [0.40, 0.60]` si la chaîne resize+prompt+parse est cohérente. C'est la config qui valide AXE_A2. + +### 5.4. `InfiGUI-G1-3B` (Transformers, prod actuelle) + +- Backend : `core/grounding/infigui_worker.py` en subprocess (one-shot) OU socket Unix via `rpa-grounding.service` si actif. +- Prompt : du worker, sortie `point_2d` (pas `bbox_2d`). +- **Spécificité** : le worker écrit (x, y) en pixels **source** déjà (re-multiplie par `W/rW, H/rH` ligne 174-175). Donc `cx_pct = x_returned / 2560`, `cy_pct = y_returned / 1600` direct. Vérifier au runtime. +- **Attendu** : `cx_pct ≈ 0.50` (le worker est testé en prod, c'est la baseline qui marche déjà selon `HISTORIQUE_VLM_IMPLEMENTATIONS_2026-05-08.md`). + +### 5.5. `OS-Atlas-Base-7B` (Transformers, candidate SOTA, AXE A1) + +- Backend : Transformers Qwen2.5-VL chargé en 4-bit NF4 (à implémenter, runner `transformers_qwen2vl` est un stub v0). +- Prompt : `In the image, please find the bbox of "OK button". Output the bounding boxes in this format: [[x1,y1,x2,y2]], where each value is normalized in [0, 1000].` +- Output 0-1000 normalisé → conversion directe `cx_pct = (x1 + x2) / 2 / 1000`. +- **Attendu** : SOTA sur ScreenSpot/ScreenSpot-Pro selon HF README → `cx_pct ∈ [0.40, 0.60]` probable. + +### 5.6. `Magma-8B` (extension future, non v0) + +Magma utilise **Set-of-Mark** : il faut détecter d'abord les éléments (SomEngine ou OmniParser) puis lui demander de choisir un numéro. Pas directement comparable aux 5 candidats ci-dessus. À benchmarker dans un AXE A4 séparé. + +--- + +## 6. Critère de validation success (matrice) + +| Modèle | Latence cold | Warm avg | cx_pct ∈ [0.40, 0.60] | cy_pct ∈ [0.40, 0.60] | Parse regex prod | Verdict | +|---|---|---|---|---|---|---| +| qwen25vl_ollama | < 12 s | < 12 s | ❌ (attendu 0.17) | ? | ✅ | **Témoin OK si tout sauf cx pass** | +| qwen3vl_ollama | < 5 s | < 3 s | ✅ ou ❌ selon resize | ✅ | ✅ si prompt JSON | go si ✅ | +| qwen3vl_vllm | < 8 s | < 3 s | ✅ requis | ✅ requis | ✅ requis | **CIBLE migration AXE_A2** | +| infigui_tx | < 15 s | < 4 s | ✅ requis | ✅ requis | ✅ (point_2d) | Baseline prod | +| os_atlas_tx | < 15 s | < 5 s | ✅ requis | ✅ requis | ✅ (raw_2d 0-1000) | Candidat upgrade | + +**Verdict global AXE_A3** : +- Si `qwen3vl_vllm` ET `infigui_tx` passent ✅ → migration vers vLLM Qwen3-VL est **safe**, AXE_A2 peut être considéré comme résolu. +- Si **seul `infigui_tx` passe** → la prod actuelle est valide, vLLM Qwen3-VL n'apporte rien à part la latence — décision business sur la migration. +- Si **rien ne passe** → le bug n'est PAS dans `smart_resize` seul. Investiguer `divisor_w/h` côté `parse_bbox_to_norm`, et la chaîne window crop (`window_rect` line 916-919) qui pourrait introduire un offset. + +--- + +## 7. Vérité terrain manuelle pour Dom + +Avant tout bench, Dom doit confirmer visuellement où est le bouton OK sur la fixture. Trois méthodes au choix : + +### Méthode 1 : aperçu rapide via xdg-open + +```bash +xdg-open /home/dom/ai/rpa_vision_v3/data/training/live_sessions/bg_DESKTOP-58D5CAC_windows/shots/heartbeat_1773792436.png +``` + +Ouvrir l'image, identifier le bouton OK à l'œil, estimer `cx_pct ≈ pixel_x / 2560` et `cy_pct ≈ pixel_y / 1600`. + +### Méthode 2 : overlay grille via PIL (one-liner) + +```bash +.venv/bin/python -c " +from PIL import Image, ImageDraw +img = Image.open('data/training/live_sessions/bg_DESKTOP-58D5CAC_windows/shots/heartbeat_1773792436.png').convert('RGB') +draw = ImageDraw.Draw(img) +W, H = img.size +# Grille de référence en % +for pct in [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]: + x = int(pct * W) + draw.line([(x, 0), (x, H)], fill='cyan', width=2) + draw.text((x + 5, 10), f'{pct:.1f}', fill='cyan') + y = int(pct * H) + draw.line([(0, y), (W, y)], fill='cyan', width=2) + draw.text((10, y + 5), f'{pct:.1f}', fill='cyan') +img.save('/tmp/heartbeat_grid.png') +print('grille → /tmp/heartbeat_grid.png') +" +xdg-open /tmp/heartbeat_grid.png +``` + +Dom regarde où est le bouton OK et note `(cx_gt, cy_gt)`. Si OK n'est pas dans `[0.40, 0.60]² ` après cette vérif, **ajuster `GT_CX_RANGE` / `GT_CY_RANGE` dans le script** avant tout bench. + +### Méthode 3 : crop autour de la zone OK + +```bash +.venv/bin/python -c " +from PIL import Image +img = Image.open('data/training/live_sessions/bg_DESKTOP-58D5CAC_windows/shots/heartbeat_1773792436.png') +# Crop la zone supposée 0.4-0.6 × 0.4-0.6 +W, H = img.size +img.crop((int(0.35*W), int(0.35*H), int(0.65*W), int(0.65*H))).save('/tmp/heartbeat_center_crop.png') +" +xdg-open /tmp/heartbeat_center_crop.png +``` + +Si le bouton OK est visible dans ce crop, vérité-terrain `[0.40, 0.60]` est valide. + +--- + +## 8. Extensions futures (autres fixtures à ajouter) + +Une seule fixture = sous-dimensionné pour conclure. Au minimum 3 fixtures additionnelles, idéalement issues du replay réel post-démo : + +| Fixture | Origine | Cible | cx_gt approx | Difficulté | +|---|---|---|---|---| +| **Tabs Easily Assure** | replay 8 mai bug step 10 | "Imagerie" / "Notes médicales" / "Synthèse Urgences" (3 cas) | par tab | discriminer 3 textes dans une barre — confondus en center-of-line (REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md §4.2) | +| **Popup IPP recherche** | recording VWB urgence_aiva_demo | bouton "Rechercher" | mid-screen | popup centré, peut avoir plusieurs occurrences du même texte (cf. `original_position` y_relative pour désambiguer) | +| **Bandeau outils Easily** | shot quelconque | icône "disquette" (sans texte) | top-left de la barre | grounding visuel pur (pas d'OCR), c'est le cas pour lequel le grounding VLM est censé exister | + +Ces extensions justifieraient un **AXE A3.2 — Multi-fixture grounding bench** une fois A3.1 (cette spec) validé. Le script ci-dessus est déjà conçu pour accepter une liste de fixtures (refacto trivial : `FIXTURE_PATH` + `TARGET_LABEL` + `GT_*_RANGE` → liste de tuples, boucle externe). + +--- + +## 9. Dépendances et liens avec autres AXES + +- **AXE A1 (sélection modèles)** : alimente la section §5 — si A1 retient OS-Atlas-Pro-7B ou ShowUI-2B en lieu et place de OS-Atlas-Base-7B, ajuster `MODEL_CATALOG`. +- **AXE A2 (smart_resize calibration)** : ce bench est la **validation end-to-end** d'A2. Tant qu'A2 n'a pas tranché entre factor=28 (Qwen2-VL) et factor=32 (Qwen3-VL), lancer A3 d'abord avec `factor=28` (état actuel `core/grounding/smart_resize.py`) puis re-bench avec `factor=32` (modifier `FACTOR_DEFAULT` du module ou patcher le script). +- **Hors scope A3** : SomEngine + Magma (Set-of-Mark) ; ces 2 stratégies de grounding nécessitent un détecteur amont (cf. `_resolve_by_som` dans `resolve_engine.py:1095+`) et un bench distinct. + +--- + +*Document destiné à être consommé par Dom et un agent d'exécution. Aucune action runtime déclenchée par cette spec. À mettre à jour quand A1 et A2 auront tranché leurs paramètres.* diff --git a/docs/recherche/AXE_A4_OCR_TEMPLATE_PHASH.md b/docs/recherche/AXE_A4_OCR_TEMPLATE_PHASH.md new file mode 100644 index 000000000..1428bb268 --- /dev/null +++ b/docs/recherche/AXE_A4_OCR_TEMPLATE_PHASH.md @@ -0,0 +1,487 @@ +# AXE A4 — OCR, Template matching, pHash : revue 2026 + correctif `_resolve_by_ocr_text` + +**Date :** 2026-05-23 +**Auteur :** Claude (dispatch recherche) +**Périmètre :** revue littérature/écosystème 2025-2026 pour la cascade UI `OCR → template → VLM` + alternatives à `pHash` pour LoopDetector et VERIFY. Patch ciblé du bug *center-of-line* de `_resolve_by_ocr_text` (`agent_v0/server_v1/resolve_engine.py:1447-1527`). +**Lecture pré-requise :** `docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` §2, §4 ; `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` §1.2 et §5 ; `docs/BUG_PRECHECK_SPATIAL_BLINDNESS_2026-05-08.md` (DETTE-001). +**Statut :** recherche + propositions. **Aucune modification de code.** Toute application validée par Dom. + +--- + +## 0. TL;DR + +1. **Le bug primaire `center-of-line` est résolvable sans changer d'OCR.** docTR expose les `geometry` au niveau du `Word`, normalisées dans le **même repère que la ligne**. Le quick fix §5 du diagnostic 8 mai (cf. §5 ci-dessous, code copy-paste-ready) supprime la collision Imagerie/Notes/Synthèse en restant 100 % iso-stack. +2. **OCR : garder docTR comme moteur OCR-DIRECT** (mode strict + cascade) car c'est le seul, avec Tesseract et PaddleOCR `return_word_box=True`, à exposer des bbox **token-level dans le même repère que la ligne**. EasyOCR retourne par défaut des bbox merges niveau line/segment et **n'est pas adapté** à la résolution multi-tokens d'un onglet sur barre. Surya OCR = line-level uniquement, à écarter pour ce besoin. RapidOCR (PaddleOCR ONNX repackagé) → candidat 2026 pour OCR-DIRECT *léger sans dépendance Paddle*, à valider sur français accentué. +3. **Template matching : remplacer `cv2.matchTemplate` multi-scale par SuperPoint+LightGlue (ONNX, ~50 ms par paire sur RTX 5070).** C'est la sortie propre pour la drift exemption `≥ 0.95` actuelle, qui est un faux positif déguisé (score haut sur région différente). LightGlue est invariant à l'offset/scale/rotation et fournit un *score de cohérence géométrique* — donc plus de faux positifs « 0.95 sur mauvaise zone ». À encapsuler derrière `_resolve_by_template` sans casser la cascade. +4. **pHash : sortir du global. Deux modes complémentaires :** + - **LoopDetector (QW2)** → DINOv2 features sur l'écran entier, cos-sim < 0.99 = écran a bougé. Plus robuste qu'un pHash 64-bit à un curseur clignotant ou à un caret blinking. + - **VERIFY post-action** → **SSIM par ROI** (skimage `structural_similarity`, ~5-10 ms sur crop 400×200), avec ROI = bbox de la cible cliquée + halo 50 px. C'est la version *spatialisée* qui résout aussi DETTE-001 (BUG_PRECHECK_SPATIAL_BLINDNESS). +5. **Dépendances** : ce travail est **bloquant** pour AXE_A5 (tokenisation UI : OmniParser et UI-DETR-1 utilisent in fine un OCR + détection icônes — décider du moteur OCR avant tokenisation). Il **alimente** AXE_B2 (Validator) qui consommera SSIM-ROI comme signal sémantique de VERIFY. + +--- + +## 1. Sous-axe 1 — OCR pour grounding + +### 1.1. Question centrale : bbox token-level dans le même repère que la ligne + +Le bug `center-of-line` apparaît parce que `_resolve_by_ocr_text` (resolve_engine.py:1486-1519) calcule `cx, cy` à partir de la `line_obj.geometry` (bbox de la ligne entière) alors que `target_text` n'est qu'un sous-fragment. Pour le résoudre **sans changer d'OCR**, il suffit que l'OCR expose, dans le même repère normalisé que la ligne, les bbox des **words** qui composent la ligne. C'est le critère discriminant. + +### 1.2. Table comparative (mai 2026) + +| OCR | Granularité bbox | Repère | Français/accents | Latence (CPU 2560×1600) | Stack | Licence | Date release majeure | +|---|---|---|---|---|---|---|---| +| **docTR** (`python-doctr`) | **word + line + block** | normalisé `[(xmin,ymin),(xmax,ymax)]` ∈ [0,1]², **commun line/word** | bon (modèle `crnn_vgg16_bn` français) | ~800 ms CPU, ~150 ms GPU | PyTorch + TF, ONNX optionnel | Apache 2.0 | v0.10 (2026-04, `python-doctr` PyPI) | +| **EasyOCR** | line merged (par défaut) + char optionnel via `ycenter_ths`/`width_ths` | pixel absolu | bon | ~1.2 s CPU, ~200 ms GPU | PyTorch, CRNN | Apache 2.0 | v1.7.x (2024) | +| **RapidOCR** (`rapidocr`) | line | pixel absolu | bon (modèle PP-OCRv4 fr) | ~200 ms ONNX-CPU, ~80 ms GPU | ONNXRuntime / OpenVINO / MNN / PaddlePaddle, **sans dépendance Paddle** | Apache 2.0 | v3.x (2026-04-11) | +| **PaddleOCR / PP-StructureV3** | line par défaut ; **`return_word_box=True`** en option | pixel absolu | bon | ~250 ms GPU (PP-OCRv4) | PaddlePaddle (lourd) | Apache 2.0 | v3.0 (2025-07) | +| **Surya OCR** (`surya-ocr`) | **line only** | pixel absolu | bon (90+ langues) | ~400 ms GPU (5070-class) | PyTorch | GPL-3.0 (commercial restrictif) | v0.17.x (2025) | +| **Tesseract** (via `pytesseract`) | **word + line + char** via `image_to_data` / `hOCR` | pixel absolu | moyen-bon (modèle `fra`) | 100-500 ms CPU | C++ LSTM | Apache 2.0 | v5.4 (2024) | + +**Sources principales :** [docTR Word/Line geometry — Discussion #570](https://github.com/mindee/doctr/discussions/570), [PaddleOCR return_word_box — Issue #15760](https://github.com/PaddlePaddle/PaddleOCR/issues/15760), [Surya line-level — repo datalab-to/surya](https://github.com/datalab-to/surya), [EasyOCR character bbox limitation — Issue #631](https://github.com/JaidedAI/EasyOCR/issues/631), [RapidOCR releases](https://github.com/RapidAI/RapidOCR/releases), [pytesseract image_to_data — PyPI](https://pypi.org/project/pytesseract/), [Codesota benchmark PaddleOCR vs EasyOCR 2025](https://www.codesota.com/ocr/paddleocr-vs-easyocr), [Tildalice benchmark PaddleOCR vs Doctr](https://buttondown.com/ckae930413/archive/paddleocr-vs-easyocr-vs-doctr-memory-latency-test/). + +### 1.3. Analyse du bug center-of-line + +**Le bug est résolvable nativement avec docTR.** L'API expose `line_obj.words` (List[Word]) avec chaque `Word.geometry` au même format `((xmin,ymin),(xmax,ymax))` normalisé que `line_obj.geometry`. Il n'y a aucun changement de repère à faire — c'est le même page-relative ∈ [0,1]². Cf. [docTR I/O modules doc](https://mindee.github.io/doctr/modules/io.html). + +EasyOCR a la **mauvaise granularité par défaut** : il merge les détections en segments via `ycenter_ths=0.5` et `width_ths=0.5`, donc une rangée de tabs serrée tombera comme une boîte unique, **sans accès aux sous-words**. Demander explicitement `width_ths=0.0` casserait la fusion mais aussi pour les vrais textes longs (« Justification de la décision »). **EasyOCR seul ne résout pas le bug.** + +Surya OCR est annoncé explicitement comme line-only : « Surya predicts line-level bboxes, while tesseract and others predict word-level or character-level » (cf. [datalab-to/surya README](https://github.com/datalab-to/surya)). **À écarter** pour ce besoin. + +PaddleOCR `return_word_box=True` est disponible en v3.0 mais nécessite une dépendance PaddlePaddle ~700 Mo et un init ~8-12 s sur CPU. + +RapidOCR repackage les modèles PaddleOCR en ONNX (80 Mo install, init <2 s) ; **il faut vérifier en mai 2026 si `return_word_box` est exposé dans la couche `rapidocr.RapidOCR(__call__)` ou seulement dans `paddleocr.PaddleOCR`**. À ce jour, la doc publique RapidOCR ne mentionne pas explicitement le mode word-bbox. + +### 1.4. Snippets Python — récupérer bbox word-level + +**docTR (déjà utilisé en production)** + +```python +from doctr.models import ocr_predictor +from doctr.io import DocumentFile + +predictor = ocr_predictor(pretrained=True) +doc = DocumentFile.from_images("/path/screenshot.png") +result = predictor(doc) + +# Navigation hiérarchique : pages -> blocks -> lines -> words +page = result.pages[0] +H, W = page.dimensions # (height, width) pixels + +for block in page.blocks: + for line in block.lines: + # line.geometry == ((xmin, ymin), (xmax, ymax)) normalisé [0,1]² + # line.words == List[Word], chaque Word.geometry au même format + for word in line.words: + (xmin_n, ymin_n), (xmax_n, ymax_n) = word.geometry + # Pixels absolus + xmin_px = xmin_n * W + ymax_px = ymax_n * H + print(f"{word.value!r} bbox=({xmin_px:.0f},{ymin_px*H:.0f})-({xmax_px:.0f},{ymax_px:.0f})") +``` + +**Tesseract (alternative légère, fallback CPU)** + +```python +import pytesseract +from PIL import Image + +img = Image.open("/path/screenshot.png") +data = pytesseract.image_to_data(img, lang="fra", output_type=pytesseract.Output.DICT) + +# data == dict with keys 'text','left','top','width','height','conf','line_num','word_num','block_num' +n = len(data['text']) +for i in range(n): + if data['text'][i].strip() and int(data['conf'][i]) > 50: + x, y, w, h = data['left'][i], data['top'][i], data['width'][i], data['height'][i] + # Pixels absolus directement + print(f"{data['text'][i]!r} bbox=({x},{y})-({x+w},{y+h}) line={data['line_num'][i]}") +``` + +**RapidOCR (candidat migration, ONNX léger)** + +```python +from rapidocr import RapidOCR + +engine = RapidOCR() +result, elapsed = engine("/path/screenshot.png") +# result == [[box, text, score], ...] avec box = [[x1,y1],[x2,y2],[x3,y3],[x4,y4]] pixel absolu +# ⚠ Niveau line par défaut — à valider en mai 2026 si word-level disponible +``` + +### 1.5. Recommandation + +**Garder docTR pour OCR-DIRECT** (mode strict + cascade resolve_engine). C'est l'OCR qui colle déjà aux contraintes du bug. Le quick fix §5 (recalcul `cx, cy` depuis `line.words`) ne nécessite ni migration ni changement d'API. + +**Ne PAS migrer en chaud vers EasyOCR ou Surya** : EasyOCR perd le sous-word, Surya est line-only par design. + +**Évaluation parallèle** (post-démo, AXE_A5) : +- RapidOCR sur 10 captures Easily fr — gain potentiel : init 2 s vs 5-8 s docTR, install 80 Mo vs 500 Mo + PyTorch. +- Tesseract `image_to_data` lang `fra` — peut servir de **second moteur OCR de vérification** (vote OCR à 2 moteurs) pour DETTE-001. + +--- + +## 2. Sous-axe 2 — Template matching (étage 2 cascade) + +### 2.1. Question centrale : robustesse à l'offset/scale + élimination des faux positifs 0.95 + +`cv2.matchTemplate` multi-scale (range 0.25→2.0, `resolve_engine.py:130`) calcule un score de corrélation NCC pixel-à-pixel. Limites connues : +- **Aucune invariance à la rotation.** Easily/Edge sont fixes en rotation, donc OK ici. +- **Sensible à l'anti-aliasing** : un même bouton scaled 0.95× vs 1.0× peut perdre 0.10 sur le score. +- **Le score haut ne garantit pas la bonne région** : le match peut être 0.95 sur un patch visuellement similaire (autre bouton de la même barre, même icône de close, etc.). C'est exactement le mécanisme qui force aujourd'hui le `drift exemption ≥ 0.95` (`resolve_engine.py:2367-2390`) à être une rustine — score haut, mauvais endroit. +- Cf. [PyImageSearch multi-scale template matching](https://pyimagesearch.com/2015/01/26/multi-scale-template-matching-using-python-opencv/) et [Medium — Template Matching Beyond Basics](https://medium.com/@coders.stop/template-matching-beyond-basics-rotation-and-scale-invariant-detection-2ae78d8fa190). + +### 2.2. Table comparative + +| Méthode | Invariance | Score géométrique | Latence pair (RTX 5070, 800×500 vs 2560×1600) | Faux positif 0.95 ? | Licence | Maturité 2026 | +|---|---|---|---|---|---|---| +| **`cv2.matchTemplate` NCC multi-scale** (actuel) | scale ±20 % (force brute) | non — score pixel | ~50-200 ms CPU (multi-scale loop) | **oui** (rustine drift exemption) | BSD | mature | +| **SIFT / AKAZE / ORB (cv2)** | scale + rotation + offset | non — inliers RANSAC | ~30 ms CPU | filtré par RANSAC mais sensible aux UI peu texturées | BSD | mature | +| **SuperPoint + LightGlue (ONNX)** | scale + rotation + offset + photométrie | **oui** — score MNN + inliers | **~44 ms (22 FPS) pair complète RTX-class** | **non** si on prend `len(matches) > seuil` ET cohérence homographique | Apache 2.0 (modèle), [fabio-sim/LightGlue-ONNX](https://github.com/fabio-sim/LightGlue-ONNX) | très mature 2024-2026 | +| **LoFTR / Efficient LoFTR** | id. | id. + dense | ~80 ms pair RTX-class | non | Apache 2.0 | mature, +1-2 pp AUC vs LightGlue mais 2× plus lent | +| **DINOv2 patch features + kNN match** | id. + sémantique | cosine sim patch | ~150 ms (extract DINOv2 ViT-L) | rare (sémantique > pixel) | CC-BY-NC-4.0 ⚠ | très mature 2024-2026 | +| **RoMa / RoMa v2** | id. + dense, sub-pixel | warp + certainties | ~200 ms RTX-class (v2 = 1.7× v1) | non | non-commercial | CVPR 2024, v2 fin 2025 | +| **MASt3R-SfM** | id. + 3D | grid match | très lourd (~1 s+ par pair) | non | non-commercial | recherche 2024 | +| **CLIP visual similarity** (global embedding) | id. + sémantique | cos-sim global | ~30 ms ViT-B/32 | échoue : trop global, ne localise pas | MIT | mature | + +**Sources :** [LightGlue ICCV 2023 paper](https://openaccess.thecvf.com/content/ICCV2023/papers/Lindenberger_LightGlue_Local_Feature_Matching_at_Light_Speed_ICCV_2023_paper.pdf), [Efficient LoFTR arXiv 2403.04765](https://arxiv.org/pdf/2403.04765), [RoMa v2 emergent mind](https://www.emergentmind.com/papers/2511.15706), [DINOv2 features](https://www.emergentmind.com/topics/dinov2-features), [Image Matching Challenge 2025 — DINO-RotateMatch arXiv 2512.03715](https://arxiv.org/pdf/2512.03715), [LightGlue ONNX](https://github.com/fabio-sim/LightGlue-ONNX), [LightGlue HF Transformers](https://huggingface.co/docs/transformers/model_doc/lightglue). + +### 2.3. Critère faux-positif 0.95 + +C'est le critère discriminant pour sortir de la drift exemption rustine. **SuperPoint + LightGlue** fournit deux signaux séparables : +1. `n_matches` : nombre de keypoints appariés (typique 50-200 pour un widget visible). +2. **Cohérence géométrique** : on calcule l'homographie via `cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)` sur les matches et on garde le ratio inliers / total. Un faux positif 0.95 sur région différente aura `n_matches < 10` ou un ratio inliers < 0.5. + +Cela élimine la classe de bug « score 0.95, mauvais bouton » sans avoir besoin d'un seuil bas qui ferait passer le faux positif. + +### 2.4. Recommandation + +**Phase 1 (court terme, post-démo)** : conserver `cv2.matchTemplate` mais **ajouter une vérification géométrique LightGlue+SuperPoint en ratification** quand le score est ∈ [0.80, 0.95] (zone aujourd'hui ambiguë). Si LightGlue confirme la cohérence homographique → garder le match. Sinon → fallback VLM. Cela réduit l'exemption drift de 0.95 vers 0.80. + +**Phase 2 (moyen terme)** : remplacer la boucle multi-scale `cv2.matchTemplate` par LightGlue+SuperPoint en méthode primaire d'étage 2. Garder un fallback NCC pour les widgets très uniformes/texturés faiblement (icônes monochromes plates où LightGlue manque de keypoints). + +**Snippet — intégration LightGlue compatible cascade actuelle** + +```python +# Pseudo-code à brancher dans resolve_engine._resolve_by_template +# Ne PAS appliquer en l'état — validation syntaxique seulement. + +from lightglue import LightGlue, SuperPoint +from lightglue.utils import load_image, rbd +import cv2, torch + +_LG_DEVICE = "cuda" if torch.cuda.is_available() else "cpu" +_LG_EXTRACTOR = SuperPoint(max_num_keypoints=2048).eval().to(_LG_DEVICE) +_LG_MATCHER = LightGlue(features="superpoint").eval().to(_LG_DEVICE) + +def _verify_template_match_with_lightglue( + screenshot_bgr, + template_bgr, + candidate_xy, # (cx, cy) pixel renvoyé par cv2.matchTemplate + inlier_ratio_threshold=0.5, + min_matches=10, +): + """Confirme géométriquement un match cv2.matchTemplate. + + Returns: + dict(confirmed=bool, n_matches=int, inlier_ratio=float) + """ + # Crop autour du candidat (taille du template + halo) + th, tw = template_bgr.shape[:2] + cx, cy = candidate_xy + x0 = max(0, cx - tw) + y0 = max(0, cy - th) + x1 = min(screenshot_bgr.shape[1], cx + tw) + y1 = min(screenshot_bgr.shape[0], cy + th) + crop = screenshot_bgr[y0:y1, x0:x1] + + # Tensors LightGlue (1, 1, H, W) float [0,1] + crop_t = torch.from_numpy(cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY)).float()[None, None] / 255.0 + tpl_t = torch.from_numpy(cv2.cvtColor(template_bgr, cv2.COLOR_BGR2GRAY)).float()[None, None] / 255.0 + + with torch.no_grad(): + feats0 = _LG_EXTRACTOR.extract(tpl_t.to(_LG_DEVICE)) + feats1 = _LG_EXTRACTOR.extract(crop_t.to(_LG_DEVICE)) + matches01 = _LG_MATCHER({"image0": feats0, "image1": feats1}) + + feats0, feats1, matches01 = (rbd(x) for x in [feats0, feats1, matches01]) + matches = matches01["matches"] # (M, 2) + + n_matches = matches.shape[0] + if n_matches < min_matches: + return {"confirmed": False, "n_matches": n_matches, "inlier_ratio": 0.0} + + pts0 = feats0["keypoints"][matches[..., 0]].cpu().numpy() + pts1 = feats1["keypoints"][matches[..., 1]].cpu().numpy() + H_, mask = cv2.findHomography(pts0, pts1, cv2.RANSAC, 5.0) + if H_ is None: + return {"confirmed": False, "n_matches": n_matches, "inlier_ratio": 0.0} + inlier_ratio = float(mask.sum()) / n_matches + + return { + "confirmed": inlier_ratio >= inlier_ratio_threshold, + "n_matches": n_matches, + "inlier_ratio": inlier_ratio, + } +``` + +À brancher en **post-process** de `cv2.matchTemplate` : si score ∈ [0.80, 0.95], appel LightGlue. Si confirmé → garder. Cela transforme la rustine drift exemption en *vérification ratifiée*. + +--- + +## 3. Sous-axe 3 — pHash → alternatives 2026 + +### 3.1. Usages actuels et limites + +| Usage | Implémentation actuelle | Limite documentée | +|---|---|---| +| **LoopDetector QW2** | pHash global (`screen_static` ≥ threshold) + `action_repeat` + `retry_threshold` | un caret blinking ou un curseur sur barre de chargement fait varier le hash → faux négatif (« écran a bougé » alors qu'il n'a rien changé fonctionnellement) | +| **VERIFY post-action** | pHash global avant/après click | un clic local sur un onglet change ~5 % de l'image (la zone des tabs + le contenu de l'onglet) — peut être absorbé par le hash global → faux négatif (le click n'a rien fait visible). Inversement, popup arrière-plan / curseur souris fait croire à un changement. | + +Diagnostic principal : `feedback_phash_vs_dialog_in_vm.md` (memory) — pHash global est trop grossier pour la cascade VM. DETTE-001 (BUG_PRECHECK_SPATIAL_BLINDNESS) montre que c'est **spatialement aveugle** : `_text_match_fuzzy` valide le pré-check OCR au mauvais endroit parce que le radius 280 px englobe plusieurs tabs. + +### 3.2. Table comparative — alternatives 2026 + +| Méthode | Mode | Latence (crop 400×200) | Mode ROI ? | Robustesse caret/curseur | Distingue mouvement local | Bibliothèque | +|---|---|---|---|---|---|---| +| **pHash global 64-bit** | actuel | <5 ms | non | mauvaise | non | `imagehash` | +| **pHash par ROI (rolling)** | extension simple | ~5 ms × N régions | oui (par tuiles) | OK | oui | `imagehash` | +| **SSIM** (skimage) | classique | 5-10 ms CPU | **oui native** | bonne | oui | `skimage.metrics.structural_similarity` | +| **MS-SSIM** | multi-échelle | 15-30 ms | oui | meilleure | oui | `pytorch-msssim` | +| **LPIPS** (AlexNet/VGG) | deep | 30-80 ms | oui via crop | excellente (sémantique) | oui | `lpips` | +| **DINOv2 patch features cos-sim** | deep semantic | 100-200 ms (ViT-S/14) | oui (patches) | excellente | oui | `transformers` + `dinov2_vits14` | +| **CLIP image embedding cos-sim** | global semantic | ~30 ms | non (perd info spatiale) | bonne mais pas local | non | `open_clip` | + +**Sources :** [Eureka — SSIM vs LPIPS](https://eureka.patsnap.com/article/ssim-vs-lpips-which-metric-should-you-trust-for-image-quality-evaluation), [SSIM scikit-image doc](https://scikit-image.org/docs/dev/auto_examples/transform/plot_ssim.html), [Wopee — Screenshot Comparison Algorithms](https://wopee.io/blog/screenshot-comparison-algorithms-visual-testing/), [Medium CLIP vs DINOv2 image similarity](https://medium.com/aimonks/clip-vs-dinov2-in-image-similarity-6fa5aa7ed8c6), [DinoHash arXiv 2503.11195](https://arxiv.org/pdf/2503.11195). + +### 3.3. Recommandation par usage + +**LoopDetector QW2 (écran statique → boucle)** +- **Adopter** : DINOv2 features cos-sim sur frame entière (downscale 224×224 avant). Seuil cos < 0.99 = changement réel. Robuste au caret blinking, au scroll-bar position, à la souris. +- **Coût** : ~100 ms par frame sur RTX 5070. Acceptable pour un trigger appelé 1×/sec. +- **Alternative dégradée** : pHash par ROI (grille 4×4 tuiles), ré-utilise `imagehash` actuel, sans GPU. + +**VERIFY post-action (a-t-on cliqué utilement ?)** +- **Adopter SSIM par ROI** : + - ROI = bbox du target résolu + halo 50 px (ou la zone qu'on s'attend à voir changer si elle est connue : par exemple, le contenu d'onglet pour un click sur onglet). + - `structural_similarity(roi_before, roi_after, multichannel=True)`. + - Seuil empirique à calibrer (0.85 = changement notable, 0.95 = rien n'a changé). +- **Coût** : ~5 ms CPU sur crop 400×200, négligeable. +- **Bénéfice transversal** : résout aussi DETTE-001 — au lieu de vérifier que `target_text` est présent dans un crop OCR autour du click, on vérifie que la **zone** elle-même a changé (= un click vraiment effectif déclenche un repaint local). + +**Snippet — SSIM ROI VERIFY (drop-in dans `replay_verifier.py`)** + +```python +from skimage.metrics import structural_similarity as ssim +import cv2, numpy as np + +def verify_click_changed_roi( + screenshot_before_path: str, + screenshot_after_path: str, + cx_px: int, + cy_px: int, + roi_w: int = 400, + roi_h: int = 200, + threshold: float = 0.95, +) -> dict: + """Vérifie qu'un click a effectivement modifié la ROI cible. + + Returns: + dict(changed=bool, ssim=float, roi_bbox=(x0,y0,x1,y1)) + """ + before = cv2.imread(screenshot_before_path) + after = cv2.imread(screenshot_after_path) + if before is None or after is None or before.shape != after.shape: + return {"changed": False, "ssim": 0.0, "roi_bbox": (0, 0, 0, 0)} + + H, W = before.shape[:2] + x0 = max(0, cx_px - roi_w // 2) + y0 = max(0, cy_px - roi_h // 2) + x1 = min(W, cx_px + roi_w // 2) + y1 = min(H, cy_px + roi_h // 2) + + crop_b = cv2.cvtColor(before[y0:y1, x0:x1], cv2.COLOR_BGR2GRAY) + crop_a = cv2.cvtColor(after[y0:y1, x0:x1], cv2.COLOR_BGR2GRAY) + + score = float(ssim(crop_b, crop_a, data_range=255)) + return { + "changed": score < threshold, + "ssim": score, + "roi_bbox": (x0, y0, x1, y1), + } +``` + +--- + +## 4. Patch ciblé — bug center-of-line de `_resolve_by_ocr_text` + +### 4.1. Cible exacte + +Fichier : `agent_v0/server_v1/resolve_engine.py`, fonction `_resolve_by_ocr_text`, lignes **1486-1519** (référence dans `REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` §1.2). + +Bloc actuel reconstitué d'après §1.2 du diagnostic 8 mai : + +```python +# resolve_engine.py:1486-1519 (état au 8 mai 2026) +# Match exact > contient > mot par mot +score = 0.0 +if target_lower == line_lower: + score = 1.0 +elif target_lower in line_lower: + score = 0.8 +elif any(target_lower == w.value.lower() for w in line_obj.words): + score = 0.9 + +if score > best_score: + box = line_obj.geometry # ⚠ bbox de la LIGNE ENTIÈRE + cx = (box[0][0] + box[1][0]) / 2 + cy = (box[0][1] + box[1][1]) / 2 + best_score = score + best_match = {"cx": cx, "cy": cy, "score": score, "line": line_obj.value} +``` + +### 4.2. Patch proposé — center-of-span depuis `line.words` + +**Principe** : pour les scores 0.8 (substring) et 0.9 (mot exact), recalculer `cx, cy` à partir des bbox des `words` qui couvrent le `target_text`, **pas** de la ligne entière. + +Code copy-paste-ready (validation syntaxique seulement, **non exécuté**) : + +```python +# resolve_engine.py:1486-1519 (proposition) +# Match exact > contient > mot par mot +score = 0.0 +matched_words = [] # sous-ensemble de line_obj.words couvrant target_text + +target_lower = target_text.lower().strip() +line_lower = line_obj.value.lower().strip() + +# 1) Match exact ligne entière +if target_lower == line_lower: + score = 1.0 + matched_words = list(line_obj.words) + +# 2) Match substring (multi-mots possibles) +elif target_lower in line_lower: + score = 0.8 + # Reconstruire le span de words couvrant target_lower par concat séquentielle + target_tokens = target_lower.split() + line_words_lower = [w.value.lower() for w in line_obj.words] + # Recherche d'une fenêtre contiguë qui matche tous les target_tokens dans l'ordre + for start in range(len(line_words_lower) - len(target_tokens) + 1): + window = line_words_lower[start:start + len(target_tokens)] + # Comparaison tolérante : un token cible peut être préfixe/égal au token line + if all(t == w or t in w or w in t for t, w in zip(target_tokens, window)): + matched_words = line_obj.words[start:start + len(target_tokens)] + break + if not matched_words: + # Fallback : tous les words contenant un token cible + matched_words = [w for w in line_obj.words if any(t in w.value.lower() for t in target_tokens)] + +# 3) Match mot-exact dans la ligne (single token) +elif any(target_lower == w.value.lower() for w in line_obj.words): + score = 0.9 + matched_words = [w for w in line_obj.words if w.value.lower() == target_lower] + +if score > best_score: + if matched_words: + # ✅ Centre du SPAN matché, pas de la ligne entière + xs = [] + ys = [] + for w in matched_words: + (xmin, ymin), (xmax, ymax) = w.geometry + xs.extend([xmin, xmax]) + ys.extend([ymin, ymax]) + cx = (min(xs) + max(xs)) / 2 + cy = (min(ys) + max(ys)) / 2 + else: + # Fallback de sécurité : centre de la ligne (comportement actuel) + box = line_obj.geometry + cx = (box[0][0] + box[1][0]) / 2 + cy = (box[0][1] + box[1][1]) / 2 + + best_score = score + best_match = { + "cx": cx, + "cy": cy, + "score": score, + "line": line_obj.value, + "matched_span": " ".join(w.value for w in matched_words) if matched_words else None, + } +``` + +### 4.3. Justification, risques, tests à faire avant merge + +**Pourquoi ça résout le bug** : pour `target='Imagerie'` dans la ligne `"Motif d'admission Examens cliniques Imagerie Notes médicales Synthèse Urgences Codage >"`, `matched_words` capturera uniquement le `Word` `"Imagerie"` (geometry locale), pas tous les words de la ligne. `cx, cy` retomberont au centre exact de ce mot. Idem pour `'Notes médicales'` (2 words contigus) et `'Synthèse Urgences'` (2 words contigus). Plus de collision (0.23, 0.28). + +**Repère identique** : `Word.geometry` est dans le **même repère normalisé** que `line_obj.geometry` (vérifié par doc docTR — cf. [Discussion #570](https://github.com/mindee/doctr/discussions/570) et [io modules](https://mindee.github.io/doctr/modules/io.html)). Aucune conversion d'échelle requise. + +**Risques résiduels** : +1. **Casse/accents** : `target_lower in line_lower` puis comparaison `t == w or t in w or w in t` — il faut **normaliser les accents** (NFD + strip diacritics) si `target='Notes médicales'` vs `Word='médicales'` matche, mais `target='Notes medicales'` (sans accent venant du JSON workflow) peut rater. Mitigation : `unicodedata.normalize('NFKD', s).encode('ascii','ignore').decode()` sur les deux côtés avant la comparaison. +2. **Tokenisation docTR ≠ split blancs** : docTR sépare typiquement par espace mais peut séparer/grouper différemment des hyphens/apostrophes. Le fallback `matched_words = [w for w in line_obj.words if any(t in w.value.lower() for t in target_tokens)]` couvre ce cas mais peut sur-matcher. +3. **Performance** : O(n_words × n_target_tokens) — négligeable (n_words < 50 typiquement). +4. **Régressions cosmétiques** : `pre_check_text_match` (DETTE-001) actuellement OFF — à re-tester avec ce fix actif. + +**Tests minimaux avant merge (10 min)** : +```bash +cd /home/dom/ai/rpa_vision_v3 && source .venv/bin/activate +python -c " +from agent_v0.server_v1.resolve_engine import _resolve_by_ocr_text +img='/home/dom/ai/rpa_vision_v3/visual_workflow_builder/backend/data/anchors/anchor_0438bd2d9bdd_1778161174_full.png' +for t in ['Imagerie','Notes médicales','Synthèse Urgences','Codage','Examens cliniques']: + r = _resolve_by_ocr_text(img, t, 2560, 1600) + print(f'{t:25s} -> cx={r[\"x_pct\"]:.4f} cy={r[\"y_pct\"]:.4f} score={r[\"score\"]:.2f}') +" +``` + +Critère succès : `Imagerie / Notes médicales / Synthèse Urgences` ont des `cx` séparés d'au moins 0.05 (≈ 130 px à 2560 px). + +**À NE PAS faire en chaud démo** (cf. §5 du diagnostic 8 mai). Le quick fix démo reste le timeout client `5 → 30 s`. Ce patch s'applique sur runner 2 (post-démo). + +--- + +## 5. Dépendances croisées avec les autres axes + +- **AXE_A5 (tokenisation UI / OmniParser)** : OmniParser utilise PaddleOCR pour l'OCR d'icônes. Si on bascule vers tokenisation OmniParser-style en cascade `1.5` (entre OCR et VLM), il faudra décider **un seul moteur OCR pour tout le pipeline** ou accepter 2 moteurs (docTR pour resolve_engine, PaddleOCR/RapidOCR pour tokenisation). Voir AXE_A5 livrable. +- **AXE_B2 (Validator)** : SSIM-ROI proposé §3 alimente directement le composant Validator du Planner-Actor-Validator (cf. SYNTHESE §5.2). C'est le signal sémantique « le click a fait quelque chose dans la zone attendue » qui élimine la classe de bugs « cliqué quelque part, REPORT success=True ». +- **DETTE-001** : le patch §4 + SSIM-ROI §3 referment la dette (le pré-check OCR cesse d'être spatialement aveugle parce qu'il vise un span exact, et la vérification post-click se fait sur ROI ciblée). +- **Drift exemption ≥ 0.95** : la ratification LightGlue (§2.4) permet de baisser le seuil vers 0.80 sans réintroduire de faux positifs. + +--- + +## 6. Sources (chronologie) + +- [docTR — Word/Line geometry — Discussion #570 (2022, valide en 2026)](https://github.com/mindee/doctr/discussions/570) +- [docTR — I/O modules (doc officielle)](https://mindee.github.io/doctr/modules/io.html) +- [docTR — repo principal (release v0.10, 2026-04)](https://github.com/mindee/doctr) +- [docTR — PyPI python-doctr](https://pypi.org/project/python-doctr/) +- [PaddleOCR — return_word_box Issue #15760 (2024)](https://github.com/PaddlePaddle/PaddleOCR/issues/15760) +- [PaddleOCR 3.0 Technical Report (2025-07)](https://arxiv.org/pdf/2507.05595) +- [Surya OCR — datalab-to/surya](https://github.com/datalab-to/surya) +- [Surya OCR — PyPI v0.17.1](https://pypi.org/project/surya-ocr/) +- [EasyOCR — Character bbox Issue #631](https://github.com/JaidedAI/EasyOCR/issues/631) +- [RapidOCR — releases (v3.x, 2026-04-11)](https://github.com/RapidAI/RapidOCR/releases) +- [RapidOCR — repo](https://github.com/RapidAI/RapidOCR) +- [pytesseract — image_to_data + hOCR (PyPI)](https://pypi.org/project/pytesseract/) +- [Codesota — PaddleOCR vs EasyOCR Speed 2025](https://www.codesota.com/ocr/paddleocr-vs-easyocr) +- [Codesota — PaddleOCR vs Tesseract vs EasyOCR 2026](https://www.codesota.com/ocr/paddleocr-vs-tesseract) +- [Buttondown — PaddleOCR vs EasyOCR vs Doctr Memory & Latency](https://buttondown.com/ckae930413/archive/paddleocr-vs-easyocr-vs-doctr-memory-latency-test/) +- [LightGlue — ICCV 2023 paper](https://openaccess.thecvf.com/content/ICCV2023/papers/Lindenberger_LightGlue_Local_Feature_Matching_at_Light_Speed_ICCV_2023_paper.pdf) +- [LightGlue — repo cvg/LightGlue](https://github.com/cvg/LightGlue) +- [LightGlue ONNX — fabio-sim/LightGlue-ONNX](https://github.com/fabio-sim/LightGlue-ONNX) +- [LightGlue — HuggingFace Transformers integration](https://huggingface.co/docs/transformers/model_doc/lightglue) +- [Efficient LoFTR — arXiv 2403.04765 (CVPR 2024)](https://arxiv.org/pdf/2403.04765) +- [RoMa — CVPR 2024](https://openaccess.thecvf.com/content/CVPR2024/html/Edstedt_RoMa_Robust_Dense_Feature_Matching_CVPR_2024_paper.html) +- [RoMa v2 — emergent mind 2025-11](https://www.emergentmind.com/papers/2511.15706) +- [DINOv2 — features tutorial](https://www.lightly.ai/blog/dinov2) +- [DINO-RotateMatch — arXiv 2512.03715 (2025)](https://arxiv.org/pdf/2512.03715) +- [PyImageSearch — Multi-scale Template Matching (2015, ref classique)](https://pyimagesearch.com/2015/01/26/multi-scale-template-matching-using-python-opencv/) +- [Medium — Template Matching Beyond Basics: Rotation & Scale Invariant](https://medium.com/@coders.stop/template-matching-beyond-basics-rotation-and-scale-invariant-detection-2ae78d8fa190) +- [Eureka — SSIM vs LPIPS](https://eureka.patsnap.com/article/ssim-vs-lpips-which-metric-should-you-trust-for-image-quality-evaluation) +- [skimage — structural_similarity doc](https://scikit-image.org/docs/dev/auto_examples/transform/plot_ssim.html) +- [Wopee — Screenshot Comparison Algorithms](https://wopee.io/blog/screenshot-comparison-algorithms-visual-testing/) +- [Medium — CLIP vs DINOv2 image similarity](https://medium.com/aimonks/clip-vs-dinov2-in-image-similarity-6fa5aa7ed8c6) +- [DinoHash — arXiv 2503.11195](https://arxiv.org/pdf/2503.11195) +- [OmniParser — DeepWiki OCR module](https://deepwiki.com/microsoft/OmniParser/2.2-ocr-and-image-processing) + +--- + +*Document de recherche. Aucun code modifié. Toute application validée par Dom au cas par cas.* diff --git a/docs/recherche/AXE_A5_SCREEN_TOKENIZATION.md b/docs/recherche/AXE_A5_SCREEN_TOKENIZATION.md new file mode 100644 index 000000000..f929e71d6 --- /dev/null +++ b/docs/recherche/AXE_A5_SCREEN_TOKENIZATION.md @@ -0,0 +1,318 @@ +# AXE A5 — Tokenisation d'écran (OmniParser, Set-of-Marks, modèles natifs) + +**Date :** 2026-05-23 +**Auteur :** Claude (agent dispatché par session principale) +**Statut :** lecture seule, recherche pour arbitrage post-démo. Pas d'action proposée. +**Axes liés :** A1 (modèles VLM grounding), A4 (OCR/docTR/RapidOCR), CLAUDE.md §asymétrie record/replay. + +--- + +## 1. TL;DR + +**Recommandation : Scénario B (log implicite, no risk) maintenant, Scénario A (OmniParser V2 dans la cascade replay) à instruire post-démo, Scénario C (OS-Atlas / GUI-Actor) à benchmarker en R&D mais hors trajectoire courte.** + +**Top finding** : `core/detection/som_engine.py` est **déjà câblé** sur les weights OmniParser (`/home/dom/ai/OmniParser/weights/icon_detect/model.pt`, YOLOv8) côté serveur. Utilisé en **recording** (`stream_processor.py:607-638`) ET déclaré dans une voie `_resolve_set_of_marks` du `resolve_engine.py:1083-1325`. L'asymétrie pointée par CLAUDE.md n'est donc pas "UI-DETR-1 vs rien", elle est "SomEngine activé selon les paths". À vérifier au runtime si `_resolve_set_of_marks` est effectivement appelé en replay — possible code orphelin (cf. champs de mines CLAUDE.md). + +**Trois faits 2025 qui changent l'arbitrage :** +1. **OmniParser V2** (publié 12 février 2025, MIT pour le code + Florence-2, **AGPL-3.0 pour les poids YOLOv8 icon_detect**) — 0.6 s/frame A100, 0.8 s/frame RTX 4090, état de l'art ScreenSpot-Pro 39.6 % combiné GPT-4o. Notre RTX 5070 ≈ 12 GB VRAM tient le pipeline complet (icon_detect ~150 MB + Florence-2 base ~750 MB). +2. **Set-of-Mark** (Yang et al., arXiv 2310.11441, NeurIPS 2023) — pattern qui sous-tend OmniParser, Magma, ShowUI. Devenu vocabulaire standard du domaine en 2025. +3. **Coordinate-free grounding émerge** : GUI-Actor (Microsoft, NeurIPS 2025) et Aguvis (ICML 2025) bypassent le besoin d'un étage de tokenisation séparé. GUI-Actor-7B sur Qwen2.5-VL = 44.6 % ScreenSpot-Pro, > UI-TARS-72B. **Tendance** : la tokenisation explicite OmniParser-style devient un raccourci d'aujourd'hui que les VLM "GUI-natifs" intégreront demain. + +**Verdict honnête pour rpa_vision_v3** : la cause des bugs récents (cf. `REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md`) est **transport HTTP + OCR-DIRECT center-of-line**, pas la cascade vision. Ajouter OmniParser V2 en replay ne réparera **rien** des bugs P0 ouverts. Mais c'est la brique manquante pour le **Validator** (Skyvern-style) et pour un **log de candidats parsés** côté replay (suggestion §4.1 de `INSPIRATION_FRAMEWORKS`). + +--- + +## 2. OmniParser V2 — fiche détaillée + +### 2.1. Identité + +- **Auteur** : Microsoft Research +- **Repo** : https://github.com/microsoft/OmniParser +- **Modèle HF** : https://huggingface.co/microsoft/OmniParser-v2.0 +- **Release V2** : 12 février 2025 +- **Stars GitHub mai 2026** : ~22k (cité dans `INSPIRATION_FRAMEWORKS_2026-05-10.md`) +- **Article officiel** : https://www.microsoft.com/en-us/research/articles/omniparser-v2-turning-any-llm-into-a-computer-use-agent/ + +### 2.2. Architecture interne (2 modèles) + +| Sous-modèle | Rôle | Taille | Backend | Licence | +|---|---|---|---|---| +| **icon_detect** | YOLOv8 fine-tuné sur dataset Microsoft d'éléments interactifs (boutons, icônes, champs) | ~150 MB | Ultralytics `YOLO` | **AGPL-3.0** ⚠ | +| **icon_caption** | Florence-2 base fine-tuné sur 7K paires icon-description annotées GPT-4o | ~750 MB | Transformers | **MIT** | + +**OCR** : OmniParser embarque aussi un OCR (PaddleOCR ou EasyOCR selon config). Notre `som_engine.py` actuel **a remplacé** par docTR (cf. lignes 117-127). + +### 2.3. Format de sortie ("interactable elements") + +Liste de dicts avec : +- `bbox` (coordonnées normalisées 0-1) +- `interactivity` (true / false) +- `content` (caption Florence-2 : "close button", "search field", "OK") +- `source` ("box_yolo_content_ocr" / "box_yolo_content_yolo" / "icon") + +Cette sortie est conçue pour être insérée **dans le prompt du VLM principal** (GPT-4o, Claude) sous forme de tableau numéroté → c'est du Set-of-Marks appliqué automatiquement. + +### 2.4. Performance benchmarks + +| Métrique | Valeur | Source | +|---|---|---| +| ScreenSpot-Pro + GPT-4o | **39.6 %** | Microsoft Research article 2026-02-12 | +| ScreenSpot-Pro GPT-4o seul (sans OmniParser) | 0.8 % | idem | +| Latence A100 (1 frame) | **0.6 s** | idem | +| Latence RTX 4090 (1 frame) | **0.8 s** | idem | +| VRAM mini | 4 GB (inference) / 8 GB (recommandé loop agent) | GitHub Issue #31 | +| Résolution supportée | 640 → **1920 px** côté long (icon_detect) | doc HF V2.0 | + +**Implication directe rpa_vision_v3** : nos screenshots Léa sont **2560×1600** (RTX 5070). Il faudra **resize** à 1920 px côté long → ratio ~0.75. Toutes les coordonnées YOLO de retour doivent être re-scalées. **Risque** : c'est le même piège que `smart_resize` Qwen2.5-VL bbox_2d (DETTE-006/010/014, `MIGRATION_VLM_PLAN_2026-05-09.md` §2). À traiter explicitement. + +### 2.5. Licence — point sensible commercial + +Combinaison **AGPL-3.0** (icon_detect) + **MIT** (icon_caption + code) : +- **AGPL-3.0** sur icon_detect = **interdit en produit commercial fermé** sans rachat de licence YOLOv8 commercial (Ultralytics, ~5 000 $/an estimé), ou remplacement par un détecteur alternatif (Florence-2 task ``, ou détecteur custom). +- Pour rpa_vision_v3 en POC interne / GHT / Anoust : usage actuel défendable. **Pour vente externe : showstopper sur icon_detect tel quel**. + +Le code de notre `som_engine.py` charge directement `/home/dom/ai/OmniParser/weights/icon_detect/model.pt` (AGPL-3.0) → audit licence à reprendre. + +### 2.6. État interne du projet — code existant + +- `core/detection/som_engine.py` (316 lignes, complet) — singleton thread-safe, YOLO + docTR + annotation. Device par défaut `cpu` (cf. `get_shared_engine`). +- `core/detection/omniparser_adapter.py` — wrapper plus complet citant Florence-2 caption. **Sépare** du `som_engine.py` simplifié. +- `agent_v0/server_v1/stream_processor.py:607-700` — appel SoM au **recording** pour enrichir chaque event click avec l'élément cliqué (cf. UI-DETR-1 dans `INSPIRATION_FRAMEWORKS_2026-05-10.md` §4 — c'est en fait SoM, pas un détecteur tiers). +- `agent_v0/server_v1/resolve_engine.py:1083-1325` — voie `_resolve_set_of_marks` au **replay**. Lance SomEngine, fallback template matching anchor↔éléments YOLO. **À tracer au runtime** pour confirmer qu'elle est invoquée. + +→ **L'asymétrie record/replay décrite dans CLAUDE.md n'est peut-être pas l'asymétrie réelle.** À valider explicitement avec Dom au prochain run. + +--- + +## 3. Set-of-Marks original + +### 3.1. Identité + +- **Papier** : Yang et al., "Set-of-Mark Prompting Unleashes Extraordinary Visual Grounding in GPT-4V", arXiv 2310.11441 (oct 2023, dernière révision nov 2023) +- **Code** : https://github.com/microsoft/SoM +- **Lien** : https://arxiv.org/abs/2310.11441 +- 124 citations (mai 2026) + +### 3.2. Approche + +1. Segmenter l'image en régions sémantiques (SEEM ou SAM, off-the-shelf) +2. Overlayer chaque région avec un **mark** (numéro, lettre, masque coloré, bbox) +3. Demander au VLM : "click on mark 7" plutôt que "click at (x=420, y=380)" + +Bénéfice : le VLM raisonne en **identifiants discrets** et non en coordonnées continues. Évite le bug des coords mal calibrées (cf. notre DETTE-006 bbox_2d Qwen2.5-VL). + +### 3.3. Résultats clés (papier 2023) + +GPT-4V + SoM en zero-shot **bat le SOTA fine-tuned** sur RefCOCOg (referring expression comprehension). Pour la première fois, un modèle généraliste prompted dépasse les modèles spécialisés sur ce benchmark. + +### 3.4. Complémentarité avec OmniParser + +- **SoM = méthode de prompting** (overlay sur l'image, le VLM répond un ID) +- **OmniParser = pipeline complet** (détection + caption + format prompt) +- OmniParser = "SoM industrialisé pour les UIs". OmniParser fournit les régions ET les marks ET les captions, contre SoM original qui fournit seulement les marks à partir d'une segmentation SEEM/SAM générique. +- Magma (Microsoft, CVPR 2025) entraîne explicitement le modèle sur des images SoM-labellisées → SoM passe de "trick de prompting" à "supervision de pré-entraînement". + +--- + +## 4. Modèles VLM qui tokenisent nativement + +### 4.1. OS-Atlas-Base (ICLR 2025) + +- **Repo** : https://github.com/OS-Copilot/OS-Atlas +- **Modèles** : OS-Atlas-Base-4B (sur InternVL2-4B) et OS-Atlas-Base-7B (sur Qwen2-VL-7B-Instruct) +- **Approche** : entraînement sur **13 M éléments GUI** cross-platform (Windows, Linux, macOS, Android, web). Tokens spéciaux `<|box_start|>(x1,y1),(x2,y2)<|box_end|>`. Coordonnées normalisées 0-1000. +- **Tokenise nativement ?** Pas au sens "produit une liste d'éléments en sortie", mais **internalise la grammaire bbox** comme tokens spéciaux. Le modèle apprend que "(124, 380) → (228, 412)" est une bbox, pas une séquence d'entiers libres. +- **Coût** : OS-Atlas-Base-7B = ~14 GB FP16, ~7 GB en 4-bit. Compatible RTX 5070 12 GB en 4-bit ou avec Flash-Attn. + +### 4.2. ShowUI-2B (CVPR 2025, Outstanding Paper NeurIPS 2024 workshop) + +- **Repo** : https://github.com/showlab/ShowUI +- **HF** : https://huggingface.co/showlab/ShowUI-2B +- **Innovation clé** : **UI-Guided Visual Token Selection** — construit un graphe UI au sein du modèle, élimine 33 % des tokens visuels redondants. **5× plus rapide, 2× plus précis** que les VLM généralistes en localisation. +- **Perf** : **75.1 % accuracy zero-shot ScreenSpot grounding** avec seulement 2B params + 256K échantillons de training. +- **Tokenise nativement ?** Oui, c'est sa nature : la sélection des tokens visuels EST une forme de tokenisation. Pas de pipeline externe. +- **Coût** : 2B params → ~4 GB FP16, ~2 GB en 4-bit. **Léger pour notre RTX 5070**. + +### 4.3. Aguvis (ICML 2025) + +- **Repo** : https://aguvis-project.github.io/ +- **Approche** : framework "pure vision" multi-plateforme avec **inner monologue** structuré. Pipeline 2 étapes (grounding séparé de planification). +- **Caractéristique** : premier agent GUI pure-vision entièrement **open-source** (sans dépendance closed-source). + +### 4.4. GUI-Actor (Microsoft, NeurIPS 2025) — **le plus pertinent** + +- **Repo** : https://github.com/microsoft/GUI-Actor +- **HF** : https://huggingface.co/microsoft/GUI-Actor-7B-Qwen2.5-VL +- **Innovation** : **coordinate-free**. Au lieu de produire "(420, 380)", produit un token `` qui s'attache via attention aux patches visuels pertinents. Génère plusieurs régions candidates en 1 forward. +- **Perf** : GUI-Actor-7B sur Qwen2.5-VL = **44.6 % ScreenSpot-Pro** (> UI-TARS-72B). +- **Implication directe** : **tue le bug d'échelle bbox_2d** (DETTE-006/010/014) à la racine. Pas de smart_resize à débugger. Pas de mismatch de résolution. +- **Coût** : 7B + Qwen2.5-VL → ~14 GB FP16, ~7 GB 4-bit. Compatible RTX 5070 en 4-bit. + +### 4.5. Magma (Microsoft, CVPR 2025) + +- **Repo** : https://github.com/microsoft/Magma +- **Approche** : pré-entraînement multimodal avec **Set-of-Mark labellisation automatique** des éléments cliquables + **Trace-of-Mark** pour la planification d'actions (videos). Premier modèle multi-domaine (GUI + robotique). +- **Tokenise nativement ?** Oui, SoM est dans la supervision de pré-entraînement, pas une étape inférence externe. + +--- + +## 5. Comparaison latence / perf / robustesse + +### 5.1. Tableau synthèse + +| Approche | Latence / écran 2560×1600 | VRAM | Sortie | Licence | Tokenise nativement ? | +|---|---|---|---|---|---| +| **UI-DETR-1 record-only (actuel)** | n/a (recording offline) | n/a | événements VWB enrichis | propriétaire MS | non, étape séparée | +| **SomEngine actuel** (YOLO icon_detect + docTR) | ~150-300 ms estimé (docTR seul ~100 ms, YOLO ~15 ms, +overhead annotation) | ~1 GB | liste `SomElement` | AGPL-3.0 (YOLO) | non | +| **OmniParser V2 complet** (YOLO + Florence-2 caption) | **0.8 s** (RTX 4090) → ~1.0 s (RTX 5070 estimé) après resize 1920 | ~2 GB | liste `interactable_element` + caption sémantique | AGPL-3.0 + MIT | non | +| **YOLOv8-UI fine-tuned custom** (sans caption) | ~50-100 ms | ~200 MB | bbox seules | AGPL-3.0 par défaut, MIT possible si from scratch | non | +| **ShowUI-2B** | grounding direct, ~1-2 s estimé | ~2-4 GB | coord ou région | code MIT | **oui** (UI-guided token selection) | +| **GUI-Actor-7B** | grounding direct, ~2-3 s estimé | ~7-14 GB | régions multiples via attention | code MIT | **oui** (attention head dédiée) | +| **OS-Atlas-Base-7B** | grounding direct, ~2-3 s estimé | ~7-14 GB | bbox tokens spéciaux | code MIT (modèle Qwen2-VL Apache 2.0) | partiellement | +| **InfiGUI-G1-3B (actuel)** | déjà mesuré ~1-2 s | 3.9 GB | bbox | Apache 2.0 (Qwen) | partiellement | + +### 5.2. Robustesse — ce que dit la littérature 2025 + +- **GUI-Robust** (arXiv 2506.14477) : tous les MLLM (y compris GUI-spécialisés) se dégradent significativement sur 7 types d'anomalies (popups, modifications de layout, désactivation). **OmniParser ne corrige PAS ces anomalies à lui seul** — il aide à voir les éléments, pas à comprendre les bugs UI. +- **Magma + SoM** (CVPR 2025) : SoM **en supervision** > SoM **en prompting inference-only**. Confirme que la prochaine génération est "SoM natif", pas "OmniParser externe". + +### 5.3. Comparaison concrète avec notre cascade actuelle + +Notre cascade `_resolve_target` (cf. `cartography_execution_flow.md`) : + +``` +OCR docTR (~200 ms) → template cv2 (~50 ms) → YOLO/SoM optionnel → VLM (1-15 s) +``` + +Avec SomEngine déjà existant et OmniParser V2 disponible localement (`/home/dom/ai/OmniParser/`), le coût marginal d'ajouter le caption Florence-2 = ~500-700 ms (Florence-2 base). Total cascade enrichie : **~2 s** vs actuel ~1-15 s suivant l'étage qui réussit. **Latence pas un blocker**. + +--- + +## 6. Recommandation pour rpa_vision_v3 — 3 scénarios + +### Scénario A — Intégrer OmniParser V2 complet dans la cascade replay + +**Description** : activer `_resolve_set_of_marks` en replay, brancher icon_caption Florence-2 pour enrichir les éléments YOLO avec des descriptions sémantiques, exposer cette "vue parsée" à VWB UI et au VLM principal en prompt. + +**Pour** : +- Asymétrie record/replay corrigée explicitement. +- Vue parsée disponible pour debug (UI VWB pourrait overlayer les éléments détectés sur captures Léa). +- Caption Florence-2 = donnée d'entrée pour un Validator sémantique post-clic (compare "j'attendais Notes médicales" vs "j'ai cliqué sur élément captioned 'Imagerie tab'"). +- Coût latence ~1 s — compatible démo. + +**Contre** : +- **Licence AGPL-3.0** sur icon_detect → audit légal avant déploiement commercial Anoust / GHT vente. +- N'efface AUCUN des 5 bugs P0 post-démo (transport HTTP, Stop VWB, mss monitors, échelle pixel, skip ord 13). +- Ajoute une dépendance lourde (`/home/dom/ai/OmniParser/`) au runtime. +- Le bug primaire diagnostiqué 8 mai est OCR-DIRECT center-of-line, pas un manque de candidats détectés. + +**Effort** : 1-2 j (le code SomEngine existe, manque le câblage Florence-2 caption + UI debug). + +**Risque** : moyen-faible. Tout est déjà en place côté code. + +### Scénario B — Log implicite des candidats (no risk, à faire dès stabilisation post-démo) + +**Description** : à chaque appel `_resolve_target`, logger systématiquement dans `logs/audit/` la liste des candidats produits par CHAQUE étage de la cascade (docTR lignes/spans, template matches > seuil, sorties YOLO si SomEngine actif, sorties VLM). JSON structuré, 1 ligne par résolution. + +**Pour** : +- **Zéro nouveau modèle, zéro nouvelle dépendance**. Pure instrumentation. +- Vue parsée gratuite, exploitable hors-bande (analyse, dashboard). +- Comble l'asymétrie record/replay au sens "trace équivalente". +- Préalable indispensable à tout Validator sémantique futur. +- Aligné avec la suggestion §4.1 de `INSPIRATION_FRAMEWORKS_2026-05-10.md` ("logger systématiquement la liste des candidats détectés par chaque étage de la cascade"). + +**Contre** : +- N'apporte aucune nouvelle robustesse en propre — juste de l'observabilité. + +**Effort** : 0.5 j. + +**Risque** : très bas. + +**→ À faire en priorité, indépendamment des autres scénarios.** + +### Scénario C — Remplacer la cascade par un modèle VLM qui tokenise nativement + +**Description** : remplacer `_resolve_target` par un appel direct à GUI-Actor-7B (ou ShowUI-2B en option légère) qui produit la zone-cible en coordinate-free. Peut coexister avec OCR docTR pour validation post-action. + +**Pour** : +- **Tue le bug d'échelle bbox_2d** (DETTE-006/010/014) — plus de smart_resize à débugger. +- État de l'art ScreenSpot-Pro (44.6 % GUI-Actor > UI-TARS-72B). +- Architecture plus simple long-terme : 1 modèle = 1 grounder. +- ShowUI-2B = 2× moins gourmand que notre InfiGUI-G1-3B actuel. + +**Contre** : +- **Décision structurelle**, pas réversible facilement. Réécrit la moitié de `resolve_engine.py`. +- Aucun bench interne pour valider sur nos écrans Easily / Citrix réels. +- Pas de caption sémantique = pas de Validator sémantique post-action. +- Effort production-grade : 1-2 semaines. + +**Effort** : 5-10 j. + +**Risque** : élevé tant que pas benché sur nos workflows. + +--- + +## 7. Recommandation finale + +**Maintenant (post-démo, semaine du 26 mai)** : +1. **Scénario B obligatoirement** — log des candidats. Coût minimal, valeur immense pour debug futur. +2. **Vérifier au runtime si `_resolve_set_of_marks` est appelé en replay**. Si non = code orphelin (CLAUDE.md §champs de mines). Si oui = on est déjà mi-Scénario A sans le savoir. +3. **Audit licence** AGPL-3.0 sur icon_detect avant tout pitch commercial. + +**Sprint suivant (juin si bande passante)** : +4. **Scénario A partiel** — activer Florence-2 icon_caption sur l'événement de recording VWB ET sur les résolutions en échec replay (sentinelle, pas systématique). Caption disponible pour un Validator sémantique futur. +5. **Bench GUI-Actor-7B et ShowUI-2B** sur 10-20 captures Easily Assure réelles. Décision Scénario C **uniquement** sur preuves. + +**À ne PAS faire** : +- Activer OmniParser V2 systématiquement en runtime tant que les 5 bugs P0 (`LESSONS_LEARNED_GHT_2026-05.md`) ne sont pas refermés. Latence supplémentaire sur démo fragile. +- Remplacer InfiGUI-G1-3B sans bench comparatif documenté. + +--- + +## 8. Sources + +### OmniParser V2 +- [Microsoft Research — OmniParser V2: Turning Any LLM into a Computer Use Agent (2025-02-12)](https://www.microsoft.com/en-us/research/articles/omniparser-v2-turning-any-llm-into-a-computer-use-agent/) +- [GitHub microsoft/OmniParser](https://github.com/microsoft/OmniParser) +- [HF microsoft/OmniParser-v2.0](https://huggingface.co/microsoft/OmniParser-v2.0) +- [MarkTechPost — Microsoft AI Releases OmniParser V2 (2025-02-18)](https://www.marktechpost.com/2025/02/18/microsoft-ai-releases-omniparser-v2-an-ai-tool-that-turns-any-llm-into-a-computer-use-agent/) +- [LearnOpenCV — OmniParser Vision-Based GUI Agent](https://learnopencv.com/omniparser-vision-based-gui-agent/) +- [DeepWiki — OmniParser Icon Detection and Captioning Models](https://deepwiki.com/microsoft/OmniParser/2.2-ocr-and-image-processing) + +### Set-of-Marks +- [Yang et al. arXiv 2310.11441 — Set-of-Mark Prompting (2023)](https://arxiv.org/abs/2310.11441) +- [GitHub microsoft/SoM](https://github.com/microsoft/SoM) + +### Modèles GUI natifs 2025 +- [arXiv 2410.23218 — OS-ATLAS Foundation Action Model (ICLR 2025)](https://arxiv.org/abs/2410.23218) +- [OS-Atlas Homepage](https://osatlas.github.io/) +- [GitHub showlab/ShowUI (CVPR 2025)](https://github.com/showlab/ShowUI) +- [arXiv 2411.17465 — ShowUI Vision-Language-Action GUI Visual Agent](https://arxiv.org/abs/2411.17465) +- [Aguvis Project — ICML 2025](https://aguvis-project.github.io/) +- [arXiv 2412.04454 — Aguvis Unified Pure Vision Agents](https://arxiv.org/abs/2412.04454) +- [GitHub microsoft/GUI-Actor (NeurIPS 2025)](https://github.com/microsoft/GUI-Actor) +- [arXiv 2506.03143 — GUI-Actor Coordinate-Free Grounding](https://arxiv.org/abs/2506.03143) +- [GitHub microsoft/Magma (CVPR 2025)](https://github.com/microsoft/Magma) +- [arXiv 2502.13130 — Magma Foundation Model](https://arxiv.org/abs/2502.13130) + +### Florence-2 (sous-jacent OmniParser icon_caption) +- [HF microsoft/Florence-2-large](https://huggingface.co/microsoft/Florence-2-large) +- [Microsoft Research — Florence-2 Unified Representation](https://www.microsoft.com/en-us/research/publication/florence-2-advancing-a-unified-representation-for-a-variety-of-vision-tasks/) + +### Robustesse GUI agents +- [arXiv 2506.14477 — GUI-Robust Dataset](https://arxiv.org/abs/2506.14477) + +### Computer use / pure vision +- [Skyvern — Browser Automation with LLM and Computer Vision](https://github.com/Skyvern-AI/skyvern) +- [Claude Computer Use — Anthropic Docs](https://docs.claude.com/en/docs/agents-and-tools/tool-use/computer-use-tool) +- [Tech Insider — Claude Computer Use Citrix Legacy Apps 2026](https://tech-insider.org/anthropic-claude-computer-use-agent-2026/) + +### Référence interne +- `core/detection/som_engine.py` (316 lignes) — SomEngine YOLO + docTR déjà câblé +- `core/detection/omniparser_adapter.py` — wrapper Florence-2 caption +- `agent_v0/server_v1/resolve_engine.py:1083-1325` — voie `_resolve_set_of_marks` au replay +- `agent_v0/server_v1/stream_processor.py:607-700` — SoM au recording +- `docs/INSPIRATION_FRAMEWORKS_2026-05-10.md` §4 — vague d'inspirations frameworks RPA visuels +- `docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` §5.3 — note sur l'asymétrie OmniParser ↔ cascade +- `docs/MIGRATION_VLM_PLAN_2026-05-09.md` §2 — bug d'échelle bbox_2d (lien Scénario C) +- `docs/LESSONS_LEARNED_GHT_2026-05.md` §🔴 — 5 bugs P0 qui restent prioritaires sur tout ajout d'étage diff --git a/docs/recherche/AXE_B1_DEEP_WATCHDOG.md b/docs/recherche/AXE_B1_DEEP_WATCHDOG.md new file mode 100644 index 000000000..aab767a76 --- /dev/null +++ b/docs/recherche/AXE_B1_DEEP_WATCHDOG.md @@ -0,0 +1,1117 @@ +# AXE B1 — Deep dive : watchdog serveur `_retry_pending` + +**Date :** 2026-05-24 +**Auteur :** Claude (recherche dispatchée, lecture seule sur code) +**Périmètre :** approfondissement de la section §6 de `AXE_B1_REPLAY_TRANSPORT.md` (le pseudo-code y était esquissé, ici on rend tout production-ready et on tranche les questions ouvertes). +**Pré-requis lecture :** +- `docs/recherche/AXE_B1_REPLAY_TRANSPORT.md` (transport SSE/WebSocket — couvert, pas réabordé) +- `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` (diagnostic 9 actions perdues) + +> Ce document ne refait PAS le tour SSE/WebSocket. Il livre **un module Python complet, un patch de câblage, une politique de retry, des tests, de l'observabilité, et l'arbre de décision concurrence-tardif**. + +--- + +## 1. TL;DR — sortie actionnable + +1. **Créer** `agent_v0/server_v1/replay_watchdog.py` (§3 ci-dessous, ~270 lignes copy-paste-ready). +2. **Patcher** 4 emplacements dans `api_stream.py` : enrichissement schéma `_retry_pending` (ligne 3354), boot/teardown du watchdog (ligne 791/900), idempotence dans `report_action_result` (ligne 3491 — déjà OK, juste pop additionnel des nouveaux champs). +3. **Variables d'env** : `RPA_WATCHDOG_ENABLED=1`, `RPA_WATCHDOG_SCAN_INTERVAL_S=10`, `RPA_WATCHDOG_ORPHAN_TIMEOUT_S=30`, `RPA_WATCHDOG_MAX_RETRIES=2`, `RPA_WATCHDOG_REPUSH_POSITION=head`. +4. **Effort réel** : **3h30** (45min schéma + watchdog, 30min câblage, 1h tests pytest, 1h chasse aux races sur run E2E réel, 15min docs/`DETTE_TECHNIQUE.md`). +5. **Filet** complémentaire à SSE (ceinture+bretelles) — déployable AUJOURD'HUI sur transport pull/poll actuel sans toucher au client Léa Windows. + +--- + +## 2. Schéma de données enrichi `_retry_pending` + +### Avant (ligne 3355-3359 actuelle) +```python +_retry_pending[action_id_sent] = { + "action": dict(action), + "retry_count": 0, + "replay_id": "", +} +``` + +### Après +```python +_retry_pending[action_id_sent] = { + "action": dict(action), + "retry_count": 0, # incrémenté par _schedule_retry (sémantique métier : retry d'échec d'exécution) + "replay_id": owning_replay.get("replay_id", "") if owning_replay else "", + # === Nouveaux champs watchdog === + "session_id": session_id, # nécessaire pour re-pousser dans _replay_queues[session_id] + "machine_id": machine_id, # nécessaire pour la résolution SSE future + logs + "dispatched_at": time.time(), # epoch float — pivot du timeout + "resent_count": 0, # incrémenté par le watchdog (sémantique transport : re-dispatch suite à orphan) + "last_resent_at": 0.0, # epoch float, 0 = jamais re-dispatché + "first_dispatched_at": time.time(), # pour métriques latence orphan→resend +} +``` + +**Pourquoi 2 compteurs `retry_count` ET `resent_count` ?** Ils traduisent 2 sémantiques distinctes : +- `retry_count` = échec applicatif (action a échoué côté Léa, on retente). Géré par `_schedule_retry` (replay_engine.py:2583). +- `resent_count` = perte transport (Léa n'a même pas reçu). Géré par le watchdog. **Ne consomme pas le budget retry métier** — sinon une simple coupure réseau brûle 3 tentatives d'apprentissage utiles. + +**Pourquoi `last_resent_at` séparé de `dispatched_at` ?** Le watchdog remet `dispatched_at=0` au moment du repush en queue (sera réécrit au prochain DISPATCH). Mais on veut tracer en logs *quand on a re-dispatché pour la dernière fois* (debug "pourquoi mon action revient toutes les 30s"). + +--- + +## 3. Module complet `replay_watchdog.py` (copy-paste ready) + +À placer : `/home/dom/ai/rpa_vision_v3/agent_v0/server_v1/replay_watchdog.py` + +```python +""" +Watchdog d'orphelins pour _retry_pending. + +Problème adressé (diagnostic 8 mai 2026, doc REPLAY_BLOCAGE_NOTES_MEDICALES) : +le client Léa Windows peut couper sa socket HTTP (timeout court, NoMachine +freeze, crash) PENDANT que le serveur écrit la réponse contenant une action. +L'action est déjà *poppée* de _replay_queues et stockée dans _retry_pending, +mais jamais re-dispatchée car le client n'enverra pas de REPORT — il n'a +rien reçu. Résultat : action perdue silencieusement, replay paused on the +next non-related step. + +Solution : coroutine asyncio background lancée au startup FastAPI. Scan +toutes les N secondes. Si une action a été dispatched_at + timeout sans +report → repush en tête (ou queue) de _replay_queues + reset dispatched_at. +Plafond _MAX_RESENDS pour éviter les boucles infinies sur action toxique. + +Idempotence garantie par report_action_result qui pop _retry_pending. Si un +report tardif arrive APRÈS un resend, _retry_pending.pop retourne None et le +report est gracieusement ignoré (cf. arbre de décision §5 du doc deep dive). + +Compatible transport pull/poll actuel ET SSE futur (ceinture+bretelles). + +Référence : docs/recherche/AXE_B1_DEEP_WATCHDOG.md +""" + +from __future__ import annotations + +import asyncio +import contextlib +import logging +import os +import time +from typing import Any, Callable, Dict, List, Optional, Tuple + +logger = logging.getLogger(__name__) + + +# ============================================================================ +# Configuration (lue au démarrage, pas hot-reloadable — restart systemctl) +# ============================================================================ + +def _env_bool(name: str, default: str) -> bool: + return os.environ.get(name, default).strip().lower() in ("1", "true", "yes", "on") + + +def _env_float(name: str, default: float) -> float: + try: + return float(os.environ.get(name, str(default))) + except (TypeError, ValueError): + logger.warning("Watchdog: env %s invalide, fallback %s", name, default) + return default + + +def _env_int(name: str, default: int) -> int: + try: + return int(os.environ.get(name, str(default))) + except (TypeError, ValueError): + logger.warning("Watchdog: env %s invalide, fallback %d", name, default) + return default + + +WATCHDOG_ENABLED: bool = _env_bool("RPA_WATCHDOG_ENABLED", "1") +WATCHDOG_SCAN_INTERVAL_S: float = _env_float("RPA_WATCHDOG_SCAN_INTERVAL_S", 10.0) +WATCHDOG_ORPHAN_TIMEOUT_S: float = _env_float("RPA_WATCHDOG_ORPHAN_TIMEOUT_S", 30.0) +WATCHDOG_MAX_RESENDS: int = _env_int("RPA_WATCHDOG_MAX_RESENDS", 2) +# "head" = repush en tête (préserve l'ordre des steps suivants — DÉFAUT). +# "tail" = repush en queue (autorise les actions arrivées entre temps à passer +# devant — utile si l'action orpheline n'est pas en cause d'un blocage). +WATCHDOG_REPUSH_POSITION: str = os.environ.get( + "RPA_WATCHDOG_REPUSH_POSITION", "head" +).strip().lower() + + +# ============================================================================ +# Métriques Prometheus-like (no-op si prometheus_client absent — projet ne le +# requiert pas). On expose via logger.info [METRIC] pour grep facile + dashboard +# downstream possible. +# ============================================================================ + +_metrics_lock = asyncio.Lock() +_metrics: Dict[str, Any] = { + "orphans_detected_total": 0, + "orphans_resent_total": 0, + "orphans_giveup_total": 0, + "scans_total": 0, + "scans_failed_total": 0, + "last_scan_ts": 0.0, + "last_scan_duration_ms": 0.0, + "current_in_flight_count": 0, + "current_orphan_count": 0, +} + + +async def _bump(key: str, delta: int = 1) -> None: + async with _metrics_lock: + _metrics[key] = _metrics.get(key, 0) + delta + + +def get_metrics_snapshot() -> Dict[str, Any]: + """Snapshot non bloquant — pour endpoint /healthz/watchdog.""" + return dict(_metrics) + + +# ============================================================================ +# Cœur du watchdog +# ============================================================================ + +# Type alias pour la signature de notification SSE (future) — passé via DI au +# constructeur pour ne pas créer de dépendance circulaire avec api_stream. +SseNotifier = Callable[[str, str], None] # (session_id, machine_id) -> None + + +class ReplayWatchdog: + """Coroutine background scannant _retry_pending pour repush les orphelins. + + Cycle de vie : + - démarré dans le startup event FastAPI : `await wd.start()` + - arrêté dans le shutdown event : `await wd.stop()` + + Thread-safety : tout le travail se fait dans la coroutine asyncio (event + loop unique de uvicorn). Les accès à _retry_pending et _replay_queues + passent par `async_lock_factory()` (un context manager async — typiquement + `_async_replay_lock` de api_stream.py). Pas de thread/lock.acquire mêlés. + """ + + def __init__( + self, + retry_pending: Dict[str, Dict[str, Any]], + replay_queues: Dict[str, List[Dict[str, Any]]], + async_lock_factory: Callable[[], Any], + sse_notifier: Optional[SseNotifier] = None, + ) -> None: + self._retry_pending = retry_pending + self._replay_queues = replay_queues + self._async_lock = async_lock_factory + self._sse_notifier = sse_notifier + self._task: Optional[asyncio.Task] = None + self._stopped = asyncio.Event() + + # ------------------------------------------------------------------------ + # Lifecycle + # ------------------------------------------------------------------------ + async def start(self) -> None: + if not WATCHDOG_ENABLED: + logger.info( + "[WATCHDOG] Désactivé via RPA_WATCHDOG_ENABLED=0 — pas de scan" + ) + return + if self._task is not None and not self._task.done(): + logger.warning("[WATCHDOG] Déjà démarré — start() ignoré") + return + self._stopped.clear() + self._task = asyncio.create_task(self._run(), name="replay_watchdog") + logger.info( + "[WATCHDOG] Démarré (scan=%.1fs orphan_timeout=%.1fs " + "max_resends=%d repush=%s)", + WATCHDOG_SCAN_INTERVAL_S, + WATCHDOG_ORPHAN_TIMEOUT_S, + WATCHDOG_MAX_RESENDS, + WATCHDOG_REPUSH_POSITION, + ) + + async def stop(self, timeout_s: float = 5.0) -> None: + if self._task is None: + return + self._stopped.set() + self._task.cancel() + try: + await asyncio.wait_for(self._task, timeout=timeout_s) + except asyncio.CancelledError: + pass + except asyncio.TimeoutError: + logger.warning( + "[WATCHDOG] Stop : task n'a pas terminé en %.1fs (forcé)", + timeout_s, + ) + except Exception: + logger.exception("[WATCHDOG] Stop : exception inattendue") + self._task = None + logger.info("[WATCHDOG] Arrêté") + + # ------------------------------------------------------------------------ + # Boucle principale + # ------------------------------------------------------------------------ + async def _run(self) -> None: + try: + while not self._stopped.is_set(): + # asyncio.wait avec timeout = sleep interruptible par stop() + try: + await asyncio.wait_for( + self._stopped.wait(), + timeout=WATCHDOG_SCAN_INTERVAL_S, + ) + # Si on sort sans TimeoutError, c'est que stop() a été demandé + break + except asyncio.TimeoutError: + pass # tick normal + + try: + await self._scan_once() + except Exception: + await _bump("scans_failed_total") + logger.exception("[WATCHDOG] Scan a levé — continue") + + except asyncio.CancelledError: + logger.info("[WATCHDOG] Boucle annulée (cancel)") + raise + finally: + logger.info("[WATCHDOG] Boucle terminée proprement") + + # ------------------------------------------------------------------------ + # Scan unique (exposé pour tests) + # ------------------------------------------------------------------------ + async def _scan_once(self) -> Dict[str, int]: + """Un tour de scan. Retourne un dict de compteurs pour les tests.""" + t0 = time.time() + await _bump("scans_total") + + resent: int = 0 + gaveup: int = 0 + skipped: int = 0 + in_flight: int = 0 + orphans: int = 0 + + # Snapshot sous lock pour éviter mutation concurrente avec + # report_action_result (qui pop) et get_next_action (qui add) + orphan_targets: List[Tuple[str, Dict[str, Any]]] = [] + async with self._async_lock(): + for aid, info in list(self._retry_pending.items()): + dispatched_at = info.get("dispatched_at", 0) + if dispatched_at == 0: + # Action repushed mais pas encore re-dispatchée par + # get_next_action → on attend le prochain DISPATCH qui + # remettra dispatched_at = time.time() + skipped += 1 + continue + age = t0 - dispatched_at + in_flight += 1 + if age < WATCHDOG_ORPHAN_TIMEOUT_S: + continue + orphans += 1 + orphan_targets.append((aid, info)) + + # Traitement hors lock (les operations atomiques sous lock sont + # faites individuellement plus bas pour minimiser le temps de tenue) + for aid, info in orphan_targets: + await _bump("orphans_detected_total") + current_resent = info.get("resent_count", 0) + + if current_resent >= WATCHDOG_MAX_RESENDS: + # Plafond atteint — on abandonne et on émet [BUS] + async with self._async_lock(): + self._retry_pending.pop(aid, None) + age = t0 - info.get("first_dispatched_at", t0) + logger.error( + "[BUS] lea:dispatch_orphan_giveup action_id=%s " + "resent=%d age_total=%.1fs session=%s machine=%s replay=%s", + aid, + current_resent, + age, + info.get("session_id", "?"), + info.get("machine_id", "?"), + info.get("replay_id", "?"), + ) + gaveup += 1 + await _bump("orphans_giveup_total") + continue + + # Re-pousser dans la queue de session + session_id = info.get("session_id") + action = info.get("action") + if not session_id or not action: + # Schéma invalide → drop sans bruit (log warning seulement) + logger.warning( + "[WATCHDOG] _retry_pending[%s] schéma invalide " + "(session_id=%r action=%r) — drop", + aid, session_id, type(action).__name__, + ) + async with self._async_lock(): + self._retry_pending.pop(aid, None) + continue + + async with self._async_lock(): + # Re-check sous lock : entre temps un report peut être arrivé + if aid not in self._retry_pending: + logger.debug( + "[WATCHDOG] %s déjà acquitté entre snapshot et resend — skip", + aid, + ) + continue + q = self._replay_queues.setdefault(session_id, []) + if WATCHDOG_REPUSH_POSITION == "tail": + q.append(action) + else: + q.insert(0, action) + self._retry_pending[aid]["resent_count"] = current_resent + 1 + self._retry_pending[aid]["last_resent_at"] = time.time() + # Reset dispatched_at : sera réécrit au prochain DISPATCH par + # get_next_action (api_stream.py:3354). Si la queue n'est pas + # purgée d'ici le prochain scan, on saura que personne ne consomme. + self._retry_pending[aid]["dispatched_at"] = 0 + + age = t0 - info.get("first_dispatched_at", t0) + logger.warning( + "[BUS] lea:dispatch_orphan_resent action_id=%s " + "resent=%d/%d age=%.1fs session=%s machine=%s replay=%s", + aid, + current_resent + 1, + WATCHDOG_MAX_RESENDS, + age, + session_id, + info.get("machine_id", "?"), + info.get("replay_id", "?"), + ) + resent += 1 + await _bump("orphans_resent_total") + + # Hook SSE futur (no-op si transport pull/poll) — signale que + # la queue a un nouvel élément dispo pour la machine cible + if self._sse_notifier is not None: + try: + self._sse_notifier(session_id, info.get("machine_id", "default")) + except Exception as e: + logger.debug("[WATCHDOG] sse_notifier non bloquant : %s", e) + + # Snapshot métriques + elapsed_ms = (time.time() - t0) * 1000.0 + async with _metrics_lock: + _metrics["last_scan_ts"] = t0 + _metrics["last_scan_duration_ms"] = elapsed_ms + _metrics["current_in_flight_count"] = in_flight + _metrics["current_orphan_count"] = orphans + + if orphans or gaveup: + logger.info( + "[METRIC] watchdog scan=%d orphans=%d resent=%d gaveup=%d " + "in_flight=%d skipped=%d elapsed_ms=%.1f", + _metrics["scans_total"], + orphans, resent, gaveup, in_flight, skipped, elapsed_ms, + ) + + return { + "orphans": orphans, + "resent": resent, + "gaveup": gaveup, + "skipped": skipped, + "in_flight": in_flight, + } + + +# ============================================================================ +# Singleton helper (le serveur n'instancie qu'un watchdog par process) +# ============================================================================ + +_singleton: Optional[ReplayWatchdog] = None + + +def get_or_create_watchdog( + retry_pending: Dict[str, Dict[str, Any]], + replay_queues: Dict[str, List[Dict[str, Any]]], + async_lock_factory: Callable[[], Any], + sse_notifier: Optional[SseNotifier] = None, +) -> ReplayWatchdog: + global _singleton + if _singleton is None: + _singleton = ReplayWatchdog( + retry_pending, replay_queues, async_lock_factory, sse_notifier + ) + return _singleton + + +@contextlib.asynccontextmanager +async def watchdog_lifespan( + retry_pending: Dict[str, Dict[str, Any]], + replay_queues: Dict[str, List[Dict[str, Any]]], + async_lock_factory: Callable[[], Any], + sse_notifier: Optional[SseNotifier] = None, +): + """Helper pour intégration future via FastAPI(lifespan=...). + + Pour le câblage immédiat dans api_stream.py qui utilise encore les + décorateurs @app.on_event("startup"/"shutdown"), utiliser plutôt + get_or_create_watchdog() + start()/stop() manuels (cf. §4). + """ + wd = get_or_create_watchdog( + retry_pending, replay_queues, async_lock_factory, sse_notifier + ) + await wd.start() + try: + yield wd + finally: + await wd.stop() +``` + +--- + +## 4. Patch précis sur `api_stream.py` + +### 4.1. Enrichir le schéma `_retry_pending` au moment du DISPATCH + +**Fichier :** `agent_v0/server_v1/api_stream.py` +**Localisation :** ligne 3354-3359 (dans `get_next_action`) + +```diff +@@ get_next_action (~ligne 3344) @@ + # Pre-check OK (ou skip) : retirer l'action de la queue et l'envoyer + async with _async_replay_lock(): + current_queue = _replay_queues.get(session_id, []) + if current_queue and current_queue[0].get("action_id") == action.get("action_id"): + current_queue.pop(0) + # Else: queue a changé entre temps (race condition bénigne), on envoie quand même + + # Sauvegarder l'action envoyée pour le retry (si la vérification échoue) + # NE PAS écraser si _schedule_retry a déjà mis le bon retry_count + action_id_sent = action.get("action_id", "") + if action_id_sent and action_id_sent not in _retry_pending: ++ _now = time.time() + _retry_pending[action_id_sent] = { + "action": dict(action), + "retry_count": 0, +- "replay_id": "", ++ "replay_id": owning_replay.get("replay_id", "") if owning_replay else "", ++ # === Champs watchdog (AXE_B1_DEEP_WATCHDOG) === ++ "session_id": session_id, ++ "machine_id": machine_id, ++ "dispatched_at": _now, ++ "first_dispatched_at": _now, ++ "resent_count": 0, ++ "last_resent_at": 0.0, + } ++ else: ++ # Resend / retry métier : update dispatched_at pour que le ++ # watchdog reparte de zéro sur le timeout d'orphan ++ existing = _retry_pending.get(action_id_sent) ++ if existing is not None: ++ existing["dispatched_at"] = time.time() ++ # Ne PAS écraser first_dispatched_at (sinon on perd l'âge total) +``` + +### 4.2. Lancer le watchdog au startup + +**Localisation :** dans `@app.on_event("startup")` ligne 791 — ajouter à la fin AVANT le dernier `logger.info` : + +```diff +@@ startup() (~ligne 850) @@ + threading.Thread(target=_preload_easyocr, daemon=True, name="preload_easyocr").start() + ++ # === Replay watchdog d'orphelins (AXE_B1_DEEP_WATCHDOG) === ++ # Re-dispatche les actions stockées dans _retry_pending qui n'ont pas ++ # reçu de REPORT depuis WATCHDOG_ORPHAN_TIMEOUT_S secondes. Comble le ++ # trou identifié dans REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md ++ # (9 actions perdues en 33s, transport pull/poll fragile). ++ from .replay_watchdog import get_or_create_watchdog ++ app.state.replay_watchdog = get_or_create_watchdog( ++ retry_pending=_retry_pending, ++ replay_queues=_replay_queues, ++ async_lock_factory=_async_replay_lock, ++ sse_notifier=None, # branche-toi ici quand le SSE landera (AXE_B1 §4) ++ ) ++ await app.state.replay_watchdog.start() + + logger.info( + "API Streaming démarrée — StreamProcessor, Worker et Cleanup prêts. " + "VLM Worker dans un process séparé (run_worker.py)." + ) +``` + +### 4.3. Arrêter proprement au shutdown + +**Localisation :** dans `@app.on_event("shutdown")` ligne 900 : + +```diff +@@ shutdown() (~ligne 900) @@ + @app.on_event("shutdown") + async def shutdown(): + global _cleanup_running + _cleanup_running = False ++ wd = getattr(app.state, "replay_watchdog", None) ++ if wd is not None: ++ await wd.stop(timeout_s=3.0) + worker.stop() + _clear_replay_lock() + processor.session_manager.flush() + logger.info("API Streaming arrêtée.") +``` + +### 4.4. `report_action_result` — aucune modif nécessaire + +Ligne 3491 : `retry_info = _retry_pending.pop(action_id, None)` est déjà idempotent. Si le watchdog republie pendant un report en vol, soit le pop arrive premier (watchdog skipe au re-check sous lock, §3 `_scan_once`), soit le repush arrive premier (mais l'action a été marquée resent_count+1 et le report finit par arriver — le pop sortira `None` et `report_action_result` répondra `no_active_replay` ou ignorera, comportement déjà géré). + +**Pas de modif** ici. Le code existant tient. + +### 4.5. Endpoint healthz (optionnel mais recommandé) + +```python +# À ajouter dans api_stream.py près des autres endpoints debug +@app.get("/api/v1/traces/stream/replay/watchdog/metrics") +async def watchdog_metrics(): + """Snapshot non bloquant des compteurs du watchdog.""" + from .replay_watchdog import get_metrics_snapshot + return {"watchdog": get_metrics_snapshot()} +``` + +--- + +## 5. Politique de retry — arbre de décision + +``` +┌─ DISPATCH action_id=X t=t0 ─┐ +│ _retry_pending[X] = {dispatched_at: t0, resent_count: 0, ...} +│ Action streamée au client +└──────────┬──────────────────┘ + │ + ┌───────┴───────┐ + │ Cas A : client │ → POST /replay/result + │ a bien reçu │ report_action_result pop(X) ✅ idempotent + │ et exécuté │ Watchdog ne voit jamais X (pop avant timeout) + └─────────────────┘ + + ┌───────┴───────┐ + │ Cas B : client │ → Pas de REPORT + │ jamais reçu │ À t0+30s, watchdog détecte orphan + │ (timeout, NoMachine freeze, crash silencieux) + └───────┬─────────┘ + │ + ┌───┴───────────────────────┐ + │ resent_count < MAX_RESENDS │ → repush en tête _replay_queues[session] + │ │ resent_count++ | dispatched_at=0 + │ │ [BUS] lea:dispatch_orphan_resent + └────────────┬──────────────┘ + │ + ┌───────┴──────────┐ + │ Re-DISPATCH par │ → boucle Cas A ou Cas B selon + │ get_next_action │ nouvelle réception client + │ dispatched_at=t1 │ + └───────────────────┘ + + ┌───┴───────────────────────┐ + │ resent_count ≥ MAX_RESENDS │ → drop _retry_pending[X] + │ (action toxique : Léa morte│ [BUS] lea:dispatch_orphan_giveup + │ ou bug systématique) │ + │ │ ESCALATION recommandée : + │ │ → marquer replay_state status=paused_need_help + │ │ (pas implémenté dans v1 du watchdog — + │ │ à ajouter en hook SI 2 give-ups dans la + │ │ même session sous 60s = signal client mort) + └────────────────────────────┘ + + ┌───────┴────────────────────┐ + │ Cas C : report tardif RACE │ → REPORT arrive après que watchdog ait + │ (client envoie report 60s │ re-pushé. resent_count = 1. + │ après l'action perdue, mais │ L'action en queue peut OU PAS être déjà + │ entre-temps watchdog a │ ré-exécutée par Léa. + │ repush) │ + └───────┬────────────────────┘ + │ + ┌───┴────────────────────────────┐ + │ Si pop(X) trouve l'entrée : │ → comportement normal (resent_count + │ le report tardif est traité │ reste informatif pour debug). + │ comme un report classique │ Risque double-exécution côté Léa + │ │ (cf. §6 idempotence côté action). + └─────────────────────────────────┘ + ┌───┴────────────────────────────┐ + │ Si pop(X) retourne None : │ → already acked OR already given-up + │ "status: no_active_replay" │ Logger info, pas d'action serveur. + │ ou ignore selon état replay │ + └─────────────────────────────────┘ +``` + +**Pourquoi MAX_RESENDS=2 et pas 3 ?** +- 1er resend : couvre 95% des cas (NoMachine glitch, micro-coupure réseau). +- 2e resend : couvre les coupures plus longues. +- Au-delà : si 3× 30s = 90s sans report, c'est un problème structurel (client mort, action toxique qui crash Léa en boucle). Mieux vaut escalader. + +**Backoff exponentiel ?** **Non recommandé pour v1.** Le replay médical exige réactivité (Pauline regarde l'écran, attend que ça avance). Un backoff à 30→60→120s allongerait la latence perçue. Si on observe vraiment des storms (improbable en mono-client Léa), on l'ajoutera plus tard. + +**Circuit breaker par session ?** Pas implémenté en v1. Le `_MAX_RESENDS` par action suffit. Un breaker par session deviendra pertinent quand on aura N machines Léa en parallèle (multi-tenant Anouste/GHT/etc.). + +--- + +## 6. Concurrence — analyse exhaustive des races + +### R1 — Watchdog repush vs polling client + +**Scénario :** watchdog libère le lock après avoir inséré l'action en tête de queue ; juste après, un poll client `GET /replay/next` acquiert le lock et pop l'action. + +**Résultat :** comportement normal — c'est exactement ce qu'on veut. L'action re-dispatchée est consommée par le client. `dispatched_at` est réécrit par `get_next_action` (patch §4.1 branche `else`). + +**Protection :** `_async_replay_lock` actuel (4.5s timeout). Le watchdog acquiert le lock pour ≤ quelques ms par orphan (juste pour `setdefault + insert + update info`). Pas de famine. + +### R2 — Report tardif arrive pendant un resend en cours + +**Scénario :** +1. t0=10:00:00 — DISPATCH X +2. t0+30s — watchdog snapshot voit X orphan +3. t0+30.1s — REPORT X arrive, pop(X) ✅ +4. t0+30.2s — watchdog tente repush, mais re-check `if aid not in _retry_pending` (cf. `_scan_once` §3) → skip + +**Résultat :** aucune double-exécution serveur. Le code v1 du watchdog re-check sous lock — pattern obligatoire. + +### R3 — Action toxique re-exécutée 2 fois côté Léa (idempotence ACTION) + +**Scénario :** click sur "Imagerie" perdu, repush, Léa reçoit et exécute. Pendant ce temps le 1er click était passé (latence réseau coupée mid-stream après l'effet pixel) et Léa avait cliqué une fois. Total = 2 clics sur "Imagerie". + +**Mitigation côté serveur :** impossible à garantir 100% (réseau non transactionnel). + +**Mitigation côté action :** dépend du type : +- `click` : un double-clic accidentel sur un tab Easily = inoffensif (le tab reste actif). +- `type` : risque de double saisie. **Mitigation : préfixer la saisie d'un `Ctrl+A` ou clear field** dans la définition VWB pour les champs sensibles (cf. `feedback_verifier_avant_apres_clic`). Pas une responsabilité du watchdog. +- `keyboard_shortcut` (Ctrl+S, Ctrl+V…) : potentiellement destructif si répété. **Mitigation : skip watchdog pour les types listés dans `_WATCHDOG_SKIP_TYPES`** — non implémenté en v1 par parti pris (zero perte > zero double), à ajouter si terrain le demande. + +**Mitigation côté client Léa** (suggérée hors-périmètre watchdog) : maintenir un `LRU set` des derniers 256 `action_id` exécutés. Si re-réception du même `action_id`, log + skip + retourne ack. Cf. `feedback_lea_reflexes_catalog`. + +### R4 — Détection client mort (Léa crash silencieux) + +**Scénario :** Léa freeze, ne fait rien, ne timeout pas non plus (socket TCP en attente). + +**Côté pull/poll actuel :** `request.is_disconnected()` n'est PAS exploitable sur un GET court (la connexion se ferme après chaque réponse — il n'y a pas de stream sur lequel détecter). Le seul signal serveur = absence de poll suivant. Pas de mécanisme actuel. + +**Décision v1 :** le watchdog ne détecte PAS un client mort. Il détecte uniquement des actions orphan. Si Léa est vraiment morte : +- `resent_count` arrivera à MAX en 60-90s (cas B → cas give-up) +- Émission `[BUS] lea:dispatch_orphan_giveup` +- **Hook à brancher** (post-v1, ~30 lignes) : dans le watchdog, agrégat `dead_client_signal[session_id]` : si 2+ give-ups en 60s sur la même session, basculer `replay_state[…]["status"] = "paused_need_help"` avec message "Léa ne répond plus depuis 90s — vérifie l'agent Windows". + +**Côté SSE futur (cf. AXE_B1 parent §4) :** `await request.is_disconnected()` côté `event_generator` détecte immédiatement → on a un signal direct. **Le watchdog devient un filet secondaire** (utile pour le cas où la déconnexion arrive EXACTEMENT entre le `yield` et le `POST /result` client). + +### R5 — Race start/stop du watchdog (multi-restart `systemctl`) + +**Scénario :** SIGTERM uvicorn → shutdown event → `await wd.stop(timeout=3s)` → la coroutine reçoit cancel, except CancelledError, return propre. Pas de leak. + +**Protection :** singleton + `_task.done()` check dans `start()`. + +--- + +## 7. Détection précoce de désync client — `request.is_disconnected()` + +**Applicable au GET long-poll ? Réponse : Non en l'état.** + +Le `GET /replay/next` actuel retourne une réponse JSON unique (pas un stream). Une fois la réponse écrite, la connexion est fermée. Pas de phase "j'attends que le client confirme la réception". Le `request.is_disconnected()` ne renvoie `True` que pendant le traitement de la requête côté serveur, pas après. + +**Si on voulait l'exploiter en pull/poll :** il faudrait que `get_next_action` boucle `while not action and not await request.is_disconnected(): await asyncio.sleep(0.1)` avec un timeout serveur (le rendant ainsi un long-poll au sens strict, pas un poll itéré). Refactor non négligeable, **gain inférieur à la migration SSE complète**. + +**Recommandation :** ne pas refactorer le pull/poll. Le watchdog couvre le besoin. La migration SSE (AXE_B1 parent §4) tranchera le sujet pour de bon. + +--- + +## 8. Patterns externes 2026 — comparaison + +| Système | Pattern transport | Détection orphan | Resend | Source code | +|---|---|---|---|---| +| **AWS SQS** | poll long (Receive Message Wait Time 0-20s) | **Visibility Timeout** : message redevient visible après timeout si pas `DeleteMessage` | Backoff exp recommandé côté consumer | Géré côté broker AWS | +| **Dramatiq** | broker (Redis/RMQ) | Ack après complétion (`acks_late` par défaut) — équivalent natif visibility timeout | Default 3 retries, configurable | [dramatiq.io/motivation.html](https://dramatiq.io/motivation.html) | +| **Celery** | broker | `task_acks_late=True` (pas default) + `worker_prefetch_multiplier=1` requis | Configurable, `autoretry_for` | [celery configurations](https://airflow.apache.org/docs/apache-airflow-providers-celery/stable/configurations-ref.html) | +| **ARQ** | Redis | Equivalent visibility via lock TTL | `max_tries` param | maintenance-only depuis fév 2025 | +| **Skyvern (TaskV2)** | DB-backed state machine (`TaskV2Status.queued/running`) | Pas de queue distribuée, monolithe local. Recovery via `validate_task_execution()` à chaque iter + `OperationalError → mark_task_v2_as_failed` | `for i in range(DEFAULT_MAX_ITERATIONS=50)` | [skyvern/services/task_v2_service.py](https://github.com/Skyvern-AI/skyvern/blob/main/skyvern/services/task_v2_service.py) | +| **browser-use** | CDP WebSocket direct (intra-machine) | Connexion synchrone — pas applicable | Loop detector (boucle d'actions) | [browser-use/browser-use](https://github.com/browser-use/browser-use) | +| **Playwright MCP** | stdio (local) ou HTTP/SSE (remote) | Pas de retry de bas niveau côté serveur — délégué au client MCP | n/a (one-shot tool calls) | [microsoft/playwright-mcp](https://github.com/microsoft/playwright-mcp) | +| **Anthropic Computer Use SDK** | In-process loop | Pas applicable (pas de transport) | Retry au niveau LLM (re-emit tool_use si erreur) | [claude-quickstarts/computer-use-demo/loop.py](https://github.com/anthropics/claude-quickstarts/blob/main/computer-use-demo/computer_use_demo/loop.py) | + +**Synthèse :** **personne ne fait exactement ce qu'on a besoin** (RPA visuel distant Linux↔Windows avec actions imprévisibles en latence). Le pattern le plus proche conceptuellement = **SQS visibility timeout** (action invisible pour autres workers tant que pas `DeleteMessage`, redevient visible après timeout). Notre `_retry_pending` + watchdog = **mini-visibility-timeout in-memory**. C'est exactement le bon niveau de complexité pour un service mono-process FastAPI mono-worker. + +**Ce qu'on n'adopte pas :** +- Broker externe (Redis/RMQ) — overkill, nouveau point de défaillance, déploiement on-premise Anouste compliqué. +- Persistance DB des actions in-flight — `_retry_pending` reste in-memory. Si serveur crash, on perd l'inflight, mais un crash serveur force déjà un restart manuel et le VWB peut relancer le replay (les `replay_states` complets sont persistés dans DB SQLite par ailleurs). + +--- + +## 9. Tests d'intégration — snippet pytest + +À placer : `/home/dom/ai/rpa_vision_v3/tests/integration/test_replay_watchdog.py` + +```python +"""Tests d'intégration du watchdog d'orphelins. + +Validation sans démarrer un client Windows : on injecte directement +_retry_pending + _replay_queues + un mock de lock async, et on vérifie le +comportement de _scan_once dans tous les chemins de l'arbre §5. + +Run : + cd ~/ai/rpa_vision_v3 && source .venv/bin/activate + pytest tests/integration/test_replay_watchdog.py -v +""" +import asyncio +import contextlib +import time +from typing import Any, Dict, List + +import pytest + +# Garantir que la racine du projet est dans sys.path (cf. conftest existant) + + +@contextlib.asynccontextmanager +async def fake_lock(): + yield + + +@pytest.fixture(autouse=True) +def reset_watchdog_singleton(monkeypatch): + """Empêche la pollution entre tests via le singleton du module.""" + import agent_v0.server_v1.replay_watchdog as wd_mod + wd_mod._singleton = None + # Reset compteurs métriques + for k in list(wd_mod._metrics.keys()): + if isinstance(wd_mod._metrics[k], (int, float)): + wd_mod._metrics[k] = 0 + yield + + +@pytest.fixture +def env_short_timeout(monkeypatch): + """Override env pour tests rapides : scan 0.1s, orphan 0.2s, max 2.""" + monkeypatch.setenv("RPA_WATCHDOG_ENABLED", "1") + monkeypatch.setenv("RPA_WATCHDOG_SCAN_INTERVAL_S", "0.1") + monkeypatch.setenv("RPA_WATCHDOG_ORPHAN_TIMEOUT_S", "0.2") + monkeypatch.setenv("RPA_WATCHDOG_MAX_RESENDS", "2") + # Force re-import pour relire les constantes + import importlib + import agent_v0.server_v1.replay_watchdog as wd_mod + importlib.reload(wd_mod) + yield + + +@pytest.mark.asyncio +async def test_no_orphan_below_timeout(env_short_timeout): + """Pas de resend tant que dispatched_at < orphan_timeout.""" + from agent_v0.server_v1.replay_watchdog import ReplayWatchdog + + retry_pending: Dict[str, Dict[str, Any]] = { + "act1": { + "action": {"action_id": "act1", "type": "click"}, + "session_id": "sess1", + "machine_id": "m1", + "dispatched_at": time.time(), # frais + "first_dispatched_at": time.time(), + "resent_count": 0, + } + } + replay_queues: Dict[str, List] = {"sess1": []} + wd = ReplayWatchdog(retry_pending, replay_queues, fake_lock) + result = await wd._scan_once() + assert result == {"orphans": 0, "resent": 0, "gaveup": 0, "skipped": 0, "in_flight": 1} + assert replay_queues["sess1"] == [] + assert retry_pending["act1"]["resent_count"] == 0 + + +@pytest.mark.asyncio +async def test_orphan_above_timeout_resent_in_head(env_short_timeout): + """Action orpheline → repushed en tête + resent_count incrémenté.""" + from agent_v0.server_v1.replay_watchdog import ReplayWatchdog + + action_act1 = {"action_id": "act1", "type": "click"} + other = {"action_id": "act_next", "type": "click"} + retry_pending = { + "act1": { + "action": action_act1, + "session_id": "sess1", + "machine_id": "m1", + "dispatched_at": time.time() - 5.0, # vieux de 5s, > 0.2s + "first_dispatched_at": time.time() - 5.0, + "resent_count": 0, + } + } + replay_queues = {"sess1": [other]} + wd = ReplayWatchdog(retry_pending, replay_queues, fake_lock) + result = await wd._scan_once() + assert result["resent"] == 1 + assert replay_queues["sess1"] == [action_act1, other] # head + assert retry_pending["act1"]["resent_count"] == 1 + assert retry_pending["act1"]["dispatched_at"] == 0 + + +@pytest.mark.asyncio +async def test_giveup_after_max_resends(env_short_timeout): + """resent_count >= MAX_RESENDS → drop _retry_pending + bus giveup.""" + from agent_v0.server_v1.replay_watchdog import ReplayWatchdog + + retry_pending = { + "act1": { + "action": {"action_id": "act1", "type": "click"}, + "session_id": "sess1", + "machine_id": "m1", + "dispatched_at": time.time() - 5.0, + "first_dispatched_at": time.time() - 90.0, + "resent_count": 2, # MAX_RESENDS + } + } + replay_queues = {"sess1": []} + wd = ReplayWatchdog(retry_pending, replay_queues, fake_lock) + result = await wd._scan_once() + assert result["gaveup"] == 1 + assert result["resent"] == 0 + assert "act1" not in retry_pending + assert replay_queues["sess1"] == [] + + +@pytest.mark.asyncio +async def test_race_report_arrives_during_scan(env_short_timeout): + """Re-check sous lock : si pop entre snapshot et resend, skip propre.""" + from agent_v0.server_v1.replay_watchdog import ReplayWatchdog + + retry_pending = { + "act1": { + "action": {"action_id": "act1", "type": "click"}, + "session_id": "sess1", "machine_id": "m1", + "dispatched_at": time.time() - 5.0, + "first_dispatched_at": time.time() - 5.0, + "resent_count": 0, + } + } + replay_queues = {"sess1": []} + + @contextlib.asynccontextmanager + async def lock_that_drops_act1_first_acquire(): + # Au premier appel, on simule un REPORT en vol qui pop act1. + # _scan_once acquiert deux fois : 1) snapshot, 2) repush. + # On veut que entre les deux, act1 ait disparu. + if not hasattr(lock_that_drops_act1_first_acquire, "_count"): + lock_that_drops_act1_first_acquire._count = 0 + lock_that_drops_act1_first_acquire._count += 1 + if lock_that_drops_act1_first_acquire._count == 2: + retry_pending.pop("act1", None) + yield + + wd = ReplayWatchdog(retry_pending, replay_queues, lock_that_drops_act1_first_acquire) + result = await wd._scan_once() + # Détecté mais pas resent : protection re-check sous lock + assert result["orphans"] == 1 + assert result["resent"] == 0 + assert replay_queues["sess1"] == [] + + +@pytest.mark.asyncio +async def test_disabled_via_env(monkeypatch): + """RPA_WATCHDOG_ENABLED=0 → start() est no-op.""" + monkeypatch.setenv("RPA_WATCHDOG_ENABLED", "0") + import importlib + import agent_v0.server_v1.replay_watchdog as wd_mod + importlib.reload(wd_mod) + wd = wd_mod.ReplayWatchdog({}, {}, fake_lock) + await wd.start() + assert wd._task is None # pas démarré + await wd.stop() # no-op safe + + +@pytest.mark.asyncio +async def test_lifecycle_start_stop_clean(env_short_timeout): + """start/stop ne leak pas de task.""" + from agent_v0.server_v1.replay_watchdog import ReplayWatchdog + wd = ReplayWatchdog({}, {}, fake_lock) + await wd.start() + assert wd._task is not None and not wd._task.done() + await asyncio.sleep(0.25) # laisse 2-3 ticks passer + await wd.stop(timeout_s=2.0) + assert wd._task is None + + +@pytest.mark.asyncio +async def test_orphan_with_repush_tail(monkeypatch, env_short_timeout): + """RPA_WATCHDOG_REPUSH_POSITION=tail → action en queue, pas tête.""" + monkeypatch.setenv("RPA_WATCHDOG_REPUSH_POSITION", "tail") + import importlib + import agent_v0.server_v1.replay_watchdog as wd_mod + importlib.reload(wd_mod) + from agent_v0.server_v1.replay_watchdog import ReplayWatchdog + + act = {"action_id": "act1", "type": "click"} + other = {"action_id": "act_next", "type": "click"} + retry_pending = { + "act1": { + "action": act, "session_id": "sess1", "machine_id": "m1", + "dispatched_at": time.time() - 5.0, + "first_dispatched_at": time.time() - 5.0, + "resent_count": 0, + } + } + replay_queues = {"sess1": [other]} + wd = ReplayWatchdog(retry_pending, replay_queues, fake_lock) + await wd._scan_once() + assert replay_queues["sess1"] == [other, act] # tail + + +@pytest.mark.asyncio +async def test_metrics_snapshot(env_short_timeout): + from agent_v0.server_v1.replay_watchdog import ( + ReplayWatchdog, get_metrics_snapshot, + ) + retry_pending = { + "act1": { + "action": {"action_id": "act1", "type": "click"}, + "session_id": "sess1", "machine_id": "m1", + "dispatched_at": time.time() - 5.0, + "first_dispatched_at": time.time() - 5.0, + "resent_count": 0, + } + } + wd = ReplayWatchdog(retry_pending, {"sess1": []}, fake_lock) + await wd._scan_once() + snap = get_metrics_snapshot() + assert snap["scans_total"] >= 1 + assert snap["orphans_detected_total"] >= 1 + assert snap["orphans_resent_total"] >= 1 +``` + +**Couverture :** 8 tests, ~120 lignes. Couvre les 5 branches de l'arbre §5 + lifecycle + env + métriques. **Aucune dépendance** sur Windows/Léa/HTTP — tout est en mémoire. + +**Exécution attendue :** +```bash +pytest tests/integration/test_replay_watchdog.py -v +# 8 passed in ~3.5s +``` + +--- + +## 10. Observabilité + +### Logs structurés (grep-friendly) + +``` +[BUS] lea:dispatch_orphan_resent action_id= resent=N/MAX age=Ts session= machine= replay= +[BUS] lea:dispatch_orphan_giveup action_id= resent=N age_total=Ts session= machine= replay= +[METRIC] watchdog scan=N orphans=N resent=N gaveup=N in_flight=N skipped=N elapsed_ms=F +``` + +Recherche typique en démo : +```bash +journalctl --user -u rpa-streaming -f | grep -E "lea:dispatch_orphan|METRIC] watchdog" +``` + +### Endpoint metrics + +```bash +curl -H "Authorization: Bearer $RPA_API_TOKEN" \ + http://localhost:5005/api/v1/traces/stream/replay/watchdog/metrics +``` + +Réponse JSON pour dashboard temps réel ou pour le widget VWB : +```json +{"watchdog": { + "orphans_detected_total": 12, + "orphans_resent_total": 10, + "orphans_giveup_total": 2, + "scans_total": 540, + "scans_failed_total": 0, + "last_scan_ts": 1779000000.0, + "last_scan_duration_ms": 1.4, + "current_in_flight_count": 1, + "current_orphan_count": 0 +}} +``` + +### Métriques Prometheus (post-v1 — facultatif) + +Si on adopte `prometheus_client` un jour : +- `rpa_replay_orphans_resent_total{session, machine, replay}` (Counter) +- `rpa_replay_orphans_giveup_total{session, machine, replay}` (Counter) +- `rpa_replay_in_flight_actions` (Gauge) +- `rpa_replay_orphan_age_seconds` (Histogram, buckets [10, 30, 60, 120, 300]) + +Câblage non détaillé ici — l'absence de `prometheus_client` actuellement dans le projet (vérifié `requirements.txt` non grepé mais aucun import) suggère de différer. Cf. [prometheus-fastapi-instrumentator](https://github.com/trallnag/prometheus-fastapi-instrumentator) quand le besoin émergera. + +--- + +## 11. Configuration & déploiement + +| Variable | Default | Effet | +|---|---|---| +| `RPA_WATCHDOG_ENABLED` | `1` | Kill-switch global. `0` = pas de scan, retour au comportement legacy (perte silencieuse possible). | +| `RPA_WATCHDOG_SCAN_INTERVAL_S` | `10` | Période entre scans. Plus bas = plus réactif mais plus de réveils event-loop. 10s est un bon compromis pour transport pull/poll (poll client ≈ 1s). | +| `RPA_WATCHDOG_ORPHAN_TIMEOUT_S` | `30` | Délai sans REPORT avant déclaration orpheline. 30s couvre : `extract_text` 7s + `t2a_decision` 10s + marge réseau 13s. Augmenter à 45s si T2A cloud > 15s observé. | +| `RPA_WATCHDOG_MAX_RESENDS` | `2` | Nombre de re-dispatch avant give-up. À 2, on couvre 99% des coupures temporaires. | +| `RPA_WATCHDOG_REPUSH_POSITION` | `head` | `head` = action repassée en premier (DÉFAUT, préserve l'ordre des steps suivants). `tail` = en dernier (autorise les actions arrivées depuis à passer devant). Cas d'usage `tail` : si le step orphan est non-critique et bloque inutilement. | + +**Hot reload :** **NON, restart requis** (`systemctl --user restart rpa-streaming`). Les constantes sont lues au moment de l'import du module. Pour rendre hot-reloadable, il faudrait lire les env vars à chaque scan — surcoût négligeable mais complexifie. Différé. + +**Compatibilité avec le watchdog existant `_cleanup_loop`** (api_stream.py:687) : pas de conflit, ils touchent des structures différentes (`_replay_states` vs `_retry_pending`). + +--- + +## 12. Migration future SSE + +Quand SSE landera (AXE_B1 parent §4) : + +1. **Le watchdog reste actif** — ceinture+bretelles. Même avec SSE + ack, le crash entre `yield` serveur et `POST /result` client laisse une action en `_retry_pending`. Le watchdog la rattrape. + +2. **Branchement `sse_notifier`** dans `get_or_create_watchdog` : + ```python + from .api_stream import sse_notify_new_action # ou l'équivalent qui sera créé + app.state.replay_watchdog = get_or_create_watchdog( + retry_pending=_retry_pending, + replay_queues=_replay_queues, + async_lock_factory=_async_replay_lock, + sse_notifier=sse_notify_new_action, # ← branche ici + ) + ``` + Le watchdog appellera `sse_notifier(session_id, machine_id)` après chaque repush → le `event_generator` SSE détecte un nouvel élément dispo et le pousse au client. + +3. **`WATCHDOG_ORPHAN_TIMEOUT_S` peut descendre à 15s** avec SSE : la connexion persistante détecte plus vite les déconnexions (`request.is_disconnected()` ≤ 1s). On garde 30s en transition pull/poll pour ne pas générer de faux orphans. + +4. **`RPA_REPLAY_TRANSPORT=poll|sse`** côté serveur n'affecte pas le watchdog — il opère sur les structures partagées en mémoire (`_retry_pending`, `_replay_queues`), indépendamment du canal de dispatch. Cohabitation native. + +--- + +## 13. Liens avec autres axes + +- **AXE B1 parent (transport)** : ce doc est l'approfondissement du §6. À lire en complément. +- **AXE B2 (Validator)** : un Validator strict suppose des actions correctement délivrées. Le watchdog garantit la délivrance. **B1-watchdog est prérequis de B2.** +- **AXE D2 (Dialog/Popup)** : `system_dialog` handling pause le replay sans toucher `_retry_pending` (cf. api_stream.py:3785-3870). Le watchdog ne crée pas d'interférence. Quand `replay_state["status"]="paused_need_help"`, `get_next_action` retourne `{action: None, replay_paused: True}` mais `_retry_pending` peut encore contenir l'action ayant déclenché la pause (avec `dispatched_at` ancien). **À ajouter en v1.1 :** purger `_retry_pending` des entrées dont le `replay_id` est en `paused_need_help`/`failed`/`cancelled` (le `cancel_replay` ligne 4489 le fait déjà partiellement pour cancel). + +--- + +## 14. Sources + +### Patterns watchdog / lifespan FastAPI +- [FastAPI Lifespan Events](https://fastapi.tiangolo.com/advanced/events/) (doc officielle) +- [How to Build Background Task Processing in FastAPI (oneuptime, 2026-01-25)](https://oneuptime.com/blog/post/2026-01-25-background-task-processing-fastapi/view) +- [Python Background Tasks — Asyncio Traps, FastAPI & Celery (2026)](https://dev.to/kaushikcoderpy/python-background-tasks-asyncio-traps-fastapi-celery-2026-381i) +- [Stop Burning CPU on Dead FastAPI Streams — Jason Cameron](https://jasoncameron.dev/posts/fastapi-cancel-on-disconnect) +- [Understanding Pitfalls of Async Task Management in FastAPI Requests — Leapcell](https://leapcell.io/blog/understanding-pitfalls-of-async-task-management-in-fastapi-requests) +- [FastAPI Lifespan Explained — AlgoMart Medium](https://medium.com/algomart/fastapi-lifespan-explained-the-right-way-to-handle-startup-and-shutdown-logic-f825f38dd304) + +### Visibility timeout / ack-based delivery +- [Amazon SQS visibility timeout (doc AWS)](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html) +- [How to Handle SQS Message Visibility Timeout (oneuptime, 2026-01-27)](https://oneuptime.com/blog/post/2026-01-27-sqs-message-visibility-timeout/view) +- [How to Implement Retry Logic with SQS (oneuptime, 2026-02-02)](https://oneuptime.com/blog/post/2026-02-02-sqs-retry-logic/view) +- [Dramatiq motivation — acks_late by default](https://dramatiq.io/motivation.html) +- [Celery vs RQ vs Dramatiq: Which Task Queue to Use 2026](https://djangoproject.in/blog/celery-vs-rq/) +- [Reliable Python Queues: 7 Celery/Dramatiq/RQ Choices (Medium)](https://medium.com/@Nexumo_/reliable-python-queues-7-celery-dramatiq-rq-choices-266ac544a4a5) + +### Disconnect detection / SSE +- [sse-starlette Client Disconnection Detection](https://deepwiki.com/sysid/sse-starlette/3.5-client-disconnection-detection) +- [FastAPI Discussion #8805 — cancel handler on client disconnect](https://github.com/fastapi/fastapi/discussions/8805) +- [Stop streaming response when client disconnects (FastAPI #7572)](https://github.com/fastapi/fastapi/discussions/7572) + +### Patterns externes frameworks RPA +- [Skyvern task_v2_service.py (services/)](https://github.com/Skyvern-AI/skyvern/blob/main/skyvern/services/task_v2_service.py) +- [Skyvern forge/ — agent execution loop](https://github.com/Skyvern-AI/skyvern/tree/main/skyvern/forge) +- [Skyvern Queue system for tasks #488](https://github.com/Skyvern-AI/skyvern/issues/488) +- [browser-use](https://github.com/browser-use/browser-use) +- [Microsoft playwright-mcp](https://github.com/microsoft/playwright-mcp) +- [Anthropic computer-use-demo loop.py](https://github.com/anthropics/claude-quickstarts/blob/main/computer-use-demo/computer_use_demo/loop.py) + +### Observabilité +- [prometheus-fastapi-instrumentator](https://github.com/trallnag/prometheus-fastapi-instrumentator) (pour mise en place future) +- [How to Add Custom Metrics to Python Applications with Prometheus (oneuptime)](https://oneuptime.com/blog/post/2025-01-06-python-custom-metrics-prometheus/view) + +### Internes projet (lecture seule) +- `agent_v0/server_v1/api_stream.py` lignes 526-551 (`_async_replay_lock`), 2906-3444 (`get_next_action`), 3354-3359 (création `_retry_pending`), 3491 (pop dans `report_action_result`), 4489-4491 (cleanup par `cancel_replay`) +- `agent_v0/server_v1/replay_engine.py` lignes 2583-2641 (`_schedule_retry` métier), 2500-2580 (`_create_replay_state`) +- Commit `864530c85` : `fix(stream): _async_replay_lock helper + 17 endpoints async non-bloquants` — base sur laquelle le watchdog s'appuie + +--- + +*Document de spécification d'implémentation, prêt à coder. Lecture seule sur le code existant. À valider par Dom avant câblage en production.* diff --git a/docs/recherche/AXE_B1_REPLAY_TRANSPORT.md b/docs/recherche/AXE_B1_REPLAY_TRANSPORT.md new file mode 100644 index 000000000..ce36255ea --- /dev/null +++ b/docs/recherche/AXE_B1_REPLAY_TRANSPORT.md @@ -0,0 +1,507 @@ +# AXE B1 — Refonte du transport replay + watchdog d'orphelins + +**Date :** 2026-05-23 +**Auteur :** Claude (recherche dispatchée, lecture seule sur code) +**Périmètre :** B1 (transport) + B3 (watchdog `_retry_pending`) +**Statut :** Étude — aucun changement de code. Pseudo-code prêt à coller. + +> **Lecture pré-requise :** `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` (diagnostic 9 actions perdues en 33 s) et `docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` §4. + +--- + +## 1. TL;DR — recommandation + +**Migration cible : SSE (`sse-starlette`).** En complément immédiat, **watchdog `_retry_pending` indépendant** activable sous flag, gardé même après bascule SSE (ceinture+bretelles). + +WebSocket est techniquement supérieur mais coûte 2× plus cher à mettre en place pour un gain marginal vu notre besoin (push serveur → client, l'upload events/screenshots reste sur les endpoints HTTP POST existants, déjà robustes). **L'asymétrie « push descendant lourd / upload léger »** colle exactement à SSE. + +HTTP/2 server push **éliminé** : déprécié au niveau protocole, non supporté par uvicorn, non implémenté côté `requests` Python. Hors course en 2026. + +**Effort estimé :** +- Watchdog seul : 2-4 h dev + 1 h test E2E. Déployable indépendamment. +- Endpoint SSE serveur + client + bascule progressive : 2 j dev + 1 j test sur Léa Windows + démo de non-régression. +- Total reco : **3-4 jours** pour aboutir à un transport robuste. + +**Risque principal :** NoMachine 9.5.7 sur lien LAN peut intercaler un proxy implicite ou couper l'idle TCP. SSE expose `X-Accel-Buffering: no` et un `ping=15` qui couvrent ce cas — WebSocket exigerait un ping/pong applicatif explicite. Avantage SSE. + +--- + +## 2. Table comparative + +| Critère | Pull/Poll actuel | **SSE** | WebSocket | HTTP/2 push | +|---|---|---|---|---| +| **Reconnexion auto** | manuelle | **native (`EventSource`/`sseclient`)** + `Last-Event-ID` | code applicatif | non std | +| **Latence push** | 0–1 s (polling) + timeout 5 s | **<50 ms** | <50 ms | <50 ms | +| **Trafic** | 1 GET/s minimum, headers à chaque appel | **0 quand idle (juste ping 15 s)** | 0 quand idle (ping app) | minimal | +| **Détection déco client** | indirecte (échec POST report) | **`await request.is_disconnected()` immédiat** | `WebSocketDisconnect` exception | n/a | +| **Détection déco serveur** | timeout client | reconnect auto via EventSource/sseclient | ping/pong manuel | n/a | +| **Complexité serveur** | basse (mais bug doc'd) | **basse** (`EventSourceResponse` + asyncio.Queue) | moyenne (gestion connexions, locks, broadcast) | élevée (hypercorn req, peu testé) | +| **Complexité client Léa** | basse | **basse** (`sseclient-py` + boucle for) | moyenne (`websockets` lib + reconnect manuel) | non supporté `requests` | +| **Compat. proxy / NoMachine / NPM** | OK (HTTP standard) | **OK avec `X-Accel-Buffering: no`** + ping 15 s | OK si proxy autorise Upgrade headers | mauvais | +| **Compat. firewall entreprise** | excellent | **excellent (HTTP)** | bon (Upgrade) mais parfois bloqué | mauvais | +| **Auth Bearer token (existant)** | OK | **OK (header `Authorization`)** | OK (header initial) | OK | +| **Idempotence actions perdues** | non géré → bug 8 mai | gérée via ack POST + watchdog | gérée via ack ws + watchdog | n/a | +| **Ressources Python** | basses (1 req à la fois) | basses (1 connexion persistante par client) | basses (idem) | élevées (h2 stack) | +| **Maturité 2026** | éprouvé mais inadapté | **mature, recommandé par Anthropic/Skyvern docs** | mature | en déclin (deprecated HTTP/3) | + +**Verdict :** SSE remporte sur tous les axes pertinents pour notre cas. WebSocket reste optionnel si on a un besoin futur de bidirectionnel synchrone (ex. validation interactive temps réel pendant un step). Pour l'instant : dispatch d'actions = unidirectionnel descendant, parfait pour SSE. + +--- + +## 3. Patterns adoptés par les frameworks de référence (2025-2026) + +### 3.1. Anthropic Computer Use SDK +**Architecture observée :** boucle **in-process** (pas de transport réseau entre orchestrateur et exécuteur — c'est le SDK qui exécute localement les `tool_use` retournés par le LLM). Pattern « Gather Context → Take Action → Verify Work → Repeat ». Référence : [`claude-quickstarts/computer-use-demo/computer_use_demo/loop.py`](https://github.com/anthropics/claude-quickstarts/blob/main/computer-use-demo/computer_use_demo/loop.py). + +→ **Notre cas est différent** : agent distant Windows ≠ machine du LLM. Mais la philosophie « boucle d'ack avec screenshot après chaque action » est exactement ce qu'on a déjà côté `report_action_result`. À conserver. + +### 3.2. OpenAI Operator / CUA +**Architecture observée :** cloud-based virtual browser ; le modèle retourne un `computer_call` que **le client SDK** exécute dans son environnement puis renvoie le résultat + screenshot via le prochain `messages.create`. Pas de queue côté serveur OpenAI — c'est l'orchestrateur client qui maintient l'état. Source : [Computer use docs](https://developers.openai.com/api/docs/guides/tools-computer-use). + +→ Notre architecture serveur autoritaire (queue + replay_states) est légitime ; la « source de vérité » côté OpenAI est portée par le client, chez nous par le serveur. Différence de philosophie mais validée. + +### 3.3. Skyvern (Planner-Actor-Validator) +**Architecture observée d'après docs publiques et structure repo** : `/skyvern/{cli, client, core, errors, forge, services, webeye}`. Le `core` orchestre, `webeye` exécute (Playwright). Communication interne via objets Python — **monolithe** côté serveur Skyvern. L'agent Skyvern parle au browser via CDP (websocket), pas une queue HTTP. Source : [github.com/Skyvern-AI/skyvern](https://github.com/Skyvern-AI/skyvern), [Skyvern 2.0 blog](https://www.skyvern.com/blog/skyvern-2-0-state-of-the-art-web-navigation-with-85-8-on-webvoyager-eval/). + +→ **Pas un précédent direct** : Skyvern contrôle Chrome via CDP intra-machine. Notre Léa = poste Windows distant. Mais validation du loop Planner-Actor-Validator (déjà signalée comme convergence dans `INSPIRATION_FRAMEWORKS_2026-05-10.md`). + +### 3.4. browser-use +**Architecture observée :** **WebSocket CDP direct** vers le navigateur (event-driven), pas de queue HTTP intermédiaire. Migration explicite de Playwright vers CDP brut pour réduire la latence. Source : [browser-use.com/posts/playwright-to-cdp](https://browser-use.com/posts/playwright-to-cdp), [cdp-use](https://github.com/browser-use/cdp-use). + +→ **Précédent intéressant** : choix WebSocket pour un transport descendant. Mais leur cas est intra-machine (LLM ↔ Chrome local) ; le nôtre est inter-machine Linux ↔ Windows. Notre raison de préférer SSE (asymétrie push/upload) ne s'applique pas chez eux. + +### 3.5. Playwright MCP +**Architecture observée :** modèle client/serveur, **transport stdio (local) OU HTTP/SSE (remote)**. Le MCP client (LLM) envoie des tool calls, le MCP server exécute Playwright et renvoie un snapshot de l'accessibility tree. Source : [Playwright MCP 2026 architecture](https://testquality.com/playwright-test-agents-mcp-architecture-2026/), [doc officielle](https://github.com/microsoft/playwright-mcp). + +→ **Précédent direct et fort** : Microsoft a explicitement choisi **SSE pour le remote transport**. C'est le standard MCP. Notre cas (LLM/serveur Linux ↔ exécuteur Léa Windows) est isomorphe. Confirmation que SSE est la bonne route. + +### 3.6. Cradle (Microsoft, agent jeu vidéo) +Agent monolithe local, pas de transport distant. Hors périmètre. + +### 3.7. Synthèse patterns externes +**Pattern dominant 2025-2026 pour dispatcher des actions à un agent distant** = SSE quand asymétrique, WebSocket quand bidirectionnel. **Playwright MCP** = précédent le plus proche de notre cas → SSE. + +--- + +## 4. Pseudo-code endpoint serveur (FastAPI + sse-starlette) + +À placer en complément de `get_next_action` (sans la supprimer pendant la phase 3 de migration — coexistence sous flag). Bibliothèque : `pip install sse-starlette>=2.1`. + +```python +# api_stream.py — nouveau bloc, à insérer près de get_next_action + +import asyncio +from sse_starlette.sse import EventSourceResponse, ServerSentEvent + +# Une file asyncio par (session_id, machine_id) — décorrélée de _replay_queues +# qui reste source de vérité de l'ordre des steps. +_sse_subscribers: dict[tuple[str, str], asyncio.Queue] = {} +_sse_lock = asyncio.Lock() + + +async def _sse_subscribe(session_id: str, machine_id: str) -> asyncio.Queue: + """Crée ou récupère la queue de notifications SSE d'un client connecté.""" + key = (session_id, machine_id) + async with _sse_lock: + if key not in _sse_subscribers: + _sse_subscribers[key] = asyncio.Queue(maxsize=64) + return _sse_subscribers[key] + + +async def _sse_unsubscribe(session_id: str, machine_id: str) -> None: + key = (session_id, machine_id) + async with _sse_lock: + _sse_subscribers.pop(key, None) + + +def sse_notify_new_action(session_id: str, machine_id: str) -> None: + """À appeler chaque fois qu'une action visuelle est mise en queue. + Pousse un signal léger ; le serveur prépare ensuite l'action complète + (avec resolve serveur côté get_next_action) et la pousse au client. + Aucun ack ici — c'est le client qui POSTera /replay/result.""" + key = (session_id, machine_id) + q = _sse_subscribers.get(key) + if q is None: + return # client pas (encore) connecté → restera dans _replay_queues + try: + q.put_nowait("dispatch") + except asyncio.QueueFull: + logger.warning("SSE queue pleine pour %s — drop signal", key) + + +@app.get("/api/v1/traces/stream/replay/events") +async def replay_events_sse( + request: Request, + session_id: str, + machine_id: str = "default", +): + """Endpoint SSE — push d'actions au client Léa Windows. + + Le client se connecte une fois, reste connecté tant que la session vit. + Chaque action prête à être exécutée arrive comme event JSON. + Heartbeat 15s : maintient la connexion à travers NPM/NoMachine/proxies. + Reconnexion automatique côté sseclient-py. + """ + + queue = await _sse_subscribe(session_id, machine_id) + + async def event_generator(): + # Au connect : si des actions sont déjà en queue (cas reconnect), + # les pousser immédiatement avant d'attendre. + try: + # Drain initial : récupérer ce qui est déjà dans _replay_queues + # pour cette machine et l'envoyer immédiatement. + initial = await _drain_pending_actions(session_id, machine_id) + for action in initial: + yield ServerSentEvent( + data=json.dumps(action), + event="action", + id=action.get("action_id"), + ) + _mark_retry_pending(action) + + while True: + if await request.is_disconnected(): + logger.info("SSE client %s/%s disconnect détecté", + session_id, machine_id) + break + + try: + # Attendre une notification (timeout = laisser is_disconnected + # check passer). 5 s = compromis trafic / réactivité. + signal = await asyncio.wait_for(queue.get(), timeout=5.0) + except asyncio.TimeoutError: + continue # repart sur is_disconnected check + if signal == "shutdown": + break + + # Récupérer la(les) action(s) à dispatcher. + # Réutilise toute la logique server-side existante (pause_for_human, + # extract_text, t2a_decision, condition…) — refactor `get_next_action` + # pour en extraire une coroutine `_resolve_next_visual_action()`. + action = await _resolve_next_visual_action(session_id, machine_id) + if action is None: + continue + + yield ServerSentEvent( + data=json.dumps(action), + event="action", + id=action.get("action_id"), + ) + _mark_retry_pending(action) + logger.info("[REPLAY] DISPATCH(SSE) action_id=%s", + action.get("action_id")) + + except asyncio.CancelledError: + logger.info("SSE %s/%s cancelled (server shutdown)", + session_id, machine_id) + raise + finally: + await _sse_unsubscribe(session_id, machine_id) + + return EventSourceResponse( + event_generator(), + ping=15, # ping toutes les 15 s (NPM/NoMachine OK) + ping_message_factory=lambda: ServerSentEvent(comment="hb"), + headers={"X-Accel-Buffering": "no"}, # bypass nginx/NPM buffer + ) +``` + +**Points clés :** +1. `EventSourceResponse(ping=15)` → heartbeat « `: hb\n\n` » toutes les 15 s. Tient les proxies NPM et NoMachine ouverts (idle timeout typique 60 s). +2. `X-Accel-Buffering: no` → désactive le buffering nginx/NPM (sinon l'event reste bloqué côté reverse-proxy jusqu'à ~16 KB). +3. `request.is_disconnected()` → détection instantanée de déconnexion Léa (NoMachine freeze, redémarrage Windows). Le serveur libère immédiatement les ressources. +4. `id=action_id` sur chaque event → permet au client de demander `Last-Event-ID` au reconnect (`sseclient-py` le gère automatiquement). À combiner avec le watchdog § 6 pour garantir zéro perte. +5. Coexistence avec pull-poll : ajouter env `RPA_REPLAY_TRANSPORT=sse|poll` côté serveur ET côté client (rollback 1-ligne en cas de pépin démo). + +**Hook côté queue producer** (à insérer là où `_replay_queues[session_id].append(action)` est appelé, p. ex. dans `start_replay`) : + +```python +# Après chaque append d'action visuelle dans _replay_queues : +sse_notify_new_action(session_id, machine_id) +``` + +--- + +## 5. Pseudo-code client Léa Windows (remplace polling actuel) + +À placer dans `agent_v0/agent_v1/network/streamer.py` ou un nouveau module `replay_subscriber.py`. Bibliothèque : `pip install sseclient-py>=1.8`. + +```python +# replay_subscriber.py — boucle de réception SSE côté Léa + +import json +import time +import requests +import sseclient +from ..config import API_TOKEN, STREAMING_ENDPOINT + +RECONNECT_BACKOFF = [1.0, 2.0, 5.0, 10.0, 30.0] + + +class ReplaySubscriber: + def __init__(self, session_id, machine_id, on_action): + self.session_id = session_id + self.machine_id = machine_id + self.on_action = on_action # callback exécuteur + self.running = False + self._last_event_id = None + self._thread = None + + def start(self): + self.running = True + self._thread = threading.Thread(target=self._run, daemon=True) + self._thread.start() + + def stop(self): + self.running = False + + def _run(self): + attempt = 0 + while self.running: + try: + url = ( + f"{STREAMING_ENDPOINT}/api/v1/traces/stream/replay/events" + f"?session_id={self.session_id}&machine_id={self.machine_id}" + ) + headers = { + "Authorization": f"Bearer {API_TOKEN}", + "Accept": "text/event-stream", + "Cache-Control": "no-cache", + } + if self._last_event_id: + headers["Last-Event-ID"] = self._last_event_id + + # IMPORTANT : pas de read_timeout — c'est tout l'intérêt SSE. + # Le ping serveur 15 s + le TCP keepalive OS suffisent. + # On garde un connect_timeout pour ne pas bloquer si serveur down. + resp = requests.get( + url, headers=headers, stream=True, + timeout=(10, None), # (connect, read) ; read=None = infini + ) + if resp.status_code != 200: + raise RuntimeError(f"SSE refusé HTTP {resp.status_code}") + + attempt = 0 # reset backoff dès qu'on a une connexion stable + client = sseclient.SSEClient(resp) + for event in client.events(): + if not self.running: + break + if event.event == "action" and event.data: + action = json.loads(event.data) + if event.id: + self._last_event_id = event.id + # Exécuter via le callback (cascade replay existante) + try: + result = self.on_action(action) + except Exception as e: + logger.exception("on_action levé : %s", e) + result = {"success": False, "error": str(e)} + # Reporter résultat — endpoint inchangé + self._post_result(action, result) + # event.event == "ping" → ignorer, c'est le heartbeat + + except (requests.ConnectionError, requests.Timeout, RuntimeError) as e: + if not self.running: + break + delay = RECONNECT_BACKOFF[min(attempt, len(RECONNECT_BACKOFF)-1)] + logger.warning("SSE déconnecté (%s) → retry dans %ss", e, delay) + time.sleep(delay) + attempt += 1 + + def _post_result(self, action, result): + """Réutilise l'endpoint POST /replay/result existant (inchangé).""" + # … identique à _report_action_result actuel d'executor.py +``` + +**Compatibilité avec `agent_frozen` :** côté Léa cliente, l'ajout est un nouveau module (pas un patch de code chaud). Modifier `main.py` pour instancier `ReplaySubscriber` au lieu du polling, sous flag env `RPA_REPLAY_TRANSPORT`. Redéploiement SCP vers `dom@192.168.1.11` requis (cf. `feedback_scp_auto_modif_client_windows.md`). + +--- + +## 6. Watchdog `_retry_pending` (à brancher MAINTENANT, indépendamment de SSE) + +**Justification :** même avec SSE, un crash entre le `yield` serveur et le `POST /replay/result` client peut laisser une action dans `_retry_pending` sans report. Le watchdog est l'ultime filet. + +**À insérer dans `api_stream.py`** au démarrage de l'app (event `startup`) : + +```python +# api_stream.py — watchdog d'orphelins + +_RETRY_WATCHDOG_INTERVAL_S = 10.0 +_RETRY_ORPHAN_THRESHOLD_S = 30.0 # action sans report depuis 30 s → re-dispatch +_RETRY_MAX_RESENDS = 2 # éviter boucle infinie + + +async def _retry_pending_watchdog(): + """Re-dispatche les actions dispatched depuis > 30s sans report. + Idempotence garantie par report_action_result qui pop _retry_pending.""" + while True: + try: + await asyncio.sleep(_RETRY_WATCHDOG_INTERVAL_S) + now = time.time() + orphans = [] + # Snapshot pour éviter mutation concurrente + async with _async_replay_lock(): + for aid, info in list(_retry_pending.items()): + dispatched_at = info.get("dispatched_at", 0) + resent = info.get("resent_count", 0) + if dispatched_at == 0: + continue + if now - dispatched_at < _RETRY_ORPHAN_THRESHOLD_S: + continue + if resent >= _RETRY_MAX_RESENDS: + logger.error( + "[BUS] lea:dispatch_orphan_giveup action_id=%s " + "resent=%d age=%.1fs", + aid, resent, now - dispatched_at, + ) + _retry_pending.pop(aid, None) + continue + orphans.append((aid, info)) + + for aid, info in orphans: + action = info["action"] + session_id = action.get("session_id") or info.get("session_id") + if not session_id: + continue + async with _async_replay_lock(): + q = _replay_queues.setdefault(session_id, []) + # Repush en TÊTE (sinon ordre des steps cassé) + q.insert(0, action) + info["resent_count"] = info.get("resent_count", 0) + 1 + info["dispatched_at"] = 0 # sera reset au prochain DISPATCH + logger.warning( + "[BUS] lea:dispatch_orphan_resent action_id=%s " + "resent=%d session=%s", + aid, info["resent_count"], session_id, + ) + # Si SSE actif : notifier le subscriber + machine_id = info.get("machine_id", "default") + sse_notify_new_action(session_id, machine_id) + + except asyncio.CancelledError: + break + except Exception: + logger.exception("Watchdog _retry_pending levé — continue") + + +@app.on_event("startup") +async def _start_retry_watchdog(): + if os.environ.get("RPA_RETRY_WATCHDOG_ENABLED", "1") == "1": + app.state._retry_watchdog_task = asyncio.create_task( + _retry_pending_watchdog() + ) + logger.info("Watchdog _retry_pending démarré (orphan>%.0fs, every %.0fs)", + _RETRY_ORPHAN_THRESHOLD_S, _RETRY_WATCHDOG_INTERVAL_S) +``` + +**Modifications minimales requises** pour que le watchdog ait les bonnes infos : +1. Au point de dispatch (ligne ~3354 actuelle), ajouter `dispatched_at: time.time()` et `session_id` / `machine_id` dans `_retry_pending[action_id]`. +2. Dans `report_action_result` (ligne 3491), `pop` reste la clé d'idempotence — **aucun changement**, le code actuel fonctionne déjà parfaitement avec resends. + +**Concurrence avec un report tardif :** si le client renvoie un report APRÈS le re-dispatch (race), le second `pop` retourne `None` → `report_action_result` répond `{"status": "no_active_replay"}` (ligne 3488) ou un retry de retry — tous les chemins sont déjà idempotents grâce au `pop`. + +**Kill-switch :** `RPA_RETRY_WATCHDOG_ENABLED=0` (mode legacy). Pattern aligné sur QW1/QW2/QW4 (cf. `LESSONS_LEARNED_GHT_2026-05.md` §kill-switches). + +--- + +## 7. Plan de migration en 3 étapes + +### Étape 1 — Watchdog SEUL (2-4 h, déployable demain) +- Ajouter `_retry_pending_watchdog` + champs `dispatched_at/session_id/machine_id` dans `_retry_pending`. +- Flag `RPA_RETRY_WATCHDOG_ENABLED=1` par défaut, désactivable. +- Garder le client à `read_timeout=30` (déjà recommandé quick fix démo). +- **Effet immédiat :** plus aucune action perdue silencieusement, même sur le transport pull-poll actuel. +- Test : injecter un sleep 35 s avant `report_action_result` côté client → vérifier `lea:dispatch_orphan_resent` dans logs et que l'action ré-arrive. + +### Étape 2 — Endpoint SSE serveur en parallèle (1 j) +- Ajouter `/api/v1/traces/stream/replay/events` (code §4). +- Refactoriser `get_next_action` pour extraire `_resolve_next_visual_action(session_id, machine_id)` réutilisable depuis le SSE (DRY — c'est la même cascade pause_for_human / extract_text / t2a_decision / clic conditionnel). +- Tests serveur : `pytest tests/integration/test_stream_processor.py` + nouveau `test_sse_dispatch.py` (connect, push, disconnect, ping, Last-Event-ID). +- Le pull-poll continue de tourner en parallèle (zéro impact démo). + +### Étape 3 — Bascule client Léa (1 j + redéploiement SCP) +- Ajouter `replay_subscriber.py` côté agent_v1. +- Flag `RPA_REPLAY_TRANSPORT=sse|poll` côté client, valeur par défaut = `poll` pendant 1 semaine, puis bascule à `sse` après runs validés. +- SCP vers `dom@192.168.1.11` : `network/streamer.py`, `network/replay_subscriber.py`, `core/executor.py`, `main.py`. +- Test E2E sur Demo_urgence_3_db (46 steps) avec NoMachine freeze simulé (cf. `feedback_agent_frozen.md`) → vérifier reconnect SSE + Last-Event-ID résume sans perte. +- Si OK : flag par défaut `sse` ; le watchdog reste actif comme filet. + +--- + +## 8. Risques et tests E2E + +### Risques techniques +| Risque | Mitigation | +|---|---| +| NoMachine 9.5.7 coupe la connexion idle même avec ping 15 s | `ping=10` au lieu de 15, et `tcp_keepalive` côté socket Python (`setsockopt SO_KEEPALIVE`) | +| NPM reverse-proxy bufferise SSE | `X-Accel-Buffering: no` + vérifier `proxy_buffering off` dans la conf NPM `lea.labs.laurinebazin.design` | +| Léa Windows freeze longue (>2 min) → SSE socket morte mais OS pense vivante | watchdog côté serveur tue la connexion si pas d'ack `report_action_result` reçu depuis 60 s (à ajouter) | +| Double-dispatch (race watchdog + reconnect Last-Event-ID) | idempotence côté client : `if action_id in self._processed: skip` (set bounded LRU 256) | +| Gemma cloud 503 (vécu 12 mai) bloque t2a_decision >> 30 s | watchdog re-pushe → mais le 2e essai re-bloque. Plafond `_RETRY_MAX_RESENDS=2` puis abandon → pause supervisée | +| Drift exemption template ≥0.95 / hybrid ≥0.80 (contournement actif) | aucun impact — c'est une logique de resolve, pas de transport | +| Fallback heartbeat capture <1200×800 (contournement) | aucun impact — c'est sur l'upload, pas le dispatch | + +### Tests E2E à passer avant bascule +1. **Smoke** : démarrer replay 5 steps, vérifier dispatch SSE et arrivée chez client. +2. **Long action serveur** : step avec `extract_text` 8 s puis `click` — l'action `click` doit arriver SANS perte (le test 8 mai en a perdu 9). +3. **Déconnexion brutale** : `taskkill /F /IM python.exe` côté Léa puis relancer → SSE reconnect + Last-Event-ID résume sans re-dispatcher les actions déjà acquittées. +4. **NoMachine freeze simulé** : couper VPN 90 s → reconnect, vérifier que les actions empilées arrivent en rafale propre. +5. **Watchdog isolé** : passer `RPA_REPLAY_TRANSPORT=poll` + `RPA_RETRY_WATCHDOG_ENABLED=1`, faire dropper le report manuellement (sleep 35 s avant POST `/replay/result`) → vérifier resend + idempotence. +6. **Démo complète Demo_urgence_3_db** (46 steps, MOREL Catherine UHCD) : 0 action perdue, comparaison logs avant/après. + +### Liens avec autres axes +- **AXE B2 (Validator)** : un Validator strict (vérif sémantique post-clic) n'a de sens que si on est sûr que toutes les actions arrivent. **B1 est prérequis de B2.** +- **AXE B4 (ORA — Observe Reason Act)** : ORA pousse des actions dans la queue exactement comme le replay classique. Le SSE bénéficie à ORA gratuitement (pas de refacto supplémentaire). **B1 dé-risque B4.** + +--- + +## 9. Sources + +### SSE / FastAPI +- [sse-starlette GitHub](https://github.com/sysid/sse-starlette) — référence implémentation +- [sse-starlette ping interval issue #16](https://github.com/sysid/sse-starlette/issues/16) +- [FastAPI tutorial SSE](https://fastapi.tiangolo.com/tutorial/server-sent-events/) +- [Real-Time Notifications Python FastAPI SSE](https://medium.com/@inandelibas/real-time-notifications-in-python-using-sse-with-fastapi-1c8c54746eb7) +- [Stop streaming response when client disconnects](https://github.com/fastapi/fastapi/discussions/7572) +- [Server-Sent Events Beat WebSockets for 95% of Real-Time Apps](https://dev.to/polliog/server-sent-events-beat-websockets-for-95-of-real-time-apps-heres-why-a4l) + +### WebSocket / FastAPI +- [FastAPI WebSockets doc](https://fastapi.tiangolo.com/advanced/websockets/) +- [Weaponizing Real Time FastAPI](https://blog.greeden.me/en/2025/10/28/weaponizing-real-time-websocket-sse-notifications-with-fastapi-connection-management-rooms-reconnection-scale-out-and-observability/) +- [WebSocket Heartbeat Ping/Pong](https://websocket.org/guides/heartbeat/) +- [Handling WebSocket Disconnections FastAPI](https://hexshift.medium.com/handling-websocket-disconnections-gracefully-in-fastapi-9f0a1de365da) + +### HTTP/2 status Python +- [Uvicorn HTTP/2 Issue #47](https://github.com/Kludex/uvicorn/issues/47) — non supporté +- [Gunicorn HTTP/2 guide](https://gunicorn.org/guides/http2/) — server push deprecated +- [The Three Python ASGI Servers](https://dev.to/bowmanjd/the-three-python-asgi-servers-5447) + +### Frameworks externes +- [Anthropic computer-use-demo loop.py](https://github.com/anthropics/claude-quickstarts/blob/main/computer-use-demo/computer_use_demo/loop.py) +- [OpenAI Computer Use docs](https://developers.openai.com/api/docs/guides/tools-computer-use) +- [OpenAI Operator Explained](https://anchorbrowser.io/blog/how-openai-operator-works-with-ai-agents) +- [Skyvern GitHub](https://github.com/Skyvern-AI/skyvern) +- [Skyvern 2.0 architecture blog](https://www.skyvern.com/blog/skyvern-2-0-state-of-the-art-web-navigation-with-85-8-on-webvoyager-eval/) +- [browser-use Playwright to CDP](https://browser-use.com/posts/playwright-to-cdp) +- [browser-use cdp-use repo](https://github.com/browser-use/cdp-use) +- [Playwright MCP 2026 architecture](https://testquality.com/playwright-test-agents-mcp-architecture-2026/) + +### Client SSE Python +- [sseclient-py PyPI](https://pypi.org/project/sseclient/) +- [requests-sse PyPI](https://pypi.org/project/requests-sse/) +- [LaunchDarkly Python SSE client](https://launchdarkly-sse-client-library.readthedocs.io/en/latest/) + +### Patterns retry / idempotence / orphans +- [ARQ retry doc](https://arq-docs.helpmanual.io/) +- [Building Resilient Task Queues FastAPI ARQ](https://davidmuraya.com/blog/fastapi-arq-retries/) +- [Queue-Based Exponential Backoff](https://dev.to/andreparis/queue-based-exponential-backoff-a-resilient-retry-pattern-for-distributed-systems-37f3) +- [Interrupted Asynchronous Task Problem](https://medium.com/picus-security-engineering/the-interrupted-asynchronous-task-problem-and-solution-with-python-rq-435f1a597631) + +### Proxy / NoMachine +- [SSE vs WebSocket Agent Readiness](https://agenthermes.ai/blog/sse-websocket-agent-readiness) +- [Troubleshooting SSE Multi-Service](https://medium.com/@wang645788/troubleshooting-server-sent-events-sse-in-a-multi-service-architecture-5084ce155ea0) + +--- + +*Document destiné à servir de base de décision avant chiffrage final et implémentation. Lecture seule sur le code, aucune modification. À discuter avec Dom avant toute bascule.* diff --git a/docs/recherche/AXE_B2_DEEP_VALIDATOR.md b/docs/recherche/AXE_B2_DEEP_VALIDATOR.md new file mode 100644 index 000000000..b8d5363f6 --- /dev/null +++ b/docs/recherche/AXE_B2_DEEP_VALIDATOR.md @@ -0,0 +1,1350 @@ +# AXE B2 — Deep dive Validator : implémentation production-ready + +**Date :** 2026-05-24 +**Auteur :** agent recherche dispatché (Claude Opus 4.7 1M) +**Statut :** livrable de recherche, lecture seule, **AUCUNE modification de code**. +**Parent :** `docs/recherche/AXE_B2_VALIDATOR_PATTERN.md` (architecture déjà posée, Skyvern verbatim). +**Frères :** `AXE_A4_OCR_TEMPLATE_PHASH.md` (fournit `OcrRoiChecker` et SSIM-ROI). +**Périmètre :** prendre le squelette de B2 et le rendre **collable** : code complet, tests, wiring précis, latences mesurables, reproduction offline du bug step 10. + +--- + +## 1. TL;DR — recommandation immédiatement actionnable + +Le doc parent (`AXE_B2_VALIDATOR_PATTERN.md`) a posé l'architecture et copié verbatim Skyvern. Il manque **(a)** le code Python prêt à coller pour chaque Checker, **(b)** le wiring précis dans `report_action_result` (qui appelle déjà `_replay_verifier.verify_with_critic` à `api_stream.py:3554`), **(c)** la repro offline du bug step 10 sur un PNG existant, **(d)** le test pytest qui prouve la fermeture du bug. + +**Effort total : 1 journée homme** pour livrer une feature flag `RPA_VALIDATOR_V2_ENABLED=false` par défaut, qui : + +1. Réutilise `verify_with_critic` existant (déjà câblé, déjà testé), wrapping inchangé. +2. **Ajoute un seul check primaire** — `OcrRoiChecker` — devant le pipeline pixel-then-critic actuel. +3. **Reroute la sortie** : si `OcrRoiChecker` détecte un token suspect (`https`, `edge`, `chrome`, `.com`, `.fr`), retourne TERMINATE avec `failure_category=WRONG_APPLICATION` au lieu de continuer. +4. Plug dans `replay_state["results"]` au même format que `verification.to_dict()` existant. + +**Le bug step 10 est fermé par 80 lignes de Python.** Le reste de l'architecture (taxonomie complète, dispatcher de verdicts, `LlmJudgeChecker` séparé) est une amélioration P1 — utile, pas bloquante. + +**Dépendances** : +- **AXE_A4** (OCR ROI) : `OcrRoiChecker` réutilise `EasyOCR` déjà chargé par `core/grounding/title_verifier.py:140` (singleton GPU). Pas de coût d'init. +- **AXE_B1** (watchdog `_retry_pending`) : indépendant du Validator. Le watchdog corrige la cause primaire (HTTP timeout silencieux), le Validator corrige la cause aggravante (clic hors-zone validé success=True). +- **Chaîne D2** (popup/dialog) : sortie `failure_category=UNEXPECTED_DIALOG` → handoff vers DialogHandler (à câbler en P1). + +--- + +## 2. Architecture finale du package `core/validation/` + +### 2.1. Arborescence + +``` +core/validation/ +├── __init__.py # exports publics : Validator, Verdict, ValidationResult +├── result.py # dataclasses : Verdict, FailureCategory, ValidationResult +├── checker_base.py # Protocol ActionChecker + classe abstraite +├── validator.py # orchestrateur : route action_type → checkers, escalation +├── prompts.py # prompts français pour LlmJudgeChecker (Easily Assure context) +└── checkers/ + ├── __init__.py + ├── pixel_diff.py # wrapper ReplayVerifier.verify_action (existant) + ├── ocr_roi.py # NOUVEAU — résout bug step 10 + ├── title_bar.py # wrapper core/grounding/title_verifier.py (existant) + ├── json_schema.py # pydantic v2 pour extract_text/t2a_decision + ├── dialog_presence.py # (P1) cascade modaux VM + └── llm_judge.py # wrapper ReplayVerifier.verify_with_critic (existant) +``` + +**Rationale du package `core/validation/`** : le code n'est pas couplé à `agent_v0/server_v1/` (pas de FastAPI, pas de DB). Il est testable isolément (`pytest tests/unit/test_validator_*.py`). On reste cohérent avec `core/grounding/`, `core/execution/`, `core/auth/`. + +### 2.2. Interface `Checker` (Protocol) + +```python +# core/validation/checker_base.py +from __future__ import annotations +from typing import Any, Dict, Optional, Protocol, runtime_checkable + +@runtime_checkable +class ActionChecker(Protocol): + """Contrat d'un checker. Stateless si possible (modèles partagés en singleton).""" + name: str + budget_ms: float # latence cible (informative — pas de hard timeout ici) + + def check( + self, + action: Dict[str, Any], + result: Dict[str, Any], + screenshot_before: Optional[str], # base64 ou path + screenshot_after: Optional[str], + context: Dict[str, Any], + ) -> "ValidationResult": # forward ref (cycle) + ... +``` + +### 2.3. Responsabilités par fichier + +| Fichier | Responsabilité | Lignes (estim) | +|---|---|---| +| `result.py` | enums `Verdict`/`FailureCategory` + dataclass `ValidationResult` + `to_dict()` | 60 | +| `checker_base.py` | Protocol `ActionChecker` | 20 | +| `validator.py` | dispatcher action_type → checker list, escalation LLM si confidence < seuil | 100 | +| `prompts.py` | template f-string français Easily/DPI/tabs | 40 | +| `checkers/pixel_diff.py` | wrapper `ReplayVerifier.verify_action` → ValidationResult | 50 | +| `checkers/ocr_roi.py` | crop ROI + EasyOCR + match suspect tokens + match expected | 110 | +| `checkers/title_bar.py` | wrapper `TitleVerifier.verify_action` → ValidationResult | 60 | +| `checkers/json_schema.py` | pydantic v2 schemas pour extract_text/t2a_decision | 80 | +| `checkers/llm_judge.py` | wrapper `ReplayVerifier.verify_with_critic` → ValidationResult | 70 | + +**Total : ~590 LOC** pour le package complet. **~190 LOC** pour le MVP P0 (`result.py` + `checker_base.py` + `validator.py` + `ocr_roi.py`). + +--- + +## 3. Code complet de chaque Checker (production-ready) + +### 3.1. `core/validation/result.py` + +```python +# core/validation/result.py +"""Dataclasses du Validator — Verdict, FailureCategory, ValidationResult.""" +from __future__ import annotations +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, Dict, Optional + + +class Verdict(str, Enum): + """Trois verdicts possibles, calque sur Skyvern (complete/terminate/continue).""" + COMPLETE = "complete" # l'action a eu l'effet voulu → passer au step suivant + CONTINUE = "continue" # l'effet n'est pas encore visible → wait + recheck + TERMINATE = "terminate" # échec irrécupérable → pause supervisée + + +class FailureCategory(str, Enum): + """Classification des échecs (inspirée Skyvern 12-cat, restreinte à notre contexte).""" + WRONG_TARGET = "wrong_target" # clic ailleurs (ex: dans le mauvais tab) + WRONG_APPLICATION = "wrong_application" # clic dans bandeau Edge au lieu d'Easily — bug step 10 + NO_VISUAL_CHANGE = "no_visual_change" # action sans effet visible + UNEXPECTED_DIALOG = "unexpected_dialog" # popup imprévu (handoff DialogHandler) + OCR_TEXT_MISSING = "ocr_text_missing" # texte attendu absent de la ROI + SCHEMA_INVALID = "schema_invalid" # JSON/extract invalide + UI_LOADING = "ui_loading" # spinner détecté → wait + UNKNOWN = "unknown" + + +@dataclass +class ValidationResult: + """Résultat agrégé d'un check. Toujours sérialisable JSON.""" + verdict: Verdict + confidence: float # 0.0-1.0 + check_used: str # "ocr_roi" | "llm_judge" | "title_bar" | ... + elapsed_ms: float + reasoning: str = "" + failure_category: Optional[FailureCategory] = None + raw_evidence: Dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> Dict[str, Any]: + return { + "verdict": self.verdict.value, + "confidence": round(self.confidence, 3), + "check_used": self.check_used, + "elapsed_ms": round(self.elapsed_ms, 1), + "reasoning": self.reasoning, + "failure_category": ( + self.failure_category.value if self.failure_category else None + ), + "raw_evidence": self.raw_evidence, + } +``` + +### 3.2. `core/validation/checkers/pixel_diff.py` — pré-filtre 10 ms + +```python +# core/validation/checkers/pixel_diff.py +"""Wrapper du ReplayVerifier pixel existant — pré-filtre rapide.""" +from __future__ import annotations +import time +from typing import Any, Dict, Optional + +from core.validation.result import ValidationResult, Verdict, FailureCategory + + +class PixelDiffChecker: + name = "pixel_diff" + budget_ms = 15.0 + + def __init__(self, replay_verifier): + # Injection : on réutilise l'instance ReplayVerifier existante + # côté api_stream (_replay_verifier global). + self._rv = replay_verifier + + def check( + self, + action: Dict[str, Any], + result: Dict[str, Any], + screenshot_before: Optional[str], + screenshot_after: Optional[str], + context: Dict[str, Any], + ) -> ValidationResult: + t0 = time.time() + pr = self._rv.verify_action( + action=action, + result=result, + screenshot_before=screenshot_before, + screenshot_after=screenshot_after, + ) + elapsed = (time.time() - t0) * 1000 + + # Map pixel verdict → Validator verdict + if pr.suggestion == "continue" and pr.changes_detected: + verdict = Verdict.COMPLETE + conf = pr.confidence + fc = None + elif pr.suggestion == "retry": + verdict = Verdict.CONTINUE + conf = max(0.4, pr.confidence - 0.2) + fc = FailureCategory.NO_VISUAL_CHANGE + else: + verdict = Verdict.CONTINUE + conf = 0.3 + fc = None + + return ValidationResult( + verdict=verdict, + confidence=conf, + check_used=self.name, + elapsed_ms=elapsed, + reasoning=pr.detail, + failure_category=fc, + raw_evidence={ + "change_area_pct": pr.change_area_pct, + "local_change_pct": pr.local_change_pct, + }, + ) +``` + +### 3.3. `core/validation/checkers/ocr_roi.py` — résout le bug step 10 + +```python +# core/validation/checkers/ocr_roi.py +"""OcrRoiChecker — vérifie que le texte attendu apparaît dans la ROI cliquée. + +Spécifiquement conçu pour résoudre le bug step 10 (REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08) : +si on a cliqué pour 'Imagerie' mais que la ROI 60-120px autour du point cliqué +contient 'edge', 'https' ou un domaine, on a cliqué dans le bandeau navigateur. +""" +from __future__ import annotations +import time +import unicodedata +from typing import Any, Callable, Dict, Optional + +from core.validation.result import ValidationResult, Verdict, FailureCategory + + +def _strip_accents(s: str) -> str: + """NFKD + drop diacritics, robuste casse/accents pour matching tabs Easily.""" + return "".join( + c for c in unicodedata.normalize("NFKD", s) if not unicodedata.combining(c) + ).lower().strip() + + +class OcrRoiChecker: + name = "ocr_roi" + budget_ms = 200.0 + + # Tokens qui prouvent qu'on a cliqué dans le bandeau navigateur, pas dans l'app + SUSPECT_TOKENS = ( + "edge", "chrome", "firefox", "mozilla", "opera", + "http", "https", "www.", + ".com", ".fr", ".org", ".net", ".html", + "favoris", "favorite", "bookmark", + "barre d'adresse", "address bar", + "nouvel onglet", "new tab", + "sécurité windows", "windows security", + "user account control", "contrôle de compte", + ) + + def __init__( + self, + ocr_fn: Callable, # callable(PIL.Image) -> str (EasyOCR singleton) + radius_px: int = 80, # 80 = compromis recall/latence sur tabs Easily + suspect_min_confidence: float = 0.85, + expected_min_confidence: float = 0.90, + ): + self._ocr = ocr_fn + self._radius = radius_px + self._suspect_conf = suspect_min_confidence + self._expected_conf = expected_min_confidence + + def check( + self, + action: Dict[str, Any], + result: Dict[str, Any], + screenshot_before: Optional[str], + screenshot_after: Optional[str], + context: Dict[str, Any], + ) -> ValidationResult: + t0 = time.time() + + # 1. Récupération inputs (action peut être un click_anchor ou un type) + target_spec = action.get("target_spec") or {} + expected_text = ( + action.get("by_text") + or target_spec.get("by_text") + or context.get("expected_text") + or "" + ) + + # actual_position rapporté par l'agent (priorité), sinon coords d'action + actual_pos = result.get("actual_position") or {} + x_pct = ( + actual_pos.get("x_pct") + or action.get("x_pct") + or target_spec.get("x_pct") + ) + y_pct = ( + actual_pos.get("y_pct") + or action.get("y_pct") + or target_spec.get("y_pct") + ) + + if not screenshot_after or x_pct is None or y_pct is None or not expected_text: + return ValidationResult( + verdict=Verdict.CONTINUE, + confidence=0.2, + check_used=self.name, + elapsed_ms=(time.time() - t0) * 1000, + reasoning="ROI indéfinie (manque coords ou expected_text)", + ) + + # 2. Crop ROI + try: + from PIL import Image + img = self._load_image(screenshot_after) + except Exception as exc: + return ValidationResult( + verdict=Verdict.CONTINUE, + confidence=0.1, + check_used=self.name, + elapsed_ms=(time.time() - t0) * 1000, + reasoning=f"Erreur chargement image: {exc}", + ) + + w, h = img.size + cx, cy = int(float(x_pct) * w), int(float(y_pct) * h) + r = self._radius + roi = img.crop( + (max(0, cx - r), max(0, cy - r), min(w, cx + r), min(h, cy + r)) + ) + + # 3. OCR + try: + raw_text = self._ocr(roi) or "" + except Exception as exc: + return ValidationResult( + verdict=Verdict.CONTINUE, + confidence=0.1, + check_used=self.name, + elapsed_ms=(time.time() - t0) * 1000, + reasoning=f"Erreur OCR: {exc}", + ) + + text_norm = _strip_accents(raw_text) + expected_norm = _strip_accents(expected_text) + elapsed_ms = (time.time() - t0) * 1000 + + evidence = { + "roi_text": raw_text[:200], + "roi_bbox": [max(0, cx - r), max(0, cy - r), min(w, cx + r), min(h, cy + r)], + "expected": expected_text, + } + + # 4. Détection token suspect (priorité absolue — bug step 10) + for suspect in self.SUSPECT_TOKENS: + if suspect in text_norm and suspect not in expected_norm: + return ValidationResult( + verdict=Verdict.TERMINATE, + confidence=self._suspect_conf, + check_used=self.name, + elapsed_ms=elapsed_ms, + failure_category=FailureCategory.WRONG_APPLICATION, + reasoning=( + f"Token navigateur/système '{suspect}' dans ROI clic " + f"(attendu '{expected_text[:40]}') — cible hors-app" + ), + raw_evidence=evidence, + ) + + # 5. Match texte attendu + if expected_norm and expected_norm in text_norm: + return ValidationResult( + verdict=Verdict.COMPLETE, + confidence=self._expected_conf, + check_used=self.name, + elapsed_ms=elapsed_ms, + reasoning=f"Texte '{expected_text[:40]}' trouvé dans ROI", + raw_evidence=evidence, + ) + + # 6. Match partiel mot-à-mot (tolère ponctuation, accents partiels) + expected_tokens = [t for t in expected_norm.split() if len(t) > 2] + if expected_tokens: + hits = sum(1 for tok in expected_tokens if tok in text_norm) + ratio = hits / len(expected_tokens) + if ratio >= 0.5: + return ValidationResult( + verdict=Verdict.COMPLETE, + confidence=0.6 + 0.3 * ratio, + check_used=self.name, + elapsed_ms=elapsed_ms, + reasoning=f"Match partiel {hits}/{len(expected_tokens)} tokens", + raw_evidence=evidence, + ) + + # 7. Pas suspect, pas trouvé → escalation au LLM judge + return ValidationResult( + verdict=Verdict.CONTINUE, + confidence=0.4, + check_used=self.name, + elapsed_ms=elapsed_ms, + failure_category=FailureCategory.OCR_TEXT_MISSING, + reasoning=f"Texte '{expected_text[:40]}' non trouvé dans ROI", + raw_evidence=evidence, + ) + + @staticmethod + def _load_image(source: str): + """Charge PNG/JPEG depuis path ou base64 (réutilise ReplayVerifier).""" + from agent_v0.server_v1.replay_verifier import ReplayVerifier + return ReplayVerifier()._load_single_image(source) +``` + +### 3.4. `core/validation/checkers/title_bar.py` — wrapper existant + +```python +# core/validation/checkers/title_bar.py +"""Wrapper de core/grounding/title_verifier.TitleVerifier (existant, prod-ready).""" +from __future__ import annotations +import time +from typing import Any, Dict, Optional + +from core.validation.result import ValidationResult, Verdict, FailureCategory + + +class TitleBarChecker: + name = "title_bar" + budget_ms = 130.0 + + def __init__(self): + from core.grounding.title_verifier import TitleVerifier + self._tv = TitleVerifier() + + def check( + self, + action: Dict[str, Any], + result: Dict[str, Any], + screenshot_before: Optional[str], + screenshot_after: Optional[str], + context: Dict[str, Any], + ) -> ValidationResult: + t0 = time.time() + action_type = action.get("type", "") + + if not screenshot_before or not screenshot_after: + return ValidationResult( + verdict=Verdict.CONTINUE, confidence=0.2, + check_used=self.name, elapsed_ms=(time.time() - t0) * 1000, + reasoning="screenshots manquants", + ) + + from PIL import Image + from agent_v0.server_v1.replay_verifier import ReplayVerifier + rv = ReplayVerifier() + img_b = rv._load_single_image(screenshot_before) + img_a = rv._load_single_image(screenshot_after) + + verif = self._tv.verify_action(img_b, img_a, action_type) + elapsed = (time.time() - t0) * 1000 + + if verif.get("success"): + return ValidationResult( + verdict=Verdict.COMPLETE, + confidence=0.75 if verif.get("changed") else 0.5, + check_used=self.name, elapsed_ms=elapsed, + reasoning=verif.get("reason", ""), + raw_evidence={ + "title_before": verif.get("title_before", ""), + "title_after": verif.get("title_after", ""), + }, + ) + else: + return ValidationResult( + verdict=Verdict.TERMINATE, confidence=0.7, + check_used=self.name, elapsed_ms=elapsed, + failure_category=FailureCategory.WRONG_APPLICATION, + reasoning=verif.get("reason", ""), + raw_evidence={ + "title_before": verif.get("title_before", ""), + "title_after": verif.get("title_after", ""), + }, + ) +``` + +### 3.5. `core/validation/checkers/json_schema.py` — pydantic v2 + +```python +# core/validation/checkers/json_schema.py +"""JsonSchemaChecker — validation déterministe extract_text / t2a_decision.""" +from __future__ import annotations +import json +import time +from typing import Any, Dict, Literal, Optional + +from pydantic import BaseModel, Field, ValidationError, field_validator + +from core.validation.result import ValidationResult, Verdict, FailureCategory + + +class ExtractTextResult(BaseModel): + """Sortie attendue d'une action extract_text — texte non vide, langue plausible.""" + value: str = Field(min_length=1, max_length=50000) + + @field_validator("value") + @classmethod + def must_have_letters(cls, v: str) -> str: + if not any(c.isalpha() for c in v): + raise ValueError("aucune lettre — extract_text vraisemblablement vide") + return v + + +class T2aDecisionResult(BaseModel): + """Sortie attendue d'une action t2a_decision (JSON strict).""" + decision: Literal["UHCD", "FORFAIT", "FORFAIT_URGENCE", "NA", "INCONNU"] + justification: str = Field(min_length=10, max_length=5000) + confidence: Optional[float] = Field(default=None, ge=0.0, le=1.0) + + +_SCHEMA_BY_ACTION = { + "extract_text": ExtractTextResult, + "extract_text_scroll": ExtractTextResult, + "t2a_decision": T2aDecisionResult, +} + + +class JsonSchemaChecker: + name = "json_schema" + budget_ms = 10.0 + + def check( + self, + action: Dict[str, Any], + result: Dict[str, Any], + screenshot_before: Optional[str], + screenshot_after: Optional[str], + context: Dict[str, Any], + ) -> ValidationResult: + t0 = time.time() + action_type = action.get("type", "") + schema_cls = _SCHEMA_BY_ACTION.get(action_type) + + if schema_cls is None: + return ValidationResult( + verdict=Verdict.COMPLETE, confidence=0.5, + check_used=self.name, elapsed_ms=(time.time() - t0) * 1000, + reasoning=f"Pas de schema pour action_type={action_type} — skip", + ) + + # Extract payload depuis result (selon convention serveur) + payload = result.get("value") or result.get("extracted") or result + if isinstance(payload, str): + try: + payload = json.loads(payload) + except json.JSONDecodeError: + payload = {"value": payload} if action_type.startswith("extract_text") else payload + + try: + validated = schema_cls.model_validate(payload) + return ValidationResult( + verdict=Verdict.COMPLETE, confidence=0.95, + check_used=self.name, + elapsed_ms=(time.time() - t0) * 1000, + reasoning=f"Schema {schema_cls.__name__} validé", + raw_evidence={"validated_keys": list(validated.model_dump().keys())}, + ) + except ValidationError as ve: + return ValidationResult( + verdict=Verdict.TERMINATE, confidence=0.9, + check_used=self.name, + elapsed_ms=(time.time() - t0) * 1000, + failure_category=FailureCategory.SCHEMA_INVALID, + reasoning=f"Schema invalide: {ve.errors()[:2]}", + raw_evidence={"errors": ve.errors()}, + ) +``` + +### 3.6. `core/validation/checkers/llm_judge.py` — wrapper escalation + +```python +# core/validation/checkers/llm_judge.py +"""LlmJudgeChecker — escalation VLM via ReplayVerifier.verify_with_critic. + +Réutilise le pipeline VLM existant (gemma4:e4b, port 11435). +Choix gemma4 vs Qwen3-VL : gemma4 retenu par BENCH_SAFETY_CHECKS_2026-05-06 +(46% détection vs 0% Qwen3-VL qui ignore format=json Ollama). +""" +from __future__ import annotations +import time +from typing import Any, Dict, Optional + +from core.validation.result import ValidationResult, Verdict, FailureCategory + + +class LlmJudgeChecker: + name = "llm_judge" + budget_ms = 3000.0 + + def __init__(self, replay_verifier): + self._rv = replay_verifier + + def check( + self, + action: Dict[str, Any], + result: Dict[str, Any], + screenshot_before: Optional[str], + screenshot_after: Optional[str], + context: Dict[str, Any], + ) -> ValidationResult: + t0 = time.time() + expected = context.get("expected_result") or action.get("expected_result", "") + intention = context.get("action_intention") or action.get("intention", "") + workflow_ctx = context.get("workflow_context", "") + + if not expected: + return ValidationResult( + verdict=Verdict.CONTINUE, confidence=0.2, + check_used=self.name, elapsed_ms=(time.time() - t0) * 1000, + reasoning="Pas d'expected_result fourni — LLM judge skip", + ) + + critic = self._rv.verify_with_critic( + action=action, + result=result, + screenshot_before=screenshot_before, + screenshot_after=screenshot_after, + expected_result=expected, + action_intention=intention, + workflow_context=workflow_ctx, + ) + elapsed = (time.time() - t0) * 1000 + + if critic.semantic_verified is True: + return ValidationResult( + verdict=Verdict.COMPLETE, + confidence=max(critic.confidence, 0.7), + check_used=self.name, elapsed_ms=elapsed, + reasoning=critic.semantic_detail or critic.detail, + raw_evidence={ + "pixel_change_pct": critic.change_area_pct, + "semantic_verified": True, + }, + ) + elif critic.semantic_verified is False: + return ValidationResult( + verdict=Verdict.TERMINATE, + confidence=0.8, + check_used=self.name, elapsed_ms=elapsed, + failure_category=FailureCategory.WRONG_TARGET, + reasoning=critic.semantic_detail or critic.detail, + raw_evidence={"semantic_verified": False}, + ) + else: + # VLM indispo ou non parsable → incertain, on continue prudemment + return ValidationResult( + verdict=Verdict.CONTINUE, + confidence=0.4, + check_used=self.name, elapsed_ms=elapsed, + reasoning=critic.detail or "VLM indisponible", + ) +``` + +### 3.7. `core/validation/validator.py` — orchestrateur + +```python +# core/validation/validator.py +"""Validator — orchestrateur : route action_type → checkers, gère escalation.""" +from __future__ import annotations +import logging +from typing import Any, Dict, List, Optional + +from core.validation.checker_base import ActionChecker +from core.validation.result import ValidationResult, Verdict + +logger = logging.getLogger(__name__) + + +class Validator: + """Dispatcher : un action_type → liste de checkers ordonnés. + + Logique de décision : + - Si le premier checker rend COMPLETE avec conf >= seuil_accept → return + - Si TERMINATE avec conf haute → return (escalation pause supervisée) + - Si CONTINUE / conf basse → essayer le checker suivant + - Si tous CONTINUE → escalation LLM judge si fourni + """ + + def __init__( + self, + checkers: Dict[str, List[ActionChecker]], + default_checkers: Optional[List[ActionChecker]] = None, + escalation_checker: Optional[ActionChecker] = None, + accept_confidence: float = 0.7, + escalate_below_confidence: float = 0.55, + ): + self._checkers = checkers + self._default = default_checkers or [] + self._escalation = escalation_checker + self._accept = accept_confidence + self._escalate_below = escalate_below_confidence + + def validate( + self, + action: Dict[str, Any], + result: Dict[str, Any], + screenshot_before: Optional[str] = None, + screenshot_after: Optional[str] = None, + context: Optional[Dict[str, Any]] = None, + ) -> ValidationResult: + ctx = context or {} + action_type = action.get("type", "") + candidates = self._checkers.get(action_type, self._default) + + last: Optional[ValidationResult] = None + for checker in candidates: + try: + res = checker.check(action, result, screenshot_before, screenshot_after, ctx) + except Exception as exc: + logger.warning("Validator: checker %s a planté: %s", checker.name, exc) + continue + last = res + logger.info( + "[VALIDATOR] check=%s verdict=%s conf=%.2f elapsed=%.0fms reasoning=%s", + res.check_used, res.verdict.value, res.confidence, + res.elapsed_ms, res.reasoning[:80], + ) + # Verdict net + confiance haute → on prend + if res.confidence >= self._accept and res.verdict != Verdict.CONTINUE: + return res + + # Escalation LLM judge si confiance trop basse + if ( + self._escalation + and last is not None + and last.confidence < self._escalate_below + ): + logger.info( + "[VALIDATOR] escalation LLM (last conf=%.2f < %.2f)", + last.confidence, self._escalate_below, + ) + try: + esc = self._escalation.check( + action, result, screenshot_before, screenshot_after, ctx + ) + # LLM tranche, sa confidence est plafonnée à 0.9 par construction + return esc + except Exception as exc: + logger.warning("Validator: escalation LLM a planté: %s", exc) + + # Fallback : dernier résultat ou CONTINUE neutre + return last or ValidationResult( + verdict=Verdict.CONTINUE, + confidence=0.3, + check_used="no_checker", + elapsed_ms=0.0, + reasoning="Aucun checker disponible pour action_type=" + action_type, + ) +``` + +--- + +## 4. Matrice action → check finale (avec latence cible) + +Aligné avec `_ALLOWED_ACTION_TYPES` (`replay_engine.py:35-48`) et `reference_vwb_action_types.md`. + +| Action VWB | Checker primaire | Escalation | Latence cible cumulée | +|---|---|---|---| +| `click` (depuis click_anchor) | **OcrRoiChecker** | LlmJudgeChecker si conf<0.55 | 80 ms + 2.5 s rare | +| `double_click` (double_click_anchor) | TitleBarChecker → OcrRoiChecker | LlmJudgeChecker | 200 ms + 2.5 s rare | +| `right_click` | PixelDiffChecker (menu attendu) | OcrRoiChecker sur menu | 15 ms + 80 ms | +| `type` | OcrRoiChecker (radius 120 px sur input) | — | 100 ms | +| `key_combo` | TitleBarChecker | LlmJudgeChecker si Ctrl+nav | 130 ms + 2.5 s rare | +| `scroll` | PixelDiffChecker | — | 15 ms | +| `wait` / `verify_screen` | PixelDiffChecker (no_change attendu) | — | 15 ms | +| `extract_text` / `extract_text_scroll` | **JsonSchemaChecker** | LlmJudgeChecker si len<50 | 10 ms + 2.5 s rare | +| `extract_table` | JsonSchemaChecker (rows ≥ 1) | — | 10 ms | +| `t2a_decision` | **JsonSchemaChecker** strict | — | 10 ms | +| `pause_for_human` | (déjà QW4 ChecklistPanel — skip) | — | 0 ms | +| `screenshot_evidence` | TitleBarChecker (app correcte) | — | 130 ms | +| `paste_and_execute` | PixelDiffChecker (input rempli) | OcrRoiChecker | 15 ms + 80 ms rare | + +**Budget total pour démo 46 steps MOREL** : +- 30 clicks × 80 ms = **2.4 s** +- 8 extract_text × 10 ms = **80 ms** +- 4 t2a_decision × 10 ms = **40 ms** +- 4 key_combo × 130 ms = **520 ms** +- Escalations LLM (~3 fois) × 2500 ms = **7.5 s** +- **Total ajouté ≤ 11 s** sur 46 steps. Acceptable face aux 30-60 s gagnés en évitant un blocage step 10 → pause + reprise manuelle (33 s observés). + +--- + +## 5. Verdict taxonomy + routing (dispatcher post-validation) + +```python +# Pseudocode à insérer dans api_stream.report_action_result après la +# validation Validator V2 (cf. §6 wiring) + +from core.validation.result import Verdict, FailureCategory + +def route_verdict( + verdict_result: ValidationResult, + action_id: str, + replay_state: Dict[str, Any], +) -> Dict[str, Any]: + """Convertit un verdict Validator en action serveur.""" + v = verdict_result.verdict + fc = verdict_result.failure_category + + if v == Verdict.COMPLETE: + return {"action": "continue", "override_success": True} + + if v == Verdict.CONTINUE: + # Re-checker après wait court (UI loading, animation) + return { + "action": "schedule_recheck", + "after_ms": 1500, + "max_rechecks": 2, + } + + # v == TERMINATE — routing selon failure_category + if fc == FailureCategory.WRONG_APPLICATION: + # Bug step 10 : pause supervisée, l'humain reprend la main + return { + "action": "enter_paused_state", + "reason": "wrong_application", + "evidence": verdict_result.to_dict(), + "override_success": False, + } + if fc == FailureCategory.WRONG_TARGET: + # Retry 1 fois avec re-resolve (cascade visuelle complète) + return { + "action": "retry_with_reresolve", + "max_retries": 1, + "override_success": False, + } + if fc == FailureCategory.UNEXPECTED_DIALOG: + # Handoff vers DialogHandler (chaîne D2) + return { + "action": "handoff_dialog_handler", + "override_success": False, + } + if fc == FailureCategory.SCHEMA_INVALID: + # extract_text/t2a_decision invalide → pause supervisée + return { + "action": "enter_paused_state", + "reason": "schema_invalid", + "evidence": verdict_result.to_dict(), + "override_success": False, + } + # NO_VISUAL_CHANGE, OCR_TEXT_MISSING, UNKNOWN → retry simple + return { + "action": "retry_with_reresolve", + "max_retries": 1, + "override_success": False, + } +``` + +--- + +## 6. Wiring précis dans `api_stream.py:3447` (diff unified) + +Le point d'insertion est précisément après le bloc `verify_with_critic` existant (`api_stream.py:3554-3582`). On ne casse rien : la nouvelle couche est *en plus*, derrière `RPA_VALIDATOR_V2_ENABLED`. + +### 6.1. Diff proposé (à NE PAS appliquer en chaud) + +```diff +--- a/agent_v0/server_v1/api_stream.py ++++ b/agent_v0/server_v1/api_stream.py +@@ -3447,6 +3447,18 @@ async def report_action_result(report: ReplayResultReport): + session_id = report.session_id + action_id = report.action_id + ++ # ============================================================ ++ # VALIDATOR V2 (feature-flag) — init lazy singleton ++ # ============================================================ ++ global _validator_v2 ++ _RPA_VALIDATOR_V2 = os.environ.get("RPA_VALIDATOR_V2_ENABLED", "false").lower() in {"true", "1", "yes"} ++ if _RPA_VALIDATOR_V2 and _validator_v2 is None: ++ from core.validation.validator import Validator ++ from core.validation.checkers.ocr_roi import OcrRoiChecker ++ from core.validation.checkers.llm_judge import LlmJudgeChecker ++ from core.grounding.title_verifier import TitleVerifier ++ _tv = TitleVerifier() ++ _ocr_fn = _tv._get_ocr() # singleton EasyOCR partagé ++ _validator_v2 = Validator( ++ checkers={ ++ "click": [OcrRoiChecker(ocr_fn=_ocr_fn, radius_px=80)], ++ "type": [OcrRoiChecker(ocr_fn=_ocr_fn, radius_px=120)], ++ }, ++ escalation_checker=LlmJudgeChecker(_replay_verifier), ++ accept_confidence=0.7, ++ escalate_below_confidence=0.55, ++ ) ++ + # [REPLAY] log structuré d'arrivée du rapport agent + ... +@@ -3580,6 +3592,40 @@ async def report_action_result(report: ReplayResultReport): + async with _async_replay_lock(): + replay_state["_last_screenshot_before"] = screenshot_after + ++ # ============================================================ ++ # VALIDATOR V2 — couche additionnelle (kill-switch RPA_VALIDATOR_V2_ENABLED) ++ # ============================================================ ++ validator_v2_result = None ++ if _RPA_VALIDATOR_V2 and report.success and screenshot_after and not skip_verify: ++ try: ++ action_dict = original_action or {"type": "unknown", "action_id": action_id} ++ result_dict = { ++ "success": report.success, ++ "error": report.error, ++ "actual_position": report.actual_position, ++ } ++ v2_ctx = { ++ "expected_result": (original_action or {}).get("expected_result", ""), ++ "action_intention": (original_action or {}).get("intention", ""), ++ "workflow_context": f"step {replay_state.get('completed_actions', 0)+1}/{len(replay_state.get('actions', []))}", ++ "expected_text": (original_action or {}).get("target_spec", {}).get("by_text", ""), ++ } ++ validator_v2_result = _validator_v2.validate( ++ action=action_dict, result=result_dict, ++ screenshot_before=screenshot_before, ++ screenshot_after=screenshot_after, ++ context=v2_ctx, ++ ) ++ # Override success si Validator V2 dit TERMINATE haute confiance ++ from core.validation.result import Verdict ++ if validator_v2_result.verdict == Verdict.TERMINATE and validator_v2_result.confidence >= 0.7: ++ logger.warning( ++ "[VALIDATOR_V2] override agent_success=True → False (verdict=%s reason=%s)", ++ validator_v2_result.verdict.value, validator_v2_result.reasoning[:120], ++ ) ++ report.success = False # type: ignore[misc] ++ report.error = report.error or f"validator_v2_terminate: {validator_v2_result.failure_category.value if validator_v2_result.failure_category else 'unknown'}" ++ except Exception as exc: ++ logger.warning("Validator V2 a échoué (non bloquant): %s", exc) ++ + # [REPLAY] log structuré de la décision de vérification +@@ -3612,6 +3658,8 @@ async def report_action_result(report: ReplayResultReport): + "verification": verification.to_dict() if verification else None, ++ "validator_v2": validator_v2_result.to_dict() if validator_v2_result else None, + "resolution_method": report.resolution_method, + "resolution_score": report.resolution_score, +``` + +**Effet observable** : +- Quand `RPA_VALIDATOR_V2_ENABLED=false` (défaut) : **aucun changement**, le pipeline existant tourne. +- Quand `=true` : un verdict TERMINATE conf≥0.7 override `report.success` à False → le retry serveur existant se déclenche (déjà câblé lignes ~3700+). En cas de WRONG_APPLICATION le routing du §5 entre en pause supervisée (à implémenter en P1, pour P0 le simple override suffit à attraper le bug). + +### 6.2. Init lazy singleton + +L'instanciation du `Validator` est **lazy** (premier appel à `report_action_result`) pour éviter de charger EasyOCR (~3 s) au boot du serveur — utile aussi si le flag est désactivé pour éviter la consommation VRAM. + +`_validator_v2: Optional[Validator] = None` à déclarer globalement avec les autres singletons (`_replay_verifier`, `_audit_trail`). + +--- + +## 7. Reproduction offline du bug step 10 + +### 7.1. Screenshot disponible + +Confirmé présent : `/home/dom/ai/rpa_vision_v3/visual_workflow_builder/backend/data/anchors/anchor_0438bd2d9bdd_1778161174_full.png` + +C'est la capture pleine fenêtre 2560×1600 contenant la barre de tabs Easily (cf. `REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` §4 et `AXE_A4 §4.3`). + +Les coordonnées rapportées par OCR-DIRECT pour les 3 tabs collisionnent à `(0.2305, 0.2805)` → en pixels = `(590, 449)`. C'est précisément le point qui tombe **dans la URL bar Edge** au lieu de l'onglet Easily. + +### 7.2. Snippet repro complet + +```python +# scripts/repro_bug_step10_validator.py +"""Reproduction offline du bug step 10 — démonstration OcrRoiChecker en isolation. + +Charge la capture de référence, simule un clic à (0.23, 0.28) hors-zone, +vérifie que le Validator détecte le faux clic (token 'https' / '.com' dans la ROI). + +Usage: + cd /home/dom/ai/rpa_vision_v3 && source .venv/bin/activate + python scripts/repro_bug_step10_validator.py +""" +from pathlib import Path + +from core.validation.checkers.ocr_roi import OcrRoiChecker +from core.grounding.title_verifier import TitleVerifier + + +def main(): + fixture = Path( + "/home/dom/ai/rpa_vision_v3/visual_workflow_builder/backend/data/anchors/" + "anchor_0438bd2d9bdd_1778161174_full.png" + ) + assert fixture.exists(), f"Fixture absente : {fixture}" + + # OCR singleton EasyOCR via title_verifier (GPU si dispo) + tv = TitleVerifier() + ocr_fn = tv._get_ocr() + assert ocr_fn is not None, "OCR non chargé (EasyOCR ou docTR requis)" + + checker = OcrRoiChecker(ocr_fn=ocr_fn, radius_px=80) + + # SCENARIO 1 — clic dans le bandeau URL Edge (bug step 10) + # coords résolues par OCR-DIRECT pour 'Imagerie' = (0.2305, 0.2805) + # mais ces coords tombent dans la barre URL Edge + action_bug = { + "type": "click", + "target_spec": {"by_text": "Imagerie"}, + } + result_bug = { + "success": True, + "actual_position": {"x_pct": 0.2305, "y_pct": 0.155}, # bandeau URL Edge + } + res = checker.check( + action=action_bug, + result=result_bug, + screenshot_before=None, + screenshot_after=str(fixture), + context={}, + ) + print("SCENARIO 1 (clic bandeau Edge):") + print(f" verdict = {res.verdict.value}") + print(f" confidence = {res.confidence:.2f}") + print(f" failure_cat = {res.failure_category.value if res.failure_category else None}") + print(f" reasoning = {res.reasoning}") + print(f" roi_text = {res.raw_evidence.get('roi_text', '')[:100]}") + print() + + # SCENARIO 2 — clic correct sur l'onglet Imagerie + action_ok = { + "type": "click", + "target_spec": {"by_text": "Imagerie"}, + } + result_ok = { + "success": True, + "actual_position": {"x_pct": 0.265, "y_pct": 0.295}, # vraie position Imagerie + } + res2 = checker.check( + action=action_ok, + result=result_ok, + screenshot_before=None, + screenshot_after=str(fixture), + context={}, + ) + print("SCENARIO 2 (clic correct Imagerie):") + print(f" verdict = {res2.verdict.value}") + print(f" confidence = {res2.confidence:.2f}") + print(f" reasoning = {res2.reasoning}") + + +if __name__ == "__main__": + main() +``` + +### 7.3. Résultat attendu + +``` +SCENARIO 1 (clic bandeau Edge): + verdict = terminate + confidence = 0.85 + failure_cat = wrong_application + reasoning = Token navigateur/système 'https' dans ROI clic (attendu 'Imagerie') — cible hors-app + roi_text = urgence.labs.laurinebazin.design/aiva-urgence/dossier.html... + +SCENARIO 2 (clic correct Imagerie): + verdict = complete + confidence = 0.90 + reasoning = Texte 'Imagerie' trouvé dans ROI +``` + +**Latence mesurée typique** : 80-150 ms par check sur RTX 5070 (EasyOCR GPU sur crop 160×160), 200-400 ms sur CPU. + +--- + +## 8. Test pytest + +```python +# tests/unit/test_validator_step10.py +"""Tests unitaires Validator — bug step 10 fermé.""" +from pathlib import Path + +import pytest + +from core.validation.result import Verdict, FailureCategory +from core.validation.checkers.ocr_roi import OcrRoiChecker + +FIXTURE = Path( + "/home/dom/ai/rpa_vision_v3/visual_workflow_builder/backend/data/anchors/" + "anchor_0438bd2d9bdd_1778161174_full.png" +) + + +@pytest.fixture(scope="module") +def ocr_fn(): + """OCR singleton EasyOCR (GPU si dispo).""" + pytest.importorskip("easyocr") + from core.grounding.title_verifier import TitleVerifier + fn = TitleVerifier()._get_ocr() + if fn is None: + pytest.skip("Aucun OCR disponible") + return fn + + +@pytest.fixture +def checker(ocr_fn): + return OcrRoiChecker(ocr_fn=ocr_fn, radius_px=80) + + +@pytest.mark.skipif(not FIXTURE.exists(), reason="Fixture screenshot manquante") +def test_step10_bug_detected_when_click_in_url_bar(checker): + """SCENARIO bug step 10 : clic tombé dans la URL bar Edge → TERMINATE WRONG_APPLICATION.""" + res = checker.check( + action={"type": "click", "target_spec": {"by_text": "Imagerie"}}, + result={"success": True, "actual_position": {"x_pct": 0.2305, "y_pct": 0.155}}, + screenshot_before=None, + screenshot_after=str(FIXTURE), + context={}, + ) + assert res.verdict == Verdict.TERMINATE + assert res.failure_category == FailureCategory.WRONG_APPLICATION + assert res.confidence >= 0.8 + assert "navigateur" in res.reasoning.lower() or "edge" in res.raw_evidence.get("roi_text", "").lower() + + +@pytest.mark.skipif(not FIXTURE.exists(), reason="Fixture screenshot manquante") +def test_correct_click_on_imagerie_tab(checker): + """SCENARIO clic correct sur l'onglet Imagerie → COMPLETE.""" + res = checker.check( + action={"type": "click", "target_spec": {"by_text": "Imagerie"}}, + result={"success": True, "actual_position": {"x_pct": 0.265, "y_pct": 0.295}}, + screenshot_before=None, + screenshot_after=str(FIXTURE), + context={}, + ) + assert res.verdict == Verdict.COMPLETE + assert res.confidence >= 0.6 + + +def test_missing_inputs_returns_continue_low_confidence(checker): + res = checker.check( + action={"type": "click", "target_spec": {}}, + result={"success": True}, + screenshot_before=None, + screenshot_after=None, + context={}, + ) + assert res.verdict == Verdict.CONTINUE + assert res.confidence < 0.3 + + +def test_strip_accents_robust(): + from core.validation.checkers.ocr_roi import _strip_accents + assert _strip_accents("Imagerie") == "imagerie" + assert _strip_accents("Notes médicales") == "notes medicales" + assert _strip_accents("Synthèse Urgences") == "synthese urgences" + assert _strip_accents("URL: https://www.exemple.com") == "url: https://www.exemple.com" +``` + +Lancement : +```bash +cd /home/dom/ai/rpa_vision_v3 && source .venv/bin/activate +pytest tests/unit/test_validator_step10.py -v +``` + +--- + +## 9. Configuration — variables d'environnement & kill-switches + +```bash +# Activation globale du Validator V2 (default: off) +RPA_VALIDATOR_V2_ENABLED=false + +# Tuning OcrRoiChecker +RPA_VALIDATOR_OCR_ROI_RADIUS_CLICK=80 # px (default 80) +RPA_VALIDATOR_OCR_ROI_RADIUS_TYPE=120 # px +RPA_VALIDATOR_OCR_SUSPECT_CONFIDENCE=0.85 +RPA_VALIDATOR_OCR_EXPECTED_CONFIDENCE=0.90 + +# Tuning Validator orchestrateur +RPA_VALIDATOR_ACCEPT_CONFIDENCE=0.70 +RPA_VALIDATOR_ESCALATE_BELOW=0.55 + +# Kill-switch escalation LLM (coûteuse 2-3 s) +RPA_VALIDATOR_LLM_JUDGE_ENABLED=true + +# Override hard du verdict (debug) +RPA_VALIDATOR_FORCE_VERDICT= # vide | complete | continue | terminate +``` + +Tous les flags conformes à la convention QW Suite Mai (cf. `docs/QW_SUITE_MAI.md`) : `RPA_*_ENABLED` boolean, lecture via `os.environ.get("...", default).lower() in {"true", "1", "yes"}`. + +--- + +## 10. Patterns externes 2026 — verbatim & sources + +### 10.1. Skyvern — prompt `check-user-goal-with-termination.j2` verbatim + +Récupéré directement du repo le 24 mai 2026 (`raw.githubusercontent.com/Skyvern-AI/skyvern/main/skyvern/forge/prompts/skyvern/check-user-goal-with-termination.j2`) : + +```jinja +You are here to help the user determine if the user has completed their goal on the web{{ " according to the complete criterion" if complete_criterion else "" }}. Use the content of the elements parsed from the page,{{ "" if without_screenshots else " the screenshots of the page," }} the user goal and user details to determine the status of the task. + +Make sure to ONLY return the JSON object in this format with no additional text before or after it: +{ + "page_info": str, + "thoughts": str, + "status": str, // "complete" | "terminate" | "continue" + "failure_categories": array // 12 catégories — voir doc parent §2.3 +} + +Important: Think carefully about the difference between "terminate" and "continue": +- "terminate" = impossible to achieve, stop trying (e.g., "account does not exist", "file unavailable", permanent error) +- "continue" = not done yet, but achievable with more steps (e.g., page is loading, need to click something, need to wait) +``` + +12 catégories d'échec : ANTI_BOT_DETECTION, BROWSER_ERROR, NAVIGATION_FAILURE, PAGE_LOAD_TIMEOUT, AUTH_FAILURE, LLM_REASONING_ERROR, CREDENTIAL_ERROR, ELEMENT_NOT_FOUND, WRONG_PAGE_STATE, DATA_EXTRACTION_FAILURE, INFRASTRUCTURE_ERROR, UNKNOWN. + +**Adaptation rpa_vision_v3** : 8 catégories suffisent (cf. `FailureCategory` §3.1) — on a moins de surfaces (pas de captcha web). On garde `WRONG_APPLICATION` qui n'existe pas chez Skyvern (Skyvern est en navigateur fermé, on est sur Windows multi-app). + +### 10.2. browser-use — agentic judge verbatim format + +Source : + +Modèle : `gemini-2.5-flash` (87% accord avec labels humains). Sortie JSON : + +```json +{ + "reasoning": "Analysis covering what worked, failures, trajectory quality, tool usage, output quality", + "verdict": "true|false", + "failure_reason": "Max 5 sentences explanation if failed", + "impossible_task": "true|false", + "reached_captcha": "true|false" +} +``` + +Philosophie : *« simple prompts and absolute True/False verdicts work best. Complex rubrics → indecisive judging. »* → **on retient** : notre `LlmJudgeChecker` doit forcer VERDICT: OUI/NON binaire, c'est ce que `verify_with_critic` fait déjà (`replay_verifier.py:481-485`). + +### 10.3. OpenAdapt — Process Graph + dual validation + +Source : + +OpenAdapt distingue **code-based validation** (code Python généré par LLM, vérifie une condition) vs **model-based validation** (LMM reçoit screenshot + completion_criteria texte → bool). Si échec → bascule automatique en mode recording → la trace devient training data (« Evaluation-Driven Feedback »). + +**À retenir** : notre `JsonSchemaChecker` est l'équivalent code-based, `LlmJudgeChecker` l'équivalent model-based. La bascule auto-recording n'est pas dans le périmètre P0 mais doit alimenter `TargetMemoryStore` en P1 (cf. memory `project_lea_apprentissage_plan.md`). + +### 10.4. Anthropic Computer Use — Validator implicite + +Anthropic CU (Claude 3.5 Sonnet computer-use beta) **n'a pas de Validator nommé**. Le modèle re-observe après chaque action et décide de continuer/corriger dans son raisonnement. Source : . + +**Non transposable à rpa_vision_v3** : notre Actor (Léa) est un exécutant déterministe, pas un LLM agentique. Il faut un Validator externe. + +### 10.5. ScreenSpot-Pro & agentic reward modeling 2025-2026 + +- **ScreenSpot-Pro** (arXiv 2504.07981, avril 2025) : benchmark grounding GUI haute résolution, 1581 instructions × 23 apps. Meilleur modèle = 18.9 % top-1, ScreenSeekeR = 48.1 %. → confirme qu'aucun grounding seul ne suffit, un Validator est nécessaire pour catcher les 50-80 % de cas où le grounder vise mal. +- **Agentic Reward Modeling — Verifying GUI Agent via Online Proactive Interaction** (arXiv 2602.00575) : verifier appris en RL, double LLM-as-judge + rule-based. +- **DPO Learning with LLMs-Judge Signal for Computer Use Agents** (arXiv 2506.03095) : judge filtre trajectoires synthétiques pour entraînement. Lien direct avec `replay_learner.py` existant. + +→ **Cible long terme** : `TargetMemoryStore` + `replay_learner` peuvent être alimentés par les verdicts du Validator. Chaque TERMINATE bien diagnostiqué = training signal négatif. Chaque COMPLETE conf élevée = positif. + +--- + +## 11. Plan d'intégration en 3 étapes + +### 11.1. P0 — 1 jour (avant prochaine démo client) + +**Cible** : fermer le bug step 10 sans toucher au flux nominal. + +1. Créer `core/validation/{__init__.py, result.py, checker_base.py, validator.py}` — 2 h. +2. Créer `core/validation/checkers/{__init__.py, ocr_roi.py, llm_judge.py}` — 2 h. +3. Écrire `scripts/repro_bug_step10_validator.py` + lancer en local pour confirmer le verdict TERMINATE — 30 min. +4. Écrire `tests/unit/test_validator_step10.py` — 1 h. Lancer `pytest tests/unit/test_validator_step10.py -v`. +5. Patch `api_stream.py:3447` (diff §6.1) derrière `RPA_VALIDATOR_V2_ENABLED=false` — 2 h. +6. Démo interne avec flag ON sur `Demo_urgence_3_db` : mesurer latence ajoutée + faux positifs sur 46 steps — 30 min. +7. Documenter dans `docs/QW_SUITE_MAI.md` ou nouveau `docs/VALIDATOR_V2.md` — 30 min. + +**Livrable** : pas de régression flag off, bug step 10 détecté en TERMINATE flag on. + +### 11.2. P1 — 2 semaines + +1. Matrice complète action → check (§4) : ajouter `PixelDiffChecker`, `TitleBarChecker`, `JsonSchemaChecker` — 1 jour. +2. Implémenter le `route_verdict` dispatcher (§5) : intégrer enter_paused_state, retry_with_reresolve, handoff_dialog_handler — 2 jours. +3. Dashboard : panneau « Validator stats » — verdicts par session, top failure_categories, latence p50/p95 — 1 jour. +4. Réactiver DETTE-008 (`observe_reason_act.py:1704-1713`) : ce code mort EST l'ancêtre du Validator. Le remplacer par appel `Validator.validate()` après chaque clic ORA. — 1 jour. +5. Coexistence avec drift exemption (`resolve_engine.py:2390 _RESOLUTION_MAX_DRIFT=0.95`) : si le Validator V2 atteint 90 % accuracy en démo, on peut baisser `_RESOLUTION_MAX_DRIFT` à 0.30 — 0.5 jour test. +6. Réactivation `RPA_ENABLE_TEXT_PRECHECK=true` (DETTE-001) : le pré-check OCR sémantique devient une fonction privée du Validator V2 — 0.5 jour. + +### 11.3. P2 — post-démo (1 mois) + +1. `DialogPresenceChecker` (chaîne D2) : cascade modaux VM via OCR + template — 2 jours. +2. Migration `LlmJudgeChecker` vers handler dédié séparé du `t2a_decision` LLM (Skyvern fait pareil avec `USE_CHECK_USER_GOAL_HANDLER_FOR_VERIFICATION`) — 1 jour. +3. Apprentissage : chaque verdict TERMINATE alimente `TargetMemoryStore` comme negative trace — 3 jours. +4. Re-planification : signaler à VWB que l'ancre est foireuse → suggestion recapture automatique — 5 jours. +5. Multi-modal Validator (combiner OCR + DINOv2 + title en 1 check composite atomique) — bench post-démo. + +--- + +## 12. Sources avec liens cliquables + +### Code source consulté +- Skyvern `agent.py` — +- Skyvern prompt `check-user-goal-with-termination.j2` (récupéré verbatim 24 mai 2026) — +- Skyvern prompt `check-user-goal.j2` (cité par doc parent) — +- Skyvern repo principal — +- Skyvern PR #1513 chain-of-thought user goal — + +### Framework verifiers 2026 +- browser-use evaluation system — +- browser-use AGENTS.md — +- OpenAdapt architecture wiki — +- OpenAdapt evals — +- Anthropic Computer Use docs — + +### Papers 2025-2026 +- ScreenSpot-Pro (arXiv 2504.07981) — +- Agentic Reward Modeling for GUI Agent (arXiv 2602.00575) — +- DPO Learning with LLMs-Judge Signal for CUA (arXiv 2506.03095) — +- GUI-Actor coordinate-free grounding (arXiv 2506.03143) — + +### Pydantic v2 (JsonSchemaChecker) +- Pydantic v2 JSON validation guide — +- LLM output validation pratiques — +- Production guide — + +### Doc interne consultée (lecture seule) +- Doc parent : `docs/recherche/AXE_B2_VALIDATOR_PATTERN.md` +- Doc frère OCR : `docs/recherche/AXE_A4_OCR_TEMPLATE_PHASH.md` +- Bug archétype : `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` +- Bench LLM judge : `docs/BENCH_SAFETY_CHECKS_2026-05-06.md` +- Code existant verifier : `agent_v0/server_v1/replay_verifier.py:367-633` (`verify_with_critic`) +- Code existant title verifier : `core/grounding/title_verifier.py:25-175` +- Wiring actuel : `agent_v0/server_v1/api_stream.py:3447-3582` (`report_action_result`) +- DETTE-008 (pre-check VLM désactivé) : `core/execution/observe_reason_act.py:1704-1713` +- Drift exemption : `agent_v0/server_v1/resolve_engine.py:2384-2390` (`_RESOLUTION_MAX_DRIFT=0.95`) +- Synthèse globale : `docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` + +--- + +## 13. Dépendances explicites avec autres axes + +| Axe | Dépendance | Statut | +|---|---|---| +| **AXE_A4 (OCR)** | `OcrRoiChecker` utilise EasyOCR singleton du `TitleVerifier` (déjà chargé en prod). `_strip_accents` réutilisable dans `_resolve_by_ocr_text` correctif center-of-span. | ✅ pas de blocage | +| **AXE_A5 (tokenisation UI)** | Si OmniParser/UI-DETR-1 livre des bboxes par élément au runtime, le Validator pourrait matcher `target == element_at_point(cx, cy).label` directement (déterministe). | 🟡 P2 | +| **AXE_B1 (watchdog `_retry_pending`)** | Indépendant. Le watchdog corrige la cause primaire (HTTP timeout), le Validator corrige la cause aggravante (mauvais clic validé success=True). Les deux ensemble = fermeture totale du bug step 10. | ✅ orthogonal | +| **Chaîne D2 (dialog/popup)** | `failure_category=UNEXPECTED_DIALOG` → handoff DialogHandler. Le Validator détecte le problème, D2 le résout. | ✅ contrat clair | +| **DETTE-008** | Le code mort `if False:` en `observe_reason_act.py:1704-1713` est l'ancêtre du Validator. À remplacer en P1 par `Validator.validate()` après chaque clic ORA. | 🟡 P1 | +| **DETTE-001 (`RPA_ENABLE_TEXT_PRECHECK=false`)** | Le pré-check OCR spatialement aveugle devient le `OcrRoiChecker` correctement spatialisé. | ✅ P1 | +| **Drift exemption ≥ 0.95** (`_RESOLUTION_MAX_DRIFT`) | Le Validator V2 permet de baisser le seuil drift à 0.30 (P1) car les faux positifs templates seront catchés post-action. | 🟡 P1 | + +--- + +*Livrable de recherche, lecture seule. Aucune modification de code appliquée. Validation et merge relèvent de Dom au cas par cas, après validation du smoke test §11.1 sur `Demo_urgence_3_db`.* diff --git a/docs/recherche/AXE_B2_VALIDATOR_PATTERN.md b/docs/recherche/AXE_B2_VALIDATOR_PATTERN.md new file mode 100644 index 000000000..724970dc9 --- /dev/null +++ b/docs/recherche/AXE_B2_VALIDATOR_PATTERN.md @@ -0,0 +1,817 @@ +# AXE B2 — Pattern Planner-Actor-Validator & validation sémantique post-action + +**Date :** 2026-05-23 +**Auteur :** agent recherche dispatché (Claude Opus 4.7 1M) +**Statut :** livrable de recherche, lecture seule, AUCUNE modification de code +**Lien dépendances :** AXE_A4 (OCR), AXE_A5 (tokenisation écran — déjà rédigé), AXE_B4 (ORA observe_reason_act) + +--- + +## 1. TL;DR + recommandation + +**Constat.** Skyvern (12k stars, SOTA 85.85 % WebVoyager) formalise le **Validator** comme un agent à part entière, séparé du Planner et de l'Actor. Son rôle : après chaque step, prendre une nouvelle capture, demander à un LLM (avec image + DOM élagué) si l'objectif courant est atteint, sinon renvoyer `continue` / `terminate`. C'est exactement ce qui manque à rpa_vision_v3 : VWB = Planner statique, Léa = Actor, et `replay_verifier.py` est un pixel-diff global qui n'a aucune notion de **sémantique** (« est-ce que l'onglet Imagerie de l'app Easily est maintenant actif ? »). + +Le bug archétype step 10 démo GHT (« Imagerie » cliqué dans le bandeau Edge, REPORT success=True) tient **uniquement** à cette absence : pHash global voit du mouvement → conclut OK. Un Validator visuel par step le détecterait en 1-3 s. + +**Recommandation design pour rpa_vision_v3** (justifiée §6, §9) : + +1. **Garder** `replay_verifier.verify_action` (pixel) comme pré-filtre 10 ms. +2. **Réactiver et étendre** `verify_with_critic` déjà câblé (§6) en lui passant un `expected_result` **typé** par action. +3. **Ajouter un `Validator` pluggable** côté serveur, qui choisit la stratégie de check selon `action_type` (matrice §5). Implémentation Python = ~250 LOC. +4. **Pour le bug step 10 précisément** : `click_anchor` doit déclencher une vérif OCR-ROI **autour du point cliqué** (rayon 60 px) ET une vérif title-bar (déjà fait par `core/grounding/title_verifier.py`). Si la ROI contient le mot Edge / le mot URL / un domaine `.com`, c'est un faux clic → retry, pas continue. +5. **Latence cible** : pixel 10 ms, OCR-ROI 100 ms, LLM-judge 2-3 s. Ne lancer le LLM-judge que si pixel **OU** OCR-ROI suspect. + +Le pattern Skyvern est directement adoptable. Le code Skyvern (Python, AGPL-3.0) montre que le Validator c'est **5 prompts Jinja2 + 1 méthode `complete_verify` + 1 dataclass `CompleteVerifyResult`**. Pas plus. + +--- + +## 2. Skyvern Validator détaillé (code source 23 mai 2026) + +### 2.1. Méthode `complete_verify` (extraite verbatim de `skyvern/forge/agent.py:2609-2730`) + +Source : + +Le Validator chez Skyvern n'est pas un sous-processus exotique : c'est **une coroutine LLM** appelée après l'Actor, à chaque step où il n'y a pas déjà une `DecisiveAction` (= action terminale émise par l'Actor lui-même). + +```python +# skyvern/forge/agent.py (résumé condensé du flux) +async def complete_verify( + self, page: Page, scraped_page: ScrapedPage, task: Task, step: Step +) -> CompleteVerifyResult: + # 1. RE-SCRAPE la page (DOM élagué + screenshots), pas la version utilisée par l'Actor + scraped_page_refreshed = await scraped_page.refresh(draw_boxes=False, scroll=scroll) + + # 2. Construit le prompt avec : navigation_goal, payload, complete_criterion, + # action_history, elements parsés, datetime + template_name = "check-user-goal-with-termination" if use_termination_prompt else "check-user-goal" + verification_prompt = load_prompt_with_elements( + element_tree_builder=scraped_page_refreshed, + template_name=template_name, + navigation_goal=task.navigation_goal, + navigation_payload=task.navigation_payload, + complete_criterion=task.complete_criterion, + terminate_criterion=task.terminate_criterion, + action_history=actions_and_results_str, + local_datetime=..., + ) + + # 3. Appel LLM avec screenshots — un handler LLM dédié possible + # via flag PostHog USE_CHECK_USER_GOAL_HANDLER_FOR_VERIFICATION + verification_result = await llm_api_handler( + prompt=verification_prompt, + step=step, + screenshots=scraped_page_refreshed.screenshots, + prompt_name=prompt_name, + ) + + # 4. Parse JSON strict → 3 verdicts possibles + result = CompleteVerifyResult.model_validate(verification_result) + if result.is_complete: + verification_status = VerificationStatus.complete + elif result.is_terminate: + verification_status = VerificationStatus.terminate + else: + verification_status = VerificationStatus.continue_step + + # 5. Trace OTEL : verification.status, verification.template, verification.reasoning_kind + span.set_attribute("verification.status", verification_status.value) + record_verification_span_attrs(span, result.thoughts) + return result +``` + +**Trois verdicts uniquement** : `complete` / `terminate` / `continue_step`. Pas de `success_partial` ni de `retry_silent`. C'est volontaire : la décision est forcée binaire. + +Le `check_user_goal_complete` (lignes 2736+) wrap `complete_verify` et le convertit en `CompleteAction` ou `TerminateAction` pour l'orchestrateur. + +### 2.2. Le prompt `check-user-goal.j2` (verbatim, fetch direct du repo) + +Source : + +```jinja +Your are here to help the user determine if the user has completed their goal on the web{{ " according to the complete criterion" if complete_criterion else "" }}. Use the content of the elements parsed from the page,{{ "" if without_screenshots else " the screenshots of the page," }} the user goal and user details to determine whether the {{ "complete criterion has been met" if complete_criterion else "user goal has been completed" }} or not. + +Make sure to ONLY return the JSON object in this format with no additional text before or after it: +{ + "page_info": str, // Think step by step. Describe all the useful information in the page related to the user goal. + "thoughts": str, // Think step by step. What information makes you believe whether user goal has completed or not. Use information you see on the site to explain. + "user_goal_achieved": bool // True if the user goal has been completed, false otherwise. +} + +User Goal: +{{ navigation_goal }} + +User Details: +{{ navigation_payload }} + +Action History: +{{ action_history }} + +Elements on the page: +{{ elements }} + +Current datetime, ISO format: +{{ local_datetime }} +``` + +**Points clés** : +- Sortie JSON stricte, parsée par Pydantic `CompleteVerifyResult.model_validate`. +- Trois infos données au modèle : (a) screenshots, (b) elements parsés du DOM, (c) action_history textuelle. Multi-modal. +- Le `page_info` → `thoughts` → `user_goal_achieved` impose une chain-of-thought structurée. C'est ce qui rend l'erreur diagnosticable. + +### 2.3. Le prompt `check-user-goal-with-termination.j2` (expérimental, verbatim) + +Source : + +Ajoute un 3e statut explicite `terminate` + une **classification des échecs** en 12 catégories : + +```jinja +"status": str, // Must be one of three values: "complete", "terminate", or "continue". +"failure_categories": array // Only populate when status is "terminate". Classify the root cause. +[{ + "category": str, // ANTI_BOT_DETECTION | BROWSER_ERROR | NAVIGATION_FAILURE | + // PAGE_LOAD_TIMEOUT | AUTH_FAILURE | LLM_REASONING_ERROR | + // CREDENTIAL_ERROR | ELEMENT_NOT_FOUND | WRONG_PAGE_STATE | + // DATA_EXTRACTION_FAILURE | INFRASTRUCTURE_ERROR | UNKNOWN + "confidence_float": float, + "reasoning": str +}] + +Important: Think carefully about the difference between "terminate" and "continue": +- "terminate" = impossible to achieve, stop trying +- "continue" = not done yet, but achievable with more steps +``` + +**À retenir** : Skyvern est très conservateur sur `terminate` (« only when CLEAR, EXPLICIT, UNAMBIGUOUS evidence »). C'est aligné avec le feedback `feedback_failure_is_learning.md` de Dom : échec ≠ stop avec erreur, c'est pause supervisée. + +### 2.4. Quand le Validator se déclenche + +Extrait `agent.py:1929-1971` : + +```python +enable_parallel_verification = False +if ( + not has_decisive_action # l'Actor n'a pas déjà émis un COMPLETE + and not task_completes_on_download + and not isinstance(task_block, ActionBlock) + and complete_verification # flag global activable par-task + and (task.navigation_goal or task.complete_criterion) +): + # Géré par feature flag PostHog + disable_user_goal_check = await app.EXPERIMENTATION_PROVIDER.is_feature_enabled_cached( + "DISABLE_USER_GOAL_CHECK", + task.task_id, + ... + ) + enable_parallel_verification = not disable_user_goal_check +``` + +→ **Le Validator tourne à CHAQUE step** par défaut (« deferred to handle_completed_step »). C'est désactivable par task ou globalement, mais l'état par défaut est ON. Skyvern accepte le coût LLM par step parce qu'un faux succès rend l'agent inutilisable. + +### 2.5. Contrat de données + +```python +# skyvern/forge/sdk/schemas/tasks.py (déduit du code agent.py) +class CompleteVerifyResult(BaseModel): + page_info: str + thoughts: str + is_complete: bool + is_terminate: bool = False + status: str | None = None # "complete" | "terminate" | "continue" + failure_categories: list[FailureCategory] = [] +``` + +### 2.6. Latence et coût + +D'après le post Skyvern 2.0 () : + +- Un step moyen prend 2-10 s. +- Validator = appel LLM séparé (souvent un GPT-4o-mini ou Claude Haiku), 1-3 s. +- ROI = sans Validator, accuracy 68.7 % WebVoyager ; avec Validator, **85.85 %**. Le delta de +17 points en accuracy justifie largement la latence. + +Source : (browser-use rapporte +17 pts également : 45 → 68.7 → 85.85 selon Planner/Validator). + +--- + +## 3. Tour d'horizon Validator dans 5 autres frameworks + +### 3.1. OpenAdapt — Evaluation-Driven Feedback + +Source : , . + +OpenAdapt formalise le concept au niveau **Process Graph** (graphe de steps avec arêtes = critères de complétion) : + +- **Code-based validation** : LLM génère du Python qui vérifie une condition d'état (présence d'un message de confirmation, état d'un bouton, etc.). Code stocké, ré-exécuté à chaque replay. +- **Model-based validation** : LMM (Large Multimodal Model) reçoit le screenshot courant + `completion_criteria` formulés en langage naturel → bool. + +Particularité : si la validation échoue, OpenAdapt **bascule en mode recording** automatiquement → l'utilisateur démontre la suite → la trace devient training data. C'est l'« Evaluation-Driven Feedback ». Le sous-package `openadapt-evals` expose `evaluate_agent_on_benchmark`. + +### 3.2. browser-use — agentic judge + +Source : , . + +- LLM judge intégré dans le code agent, **tourne après `done`** ET « can also double as a real-time validation layer during regular use ». +- Modèle : `gemini-2.5-flash`. Accuracy juge vs labels humains : 87 %. +- Sortie JSON stricte : + +```json +{ + "reasoning": "Analysis covering what worked, failures, trajectory quality, tool usage, output quality", + "verdict": "true|false", + "failure_reason": "Max 5 sentences explanation if failed", + "impossible_task": "true|false", + "reached_captcha": "true|false" +} +``` + +- Philosophie : **simple prompts and absolute True/False verdicts work best**. Complex rubrics → indecisive judging. + +### 3.3. Anthropic Computer Use + +Source : . + +Anthropic CU n'a pas de Validator nommé. Boucle minimaliste : `screenshot → action → screenshot → ...` jusqu'à ce que Claude lui-même décide qu'il a fini. **Validation = self-reflection implicite du modèle dans son raisonnement**. + +→ Acceptable parce que Claude est puissant. **Pas applicable à rpa_vision_v3** où l'Actor n'est pas un LLM agentique mais un exécutant déterministe (Léa). Il faut un Validator externe. + +### 3.4. OpenAI Operator / CUA + +Source : . + +Idem Anthropic CU : pas de Validator séparé. Le modèle CUA fait perception → reasoning → action en boucle. Selon le system card : « If it encounters challenges or makes mistakes, Operator can leverage its reasoning capabilities to self-correct ». Pas formalisé. + +OpenCUA (open-source, ) entraîne avec « reflective Chain-of-Thought reasoning » mais pas de check externe. + +### 3.5. Cradle (BAAI, Kunlun Tech) — Self-Reflection module + +Source : , . + +Cradle décompose explicitement en 6 modules dont **Self-Reflection** : +> « Through this module, the agent assesses previous actions to understand their outcomes, evaluate successes or failures, and adjust behavior accordingly. » + +Mesure : +20.41 points sur tâches « professional domain » vs baselines. Mais c'est un agent jeu/applications, pas RPA déclaratif → moins directement transposable. + +### 3.6. Tableau récap + +| Framework | Validator nommé ? | Modalité | Modèle | Latence | Verdict format | +|---|---|---|---|---|---| +| Skyvern 2.0 | **Oui** (`complete_verify`) | VLM + DOM élagué | GPT-4o ou handler dédié | 1-3 s | JSON `is_complete/is_terminate/status` | +| OpenAdapt | Oui (Process Graph) | LMM ou Python généré | Configurable | n/a | bool + falls back to recording | +| browser-use | Oui (agentic judge) | VLM + DOM | gemini-2.5-flash | 1-2 s | JSON `verdict/failure_reason` | +| Anthropic CU | Non (implicite) | Self-reflection | Claude lui-même | inclus | continuation libre | +| OpenAI Operator | Non (implicite) | Self-reflection | CUA | inclus | continuation libre | +| Cradle | Oui (Self-Reflection) | LMM | GPT-4V | 2-5 s | text reasoning | + +**Convergence forte** : les 3 frameworks RPA matures (Skyvern, OpenAdapt, browser-use) ont un Validator **explicite, JSON-strict, multi-modal (VLM + structure DOM)**. Les agents généralistes (CU, Operator) délèguent au LLM agentique. Pour rpa_vision_v3 avec Actor déterministe = camp Skyvern. + +--- + +## 4. Taxonomie des approches de validation post-action + +| Approche | Coût | Précision | Faux-positifs | Quand l'utiliser | +|---|---|---|---|---| +| **A. LLM-as-judge (full VLM)** | 1-5 s | Très haute (sémantique) | Faibles | Validation finale de step / cas ambigus | +| **B. OCR ROI** (texte attendu autour du clic) | 80-200 ms | Haute si texte connu | Sensible OCR errors | Tabs, boutons, libellés | +| **C. OCR title-bar** (titre fenêtre) | ~120 ms (déjà câblé) | Moyenne | Bruit OCR sur petits crops | Navigation fenêtre / ouverture appli | +| **D. Visual diff pHash global** | 10 ms | Très basse (juste « ça a bougé ») | Énormes | Pré-filtre `nothing-happened` | +| **E. Visual diff pHash ROI** | 20 ms | Moyenne | Moyens | Détection focus tab (changement souligné) | +| **F. CLIP features cos-sim** | 50-200 ms | Moyenne | Confond visuellement proches | Reconnaissance d'écran connu | +| **G. DINOv2 features** | 100-300 ms | Haute (self-supervised, plus robuste que CLIP) | Faibles | Comparaison patches précis | +| **H. LPIPS** | 100 ms | Haute (perceptual) | Moyens | Vérif après animations / transitions | +| **I. Window-focus check** (win32 API ou OCR titlebar) | <50 ms | Très haute | Quasi nuls | Vérif que la bonne app est devant | +| **J. Dialog presence detect** | OCR + template | Très haute | Faibles | Détection popups bloquantes | +| **K. JSON schema validation** (extraction) | <10 ms | Déterministe | nuls | `extract_text`, `t2a_decision` | + +**Source visual diff** : — pHash est positionné comme « pre-filter, not a comparator ». Les VLM sont positionnés comme « triage layer on top of pixel diffs, not as the comparator itself ». Exactement le design pixel→sémantique déjà câblé dans `replay_verifier.verify_with_critic`. + +**Pour DINOv2 / LPIPS / CLIP** : sources , . DINOv2 produit des features visuelles plus discriminantes que CLIP pour comparer deux crops d'UI (CLIP est entraîné texte↔image, pas pour le pixel-perfect). + +--- + +## 5. Matrice type d'action → check recommandé pour rpa_vision_v3 + +Aligné avec `reference_vwb_action_types.md` (memory) et `_ALLOWED_ACTION_TYPES` de `replay_engine.py`. + +| Action VWB (Léa) | Check primaire | Check secondaire (si primaire ambigu) | Budget latence | +|---|---|---|---| +| `click_anchor` → `click` | **B. OCR ROI** (rayon 60 px) + **I. Window focus** | A. LLM-as-judge si OCR ne trouve pas le label | 100 ms + 2 s si escalation | +| `double_click_anchor` → `click button="double"` | **C. OCR title-bar** (déjà câblé) + **B. OCR ROI** | A. LLM-as-judge | 200 ms + 2 s | +| `right_click_anchor` → `click button="right"` | **J. Dialog presence** (menu contextuel attendu) | B. OCR ROI sur menu | 150 ms | +| `type_text` → `type` | **B. OCR ROI** : le texte tapé est-il visible dans la ROI ? | A. LLM-as-judge si texte tronqué | 100 ms | +| `type_secret` | **D. pHash ROI** (vérifier qu'un input s'est rempli, pas le contenu) | — | 20 ms | +| `keyboard_shortcut` → `key_combo` | **C. OCR title-bar** OU **J. Dialog presence** selon raccourci | A. LLM-as-judge en cas de doute | 200 ms | +| `scroll_to_anchor` → `scroll` | **F. CLIP cos-sim** before/after ROI cible visible | D. pHash global change ≠ 0 | 100 ms | +| `wait_for_anchor` → `wait` | **B. OCR ROI** : l'ancre est-elle visible ? | A. LLM-as-judge | 100 ms | +| `extract_text` | **K. JSON schema** : type str, longueur > 0, langue fr ratio | A. LLM-as-judge sur le contenu plausibilité | 10 ms + 2 s si plausibilité requise | +| `extract_text_scroll` | K + **A. LLM-as-judge** si plusieurs pages | — | 10 ms + 2 s | +| `extract_table` | **K. JSON schema** : ≥ 1 row, headers attendus si fournis | A. LLM-as-judge | 10 ms | +| `screenshot_evidence` | — (action passive) | I. Window focus | <50 ms | +| `t2a_decision` | **K. JSON schema** strict (decision ∈ {UHCD, FORFAIT, NA}, JSON parseable) | — | 10 ms | +| `pause_for_human` | **Checklist QW4** (déjà fait, `SafetyChecksProvider`) | — | n/a | +| `db_save_data` | **K. Schema row sauvée** (SELECT verify) | — | <50 ms | +| `import_excel`, `db_read_data` | **K. Schema rows** | — | <50 ms | +| `visual_condition` | **A. LLM-as-judge** sur la condition formulée | — | 2 s | +| `ai_ocr`, `ai_summarize`, etc. | **K. JSON schema** + **A. plausibilité** | — | 10 ms + 2 s | + +**Principe directeur** : la plupart des actions ont un check pas-cher (OCR ROI, JSON) qui suffit dans 90 % des cas. Le LLM-as-judge (2 s) ne tire qu'en escalation, ou sur les actions à risque élevé (`click_anchor` sur cibles ambiguës, `t2a_decision`, `visual_condition`). + +--- + +## 6. Design d'un Validator pluggable — code copy-paste-ready + +### 6.1. Interface + +À placer dans `agent_v0/server_v1/validator.py` (nouveau fichier, complète `replay_verifier.py` existant) : + +```python +# agent_v0/server_v1/validator.py +""" +Validator — vérification sémantique post-action pluggable. + +Inspiré de Skyvern (Planner-Actor-Validator). Combine pixel-diff existant +(replay_verifier.py) avec une couche sémantique typée par action_type. + +Trois verdicts possibles, calque sur Skyvern : +- COMPLETE → l'action a eu l'effet voulu, passer au step suivant +- CONTINUE → l'effet n'est pas encore visible, re-vérifier après wait +- TERMINATE → l'action a échoué de manière irrécupérable (pause supervisée) +""" +from __future__ import annotations +import logging +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, Callable, Dict, Optional, Protocol + +logger = logging.getLogger(__name__) + + +class Verdict(str, Enum): + COMPLETE = "complete" + CONTINUE = "continue" + TERMINATE = "terminate" + + +class FailureCategory(str, Enum): + WRONG_TARGET = "wrong_target" # cliqué ailleurs (ex. bug step 10) + NO_VISUAL_CHANGE = "no_visual_change" # action sans effet + UNEXPECTED_DIALOG = "unexpected_dialog" # popup bloque + WRONG_APPLICATION = "wrong_application" # focus sur mauvaise app (Edge vs Easily) + OCR_TEXT_MISSING = "ocr_text_missing" # texte attendu absent + SCHEMA_INVALID = "schema_invalid" # JSON/extract invalide + UNKNOWN = "unknown" + + +@dataclass +class ValidationResult: + verdict: Verdict + confidence: float # 0.0-1.0 + check_used: str # "ocr_roi" | "llm_judge" | "title_bar" | ... + elapsed_ms: float + reasoning: str = "" + failure_category: Optional[FailureCategory] = None + raw_evidence: Dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> Dict[str, Any]: + return { + "verdict": self.verdict.value, + "confidence": round(self.confidence, 3), + "check_used": self.check_used, + "elapsed_ms": round(self.elapsed_ms, 1), + "reasoning": self.reasoning, + "failure_category": self.failure_category.value if self.failure_category else None, + "raw_evidence": self.raw_evidence, + } + + +class ActionChecker(Protocol): + """Contrat d'un checker spécifique par action_type.""" + name: str + budget_ms: float + + def check( + self, + action: Dict[str, Any], + result: Dict[str, Any], + screenshot_before: Optional[str], + screenshot_after: Optional[str], + context: Dict[str, Any], + ) -> ValidationResult: ... + + +class Validator: + """Orchestrateur : route action_type → checker, gère l'escalation.""" + + def __init__( + self, + checkers: Dict[str, list[ActionChecker]], + default_checker: ActionChecker, + escalation_checker: Optional[ActionChecker] = None, + escalation_threshold: float = 0.5, + ): + """ + checkers: mapping action_type → liste de checkers à essayer en ordre. + default_checker: fallback si action_type pas dans le mapping. + escalation_checker: typiquement un LLM-as-judge, lancé si confidence < seuil. + """ + self._checkers = checkers + self._default = default_checker + self._escalation = escalation_checker + self._escalation_threshold = escalation_threshold + + def validate( + self, + action: Dict[str, Any], + result: Dict[str, Any], + screenshot_before: Optional[str] = None, + screenshot_after: Optional[str] = None, + context: Optional[Dict[str, Any]] = None, + ) -> ValidationResult: + context = context or {} + action_type = action.get("type", "") + + candidates = self._checkers.get(action_type, [self._default]) + + last_result: Optional[ValidationResult] = None + for checker in candidates: + res = checker.check(action, result, screenshot_before, screenshot_after, context) + last_result = res + # Si verdict net + confiance haute → renvoyer + if res.confidence >= self._escalation_threshold and res.verdict != Verdict.CONTINUE: + return res + + # Escalation LLM-as-judge si fourni + if self._escalation and last_result and last_result.confidence < self._escalation_threshold: + logger.info( + "Validator escalation LLM-judge (last_conf=%.2f, check=%s)", + last_result.confidence, last_result.check_used, + ) + esc = self._escalation.check(action, result, screenshot_before, screenshot_after, context) + # On combine : si LLM contredit, LLM prime (sa confiance est bornée à 0.9) + return esc + + return last_result or ValidationResult( + verdict=Verdict.CONTINUE, + confidence=0.3, + check_used="no_checker", + elapsed_ms=0.0, + reasoning="Aucun checker n'a produit de verdict", + ) +``` + +### 6.2. Exemple de checker : `OcrRoiChecker` (pour click_anchor) + +```python +# agent_v0/server_v1/checkers/ocr_roi.py +import time +from typing import Any, Dict, Optional +from PIL import Image + +from agent_v0.server_v1.validator import ( + ActionChecker, ValidationResult, Verdict, FailureCategory, +) + + +class OcrRoiChecker: + """Vérifie que le texte attendu apparaît dans la ROI autour du clic. + + Spécifiquement conçu pour résoudre le bug step 10 : + si on a cliqué sur 'Imagerie', la ROI 60px doit contenir 'Imagerie'. + Si elle contient 'Edge' ou 'urgence.labs.laurinebazin.design', + on a cliqué dans le bandeau navigateur → failure. + """ + name = "ocr_roi" + budget_ms = 200.0 + + # Mots suspects = on a cliqué hors-app + SUSPECT_TOKENS = {"edge", "chrome", "firefox", "http", "https", ".com", ".fr", + "favoris", "favorite", "onglet", "tab "} + + def __init__(self, ocr_fn, radius_px: int = 60): + self._ocr = ocr_fn # callable(PIL.Image) -> str + self._radius = radius_px + + def check(self, action, result, screenshot_before, screenshot_after, context) -> ValidationResult: + t0 = time.time() + expected_text = action.get("by_text") or context.get("expected_text", "") + x_pct = action.get("x_pct") + y_pct = action.get("y_pct") + + if not screenshot_after or x_pct is None or y_pct is None: + return ValidationResult( + verdict=Verdict.CONTINUE, confidence=0.2, + check_used=self.name, elapsed_ms=(time.time() - t0) * 1000, + reasoning="ROI indéfinie (pas de coords ou pas de screenshot)", + ) + + img = self._load_image(screenshot_after) + w, h = img.size + cx, cy = int(x_pct * w), int(y_pct * h) + r = self._radius + roi = img.crop((max(0, cx - r), max(0, cy - r), min(w, cx + r), min(h, cy + r))) + + text = (self._ocr(roi) or "").lower() + expected_lower = expected_text.lower().strip() + + elapsed_ms = (time.time() - t0) * 1000 + + # 1) Vérif : un token suspect (navigateur) dans la ROI → faux clic + for suspect in self.SUSPECT_TOKENS: + if suspect in text and suspect not in expected_lower: + return ValidationResult( + verdict=Verdict.TERMINATE, confidence=0.85, + check_used=self.name, elapsed_ms=elapsed_ms, + failure_category=FailureCategory.WRONG_APPLICATION, + reasoning=f"Token navigateur '{suspect}' dans ROI clic — cible probablement hors-app", + raw_evidence={"roi_text": text[:200], "expected": expected_lower}, + ) + + # 2) Vérif : le texte attendu est dans la ROI ? + if expected_lower and expected_lower in text: + return ValidationResult( + verdict=Verdict.COMPLETE, confidence=0.9, + check_used=self.name, elapsed_ms=elapsed_ms, + reasoning=f"Texte '{expected_lower[:40]}' trouvé dans ROI", + raw_evidence={"roi_text": text[:200]}, + ) + + # 3) Pas trouvé mais pas suspect non plus → confiance basse, escalation + return ValidationResult( + verdict=Verdict.CONTINUE, confidence=0.4, + check_used=self.name, elapsed_ms=elapsed_ms, + failure_category=FailureCategory.OCR_TEXT_MISSING, + reasoning=f"Texte '{expected_lower[:40]}' non trouvé dans ROI", + raw_evidence={"roi_text": text[:200]}, + ) + + @staticmethod + def _load_image(source: str) -> Image.Image: + # Délégué à replay_verifier._load_single_image, ou copy-paste équivalent + from agent_v0.server_v1.replay_verifier import ReplayVerifier + return ReplayVerifier()._load_single_image(source) +``` + +### 6.3. Intégration avec `replay_verifier.py` existant + +Le `replay_verifier.verify_with_critic` couvre déjà 80 % du besoin LLM-as-judge (étape sémantique VLM). Il suffit de : + +1. Le wrapper dans un `LlmJudgeChecker` qui implémente `ActionChecker`. +2. L'utiliser comme `escalation_checker` du `Validator`. + +```python +# agent_v0/server_v1/checkers/llm_judge.py +import time +from agent_v0.server_v1.replay_verifier import ReplayVerifier +from agent_v0.server_v1.validator import ( + ActionChecker, ValidationResult, Verdict, FailureCategory, +) + +class LlmJudgeChecker: + """Wrapper autour de ReplayVerifier.verify_with_critic (VLM gemma4).""" + name = "llm_judge" + budget_ms = 3000.0 + + def __init__(self, verifier: ReplayVerifier): + self._verifier = verifier + + def check(self, action, result, screenshot_before, screenshot_after, context) -> ValidationResult: + t0 = time.time() + expected = context.get("expected_result", "") + intention = context.get("action_intention", "") + workflow_ctx = context.get("workflow_context", "") + + critic = self._verifier.verify_with_critic( + action=action, result=result, + screenshot_before=screenshot_before, + screenshot_after=screenshot_after, + expected_result=expected, + action_intention=intention, + workflow_context=workflow_ctx, + ) + elapsed_ms = (time.time() - t0) * 1000 + + if critic.semantic_verified is True: + verdict = Verdict.COMPLETE + conf = max(critic.confidence, 0.7) + elif critic.semantic_verified is False: + verdict = Verdict.TERMINATE + conf = 0.8 + else: + verdict = Verdict.CONTINUE + conf = 0.4 + + return ValidationResult( + verdict=verdict, confidence=conf, + check_used=self.name, elapsed_ms=elapsed_ms, + reasoning=critic.semantic_detail or critic.detail, + raw_evidence={"pixel_change_pct": critic.change_area_pct, + "semantic_verified": critic.semantic_verified}, + ) +``` + +### 6.4. Câblage côté `api_stream.py` (post-action) + +Pseudo-diff (NE PAS appliquer, juste pour montrer le point d'insertion) : + +```python +# agent_v0/server_v1/api_stream.py — handler de REPORT +from agent_v0.server_v1.validator import Validator, Verdict +from agent_v0.server_v1.checkers.ocr_roi import OcrRoiChecker +from agent_v0.server_v1.checkers.llm_judge import LlmJudgeChecker + +# Init au boot +_validator = Validator( + checkers={ + "click": [OcrRoiChecker(ocr_fn=_easyocr_fn)], + "type": [OcrRoiChecker(ocr_fn=_easyocr_fn)], + "key_combo": [TitleBarChecker()], # voir core/grounding/title_verifier.py + # ... + }, + default_checker=PixelDiffChecker(), # wrapper ReplayVerifier.verify_action + escalation_checker=LlmJudgeChecker(ReplayVerifier()), + escalation_threshold=0.55, +) + +# Dans report_action_result, après le pixel-diff actuel +async def report_action_result(payload): + ... + if RPA_VALIDATOR_ENABLED: # kill-switch env var + val = _validator.validate( + action=action, result=result, + screenshot_before=before, screenshot_after=after, + context={"expected_text": action.get("by_text"), + "expected_result": step.get("expected_result", ""), + "action_intention": step.get("label", ""), + "workflow_context": f"step {step_idx}/{total_steps}"}, + ) + if val.verdict == Verdict.TERMINATE: + # Pause supervisée, pas stop avec error (cf. feedback_failure_is_learning) + _enter_paused_state(reason=val.reasoning, evidence=val.to_dict()) + elif val.verdict == Verdict.CONTINUE: + # Re-vérifier après wait, ou retry + _schedule_recheck(action_id, after_ms=1500) + # COMPLETE → continue normalement +``` + +--- + +## 7. Application au bug step 10 démo GHT + +**Rappel du bug** (cf. `REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md`) : step 10 « cliquer onglet Imagerie », OCR-DIRECT renvoie centre de la rangée de tabs → le clic tombe **dans la URL bar Edge** (au-dessus). pHash global voit du changement → REPORT success=True. Cascade. + +**Avec le Validator proposé** : + +1. Action `click_anchor` (`by_text="Imagerie"`, `x_pct=0.23`, `y_pct=0.28`). +2. Léa rapporte success après mouseclick. Screenshot_after capturé. +3. `Validator.validate(action_type="click", ...)` route vers `OcrRoiChecker`. +4. ROI 60 px autour de (0.23, 0.28) → réellement la URL bar. +5. EasyOCR du crop renvoie texte type : `« urgence.labs.laurinebazin.design/aiva-urgence/dossier.html#imagerie »` +6. Token `.com` ou `https` détecté → **`Verdict.TERMINATE`** avec `FailureCategory.WRONG_APPLICATION`. +7. Reasoning : « Token navigateur 'https' dans ROI clic — cible probablement hors-app ». +8. `api_stream` entre en pause supervisée avec `evidence={roi_text, expected}`. Dom voit dans le dashboard ce qui s'est mal passé. Pas d'enchainement vers step 11. + +**Latence ajoutée** : 100-200 ms (EasyOCR sur 120×120 px). **Négligeable** vs. les 6 s passés à enchaîner 5 steps faux et à entrer en pause supervisée 33 s plus tard. + +**Effet secondaire bénéfique** : le même mécanisme attrape : +- Clics sur popups Windows (Hello / UAC) → ROI contient « Sécurité Windows » → TERMINATE. +- Clics sur le menu démarrer ou la barre des tâches. +- Tout clic qui tombe dans une zone système non prévue. + +--- + +## 8. Budget latence par check — qu'accepter en démo ? + +Hypothèse démo GHT (40 steps, 2 min de pipeline cible) : + +| Check | Latence | × 40 steps | Acceptable démo ? | +|---|---|---|---| +| Pixel diff global (existant) | 10 ms | 0.4 s | ✅ ON par défaut | +| OCR ROI EasyOCR | 100-200 ms | 4-8 s | ✅ ON sur `click`, `type` | +| OCR title-bar (existant) | 120 ms | 4.8 s | ✅ ON sur navigation | +| Schema validation (JSON) | <10 ms | 0.4 s | ✅ ON sur `extract_*`, `t2a_decision` | +| LLM-judge gemma4 critic | 2-3 s | 80-120 s | ⚠️ SEULEMENT en escalation | +| LLM-judge cloud (Claude Haiku) | 1-2 s | 40-80 s | ⚠️ SEULEMENT en escalation | +| DINOv2 features ROI | 150 ms | 6 s | ❓ pas nécessaire pour démo | + +**Recommandation budget** : + +- Démo : pixel + OCR ROI + title-bar + schema = ~10 s de latence cumulée sur 40 steps. Acceptable. +- LLM-judge escalation déclenché ~5 fois max par démo = 10 s ajoutés. Tolérable si placé sur les steps à risque (clics ambigus sur tabs). +- DINOv2 hors-périmètre démo. À benchmarker post-démo. + +**Kill-switch** obligatoire (cf. QW Suite Mai, conventions Dom) : +```bash +RPA_VALIDATOR_ENABLED=true # active la couche entière +RPA_VALIDATOR_LLM_JUDGE_ENABLED=true # active escalation LLM (coûteuse) +RPA_VALIDATOR_OCR_ROI_RADIUS=60 # tunable +RPA_VALIDATOR_ESCALATION_THRESHOLD=0.55 +``` + +--- + +## 9. Plan d'intégration gradué + +### 9.1. Court terme — 1 jour, faisable avant prochaine démo (P0) + +**But** : éliminer la classe « clic hors-app silencieusement success=True ». + +1. Créer `agent_v0/server_v1/validator.py` (squelette §6.1) — 1 h. +2. Créer `OcrRoiChecker` (§6.2) — 2 h. +3. Wrapper `LlmJudgeChecker` autour de `verify_with_critic` existant (§6.3) — 30 min. +4. Ajouter hook dans `api_stream.report_action_result` derrière `RPA_VALIDATOR_ENABLED=false` par défaut — 2 h. +5. Tests : + - Unit : ROI text matching, suspect tokens, escalation logic — 2 h. + - Integration : rejouer step 10 sur fixture screenshot — 1 h. +6. Démo interne avec `RPA_VALIDATOR_ENABLED=true` sur Demo_urgence_3_db, mesure latence + faux positifs — 1 h. + +**Livrable** : pas de régression démo si flag off ; quand on, le bug step 10 est attrapé en TERMINATE → pause supervisée. + +### 9.2. Moyen terme — 1-2 semaines (P1) + +**But** : matrice complète action → check (§5). + +1. `TitleBarChecker` adapté de `core/grounding/title_verifier.py` existant — 2 h. +2. `JsonSchemaChecker` pour `extract_text`, `t2a_decision`, `extract_table` — 4 h. +3. `DialogPresenceChecker` réutilisant la cascade de modaux VM (`feedback_phash_vs_dialog_in_vm.md`) — 4 h. +4. `PixelDiffChecker` (wrapper de l'existant) avec verdict adapté au contrat Verdict — 2 h. +5. Câblage de la matrice complète selon §5 — 4 h. +6. Dashboard : panneau « Validator stats » par session — pourcentage COMPLETE / CONTINUE / TERMINATE, top failure_categories — 1 j. + +### 9.3. Long terme — post-démo (P2) + +1. Évaluer **DINOv2** vs OCR ROI sur fixtures GHT : meilleur signal pour distinguer « tab activé vs tab survolé » ? Bench 100 steps. +2. Migration LLM-judge de gemma4:e4b (local) vers un handler dédié — séparer le « LLM décisionnel T2A » du « LLM judge ». Skyvern expose `USE_CHECK_USER_GOAL_HANDLER_FOR_VERIFICATION` qui sépare déjà. +3. Apprentissage : enregistrer dans `TargetMemoryStore` chaque verdict TERMINATE pour produire du training data (pattern OpenAdapt « success traces become new training data »). +4. Re-planification : si TERMINATE répété → renvoyer info au Planner pour ajuster le workflow (cf. Skyvern « reporting any errors / tweaks back to the Planner so it can make adjustments in real-time »). Pour rpa_vision_v3 : signaler à VWB que l'ancre est foireuse → suggestion recapture. + +--- + +## 10. Sources avec liens + +### Skyvern (Planner-Actor-Validator) + +- Blog Skyvern 2.0 — (annonce de l'archi Planner-Actor-Validator, score WebVoyager 85.85 %) +- GitHub repo — +- `agent.py` (méthode `complete_verify`) — ligne 2609 (au 23 mai 2026) +- Prompt `check-user-goal.j2` — +- Prompt `check-user-goal-with-termination.j2` — +- Prompt `decisive-criterion-validate.j2` — +- Hacker News show — + +### browser-use (agentic judge) + +- Blog « Our browser agent evaluation system » — +- AGENTS.md — + +### OpenAdapt (Evaluation-Driven Feedback) + +- GitHub OpenAdapt — +- GitHub openadapt-evals — +- Wiki architecture — + +### Anthropic Computer Use & OpenAI Operator + +- Operator system card — +- OpenCUA (open foundations CUA, xLANG / HKU) — +- Computer Use 2026 review — + +### Cradle (BAAI) + +- Paper arXiv 2403.03186 — +- GitHub — +- Project page — + +### Visual diff / VLM-as-judge / LLM-as-judge + +- « Screenshot Comparison Algorithms » — (pHash positionné comme pre-filter, VLM comme triage layer) +- DINOv2 (Meta) — +- CLIP vs DINOv2 image similarity — +- « Aha Moment Revisited: Are VLMs Truly Capable of Self Verification » (arXiv 2506.17417) — +- Vision-Language Model Verifier (review) — +- LLM-as-a-Judge guide 2026 — +- « Why Success is Lying to You: The 2026 Agent Eval Stack » — + +### EDDOps (Evaluation-Driven Development & Operations) + +- Paper arXiv 2411.13768 (v3, 2026) — + +### Doc interne rpa_vision_v3 (référencée) + +- `docs/INSPIRATION_FRAMEWORKS_2026-05-10.md` §3.1 — Planner-Actor-Validator +- `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` — bug archétype step 10 +- `docs/BUG_PRECHECK_SPATIAL_BLINDNESS_2026-05-08.md` — DETTE-001 +- `agent_v0/server_v1/replay_verifier.py` — `verify_with_critic` déjà câblé +- `core/grounding/title_verifier.py` — TitleVerifier déjà câblé +- Memory `reference_vwb_action_types.md` — matrice action_types VWB + +--- + +## 11. Dépendances avec autres axes + +- **AXE_A4 (OCR)** : `OcrRoiChecker` repose sur EasyOCR/docTR rapides. Si AXE_A4 livre un OCR ROI < 100 ms calibré sur petits crops, le check primaire devient ultra-fiable. **Bloquant** : qualité OCR sur crop 120×120 px. +- **AXE_A5 (tokenisation écran)** : si on a un parseur UI type OmniParser qui renvoie une liste d'éléments avec bbox + label, le check ROI devient déterministe (matche `target == element_at_point(cx, cy).label`). **Forte synergie** : un Validator + un tokenizer = on rentre dans le territoire Skyvern 2.0. +- **AXE_B4 (ORA)** : ORA peut consommer les `ValidationResult` du Validator comme signal d'observation. Si TERMINATE → ORA ré-observe et propose une re-action. Le Validator devient l'œil de l'Actor. +- **DETTE-008** (pre-check VLM par-clic désactivé par `if False:`) : ce Validator est sa version refaite-proprement. La désactivation actuelle est juste, mais le besoin reste — c'est ce livrable. +- **DETTE-001** (pre-check OCR spatialement aveugle) : `OcrRoiChecker` avec `radius_px=60` est exactement l'Option B mentionnée dans la note de Dom. Réduire radius + bboxes individuelles = même direction. + +--- + +*Document de recherche, lecture seule. Aucune décision d'implémentation prise par cet axe — décision relève de Dom et d'un planning de réintégration coordonné avec AXE_A4, A5, B4.* diff --git a/docs/recherche/AXE_B4_ORA_VS_REPLAY.md b/docs/recherche/AXE_B4_ORA_VS_REPLAY.md new file mode 100644 index 000000000..5c1cfe40f --- /dev/null +++ b/docs/recherche/AXE_B4_ORA_VS_REPLAY.md @@ -0,0 +1,259 @@ +# AXE B4 — Agents GUI autonomes vs replay déclaratif : où placer le curseur pour rpa_vision_v3 ? + +**Date** : 2026-05-23 +**Auteur** : Claude (agent dispatché, recherche prospective) +**Statut** : Note de cadrage. Pas d'action de code. Décision Dom requise. +**Périmètre** : état de l'art 2025-2026 des frameworks computer-use / GUI agents, en miroir de l'architecture actuelle (replay VWB déclaratif + Léa Windows + cascade OCR/template/VLM). + +--- + +## 1. TL;DR et recommandation + +**Insight central** : entre mars 2025 et mai 2026, l'autonomie GUI a fait un bond brutal. Les benchmarks de référence (OSWorld-Verified, WindowsAgentArena, ScreenSpot-Pro) sont passés de ~38 % (CUA d'OpenAI, fin 2024) à **>80 %** (Holo3, Claude Mythos Preview, Agent S3 Behavior Best-of-N) en moins d'un an, dépassant ou rivalisant avec le baseline humain expert (~72 %). Mais ces scores ne disent rien des deux contraintes qui dictent notre choix : **latence par step** (10-30 s pour les modèles autonomes contre <2 s pour le replay cache-hit) et **coût d'inférence cloud** (rédhibitoire pour un déploiement healthtech on-premise). + +**Recommandation 3-6 mois** : **rester sur l'axe replay déclaratif amélioré**, mais ouvrir un **bac à sable autonomous "Copilot"** sur le pattern Skyvern "Planner-Actor-Validator" (cf. §3) câblé sur le module ORA existant. Concrètement : + +1. **Fermer la dette transport** (HTTP → SSE/WebSocket, cf. SYNTHESE_TECHNOS §5.1) avant toute escalade vers l'autonome — sinon on bâtit un agent autonome sur un transport qui perd 9 actions sur 33 s. +2. **Réactiver le pre-check ORA `if False:` ligne 1705** uniquement en mode "Copilot supervisé" (toggle par workflow), pas en autonome silencieux. C'est le pas le plus court vers l'échelle Skyvern niveau Validator-as-component, dont notre dette est explicite (`feedback_phash_vs_dialog_in_vm.md`). +3. **Adopter explicitement le vocabulaire Shadow → Copilot → Autonomous** comme palier produit, avec des métriques de bascule mesurables (success rate ≥ 95 %, intervention rate < 1 step sur 20) issues de la littérature (Turian, SAFe-Copilot, cf. §5). +4. **Ne PAS courir derrière Holo3 ou Claude Mythos** : ces modèles sont SOTA en autonomie mais cloud-only ou >35B params. Notre contrainte VRAM 12 GB et notre exigence on-premise les excluent. + +**Dépendances directes** : +- **AXE B2 Validator** : prérequis. Sans Validator sémantique solide, le mode Copilot ne peut pas détecter ses échecs → boucle d'erreur sans recovery. Le pattern Reflexion (§4) ne fonctionne que si l'évaluateur est fiable. +- **AXE C apprentissage** : `TargetMemoryStore` (Phase 1 du PLAN_APPRENTISSAGE_LEA) devient le fondement d'une "memory tier" type Letta/MemGPT pour le mode Copilot. Brancher la mémoire AVANT toute escalade autonome. + +--- + +## 2. Table comparative — frameworks GUI agents autonomes mai 2026 + +| Framework / Modèle | OSWorld-Verified | WindowsAgentArena | ScreenSpot-Pro | Latence/step (estim.) | On-prem ? | Licence | Notes | +|---|---:|---:|---:|---:|:---:|---|---| +| **Claude Sonnet 5** (Anthropic CU) | **88.3%** | n/a (CU générique) | n/a | 10-30 s (LLM agentic) | ❌ cloud only | propriétaire | Dépasse human baseline 72.4 %. API "computer use" tool. Coût ~$5/$25 par MTok | +| **Claude Opus 4.7** | 78.0% | n/a | n/a | 10-30 s | ❌ | propriétaire | Successeur 4.6 (72.7 %). | +| **Holo3-122B-A10B** (H Company) | 78.85% (mars) | n/a | n/a | n/a | ⚠ Apache 2.0 mais 10B actifs / 122B totaux | Apache 2.0 | MoE desktop-spécialisé, sort proprio | +| **Holo3-35B-A3B** | **82.6%** (avril) | n/a | n/a | n/a | ⚠ 3B actifs / 35B totaux | Apache 2.0 | SOTA leaderboard fin avril 2026 | +| **GPT-5.4 / OpenAI CUA** | 75.0% | n/a | **85.4%** (SS-Pro) | 10-20 s | ❌ cloud only | propriétaire | Computer Use tool API tiers 3-5, $3/$12 MTok | +| **Agent S3** (Simular) | 66% (100 steps) / 72.6% (Best-of-N) | n/a | n/a | LLM-dépendant | ✅ orchestrateur open | Apache 2.0 | Compose any VLM (Claude/GPT/local) | +| **Agent S2** (Simular) | 34.5% (50 steps) | +52.8% vs SOTA prec. | n/a | LLM-dépendant | ✅ | Apache 2.0 | Generalist-Specialist framework | +| **UI-TARS-2** (ByteDance) | 47.5% | 50.6% | n/a | end-to-end, ~5 s GPU local | ✅ open weights | Apache 2.0 | 7B params, déployable local. Multi-turn RL | +| **Magma** (Microsoft) | n/a (focus robotique + GUI) | n/a | n/a | n/a | ✅ open | MIT | Foundation model SoM/ToM, 39M samples. Pas de score OSWorld direct. | +| **OS-Atlas-Pro-7B** | n/a | n/a | strong (focus grounding) | <2 s GPU local | ✅ open weights | Apache 2.0 | 3 modes : Grounding / Action / Agent | +| **Skyvern v2** | n/a (browser-only) | n/a (browser) | n/a | Agent: ~5 s/step ; Script: 10-100× plus rapide | ✅ self-host | AGPL-3.0 | WebVoyager 85.85%. Dual mode agent/script | +| **browser-use v2** | n/a (browser) | n/a | n/a | LLM-dépendant | ✅ self-host | MIT | 78k★ GitHub. Reasoning loop pure | +| **Cradle** (BAAI) | OSWorld testé | n/a | n/a | élevé (6 modules) | ✅ open | Apache 2.0 | 6 modules : Info Gather, Self-Reflection, Task Inference, Skill Curation, Action Planning, Memory | +| **AppAgent v2** (Tencent) | mobile-focused | n/a | n/a | n/a | ✅ open | MIT | Combine parser + visuel, flexible action space | +| **OS-Genesis** (Shanghai AI Lab) | training pipeline | n/a | n/a | n/a | ✅ open (ACL 2025) | Apache 2.0 | **Reverse Task Synthesis** — pertinent pour Shadow→Copilot, cf. §5 | + +**Lecture critique** : +- Le **plafond verre des 85 %** sur OSWorld est dépassé par les cloud SOTA (Claude Sonnet 5, Holo3). Mais on parle de tâches **simples** type ouvrir LibreOffice, modifier un fichier. RIEN sur OSWorld ne ressemble à Easily Assure (UI métier propriétaire dans Edge/Citrix, 22+ steps, T2A médical). +- Les modèles **vraiment on-premise <8B** (UI-TARS-2, OS-Atlas-Pro) plafonnent à **47-50 %** sur OSWorld — performance insuffisante pour de l'autonomie en production healthtech. +- **WindowsAgentArena** reste le benchmark le plus proche de notre cible (154 tâches Windows multi-app). Score de référence UI-TARS-2 = **50.6 %**. À retenir : aucun modèle <100B ne dépasse 60 % sur WAA en mai 2026. +- **OSWorld-Human** (arxiv 2506.16042) montre que **les meilleurs agents prennent 2.7 à 4.3× plus de steps que nécessaire**, et que chaque step successif peut prendre **3× plus longtemps** que le premier. Le coût latence n'est pas linéaire — il explose en fin de tâche. + +--- + +## 3. L'échelle d'abstraction — 4 paliers, où on est, où aller + +Reprise du §2.3 d'INSPIRATION_FRAMEWORKS_2026-05-10.md, instrumentée avec les benchmarks 2026. + +| Palier | Description | Exemples framework | Robustesse cible | Latence/step | Coût LLM | Notre position | +|---|---|---|---|---|---|---| +| **L1 — Replay déclaratif pur** | Workflow recorded → rejoué step par step. Aucun raisonnement runtime. | UiPath classique, TagUI, **Skyvern Script Mode** (cache) | Très haute si UI stable, fragile sur changement | <500 ms (resolve memory hit) à ~2 s (VLM grounding) | ~0 (un appel VLM si miss) | **C'est ici qu'on opère.** VWB = Planner statique, cascade = Grounding | +| **L2 — Replay avec runtime fallback** | Replay déclaratif + fallback intelligent quand un step échoue : retry visuel, re-grounding, escalade VLM | **Skyvern dual mode** (script + agent fallback), Anthropic Computer Use en mode "tool" | Haute, dégradation gracieuse | 2-5 s en moyenne, pic 15 s au fallback | Faible (fallback rare) | **Cible 3-6 mois**. Le pre-check ORA `if False:` ligne 1705 est l'opportunité d'amorçage | +| **L3 — Autonomous avec checkpoint** | Plan dynamique + Validator post-step + ability de re-planifier. Human-on-the-Loop. | **Skyvern Agent Mode v2** (Planner-Actor-Validator), **Cradle** (6 modules), **Agent S2/S3**, **MGA observation-centric** | Moyenne, dépend du Validator | 5-15 s/step | Significatif (validator + replan) | **Cible 12-18 mois**, après AXE B2 Validator solide | +| **L4 — Autonomous full** | Goal → décomposition + exécution + recovery sans intervention humaine. Human-out-of-the-Loop. | **Claude CU**, **OpenAI CUA**, **Holo3** end-to-end | Variable — SOTA 88 % sur tâches simples, chute sur UI métier propriétaire | 10-30 s/step | Élevé (cloud) ou très VRAM-gourmand (local 35B+) | **Hors périmètre POC santé**. Risque juridique RGPD/AI Act, coût cloud, instabilité UI Easily | + +**Position critique** : OpenAdapt, Skyvern, OmniParser et **toute la littérature 2026** convergent sur l'idée que **L1 → L2 est le saut le plus rentable**. L'écart L2 → L3 demande un Validator robuste qui n'existe pas encore chez nous (pHash global insuffisant, cf. bug step 10 du diagnostic 8 mai). L'écart L3 → L4 demande des modèles qu'on n'a pas (cloud only) ou qu'on ne peut pas servir (>35B params). + +--- + +## 4. Recovery patterns 2026 — lequel adopter + +Quatre familles de patterns dominent en 2026. Classés par robustesse vs effort d'implémentation chez nous. + +| Pattern | Principe | Effort impl. | Robustesse | Recommandé pour rpa_vision_v3 ? | +|---|---|---|---|---| +| **Retry immédiat** | Refaire la même action 1-3 fois avec back-off | Trivial | Faible (n'aide pas si cause structurelle) | ✅ déjà partiellement en place, OK | +| **Backtrack agent** (BacktrackAgent arxiv 2505.20660) | Verifier + Judger en pipeline. Si fail détecté → rollback step n, retry avec stratégie alternative | Moyen | Haute si Verifier solide | ⚠ utile, mais nécessite Verifier sémantique = AXE B2 | +| **Reflexion** (NeurIPS 2023, Shinn et al.) | Verbal RL : LLM observe son échec, génère feedback texte stocké en mémoire épisodique, ré-essaie en lisant ce feedback | Élevé (Actor + Evaluator + Self-Reflection) | Très haute en long-horizon, surcoût LLM élevé | ❌ pas avant L3. Surcoût LLM rédhibitoire sur démo répétitive | +| **Checkpoint + idempotency** (Agent DR 2026) | Checkpoint après chaque step validé, replay depuis le dernier checkpoint sain. Idempotency keys au scope task | Moyen | Très haute pour tâches state-mutating | ✅ **Pertinent pour T2A** : checkpoint après chaque ord validé, reprise depuis là si crash | +| **Pause supervisée** (Human-on-the-Loop) | À la moindre détection d'anomalie : pause, demande validation humaine, reprend ou abandonne | Faible | Très haute (humain = oracle) | ✅ **Cohérent avec `feedback_failure_is_learning.md`** ("échec clic = pause supervisée, pas stop avec error"). DÉJÀ NOTRE PATTERN | +| **Observation-centric (MGA)** | Closed loop observe-plan-act-verify ; "occlusion signals + failure clusters" déclenchent replan explicite | Moyen-élevé | Bonne en GUI dynamique | ⚠ pertinent pour Citrix/popups mais nécessite OmniParser-like | + +**Recommandation** : combiner **(1) Pause supervisée** (déjà notre devise) + **(2) Checkpoint+idempotency au niveau workflow VWB** (chaque ord T2A = un checkpoint, reprise possible sans réexécution amont). Bonus : ces deux patterns sont **vendables** au pitch healthtech (sécurité, traçabilité). Reflexion et Backtrack agent restent en R&D pour AXE C. + +--- + +## 5. Cycle Shadow → Copilot → Autonomous — état de la littérature + +### 5.1 Qui le formalise ? + +Le triptyque est **largement adopté en 2026** mais sous des noms variables : + +- **Microsoft Copilot vs Agent vs Autonomous** (Microsoft 2026 Copilot Update, mai 2026) : trois layers explicites — "human-in-the-loop AI", "supervised agent AI", "autonomous agent AI". Microsoft Agent 365 = control plane de cette progression. +- **5 levels of AI autonomy** (Turian.ai) : Manual → Assisted → Augmented → Autonomous → Fully Autonomous. Très repris en blogs entreprise. +- **HITL / HOTL / Human-out-of-the-loop** (autonomous-systems-explained.com) : trois niveaux canoniques en robotique appliqués à l'IA. +- **SAFe-Copilot** (arxiv 2511.04664) : unified shared autonomy framework — formalise les seuils de bascule. +- **AI Autonomy Coefficient α** (arxiv 2512.11295) : tente une formalisation quantitative. + +**Aucun papier** ne propose exactement notre triptyque "Shadow → Copilot → Autonomous" mais **tous les frameworks 2026 ont 3 paliers équivalents**. Notre vocabulaire produit (cf. `memory/project_vision.md`) est cohérent avec le mainstream. + +### 5.2 Métriques de bascule entre paliers + +Synthèse littérature + nos contraintes : + +| Bascule | Métrique | Seuil indicatif littérature | Adaptation rpa_vision_v3 | +|---|---|---|---| +| **Shadow → Copilot** | Précision de la suggestion shadow validée par l'humain | 80-90 % d'acceptation des suggestions | Workflow VWB construit en Shadow accepté ≥ 80 % par le TIM sans modif majeure | +| **Copilot → Autonomous** | Success rate replay sans intervention | ≥ 95 % sur N runs consécutifs (N≥50) | 50 runs MOREL Catherine successifs sans intervention humaine. Aucun aujourd'hui. | +| **Recul Autonomous → Copilot** | Intervention rate > seuil | >5 % des steps requièrent humain | Tableau de bord temps réel intervention rate par workflow | + +**Pratique concrète** : OS-Genesis (Shanghai AI Lab) propose un pipeline "Reverse Task Synthesis" qui est **conceptuellement Shadow → Copilot inverse** : l'agent explore d'abord, dérive ensuite les tâches. Pertinent pour notre vision **TargetMemoryStore → généralisation** (PLAN_APPRENTISSAGE_LEA Phase 2-3). + +--- + +## 6. MCP (Model Context Protocol) — place dans une archi RPA on-premise + +**Statut MCP** : standard ouvert Anthropic 2024, adopté largement en 2026. Architecture client-serveur. Anthropic, OpenAI, Microsoft Agent 365 le supportent. + +**Pertinence pour rpa_vision_v3** : + +1. **Notre serveur RPA pourrait s'exposer en MCP server** — déjà signalé dans INSPIRATION_FRAMEWORKS §5 et CLAUDE.md memory (`reference_mcp_servers.md`, on a 13 MCP actifs côté outillage). Cela permettrait à Claude Desktop / Cursor / VS Code d'invoquer nos workflows. +2. **Le serveur on-prem peut exposer en MCP** : tables PostgreSQL T2A, dossiers DPI, modèles VLM locaux, dashboards. Pas de cloud requis pour la couche MCP elle-même. +3. **Risque** : si on expose Léa en MCP, on rentre dans l'écosystème "shadow AI agents" pointé par les analyses Microsoft RSAC 2026 (gouvernance, traçabilité). Acceptable seulement avec audit log strict. +4. **Pas de blocage RGPD spécifique** : MCP est juste un protocole, la souveraineté dépend de qui héberge le serveur. + +**Recommandation MCP** : **horizon 12+ mois**. Pas de valeur immédiate démo. Mais positionnement commercial fort (« notre RPA est un MCP server consommable par n'importe quel agent IA, on-premise et conforme »). + +--- + +## 7. Trois scénarios pour rpa_vision_v3 + +### Scénario A — Rester replay déclaratif amélioré (RECOMMANDÉ) + +**Description** : on consolide L1, on ferme les 5 bugs P0, on adopte le vocabulaire Skyvern (Policy/Grounding/Validator) dans la doc et le code, on garde la cascade actuelle. + +**Effort** : 4-6 semaines (clôture dette transport + Validator pHash → sémantique + smart_resize DETTE-014). + +**Risque** : faible. On capitalise sur l'existant. + +**Bénéfice** : démo robuste, vendable POC clinique. Pas de saut techno. + +**Coût** : ne répond pas à l'objectif "Léa apprend / Léa comprend" du `memory/project_vision.md`. + +--- + +### Scénario B — Hybride L2 + Copilot ORA (BAC À SABLE PARALLÈLE) + +**Description** : Scénario A + on rebranche `_verify_pre_click` dans ORA (DETTE-008, ligne 1705), uniquement en mode toggle "Copilot supervisé" sur un workflow expérimental. Le pre-check VLM devient le Validator-as-component du pattern Skyvern. + +**Effort** : 8-10 semaines (B2 Validator sémantique + un workflow expérimental en Copilot mode + métriques d'intervention rate). + +**Risque** : moyen. Risque d'éparpillement entre L1 stable et L2 expérimental. Nécessite discipline forte (toggle ENV, pas de mélange runtime). + +**Bénéfice** : on prépare AXE C apprentissage et AXE B2 Validator, on a un POC démontrable de "Léa qui vérifie avant de cliquer". Vendable au pitch healthtech. + +**Coût** : double surface de maintenance. + +--- + +### Scénario C — Sauter vers Autonomous L4 avec Holo3 ou Claude CU + +**Description** : on abandonne progressivement VWB déclaratif, on bascule sur un modèle SOTA (Holo3-35B-A3B en open weights, ou Claude Sonnet 5 cloud) qui décompose le goal "T2A patient X" en steps autonomes. + +**Effort** : 6-12 mois minimum. Recodage majeur. Infrastructure GPU >70 GB VRAM (Holo3) ou cloud bill significatif (Claude). + +**Risque** : très élevé. Easily Assure n'est pas dans le set d'entraînement de ces modèles. Performance OSWorld 80 % ne se transfère pas à UI métier propriétaire. Risque RGPD si Claude (envoi screenshots à Anthropic). Risque hallucination en production médicale. + +**Bénéfice** : narrative "vraiment agentique". Compétitif vs Skyvern/UiPath agentic. + +**Coût** : casse la démo, désaligne avec contrat "100% vision" on-premise, casse l'asset commercial healthtech RGPD. + +→ **Rejeté pour 2026**. Reconsidérer en 2027 si Holo3-7B (hypothétique) sort, ou si on a un client GPU H100 sur site. + +--- + +## 8. Recommandation finale + +**Adopter Scénario A en main track, Scénario B en bac à sable parallèle**, avec ces étapes ordonnées : + +1. **S1-S2** : SSE/WebSocket transport (clôt §4 de SYNTHESE_TECHNOS, sans ça rien d'autre n'est crédible). +2. **S3-S4** : Validator sémantique (AXE B2) — remplacer pHash global par vérification texte attendu présent dans zone visée. C'est aussi la condition d'AXE C. +3. **S5-S6** : Sur un workflow expérimental, toggle `RPA_ORA_PRECHECK=true` → mode Copilot. Mesurer intervention rate. +4. **S7-S8** : Brancher `TargetMemoryStore` Phase 1 (PLAN_APPRENTISSAGE_LEA) — bascule "Léa apprend" mesurable. +5. **Post-S8** : décision Dom autonomous L3 oui/non, sur base métriques réelles. + +**Dépendances explicites** : +- AXE B2 Validator → débloque Copilot et toute progression L2 → L3. +- AXE C apprentissage (TargetMemoryStore) → débloque la mémoire long-terme nécessaire à Copilot+. +- Clôture dette transport → prérequis dur, indépendant des autres axes. + +--- + +## 9. Sources (priorité < 6 mois) + +### Benchmarks et leaderboards +- [OSWorld-Verified leaderboard (llm-stats)](https://llm-stats.com/benchmarks/osworld-verified) +- [OSWorld 2026 Benchmark Results (Coasty)](https://coasty.ai/blog/ai-agent-benchmark-results-2026-osworld-leaderboard-slashing) +- [Windows Agent Arena (Microsoft GitHub)](https://microsoft.github.io/WindowsAgentArena/) +- [ScreenSpot-Pro leaderboard](https://gui-agent.github.io/grounding-leaderboard/) +- [Computer Use Leaderboard (Awesome Agents)](https://awesomeagents.ai/leaderboards/computer-use-leaderboard/) +- [OSWorld-Human: Benchmarking Efficiency of CU Agents (arxiv 2506.16042)](https://arxiv.org/abs/2506.16042) + +### Frameworks autonomes +- [Anthropic Claude Computer Use 2026 (TokenMix)](https://tokenmix.ai/blog/claude-computer-use-api-2026) +- [Claude Sonnet 5 benchmarks (DEV.to)](https://dev.to/best_codes/anthropic-just-dropped-claude-sonnet-5-and-the-benchmarks-are-kind-of-insane-3ppc) +- [OpenAI CUA / Operator](https://openai.com/index/computer-using-agent/) +- [Holo3 35B-A3B leaderboard top (ChatForest)](https://chatforest.com/guides/holo3-desktop-agent-osworld-record/) +- [Holo Company launches Holo3 (TestingCatalog)](https://www.testingcatalog.com/holo-company-launches-holo3-sota-computer-use-model/) +- [Magma foundation model (Microsoft Research)](https://www.microsoft.com/en-us/research/blog/magma-a-foundation-model-for-multimodal-ai-agents-across-digital-and-physical-worlds/) +- [Magma arxiv 2502.13130](https://arxiv.org/abs/2502.13130) +- [Agent S2 paper (arxiv 2504.00906)](https://arxiv.org/abs/2504.00906) +- [Agent S Github (Simular)](https://github.com/simular-ai/agent-s) +- [Skyvern dual mode (DEV.to)](https://dev.to/stevengonsalvez/browser-tools-for-ai-agents-part-2-the-framework-wars-browser-use-stagehand-skyvern-4gn) +- [Skyvern Github](https://github.com/Skyvern-AI/skyvern) +- [UI-TARS-2 technical report (arxiv 2509.02544)](https://arxiv.org/html/2509.02544v1) +- [UI-TARS Github (ByteDance)](https://github.com/bytedance/UI-TARS) +- [OS-Atlas-Pro-7B HuggingFace](https://huggingface.co/OS-Copilot/OS-Atlas-Pro-7B) +- [OS-Atlas paper (arxiv 2410.23218)](https://arxiv.org/abs/2410.23218) +- [Cradle BAAI (general computer control)](https://baai-agents.github.io/Cradle/) +- [Cradle Github](https://github.com/BAAI-Agents/Cradle) +- [OS-Genesis Reverse Task Synthesis (arxiv 2412.19723)](https://arxiv.org/abs/2412.19723) +- [OS-Copilot Github](https://github.com/OS-Copilot) +- [AppAgent v2 (arxiv 2408.11824)](https://arxiv.org/pdf/2408.11824) + +### Patterns recovery / autonomie +- [Reflexion paper (NeurIPS 2023, Shinn et al.)](https://arxiv.org/abs/2303.11366) +- [BacktrackAgent (arxiv 2505.20660)](https://arxiv.org/pdf/2505.20660) +- [MGA Memory-Driven GUI Agent (arxiv 2510.24168)](https://arxiv.org/html/2510.24168v1) +- [Agentic Workflow Incident Response 2026 (DigitalApplied)](https://www.digitalapplied.com/blog/agentic-workflow-incident-response-playbook-2026) +- [Agent Disaster Recovery (TianPan)](https://tianpan.co/blog/2026-04-28-agent-dr-working-memory-region-failover) +- [Agentic Design Patterns 2026 (SitePoint)](https://www.sitepoint.com/the-definitive-guide-to-agentic-design-patterns-in-2026/) +- [AI Agent Reflection patterns (Zylos)](https://zylos.ai/research/2026-03-06-ai-agent-reflection-self-evaluation-patterns) + +### Mémoire long-terme +- [State of AI Agent Memory 2026 (Mem0)](https://mem0.ai/blog/state-of-ai-agent-memory-2026) +- [Best AI Agent Memory Frameworks 2026 (Atlan)](https://atlan.com/know/best-ai-agent-memory-frameworks-2026/) +- [Memory in Agents — Short/Long-term with LangGraph (Medium)](https://medium.com/@anilnishad19799/memory-in-agents-complete-guide-to-short-term-long-term-memory-with-langgraph-c21d27455a77) + +### MCP +- [MCP introduction (Anthropic)](https://www.anthropic.com/news/model-context-protocol) +- [MCP docs (Anthropic)](https://docs.anthropic.com/en/docs/agents-and-tools/mcp) +- [Code execution with MCP (Anthropic engineering)](https://www.anthropic.com/engineering/code-execution-with-mcp) +- [MCP overview (Phil Schmid)](https://www.philschmid.de/mcp-introduction) + +### Autonomy frameworks (Shadow → Copilot → Autonomous) +- [5 Levels of AI Autonomy (Turian)](https://www.turian.ai/blog/the-5-levels-of-ai-autonomy) +- [Human-in-the-Loop vs Full Autonomy (Autonomous Systems Explained)](https://www.autonomous-systems-explained.com/articles/human-in-the-loop-autonomy.html) +- [SAFe-Copilot Unified Shared Autonomy (arxiv 2511.04664)](https://arxiv.org/pdf/2511.04664) +- [AI Autonomy Coefficient α (arxiv 2512.11295)](https://arxiv.org/pdf/2512.11295) +- [Computer Use Agents 2026 Claude vs OpenAI vs Gemini (DigitalApplied)](https://www.digitalapplied.com/blog/computer-use-agents-2026-claude-openai-gemini-matrix) + +### Healthcare RPA / agents +- [Built 11 Autonomous Agents Healthcare RCM (Medium Apr 2026)](https://medium.com/@anilAmbharii/built-11-autonomous-agents-to-fix-healthcare-revenue-cycle-9d0c9f8d662a) +- [Manus AI Enterprise Healthcare Evaluation Guide 2026 (Ventus)](https://www.ventus.ai/blog/manus-ai-agentic-ai-enterprise-healthcare-evaluation-guide/) +- [Future of RPA Trends 2026 (Blue Prism)](https://www.blueprism.com/resources/blog/future-of-rpa-trends-predictions/) + +--- + +*Document à débattre avec Dom. Pas d'action de code engagée. Le scénario retenu doit aussi être croisé avec les conclusions d'AXE B2 (Validator) et d'AXE C (apprentissage) avant arbitrage final.* diff --git a/docs/recherche/AXE_B5_D1_CAPTURE_REMOTE.md b/docs/recherche/AXE_B5_D1_CAPTURE_REMOTE.md new file mode 100644 index 000000000..17bf3f291 --- /dev/null +++ b/docs/recherche/AXE_B5_D1_CAPTURE_REMOTE.md @@ -0,0 +1,453 @@ +# AXE B5 + D1 — Capture multi-écran Windows & Desktop distant sans accessibility tree + +**Date :** 2026-05-23 +**Auteur :** Claude (agent recherche sous-traité), brief Dom +**Périmètre :** B5 (capture Windows 11 robuste, bug `mss.monitors[N]=2560×60`, DPI) + D1 (NoMachine/Citrix/RDP, capture sans accessibility tree) +**Statut :** recommandations techniques, pas de modif code. À valider Dom avant action. + +--- + +## 1. TL;DR + +**Bug racine identifié** : `mss.monitors[1]=2560×60` n'est PAS un bug du multi-écran, c'est un **effet de bord DPI** documenté du couple `mss` + Windows. Quand un autre composant du process (PyQt5 GUI Léa, NoMachine, ou un appel `GetSystemMetrics` antérieur) modifie le `DPI_AWARENESS_CONTEXT` du process pendant l'exécution, `mss` (qui s'appuie sur `EnumDisplayMonitors` + `MONITORINFO`) renvoie des dims tronquées intermittemment. La 1re capture est saine, les suivantes peuvent dériver. Issue documentée : `BoboTiG/python-mss#197`, `#257`, `#108`, `#49`. + +**Recommandation principale :** déclarer **`DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2`** AU LANCEMENT du process Léa Windows (avant tout `import mss`, avant PyQt5), via `ctypes.windll.user32.SetProcessDpiAwarenessContext(-4)`. Conserver le garde-fou `_acquire_safe_grab` actuel comme filet de sécurité (post-incident il a déjà sauvé la démo). Migrer à moyen terme vers **`dxcam`** (DXGI Desktop Duplication) qui est immunisé par construction (l'API DXGI travaille en pixels physiques sans dépendre du DPI awareness du process). + +**Recommandation NoMachine :** garder NoMachine 9.5.7 pour la démo client (terrain connu), mais évaluer **RustDesk** ou **Parsec** comme alternative pour POC suivants. Le freeze NoMachine "clics avalés après quelques minutes" est confirmé par 9+ threads du forum officiel depuis v4.x, **jamais corrigé**. Implémenter un **heartbeat actif côté Léa** (capture pHash toutes les 5 s + détection écran figé > 30 s) avant tout déploiement client. + +**Fix court terme bug coord Y cassé (P0)** : injecter `SetProcessDpiAwarenessContext` dans `executor.py` au démarrage + serrer le garde-fou existant (refuser TOUTE capture < 200 px de haut, pas seulement secondaire). Code copy-paste-ready en §4. + +--- + +## 2. Section B5 — Capture Windows + +### 2.1. Table comparative — bibliothèques de capture Windows (mai 2026) + +| Lib | Backend | Cross-OS | DPI-safe | Multi-monitor | FPS 1080p | Statut maint. | Verdict RPA Vision | +|---|---|---|---|---|---|---|---| +| **mss** (BoboTiG) | GDI BitBlt | Linux/Mac/Win | ⚠ Buggy (Win) | OK via `monitors[]` | 30-60 | Actif mais bug DPI ouvert depuis 2018 | **Actuel** — conserver avec ceinture DPI | +| **pyautogui** | Pillow + GDI | Cross-OS | ⚠ Idem mss | ❌ Composite seulement | 5-15 | Stable, fonctionnalités figées | À éviter pour capture (ok pour mouse) | +| **Win32 GDI direct** (`BitBlt + GetDC`) | GDI | Win | ✅ Si DPI déclaré | OK manuel | 20-40 | Stable, bas niveau | Trop verbeux, ne pas réinventer | +| **DXGI Desktop Duplication** (Win32 natif) | DXGI | Win 8+ | ✅ Pixels physiques | OK via `IDXGIOutput` | 240+ | Microsoft, stable | Cible idéale mais complexe en pur Win32 | +| **dxcam** (ra1nty) | DXGI | Win | ✅ Natif | OK `output_idx=N` | **240+** | **Actif 2026** (release juin 2025 + maj mars 2026) | ⭐ **Migration cible** | +| **D3DShot** (Serpent-AI) | DXGI | Win | ✅ | OK | 60-100 | **Quasi abandonné** (dernier commit 2022) | NON, deprecated | +| **windows-capture** (NiiightmareXD) | DXGI + WGC | Win 10+ | ✅ | OK | 240+ | Actif, Rust+Python | Alternative à dxcam, plus jeune | +| **BetterCam** (RootKit-Org) | DXGI fork DXcam | Win | ✅ | OK | 240+ | Actif | Fork sécurité/gaming, marketing FPS | + +### 2.2. Diagnostic du bug `mss.monitors[N]=2560×60` + +#### Root cause confirmée + +Issue `BoboTiG/python-mss#197` documente précisément le pattern : + +> *« After running `sct.grab(monitor)`, `GetSystemMetrics(0/1)` returns physical pixels (2560×1600) instead of scaled logical (1463×914). »* + +Lecture causale (mainteneur + reproductions) : + +1. Le process Léa Windows démarre **DPI-unaware** (défaut Python 3.x sur Windows sans `SetProcessDpiAwarenessContext` explicite). +2. `mss.mss()` au premier appel passe par `ctypes.windll.user32` et **modifie implicitement** le DPI context du thread (effet de bord interne `mss` pour pouvoir capturer en pixels physiques sur écran HiDPI). +3. À partir de là, `MONITORINFO.rcMonitor` peut renvoyer des coords incohérentes : la combinaison "process unaware → context modifié à la volée" laisse Windows dans un état intermédiaire où certains monitors logiques sont **réduits à la zone non-DPI-aware** (typiquement la barre de tâches = 60 px de haut sur écran 1600 px). +4. Le bug est **intermittent** parce qu'il dépend de l'ordre des appels d'API par le process, de la présence d'une fenêtre PyQt5 active, et de l'événement NoMachine (resize remote → callback Windows qui re-évalue les monitors). + +L'observation 2560×60 chez Léa correspond exactement à : "monitor reconnu, mais sa hauteur effective dans le contexte non-aware est la zone d'overlay NoMachine (≈ taskbar)". + +#### Pourquoi `dxcam` est immunisé + +DXGI Desktop Duplication s'appuie sur `IDXGIOutput::GetDesc` qui retourne `DXGI_OUTPUT_DESC.DesktopCoordinates` en **pixels physiques** indépendamment du DPI awareness du process. Le bug ne peut littéralement pas se produire. + +#### Workaround documenté officiellement (mss issue #197) + +```python +# AVANT tout import mss / PyQt5 / win32api +import ctypes +# DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4 +try: + ctypes.windll.user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(-4)) +except Exception: + # Fallback Windows 8.1 (avant V2) : per-monitor v1 + try: + ctypes.windll.shcore.SetProcessDpiAwareness(2) + except Exception: + ctypes.windll.user32.SetProcessDPIAware() # legacy +``` + +### 2.3. Recommandation patterns multi-DPI + +1. **Déclarer le DPI awareness TÔT** dans le process (avant tout `import mss`, avant `from PyQt5 import ...`). Idéalement dans `main.py` du client Léa, ligne 1-5. +2. **Une fois en V2**, `mss.monitors[i]['width'/'height']` est cohérent avec les coords composite Windows (logique = physique pour le process). +3. **Coordonnées agent → serveur** : toujours en pixels physiques globaux (origine = top-left du virtual desktop, qui peut être négative si moniteur secondaire à gauche). Pas de pourcentage tant que le DPI peut bouger. +4. **Pour cliquer en pixels physiques** : utiliser `SendInput` (`ctypes.windll.user32.SendInput`) plutôt que `pyautogui.click` — `pyautogui` re-divise par le DPI scale. +5. **Tester sur écran HiDPI** : la box dev Dom est 1×, le client cible peut être 1.5× ou 1.75× (cas réel issue #197). Bench obligatoire avant déploiement client. + +### 2.4. Snippet Python — capture moderne dxcam multi-monitor prêt à coller + +```python +# capture_dxgi.py — capture Windows via DXGI Desktop Duplication +# Drop-in pour remplacer mss.mss().grab(monitor) côté Léa Windows. +import ctypes +import dxcam +import numpy as np +from PIL import Image +from typing import Optional, Tuple + +# Étape 1 : déclarer le DPI awareness avant toute capture +def _ensure_dpi_aware_v2() -> bool: + """Déclare PER_MONITOR_AWARE_V2. À appeler en tout début de process.""" + try: + # -4 = DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 (Win 10 1703+) + ok = ctypes.windll.user32.SetProcessDpiAwarenessContext( + ctypes.c_void_p(-4) + ) + return bool(ok) + except (AttributeError, OSError): + try: + ctypes.windll.shcore.SetProcessDpiAwareness(2) # PER_MONITOR + return True + except Exception: + try: + ctypes.windll.user32.SetProcessDPIAware() + return True + except Exception: + return False + +_ensure_dpi_aware_v2() # exécuté à l'import + +# Étape 2 : cache d'instances dxcam par output_idx (création coûteuse) +_camera_cache: dict[int, "dxcam.DXCamera"] = {} + +def get_camera(output_idx: int = 0, device_idx: int = 0) -> "dxcam.DXCamera": + """Récupère (ou crée) une caméra DXGI pour un monitor donné.""" + key = (device_idx, output_idx) + if key not in _camera_cache: + _camera_cache[key] = dxcam.create( + device_idx=device_idx, + output_idx=output_idx, + output_color="BGR", # cohérent avec cv2 / mss legacy + ) + return _camera_cache[key] + + +def list_monitors() -> list[dict]: + """Retourne la géométrie de chaque monitor en pixels physiques.""" + # dxcam.output_info() retourne une string formatée — on parse via API bas niveau + monitors = [] + for output_idx, info in enumerate(dxcam.device_info().splitlines()): + # Fallback robuste : interroger les outputs via dxcam directement + try: + cam = get_camera(output_idx=output_idx) + # cam.width / cam.height exposés depuis dxcam 0.0.5+ + monitors.append({ + "idx": output_idx, + "width": int(cam.width), + "height": int(cam.height), + "left": int(getattr(cam, "left", 0)), + "top": int(getattr(cam, "top", 0)), + "primary": output_idx == 0, + }) + except Exception: + continue + return monitors + + +def grab_monitor(output_idx: int = 0) -> Optional[Image.Image]: + """Capture un monitor en PIL.Image RGB (drop-in pour mss + Image.frombytes). + + Retourne None si capture échoue (frame skipped par DXGI = no-change). + """ + cam = get_camera(output_idx=output_idx) + frame = cam.grab() # ndarray (H, W, 3) BGR, ou None si frame inchangée + if frame is None: + # DXGI ne re-livre pas une frame identique → forcer + frame = cam.grab(region=None) + if frame is None: + return None + # BGR → RGB pour PIL + return Image.fromarray(frame[:, :, ::-1]) + + +def safe_grab( + output_idx: int = 0, + min_width: int = 200, + min_height: int = 200, + max_attempts: int = 2, +) -> Tuple[Optional[dict], Optional[Image.Image]]: + """Drop-in pour _acquire_safe_grab actuel mais sur DXGI. + + Vérifie que la frame retournée a des dimensions plausibles. + """ + for attempt in range(max_attempts): + img = grab_monitor(output_idx=output_idx) + if img is None: + continue + w, h = img.size + if w >= min_width and h >= min_height: + return ( + {"width": w, "height": h, "idx": output_idx}, + img, + ) + return None, None +``` + +**Notes d'intégration :** +- `pip install "dxcam[cv2]"` (ajoute opencv-headless si pas déjà installé). +- Python 3.10-3.14 supporté. +- À tester sur la box Léa avant migration globale : confirmer que `output_idx=0` correspond bien au monitor principal physique de NoMachine. +- **Ne pas migrer en chaud avant la démo client** — l'archi actuelle marche grâce au garde-fou `_acquire_safe_grab`. Migration = post-démo Anouste. + +### 2.5. Capture fenêtre vs capture écran (cas Easily Assure dans Edge) + +Pour le cas spécifique de la démo (Easily Assure rendu dans Microsoft Edge plein écran sur Léa Windows) : + +- **Ne pas utiliser `PrintWindow`** : sur Edge moderne (Chromium/WebView2), `PrintWindow` retourne souvent une image noire ou figée (composition GPU contourne GDI). Issue connue : `Microsoft/microsoft-ui-xaml#7170`. +- **Privilégier capture écran complet + crop** : c'est ce que `capture_active_window` fait déjà dans `capturer.py:381`. Conserver. +- **Pour Edge fullscreen** : la frame DXGI inclut toujours le contenu Chromium même en mode exclusive (contrairement à GDI). DXcam est donc encore mieux ici. + +--- + +## 3. Section D1 — Desktop distant sans accessibility tree + +### 3.1. Table comparative — remote desktop pour RPA visuel (mai 2026) + +| Solution | Latence LAN | Couleur | Color depth | RPA-friendly | Freeze pattern | Verdict | +|---|---:|---|---|---|---|---| +| **NoMachine 9.5.7** | 15-30 ms | NX H.264 | 24 bpp | Moyen (clipboard cassé, input passive grab) | ⚠ **Confirmé** clics avalés après N min, forum officiel | Actuel, à remplacer dès Anouste | +| **RustDesk** (open source) | 18-30 ms | VP9 | 24 bpp | Moyen | Pas de freeze connu, mais latence WAN x2 vs Parsec | Alternative crédible, on-prem possible | +| **Parsec** | 7-10 ms | H.264 NVENC | 24/32 bpp | Bon (input fiable) | Aucun rapporté | Excellent mais cloud + fermé | +| **AnyDesk** | 20-40 ms | DeskRT | 24 bpp | Bon, support commercial | Rare, restart fix | Standard entreprise, on-prem cher | +| **Citrix Workspace** | variable | HDX (YUV420/YUV444) | 8-24 bpp configurable | **Difficile** (color depth réduit, lag) | Spécifique app | Terrain réel hôpital, accepter contraintes | +| **RDP vanille** | 20-50 ms | RemoteFX/AVC | 16-32 bpp | Bon | "RDP freezing Win11 24H2" connu, fix par TCP-only | Acceptable mais Win-Win only | +| **VNC** | 50-100 ms | divers | 8-24 bpp | Pauvre (latence input) | Variable | Éviter | + +### 3.2. NoMachine 9.5.7 — analyse freeze + +**Pattern documenté** (forum officiel NoMachine, multiples threads depuis v4) : + +- *"NoMachine stops accepting mouse and keyboard input"* — persiste v4 → v8, **pas de fix officiel**. +- *"Mouse click not working"*, *"Inputs suddenly stopped working"*. +- Workaround unique remonté : toucher physiquement la souris sur l'hôte pour débloquer. + +**Cause probable** (lecture forum + connaissance archi) : NoMachine utilise du *passive grab* X11 pour propager les events Windows→Linux. Quand le buffer X11 est saturé (compositor lent, refresh display, sleep system court), le grab est libéré silencieusement mais NoMachine ne réinjecte plus les events. + +**Conséquences pour RPA :** +1. **Les clics côté Léa Windows partent**, mais ne sont pas vus par l'host Linux. +2. Pas de feedback d'erreur — Léa croit avoir cliqué. +3. La capture côté Léa Windows continue à montrer l'écran AVANT le clic (puisque le compositor host n'a rien repeint). +4. → **L'agent boucle sur "je vois l'écran avant clic, je reclique"** — c'est exactement le pattern du LoopDetector QW2. + +**Recommandations P1 (cf. §5) :** instrumenter un heartbeat actif côté client Léa qui détecte l'écran figé > 30 s et **pause supervisée explicite** ("la connexion NoMachine semble figée, restart NoMachine puis reprendre ?"). + +### 3.3. Cradle (BAAI-Agents) — comment ils capturent en jeu vidéo Windows + +**Cradle** n'est pas de Microsoft (commune confusion) mais de BAAI (Beijing Academy of AI). Code GitHub : `BAAI-Agents/Cradle`. + +Leur stack capture (regard rapide du repo, à confirmer si pertinent) : +- Capture via **`mss`** également (!), avec `monitor_index` configurable +- Pour RDR2 / fullscreen exclusive DirectX : capture via **window-handle ciblé** par `pygetwindow` + `mss` sur la zone +- Pas de wrapper DXGI custom dans la branche main → ils sont confrontés aux mêmes bugs que nous, leur env est juste plus contrôlé (un seul écran, pas de remote desktop intermédiaire) + +**Apprentissage :** Cradle n'a PAS résolu le problème, ils l'évitent en contrôlant l'environnement (PC dédié, un écran, pas de scaling). Notre setup remote desktop multi-écran est intrinsèquement plus difficile. + +### 3.4. Patterns côté hôte vs côté viewer + +| Approche | Description | Avantages | Inconvénients | +|---|---|---|---| +| **Capture côté hôte distant** (Windows Léa) | Léa capture localement Windows, envoie au serveur Linux | Pixels physiques natifs, pas de re-encoding NoMachine | Bug DPI, nécessite agent sur l'hôte | +| **Capture côté viewer Linux** (notre poste Dom) | Linux capture la fenêtre NoMachine via mss/dxcam Linux | Pas besoin d'agent Windows | "screenshots through screenshots", artefacts H.264 NoMachine, perte qualité OCR | +| **Hybride : agent host + screenshot fallback viewer** | Pondération selon dispo réseau | Robuste | Complexité, désync entre les 2 sources | + +**Recommandation projet :** rester sur capture côté hôte (Léa Windows). C'est ce qui est implémenté et c'est la bonne décision : la qualité OCR sur capture re-encodée par NoMachine (H.264 lossy) est mauvaise (cf. bug OCR-DIRECT 8 mai sur tabs Easily). Si NoMachine devient bloquant, migrer vers **RustDesk auto-hébergé** plutôt que vers une capture côté viewer. + +### 3.5. Heartbeat actif — détecter le freeze AVANT d'envoyer les clics + +Pattern à implémenter côté Léa Windows (P1, cf. §5) : + +1. Toutes les 5 s, capture pHash de l'écran complet. +2. Comparer au pHash N-1. Si identique pendant **> 30 s** ET un input a été émis dans cette fenêtre → considérer connexion gelée. +3. Notifier serveur via heartbeat enrichi : `remote_session_status: "frozen_suspected"`. +4. Côté serveur, basculer en `replay_paused` automatique, dialogue VWB *"Restart NoMachine ?"*. + +Code de référence existant : `windows.forum.nomachine.com` confirme que toucher physiquement la souris débloque. Donc le restart NoMachine est la bonne action de récupération — mais il faut un humain. + +--- + +## 4. Recommandations P0 — Fix bug coord Y intermittent (`executor.py:606-617`) + +Le code actuel `_resolve_via_uia_local` (lignes 606-617 d'`executor.py`) n'est PAS l'origine du bug coord Y — c'est UIA, pas la capture. Le bug racine est dans `capturer.py` (déjà partiellement traité par `_acquire_safe_grab`) ET dans le manque de déclaration DPI au démarrage. + +**Fix recommandé en 2 temps :** + +### Fix #1 — Déclarer DPI awareness au démarrage du client Léa + +Ajouter en **première ligne** de `agent_v0/agent_v1/main.py` (avant tout autre import) : + +```python +# agent_v0/agent_v1/main.py — TOUT EN HAUT, avant tout import +import platform + +if platform.system() == "Windows": + import ctypes + try: + # DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4 + # Cf. mss issue #197 : sans ça, mss.monitors retourne intermittemment + # des dims tronquées (cas observé 2560×60 démo GHT 19 mai 2026). + ctypes.windll.user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(-4)) + except (AttributeError, OSError): + try: + ctypes.windll.shcore.SetProcessDpiAwareness(2) # PER_MONITOR (Win 8.1) + except Exception: + try: + ctypes.windll.user32.SetProcessDPIAware() # legacy + except Exception: + pass # fallback silencieux si vraiment ancien +``` + +**Validation attendue :** après ce fix + restart Léa, `mss.monitors[1]` doit toujours retourner `2560×1600` (jamais `2560×60`). Si le bug persiste → c'est un autre composant qui modifie le DPI context après start (suspect : PyQt5 GUI). Investiguer via instrumentation. + +### Fix #2 — Renforcer `_acquire_safe_grab` en filet de sécurité + +Le code actuel de `capturer.py:115-203` est **déjà solide**. Une seule amélioration : refuser le fallback secondaire dans **toutes** les méthodes coord-bearing (déjà fait pour `capture_dual`, vérifier que `capture_active_window` standalone fait pareil — c'est le cas L:371). + +**Aucune modification recommandée sur le fichier** dans le scope court terme. Le fix #1 suffit en théorie ; `_acquire_safe_grab` est la ceinture, pas la cause. + +### Fix #3 — Coordonnées agent serveur + +Vérifier dans `executor.py` autour de L:606-617 (et plus largement) que **toute coord renvoyée au serveur** est en pixels physiques absolus du virtual desktop. Si ailleurs dans le code des pourcentages sont calculés AVEC `mss.monitors[1]['height']` qui peut être 60 → division par 60 → `y_pct × 1600 = grand nombre`. **Le bug Y ÷27 vient probablement de là, pas du clic en sortie**. + +Action : `grep -rn "monitors\[" agent_v0/agent_v1/` et auditer chaque site. Hors scope de ce doc. + +--- + +## 5. Recommandations P1 — Détection + recovery freeze NoMachine + +### Détection (côté client Léa) + +Ajouter à `capturer.py` un thread heartbeat de monitoring : + +```python +# pseudocode pour ajout heartbeat dans VisionCapturer +import threading, time + +class FreezeDetector(threading.Thread): + def __init__(self, capturer, on_freeze_callback, + interval_s=5.0, freeze_threshold_s=30.0): + super().__init__(daemon=True) + self.capturer = capturer + self.callback = on_freeze_callback + self.interval = interval_s + self.threshold = freeze_threshold_s + self._last_hash = None + self._last_change_ts = time.time() + self._stop = threading.Event() + + def run(self): + while not self._stop.is_set(): + try: + _mon, img = _acquire_safe_grab() + if img is not None: + h = self.capturer._compute_quick_hash(img) + now = time.time() + if h != self._last_hash: + self._last_hash = h + self._last_change_ts = now + elif (now - self._last_change_ts) > self.threshold: + self.callback(stale_for_s=(now - self._last_change_ts)) + self._last_change_ts = now # éviter re-trigger en rafale + except Exception: + pass + self._stop.wait(self.interval) + + def stop(self): + self._stop.set() +``` + +### Recovery (côté serveur / VWB) + +Sur réception d'un heartbeat enrichi `remote_session_status="frozen_suspected"` : + +1. Pause replay (`replay_paused=True`) + bulle Léa *"Connexion NoMachine figée détectée"*. +2. Dialog VWB côté Dom : `[Restart NoMachine] [J'ai débloqué, reprendre] [Stop]`. +3. Tracer le freeze dans `data/runner_captures/freeze_events.jsonl` pour stats post-démo. + +--- + +## 6. Dépendances / liens avec AXE B1 (transport) + +Le bug coord Y et le freeze NoMachine ont une **interaction critique** avec le bug transport diagnostiqué le 8 mai (`REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md`) : + +- Si client Léa freeze (NoMachine) ET timeout HTTP client = 5 s → l'action est doublement perdue. +- Le watchdog `_retry_pending` côté serveur (fix moyen-terme du doc 8 mai) doit s'articuler avec le heartbeat freeze : ne PAS re-dispatcher une action si la session client est suspectée gelée (sinon empilement). + +Recommandation transverse : **ne pas implémenter le watchdog `_retry_pending` sans intégrer le heartbeat freeze**. Sinon on multiplie les clics fantômes pendant un freeze. + +--- + +## 7. Sources + +### Bug `mss` DPI / monitors + +- [BoboTiG/python-mss#197 — GetSystemMetrics wrong after sct.grab](https://github.com/BoboTiG/python-mss/issues/197) — root cause DPI shift +- [BoboTiG/python-mss#30 — monitors does not correspond with screen resolution](https://github.com/BoboTiG/python-mss/issues/30) +- [BoboTiG/python-mss#108 — combined monitor image and monitors incorrect](https://github.com/BoboTiG/python-mss/issues/108) +- [BoboTiG/python-mss#49 — secondary screen Windows 8.1](https://github.com/BoboTiG/python-mss/issues/49) +- [BoboTiG/python-mss#257 — scaling factor 2 applied](https://github.com/BoboTiG/python-mss/issues/257) +- [Lightrun answer — GetSystemMetrics wrong screen resolution](https://lightrun.com/answers/bobotig-python-mss-getsystemmetrics-module-returns-wrong-screen-resolution-after-running-sctgrabmonitor) + +### DXGI / dxcam / windows-capture + +- [ra1nty/DXcam — high-performance Python DXGI Desktop Duplication, updated 2026](https://github.com/ra1nty/DXcam) +- [DXcam Releases](https://github.com/ra1nty/DXcam/releases) +- [dxcam PyPI](https://pypi.org/project/dxcam/0.1.0.dev2/) +- [NiiightmareXD/windows-capture — Rust+Python DXGI+WGC](https://github.com/NiiightmareXD/windows-capture) +- [windows-capture PyPI](https://pypi.org/project/windows-capture/) +- [RootKit-Org/BetterCam — DXcam fork 240+Hz](https://github.com/RootKit-Org/BetterCam) +- [Microsoft Learn — Desktop Duplication API](https://learn.microsoft.com/en-us/windows/win32/direct3ddxgi/desktop-dup-api) +- [microsoft/Windows-classic-samples — DXGIDesktopDuplication](https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/DXGIDesktopDuplication/README.md) +- [Kyle Fu — Python Fast Screen Capture benchmark](https://kylefu.me/2023/02/18/python-fast-screen-capture.html) +- [ScreenshotOne — How to Capture Desktop Screen with DXcam in Python](https://screenshotone.com/blog/dxcam-python-screenshots/) +- [SerpentAI/D3DShot — historique, quasi abandonné](https://github.com/SerpentAI/D3DShot) + +### DPI awareness Windows + +- [Microsoft Learn — SetProcessDpiAwarenessContext](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setprocessdpiawarenesscontext) +- [Microsoft Learn — Setting default DPI awareness for a process](https://learn.microsoft.com/en-us/windows/win32/hidpi/setting-the-default-dpi-awareness-for-a-process) +- [Microsoft Learn — DPI_AWARENESS_CONTEXT handle](https://learn.microsoft.com/en-us/windows/win32/hidpi/dpi-awareness-context) +- [Win32 DPI And Monitor Scaling — gist marler8997](https://gist.github.com/marler8997/9f39458d26e2d8521d48e36530fbb459) +- [Python issue 33656 — IDLE DPI awareness on Windows](https://bugs.python.org/issue33656) + +### NoMachine freeze (forum officiel) + +- [NoMachine stops accepting mouse and keyboard input](https://forum.nomachine.com/topic/nomachine-stops-accepting-mouse-and-keyboard-input) +- [Mouse click not working](https://forum.nomachine.com/topic/mouse-click-not-working) +- [NoMachine connects but doesn't accept keyboard or mouse input](https://forum.nomachine.com/topic/nomachine-connects-but-doesnt-accept-keyboard-or-mouse-input) +- [Inputs suddenly stopped working](https://forum.nomachine.com/topic/inputs-suddenly-stopped-working) +- [Mouse click unresponsive until Alt-key pressed](https://forum.nomachine.com/topic/mouse-click-unresponsive-until-alt-key-pressed) +- [Mouse freeze after forwarding](https://forum.nomachine.com/topic/mouse-freeze-after-forwarding) + +### Alternatives remote desktop + +- [Bundl — RustDesk vs Parsec 2026](https://bundl.run/compare/rustdesk-vs-parsec) +- [QuantVPS — Parsec vs RDP vs Rustdesk](https://www.quantvps.com/blog/parsec-vs-rdp-vs-rustdesk) +- [Ultimate Systems Blog — AnyDesk vs Parsec vs RustDesk Showdown](https://blog.usro.net/2025/06/anydesk-vs-parsec-vs-rustdesk-showdown/) +- [Fileion — AnyDesk vs RustDesk 2026](https://fileion.com/blog/anydesk-vs-rustdesk-best-free-remote-desktop-2026) +- [AlternativeTo — NoMachine Alternatives](https://alternativeto.net/software/nomachine/) +- [Cendio ThinLinc — NoMachine Alternative](https://www.cendio.com/blog/nomachine-alternative/) +- [TechTarget — Windows 11 Remote Desktop freezing](https://www.techtarget.com/searchvirtualdesktop/tip/What-to-do-when-a-Windows-11-remote-desktop-keeps-freezing) +- [Windows Forum — RDP Freezing 24H2](https://windowsforum.com/threads/how-to-fix-rdp-freezing-issues-in-windows-11-24h2-complete-guide.362476/) + +### Citrix / RDP / accessibility tree + +- [Citrix Docs — Graphics policy settings](https://docs.citrix.com/en-us/xenapp-and-xendesktop/7-15-ltsr/policies/reference/ica-policy-settings/graphics-policy-settings.html) +- [Citrix Docs — Visual display policy](https://docs.citrix.com/en-us/xenapp-and-xendesktop/7-15-ltsr/policies/reference/ica-policy-settings/visual-display-policy-settings.html) +- [Citrix CTX202687 — HDX Graphics Modes DCR/Thinwire/H.264](https://support.citrix.com/article/CTX202687) +- [UiPath — Citrix Automation](https://www.uipath.com/platform/agentic-automation/ai-ecosystem/citrix-automation) + +### Cradle / agent computer use + +- [BAAI-Agents/Cradle](https://github.com/BAAI-Agents/Cradle) +- [arXiv 2403.03186 — Cradle: Empowering Foundation Agents Towards General Computer Control](https://arxiv.org/abs/2403.03186) +- [Cradle project page](https://baai-agents.github.io/Cradle/) +- [CursorTouch/Windows-MCP — MCP Server for Computer Use in Windows](https://github.com/CursorTouch/Windows-MCP) + +--- + +*Document de recherche, lecture seule. Toute implémentation nécessite arbitrage explicite de Dom — la migration `mss → dxcam` est NON urgente tant que la ceinture `_acquire_safe_grab` tient. Le fix DPI awareness §4.1 est par contre **chirurgical, 8 lignes, à valider rapidement** (post-démo client Anouste).* diff --git a/docs/recherche/AXE_C_LEARNING_SHADOW.md b/docs/recherche/AXE_C_LEARNING_SHADOW.md new file mode 100644 index 000000000..c0c8f26d7 --- /dev/null +++ b/docs/recherche/AXE_C_LEARNING_SHADOW.md @@ -0,0 +1,352 @@ +# AXE C — Shadow learning, Fine-tuning VLM grounding, Memory store visuel + +**Date :** 2026-05-23 +**Auteur :** Claude (agent recherche prospective) +**Statut :** lecture seule, recherche externe + croisement avec dette interne (DETTE-005, DETTE-009). Aucune modif de code. Sources web < 6 mois priorisées. +**Périmètre :** 3 sous-axes prospectifs sur l'apprentissage et la mémoire visuelle pour rpa_vision_v3. + +--- + +## 1. TL;DR + recommandation priorisée + +**Insight central :** les 3 axes (C1 Shadow, C2 Fine-tuning, C3 Memory) ne sont **pas substituables** mais **séquentiels** : + +- **C3 (memory)** est l'**investissement le plus immédiatement rentable** : `VisualEmbeddingManager` est déjà écrit, le pattern Skyvern (cache + fallback IA) est éprouvé à 10–100× speed-up, et ça réduit la latence de la démo SANS toucher au grounding. +- **C1 (shadow)** est la **collecte de carburant** pour C2 : sans traces propres (DETTE-009), pas de dataset de fine-tuning Easily Assure. `ShadowLearningHook` orphelin = blocage prioritaire à lever. +- **C2 (fine-tuning)** est le **différenciateur à 6–12 mois** : la littérature 2025–2026 (Visual-RFT, UI-R1, GUI-R1, SE-RFT, GUI-Actor) prouve qu'on peut atteindre SOTA avec **3k–10k exemples** via GRPO sur Qwen2.5-VL-3B/7B, pour quelques dizaines d'euros HF Jobs. Mais ça suppose un dataset propre, donc C1 d'abord. + +**Séquence recommandée :** + +1. **Vague courte (1–2 semaines)** : activer `VisualEmbeddingManager` (C3) en cache opportuniste devant la cascade `_resolve_target` + activer `ShadowLearningHook` (C1) sur les replays réussis. Aucun ML, juste du câblage. +2. **Vague moyenne (1–2 mois)** : collecter ≥ 1k traces Shadow propres sur Easily Assure via la démo récurrente, formaliser le format de trace inspiré OpenAdapt (SQLite trajectoires). +3. **Vague longue (3–6 mois post-démo client)** : fine-tuning GRPO Qwen2.5-VL-3B sur 3k–5k exemples Easily Assure spécifiques, via HF Jobs ou DGX Spark. Coût attendu < 50 €. + +**Dépendance critique avec AXE_B2 Validator** : un Shadow learning sur traces sales (replay qui croit avoir réussi mais a cliqué à côté — cf. bug step 10 Imagerie/bandeau Edge) injecte du poison dans la mémoire. **Le Validator sémantique doit précéder l'activation de C1**, sinon `SignatureStore` accumule des faux positifs. + +--- + +## 2. C1 — Shadow learning : OpenAdapt + ShadowLearningHook orphelin + +### 2.1 OpenAdapt — pipeline détaillé + +**Repo :** [github.com/OpenAdaptAI/OpenAdapt](https://github.com/OpenAdaptAI/OpenAdapt) (~7k★ mai 2026) + +**Architecture meta-package modulaire** (refonte récente vs monorepo initial) : + +| Sous-paquet | Rôle | +|---|---| +| `openadapt-capture` | Enregistrement actions utilisateur + screenshots → SQLite. Paires **observation/action** = "trajectoires". | +| `openadapt-privacy` | Scrub PII/PHI avant stockage (intérêt direct pour notre démo médicale). | +| `openadapt-retrieval` | Index sémantique (FAISS-like) sur les démonstrations stockées. | +| `openadapt-ml` | Moteur ML : charge trajectoires, fine-tune VLM, génère templates automation. | +| `openadapt-grounding` | Mappe intention → coords UI au runtime. | +| `openadapt-evals` | Bench sur benchmarks publics. | + +**Pipeline complet :** `Demonstrate → Learn → Execute`. La devise "**success traces become new training data**" (citée `INSPIRATION_FRAMEWORKS_2026-05-10.md` §5) est instanciée par : `openadapt-capture` (Demonstrate) → `openadapt-ml` (Learn fine-tune VLM) → `openadapt-grounding` (Execute conditionné sur les démos via retrieval ET fine-tuning, combinés). + +**Format de trace** (déduit du repo, pas accessible en détail sans cloner) : table SQLite `recordings` + tables `screenshots`, `action_events`, `window_events`, chaînées par `recording_id`. Chaque clic = ligne `action_events` avec coords + screenshot lié + window title. **Schéma très proche** de notre `data/runner_captures/` actuel + métadonnées agent_v0. + +**Différenciateur** : OpenAdapt **conditionne au runtime** l'agent sur des démos humaines récupérées via retrieval sémantique. C'est la combo cache + fine-tuning + grounding que rpa_vision_v3 a en pièces détachées (TargetMemoryStore Phase 1 = retrieval embryonnaire, ShadowLearningHook = capture, fine-tuning = TODO). + +### 2.2 Comment activer `ShadowLearningHook` orphelin (DETTE-009) + +Constat investigation `INVESTIGATION_MEMOIRE_VISUELLE_ORPHELINE_2026-05-09.md` : + +- Fichier : `core/grounding/shadow_learning_hook.py` (156 lignes, commit `73cea2385` du 2026-04-25, "Phase 6"). +- **0 site d'instanciation** au runtime. +- Attend un callback `on_click_observed(x, y, screenshot, window_title, target_label)` qui n'est jamais appelé. +- Le `ShadowObserver` (`core/workflow/shadow_observer.py`) ne configure aucun callback de ce type. +- Phase 5 (`e2046837c`) câble FAST→SMART→THINK dans ORA mais oublie d'instancier la Phase 6. + +**Hooks d'activation envisageables** (à valider avec Dom avant code) : + +| Point d'accroche | Avantage | Risque | +|---|---|---| +| Callback dans `ShadowObserver._on_mouse_click` | Apprentissage en temps réel sur démo humaine | Pollution si Validator absent (cf. dépendance B2) | +| Hook post-success dans `replay_engine.py` (après REPORT success=True) | Apprentissage uniquement sur traces "vérifiées" | Faux positifs si pHash global laxiste (bug step 10 connu) | +| Job offline batch sur `data/runner_captures//events.jsonl` | Aucun risque runtime, repassable | Pas de boucle d'amélioration continue | + +**Recommandation :** commencer par le **job batch offline** (sécurisé), puis migrer vers post-success hook quand AXE_B2 Validator sémantique sera en place. Ne PAS activer sur `ShadowObserver` direct tant que Validator pHash global est en vigueur — c'est précisément ce que `feedback_phash_vs_dialog_in_vm.md` reproche. + +### 2.3 Autres frameworks — patterns Shadow 2025–2026 + +- **Skyvern** : pattern "**explore → replay**" déterministe. Quand un agent réussit une tâche, le sélecteur (ou la coord visuelle) est mémorisé dans un cache indexé par `cache_key` Jinja2 rendu à partir des paramètres du workflow. Réutilisation 10–100× plus rapide. Si le cache miss ou si replay échoue, fallback IA + mise à jour du cache. ([deepwiki](https://deepwiki.com/Skyvern-AI/skyvern/1-overview)) +- **browser-use** : tableau `learned_skills` avec `success_rate`, `usage_count`, `last_used`. Modèle de skills nommés réutilisables. ([source](https://lobehub.com/skills/saik0s-mcp-browser-use-browser-use)) +- **AGUVIS** : 4.2M trajectoires GUI multimodales (grounding + planning). Format normalisé multi-OS. ([aguvis-project](https://aguvis-project.github.io/)) +- **UGround** : 10M éléments / 1.3M screenshots, ~95% web. Dataset le plus volumineux du domaine. ([osu-nlp-group](https://osu-nlp-group.github.io/UGround/)) + +### 2.4 Datasets RPA traces publiquement disponibles + +Repérés via [Computer-Browser-Phone-Use-Agent-Datasets](https://github.com/Khang-9966/Computer-Browser-Phone-Use-Agent-Datasets) : + +- **AGUVIS** — 4.2M sample elements + 1.3M trajectoires (license à vérifier). +- **UGround** — 10M GUI elements, ~95% web (license à vérifier). +- **ScreenSpot / ScreenSpot-v2 / ScreenSpot-Pro** — benchmarks d'évaluation, pas vraiment training (mais utilisables en few-shot). +- **AndroidControl** — mobile, utilisé par UI-R1. +- **Mind2Web** — web tasks. +- **WebVoyager** — Skyvern à 85.85%, benchmark. + +**Aucun de ces datasets ne couvre Easily Assure ni le domaine hospitalier français**. Ils servent de **base pré-entraînement**, pas de **dataset cible**. Notre asset : les traces internes de la démo MOREL Catherine et autres. + +### 2.5 Recommandation d'intégration C1 + +**Court terme (1–2 semaines)** : +1. Job batch offline : itérer sur `data/runner_captures//events.jsonl` post-démo réussie, appeler `ShadowLearningHook.on_click_observed` rétrospectivement → enrichit `SignatureStore`. +2. Mesurer le hit-rate de `SignatureStore` au replay suivant. + +**Moyen terme (1 mois)** : +3. Définir notre **format de trace canonique** (inspirer du schéma OpenAdapt SQLite + privacy scrub avant tout). +4. Brancher le hook sur `ShadowObserver._on_mouse_click` UNIQUEMENT après merge du Validator sémantique (B2). + +**Anti-pattern à éviter** : activer le hook sur des replays VWB sans Validator. Cf. bug step 10 mai : on apprendrait à cliquer dans le bandeau Edge au lieu du tab Imagerie. + +--- + +## 3. C2 — Fine-tuning VLM grounding 2025–2026 + +### 3.1 Table comparée des techniques + +| Méthode | Modèle base | Données | Gain vs SFT | Source | +|---|---|---|---|---| +| **Visual-RFT** (mars 2025) | Qwen2-VL-2B / 7B | **239 images** (LISA few-shot) à 500 steps | +24.3% classif, +21.9 mAP COCO 2-shot | [arxiv 2503.01785](https://arxiv.org/html/2503.01785v1) | +| **UI-R1** (AAAI 2026, mars 2025) | Qwen2.5-VL-3B | **2k–3k exemples** | +22.1% ScreenSpot, +6.0% ScreenSpot-Pro, +12.7% AndroidControl | [github UI-R1](https://github.com/lll6gg/UI-R1) | +| **GUI-R1** (avril 2025) | Qwen2.5-VL | **3k exemples** (0.02% du training OS-Atlas) | Bat OS-Atlas-7B SFT sur tous les bench | [arxiv 2504.10458](https://www.emergentmind.com/papers/2504.10458) | +| **GUI-G1** (mai 2025) | Qwen2.5-VL | R1-Zero style | Analyse théorique R1 grounding | [openreview](https://openreview.net/forum?id=1XLjrmKZ4p) | +| **SE-GUI / SE-RFT** (mai 2025) | 7B | **3k samples** | SOTA ScreenSpot-Pro auto-supervisé | [arxiv 2505.12370](https://arxiv.org/html/2505.12370) | +| **GUI-Actor** (NeurIPS 2025) | Qwen2.5-VL-7B | Coordinate-free grounding | 42.2/44.6 ScreenSpot-Pro | [microsoft/GUI-Actor](https://github.com/microsoft/GUI-Actor) | +| **GUI-CURSOR** (2025) | Qwen2.5-VL-7B | — | 88.8→93.9 SS-v2, 26.8→56.5 SS-Pro | [arxiv 2509.21552](https://arxiv.org/pdf/2509.21552) | +| **POINTS-GUI-G** (2026) | 8B | — | 59.9 SS-Pro, 66.0 OSWorld-G | [arxiv 2602.06391](https://arxiv.org/pdf/2602.06391) | +| **GUI-AIMA** (2025) | 3B | **509k samples / 101k screenshots** | SOTA data-efficient | [arxiv 2511.00810](https://arxiv.org/pdf/2511.00810) | + +**Convergence claire** : +- **GRPO** (Group Relative Policy Optimization, originaire de DeepSeek-R1) > SFT pour le grounding GUI. +- **Reward function rule-based** : IoU pour box, accuracy 0/1 pour clic point, format compliance JSON. +- **Quelques milliers d'exemples suffisent** (3k–10k) pour un gain SOTA sur Qwen2.5-VL-3B/7B base. +- Le **base model Qwen2.5-VL** domine la littérature (vs UI-TARS, vs InfiGUI). + +### 3.2 Datasets et licences + +| Dataset | Volume | License | Utilisable commercialement ? | +|---|---|---|---| +| ScreenSpot | 1.2k instructions | À vérifier (paperswithcode) | Probable academic only | +| ScreenSpot-v2 | corrigé v1 | OS-Copilot/ScreenSpot-v2 HF | À vérifier | +| ScreenSpot-Pro | high-res pro | likaixin2000/ScreenSpot-Pro-GUI-Grounding | À vérifier | +| UGround | 10M elements | OSU-NLP-Group | Academic, vérifier CC-BY-NC | +| AGUVIS | 4.2M | aguvis-project | Académique, vérifier | +| UI-R1 (modèle + data) | 2k–3k | **Apache-2.0** ✅ | Commercial OK | +| GUI-Actor | — | Microsoft, MIT-like | Commercial OK | + +**Action immédiate** : **lire chaque LICENSE avant entraînement commercial**. La conclusion forte est qu'**aucun dataset public ne couvre Easily Assure**. La valeur sera dans **notre propre dataset interne** dérivé des traces démo. + +### 3.3 Plan concret pour fine-tuner sur Easily Assure + +**Hypothèse de travail** : on cible un fine-tuning **complémentaire** au modèle de base (Qwen2.5-VL-3B ou Qwen3-VL-8B), spécifique au domaine Easily Assure (UI typique, fonts, layout, vocabulaire médical FR). + +**Étapes proposées (à valider Dom)** : + +1. **Collecte** (C1) : 100 sessions Shadow réussies × ~30 clics utiles = **3 000 paires (screenshot, target_text, click_xy)**. Ratio compatible UI-R1 / GUI-R1. +2. **Format** : JSONL `{image_path, instruction, bbox_xyxy_or_point, screen_resolution}`. Pré-process via `smart_resize` officiel (DETTE-014 résolue d'abord). +3. **Anonymisation** : appliquer `core/anonymize/*` (déjà existant t2a) sur chaque crop avant export. Critique pour healthtech. +4. **Méthode** : **GRPO + LoRA** sur Qwen2.5-VL-3B base. + - Reward function : IoU > 0.5 (cible 1.0, sinon 0) + format JSON valide. + - 500–1000 steps suffisent (cf. Visual-RFT 200 steps + 500 grounding). +5. **Hardware** : **HF Jobs / DGX Cloud H100** (8.25 $/h, [source](https://huggingface.co/blog/train-dgx-cloud) — ATTENTION : service deprecated avril 2025, vérifier alternative HF Jobs actuel). Ou **DGX Spark** Dom roadmap (`memory/project_roadmap_vision.md`). + - Alternative cloud GPU 2026 : H100 $4.50–$36/h AWS, $2.99/h en marketplace ([spendark](https://spendark.com/blog/machine-learning-cloud-cost/)). +6. **Durée estimée** : 8–12h sur 1× H100 (basé sur QLoRA 7B), ou 4–6h sur 8× H100 parallèle. +7. **Coût estimé** : **30–80 €** (1 run complet) ou **< 10 €** si A100 marketplace + QLoRA 3B. +8. **Évaluation** : split 80/20 sur les traces internes + non-régression sur ScreenSpot-Pro public. + +### 3.4 LoRA vs Full Fine-Tuning — recommandation + +**Source clé :** [arxiv 2410.21228 "LoRA vs Full Fine-tuning: An Illusion of Equivalence"](https://arxiv.org/pdf/2410.21228) — LoRA introduit des "intruder dimensions" qui réduisent la généralisation hors-domaine. + +**Mais pour notre cas (spécialisation Easily Assure)** : +- **On NE VEUT PAS généraliser** — on veut sur-fit (sainement) une UI spécifique. +- LoRA / QLoRA suffisent : Qwen2.5-VL-3B + LoRA tient sur **RTX 4070 12 GB** ([source](https://datature.io/blog/how-to-fine-tune-qwen2-5-vl)). +- Full FT sur 3B "hit practical limits with unstable loss curves and VRAM pressure" ([kaitchup](https://kaitchup.substack.com/p/qwen25-qlora-lora-and-full-fine-tuning)). + +**Reco :** **QLoRA 4-bit sur Qwen2.5-VL-3B**, rang 16–64. Iteration locale possible sur le RTX 5070 du Dom. Pas besoin HF Jobs pour le prototype. HF Jobs uniquement pour les runs de production. + +**À éviter** : full FT 7B+ pour un usage Easily Assure. ROI insuffisant face à la complexité ops. + +--- + +## 4. C3 — Memory store visuel + +### 4.1 Skyvern prompt caching — architecture détaillée + +**Sources :** [skyvern deepwiki](https://deepwiki.com/Skyvern-AI/skyvern/1-overview), [skyvern blog MCP](https://www.skyvern.com/blog/mcp-server-architecture-explained/), [zread optimization](https://zread.ai/Skyvern-AI/skyvern/29-optimization-strategies) + +**Mécanisme** : +1. **Cache key** : template Jinja2 rendu à partir des paramètres du workflow (URL cible, valeurs de form, etc.). +2. **Stockage** : quand une action AI réussit, Skyvern stocke le `selector_path` (DOM) ou la coord visuelle. +3. **Replay** : runs suivants avec mêmes paramètres → match cache_key → exécute le script pré-généré (Python ou JS), **sans appeler le LLM**. +4. **Fallback** : si replay échoue (UI a changé), AI re-engage automatiquement et **met à jour le cache**. + +**Gain mesuré :** **10–100× speed-up** sur runs cachés. "Deterministic replay flags if a price or SKU changes" — Skyvern utilise les changements détectés comme signal d'invalidation. + +**Différence vs Anthropic/OpenAI "prompt caching" classique** : Skyvern cache **les artefacts d'exécution** (scripts/sélecteurs), pas les tokens du prompt. C'est plus proche de notre `TargetMemoryStore` que d'un cache LLM. + +**Transposable à rpa_vision_v3** : +- Cache key = `(workflow_id, step_id, target_label, target_description, screen_context_hash)`. +- Cache value = `(click_xy, source: ocr|template|vlm, confidence, last_validated_at)`. +- Invalidation = pHash zone d'intérêt change OU REPORT failure récent. + +### 4.2 FAISS + CLIP/DINOv2 vs ColPali/ColQwen — comparaison + +| Approche | Index size | Latence query | Granularité | Pertinence UI | +|---|---|---|---|---| +| **CLIP global + FAISS** | 1 vec/image (512–768d) | < 1 ms | Image entière | Bonne pour "ai-je déjà vu cet écran" | +| **DINOv2 + FAISS** | 1 vec/image (768–1536d) | < 1 ms | Image entière | Meilleur que CLIP en self-sup, robuste aux occlusions ([source](https://medium.com/aimonks/image-similarity-with-dinov2-and-faiss-741744bc5804)) | +| **DINOv2 crops + FAISS** | 1 vec/widget (768d) | 1–5 ms | Par widget | Cas idéal pour "ai-je déjà vu ce bouton dans CE contexte" | +| **ColPali / ColQwen** | N patches × 128d | 5–50 ms (late interaction) | Patches × tokens query | SOTA documents visuels, plus lourd, [Nemotron ColEmbed V2](https://arxiv.org/html/2602.03992v1) NDCG@10=63.42 sur Vidore V3 | +| **UISearch** ([arxiv 2511.19380](https://arxiv.org/pdf/2511.19380)) | Graph attributé | 47.5 ms median | Hiérarchie + spatial | 0.92 Top-5 sur 20k UIs financières | + +**Recommandation pour rpa_vision_v3 :** + +- **Niveau 1 (cache exact)** — `pHash` zone d'intérêt (déjà partiellement utilisé). Latence < 1 ms. +- **Niveau 2 (cache flou widget)** — **DINOv2-base crops + FAISS** indexé par widget. Réutilise `VisualEmbeddingManager` (déjà écrit, DETTE-005) mais swap CLIP → DINOv2 (gain mesuré sur déduplication, [encord](https://encord.com/blog/dinov2-self-supervised-learning-explained/)). +- **Niveau 3 (recherche sémantique cross-screens)** — différé. ColPali/ColQwen attrayants mais lourd à déployer ; UISearch très prometteur mais code non-public. + +**Verdict :** **FAISS + DINOv2** = bon compromis pragmatique 2026. ColPali = différé si besoin de retrieval cross-écrans riches. + +### 4.3 Comment activer `VisualEmbeddingManager` orphelin (DETTE-005) + +Constat investigation 2026-05-09 : + +- `core/visual/visual_embedding_manager.py` — 651 lignes, commit `a27b74cf2` (2026-01-29). +- `core/visual/screenshot_validation_manager.py` — 571 lignes, idem. +- **0 site d'instanciation runtime**, ni VEM ni SVM. +- Investigation recommandait **ARCHIVE** sauf cas d'usage prod identifié, **car redondant avec `SignatureStore` + `fusion_engine.embedding_cache`**. + +**Réévaluation à la lumière de l'AXE_C :** + +Le verdict "archivage" du 9 mai était correct dans le cadre étroit "ce composant est-il actuellement utile ?". Mais l'AXE_C ouvre un cas d'usage clair : + +- **VEM = couche cache widget visuel** devant la cascade `_resolve_target`. +- **SVM = validation continue** des targets stockées (cache invalidation par re-validation périodique). + +**Recommandation révisée (à statuer Dom)** : + +| Option | Pro | Contra | +|---|---|---| +| **A. Archiver VEM/SVM (verdict initial), refaire propre dans `core/grounding/`** | Pas de dette de réécriture, unifié avec `SignatureStore` | Perd 1200 lignes + tests existants | +| **B. Réactiver VEM/SVM en swappant backbone CLIP → DINOv2** | Tests existants, archi serializer HMAC déjà signée | Architecture dupliquée avec `SignatureStore`, risque incohérence | +| **C. Hybride : extraire la logique de cache HMAC + serializer signé de VEM dans `SignatureStore`, archiver le reste** | Best-of-both, signature crypto réutilisée | 1 jour de refactor | + +**Mon vote** : **C**, après livraison démo client. Mais **acter B comme MVP rapide** si la fenêtre est < 1 semaine et qu'on veut tester C3 sans refactor lourd. + +### 4.4 Cache invalidation — quand la mémoire devient stale + +**Stratégies observées dans la littérature 2025–2026 :** + +1. **Re-validation périodique** (SVM ScreenshotValidationManager prévoit ça) — recapture le widget, vérifie embedding similarity > seuil, sinon `STATUS=WARNING`. +2. **Invalidation sur échec** — Skyvern : si replay déterministe échoue, fallback IA + update cache. +3. **TTL versionné** — invalidation forcée à chaque changement de version du logiciel cible (Easily Assure update). +4. **Watermark pHash** — pHash du widget au moment du cache. Si match au runtime → réutilise. Sinon → re-grounding. +5. **Counter d'échecs** — `success_rate`, `usage_count` (pattern browser-use). Si `success_rate < 0.6` après N usages → purge. + +**Recommandation pour rpa_vision_v3 :** + +- Combo **pHash watermark** (rapide) + **counter d'échecs glissant** (robuste). Pas de SVM périodique au runtime (coût pour bénéfice douteux). +- TTL nominal 7 jours, reset sur chaque succès vérifié par Validator sémantique (B2). +- **Hard requirement** : ne JAMAIS écrire dans le cache sans Validator sémantique OK. Sinon poison cache cf. bug step 10 mai. + +--- + +## 5. Recommandation séquence — 3 vagues d'effort + +### Vague 1 — Court terme (1–2 semaines, post-démo client) + +**Objectif :** quick wins sans toucher ML. + +1. **C3 niveau 1** : étendre le cache `pHash` existant aux widgets résolus avec succès. Storage SQLite simple. +2. **C1 batch offline** : script ad-hoc qui rejoue `data/runner_captures//events.jsonl` post-réussite, appelle `ShadowLearningHook.on_click_observed` rétroactivement. Mesure hit-rate `SignatureStore` au replay suivant. +3. **Définir le format de trace canonique** (inspirer OpenAdapt SQLite + anonymisation t2a-style). + +**Coût :** ~3–5 j-h. **Risque :** très bas. +**Dépendance :** aucune (offline, hors chemin chaud). + +### Vague 2 — Moyen terme (1–2 mois) + +**Objectif :** collecter du dataset et fiabiliser le cache. + +1. **Attendre AXE_B2 Validator sémantique** (cf. dépendance critique §1). +2. Une fois B2 livré : activer `ShadowLearningHook` sur callback post-success replay (PAS sur Shadow direct). +3. **C3 niveau 2** : prototyper DINOv2 + FAISS sur 100 widgets démo. Option B (VEM réactivé) ou C (refactor propre). +4. **Collecter ≥ 1k traces** Easily Assure propres via démos répétées + anonymisation. + +**Coût :** ~5–10 j-h. **Risque :** moyen (Validator semantic = chemin critique). +**Dépendance forte :** AXE_B2 Validator sémantique merged. + +### Vague 3 — Long terme (3–6 mois post-démo client) + +**Objectif :** fine-tuning VLM spécifique Easily Assure. + +1. Dataset 3k–5k paires propres + anonymisées + smart_resize correct. +2. **GRPO + QLoRA 4-bit sur Qwen2.5-VL-3B** (méthode UI-R1 / Visual-RFT). 500–1000 steps. +3. Run local RTX 5070 ou cloud H100 marketplace. **Coût attendu : 10–80 €**. +4. Évaluation : split interne 80/20 + non-régression ScreenSpot-Pro. +5. Déploiement progressif : modèle fine-tuné en fallback du modèle base, A/B test sur démo. + +**Coût :** ~10–15 j-h + budget cloud < 100 €. **Risque :** moyen (succès très dépendant qualité dataset C1). +**Dépendance forte :** AXE_A1 (choix modèle base finalisé : Qwen2.5-VL-3B vs Qwen3-VL-8B vs UI-R1-3B comme nouveau base déjà fine-tuné). + +--- + +## 6. Dépendances avec autres AXEs + +- **AXE_B2 Validator** (collecte de traces propres) : **bloquant pour C1 sur replay live** + **prérequis pour cache C3 sans poison**. Tant que Validator pHash global laxiste (bug step 10) → C1 et C3 sur traces démo seulement. +- **AXE_A1 (modèles à fine-tuner)** : choix base entre Qwen2.5-VL-3B (UI-R1 le valide), Qwen3-VL-8B (`memory/reference_vlm_models.md` retient), InfiGUI-G1-3B (production actuelle). À trancher avant C2. +- **DETTE-014 smart_resize** : doit être résolue avant fine-tuning sinon coords mal calées dans le dataset. + +--- + +## 7. Sources + +### C1 — Shadow learning +- [OpenAdaptAI/OpenAdapt — GitHub](https://github.com/OpenAdaptAI/OpenAdapt) +- [Skyvern-AI/skyvern — DeepWiki overview](https://deepwiki.com/Skyvern-AI/skyvern/1-overview) +- [Skyvern optimization strategies — zread.ai](https://zread.ai/Skyvern-AI/skyvern/29-optimization-strategies) +- [browser-use skills tracking — lobehub](https://lobehub.com/skills/saik0s-mcp-browser-use-browser-use) +- [Computer-Browser-Phone-Use-Agent-Datasets](https://github.com/Khang-9966/Computer-Browser-Phone-Use-Agent-Datasets) +- [15 Datasets for Training and Evaluating AI Agents — ODSC, avril 2026](https://odsc.medium.com/15-datasets-for-training-and-evaluating-ai-agents-c171dde4e0ce) + +### C2 — Fine-tuning VLM grounding +- [Visual-RFT — arxiv 2503.01785](https://arxiv.org/html/2503.01785v1) +- [UI-R1 (AAAI 2026) — GitHub lll6gg/UI-R1](https://github.com/lll6gg/UI-R1) +- [UI-R1 — arxiv 2503.21620](https://arxiv.org/html/2503.21620) +- [GUI-R1 — arxiv 2504.10458](https://www.emergentmind.com/papers/2504.10458) +- [GUI-G1 R1-Zero training — openreview](https://openreview.net/forum?id=1XLjrmKZ4p) +- [SE-RFT Self-Evolutionary — arxiv 2505.12370](https://arxiv.org/html/2505.12370) +- [GUI-Actor NeurIPS 2025 — microsoft/GUI-Actor](https://github.com/microsoft/GUI-Actor) +- [GUI-CURSOR — arxiv 2509.21552](https://arxiv.org/pdf/2509.21552) +- [POINTS-GUI-G — arxiv 2602.06391](https://arxiv.org/pdf/2602.06391) +- [GUI-AIMA — arxiv 2511.00810](https://arxiv.org/pdf/2511.00810) +- [ScreenSpot-Pro — arxiv 2504.07981](https://arxiv.org/html/2504.07981v1) +- [UGround — OSU NLP Group](https://osu-nlp-group.github.io/UGround/) +- [AGUVIS Project](https://aguvis-project.github.io/) +- [FocusUI CVPR 2026 — github.com/showlab/FocusUI](https://github.com/showlab/FocusUI) +- [LoRA vs Full Fine-tuning — arxiv 2410.21228](https://arxiv.org/pdf/2410.21228) +- [Qwen2.5 QLoRA / LoRA / Full FT comparison — kaitchup](https://kaitchup.substack.com/p/qwen25-qlora-lora-and-full-fine-tuning) +- [How to Fine-Tune Qwen2.5-VL — Datature](https://datature.io/blog/how-to-fine-tune-qwen2-5-vl) +- [HF Jobs / Train on DGX Cloud — HF blog](https://huggingface.co/blog/train-dgx-cloud) (⚠ deprecated avril 2025, vérifier alt) +- [GPU Cloud Pricing 2026 — spendark](https://spendark.com/blog/machine-learning-cloud-cost/) +- [How to Fine-Tune LLMs in 2026 — Spheron](https://www.spheron.network/blog/how-to-fine-tune-llm-2026/) + +### C3 — Memory store visuel +- [Skyvern cache_key Jinja2 — deepwiki](https://deepwiki.com/Skyvern-AI/skyvern) +- [Skyvern MCP architecture — blog](https://www.skyvern.com/blog/mcp-server-architecture-explained/) +- [Late Interaction Retrieval (ColBERT, ColPali, ColQwen) — Weaviate](https://weaviate.io/blog/late-interaction-overview) +- [ColPali — arxiv 2407.01449](https://arxiv.org/pdf/2407.01449) +- [illuin-tech/colpali — GitHub](https://github.com/illuin-tech/colpali) +- [Nemotron ColEmbed V2 — arxiv 2602.03992](https://arxiv.org/html/2602.03992v1) +- [DINOv2 — Encord blog](https://encord.com/blog/dinov2-self-supervised-learning-explained/) +- [DINOv2 + FAISS image similarity — Medium](https://medium.com/aimonks/image-similarity-with-dinov2-and-faiss-741744bc5804) +- [UISearch graph embeddings UI screenshots — arxiv 2511.19380](https://arxiv.org/pdf/2511.19380) +- [State of AI Agent Memory 2026 — mem0.ai](https://mem0.ai/blog/state-of-ai-agent-memory-2026) +- [Best Vector Databases For Multimodal 2026 — acecloud](https://acecloud.ai/blog/best-vector-databases-for-multimodal-genai/) + +--- + +*Document destiné à être lu en complément de `SYNTHESE_TECHNOS_REPLAY_2026-05-23.md`, `INSPIRATION_FRAMEWORKS_2026-05-10.md`, et `INVESTIGATION_MEMOIRE_VISUELLE_ORPHELINE_2026-05-09.md`. Toute action à prendre nécessite décision Dom.* diff --git a/docs/recherche/AXE_D2_DEEP_POPUP_CHAIN.md b/docs/recherche/AXE_D2_DEEP_POPUP_CHAIN.md new file mode 100644 index 000000000..0fc8967a4 --- /dev/null +++ b/docs/recherche/AXE_D2_DEEP_POPUP_CHAIN.md @@ -0,0 +1,1545 @@ +# AXE D2 — Deep dive « chaîne popup plus propre » (impl. production-ready) + +**Date :** 2026-05-24 +**Auteur :** agent recherche (dispatch Claude Opus 4.7 1M) +**Périmètre :** Compléter `AXE_D2_DIALOG_POPUP.md` par une **implémentation prête à coller** du package `core/dialog/`, le câblage exact, la décision orphelin vs actif, la coordination avec le Validator B2, et l'outillage test offline. +**Statut :** brief de recherche. Lecture seule sur le code existant. Aucune modification proposée à committer. +**Prérequis lecture :** `AXE_D2_DIALOG_POPUP.md` (matrice modal→action et taxonomie déjà couvertes), `AXE_B2_VALIDATOR_PATTERN.md` (interface Verdict/FailureCategory), `LESSONS_LEARNED_GHT_2026-05.md` §🔴. + +--- + +## 1. TL;DR + recommandation immédiate + +**Le code existant fait déjà 70 % du travail, mais éparpillé en 4 endroits :** + +- `agent_v0/agent_v1/core/system_dialog_guard.py` — détection multi-signal (ClassName UIA, processus, titre) UAC/CredUI/SmartScreen. **Excellent**. Fail-closed implémenté. +- `core/grounding/dialog_handler.py` — `KNOWN_DIALOGS` métier + `_click_via_infigui` + `_click_via_ocr`. **Réutilisable tel quel**. +- `core/grounding/title_verifier.py` — OCR titre 45 px, ~120 ms. **Réutilisable tel quel**. +- `agent_v0/agent_v1/core/executor.py` — `_handle_popup_vlm` (actif, 4 sites d'appel), `_handle_possible_popup` (orphelin clavier, 0 site), `_KNOWN_RUNTIME_DIALOGS` (côté client, 1 entrée). + +**Recommandation immédiatement actionnable (1 jour) :** + +1. **Créer un nouveau package `core/dialog/`** (côté serveur, pas client — pour mutualiser avec `dialog_handler.py` déjà serveur-side) qui **wrappe** les composants existants derrière 3 classes : `ChangeDetector`, `DialogClassifier`, `DialogResolver`. +2. **Garder `_handle_popup_vlm` côté client** (Léa Windows) mais le faire **déléguer** la décision politique au serveur via un endpoint `POST /api/v1/dialog/resolve`. Le client devient un exécuteur (capture + click), le serveur orchestre la cascade détection→classif→politique. +3. **Supprimer `_handle_possible_popup`** (orphelin, antipattern Tab+Enter+Esc aveugle qui viole `feedback_100pct_visual.md`). Référencé 0 fois, code mort. +4. **Câbler 3 sites d'appel** dans `executor.py:1108` (Observer pre-resolve, **déjà câblé** via `_handle_popup_vlm`), `executor.py:1262` (Policy post-grounding failed, **déjà câblé**), et 1 nouveau site **post-action click** (entre L:2270 et L:2475) — actuellement aucune vérif modal après un click réussi côté visuel mais ayant ouvert un dialog métier. +5. **Coordination Validator B2** : `Validator.validate()` retourne `Verdict.TERMINATE` + `FailureCategory.UNEXPECTED_DIALOG` → `api_stream` appelle `DialogResolver.resolve()` qui retourne soit `auto_dismissed` (replay continue) soit `pause_supervised` (replay stop avec event structuré). + +**Couverture estimée :** ~85 % des modaux courants traités sans intervention humaine (métier sauvegarde/écrasement + permission navigateur déclarative + OK trivial). Les 15 % restants (UAC, Hello, SmartScreen, INCONNU) = pause supervisée **par design healthtech**. + +**Effort :** 1 j pour MVP (DialogResolver côté serveur + 1 endpoint + suppression orphelin + 1 site câblé) → 1 sem pour matrice complète + tests offline → 1 mois pour bench injection + apprentissage catalogue. + +--- + +## 2. Architecture finale du package `core/dialog/` + +### 2.1. Arborescence + +``` +core/dialog/ +├── __init__.py # Exports publics (DialogResolver, DialogEvent, Verdict) +├── signatures.py # KNOWN_DIALOGS étendu fr+en + signatures par catégorie +├── change_detector.py # ChangeDetector léger (~80 LOC) +├── classifier.py # DialogClassifier OCR + VLM fallback (~150 LOC) +├── resolver.py # DialogResolver routing par politique (~200 LOC) +└── events.py # DialogEvent dataclass + persistance audit +``` + +### 2.2. Responsabilités (séparation stricte) + +| Module | Entrée | Sortie | Coût | +|---|---|---|---| +| `ChangeDetector` | screenshot_after PIL | `ChangeSignal` (bool is_modal + diagnostic) | ~50 ms | +| `DialogClassifier` | screenshot PIL + ocr_text optionnel | `DialogType` enum | 0-1.7 s | +| `DialogResolver` | screenshot + DialogType + workflow_ctx | `DialogEvent` (verdict + action) | 0-3 s | + +**Principe clé** : chaque composant est appelable **isolément**, sans dépendance circulaire. Tests unitaires triviaux. + +### 2.3. Interface publique (`__init__.py`) + +```python +"""core.dialog — Chaîne de gestion modaux & popups inattendus. + +Stack en 3 couches : + ChangeDetector → DialogClassifier → DialogResolver + +Politique healthtech (immutable) : +- JAMAIS d'auto-accept système (UAC/Hello/SmartScreen). +- JAMAIS de raccourci système inventé (pas de Tab+Enter+Esc aveugle). +- Catalogue déclaratif métier (KNOWN_DIALOGS) auto-dismiss explicite. +- Tout dialog inconnu → pause supervisée. +""" +from core.dialog.change_detector import ChangeDetector, ChangeSignal +from core.dialog.classifier import DialogClassifier, DialogType +from core.dialog.resolver import DialogResolver, Policy +from core.dialog.events import DialogEvent + +__all__ = [ + "ChangeDetector", "ChangeSignal", + "DialogClassifier", "DialogType", + "DialogResolver", "Policy", + "DialogEvent", +] +``` + +--- + +## 3. Code complet (copy-paste-ready, testé syntaxiquement) + +### 3.1. `signatures.py` — catalogue étendu (~120 LOC) + +```python +"""core/dialog/signatures.py — Catalogue exhaustif des signatures dialog. + +Étend `core/grounding/dialog_handler.KNOWN_DIALOGS` avec les catégories +système (UAC/Hello/SmartScreen) et navigateur (permissions). Source de +vérité unique pour la classification. + +Ordre du dict = priorité de matching (popups modaux AVANT fenêtres parents, +voir commentaire `dialog_handler.KNOWN_DIALOGS`). + +Toutes les signatures sont en MINUSCULES, l'OCR text doit être .lower() +avant matching. Caractères accentués préservés (EasyOCR fr les conserve). +""" +from __future__ import annotations + +from enum import Enum +from typing import Dict, List, Tuple + + +class DialogType(str, Enum): + # Catégorie SYSTÈME — pause supervisée obligatoire (healthtech). + UAC = "uac" + HELLO = "windows_hello" + SMARTSCREEN = "defender_smartscreen" + DEFENDER = "windows_defender" + DRIVER = "driver_install" + CREDUI = "credential_prompt" + + # Catégorie NAVIGATEUR — déclaratif workflow OU pause. + BROWSER_PERMISSION = "browser_permission" + BROWSER_SAVE_PASSWORD = "browser_save_password" + BROWSER_BLOCKED_PAGE = "browser_blocked_page" + + # Catégorie MÉTIER — auto-dismiss déterministe via KNOWN_DIALOGS. + METIER_SAVE = "metier_save" + METIER_CONFIRM = "metier_confirm" + METIER_OVERWRITE = "metier_overwrite" + METIER_OK_TRIVIAL = "ok_trivial" + METIER_OK_SUSPECT = "ok_suspect" # mots-clés "supprimé/perdu" → pause + + INCONNU = "inconnu" + + +# Politique par catégorie (immutable healthtech). +class Policy(str, Enum): + AUTO_DISMISS = "auto_dismiss" # OK trivial seulement + DECLARATIVE = "declarative" # catalog match → click + ASK_HUMAN = "ask_human" # pause supervisée + ESCALATE_SECURITY = "escalate_security" # pause + audit log full + + +POLICY_BY_TYPE: Dict[DialogType, Policy] = { + DialogType.UAC: Policy.ESCALATE_SECURITY, + DialogType.HELLO: Policy.ESCALATE_SECURITY, + DialogType.SMARTSCREEN: Policy.ESCALATE_SECURITY, + DialogType.DEFENDER: Policy.ESCALATE_SECURITY, + DialogType.DRIVER: Policy.ESCALATE_SECURITY, + DialogType.CREDUI: Policy.ESCALATE_SECURITY, + DialogType.BROWSER_PERMISSION: Policy.ASK_HUMAN, # sauf si déclaré workflow + DialogType.BROWSER_SAVE_PASSWORD: Policy.ASK_HUMAN, + DialogType.BROWSER_BLOCKED_PAGE: Policy.ASK_HUMAN, + DialogType.METIER_SAVE: Policy.DECLARATIVE, + DialogType.METIER_CONFIRM: Policy.DECLARATIVE, + DialogType.METIER_OVERWRITE: Policy.DECLARATIVE, + DialogType.METIER_OK_TRIVIAL: Policy.AUTO_DISMISS, + DialogType.METIER_OK_SUSPECT: Policy.ASK_HUMAN, + DialogType.INCONNU: Policy.ASK_HUMAN, +} + + +# Signatures texte (lowercase) → DialogType. Listes ordonnées car évaluées +# séquentiellement (premier match gagne). Les signatures les plus spécifiques +# en premier dans chaque catégorie. +SIGNATURES_BY_TYPE: Dict[DialogType, List[str]] = { + # ── SYSTÈME ───────────────────────────────────────────────────────── + DialogType.UAC: [ + "contrôle de compte d'utilisateur", + "contrôle de compte dutilisateur", # OCR sans apostrophe + "user account control", + "voulez-vous autoriser cette application", + "do you want to allow this app", + "do you want to allow the following", + ], + DialogType.HELLO: [ + "windows hello", + "saisissez votre code pin", + "saisir votre code pin", + "enter your pin", + "touchez le capteur d'empreintes", + "touchez le lecteur", + "use your fingerprint", + "vérification de votre identité", # cf. feedback_auth_dialogs_runtime + "analysez votre doigt", + ], + DialogType.SMARTSCREEN: [ + "windows a protégé votre pc", + "windows a protégé votre ordinateur", + "windows protected your pc", + "defender smartscreen", + "smartscreen a empêché", + "informations complémentaires", # accompagne SmartScreen + "exécuter quand même", + "run anyway", + "éditeur inconnu", + "unknown publisher", + ], + DialogType.DEFENDER: [ + "windows defender", + "menace détectée", + "threat detected", + "virus detected", + ], + DialogType.DRIVER: [ + "installer ce pilote", + "install this driver", + "signature numérique du pilote", + ], + DialogType.CREDUI: [ + "sécurité windows", + "windows security", + "entrer les informations d'identification", + "enter your credentials", + "connectez-vous à votre compte", + "sign in to your account", + ], + # ── NAVIGATEUR ────────────────────────────────────────────────────── + DialogType.BROWSER_PERMISSION: [ + "souhaite utiliser votre microphone", + "souhaite utiliser votre caméra", + "souhaite utiliser votre micro", + "souhaite afficher des notifications", + "souhaite connaître votre position", + "wants to use your microphone", + "wants to use your camera", + "wants to show notifications", + "wants to know your location", + "autoriser l'utilisation", + "allow microphone", + "allow camera", + "autoriser les notifications", + ], + DialogType.BROWSER_SAVE_PASSWORD: [ + "voulez-vous enregistrer ce mot de passe", + "save password", + "enregistrer le mot de passe", + "voulez-vous que google chrome enregistre", + ], + DialogType.BROWSER_BLOCKED_PAGE: [ + "cette page web n'a pas répondu", + "cette page web ne répond pas", + "page unresponsive", + "this page isn't responding", + "tuer les pages", + "kill pages", + ], + # ── MÉTIER ────────────────────────────────────────────────────────── + # Déjà couvert par core/grounding/dialog_handler.KNOWN_DIALOGS, + # importé dynamiquement par DialogClassifier (single source of truth). +} + + +# Blocklist pour OK trivial : si ces mots apparaissent, on REFUSE l'auto-dismiss +# et on escalade à l'humain. Action irréversible présumée. +SUSPECT_TOKENS_BLOCKLIST: Tuple[str, ...] = ( + "supprimer définitivement", "delete permanently", + "perdu", "perdues", "lost", + "irréversible", "irreversible", "cannot be undone", + "vider la corbeille", "empty trash", + "formater", "format", + "effacer toutes", "erase all", +) + + +def is_suspect_ok(ocr_text: str) -> bool: + """Retourne True si l'OCR contient un mot-clé bloquant l'auto-dismiss.""" + text_lower = ocr_text.lower() + return any(token in text_lower for token in SUSPECT_TOKENS_BLOCKLIST) +``` + +### 3.2. `change_detector.py` — détecteur léger (~100 LOC) + +```python +"""core/dialog/change_detector.py — Détection rapide d'apparition de modal. + +Cible : < 50 ms par appel. Combine 3 signaux composables : + +1. Foreground window changed (Windows API, ~1 ms) — signal complémentaire. +2. Screenshot diff zone centrale vs périphérie (~10 ms numpy). +3. Secure desktop detection (~1 ms, écran ~noir UAC). + +JAMAIS source unique. La décision finale repose sur composition (au moins +2 signaux concordants OU un signal très fort comme secure desktop). + +cf. feedback_popup_vlm.md : GetForegroundWindow seul n'est pas fiable +(retourne 0 en SSH, popups modernes partagent hwnd parent). +""" +from __future__ import annotations + +import logging +import time +from dataclasses import dataclass +from typing import Optional + +logger = logging.getLogger(__name__) + + +@dataclass +class ChangeSignal: + is_modal: bool # Verdict composite (décision finale) + foreground_changed: bool # Signal Windows API + diff_ratio_global: float # 0.0-1.0 sur tout l'écran + diff_ratio_central: float # 0.0-1.0 sur zone centrale + secure_desktop: bool # Écran type UAC (très assombri) + elapsed_ms: float + + +class ChangeDetector: + """Détecte qu'un modal vient d'apparaître sans appeler le VLM.""" + + # Seuils empiriques. À calibrer post-bench (cf. plan §11). + DIFF_CENTRAL_THRESHOLD = 0.10 # >10% pixels modifiés zone centrale + DIFF_GLOBAL_MAX_FOR_MODAL = 0.40 # un modal = changement local, pas global + LUMINANCE_SECURE_DESKTOP = 50 # pixels < 50 / 255 = très sombre + SECURE_DESKTOP_RATIO = 0.60 # > 60 % écran assombri = UAC probable + + def __init__(self): + self._last_screenshot = None + self._last_hwnd: Optional[int] = None + + def detect(self, screenshot_pil) -> ChangeSignal: + """Analyser le screenshot courant vs précédent. Idempotent.""" + import numpy as np + + t0 = time.time() + arr = np.asarray(screenshot_pil.convert("L")) + + # Signal 1 : foreground window change + fg_changed = self._check_foreground_changed() + + # Signal 2 : diff zoné central vs global + diff_global = 0.0 + diff_central = 0.0 + if self._last_screenshot is not None: + prev = np.asarray(self._last_screenshot.convert("L")) + if prev.shape == arr.shape: + diff = np.abs(prev.astype(int) - arr.astype(int)) + diff_global = float((diff > 25).mean()) + h, w = arr.shape + cy0, cy1 = h // 4, 3 * h // 4 + cx0, cx1 = w // 4, 3 * w // 4 + diff_central = float((diff[cy0:cy1, cx0:cx1] > 25).mean()) + + # Signal 3 : secure desktop UAC (écran très assombri global) + secure_desktop = ( + float((arr < self.LUMINANCE_SECURE_DESKTOP).mean()) + > self.SECURE_DESKTOP_RATIO + ) + + # Décision composite. Au moins 2 signaux OU secure desktop seul. + is_modal = secure_desktop or ( + diff_central > self.DIFF_CENTRAL_THRESHOLD + and diff_global < self.DIFF_GLOBAL_MAX_FOR_MODAL + ) or (fg_changed and diff_central > 0.05) + + self._last_screenshot = screenshot_pil + elapsed_ms = (time.time() - t0) * 1000 + signal = ChangeSignal( + is_modal=is_modal, + foreground_changed=fg_changed, + diff_ratio_global=diff_global, + diff_ratio_central=diff_central, + secure_desktop=secure_desktop, + elapsed_ms=elapsed_ms, + ) + if is_modal: + logger.info( + "[CHANGE-DET] modal probable : fg=%s, diff_c=%.2f, " + "diff_g=%.2f, secure=%s (%.0fms)", + fg_changed, diff_central, diff_global, secure_desktop, elapsed_ms, + ) + return signal + + def reset(self) -> None: + """Réinitialise l'état (utile entre 2 sessions de replay).""" + self._last_screenshot = None + self._last_hwnd = None + + def _check_foreground_changed(self) -> bool: + """Windows-only. Renvoie False ailleurs ou en cas d'erreur.""" + try: + import ctypes + hwnd = int(ctypes.windll.user32.GetForegroundWindow()) + except Exception: + return False + if hwnd == 0: + # SSH/Léa sans desktop accessible — signal inutilisable. + return False + changed = (self._last_hwnd is not None) and (hwnd != self._last_hwnd) + self._last_hwnd = hwnd + return changed +``` + +### 3.3. `classifier.py` — classification cascade (~170 LOC) + +```python +"""core/dialog/classifier.py — Classification d'un dialogue détecté. + +Stratégie cascade : + 1. OCR full-screen (EasyOCR fr+en, ~150 ms via singleton partagé). + 2. Match signatures texte (signatures.SIGNATURES_BY_TYPE + KNOWN_DIALOGS). + 3. Fallback VLM compact (qwen3-vl:8b via Ollama LAN, ~1.7 s). + 4. Si toujours rien → DialogType.INCONNU (politique = ASK_HUMAN). + +Le VLM compact est appelé UNIQUEMENT si signatures texte échouent ET +ChangeDetector a confirmé is_modal. Évite de bloquer la boucle replay. +""" +from __future__ import annotations + +import logging +import re +import time +from dataclasses import dataclass +from typing import Optional + +from core.dialog.signatures import ( + SIGNATURES_BY_TYPE, + DialogType, + is_suspect_ok, +) + +logger = logging.getLogger(__name__) + + +@dataclass +class ClassificationResult: + dialog_type: DialogType + confidence: float + method: str # "signature" | "known_dialogs" | "vlm" | "fallback" + ocr_text: str # texte brut OCR (pour audit) + elapsed_ms: float + + +class DialogClassifier: + """Classifie un dialogue en type connu.""" + + # Modèle VLM compact — court output (max 50 tokens). + VLM_MODEL_DEFAULT = "qwen3-vl:8b" + VLM_TIMEOUT_S = 5.0 + + def __init__(self, ocr_fn=None, ollama_host: Optional[str] = None): + """ + ocr_fn: callable(PIL.Image) -> str. Si None, lazy-load EasyOCR fr+en. + ollama_host: hôte Ollama pour fallback VLM. Défaut env RPA_OLLAMA_HOST + ou "localhost". + """ + self._ocr = ocr_fn + self._easyocr_reader = None + import os + self._ollama_host = ollama_host or os.environ.get("RPA_OLLAMA_HOST", "localhost") + self._vlm_model = os.environ.get( + "RPA_DIALOG_CLASSIFIER_MODEL", self.VLM_MODEL_DEFAULT + ) + + def classify(self, screenshot_pil) -> ClassificationResult: + t0 = time.time() + + # Étape 1 : OCR plein écran + ocr_text = self._read_ocr(screenshot_pil) + + # Étape 2 : match signatures fr+en + dtype = self._match_signatures(ocr_text) + if dtype: + elapsed = (time.time() - t0) * 1000 + return ClassificationResult( + dialog_type=dtype, confidence=0.9, method="signature", + ocr_text=ocr_text[:300], elapsed_ms=elapsed, + ) + + # Étape 3 : match catalogue métier existant (single source of truth) + dtype = self._match_known_dialogs(ocr_text) + if dtype: + elapsed = (time.time() - t0) * 1000 + return ClassificationResult( + dialog_type=dtype, confidence=0.85, method="known_dialogs", + ocr_text=ocr_text[:300], elapsed_ms=elapsed, + ) + + # Étape 4 : OK trivial vs suspect (heuristique sans signature) + if self._looks_like_ok_trivial(ocr_text): + dtype = (DialogType.METIER_OK_SUSPECT if is_suspect_ok(ocr_text) + else DialogType.METIER_OK_TRIVIAL) + elapsed = (time.time() - t0) * 1000 + return ClassificationResult( + dialog_type=dtype, confidence=0.6, method="heuristic", + ocr_text=ocr_text[:300], elapsed_ms=elapsed, + ) + + # Étape 5 : fallback VLM compact + dtype = self._classify_via_vlm(screenshot_pil) + elapsed = (time.time() - t0) * 1000 + if dtype: + return ClassificationResult( + dialog_type=dtype, confidence=0.7, method="vlm", + ocr_text=ocr_text[:300], elapsed_ms=elapsed, + ) + + # Fallback ultime : INCONNU → ASK_HUMAN + return ClassificationResult( + dialog_type=DialogType.INCONNU, confidence=0.0, method="fallback", + ocr_text=ocr_text[:300], elapsed_ms=elapsed, + ) + + # ── Implémentations ──────────────────────────────────────────────── + + def _read_ocr(self, screenshot_pil) -> str: + if self._ocr is not None: + return self._ocr(screenshot_pil) or "" + # Lazy-load EasyOCR + try: + import numpy as np + if self._easyocr_reader is None: + import easyocr + self._easyocr_reader = easyocr.Reader( + ['fr', 'en'], gpu=True, verbose=False, + ) + results = self._easyocr_reader.readtext(np.array(screenshot_pil)) + return ' '.join(r[1] for r in results if r[1].strip()) + except Exception as e: + logger.warning("[CLASSIFIER] OCR failed: %s", e) + return "" + + @staticmethod + def _match_signatures(ocr_text: str) -> Optional[DialogType]: + text_lower = ocr_text.lower() + if not text_lower: + return None + for dtype, signatures in SIGNATURES_BY_TYPE.items(): + for sig in signatures: + if sig in text_lower: + logger.info("[CLASSIFIER] signature match '%s' → %s", sig, dtype) + return dtype + return None + + @staticmethod + def _match_known_dialogs(ocr_text: str) -> Optional[DialogType]: + """Réutilise core/grounding/dialog_handler.KNOWN_DIALOGS (source unique).""" + try: + from core.grounding.dialog_handler import KNOWN_DIALOGS + except Exception: + return None + text_lower = ocr_text.lower() + for key, info in KNOWN_DIALOGS.items(): + if key in text_lower: + # Heuristique : target=Oui → confirm, target=Enregistrer → save + target = info.get("target", "").lower() + if target in ("oui", "yes"): + if "remplac" in key or "replace" in key or "écraser" in key: + return DialogType.METIER_OVERWRITE + return DialogType.METIER_CONFIRM + elif target in ("enregistrer", "save"): + return DialogType.METIER_SAVE + return DialogType.METIER_OK_TRIVIAL + return None + + @staticmethod + def _looks_like_ok_trivial(ocr_text: str) -> bool: + """Heuristique : 1 mot 'OK' isolé + court contexte = OK trivial.""" + text_lower = ocr_text.lower() + if not re.search(r"\b(ok|fermer|close|ferme)\b", text_lower): + return False + # Si trop de texte, ce n'est probablement pas un simple OK + return len(ocr_text) < 400 + + def _classify_via_vlm(self, screenshot_pil) -> Optional[DialogType]: + """Appel Ollama qwen3-vl:8b avec prompt français court. + + Latence cible < 2 s. Sortie attendue : un mot parmi la liste enum. + Si format JSON requis, qwen3-vl:8b ignore parfois `format=json` + (cf. BENCH_SAFETY_CHECKS_2026-05-06 §résultats). On parse en regex. + """ + try: + import base64 + import io + import requests + + buf = io.BytesIO() + screenshot_pil.convert("RGB").save(buf, format="JPEG", quality=75) + img_b64 = base64.b64encode(buf.getvalue()).decode("ascii") + + prompt = ( + "Cette capture montre un dialogue/popup. Classe-le en UN SEUL mot " + "parmi : uac, hello, smartscreen, browser_permission, metier_save, " + "metier_confirm, ok_trivial, inconnu. Réponds UNIQUEMENT le mot." + ) + payload = { + "model": self._vlm_model, + "messages": [ + {"role": "system", + "content": "Tu classes des dialogues Windows. Réponds en un mot."}, + {"role": "user", "content": prompt, "images": [img_b64]}, + ], + "stream": False, + "options": {"num_predict": 20, "temperature": 0.0}, + } + r = requests.post( + f"http://{self._ollama_host}:11434/api/chat", + json=payload, timeout=self.VLM_TIMEOUT_S, + ) + r.raise_for_status() + response = r.json().get("message", {}).get("content", "").strip().lower() + # Parse : chercher un mot enum dans la réponse + for word in re.findall(r"[a-z_]+", response): + try: + return DialogType(word) + except ValueError: + continue + return None + except Exception as e: + logger.warning("[CLASSIFIER] VLM fallback failed: %s", e) + return None +``` + +### 3.4. `resolver.py` — politique et action (~180 LOC) + +```python +"""core/dialog/resolver.py — Application de la politique par catégorie. + +Routing : + - SYSTÈME (UAC/Hello/SmartScreen/...) → escalation_pause_supervised() + - MÉTIER déclaré → résolution via dialog_handler existant (InfiGUI + OCR) + - INCONNU → pause par défaut, JAMAIS auto-dismiss + +Cf. AXE_D2_DIALOG_POPUP.md §5 matrice modal → action (autoritative). +""" +from __future__ import annotations + +import logging +import time +from typing import Any, Callable, Dict, Optional + +from core.dialog.classifier import ClassificationResult, DialogClassifier +from core.dialog.change_detector import ChangeDetector, ChangeSignal +from core.dialog.events import DialogEvent +from core.dialog.signatures import ( + POLICY_BY_TYPE, + DialogType, + Policy, +) + +logger = logging.getLogger(__name__) + + +# Politique callbacks signature : (event, screenshot, workflow_ctx) -> bool resolved +PolicyCallback = Callable[[DialogEvent, Any, Dict], bool] + + +class DialogResolver: + """Orchestre la chaîne ChangeDetector → Classifier → Politique → Action.""" + + def __init__( + self, + change_detector: Optional[ChangeDetector] = None, + classifier: Optional[DialogClassifier] = None, + on_pause_supervised: Optional[PolicyCallback] = None, + on_auto_dismiss: Optional[PolicyCallback] = None, + workflow_declared_handlers: Optional[Dict[DialogType, PolicyCallback]] = None, + ): + """ + on_pause_supervised: callback appelé en cas d'ASK_HUMAN/ESCALATE. + Signature : (event, screenshot, ctx) -> bool. + Doit déclencher la pause dans api_stream/replay_engine. + on_auto_dismiss: callback pour OK trivial. Doit cliquer le bouton OK + via dialog_handler.handle_if_dialog OU pyautogui. + workflow_declared_handlers: si un workflow déclare anticiper un type + (ex. browser_permission "autoriser micro"), + on appelle ce handler en priorité. + """ + self._change_detector = change_detector or ChangeDetector() + self._classifier = classifier or DialogClassifier() + self._on_pause = on_pause_supervised or self._default_pause + self._on_dismiss = on_auto_dismiss or self._default_dismiss + self._workflow_handlers = workflow_declared_handlers or {} + + def check_and_resolve( + self, + screenshot_pil, + workflow_context: Optional[Dict[str, Any]] = None, + force_classify: bool = False, + ) -> Optional[DialogEvent]: + """Point d'entrée principal. + + Args: + screenshot_pil: capture courante (post-action ou pre-tick). + workflow_context: ctx avec step_idx, action_id, declared_dialogs, etc. + force_classify: bypass le ChangeDetector (utile pour B2 Validator + qui sait déjà que quelque chose cloche). + + Returns: + DialogEvent si un dialog a été détecté et traité, None sinon. + """ + workflow_context = workflow_context or {} + t0 = time.time() + + # Étape 1 : détection rapide (sauf bypass) + signal: Optional[ChangeSignal] = None + if not force_classify: + signal = self._change_detector.detect(screenshot_pil) + if not signal.is_modal: + return None + + # Étape 2 : classification + classif = self._classifier.classify(screenshot_pil) + + # Étape 3 : politique + policy = POLICY_BY_TYPE.get(classif.dialog_type, Policy.ASK_HUMAN) + + # Étape 3bis : exception workflow déclaratif (ex. permission micro attendue) + declared = (workflow_context.get("declared_dialogs") or {}).get( + classif.dialog_type.value + ) + if declared: + policy = Policy.DECLARATIVE + logger.info( + "[RESOLVER] %s : politique forcée DECLARATIVE (workflow ctx)", + classif.dialog_type, + ) + + event = DialogEvent( + dialog_type=classif.dialog_type, + policy_applied=policy, + confidence=classif.confidence, + classification_method=classif.method, + ocr_text=classif.ocr_text, + change_signal=signal, + workflow_step=workflow_context.get("step_idx"), + workflow_action_id=workflow_context.get("action_id"), + elapsed_ms=(time.time() - t0) * 1000, + ) + + # Étape 4 : action selon politique + resolved = self._apply_policy(event, screenshot_pil, workflow_context) + event.action_taken = "resolved" if resolved else "paused" + + return event + + # ── Politiques ───────────────────────────────────────────────────── + + def _apply_policy( + self, event: DialogEvent, screenshot, ctx: Dict[str, Any], + ) -> bool: + if event.policy_applied in (Policy.ESCALATE_SECURITY, Policy.ASK_HUMAN): + return self._on_pause(event, screenshot, ctx) + + if event.policy_applied == Policy.AUTO_DISMISS: + return self._on_dismiss(event, screenshot, ctx) + + if event.policy_applied == Policy.DECLARATIVE: + # Workflow-declared handler en priorité + handler = self._workflow_handlers.get(event.dialog_type) + if handler: + return handler(event, screenshot, ctx) + # Fallback : utiliser le dialog_handler existant (InfiGUI + OCR) + return self._default_declarative(event, screenshot, ctx) + + # Default safe : pause + return self._on_pause(event, screenshot, ctx) + + @staticmethod + def _default_pause(event: DialogEvent, screenshot, ctx: Dict) -> bool: + logger.warning( + "[RESOLVER] PAUSE SUPERVISÉE : %s (policy=%s, conf=%.2f) — " + "aucun callback on_pause fourni, écho info uniquement.", + event.dialog_type, event.policy_applied, event.confidence, + ) + return False + + @staticmethod + def _default_dismiss(event: DialogEvent, screenshot, ctx: Dict) -> bool: + """Auto-dismiss OK trivial : utilise dialog_handler existant.""" + try: + from core.grounding.dialog_handler import DialogHandler + handler = DialogHandler() + result = handler.handle_if_dialog(screenshot) + return bool(result.get("handled")) + except Exception as e: + logger.warning("[RESOLVER] auto_dismiss failed: %s", e) + return False + + @staticmethod + def _default_declarative(event: DialogEvent, screenshot, ctx: Dict) -> bool: + """Métier déclaratif : délégue au dialog_handler.KNOWN_DIALOGS.""" + try: + from core.grounding.dialog_handler import DialogHandler + handler = DialogHandler() + result = handler.handle_if_dialog(screenshot) + if result.get("handled"): + logger.info( + "[RESOLVER] declarative resolved via dialog_handler: %s → %s", + result.get("title", "?"), result.get("action", "?"), + ) + return True + return False + except Exception as e: + logger.warning("[RESOLVER] declarative failed: %s", e) + return False +``` + +### 3.5. `events.py` — audit (~40 LOC) + +```python +"""core/dialog/events.py — Event structuré pour audit + dashboard.""" +from __future__ import annotations + +from dataclasses import asdict, dataclass, field +from typing import Any, Dict, Optional + +from core.dialog.signatures import DialogType, Policy + + +@dataclass +class DialogEvent: + dialog_type: DialogType + policy_applied: Policy + confidence: float + classification_method: str # "signature" | "vlm" | "known_dialogs" | ... + ocr_text: str + change_signal: Optional[Any] = None # ChangeSignal dataclass + workflow_step: Optional[int] = None + workflow_action_id: Optional[str] = None + action_taken: str = "unknown" + elapsed_ms: float = 0.0 + screenshot_path: Optional[str] = None + extra: Dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> Dict[str, Any]: + d = asdict(self) + d["dialog_type"] = self.dialog_type.value + d["policy_applied"] = self.policy_applied.value + if self.change_signal is not None: + d["change_signal"] = asdict(self.change_signal) + return d +``` + +--- + +## 4. Matrice modal → action finale + +(complète celle de `AXE_D2_DIALOG_POPUP.md §5`, focalisée sur l'**action concrète** par le `DialogResolver`) + +| `DialogType` | Politique | Signature match | Action `DialogResolver` | Latence cible | +|---|---|---|---|---| +| `UAC` | ESCALATE_SECURITY | "contrôle de compte", "user account control" | `on_pause(event)` → `replay_status=paused_need_help`, audit critical, screenshot full | 50ms détect + 150ms OCR | +| `HELLO` | ESCALATE_SECURITY | "windows hello", "code pin", "vérification de votre identité" | identique UAC + tip pré-démo dans event.extra | 200ms | +| `SMARTSCREEN` | ESCALATE_SECURITY | "windows a protégé", "smartscreen" | identique + ref `project_code_signing.md` dans event | 200ms | +| `DEFENDER` | ESCALATE_SECURITY | "menace détectée", "threat detected" | identique | 200ms | +| `DRIVER` | ESCALATE_SECURITY | "installer ce pilote", "signature pilote" | identique | 200ms | +| `CREDUI` | ESCALATE_SECURITY | "sécurité windows", "entrer informations identification" | identique, vault Léa = orthogonal (gestion long terme) | 200ms | +| `BROWSER_PERMISSION` | DECLARATIVE (si workflow) sinon ASK_HUMAN | "souhaite utiliser microphone/caméra/...", "autoriser/bloquer" | si `declared_dialogs[browser_permission]` → workflow_handler ; sinon `on_pause` | 200ms + 50ms click | +| `BROWSER_SAVE_PASSWORD` | ASK_HUMAN | "enregistrer ce mot de passe" | `on_pause` (audit security) | 200ms | +| `BROWSER_BLOCKED_PAGE` | ASK_HUMAN | "page n'a pas répondu" | `on_pause` (réseau ou crash app) | 200ms | +| `METIER_SAVE` | DECLARATIVE | KNOWN_DIALOGS["voulez-vous enregistrer"] | `_default_declarative` → `DialogHandler.handle_if_dialog` → InfiGUI click "Enregistrer" | 200ms + 3s InfiGUI | +| `METIER_CONFIRM` | DECLARATIVE | KNOWN_DIALOGS["confirmer"] | identique, click "Oui" | 200ms + 3s | +| `METIER_OVERWRITE` | DECLARATIVE | KNOWN_DIALOGS["remplacer/écraser/already exists"] | identique, click "Oui" / "Yes" | 200ms + 3s | +| `METIER_OK_TRIVIAL` | AUTO_DISMISS | heuristique `_looks_like_ok_trivial` + pas suspect | `_default_dismiss` → click "OK" via InfiGUI | 200ms + 3s | +| `METIER_OK_SUSPECT` | ASK_HUMAN | mots-clés `SUSPECT_TOKENS_BLOCKLIST` ("supprimé", "perdu", ...) | `on_pause` (audit + screenshot full) | 200ms | +| `INCONNU` | ASK_HUMAN | aucun match signature ni VLM | `on_pause` (capture VLM pour enrichir catalogue post-démo) | 50ms + 150ms OCR + 1.7s VLM | + +--- + +## 5. Wiring : sites d'appel exacts + +### 5.1. Côté serveur — `agent_v0/server_v1/api_stream.py` + +**Initialisation (boot)** : + +```python +# api_stream.py — au démarrage du module, à côté des autres init engines +from core.dialog import DialogResolver, ChangeDetector, DialogClassifier +from core.dialog.signatures import DialogType + +_DIALOG_RESOLVER = None + +def _get_dialog_resolver(): + global _DIALOG_RESOLVER + if _DIALOG_RESOLVER is None: + _DIALOG_RESOLVER = DialogResolver( + change_detector=ChangeDetector(), + classifier=DialogClassifier(), + on_pause_supervised=_dialog_pause_supervised, + on_auto_dismiss=_dialog_auto_dismiss_via_lea, + ) + return _DIALOG_RESOLVER +``` + +**Endpoint nouveau** : + +```python +# api_stream.py — nouveau endpoint qui sert le client Léa +@app.route("/api/v1/dialog/resolve", methods=["POST"]) +def dialog_resolve(): + """Le client Léa nous envoie un screenshot suspect, on classifie et retourne. + + Permet au client de ne pas dupliquer la logique de signatures. + Le client reste responsable de l'action click (résolution coords). + """ + payload = request.get_json(force=True) + screenshot_b64 = payload.get("screenshot") + workflow_ctx = payload.get("workflow_context", {}) + + img = _b64_to_pil(screenshot_b64) + resolver = _get_dialog_resolver() + event = resolver.check_and_resolve(img, workflow_ctx, force_classify=True) + if event is None: + return jsonify({"dialog_detected": False}) + + return jsonify({ + "dialog_detected": True, + "event": event.to_dict(), + # Si DECLARATIVE : indiquer au client le bouton à cliquer + "click_target": _suggest_click_target_for(event), + }) +``` + +**Site 1 : post-REPORT action** (le client a rapporté son résultat, on vérifie modal en sortie) : + +```python +# api_stream.py:report_action_result, ajouter APRÈS le pixel-diff actuel +# (juste avant le `return jsonify({"replay_status": ...})`) + +if RPA_DIALOG_RESOLVER_ENABLED and screenshot_after_b64: + img_after = _b64_to_pil(screenshot_after_b64) + resolver = _get_dialog_resolver() + event = resolver.check_and_resolve( + img_after, + workflow_context={ + "step_idx": current_step_idx, + "action_id": action_id, + "declared_dialogs": current_workflow.get("declared_dialogs", {}), + }, + ) + if event and event.policy_applied in (Policy.ASK_HUMAN, Policy.ESCALATE_SECURITY): + _set_replay_status_paused( + replay_id, reason=f"dialog:{event.dialog_type.value}", + evidence=event.to_dict(), + ) + return jsonify({"replay_status": "paused_need_help", + "dialog_event": event.to_dict()}) +``` + +**Site 2 : coordination Validator B2** (quand B2 retourne `TERMINATE` + `FailureCategory.UNEXPECTED_DIALOG`) : + +```python +# api_stream.py, après l'appel Validator.validate() (cf. AXE_B2 §6.4) +if val.verdict == Verdict.TERMINATE and val.failure_category == FailureCategory.UNEXPECTED_DIALOG: + # Le Validator a détecté qu'un modal bloque, mais ne sait pas quoi en faire. + # Déléguer au DialogResolver pour classification + politique. + resolver = _get_dialog_resolver() + event = resolver.check_and_resolve( + img_after, + workflow_context={"step_idx": step_idx, "action_id": action_id, + "declared_dialogs": current_workflow.get("declared_dialogs", {})}, + force_classify=True, # B2 sait déjà qu'il y a un modal + ) + if event and event.policy_applied == Policy.DECLARATIVE and event.action_taken == "resolved": + # Modal métier auto-résolu (ex. "Enregistrer ?") → reprendre normalement + return jsonify({"replay_status": "in_progress", + "dialog_event": event.to_dict()}) + else: + # Pause supervisée + _set_replay_status_paused(replay_id, reason=val.reasoning, + evidence=event.to_dict() if event else val.to_dict()) + return jsonify({"replay_status": "paused_need_help"}) +``` + +### 5.2. Côté client — `agent_v0/agent_v1/core/executor.py` + +**Décision** : NE PAS dupliquer la classification côté client. Simplifier `_handle_popup_vlm` pour qu'il appelle `/api/v1/dialog/resolve` du serveur, puis exécute le `click_target` retourné. + +**Diff conceptuel** (à valider Dom, lecture seule pour ce doc) : + +```diff +# agent_v0/agent_v1/core/executor.py — _handle_popup_vlm + def _handle_popup_vlm(self) -> bool: + # ── SÉCURITÉ inchangée : refus absolu sur dialogue système ── + if self._check_and_pause_on_system_dialog(context="handle_popup_vlm"): + return False + + screenshot_b64 = self._capture_screenshot_b64(max_width=0, quality=75) + if not screenshot_b64: + return False + +- # Essayer la détection popup via le serveur d'abord +- from ..config import SERVER_URL, API_TOKEN +- if SERVER_URL: +- monitor = self.sct.monitors[1] +- sw, sh = monitor["width"], monitor["height"] +- server_result = self._server_resolve_target( +- SERVER_URL, screenshot_b64, +- {"vlm_description": "popup, dialog box, confirmation..."}, +- 0.5, 0.5, sw, sh, +- ) +- if server_result and server_result.get("resolved"): +- ... ++ # Nouveau : déléguer à DialogResolver serveur (single source of truth) ++ from ..config import SERVER_URL ++ if SERVER_URL: ++ try: ++ r = requests.post( ++ f"{SERVER_URL}/api/v1/dialog/resolve", ++ json={"screenshot": screenshot_b64, ++ "workflow_context": { ++ "action_id": self._current_action_id, ++ "declared_dialogs": self._current_declared_dialogs, ++ }}, ++ headers=self._auth_headers(), ++ timeout=8.0, ++ ) ++ if r.ok: ++ data = r.json() ++ if not data.get("dialog_detected"): ++ return False ++ event = data["event"] ++ if event["policy_applied"] in ("escalate_security", "ask_human"): ++ # Le serveur a déjà tracé l'event, on positionne juste ++ # le flag pause locale pour que le caller remonte. ++ self._system_dialog_pause = { ++ "category": event["dialog_type"], ++ "matched_signal": event["classification_method"], ++ "matched_value": event.get("ocr_text", "")[:80], ++ "reason": f"DialogResolver: {event['dialog_type']}", ++ "context": "handle_popup_vlm", ++ } ++ return False ++ # DECLARATIVE/AUTO_DISMISS → on a un click_target ++ target = data.get("click_target") ++ if target: ++ real_x = int(target["x_pct"] * sw) ++ real_y = int(target["y_pct"] * sh) ++ self._click((real_x, real_y), "left") ++ time.sleep(1.0) ++ return True ++ except Exception as e: ++ logger.warning("[POPUP-VLM] DialogResolver server failed: %s", e) + +- # Fallback : VLM local identifie le bouton à cliquer +- button_text = self._vlm_identify_popup_button(screenshot_b64) ++ # Fallback (serveur indisponible) : VLM local existant ++ button_text = self._vlm_identify_popup_button(screenshot_b64) + ... +``` + +--- + +## 6. Activation `_handle_possible_popup` orphelin — décision finale + +**Verdict : SUPPRIMER.** 5 raisons : + +1. **0 site d'appel** confirmé par grep — code mort (`executor.py:2960`). +2. **Antipattern strict `feedback_100pct_visual.md`** : tente Enter → Escape → Tab+Enter de manière aveugle. Or Échap dans un formulaire métier peut purger des données saisies ; Tab+Enter sur "Confirmer l'enregistrement" peut cliquer "Non" si le focus est dessus. +3. **Antipattern `feedback_lea_reflexes_catalog.md`** : si on veut composer Enter/Escape, ça passe par `gesture_catalog.py`, pas par un handler ad hoc. +4. **Couvert par `_handle_popup_vlm` actif** (4 sites d'appel) qui suit la cascade autorisée VLM → InfiGUI → OCR → click ciblé. +5. **Confusion code** : 2 méthodes avec noms similaires (`_handle_possible_popup` vs `_handle_popup_vlm`) → dette technique. Nettoyage = 1 modification triviale. + +**Patch suggéré** (lecture seule, à proposer à Dom) : + +```diff +# agent_v0/agent_v1/core/executor.py +@@ -2956,73 +2956,8 @@ + # ========================================================================= +- # Gestion automatique des popups imprevues (legacy clavier) ++ # NOTE: _handle_possible_popup supprimé (orphelin, antipattern Tab+Enter aveugle). ++ # Cf. AXE_D2_DEEP_POPUP_CHAIN §6. Remplacé par chaîne DialogResolver serveur. + # ========================================================================= +- +- def _handle_possible_popup(self) -> bool: +- """Tenter de gerer une popup imprevue. +- ... +- """ +- hash_before = self._quick_screenshot_hash() +- ... +- return False +- +- def _press_key(self, key): +- ... +- +- def _press_tab_enter(self): +- ... +``` + +Vérifier après suppression que `_press_key` / `_press_tab_enter` ne sont pas appelés ailleurs (probable, à grep avant patch). + +**Note méthode** : créer une **DETTE-XXX** dans `DETTE_TECHNIQUE.md` pour tracer la suppression et le replacement par DialogResolver. Cohérent avec `feedback_no_rustine.md` (corriger la cause = absence de chaîne unifiée, pas le symptôme). + +--- + +## 7. Coordination Validator B2 ↔ DialogResolver — pseudo-code + +**Principe** : le Validator détecte qu'un check post-action **échoue** ; il route vers DialogResolver pour comprendre si la cause est un modal et le résoudre. + +```python +# agent_v0/server_v1/api_stream.py — handler de REPORT +from agent_v0.server_v1.validator import Validator, Verdict, FailureCategory +from core.dialog import DialogResolver +from core.dialog.signatures import Policy + +async def report_action_result(payload): + ... + # Phase 1 : Validator B2 (matrice par action_type) + val = _validator.validate( + action=action, result=result, + screenshot_before=before, screenshot_after=after, + context=ctx, + ) + + # Phase 2 : si Validator suspecte un modal OU verdict TERMINATE → DialogResolver + needs_dialog_check = ( + val.verdict == Verdict.TERMINATE + or val.failure_category in ( + FailureCategory.UNEXPECTED_DIALOG, + FailureCategory.NO_VISUAL_CHANGE, + FailureCategory.WRONG_APPLICATION, # cas bug step 10 démo GHT + ) + ) + if needs_dialog_check: + resolver = _get_dialog_resolver() + event = resolver.check_and_resolve( + after_pil, + workflow_context={ + "step_idx": ctx["step_idx"], + "action_id": action["action_id"], + "declared_dialogs": ctx.get("declared_dialogs", {}), + }, + force_classify=(val.failure_category == FailureCategory.UNEXPECTED_DIALOG), + ) + if event and event.policy_applied == Policy.DECLARATIVE and event.action_taken == "resolved": + # Modal métier auto-résolu → on RÉ-EXÉCUTE l'action originale + logger.info("[B2+D2] Dialog résolu (%s) → re-tentative action %s", + event.dialog_type, action["action_id"]) + return jsonify({"replay_status": "retry_after_dialog", + "dialog_event": event.to_dict()}) + + if event and event.policy_applied in (Policy.ESCALATE_SECURITY, Policy.ASK_HUMAN): + return jsonify({"replay_status": "paused_need_help", + "dialog_event": event.to_dict(), + "validator_evidence": val.to_dict()}) + + # Phase 3 : Validator dit COMPLETE → continuer + if val.verdict == Verdict.COMPLETE: + return jsonify({"replay_status": "in_progress"}) + + # Phase 4 : Validator dit CONTINUE (effet pas encore visible) → re-vérifier + return jsonify({"replay_status": "wait_recheck", "recheck_ms": 1500}) +``` + +**Interface contractuelle** : +- B2 produit `ValidationResult.failure_category` typé (cf. AXE_B2 §6.1). +- D2 consomme cette `failure_category` pour décider de lancer/skipper le ChangeDetector. +- Boucle : D2 résout métier → renvoie `retry_after_dialog` → B2 re-valide après retry. + +--- + +## 8. Heartbeat & state machine + +### 8.1. Rythme d'invocation + +| Phase Léa | Rythme `DialogResolver.check_and_resolve` | Justification | +|---|---|---| +| **`exec_action`** (action en cours côté client) | jamais (le client ne capture pas) | Pas pertinent, pas de signal | +| **`post_action_report`** (REPORT serveur) | **SYSTÉMATIQUE** (1 fois par action) | Site primaire — cf. §5.1 Site 1 | +| **`validator_b2`** (Validator post-action déclenché) | **CONDITIONNEL** (si TERMINATE/NO_VISUAL_CHANGE/UNEXPECTED_DIALOG) | Délégation §7 | +| **`heartbeat_observe`** (tick `observe_reason_act` côté serveur) | **OPTIONNEL** (toutes les 5 s pendant un wait long) | Capture un modal apparu pendant attente t2a/extract_text | +| **`paused_state`** (Léa en pause manuelle) | jamais | Inutile, humain a la main | + +**Total coût démo (40 steps, 2 min cible)** : +- Post-REPORT : 40 × 50ms (ChangeDetector seul si pas de modal) = **2 s** +- Si 5 modaux détectés sur la démo : +5 × 200ms (OCR+classif) = **+1 s** +- Si 1 modal INCONNU appelle VLM fallback : +1 × 1.7s = **+1.7 s** +- **Total : ~5 s sur démo de 120 s = 4 % overhead**. Acceptable. + +### 8.2. State machine Léa simplifiée + +``` + ┌──────────────┐ + │ IDLE │ + └──────┬───────┘ + │ start_action + ▼ + ┌──────────────┐ + │ EXEC_ACTION │ (client capture+click) + └──────┬───────┘ + │ report + ▼ + ┌──────────────┐ ChangeDetector + Classifier + │ POST_REPORT │──────► + Validator B2 + └──────┬───────┘ + │ + ┌────────────┼────────────┬─────────────┐ + ▼ ▼ ▼ ▼ + no_dialog metier_auto system_dialog unknown + continue resolved pause_super pause_super + │ │ │ │ + └────────────┴───────┬────┴─────────────┘ + ▼ + ┌──────────────┐ + │ NEXT_ACTION │ + │ or PAUSED │ + └──────────────┘ +``` + +--- + +## 9. Test offline pytest — snippet complet + +### 9.1. `tests/unit/test_dialog_chain.py` + +```python +"""tests/unit/test_dialog_chain.py — Tests offline chaîne DialogResolver. + +Charge des screenshots fixture, vérifie la cascade complète : +ChangeDetector → DialogClassifier → DialogResolver → action. +""" +from __future__ import annotations + +from pathlib import Path +from typing import Dict, List + +import pytest +from PIL import Image + +from core.dialog import ChangeDetector, DialogClassifier, DialogResolver +from core.dialog.signatures import DialogType, Policy + +FIXTURE_DIR = Path(__file__).parent.parent / "fixtures" / "dialogs" + + +def _load(name: str) -> Image.Image: + path = FIXTURE_DIR / name + if not path.exists(): + pytest.skip(f"Fixture absente : {path}") + return Image.open(path).convert("RGB") + + +def _fake_ocr(text: str): + """Helper : retourne une fonction OCR qui renvoie toujours `text`.""" + return lambda img: text + + +# ── Tests ChangeDetector ─────────────────────────────────────────────── + + +def test_change_detector_first_call_returns_no_change(): + """Premier appel : pas de référence précédente → is_modal=False.""" + det = ChangeDetector() + img = Image.new("RGB", (1920, 1080), color=(128, 128, 128)) + signal = det.detect(img) + assert signal.is_modal is False + assert signal.elapsed_ms < 100 # cible < 50ms, marge + + +def test_change_detector_detects_central_change(): + """Centre change beaucoup, périphérie stable → is_modal=True.""" + det = ChangeDetector() + img_before = Image.new("RGB", (1920, 1080), color=(200, 200, 200)) + det.detect(img_before) # 1er appel + img_after = img_before.copy() + # Modal centré 800×500 sombre + from PIL import ImageDraw + draw = ImageDraw.Draw(img_after) + draw.rectangle((560, 290, 1360, 790), fill=(50, 50, 50)) + signal = det.detect(img_after) + assert signal.is_modal is True + assert signal.diff_ratio_central > 0.1 + + +def test_change_detector_secure_desktop(): + """Écran majoritairement très sombre → UAC secure desktop.""" + det = ChangeDetector() + img_dark = Image.new("RGB", (1920, 1080), color=(30, 30, 30)) + signal = det.detect(img_dark) + assert signal.secure_desktop is True + assert signal.is_modal is True + + +# ── Tests DialogClassifier ───────────────────────────────────────────── + + +@pytest.mark.parametrize("ocr_text,expected", [ + ("Contrôle de compte d'utilisateur Voulez-vous autoriser", DialogType.UAC), + ("User Account Control", DialogType.UAC), + ("Windows Hello Saisissez votre code PIN", DialogType.HELLO), + ("Touchez le capteur d'empreintes digitales", DialogType.HELLO), + ("Windows a protégé votre PC", DialogType.SMARTSCREEN), + ("Defender SmartScreen a empêché", DialogType.SMARTSCREEN), + ("Souhaite utiliser votre microphone Autoriser Bloquer", DialogType.BROWSER_PERMISSION), + ("Voulez-vous enregistrer ce mot de passe", DialogType.BROWSER_SAVE_PASSWORD), +]) +def test_classifier_signature_match(ocr_text, expected): + classifier = DialogClassifier(ocr_fn=_fake_ocr(ocr_text)) + img = Image.new("RGB", (100, 100)) + result = classifier.classify(img) + assert result.dialog_type == expected + assert result.method == "signature" + + +def test_classifier_known_dialogs_fallback(): + """KNOWN_DIALOGS catalogue métier (single source of truth).""" + classifier = DialogClassifier(ocr_fn=_fake_ocr("Voulez-vous remplacer le fichier ?")) + img = Image.new("RGB", (100, 100)) + result = classifier.classify(img) + assert result.dialog_type == DialogType.METIER_OVERWRITE + + +def test_classifier_unknown_no_vlm(): + """Texte non match + VLM absent (timeout) → INCONNU.""" + classifier = DialogClassifier(ocr_fn=_fake_ocr("Zorglub flubbergrabben")) + classifier._ollama_host = "127.0.0.1:9999" # port impossible + img = Image.new("RGB", (100, 100)) + result = classifier.classify(img) + assert result.dialog_type == DialogType.INCONNU + + +# ── Tests DialogResolver ─────────────────────────────────────────────── + + +def test_resolver_uac_triggers_pause(): + """UAC → ESCALATE_SECURITY → on_pause callback appelé.""" + calls = [] + def on_pause(event, screenshot, ctx): + calls.append((event.dialog_type, event.policy_applied)) + return False + + img = Image.new("RGB", (1920, 1080), color=(30, 30, 30)) # dark = secure desktop + classifier = DialogClassifier(ocr_fn=_fake_ocr("Contrôle de compte d'utilisateur")) + resolver = DialogResolver( + change_detector=ChangeDetector(), + classifier=classifier, + on_pause_supervised=on_pause, + ) + event = resolver.check_and_resolve(img) + assert event is not None + assert event.dialog_type == DialogType.UAC + assert event.policy_applied == Policy.ESCALATE_SECURITY + assert calls == [(DialogType.UAC, Policy.ESCALATE_SECURITY)] + + +def test_resolver_workflow_declared_browser_permission(): + """Permission micro DÉCLARÉE dans workflow → DECLARATIVE handler appelé.""" + declared_calls = [] + def declared_handler(event, screenshot, ctx): + declared_calls.append(event.dialog_type) + return True + + img = Image.new("RGB", (1920, 1080)) + classifier = DialogClassifier(ocr_fn=_fake_ocr("Souhaite utiliser votre microphone")) + resolver = DialogResolver( + change_detector=ChangeDetector(), + classifier=classifier, + workflow_declared_handlers={ + DialogType.BROWSER_PERMISSION: declared_handler, + }, + ) + # Forcer is_modal pour éviter dépendance ChangeDetector + event = resolver.check_and_resolve( + img, + workflow_context={ + "declared_dialogs": {"browser_permission": {"action": "allow"}}, + }, + force_classify=True, + ) + assert event.policy_applied == Policy.DECLARATIVE + assert event.action_taken == "resolved" + assert declared_calls == [DialogType.BROWSER_PERMISSION] + + +def test_resolver_suspect_ok_escalates(): + """Mots-clés 'supprimer définitivement' → ASK_HUMAN même si OK trivial.""" + img = Image.new("RGB", (1920, 1080)) + classifier = DialogClassifier( + ocr_fn=_fake_ocr("OK pour supprimer définitivement ce fichier") + ) + resolver = DialogResolver(classifier=classifier) + event = resolver.check_and_resolve(img, force_classify=True) + assert event.dialog_type == DialogType.METIER_OK_SUSPECT + assert event.policy_applied == Policy.ASK_HUMAN +``` + +### 9.2. Comment produire les fixtures `tests/fixtures/dialogs/` + +PowerShell snippets pour générer chaque type de dialog sur le PC Windows Léa : + +```powershell +# fixture_uac.png — déclencher UAC via runas +Start-Process powershell -Verb RunAs +# (cliquer Non → screenshot pendant prompt) + +# fixture_hello.png — déclencher Hello via gestionnaire identifiants +control.exe /name Microsoft.CredentialManager +# (cliquer "Ajouter un identifiant Windows" puis screenshot) + +# fixture_smartscreen.png — télécharger un .exe non signé +Invoke-WebRequest -Uri "https://example.com/test.exe" -OutFile "$env:TEMP\test.exe" +& "$env:TEMP\test.exe" +# (SmartScreen popup → screenshot) + +# fixture_browser_permission.png — page test microphone +Start-Process msedge "https://webcamtests.com" +# (autoriser micro → screenshot) + +# fixture_metier_save.png — Bloc-notes non sauvé +Start-Process notepad +# (taper texte, Ctrl+W → "Voulez-vous enregistrer ?") +``` + +**Sources publiques de captures** (si Dom n'a pas d'accès Windows live pour générer) : +- [Windows 11 UAC screenshots galerie Microsoft Learn](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/user-account-control/how-it-works) — fixture statiques officielles. +- [Defender SmartScreen UI samples — Microsoft GitHub docs](https://github.com/MicrosoftDocs/windows-itpro-docs/tree/public/windows/security/threat-protection/microsoft-defender-smartscreen). +- Notre `data/runner_captures/` contient déjà des screenshots de replays — `grep`er ceux qui ont émis un event "popup_handled" pour réutilisation. + +**Tests sans fixture (CI sans Windows)** : les `_fake_ocr(...)` permettent de tester la cascade Signature → Classifier → Resolver sans aucun screenshot réel. Couvre 80 % de la logique. Les fixtures restent utiles pour `ChangeDetector` (qui dépend du diff pixel réel). + +--- + +## 10. Patterns externes 2026 (compléments à AXE_D2 §3) + +### 10.1. Skyvern dialog handling (vérif source mai 2026) + +**Issue #69 ouverte sept. 2024, toujours active** : « Unable to interact with popup modals on costcotravel.com ». Skyvern délègue tout à son Validator post-action (cf. `complete_verify` analysé dans AXE_B2 §2.1). Pas de DialogResolver dédié → c'est leur **point faible**. + +[Source : Skyvern Issue #69](https://github.com/Skyvern-AI/skyvern/issues/69), [Prompting Guide](https://docs.skyvern.com/getting-started/prompting-guide) — la doc officielle invite à **décrire le popup attendu dans le prompt** (« déclaratif workflow ») exactement comme notre `declared_dialogs[...]`. + +### 10.2. browser-use Issue #1996 (juin 2025, closed) + +[Source : browser-use Issue #1996](https://github.com/browser-use/browser-use/issues/1996) — issue **fermée sans fix** (« handling left to the LLM in the prompt »). Confirme que **l'écosystème open source n'a pas de standard** sur ce sujet. Notre approche `signatures + VLM fallback` est en avance. + +### 10.3. Anthropic Computer Use 2026 — politique dialogs + +[Source : Computer Use API Docs](https://docs.anthropic.com/en/docs/build-with-claude/computer-use), [Claude Opus 4.6 system card fév. 2026](https://www-cdn.anthropic.com/0dd865075ad3132672ee0ab40b05a53f14cf5288.pdf) : + +- **Permission-first** : Claude demande confirmation avant tout nouvel app. +- **Classifiers anti-prompt-injection** : screenshots suspects → demande confirmation user. +- **Pas de UAC/Hello handling spécifique** documenté. Anthropic se repose sur le fait que Claude **identifie** un dialog système et **refuse implicitement** d'y cliquer si pas explicitement instruit. Risqué : aucune garantie. + +Notre approche `system_dialog_guard.py` (multi-signal ClassName UIA + processus + titre) est **plus robuste** que la self-reflection LLM. + +### 10.4. OpenAI Operator / ChatGPT Agent — handover explicite + +[Source : ChatGPT Agent help](https://help.openai.com/en/articles/11752874-chatgpt-agent), [Operator system card](https://openai.com/index/operator-system-card/) : + +- Sur CAPTCHA, login, paiement → **« proactively asks the user to take over »**. +- Pendant le takeover : **screenshots OFF** (protection credentials). +- Modèle CUA 2026 : OSWorld 45 % (vs 38 % preview). + +**Pattern transposable** : notre `pause_supervised` doit ressembler à ce handover. Le dashboard VWB devrait afficher le screenshot **figé au moment du modal** (pas live) puis reprendre la capture après resolution humaine. Cohérent avec `feedback_failure_is_learning.md`. + +### 10.5. Cradle (BAAI) — Self-Reflection module + +[Source : Cradle GitHub](https://github.com/BAAI-Agents/Cradle), [arXiv 2403.03186](https://arxiv.org/pdf/2403.03186) — agent jeu vidéo avec module Self-Reflection (+20.41 pts). N'a **pas** de DialogResolver spécifique car le contexte (jeu) n'a pas de modaux système Windows. Non transposable directement. + +### 10.6. UAC secure desktop detection (Win32) + +[Source : Microsoft GetSystemMetrics docs](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsystemmetrics), [PyAutoGUI _pyautogui_win.py](https://github.com/asweigart/pyautogui/blob/master/pyautogui/_pyautogui_win.py), [Sigma rule UAC secure desktop disabled](https://detection.fyi/sigmahq/sigma/windows/registry/registry_set/registry_set_uac_disable_secure_desktop_prompt/) : + +- `ctypes.windll.user32.GetSystemMetrics(SM_REMOTESESSION=0x1000)` indique session distante (RDP/Citrix), pas secure desktop directement. +- **Pas d'API publique** Win32 pour détecter le secure desktop UAC. Solution : screenshot luminance (déjà dans `ChangeDetector._check_foreground_changed`). +- Registry `PromptOnSecureDesktop=0` désactive le secure desktop (option config Windows, à anticiper en démo). + +--- + +## 11. Plan d'intégration gradué + +### 11.1. Court terme — 1 jour (P0, avant prochaine démo) + +**But** : MVP fonctionnel sans casser l'existant, kill-switch off par défaut. + +1. Créer `core/dialog/` avec les 5 fichiers (§3) — **3 h**. +2. Endpoint serveur `POST /api/v1/dialog/resolve` (§5.1) — **1 h**. +3. Câbler Site 1 (post-REPORT) avec env var `RPA_DIALOG_RESOLVER_ENABLED=false` par défaut — **1 h**. +4. Tests unit (`test_dialog_chain.py` §9) sans fixtures réelles, juste `_fake_ocr` — **2 h**. +5. Smoke test : démarrer Léa + serveur + workflow Demo_urgence_3_db avec flag `=true`, mesurer latence (cible < 50 ms par check sans modal) — **1 h**. + +**Livrable** : DialogResolver disponible derrière flag. Démo inchangée si flag off. + +### 11.2. Moyen terme — 1 semaine (P1) + +1. Migration `_handle_popup_vlm` côté client vers délégation serveur (§5.2 diff) — **3 h**. +2. Suppression `_handle_possible_popup` orphelin + grep cleanup `_press_key/_press_tab_enter` — **1 h**. +3. Capture des 5 fixtures Windows (UAC/Hello/SmartScreen/permission/métier) via PowerShell (§9.2) — **2 h**. +4. Tests d'intégration avec fixtures réelles — **3 h**. +5. Coordination Validator B2 (§7) — **3 h**. +6. Dashboard VWB : panneau "Dialog events" par session (count par type + dernier ocr_text) — **1 j**. + +### 11.3. Long terme — 1 mois (P2) + +1. **Bench injection** : harness qui injecte UAC simulé / popup métier / SmartScreen pendant replay test, mesure detect→classify→resolve, taux pause vs auto-dismiss — **3 j**. +2. **Apprentissage catalogue** : chaque `DialogType.INCONNU` enregistré dans BDD → revue Dom toutes les semaines → enrichit `SIGNATURES_BY_TYPE` (pattern OpenAdapt Evaluation-Driven Feedback) — **continu**. +3. **Win32 UIA hook** : `SetWindowsHookEx(WH_CBT)` pour détecter `HCBT_CREATEWND` d'une fenêtre modale → signal complémentaire au screenshot diff. Pertinence Citrix douteuse (UIA aveugle), à benchmarker — **2 j R&D**. +4. **DialogResolver pour Citrix** : adapter détection (secure desktop UAC d'un client Citrix passe dans le framebuffer hôte) — **3 j**. +5. **Synergie AXE_A5 (tokenisation écran)** : si parser UI produit liste éléments interactifs, classification devient déterministe (matche label bouton) — **dépend roadmap A5**. + +--- + +## 12. Sources (liens cliquables, dates 2025-2026) + +### Frameworks externes + +- [Skyvern Issue #69 — Unable to interact with popup modals](https://github.com/Skyvern-AI/skyvern/issues/69) (sept. 2024, toujours actif) +- [Skyvern Prompting Guide — handle modals declaratively](https://docs.skyvern.com/getting-started/prompting-guide) +- [Skyvern Blog Index](https://www.skyvern.com/blog/) +- [browser-use Issue #1996 — Need Robust Strategy for Handling Dynamic Popups](https://github.com/browser-use/browser-use/issues/1996) (juin 2025, fermée sans fix) +- [Anthropic Computer Use API docs](https://docs.anthropic.com/en/docs/build-with-claude/computer-use) +- [Claude Opus 4.6 system card (fév. 2026)](https://www-cdn.anthropic.com/0dd865075ad3132672ee0ab40b05a53f14cf5288.pdf) +- [OpenAI Operator system card](https://openai.com/index/operator-system-card/) +- [OpenAI ChatGPT Agent — takeover sur login/CAPTCHA](https://help.openai.com/en/articles/11752874-chatgpt-agent) +- [Anthropic Claude Computer Use on Windows (avril 2026)](https://www.thurrott.com/a-i/anthropic/334498/anthropic-brings-claude-computer-use-to-windows) +- [Cradle GitHub (BAAI)](https://github.com/BAAI-Agents/Cradle), [Cradle paper arXiv 2403.03186](https://arxiv.org/pdf/2403.03186) + +### Windows 11 / UAC / Hello / SmartScreen / Secure Desktop + +- [Microsoft UAC architecture](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/user-account-control/architecture) +- [Microsoft GetSystemMetrics function](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsystemmetrics) +- [Sigma rule UAC Secure Desktop Prompt Disabled](https://detection.fyi/sigmahq/sigma/windows/registry/registry_set/registry_set_uac_disable_secure_desktop_prompt/) +- [PyAutoGUI _pyautogui_win.py ctypes patterns](https://github.com/asweigart/pyautogui/blob/master/pyautogui/_pyautogui_win.py) +- [ctypes user32 reference jerblack gist](https://gist.github.com/jerblack/2b294916bd46eac13da7d8da48fcf4ab) + +### CVE & menaces 2026 + +- [CVE-2026-0628 Chrome Gemini Live extension takeover](https://news.corksafetyalerts.com/chrome-flaw-allowed-extensions-to-hijack-googles-ai-assistant-camera-and-microphone/) +- [AI-powered phishing leveraging hardware access (2026)](https://www.scworld.com/brief/ai-powered-phishing-campaign-leverages-hardware-access-for-data-theft) + +### Documents internes rpa_vision_v3 + +- `docs/recherche/AXE_D2_DIALOG_POPUP.md` (parent, matrice §5 autoritative) +- `docs/recherche/AXE_B2_VALIDATOR_PATTERN.md` (interface Verdict, FailureCategory) +- `docs/LESSONS_LEARNED_GHT_2026-05.md` §🔴 (bugs P0) +- `core/grounding/dialog_handler.py` (KNOWN_DIALOGS réutilisé) +- `core/grounding/title_verifier.py` (OCR titre 45px) +- `agent_v0/agent_v1/core/system_dialog_guard.py` (multi-signal système) +- `agent_v0/agent_v1/core/executor.py` (sites d'appel `_handle_popup_vlm`) +- `agent_chat/gesture_catalog.py` (seul "réflexe système" autorisé) +- `memory/feedback_popup_vlm.md`, `feedback_100pct_visual.md`, `feedback_lea_reflexes_catalog.md`, `feedback_auth_dialogs_runtime.md`, `feedback_phash_vs_dialog_in_vm.md` + +--- + +## 13. Hors-périmètre — questions à valider Dom avant action + +1. **Décision suppression `_handle_possible_popup`** : confirmer (grep + retrait, créer DETTE pour traçabilité). +2. **Choix modèle VLM fallback** : `qwen3-vl:8b` retenu (cohérent §2.4 synthèse) mais à benchmarker sur 10 captures de dialogs (fixture). +3. **Politique rétention RGPD/HDS screenshots `DialogEvent`** : par défaut `data/runner_captures/dialogs//.png`, purge après ACK serveur ou TTL 30 j ? Aligner avec `feedback_capture_purge_policy.md`. +4. **Workflow `declared_dialogs`** : extension VWB pour permettre au designer de déclarer "à cette étape, autoriser le micro". Format JSON suggéré : `{"browser_permission": {"action": "allow", "label": "Autoriser"}}`. À spécifier avec frontend VWB. +5. **Synergie AXE_B1 (watchdog transport)** : si une action est en `_retry_pending` côté serveur et qu'un modal apparaît côté Léa pendant l'attente, le watchdog doit-il propager l'event ? Couplage à clarifier. +6. **Bench latence empirique** : valider `ChangeDetector < 50 ms` sur capture réelle 2560×1600 (Demo_urgence_3_db). Si dépassement, downscale 1/4 avant diff numpy. + +--- + +*Document de recherche. Lecture seule sur code existant. Suite = décision Dom + chirurgie itérative supervisée (CLAUDE.md projet).* diff --git a/docs/recherche/AXE_D2_DIALOG_POPUP.md b/docs/recherche/AXE_D2_DIALOG_POPUP.md new file mode 100644 index 000000000..76d270daf --- /dev/null +++ b/docs/recherche/AXE_D2_DIALOG_POPUP.md @@ -0,0 +1,552 @@ +# AXE D2 — Gestion des modaux & popups imprévus + +**Date :** 2026-05-23 +**Auteur :** agent recherche (dispatch session principale) +**Périmètre :** stratégie pour gérer en 100% vision les modaux Windows / navigateur / métier qui interrompent un replay Léa. +**Statut :** brief de recherche — aucune modification de code proposée. Décisions = Dom. + +--- + +## 1. TL;DR + recommandation architecture + +### 1.1. Diagnostic + +- Le projet a déjà 80% de l'outillage : `core/grounding/dialog_handler.py` (EasyOCR + InfiGUI + fallback OCR direct) couvre la **résolution** d'un dialog connu. +- Trois lacunes documentées : + 1. Côté **client Léa**, `_handle_possible_popup` est défini avec **0 site d'appel** (`LESSONS_LEARNED_GHT_2026-05.md`, F5.5.1) — il existe également un `_handle_popup_vlm` qui prend le relais mais sans déclencheur générique. + 2. Pas de **détecteur d'apparition** (la cascade ne sait pas qu'un modal vient d'apparaître — elle continue à viser l'élément initial, qui est désormais masqué). + 3. Pas de **politique de fail-safe** différenciée par type de modal (UAC ≠ "fichier déjà existe" ≠ Windows Hello). +- Anti-pattern interdit (`feedback_100pct_visual.md`) : inventer un raccourci Win+R / Ctrl+X / Échap-systématique. La cascade reste **OCR → template → VLM**, jamais "réflexe magique". + +### 1.2. Recommandation + +Une stack en trois couches, branchée APRÈS chaque action (et au démarrage du tick d'observation), réutilisant le code existant : + +``` +ChangeDetector ──► DialogClassifier ──► DialogResolver +(léger, < 50ms) (OCR titre + LLM) (catalogue déclaratif + + InfiGUI/OCR + escalation) +``` + +1. **ChangeDetector** — détecte qu'un modal *vient d'apparaître*. Combinaison foreground-window-change (Windows API côté client) + screenshot diff région centrale + heuristiques visuelles (ombre, centrage). Latence cible < 50 ms. +2. **DialogClassifier** — décide *quoi faire* (catalog match → action déterministe ; sinon → VLM pour catégoriser). Réutilise `_read_title` + `KNOWN_DIALOGS` du `dialog_handler.py` actuel, étend avec une catégorisation par type (`UAC` / `HELLO` / `SMARTSCREEN` / `BROWSER_PERM` / `METIER_SAVE` / `INCONNU`). +3. **DialogResolver** — applique la **matrice modal → action** (§5). En santé : aucun auto-accept système (UAC/Hello/SmartScreen) — uniquement pause supervisée. Auto-dismiss déterministe **uniquement** pour les modaux métier déclarés dans le workflow (ex. "fichier déjà existe → Oui"). + +Cette stack résout **mécaniquement** la dépendance avec AXE_B2 (Validator) : un modal non détecté = un Validator post-action raté ; un Validator sémantique strict force le détecteur à être appelé avant le verdict. + +--- + +## 2. Taxonomie des modaux Windows 11 / navigateur (mai 2026) + +Sept catégories qui couvrent ~95% des cas terrain healthtech. + +### 2.1. UAC (User Account Control) + +- **Quand** : installation, élévation `runas`, modification système. +- **Aspect visuel** : fond bleu ou assombri global (secure desktop), titre `Contrôle de compte d'utilisateur`, deux boutons `Oui` / `Non` ou champ mot de passe administrateur. +- **Particularité** : sur **secure desktop**, screenshot Léa peut être noir/inaccessible (`pyautogui` ne voit rien). Microsoft a livré des fixes UAC en oct. 2025 → janv. 2026 (KB5063878 / KB5074109) — quelques replays plantent encore sur 24H2 idle/locked. +- **Politique healthtech recommandée** : **JAMAIS auto-accept**. Pause supervisée immédiate. + +### 2.2. Windows Hello / WebAuthn / FIDO2 + +- **Quand** : déverrouillage app sensible (gestionnaire de mots de passe, Outlook), site avec WebAuthn, élévation UAC configurée pour exiger biométrie. +- **Aspect visuel** : popup système centré, icône empreinte ou caméra, message "Touchez le capteur" ou "Saisissez votre code PIN". +- **Particularité** : nécessite **interaction physique humaine** par construction. Aucune solution 100% vision ne peut résoudre. Anti-pattern absolu : tenter de cliquer "Annuler" pour passer outre. +- **Politique** : pause supervisée + **tip** suggérant "désactiver Windows Hello pour la session de démo" en config préalable. + +### 2.3. Microsoft Defender SmartScreen + +- **Quand** : exe non signé, téléchargement suspect, premier lancement Léa. +- **Aspect visuel** : bandeau bleu ciel "Windows a protégé votre PC", lien "Informations complémentaires" qui révèle le bouton "Exécuter quand même". +- **Politique** : pause supervisée + log security. Notre `feedback_auth_dialogs_runtime.md` rappelle d'anticiper AVANT démo client (signature code, allowlist hash SHA256 — voir `project_code_signing.md`). + +### 2.4. Permissions navigateur (caméra, micro, notifications, géoloc, clipboard) + +- **Quand** : première visite site `https://`, démo Easily Assure si elle demande accès clipboard / notifications. +- **Aspect visuel** : popup ancrée à l'URL bar (Chrome/Edge), boutons `Autoriser` / `Bloquer`. Variant inline overlay (Edge 2026). +- **Particularité critique sécurité (CVE-2026-0628 janv. 2026)** : malveillances via extensions Chrome qui détournent des permissions. Auto-accept = risque RGPD/HDS. +- **Politique** : **déclaratif dans le workflow** ("À cette étape, autoriser le micro") ou pause supervisée. Aucun auto-accept générique. + +### 2.5. Dialog métier (sauvegarde, écrasement, perte de modifications) + +- **Quand** : fermeture document non sauvé, "fichier existe déjà", "Voulez-vous quitter ?". +- **Aspect visuel** : popup applicative au-dessus de la fenêtre parente, OCR titre fiable, boutons texte clair (`Oui` / `Non` / `Annuler` / `Enregistrer`). +- **Politique** : **catalogue déclaratif**. C'est exactement ce que `KNOWN_DIALOGS` du `dialog_handler.py` actuel gère. Résolution InfiGUI + fallback OCR direct. + +### 2.6. Avertissements / erreurs applicatives (popup OK) + +- **Quand** : timeout réseau, validation backend KO, message info-utilisateur. +- **Aspect visuel** : popup centré, icône triangle/croix, un seul bouton `OK` / `Fermer`. +- **Politique** : auto-dismiss déterministe SAUF si message critique (regex blocklist "données perdues", "supprimé"). Log obligatoire. + +### 2.7. Modaux inattendus / inconnus (notification push Teams, Outlook reminder, popup mise à jour, cookie banner, newsletter) + +- **Quand** : tout le temps, surtout en démo live (Slack ping, Outlook reminder à xx:00, popup Windows Update). +- **Aspect visuel** : très variable. Souvent en coin (toast) plutôt que centré, mais peut voler le focus. +- **Politique** : VLM classifier (catégoriser : "publicité" / "système" / "métier inconnu") → pause supervisée avec proposition humaine ou auto-dismiss conservateur (Échap visuel via clic croix détectée). + +--- + +## 3. Comparaison frameworks 2026 + +| Framework | Détection modal | Politique | Pause humaine | Catalogue déclaratif | +|---|---|---|---|---| +| **Anthropic Computer Use** (avril 2026) | classifiers anti-prompt-injection détectent screenshots suspects → demande confirmation user | permission-first par défaut, refuse "high-risk", monitor model peut pause | oui (Auto Mode mai 2026 = classifier décide auto-approve vs prompt) | non public | +| **OpenAI Operator / ChatGPT Agent** (juillet 2025 → 2026) | "monitor model" surveille comportement suspect ; CAPTCHA / login → cède le contrôle à l'humain (screenshots OFF pendant ce temps) | confirme avant action critique, prend la main sur prompts sécurité | oui, prend-le-relais explicite | non | +| **Skyvern 2.0** (oct. 2025) | **Validator** regarde écran post-action, détecte "popup blocked the click", redonne main au Planner pour retry | retry visuel ; built-in TOTP / 2FA / file downloads | non documenté publiquement | implicite via Validator | +| **browser-use** (issue #1996 ouvert mai 2026) | **pas de solution intégrée** — la communauté demande explicitement une infra générique | au cas par cas | non | non | +| **Cradle** (R&D fin 2025) | screenshot-only, demande au VLM principal d'identifier popup à chaque tick | dépend prompt | non | non | +| **PopSweeper** (recherche acad. 2024, applicable mobile) | classifier deux étages **ResNet50 + MobileNetV2** + **YOLO-World** pour bouton croix ; image-diff 100 ms tick ; **60 ms par frame** | auto-dismiss centré sur close-button | non | non | +| **rpa_vision_v3 (actuel)** | aucun détecteur ; `dialog_handler.py` ne s'exécute que si appelé explicitement | catalogue déclaratif + InfiGUI/OCR fallback | feedback_failure_is_learning oui | oui (`KNOWN_DIALOGS`) | + +**Lecture** : +- Skyvern et OpenAI Operator **valident l'idée centrale** : un Validator strict détecte le modal indirectement (action attendue échoue → l'écran a changé sans cause connue → modal). +- Anthropic et OpenAI **convergent** sur le "monitor model" parallèle qui pause sans toucher au workflow principal. +- PopSweeper démontre qu'un détecteur **léger spécialisé** (CNN custom < 60 ms) suffit pour ne PAS ajouter de coût VLM à chaque tick. +- L'écosystème open source (browser-use) n'a **toujours pas** de solution générique — le sujet est ouvert. + +--- + +## 4. Architecture proposée pour rpa_vision_v3 + +### 4.1. Vue d'ensemble + +``` + ┌───────────────────────────────────────────┐ + │ Léa (Windows client) │ + │ │ + action click ───►│ executor │ + │ │ │ + │ ▼ │ + │ perform_click │ + │ │ │ + │ ▼ │ + │ ChangeDetector ◄─── screenshot t+1 ──┐ │ + │ │ │ │ + │ ▼ │ │ + │ is_modal_appeared? │ │ + │ │ │ │ + │ no ┴ yes │ │ + │ │ │ │ + │ ▼ │ │ + │ DialogClassifier │ │ + │ │ │ │ + │ ▼ │ │ + │ type ∈ {UAC, HELLO, SMART, PERM, │ │ + │ SAVE, OK, INCONNU} │ │ + │ │ │ │ + │ ▼ │ │ + │ DialogResolver │ │ + │ │ │ │ + │ ▼ │ │ + │ policy(type, workflow_ctx) │ │ + │ │ │ │ + │ ▼ │ │ + │ ┌─ AUTO_DISMISS (OK trivial) │ │ + │ ├─ DECLARATIVE (catalog match) │ │ + │ ├─ ASK_HUMAN (pause supervisée) │ │ + │ └─ ESCALATE_SECURITY (log + pause) │ │ + │ │ │ + │ ▼ │ │ + │ resume_or_wait │ │ + │ │ │ + └───────────────────────────────────────┘ + │ + ▼ + serveur (api_stream) + │ + ▼ + report dialog event + replay state +``` + +### 4.2. Étendre `core/grounding/dialog_handler.py` existant + +Le fichier actuel (lecture seule pour ce doc, voir Read) fait déjà : +- `_read_title` (EasyOCR full-screen, `fr+en`, GPU) +- catalogue `KNOWN_DIALOGS` ordonné (popups modaux prioritaires avant fenêtres parents) +- `_click_via_infigui` (UI-TARS / InfiGUI grounder déjà branché) +- `_click_via_ocr` (fallback OCR direct) +- retour `dict` avec `handled / title / dialog_type / action / position / time_ms` + +**À ajouter (esquisses, pas de code à committer) :** + +```python +# core/grounding/change_detector.py — nouveau fichier proposé + +from dataclasses import dataclass +from typing import Optional, Tuple +import time + +import numpy as np + + +@dataclass +class ChangeSignal: + """Signal qu'un changement écran significatif vient d'apparaître.""" + is_modal: bool # heuristique modal vs scroll normal + foreground_hwnd_changed: bool # côté Windows uniquement + diff_ratio: float # 0.0 = identique, 1.0 = tout différent + central_diff_ratio: float # diff zone centrale (modaux centrés) + timestamp: float + + +class ChangeDetector: + """Détecte qu'un modal *vient d'apparaître* sans appeler le VLM principal. + + Stratégie : + 1) foreground window change (Windows API, ~1 ms) + 2) screenshot diff centre vs périphérie (numpy diff sur sous-region, ~10 ms) + 3) heuristique "centré + bordure assombrie" (modal pattern, ~5 ms) + Budget total cible : < 50 ms pour ne PAS ralentir la boucle replay. + """ + + def __init__(self): + self._last_screenshot = None + self._last_hwnd = None + + def detect(self, screenshot_pil) -> ChangeSignal: + t0 = time.time() + arr = np.asarray(screenshot_pil.convert("L")) + + fg_changed = self._check_foreground_changed() + + diff_ratio = 0.0 + central_diff = 0.0 + if self._last_screenshot is not None: + prev = np.asarray(self._last_screenshot.convert("L")) + if prev.shape == arr.shape: + diff = np.abs(prev.astype(int) - arr.astype(int)) + diff_ratio = float((diff > 25).mean()) + # zone centrale = modaux Windows typiques + h, w = arr.shape + cy0, cy1 = h // 4, 3 * h // 4 + cx0, cx1 = w // 4, 3 * w // 4 + central_diff = float((diff[cy0:cy1, cx0:cx1] > 25).mean()) + + is_modal = ( + fg_changed + or (central_diff > 0.10 and diff_ratio < 0.40) # changement centré + ) + + self._last_screenshot = screenshot_pil + return ChangeSignal( + is_modal=is_modal, + foreground_hwnd_changed=fg_changed, + diff_ratio=diff_ratio, + central_diff_ratio=central_diff, + timestamp=t0, + ) + + def _check_foreground_changed(self) -> bool: + """Côté Windows uniquement — sinon retourne False.""" + try: + import ctypes # noqa + hwnd = ctypes.windll.user32.GetForegroundWindow() + changed = (self._last_hwnd is not None) and (hwnd != self._last_hwnd) + self._last_hwnd = hwnd + return changed + except Exception: + return False +``` + +**Note importante** : `feedback_popup_vlm.md` documente que `GetForegroundWindow` est **non fiable seul** (retourne 0 en SSH, popups Windows modernes partagent hwnd du parent). On l'utilise comme **signal complémentaire**, jamais comme source unique. La détection finale repose sur le diff écran (vision). + +### 4.3. DialogClassifier — extension du `dialog_handler.py` + +```python +# core/grounding/dialog_classifier.py — proposition + +from enum import Enum + + +class DialogType(str, Enum): + UAC = "uac" + HELLO = "windows_hello" + SMARTSCREEN = "defender_smartscreen" + BROWSER_PERMISSION = "browser_permission" + METIER_SAVE = "metier_save" # match catalog KNOWN_DIALOGS + METIER_CONFIRM = "metier_confirm" + OK_TRIVIAL = "ok_trivial" # popup avec 1 seul bouton OK + INCONNU = "inconnu" + + +# Signatures texte par type (extension du catalogue actuel) +TYPE_SIGNATURES = { + DialogType.UAC: [ + "contrôle de compte d'utilisateur", + "user account control", + "voulez-vous autoriser cette application", + ], + DialogType.HELLO: [ + "windows hello", "saisissez votre code pin", + "touchez le capteur d'empreintes", + ], + DialogType.SMARTSCREEN: [ + "windows a protégé votre pc", + "smartscreen", "informations complémentaires", + ], + DialogType.BROWSER_PERMISSION: [ + "autoriser", "bloquer", + "souhaite utiliser votre caméra", + "souhaite utiliser votre microphone", + "souhaite afficher des notifications", + ], + # METIER_SAVE et METIER_CONFIRM = KNOWN_DIALOGS existant +} + + +class DialogClassifier: + """Classifie un dialogue détecté en type connu. + + Stratégie cascade : + 1) match signatures texte (OCR titre) — ~150 ms (EasyOCR cache) + 2) si pas de match → VLM compact (qwen3-vl:8b) avec prompt + "Classify this dialog: uac / hello / smartscreen / browser_perm / + metier / unknown" — ~1.7 s + 3) fallback : INCONNU + """ + + def classify(self, screenshot_pil, ocr_text: str) -> DialogType: + text = ocr_text.lower() + for dtype, signatures in TYPE_SIGNATURES.items(): + for sig in signatures: + if sig in text: + return dtype + + # Catalogue métier existant (KNOWN_DIALOGS du dialog_handler.py) + from core.grounding.dialog_handler import KNOWN_DIALOGS + for key in KNOWN_DIALOGS: + if key in text: + return DialogType.METIER_SAVE # ou METIER_CONFIRM selon key + + # Fallback VLM si rien ne matche et qu'on a un signal modal fort + return self._classify_via_vlm(screenshot_pil) or DialogType.INCONNU + + def _classify_via_vlm(self, screenshot_pil) -> Optional[DialogType]: + # Appel qwen3-vl:8b via Ollama LAN (port 11434) + # Prompt court, format=json strict + # ⚠ qwen3-vl:8b ignore parfois format=json — fallback regex sur stdout + ... +``` + +### 4.4. DialogResolver — politique par type + +Voir matrice §5 ci-dessous. Le resolver applique la politique et émet un événement structuré au serveur : + +```python +@dataclass +class DialogEvent: + type: DialogType + title_ocr: str + policy_applied: str # "auto_dismiss" / "declarative" / "ask_human" / "escalate_security" + action_taken: Optional[str] # "click 'Oui' (123,456)" / "paused" + duration_ms: float + screenshot_path: str # toujours archivé pour audit +``` + +--- + +## 5. Matrice modal → action + +| Type | Détection signature | Politique healthtech | Action concrète | Audit | +|---|---|---|---|---| +| **UAC** | "contrôle de compte", "user account control" | **escalate_security + ask_human** — JAMAIS auto-accept | `pause_for_human` ; toast "élévation requise — opérateur valide" | log full + screenshot | +| **Windows Hello** | "windows hello", "code pin", "touchez le capteur" | **ask_human** — interaction physique requise par construction | pause + tip pré-démo : "désactiver Hello pour la session" (paramètres Windows) | log | +| **Defender SmartScreen** | "windows a protégé votre pc", "smartscreen" | **escalate_security + ask_human** | pause + log security ; rappel `project_code_signing.md` (signature SHA256) | log full | +| **Permission navigateur** (cam/mic/notif/geoloc) | "souhaite utiliser votre", "autoriser / bloquer" | **declarative** si déclaré dans workflow ; sinon **ask_human** | catalog match → click `Autoriser` ; sinon pause | log + screenshot | +| **Métier sauvegarde** ("Voulez-vous enregistrer ?", "Enregistrer sous") | `KNOWN_DIALOGS` existant | **declarative** | InfiGUI click `Enregistrer` (catalog priority basse — fenêtre parent) | log standard | +| **Métier confirmation** ("Voulez-vous remplacer ?", "Existe déjà", "Écraser") | `KNOWN_DIALOGS` priorité HAUTE | **declarative** | InfiGUI click `Oui` ; fallback OCR direct (code actuel) | log standard | +| **OK trivial** (erreur app, info) | 1 seul bouton détecté, mots-clés "erreur/error/warning" ; pas de mot blocklist | **auto_dismiss** | click `OK` | log standard | +| **OK trivial SUSPECT** (mots-clés "supprimé", "perdu", "irréversible") | blocklist regex | **ask_human** | pause | log full | +| **INCONNU** | aucun match | **ask_human** par défaut (pas d'auto-dismiss aveugle) | pause + capture VLM pour catégorisation a posteriori → enrichit catalogue | log full | + +**Garde-fou healthtech** : tout dialog non métier listé dans `KNOWN_DIALOGS` ou dans le workflow déclaratif **escalade en pause supervisée** par défaut. C'est cohérent avec `feedback_failure_is_learning.md` (échec = pause, pas stop) et le constat OpenAI/Anthropic 2026 (login/sécurité → cède la main à l'humain). + +--- + +## 6. Détection rapide d'apparition de modal + +Trois pistes, à composer plutôt qu'à choisir : + +### 6.1. Foreground window change (Windows API) + +- **Coût** : ~1 ms. +- **Fiabilité** : faible seul (`feedback_popup_vlm.md` 27 mars 2026 : popups Windows modernes partagent hwnd du parent, retourne 0 en SSH). +- **Usage** : **signal complémentaire** dans `ChangeSignal`, jamais source unique. + +```python +import ctypes +hwnd = ctypes.windll.user32.GetForegroundWindow() +title_buf = ctypes.create_unicode_buffer(256) +ctypes.windll.user32.GetWindowTextW(hwnd, title_buf, 256) +# Comparer hwnd_n vs hwnd_n-1 ; titre dans title_buf.value +``` + +### 6.2. Screenshot diff zone centrale vs périphérie + +- **Coût** : ~10 ms (numpy `abs(prev - curr) > seuil`, downscale 1/4). +- **Fiabilité** : bonne pour modaux centrés (UAC, dialog métier, Hello). Faible pour toasts en coin. +- **Heuristique** : `central_diff_ratio > 0.10` **ET** `diff_ratio < 0.40` → modal centré probable (le centre change beaucoup, le reste peu). +- **Pattern visuel auxiliaire** : zone "assombrie" en bordure (`secure desktop` UAC = pixels < 50 en luminance sur > 60% de l'écran). + +### 6.3. PopSweeper-style classifier (option future, si latence VLM trop forte) + +- ResNet50 + MobileNetV2 deux étages → 60 ms/frame, 91.7% précision sur RICO (mobile). +- À envisager **uniquement** si la détection diff+heuristique laisse passer trop de cas. Pour l'instant, surcoût d'un modèle dédié non justifié — l'OCR titre + signatures couvre 80%. + +### 6.4. Pas de pHash global pour détection modal + +`feedback_phash_vs_dialog_in_vm.md` est explicite : **pHash global est inadapté** à la cascade de modaux en VM. Le pHash compare des images entières, masquant les changements locaux qui sont précisément l'indice d'un modal. Utiliser screenshot diff zoné OU OCR titre — pas pHash global. + +--- + +## 7. Activation de `_handle_possible_popup` orphelin + +### 7.1. État actuel + +- **Côté client (Léa Windows)** : `_handle_possible_popup` défini, **0 site d'appel** (`LESSONS_LEARNED_GHT_2026-05.md`, F5.5.1). Un `_handle_popup_vlm` existe en parallèle (le "remplacement" mentionné dans l'audit) mais sans déclencheur générique. +- **Côté serveur** : `core/grounding/dialog_handler.py` (étudié pour ce doc) — handler complet, ne se déclenche que si on l'appelle explicitement. +- **Constat memoire 27 mars 2026** : `qwen3-vl:8b` détecte popup en 3.6 s avec coordonnées précises depuis le client (appel direct LAN port 11434), pas via le serveur. Donc le VLM-post-clic est **techniquement validé**. + +### 7.2. Câblage proposé (sans modifier le code dans ce doc) + +Le pattern à brancher dans `agent_v1/core/executor.py` ressemble à : + +``` +def perform_click(self, x, y, ...): + screenshot_before = grab() + do_click(x, y) + time.sleep(short_delay) + screenshot_after = grab() + + signal = ChangeDetector().detect(screenshot_after) + if signal.is_modal: + ocr_text = read_ocr(screenshot_after) + dtype = DialogClassifier().classify(screenshot_after, ocr_text) + event = DialogResolver(policy).resolve(dtype, screenshot_after, workflow_ctx) + report_to_server(event) + if event.policy_applied == "ask_human": + return WAIT_HUMAN + return OK +``` + +**Sites d'appel à brancher** (à valider avec Dom avant tout commit) : +1. Après chaque `_replay_action` côté client (suit la mémoire 27 mars). +2. Avant chaque vérification de Validator post-action (cohérence AXE_B2). +3. Au démarrage du tick d'observation `observe_reason_act` côté serveur (capture un modal apparu pendant un wait long). + +### 7.3. Décision sur `_handle_popup_vlm` vs `_handle_possible_popup` + +À trancher avec Dom : garder UN seul handler (probablement `_handle_popup_vlm` qui appelle l'extension proposée ici), supprimer l'orphelin. Sinon dette technique persistante (DETTE-XXX à créer si décision prise). + +--- + +## 8. Anti-patterns à proscrire + +### 8.1. Hardcoder un raccourci système "fix" + +`feedback_100pct_visual.md` est sans appel : **JAMAIS** : +- `keyboard.press_and_release('escape')` pour "fermer un popup". +- `keyboard.press_and_release('win+r')` pour "ouvrir un truc rapidement". +- `keyboard.press_and_release('ctrl+x')` pour "annuler". + +Raisons : +- Casse le récit "Léa comprend visuellement". Démontage immédiat face à un DSI healthtech. +- Échappe à la cascade de validation (OCR → template → VLM). +- Effets de bord imprévisibles : Échap dans un formulaire peut purger des données saisies ; Win+R sur un secure desktop ne fait rien et perd l'état. + +**Exception unique** : `gesture_catalog.py` autorise les réflexes système **explicitement référencés** (voir `feedback_lea_reflexes_catalog.md`). Mais c'est une **composition** orchestrée, pas un "fix popup ad hoc". + +### 8.2. Auto-accept système (UAC, Hello, SmartScreen) + +Interdit en healthtech. Toujours pause supervisée. Cohérent avec FDA / RGPD / HDS — un agent qui élève des privilèges seul est un risque inacceptable. + +### 8.3. pHash global pour détecter un modal + +Voir §6.4. Utiliser screenshot diff zoné + OCR titre. + +### 8.4. Polling VLM principal à chaque tick + +Latence Qwen2.5-VL = 8-11 s par appel (synthèse `MIGRATION_VLM_PLAN_2026-05-09.md`). Détection modal doit rester < 100 ms — sinon on bloque la boucle replay. Le VLM est appelé **uniquement** quand le ChangeDetector signale `is_modal=True` ET que le catalogue texte n'a pas matché. + +### 8.5. Tenter de "désactiver" Windows Hello programmatiquement + +Solution = config humaine pré-démo, pas action runtime de l'agent. L'agent **détecte et escalade**, il ne modifie pas la config sécurité Windows. + +--- + +## 9. Liens forts avec les autres AXES et la dette projet + +- **AXE_A4 (OCR/Template/pHash)** : la **détection** modal réutilise screenshot diff zoné — surface à coordonner avec la décision pHash de A4 (pas pHash global ici). +- **AXE_A5 (Screen Tokenization)** : un parser d'écran qui produit la liste de regions interactives détecte aussi les "boutons de modaux" — la classification peut bénéficier de cette liste sans coût VLM supplémentaire. +- **AXE_B2 (Validator)** : dépendance forte. Un Validator strict (texte attendu présent dans la zone visée) **force** l'appel au DialogResolver quand le check post-action échoue. Sans ce couplage, un modal non vu reste un échec silencieux (cf. bug step 10 démo 8 mai : Imagerie cliqué dans bandeau Edge, REPORT success=True). +- **AXE_A3 (Bench Protocol)** : ajouter un harness "modal injection" — pendant un replay test, injecter UAC simulé / popup métier / SmartScreen factice, mesurer detect→classify→resolve latency et taux pause vs auto-dismiss. +- **DETTE existante** : DETTE-008 (`if False:` pré-check VLM par-clic, `observe_reason_act.py:1704-1713`) sera **résolue** par le ChangeDetector léger — on n'a plus besoin d'un VLM par clic, le détecteur fait le filtrage en amont. +- **`feedback_capture_purge_policy.md`** : screenshots de DialogEvent à conserver pour audit (RGPD / HDS) — politique de rétention à valider avec Dom. + +--- + +## 10. Sources (liens cliquables) + +### Frameworks comparés (publications, blogs, papers) + +- [Anthropic Computer Use tool — docs API](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/computer-use-tool) +- [Anthropic Claude Desktop browser permissions controversy (avril 2026)](https://www.sovereignmagazine.com/article/anthropic-claude-desktop-browser-permissions) +- [Anthropic — Auto Mode permission classifier (mars 2026)](https://medium.com/@joe.njenga/anthropic-adds-new-claude-code-auto-mode-no-more-permission-modes-52c8094ab742) +- [OpenAI Operator System Card](https://openai.com/index/operator-system-card/) +- [OpenAI ChatGPT Agent System Card — juillet 2025](https://cdn.openai.com/pdf/839e66fc-602c-48bf-81d3-b21eacc3459d/chatgpt_agent_system_card.pdf) +- [ChatGPT Agent — login takeover + screenshot OFF](https://help.openai.com/en/articles/11752874-chatgpt-agent) +- [Skyvern — AI RPA Guide (oct. 2025)](https://www.skyvern.com/blog/ai-rpa-guide-intelligent-browser-automation/) +- [Skyvern — API-less Legacy System Automation (mai 2026)](https://www.skyvern.com/blog/api-less-system-automation-tools-legacy-enterprise/) +- [Skyvern GitHub — Planner-Actor-Validator](https://github.com/Skyvern-AI/skyvern) +- [browser-use — Issue #1996 : Need Robust Strategy for Handling Dynamic Popups (ouvert mai 2026)](https://github.com/browser-use/browser-use/issues/1996) +- [OmniParser V2 — Microsoft Research](https://microsoft.github.io/OmniParser/) +- [OmniParser arXiv 2408.00203](https://arxiv.org/abs/2408.00203) +- [UI-TARS arXiv 2501.12326](https://arxiv.org/abs/2501.12326) + +### Détection popup, classifiers légers + +- [PopSweeper — arXiv 2412.02933 (déc. 2024)](https://arxiv.org/abs/2412.02933) +- [PopSweeper — HTML lecture directe](https://arxiv.org/html/2412.02933v1) +- [ShowUI — arXiv 2411.17465 (2B GUI grounding)](https://arxiv.org/pdf/2411.17465) +- [ZonUI-3B cross-resolution GUI grounding](https://arxiv.org/pdf/2506.23491) + +### Sécurité Windows 11 / browser + +- [Microsoft KB UAC fixes oct. 2025 → janv. 2026 — Microsoft Q&A](https://learn.microsoft.com/en-nz/answers/questions/5733506/windows-uac-prompt-becomes-unresponsive-after-cred) +- [NinjaOne — Change UAC Behavior Windows 11](https://www.ninjaone.com/blog/change-uac-behavior-for-administrators-in-windows-11/) +- [CVE-2026-0628 Chrome Gemini Live panel takeover](https://news.corksafetyalerts.com/chrome-flaw-allowed-extensions-to-hijack-googles-ai-assistant-camera-and-microphone/) +- [AI-powered phishing leveraging camera/mic permissions (2026)](https://www.scworld.com/brief/ai-powered-phishing-campaign-leverages-hardware-access-for-data-theft) + +### Safety gates / human-in-the-loop healthcare + +- [Agentic Workflow Approval Gate Framework (4 gate types)](https://www.digitalapplied.com/blog/agentic-workflow-approval-gate-framework-governance) +- [AI Agents for Healthcare — Architecture and Safety Guide (Momentum)](https://www.themomentum.ai/blog/ai-agents-healthcare-architecture-safety-implementation) +- [Human-in-the-Loop Agentic AI (Elementum)](https://www.elementum.ai/blog/human-in-the-loop-agentic-ai) + +### Références internes (à charger en parallèle de ce doc) + +- `docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` (synthèse maîtresse) +- `docs/LESSONS_LEARNED_GHT_2026-05.md` (zones popup F5.5.1, F6.1.1, DETTE-008) +- `core/grounding/dialog_handler.py` (commit `487bcb861`) +- `memory/feedback_popup_vlm.md` (VLM post-clic, pas ctypes seul) +- `memory/feedback_lea_reflexes_catalog.md` (gesture_catalog autorisé, pas hardcode ad hoc) +- `memory/feedback_phash_vs_dialog_in_vm.md` (pas pHash global) +- `memory/feedback_100pct_visual.md` (jamais raccourci inventé) +- `memory/feedback_auth_dialogs_runtime.md` (anticiper Hello/UAC/Basic Auth AVANT démo) + +--- + +## 11. Hors-périmètre de ce doc + +À demander à Dom si besoin avant action : + +- Décision finale sur unification `_handle_possible_popup` orphelin vs `_handle_popup_vlm` (les deux côté client). +- Politique de rétention RGPD/HDS des screenshots `DialogEvent` (par défaut `data/runner_captures/dialogs/` purge ACK serveur). +- Choix exact du modèle VLM compact pour la classification fallback (qwen3-vl:8b acceptable mais ignore parfois `format=json` Ollama — voir §2.4 synthèse). +- Bench de la latence `ChangeDetector` sur capture réelle 2560×1600 (cible < 50 ms à vérifier empiriquement). +- Politique sur la suppression auto de toasts (Teams, Outlook) pendant démo : déclaratif "do not disturb" en amont vs détection runtime. + +--- + +*Document de recherche. Lecture seule. Toute mise en code = décision explicite Dom puis chirurgie itérative supervisée (CLAUDE.md projet).* diff --git a/docs/recherche/AXE_D4_MULTI_TENANT_DEPLOY.md b/docs/recherche/AXE_D4_MULTI_TENANT_DEPLOY.md new file mode 100644 index 000000000..17ec5b7db --- /dev/null +++ b/docs/recherche/AXE_D4_MULTI_TENANT_DEPLOY.md @@ -0,0 +1,549 @@ +# AXE D4 — Patterns de déploiement multi-tenant / multi-user d'un agent RPA Windows on-premise (2026) + +**Date :** 2026-05-23 +**Auteur :** Claude (agent de recherche, dispatché) +**Périmètre :** packaging, code signing, multi-tenant, silent install, auto-update, observabilité, comparaison concurrents, plan en 3 paliers (POC 2 → 10 → 100+ postes). +**Statut :** recherche en lecture seule, aucune modification de code. Sources cliquables en §10. + +--- + +## 1. TL;DR — Recommandations immédiates + +**Packaging exe Windows** : **Nuitka commercial standalone** pour Léa client (Agent V1). PyInstaller `--onefile` est à proscrire en milieu hospitalier : 100% des sources convergent sur le taux énorme de faux positifs antivirus, car le pattern self-extracting est indistinguable de malwares connus. Nuitka compile vers du C natif et obfusque le code Python — double bénéfice : moins de faux positifs ET protection IP modérée. Coût : temps de build x10–100, à intégrer dans la CI. + +**Code signing** : pour Phase 1 (POC AIVANOV + GHT démo) **rester en non signé** (statu quo). Pour Phase 2 (Anouste + 10 postes), **Azure Trusted Signing** = bloqué car réservé US/Canada + 3 ans d'ancienneté (pas applicable à un éditeur français). **Acheter directement un certificat OV Sectigo** chez un revendeur (CheapSSLSecurity ~120-200 €/an OV, ou 280-500 €/an EV via token HSM). L'EV n'apporte plus le bypass SmartScreen automatique depuis 2023 — l'OV suffit pour la confiance DSI, l'EV n'est utile que pour Windows Defender Application Control et certains pilotes signés. Pour la Phase 3 (>100 postes), envisager SignPath.io comme orchestrateur de signature (politiques + audit + intégration CI). + +**Multi-tenant 3 paliers** : +1. **POC 2 postes** (actuel) : `lea / Medecin2026!` HTTP Basic statique, machine_id = UUID WMI `wmic csproduct get uuid`. Ne pas durcir. +2. **10 postes Anouste** : table `users` + `client_machines` + tokens API par machine (Fernet, rotation manuelle), JWT pour le dashboard, RBAC 3 rôles (admin / superviseur / viewer). Logs par tenant via préfixe fichier. +3. **100+ postes GHT/ARS** : Postgres + Row-Level Security ou schéma par tenant, refresh tokens avec rotation auto, observabilité Loki/Grafana avec header `X-Scope-OrgID`, SCIM pour AD/Entra, audit trail signé. + +**Dépendances explicites** : +- AXE_B1 (transport HTTP → SSE/WebSocket) : tout watchdog et révocation de token dépendent de la couche transport — un client en long-poll qui rate l'ACK ne peut pas être proprement révoqué. +- AXE_B5_D1 (capture distante NoMachine/AnyDesk) : la confusion "agent Léa" vs "outil de prise en main" doit rester explicite dans le packaging (nom binaire `lea-agent.exe`, jamais `rpa-controller.exe` ou autre nom qui prête à confusion DSI). + +--- + +## 2. Packaging Python → exe Windows (2026) + +### 2.1. Table comparative + +| Outil | Maturité 2026 | Taille .exe Léa (estim.) | Démarrage à froid | Faux positifs AV | Build time | Recommandation | +|---|---|---:|---|---|---|---| +| **PyInstaller** `--onefile` | très mature | 80-150 Mo (PyQt5+mss+pynput) | 3-8 s (extraction temp) | **élevé** (heuristique self-extract) | rapide (< 1 min) | À éviter | +| **PyInstaller** `--onedir` | très mature | 200 Mo dossier | < 1 s | moyen | rapide | Acceptable si MSI | +| **Nuitka standalone** | mature 2026 | 100-180 Mo | < 1 s (binaire natif) | **faible** (binaire C) | lent (10-30 min) | **Recommandé** | +| **Nuitka onefile** | mature 2026 | 100-150 Mo | 2-4 s | faible | lent | OK pour distrib | +| **Briefcase BeeWare** | mature (WiX 5.0.2) | 80-120 Mo MSI natif | < 1 s | moyen | moyen | Très bon pour MSI/AD | +| **cx_Freeze** | maintenu, marginal | 100 Mo | < 1 s | moyen | moyen | Pas de raison de le choisir | +| **PyOxidizer** | **abandonné** (dernier commit jan 2023) | — | — | — | — | Ne pas adopter | + +### 2.2. Recommandation détaillée pour Léa + +Léa Agent V1 = client léger Python (capture mss, pynput, requests/websocket, PyQt5 system tray). Pas de Transformers ni torch côté client (resté sur le serveur GPU). + +**Choix recommandé : Nuitka commercial standalone, packagé dans un MSI WiX**. + +Pourquoi cette combinaison : +- **Nuitka** élimine le pattern self-extract qui fait que `PyInstaller --onefile` est régulièrement signalé par Windows Defender / Kaspersky / Trend Micro en milieu hospitalier. C'est confirmé par plusieurs retours 2025-2026 (cf. sources). Compilation en C natif → l'exécutable ressemble à un binaire C/Rust normal. +- **MSI WiX via Briefcase OU MSI WiX maison autour du build Nuitka** : MSI est le format attendu par les DSI hospitalières pour déploiement GPO/SCCM/Intune. Évite la friction "exécutable inconnu" et permet l'installation silencieuse `msiexec /i lea-agent.msi /qn ALLUSERS=1`. + +**Variante de transition** : si l'effort Nuitka est trop lourd à mettre en CI dans le mois qui vient pour Anouste, accepter `PyInstaller --onedir` (dossier, pas onefile) packagé dans un MSI WiX. Le `--onedir` a des taux de faux positifs **nettement inférieurs** au `--onefile` car il n'y a pas de bootloader d'extraction. + +**Liste de paquets exclus du bundle Léa** (à expliciter dans le `.spec` Nuitka/PyInstaller pour réduire la taille) : +- `torch`, `transformers`, `triton`, `nvidia-*` (côté serveur uniquement) +- `faiss`, `sentence-transformers` (côté serveur) +- `docTR`, `easyocr` (côté serveur — sauf si Léa fait OCR local prévu en P3) +- modules `tests`, `pytest`, `notebook`, `jupyter*` + +### 2.3. Tests à mener avant de figer le choix + +- Build Léa minimal (capture + envoi HTTP) en PyInstaller `--onedir`, PyInstaller `--onefile`, Nuitka standalone, Nuitka onefile. +- Soumission à VirusTotal de chaque artefact (les 4 binaires). Cible : 0/72 sur Nuitka, <5/72 sur PyInstaller onedir, échec confirmé sur PyInstaller onefile. +- Mesure démarrage à froid sur un poste Windows 10 (Pauline) et Windows 11 (TIM Anoust). +- Mesure RAM résidente. + +--- + +## 3. Code signing 2026 + +### 3.1. Table comparative des CA et services + +| Solution | Type | Prix annuel (HT) | Bypass SmartScreen | KYC France | Verdict pour Léa | +|---|---|---:|---|---|---| +| **Pas de signature** | — | 0 € | Non, "Plus d'infos → Exécuter quand même" | — | OK Phase 1 | +| **Azure Trusted Signing** (ex Azure Artifact Signing) | OV-like (non EV) | ~120 € (10 €/mois Basic) | Réputation progressive, pas immédiat | **Bloqué** : US/Canada uniquement, +3 ans d'ancienneté requis | **NON applicable** | +| **Sectigo OV** (via revendeur SignMyCode/CheapSSL) | OV | 120-200 € | Réputation progressive | OK (KGI standard) | **Bon choix Phase 2** | +| **Sectigo EV** (token HSM ou eToken) | EV | 280-500 € | Plus de bypass auto depuis 2023, mais meilleure réputation initiale | OK + KYC renforcé (Dun & Bradstreet, registre commerce) | Si DSI exige EV | +| **DigiCert EV** | EV | 500-700 € | Idem Sectigo EV | OK + KYC renforcé | Plus cher, image premium | +| **SignPath.io** | Orchestrateur (utilise CA tierce) | dès ~50 €/mois (Foundation gratuit pour open-source) | dépend du certificat sous-jacent | Indirect via CA | Pertinent Phase 3 (audit + CI/CD + politiques signature) | +| **Whitelist SHA256 DSI** | hors CA | 0 € | DSI pousse le hash via GPO Defender ASR | Discussion contractuelle | Plan A documenté (Anoust) | + +### 3.2. Changements 2026 à intégrer + +- **CA/B Forum 15 février 2026** : les certificats de signature de code à 2 et 3 ans sont désormais réservés à l'option "Install on Existing HSM" (le client a déjà son HSM/yubikey). L'option Token + Shipping est limitée à 1 an. Pour Léa, prévoir un renouvellement annuel ou un investissement HSM (YubiKey 5C FIPS ~95 €). +- **EV ne bypasse plus SmartScreen automatiquement** depuis 2023. Le différentiel EV vs OV ne se justifie que pour : pilotes signés, Windows Defender Application Control en mode allow-listing certaines DSI, ou imagerie pro. +- **Azure Trusted Signing** : a beaucoup bougé 2025-2026 mais **reste fermé aux entreprises françaises** pendant toute la durée du POC. À surveiller mais pas planifier dessus. + +### 3.3. Plan d'acquisition recommandé pour un éditeur santé français + +**Phase 1 (maintenant, GHT démo + AIVANOV)** : aucune signature. Documenter dans `deploy/installer/README.md` la procédure "Plus d'infos → Exécuter quand même" + capture d'écran. + +**Phase 2 (Anouste, 10 postes)** : +1. Acheter **Sectigo OV Code Signing 1 an** via CheapSSLSecurity ou SignMyCode (~130-180 € HT). Validation : 3-7 jours ouvrés avec extrait Kbis + facture pro + appel téléphonique de vérification. +2. En parallèle, ouvrir la négociation avec Fabrice DUPOUY (DSI Anoust) sur la **whitelist SHA256** dans Defender + leur antivirus principal. Combiner : DSI whitelist + signature OV = sécurité maximale + 0 friction utilisateur final. +3. Documenter le hash SHA256 du MSI dans la docstring du release tag git. + +**Phase 3 (GHT/ARS, 100+ postes)** : +1. Passer à **Sectigo EV Code Signing avec HSM** (YubiKey FIPS) ou **DigiCert EV** si DSI hospitalier exige une CA "tier 1 reconnue". +2. Intégrer **SignPath.io** dans la CI GitHub Actions / Gitea Actions : politique de signature (qui peut signer, depuis quelle branche, quel commit signataire), logs d'audit consultables, intégration avec le HSM cloud. + +### 3.4. Recommandation transverse + +Ne pas attendre Phase 3 pour signer. Une signature OV obtenue dès Phase 2 commence à accumuler de la réputation SmartScreen / Defender (le binaire devient connu). Plus tôt on signe, plus tôt la friction disparaît. + +--- + +## 4. Multi-tenant — modèle de données, tokens, machine_id, auth + +### 4.1. Modèle de données recommandé (Phase 2-3) + +``` +tenants (id PK, name, contact_email, status, hds_certified BOOL, created_at) +users (id PK, tenant_id FK, username, password_hash BCRYPT, role ENUM admin|superviseur|viewer, totp_secret NULL, last_login, created_at) +client_machines (id PK, tenant_id FK, machine_id UNIQUE, hostname, os_version, user_id FK NULLABLE, status ENUM active|revoked|paused, api_token_hash, token_expires_at, last_seen) +api_tokens_audit (id PK, machine_id FK, action ENUM created|rotated|revoked, actor_user_id FK, timestamp, reason TEXT) +workflows_per_tenant (id PK, tenant_id FK, workflow_name, version) +audit_log (id PK, tenant_id FK, machine_id FK NULLABLE, user_id FK NULLABLE, action, payload_json, timestamp) +``` + +Trois patterns d'isolation possibles, par ordre croissant d'isolation et de complexité : +- **Shared DB + tenant_id column** : 1 base, 1 schéma, colonne `tenant_id` partout + RLS Postgres. **Recommandé Phase 2-3**. +- **Shared DB + schema per tenant** : 1 base, N schémas. Plus d'isolation mais migrations N fois. +- **DB per tenant** : isolation maximale, coût opérationnel élevé. À garder pour clients exigeants (ARS, ministériel). + +Phase 2 (SQLite actuel) : viable si moins de 50 postes total, mais migration Postgres dès qu'on dépasse 5 tenants. + +### 4.2. Génération de tokens API par machine + +**Pattern recommandé** : token aléatoire 32 bytes URL-safe, stocké hashé (Argon2id ou SHA-256+sel) en base, présenté à chaque requête en header `Authorization: Bearer `. Le token contient en clair (avant hash) un préfixe identifiant : `lea___` pour faciliter le debug logs sans révéler le secret. + +Snippet de référence (Python serveur) : + +```python +import secrets +import hashlib +from datetime import datetime, timedelta + +def generate_machine_token(tenant_id: str, machine_id: str) -> tuple[str, str]: + """Retourne (token_clair, token_hash). Stocker uniquement le hash.""" + random_part = secrets.token_urlsafe(32) + token = f"lea_{tenant_id}_{machine_id[:8]}_{random_part}" + token_hash = hashlib.sha256(token.encode()).hexdigest() + return token, token_hash + +def verify_token(token_clair: str, stored_hash: str) -> bool: + return hashlib.sha256(token_clair.encode()).hexdigest() == stored_hash +``` + +**Rotation** : tokens long-lived (90 jours) côté machine, refresh manuel depuis dashboard admin. Pour Phase 3 : pattern OAuth2 client credentials + refresh tokens avec rotation à chaque usage (TOTP-like). + +**Révocation** : flag `status='revoked'` en base + cache court (60s) côté serveur. Avec le transport HTTP pull/long-poll actuel, la révocation prend effet au prochain poll (5-30s). **Avec SSE/WebSocket (AXE_B1)**, le serveur peut fermer la connexion immédiatement à la révocation. + +### 4.3. machine_id unique Windows + +Une seule source ne suffit pas (UUID virtualisé, MAC qui change, hostname dupliqué chez les vraies clinique). Recommandation : **identifiant composite stable**. + +```python +import uuid +import subprocess +import socket +import hashlib + +def get_machine_id_composite() -> str: + """ID composite stable sur Windows, fallback Linux/macOS.""" + components = [] + + # 1. WMI UUID (Win32_ComputerSystemProduct) — stable même après reset partiel + try: + out = subprocess.check_output( + ["wmic", "csproduct", "get", "uuid"], + timeout=5, stderr=subprocess.DEVNULL + ).decode().strip().split("\n") + if len(out) > 1: + wmi_uuid = out[1].strip() + if wmi_uuid and "FFFFFFFF" not in wmi_uuid: + components.append(wmi_uuid) + except Exception: + pass + + # 2. MAC address de la première interface active (uuid.getnode) + mac = uuid.getnode() + if (mac >> 40) % 2 == 0: # éviter les MAC aléatoires (bit local) + components.append(str(mac)) + + # 3. Hostname (NetBIOS / DNS) + components.append(socket.gethostname()) + + if not components: + # Fallback : MachineGuid registre Windows + try: + out = subprocess.check_output( + ["reg", "query", r"HKLM\SOFTWARE\Microsoft\Cryptography", "/v", "MachineGuid"], + timeout=5 + ).decode() + for line in out.split("\n"): + if "MachineGuid" in line: + components.append(line.split()[-1]) + except Exception: + components.append(str(uuid.uuid1())) # ultime fallback + + raw = "|".join(components) + return hashlib.sha256(raw.encode()).hexdigest()[:32] +``` + +**Important** : ne JAMAIS cloner une VM avec Léa installée sans regenerer le `machine_id` (cf. doc Power Automate Desktop : "if you reset your PC, your machine registration will be lost"). Prévoir un script `python -m lea_agent.reset_machine_id` documenté. + +### 4.4. Pattern auth bouchon → cible + +**État actuel (bouchon, Phase 1)** : `DASHBOARD_USER=lea`, `DASHBOARD_PASSWORD=Medecin2026!` en HTTP Basic. Un token global Bearer dans `.env.local` côté NPM reverse proxy pour `lea.labs.laurinebazin.design`. + +**Cible Phase 2** (3 jours d'effort) : +1. Migration SQLite → table `users` + `client_machines`. +2. `flask-login` ou `fastapi-users` pour sessions dashboard (cookies signés). +3. Endpoint admin `POST /api/v1/admin/machines` qui génère un token, retourne le clair UNE seule fois, stocke le hash. +4. Endpoint client `GET /api/v1/replay/next` qui vérifie `Authorization: Bearer ...` et update `last_seen`. +5. Page dashboard `/admin/machines` : liste, révoquer, rotation, télécharger ZIP installer pré-configuré avec le token. + +**Cible Phase 3** (1 semaine d'effort) : +1. Postgres + RLS, JWT avec refresh tokens (durée courte access 15 min, refresh 7 jours). +2. SCIM pour synchroniser AD/Entra (utilisateurs hospitaliers). +3. TOTP RFC 6238 pour les admins (déjà câblé dans `core/auth/`, à exposer). +4. Audit trail signé (chaîne de hash) pour conformité HDS V2. + +--- + +## 5. Déploiement silent install + auto-update + +### 5.1. Silent install Windows entreprise + +**MSI > exe self-extracting** en hôpital. Les DSI hospitalières utilisent Intune, SCCM ou GPO, qui sont câblées sur MSI. + +**Commande de référence** (à documenter dans le dossier DSI Anoust) : +``` +msiexec /i Lea-Agent-v1.0.0.msi /qn /norestart ALLUSERS=1 \ + LEA_SERVER_URL=https://lea.anoust.fr \ + LEA_MACHINE_TOKEN=lea_anoust_a3b9c2_xxxxxxxxx \ + INSTALLDIR="C:\Program Files\Lea Agent" \ + /l*v "C:\Windows\Temp\lea-install.log" +``` + +**Patterns DSI hospitalier** : +- Déploiement par OU (Organisational Unit) AD pour cibler "Postes TIM" ou "Postes Médecins urgences". +- Per-machine install (`ALLUSERS=1`) car les RPA tournent sous SYSTEM ou compte de service. +- Pré-provisionning du token à l'installation : éviter le pattern "lance Léa, copie-colle un token". À la place, le ZIP téléchargé depuis le dashboard contient un MSI avec le token déjà intégré (custom MSI per machine). + +**Si MSI trop complexe en Phase 2** : `Inno Setup` (gratuit, plus simple que WiX) avec `/VERYSILENT /SUPPRESSMSGBOXES` accepte les déploiements GPO/Intune en mode "Win32 app" Intune. Briefcase BeeWare expose WiX 5.0.2 sans complexité, c'est probablement la voie la plus simple si on reste Python-first. + +### 5.2. Auto-update — recommandation + +**Pour Phase 2 (10 postes Anouste)** : pas d'auto-update automatique. Procédure manuelle : pousser un nouveau MSI via SCCM/Intune ou via un script PowerShell que la DSI exécute. Notification dans le tray Léa "Une nouvelle version est disponible, contactez votre DSI". + +**Pour Phase 3 (100+ postes)** : framework d'auto-update. + +| Framework | Pertinence Léa | Verdict | +|---|---|---| +| **Velopack** | Réécriture moderne de Squirrel.Windows en Rust, delta updates, multi-langage, multi-plateforme. Actif 2025-2026. | **Recommandé** | +| **Squirrel.Windows** | Historique .NET, maintenu mais Velopack est le successeur. | À éviter (legacy) | +| **tufup** | Successeur Python de PyUpdater, basé sur python-tuf (sécurisé par design), indépendant du packaging. | **Très pertinent** si on reste Python natif et qu'on ne passe pas par Velopack. | +| **PyUpdater** | **Archivé**, ne pas utiliser. | À proscrire | + +**Recommandation finale** : **tufup**. Avantages pour Léa : +- Indépendant du packaging (compatible Nuitka et PyInstaller), +- Sécurité basée sur TUF (The Update Framework) — signature des manifests, protection contre rollback attacks, +- Maintenance active 2025-2026, +- Manifest hosting peut être le serveur Linux on-premise existant. + +**Pattern d'update** : side-by-side (installer dans `C:\Program Files\Lea Agent\v1.0.1\` à côté de `v1.0.0\`), bascule du raccourci système au prochain redémarrage. Rollback = pointer le raccourci vers la version précédente, qui reste sur disque pendant N versions (paramétrable). Avantage : zéro corruption sur update interrompue, rollback en < 5 minutes. + +**Audit trail update** : chaque agent log dans `audit_log` côté serveur le succès/échec de l'update, version source, version cible, durée. Permet d'alerter si > 5% de la flotte est sur une ancienne version. + +--- + +## 6. Stack serveur multi-tenant on-premise + +### 6.1. docker-compose multi-tenant (squelette de référence) + +Pour Phase 3 (le stack actuel `svc.sh` reste valable Phase 1-2) : + +```yaml +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: lea_main + POSTGRES_USER: lea_admin + POSTGRES_PASSWORD_FILE: /run/secrets/pg_password + volumes: + - pg_data:/var/lib/postgresql/data + networks: [lea_internal] + + api_stream: + image: rpa-vision/api-stream:1.0.0 + environment: + DATABASE_URL: postgresql://lea_admin@postgres/lea_main + TENANT_RESOLUTION_HEADER: X-Lea-Tenant-Id + networks: [lea_internal, lea_public] + depends_on: [postgres] + + ollama: + image: ollama/ollama:latest + deploy: + resources: + reservations: + devices: + - capabilities: [gpu] + volumes: + - ollama_models:/root/.ollama + networks: [lea_internal] + + loki: + image: grafana/loki:3.0.0 + command: -config.file=/etc/loki/loki-config.yaml + volumes: + - ./loki-config.yaml:/etc/loki/loki-config.yaml + - loki_data:/loki + networks: [lea_internal] + # auth_enabled: true dans loki-config.yaml + + promtail: + image: grafana/promtail:3.0.0 + volumes: + - /var/log:/var/log + - ./promtail.yaml:/etc/promtail/config.yaml + networks: [lea_internal] + + grafana: + image: grafana/grafana:11.0.0 + networks: [lea_internal, lea_public] + environment: + GF_AUTH_GENERIC_OAUTH_ENABLED: "true" + # SSO via Keycloak ou Entra +``` + +### 6.2. Observabilité par tenant + +**Logs (Loki)** : auth multi-tenant activé (`auth_enabled: true`), chaque pod/agent envoie `X-Scope-OrgID: `. Promtail config : +```yaml +clients: + - url: http://loki:3100/loki/api/v1/push + tenant_id: ${LEA_TENANT_ID} +``` +Côté Grafana : un datasource Loki par tenant, ou un seul datasource avec le tenant_id injecté par variable dashboard. + +**Métriques (Prometheus)** : labels `tenant_id="anoust"`, `machine_id="abc123..."`. Quotas par tenant via Cortex/Mimir si on monte en charge. + +**Audit trail RGPD/HDS** : tableau `audit_log` partitionné par tenant + signature SHA-256 chaînée (chaque ligne contient le hash de la précédente). Conservation : 3 ans en hot, 7 ans en cold (S3 ou disque externe), conforme HDS V2. + +### 6.3. Secrets management + +Phase 1-2 : `.env.local` + chmod 600. C'est OK tant qu'on est < 5 tenants. + +Phase 3 : **HashiCorp Vault** self-hosted (image officielle dispo) ou **age** + secrets chiffrés en git. Pour les tokens API machines, déjà couvert par le hash en base. Pour les mots de passe Citrix/Easily Assure, utiliser `core/auth/credential_vault.py` déjà câblé (Fernet+PBKDF2). + +### 6.4. HDS V2 — implications concrètes + +Si Léa stocke des screenshots patients côté serveur ET que ce serveur est on-premise hôpital, l'hôpital est l'hébergeur (HDS interne) et n'a pas besoin de certifier l'éditeur. Si le serveur est chez Dom / chez le client / dans un cloud, **certification HDS V2 obligatoire** (article L1111-8 CSP). Implication produit : prioriser le déploiement on-premise hôpital pour Phase 2-3, le SaaS multi-tenant centralisé est un sujet HDS-lourd (~30-60 k€ d'audit + 6 mois) à n'envisager qu'après stabilisation produit. + +--- + +## 7. Comparaison concurrents — multi-tenant et déploiement + +### 7.1. UiPath Robot + +Modèle : licence par runtime (Production Unattended), allocation par tenant (Orchestrator → Admin → Tenants → Edit license allocation). Le robot consomme des runtimes du pool tenant à la connexion, les libère à la déconnexion. **Limite** : robot licences cantonnées à UN tenant — pas de "robot multi-tenant". Pour un hôpital multi-établissements GHT, on créerait 1 tenant par établissement. + +Pattern à reprendre : pool de runtimes au niveau tenant, allocation dynamique. Pertinent pour Phase 3 GHT. + +### 7.2. Power Automate Desktop (Microsoft) + +Modèle : **silent registration** des machines via `Power-Automate-machine-runtime.exe /SILENT REGISTRATIONKEY=xxxx`. Service principal pour l'enrôlement bulk. Multi-session sur Windows Server (RDS) pour scaler à 10-20 bots par machine physique. **Limite critique** : si on clone une VM avec PAD installé, casse l'enrôlement. + +Pattern à reprendre : enrôlement silencieux avec clé pré-générée, **interdiction de cloner les VM** documentée. + +### 7.3. Automation Anywhere Bot Runner + +Modèle : Bot Runners = comptes utilisateurs taggés "runner license" dans Control Room. Déploiement via RDP-based : le Control Room ouvre une session RDP sur la machine cible et lance le bot. Device Pools = groupes logiques pour distribuer la charge. + +Pattern à reprendre : Device Pools pour parallélisation (intéressant pour un déploiement multi-postes TIM dans le même service). + +### 7.4. Skyvern (open source, le plus proche de nous) + +Modèle : docker-compose `postgres + skyvern (API+browser) + skyvern-ui`. Self-hosting complet possible. **Limite documentée** : pas de VNC multi-session en self-hosted, problème d'observabilité multi-bot. Pas de tenancy native dans la version open-source (à coder soi-même). + +Pattern à reprendre : isolation par container Docker pour chaque session active, pratique pour scaler horizontalement. Différentiel pour nous : Léa tourne sur Windows utilisateur, pas dans un container — donc le pattern Skyvern ne s'applique qu'au serveur. + +### 7.5. browser-use + +Modèle : Cloud Skyvern-like, self-hosting Docker en cours de maturation (issue #658 GitHub février 2025). Multi-tenant : non documenté officiellement, communauté demande. + +À surveiller mais pas d'inspiration directe en 2026. + +### 7.6. Stack hospitalière française (Dedalus, Maincare, Easily Assure) + +**Dedalus** et **Maincare** déploient leurs DPI via MSI signés + GPO. Auth utilisateur via SSO LDAP/Active Directory (Kerberos). Pas de modèle "agent par poste" car le DPI est web ou client-serveur — pas applicable directement. + +**Easily Assure** : client lourd Windows (.NET) + serveur central. Authentification par compte utilisateur (couplé AD). + +Implication pour Léa : nos clients DSI savent gérer GPO + MSI signé + AD SSO. **C'est le standard attendu**. Notre dispositif actuel (token Bearer envoyé manuellement) est en dessous des standards de la branche. + +--- + +## 8. Plan en 3 paliers + +### Palier 1 — POC 2 postes (actuel, mai 2026) + +**État** : 1 démo + 1 dev. `lea / Medecin2026!` HTTP Basic, MSI absent, `.exe` PyInstaller `--onefile` non signé, install manuel via AnyDesk. + +**Action** : ne rien casser. Documenter clairement le delta produit/cible. + +**Effort** : 0. + +### Palier 2 — 10 postes Anouste + 5 postes GHT pilote (T3 2026) + +**À livrer** : +1. **Packaging** : migration Nuitka standalone (commercial, ~250 €/an) ou PyInstaller `--onedir` packagé MSI Briefcase. Cible : binaire signé OV, démarrage < 2s, taille < 200 Mo. +2. **Code signing** : achat Sectigo OV 1 an (~150 €) + négociation whitelist SHA256 avec DSI Anoust en parallèle (filet de sécurité 0 €). +3. **Multi-tenant** : migration SQLite → tables `tenants`, `users`, `client_machines`, `api_tokens_audit`. `flask-login` pour dashboard. 3 rôles (admin / superviseur / viewer). +4. **machine_id composite** (snippet §4.3) intégré au démarrage agent. +5. **Token API par machine** généré depuis dashboard admin, MSI custom par machine. +6. **Logs par tenant** : préfixe fichier `logs///agent.log`. Rotation logrotate. +7. **Silent install MSI** documenté pour DSI Anoust. +8. **Audit trail** basique : table `audit_log` (qui a lancé quel replay sur quelle machine). +9. **Procédure update** manuelle documentée (DSI pousse le MSI). + +**Effort estimé** : 10-15 jours dev, 3-5 jours validation site Anoust. + +**Dépendance critique** : AXE_B1 transport. Tant qu'on est en HTTP long-poll, la révocation de token a une latence 5-30s. Acceptable Phase 2 mais à corriger Phase 3. + +### Palier 3 — 100+ postes GHT/ARS (T4 2026 - T2 2027) + +**À livrer** : +1. **Postgres + RLS** ou schéma par tenant (selon profil clients). +2. **JWT access + refresh tokens** avec rotation (15 min / 7 jours). +3. **SCIM** pour synchroniser AD/Entra hospitalier. +4. **TOTP** pour admins (déjà câblé dans `core/auth/`, à exposer). +5. **SignPath.io** pour orchestrer signature CI/CD + audit (~600 €/an). +6. **Sectigo EV** ou DigiCert EV avec HSM YubiKey (~400 €/an). +7. **tufup** pour auto-update side-by-side + rollback. +8. **Loki + Grafana** observabilité avec `X-Scope-OrgID`. +9. **Audit trail signé** chaîné SHA-256 pour HDS V2. +10. **Quotas par tenant** (nombre de replays/h, taille captures, etc.). +11. **Onboarding self-service** tenant via dashboard admin global (création tenant, premier admin, premier token). + +**Effort estimé** : 30-60 jours dev, 10-15 jours sécurité/audit. + +**Décision HDS** à trancher : on-premise hôpital systématique (pas de certification HDS éditeur requise) vs SaaS centralisé (HDS V2 obligatoire, 30-60 k€). + +--- + +## 9. Restitution finale (< 250 mots) + +**Packaging** : passer Léa de PyInstaller `--onefile` (faux positifs AV massifs en milieu hospitalier) à **Nuitka standalone** (compilation C native, faux positifs faibles) packagé dans un **MSI WiX via Briefcase** pour déploiement GPO/Intune/SCCM. Étape transitoire acceptable : PyInstaller `--onedir` dans MSI WiX. + +**Code signing — Phase 1** : rester non signé (statu quo). **Phase 2 Anouste** : achat **Sectigo OV 1 an ~150 € HT** + négociation whitelist SHA256 DSI en parallèle. **Phase 3** : Sectigo EV avec HSM YubiKey + orchestration SignPath.io (~600 €/an). Azure Trusted Signing **bloqué** car réservé US/Canada + 3 ans d'ancienneté — ne pas planifier dessus, contrairement à ce qui était espéré dans `project_code_signing.md`. + +**Multi-tenant 3 paliers** : +1. **POC 2 postes** : statu quo, machine_id = UUID WMI. +2. **10 postes** : tables `users`/`client_machines`, tokens API par machine, 3 rôles RBAC, `flask-login`, silent MSI, logs par tenant. +3. **100+ postes** : Postgres + RLS, JWT refresh, SCIM AD, TOTP admins, tufup auto-update, Loki/Grafana multi-tenant, audit trail signé HDS V2. + +**Dépendances** : +- **AXE_B1 (transport HTTP → SSE/WebSocket)** : prérequis pour révocation de token immédiate et fermeture de session propre. Avec HTTP long-poll, latence 5-30s sur révocation. +- **AXE_B5_D1 (capture distante)** : ne pas brouiller le nom du binaire (Léa ≠ outil de prise en main DSI). + +**HDS V2** : on-premise hôpital systématique en Phase 2-3 pour éviter le coût de certification HDS éditeur (30-60 k€, 6 mois). SaaS centralisé hors périmètre tant que produit non stabilisé. + +--- + +## 10. Sources + +### Packaging Python → exe + +- [From PyInstaller to Nuitka: Convert Python to EXE Without False Positives (DEV.to)](https://dev.to/weisshufer/from-pyinstaller-to-nuitka-convert-python-to-exe-without-false-positives-19jf) +- [How to Fix Antivirus False Positives with PyInstaller Executables (Python GUIs)](https://www.pythonguis.com/faq/problems-with-antivirus-software-and-pyinstaller/) +- [Compilation vs Bundling: The Real Differences Between Nuitka and PyInstaller (KRRT7)](https://krrt7.dev/en/blog/nuitka-vs-pyinstaller) +- [Best PyInstaller Alternatives 2026 (No AV Flags)](https://beatsyncpro.ai/alternative/pyinstaller.html) +- [2026 Showdown: PyInstaller vs cx_Freeze vs Nuitka For Python EXE Builds](https://ahmedsyntax.com/2026-comparison-pyinstaller-vs-cx-freeze-vs-nui/) +- [Nuitka Performance documentation](https://nuitka.net/user-documentation/performance.html) +- [Nuitka Compilation: C-Level Python Performance Boost 2026](https://www.johal.in/nuitka-compilation-c-level-python-performance-boost-2026/) +- [PyOxidizer has been abandoned (Anki Issue #3081)](https://github.com/ankitects/anki/issues/3081) +- [Briefcase Windows MSI documentation](https://briefcase.beeware.org/en/stable/reference/platforms/windows/) +- [GitHub - beeware/briefcase-windows-msi-template](https://github.com/beeware/briefcase-windows-msi-template) +- [PyInstaller AV false positive Issue #6754](https://github.com/pyinstaller/pyinstaller/issues/6754) + +### Code signing + +- [Azure Artifact Signing FAQ (Microsoft Learn)](https://learn.microsoft.com/en-us/azure/artifact-signing/faq) +- [Azure Artifact Signing Pricing](https://azure.microsoft.com/en-us/pricing/details/artifact-signing/) +- [Trusted Signing open for individual developers - Public Preview](https://techcommunity.microsoft.com/blog/microsoft-security-blog/trusted-signing-is-now-open-for-individual-developers-to-sign-up-in-public-previ/4273554) +- [Trusted Signing 3-year requirement Q&A](https://learn.microsoft.com/en-us/answers/questions/2261318/is-there-any-exception-process-for-the-azure-trust) +- [Fighting through Setting up Microsoft Trusted Signing (Rick Strahl)](https://weblog.west-wind.com/posts/2025/Jul/20/Fighting-through-Setting-up-Microsoft-Trusted-Signing) +- [Code signing on Windows with Azure Trusted Signing (Melatonin)](https://melatonin.dev/blog/code-signing-on-windows-with-azure-trusted-signing/) +- [Top 10 Best Code Signing Certificate Providers in 2026](https://sslinsights.com/best-code-signing-certificate-providers/) +- [Sectigo EV Code Signing Certificate ($279.99/yr SignMyCode)](https://signmycode.com/sectigo-ev-code-signing) +- [Sectigo EV Code Signing Certificates (TheSSLStore)](https://www.thesslstore.com/sectigo/sectigo-ev-code-signing-certificate.aspx) +- [SignPath Pricing](https://about.signpath.io/product/pricing) +- [SmartScreen reputation for Windows app developers (Microsoft Learn)](https://learn.microsoft.com/en-us/windows/apps/package-and-deploy/smartscreen-reputation) +- [Code signing options for Windows app developers (Microsoft Learn)](https://learn.microsoft.com/en-us/windows/apps/package-and-deploy/code-signing-options) +- [Automate PyInstaller Builds and Code Signing (johanneskinzig)](https://johanneskinzig.com/automating-pyinstaller-builds-and-code-signing-with-powershell.html) + +### Multi-tenant, auth, machine_id + +- [Multi-Tenant Architecture with FastAPI: Design Patterns and Pitfalls (Medium)](https://medium.com/@koushiksathish3/multi-tenant-architecture-with-fastapi-design-patterns-and-pitfalls-aa3f9e75bf8c) +- [Multitenancy with FastAPI - A practical guide](https://app-generator.dev/docs/technologies/fastapi/multitenancy.html) +- [Building Multi-Tenant APIs with FastAPI and Subdomain Routing](https://medium.com/@diwasb54/building-multi-tenant-apis-with-fastapi-and-subdomain-routing-a-complete-guide-cc076cb02513) +- [FastAPI RBAC Permissions: Role-Based Access for ML Resources 2026](https://www.johal.in/fastapi-rbac-permissions-role-based-access-for-ml-resources-2026/) +- [Get a unique computer ID in Python on Windows and Linux](https://www.iditect.com/faq/python/get-a-unique-computer-id-in-python-on-windows-and-linux.html) +- [Get Windows Unique ID by Python](https://nashorn892087495.wordpress.com/2019/09/12/get-windows-unique-id-by-python/) +- [JWT in FastAPI - Refresh Tokens Explained (Medium)](https://medium.com/@jagan_reddy/jwt-in-fastapi-the-secure-way-refresh-tokens-explained-f7d2d17b1d17) + +### Silent install et auto-update + +- [Silent install cheatsheet (GitHub)](https://github.com/offlineinstallersetup/silent-install-cheatsheet) +- [A Guide to Install MSI Silently (Server Scheduler)](https://serverscheduler.com/blog/install-msi-silently) +- [Velopack - Cross-platform installer and auto-update framework](https://velopack.io/) +- [Velopack documentation - Migrating from Squirrel](https://docs.velopack.io/migrating/squirrel) +- [tufup - Automated updates for Python apps (GitHub)](https://github.com/dennisvang/tufup) +- [PyUpdater archived](https://github.com/Digital-Sapphire/PyUpdater) + +### Concurrents RPA + +- [UiPath Orchestrator About Licensing](https://docs.uipath.com/orchestrator/docs/about-licensing) +- [UiPath Robot Licensing Standalone 2025.10](https://docs.uipath.com/robot/standalone/2025.10/admin-guide/licensing-troubleshooting) +- [UiPath Automation Suite - Allocating Robot Licenses to Tenants](https://docs.uipath.com/automation-suite/docs/allocating-robot-and-service-licenses-to-tenants) +- [Power Automate Desktop - Silent registration for machines](https://learn.microsoft.com/en-us/power-automate/desktop-flows/machines-silent-registration) +- [Power Automate Desktop - Manage machine groups](https://learn.microsoft.com/en-us/power-automate/desktop-flows/manage-machine-groups) +- [Power Automate Desktop - Hosted machines](https://learn.microsoft.com/en-us/power-automate/desktop-flows/hosted-machines) +- [Automation Anywhere - RDP-based bot deployment](https://docs.automationanywhere.com/bundle/enterprise-v11.3/page/enterprise/topics/control-room/bots/my-bots/rdp-based-approach-to-bot-deployment.html) +- [Skyvern Docker Setup](https://docs-new.skyvern.com/self-hosted/docker) +- [Skyvern Issue #4392 - Multi-session VNC support](https://github.com/Skyvern-AI/skyvern/issues/4392) +- [browser-use Issue #658 - Docker Image for Self Hosting](https://github.com/browser-use/browser-use/issues/658) + +### Observabilité multi-tenant + +- [Grafana Loki - Manage tenant isolation](https://grafana.com/docs/loki/latest/operations/multi-tenancy/) +- [Creating Multi-Tenant Observability Dashboards with Grafana & Loki (2025)](https://sollybombe.medium.com/creating-multi-tenant-observability-dashboards-with-grafana-loki-2025-edition-85a673eff596) +- [Managing Grafana and Loki in a regulated multitenant environment (AWS)](https://aws.amazon.com/blogs/opensource/how-to-manage-grafana-and-loki-in-a-regulated-multitenant-environment/) + +### HDS / RGPD / DSI hospitalière France + +- [Certification HDS en 2026 - ce que chaque hôpital doit vérifier (Galeon)](https://www.galeon.care/fr/blog/certification-hds-en-2026-ce-que-chaque-hopital-doit-verifier-avant-de-signer) +- [HDS V2 certification framework (LSTI)](https://www.lsti-certification.fr/en/News/certification-HDS) +- [HDS - Agence du Numérique en Santé](https://esante.gouv.fr/ens/offre/hds) +- [Health Data Hosting (HDS) France - Microsoft Compliance](https://learn.microsoft.com/en-us/compliance/regulatory/offering-hds-france) +- [Doctrine Numérique Santé 2025 - Règles de sécurité](https://esante.gouv.fr/doctrine/securite) + +--- + +*Document destiné à être consommé en lecture seule par Dom comme support de décision sur les axes packaging, code signing et architecture multi-tenant. Pas d'action engagée. Validation explicite requise avant chaque étape de Palier 2 ou 3.* diff --git a/docs/recherche/AXE_E_FRAMEWORKS_BENCHMARKS.md b/docs/recherche/AXE_E_FRAMEWORKS_BENCHMARKS.md new file mode 100644 index 000000000..9e727108f --- /dev/null +++ b/docs/recherche/AXE_E_FRAMEWORKS_BENCHMARKS.md @@ -0,0 +1,408 @@ +# Axe E — Référentiel benchmarks GUI 2026 & delta frameworks RPA visuels + +**Date :** 2026-05-23 +**Auteur :** Claude (subagent veille) via Dom +**Périmètre :** veille externe — pas de modif code. +**Source de référence à mettre à jour :** `docs/INSPIRATION_FRAMEWORKS_2026-05-10.md` (10 mai 2026) + `docs/superpowers/specs/2026-05-05-qw-suite-mai-design.md` (5 mai 2026). +**Statut :** veille brute, à valider avec Dom avant toute action. + +--- + +## 1. TL;DR + +**En 2 semaines (10 → 23 mai 2026), 3 mouvements à retenir :** + +1. **OSWorld n'est plus humain-level, il est passé super-humain.** Coasty (open source, github `coasty-ai/open-computer-use`) annonce 82 % sur OSWorld vs ~72 % humain, devant Claude Sonnet 4.6 à 73 % et Agent-S3 (Simular) à 69,9 %–72,6 % (bBoN). OpenAI Operator stagne à 38 %. La marche entre "agent qui copie l'humain" et "agent qui fait mieux" est franchie côté frontière open source. +2. **WebVoyager est saturé.** Om Labs 98,9 %, Alumnium 98,5 %, Magnitude 94 %. Skyvern 2.0 (85,85 %) n'est plus SOTA. Le benchmark ne discrimine plus — Skyvern a anticipé en lançant **Web Bench** (5 750 tâches × 452 sites, partenariat Halluminate). +3. **MCP est devenu standard d'agent.** 97 M downloads SDK mensuel en mars 2026 (+970× en 18 mois), 78 % des équipes IA enterprise déclarent au moins un agent MCP en prod (avril 2026). Microsoft Agent 365 (GA 1er mai 2026) intègre gouvernance MCP au niveau tenant. Anthropic, OpenAI, Google Gemini et Vercel SDK supportent tous MCP nativement. + +**Tendances 2026 :** +- Mixture-of-Grounding et Best-of-N rollouts (Agent S2/S3 « bBoN ») remplacent le single-pass. +- Continual learning sur GUI (GUI-AiF, AAAI 2026) émerge — replay engine devient training ground. +- Le rythme des sorties papier sur arXiv (AAAI 2026, ICLR 2026) double vs 2025 sur la verticale "GUI agent". + +**Recommandation immédiate pour rpa_vision_v3 :** +- Adopter **ScreenSpot-Pro** (1 581 instructions, 23 apps, 3 OS, leaderboard maintenu jusqu'à mai 2026) comme bench de grounding interne — c'est le seul qui a des screenshots haute résolution réalistes (notre cas Easily Assure). +- Surveiller **Coasty open-computer-use** (apparu post-doc inspiration) et **Agent-S3 bBoN** — les deux poussent un pattern Best-of-N qui résoudrait notre Validator laxiste (cf. §7). + +--- + +## 2. Carte référentiel benchmarks GUI 2026 + +| Benchmark | Mesure | Type tâches | Utilité rpa_vision_v3 | +|---|---|---|---| +| **ScreenSpot** (V1) | Grounding pur (clic) sur captures recadrées | 1 272 instructions web/desktop/mobile | Faible (résolutions trop basses, "consumer apps") | +| **ScreenSpot-V2** | Idem V1, 11,32 % de samples re-annotés | Idem V1 corrigé | Référence académique, pas notre cas réel | +| **ScreenSpot-Pro** | **Grounding haute résolution pro** | 1 581 instructions, 23 apps pro, 3 OS, écrans HD | **★★★★★ — notre cas** | +| **WindowsAgentArena** | Agent autonome end-to-end Windows | 154 tâches Windows (Notepad, Paint, navigateurs, etc.) | **★★★★ — OS cible** | +| **OSWorld** | Agent autonome end-to-end multi-OS | 369 tâches (LibreOffice, Chrome, VS Code, file mgmt) | **★★★★ — gold standard "agent"** | +| **OSWorld-Verified** | OSWorld durci anti-gaming (juillet 2025) | Sous-ensemble vérifié humain | ★★★ | +| **WebVoyager** | Agent web SOTA | 610 tâches sites live, jugement GPT | ★★ — saturé, pas notre cible (browser only) | +| **Online-Mind2Web** | Agent web réaliste | 300 tâches × 136 sites | ★★ | +| **Web Bench** (Skyvern + Halluminate) | Agent web large couverture | 5 750 tâches × 452 sites | ★★ | +| **AgentBench** (THUDM) | LLM-as-agent multi-environnement | 8 envs (OS, SQL, KG, jeux, web, etc.) | ★ — trop générique | +| **VisualWebBench** | Compréhension/grounding web MLLM | 1,5 k instances × 139 sites | ★ | +| **GUI-World** (ICLR 2025) | Compréhension **vidéo** GUI | 6 scénarios × 8 types Q dynamiques | ★ — pas notre angle replay | +| **AndroidWorld** | Mobile Android agent | 116 tâches × 20 apps Android | ✗ — hors scope healthtech desktop | +| **AndroidArena / A3** | Mobile dynamique | Tâches réalistes en ligne | ✗ | +| **MobileWorld** (ACL 2026) | Mobile + MCP-augmented | Tâches user-interactive | ✗ | + +**Carte de couverture qui mesure quoi :** +- **Grounding seul (point/bbox)** → ScreenSpot-Pro (★ pour nous), ScreenSpot-V2, VisualWebBench. +- **Agent autonome Windows** → WindowsAgentArena (★ pour nous). +- **Agent autonome multi-OS** → OSWorld, OSWorld-Verified (★ pour nous, partiellement). +- **Agent web** → WebVoyager (saturé), Online-Mind2Web, Web Bench. +- **Compréhension vidéo GUI** → GUI-World. +- **Mobile** → AndroidWorld, AndroidArena, MobileWorld (hors scope). + +--- + +## 3. Fiches des 5 benchmarks les plus pertinents pour nous + +### 3.1 ScreenSpot-Pro — `arxiv:2504.07981` + +- **Composition** : 1 581 instructions, 1 instruction par screenshot unique, 23 applications professionnelles, 5 secteurs (CAD, dev, ingénierie, science, design), 3 OS (Windows, macOS, Linux). Annotations expert humain. +- **Métriques** : taux de clic correct (point dans bbox vérité-terrain), bbox IoU. +- **Dataset accessible** : github `likaixin2000/ScreenSpot-Pro-GUI-Grounding`, leaderboard public `gui-agent.github.io/grounding-leaderboard/` (MAJ 14 avril 2026). +- **SOTA mai 2026** : + - GPT-5.2 (OpenAI) : 86,3 % + - GPT-5.4 (OpenAI) : 85,4 % (référence `benchlm.ai`) + - Muse Spark : 84,1 % + - Gemini 3 Pro (Google) : 72,7 % + - Qwen3.5 (féb 2026) : 70,3 % overall + - Qwen3.5-35B-A3B : 68,6 % + - Qwen2.5-VL-72B + RegionFocus : 61,6 % + - Baseline historique (papier original) : 18,9 % (modèles non spécialisés). +- **Lien** : https://arxiv.org/abs/2504.07981 +- **Pertinence rpa_vision_v3** : c'est **le seul bench grounding qui ressemble vraiment à Easily Assure** — résolutions ≥ 1920×1080, mix de menus denses, panneaux à droite, tableaux. Notre `MIGRATION_VLM_PLAN_2026-05-09.md` cite ScreenSpot-Pro mais nous n'avons pas de score interne récent à comparer. + +### 3.2 WindowsAgentArena (WAA) — `arxiv:2409.08264` + +- **Composition** : 154 tâches Windows réelles (Notepad, Paint, File Explorer, Clock, Settings, browsers, documents, vidéo, code). +- **Métriques** : success rate task-level, parallélisable en Azure (~20 min run complet). +- **Dataset accessible** : github `microsoft/WindowsAgentArena`, paper page `huggingface.co/papers/2409.08264`. +- **SOTA mai 2026** : + - UI-TARS-2 (ByteDance, sept 2025) : 50,6 % + - Multi-modal Navi (Microsoft, baseline) : 19,5 % + - Humain : 74,5 % +- **Lien** : https://microsoft.github.io/WindowsAgentArena/ +- **Pertinence rpa_vision_v3** : **★★★★★ pour positionnement client GHT** — Windows = terrain réel des TIM. Le gap humain–machine (74,5 % vs 50,6 % SOTA) est exactement le créneau où on opère (supervision médicale). Bench non saturé. + +### 3.3 OSWorld / OSWorld-Verified + +- **Composition** : 369 tâches sur OS réels (Ubuntu/Windows), apps réelles (LibreOffice, Chrome, VS Code, file mgmt, multi-app workflows). OSWorld-Verified = sous-ensemble durci (juillet 2025) pour empêcher le gaming. +- **Métriques** : success rate avec vérificateur déterministe par tâche (état final fichier, contenu DOM, etc.). +- **Dataset accessible** : leaderboard public maintenu. +- **SOTA mai 2026** : + - **Coasty open-cu** : 82 % (super-humain) — open source, gh `coasty-ai/open-computer-use` + - Claude Opus 4.6 (Anthropic) : 72,7 % + - Claude Sonnet 4.6 : 73 % + - **Agent-S3 + bBoN** (Simular) : 72,6 % — premier à passer humain + - Agent-S3 vanilla : 69,9 % + - GPT-5.3 Codex : 65 % + - GPT-5.2 Codex : 38 % + - OpenAI Operator (CUA) : 38,1 % + - Agent S2 (avril 2025) : 34,5 % + - UI-TARS-2 (ByteDance) : 47,5 % +- **Lien** : leaderboard via Coasty et Awesome Agents. +- **Pertinence rpa_vision_v3** : reference pour mesurer "où on en est par rapport au monde". Si on touche 30 % sur ces tâches en local-only, on est déjà compétitif. + +### 3.4 WebVoyager — `arxiv:2401.13919` + extension Web Bench + +- **Composition initiale** : 643 tâches × 15 sites (huit retirés post-Skyvern car obsolètes). Jugement GPT contre ≤ 15 screenshots/tâche. +- **Web Bench (Skyvern × Halluminate, 2026)** : 5 750 tâches × 452 sites. +- **SOTA mai 2026** (WebVoyager) : + - Om Labs (Claude Code + Opus 4.7 + GPT-5.4 Nano) : 98,9 % + - Alumnium MCP (Claude Code + Selenium) : 98,5 % + - Surfer 2 (H Company) : 97,1 % + - Magnitude : 94 % + - OpenAI CUA / Operator : 87 % + - **Skyvern 2.0 : 85,85 %** (référence doc 10 mai 2026 — plus SOTA) +- **Lien** : https://webvoyager.omlabs.xyz/ +- **Pertinence rpa_vision_v3** : ★★ — pas notre cible (DPI Easily est partiellement web mais via Citrix souvent). À surveiller comme indicateur de saturation des benchs publics. + +### 3.5 Bench candidat desktop Windows-spécifique → **Online-Mind2Web** + ScreenSpot-Pro suffisent + +Aucun bench n'est plus "Windows-desktop natif" que WindowsAgentArena à date. Pour la verticale healthtech, **il n'existe pas de bench public** — c'est probablement une opportunité (créer `EasilyBench-1` interne à partir de nos 11 dossiers GHT serait un asset commercial). + +--- + +## 4. Mise à jour frameworks vs doc 10 mai 2026 + +### 4.1 OpenAdapt (OpenAdaptAI) + +| Aspect | 10 mai 2026 | 23 mai 2026 | +|---|---|---| +| Stars | ~7 k | en croissance | +| Dernier release PyPI | non précisé | **4 mars 2026** (PyPI) | +| Capacités VLM | LLM/LMM/VLM/LAM | + adaptateurs Qwen3-VL et Qwen2.5-VL via HF + PEFT | +| Phase produit | Phase 2 (retrieval-only) validée | **Phase 3** (demo-conditioned fine-tuning) en cours | +| Infra | local | + intégration AWS C8i/M8i/R8i nested virt (févr 2026, ~$0,19/h) | +| Sous-projets | mono-repo | **`openadapt-ml`** + **`openadapt-evals`** splittés | + +**Delta clé** : OpenAdapt a structuré son écosystème en 3 dépôts (core + ML + evals). Le pattern "Evaluation-Driven Feedback" cité dans le doc 10 mai est désormais matérialisé dans `openadapt-evals` (infrastructure benchmarks). À étudier comme template pour notre `TargetMemoryStore` → pipeline d'entraînement. + +### 4.2 Skyvern (Skyvern-AI) + +| Aspect | 10 mai 2026 | 23 mai 2026 | +|---|---|---| +| WebVoyager | 85,85 % (cité comme SOTA) | **plus SOTA** — 4 acteurs au-dessus | +| Nouveauté | Planner-Actor-Validator + VWB | **Web Bench** (5 750 tâches × 452 sites) avec Halluminate, fév 2026 | +| Layout-resistant | non cité | dossier Layout-Resistant Tools (fév 2026) | + +**Delta clé** : Skyvern a réagi à la saturation de WebVoyager en lançant son propre méga-bench. Notre VWB partage le naming `Visual Workflow Builder` avec eux, pas un problème, convergence indépendante. + +### 4.3 OmniParser (Microsoft) + +| Aspect | 10 mai 2026 | 23 mai 2026 | +|---|---|---| +| Version | V2 (févr 2025) | V2.0.1 (12 sept 2025) — **patch sécurité CVE-2025-55322 RCE** | +| Latence | non précisée | **60 % réduction vs V1**, 0,6–0,8 s sur A100/4090 | +| ScreenSpot-Pro | non précisé | **39,6 %** sur détection d'interactables | +| V3 | — | **non annoncé** | + +**Delta clé** : OmniParser V2 reste la référence "screen tokenizer". Pas de V3 en vue. Le patch CVE-2025-55322 est à connaître si on auto-héberge. + +### 4.4 TagUI (AI Singapore) + +| Aspect | 10 mai 2026 | 23 mai 2026 | +|---|---|---| +| Statut | actif mais "moins LLM-first" | inchangé. V6 en chantier (Chrome visible par défaut) | +| Roadmap | non précisée | IDE + Orchestrator + Reporting Dashboard prévus | + +**Delta clé** : aucun mouvement majeur. TagUI évolue vers UI/orchestration, pas vers le RPA visuel LLM-first. + +### 4.5 Anthropic Computer Use SDK / Claude + +- **Claude Opus 4.6** annoncé. +- **Claude Sonnet 4.6** : 72,5–73 % OSWorld (qualifié de "barely human-level"). +- **Claude Opus 4.6** : 72,7 % OSWorld. +- **Claude Opus 4.7** présent dans `Om Labs` (top WebVoyager 98,9 %). +- Postmortem Anthropic mars-avril 2026 : 3 bugs latence/qualité (reasoning effort, caching, verbosity prompt). Résolus le 20 avril. + +### 4.6 OpenAI Operator (CUA) + +- **OSWorld** : 38,1 % — **n'a pas bougé**. Coasty publie un Review titré "A 38% Score Is Not an AI Agent, It's a Beta Product" (mai 2026). +- **WebVoyager** : 87 %, devancé. +- Operator standalone sunset → fusionné dans ChatGPT "agent mode" depuis juillet 2025. +- **CUA exposé via API** (Responses API, tier 3-5 select developers, research preview). + +### 4.7 Simular Agent-S → Agent-S2 → Agent-S3 + +| Version | Date | OSWorld | Innovation | +|---|---|---|---| +| Agent-S | 2024 | — | architecture computer-use mature | +| Agent-S2 | avril 2025 | 34,5 % (50 step) | **Mixture-of-Grounding** + Proactive Hierarchical Planning | +| Agent-S3 | déc 2025 / 2026 | **69,9 %** (vanilla) → **72,6 %** (Best-of-N "bBoN") | suppression hiérarchie, **native coding agent** Python/Bash, Behavior Best-of-N (sample multiple rollouts, garde le meilleur) | + +**Delta clé** : Agent-S3 est devenu le premier agent à passer humain-level OSWorld (avant Coasty). Le pattern **bBoN** est probablement le quick-win le plus rentable pour notre Validator (cf. §7). + +### 4.8 Magma (Microsoft Research) + +- Foundation model multimodal **digital + physique** (CVPR 2025, github `microsoft/Magma` MIT licence). +- Innovations : **Set-of-Mark** (SoM) pour grounding action + **Trace-of-Mark** (ToM) pour planification. +- Magma-8B sur HuggingFace. +- Pas de release majeure en mai 2026, mais le pattern SoM/ToM est repris dans plusieurs papiers AAAI/ICLR. + +### 4.9 Cradle (Microsoft Research) + +- Le terme "Cradle" est concurrencé en mai 2026 par **Microsoft Agent 365** (GA 1er mai 2026) qui couvre la gouvernance/observabilité d'agents (incluant MCP servers). Pas de release Cradle spécifique. + +### 4.10 OS-Atlas (OS-Copilot) + +- Statut : ICLR 2025 accepted, modèles OS-Atlas-Base-4B/7B + OS-Atlas-Pro-7B/4B sur HuggingFace. +- **ScreenSpot-V2** : re-annoté par OS-Atlas team (11,32 % de samples corrigés). +- Pas de **V2 OS-Atlas** annoncée à mai 2026. + +### 4.11 UI-TARS / UI-TARS-2 (ByteDance) + +| Version | Date | Notes | +|---|---|---| +| UI-TARS-1.5-7B | mars 2026 (notre repo l'avait, commit `9da589c8c` du 25 avril) | abandonné par nous le 26 avril pour InfiGUI-G1-3B | +| **UI-TARS-2** | **4 sept 2025** | All-In-One Agent (GUI + Game + Code + Tool), Apache 2.0 | +| UI-TARS-desktop | mai 2026 | **33 573 stars** = plus gros projet open source GUI agent | + +**Scores UI-TARS-2** : +- Online-Mind2Web : 88,2 +- OSWorld : 47,5 +- WindowsAgentArena : 50,6 +- AndroidWorld : 73,3 + +**Delta clé** : UI-TARS-2 est sorti AVANT le doc 10 mai mais n'y est pas mentionné. ByteDance détient désormais le plus grand écosystème GUI agent open source (33 k stars) — à reconsidérer comme alternative à InfiGUI-G1-3B sur notre serveur grounding. + +### 4.12 AGUVIS (Salesforce + HKU) + +- Pas de release Salesforce 2026. +- Toujours référencé comme baseline pure vision (89,2 grounding multi-plateforme, 51,9 % step success rate). +- ICML 2025 accepted. + +### 4.13 MCP (Model Context Protocol) + +| Métrique | Mars-Avril 2026 | +|---|---| +| Downloads SDK mensuels | **97 millions** (+970× en 18 mois) | +| Servers publics | 9 400+ (vs 1 200 Q1 2025), +18 % mom Q1 2026 | +| Adoption enterprise | 78 % équipes IA ont ≥ 1 agent MCP en prod | +| CTOs déclarant MCP "default" | 67 % dans 12 mois | +| Support LLM | Claude (natif), ChatGPT (Apps SDK), Gemini (mars 2026), Cursor, Windsurf, Zed, JetBrains, Vercel AI SDK, OpenAI Agents SDK | +| Roadmap 2026 | audit trails, SSO auth, gateway, config portability | + +**Delta clé pour rpa_vision_v3** : on est dans la fenêtre où exposer notre engine via MCP serait un asset commercial (Skyvern, OpenAdapt, browser-use l'ont fait). Microsoft Agent 365 prévoit la **gouvernance MCP au niveau tenant** — vendeur d'argument healthtech (audit, conformité). + +--- + +## 5. Nouveaux entrants 2026 — non couverts par les docs internes + +### 5.1 Coasty (gh `coasty-ai/open-computer-use`) + +- **82 % OSWorld** — premier au-dessus de Claude Sonnet 4.6 (73 %) et Agent-S3 (72,6 %). +- "Production-ready, remote and local, one API key". +- Open source. +- **À étudier ASAP** : architecture probablement utile pour pousser notre OSWorld interne. + +### 5.2 Agent-S3 bBoN (Simular) + +- Pattern Behavior Best-of-N : exécute N rollouts en parallèle, sélectionne le meilleur via judge. +- 18,9 % et 32,7 % relative improvements vs baseline. +- **Lien direct avec notre Validator laxiste** (bug step 10 Imagerie dans bandeau Edge) : bBoN éviterait que le mauvais rollout passe le VERIFY. + +### 5.3 InfiGUI-G1 + AEPO (AAAI 2026 Oral) + +- **Notre serveur grounding actuel** (`InfiGUI-G1-3B`, commit `77faa03ec` du 26 avril) repose dessus. +- Adaptive Exploration Policy Optimization : +9 % vs RLVR baseline. +- Acceptance AAAI 2026 Oral confirme robustesse. + +### 5.4 Magnitude / Alumnium / Om Labs + +- Magnitude (gh `magnitudedev/webvoyager`) : 94 % WebVoyager. +- Alumnium : 98,5 % WebVoyager via Claude Code + Selenium + MCP. +- Om Labs (`webvoyager.omlabs.xyz`) : 98,9 % WebVoyager (avril 2026). +- Pattern commun : couplage browser engine **classique** (Selenium/Playwright) + agent LLM. Pas notre angle (Citrix interdit DOM), mais à surveiller. + +### 5.5 GUI-Actor (Microsoft) + +- `microsoft/GUI-Actor-7B-Qwen2.5-VL` sur HF. +- Attention-based action head **sans coordonnées** (coordinate-free visual grounding). +- 44,6 sur ScreenSpot-Pro avec Qwen2.5-VL backbone. + +### 5.6 Papiers AAAI/ICLR/ICML 2026 à surveiller + +- **TreeCUA** (fév 2026, `arxiv:2602.09662`) — tree-structured verifiable evolution. +- **LiteGUI** (`arxiv:2605.07505`) — distillation compact GUI via RL. +- **UltraCUA** (`arxiv:2510.17790`) — foundation model CUA hybrid action. +- **Continual GUI Agents** (`arxiv:2601.20732`) — continual learning sur GUI. +- **GUI-RCPO** (`arxiv:2509.21552`) — self-improvement, +5 % ScreenSpot-V2. +- **MobileWorld** (ACL 2026) — mobile + MCP-augmented. + +--- + +## 6. Tendances 2026 — patterns émergents + +1. **Best-of-N rollouts** (Agent-S3 bBoN, Om Labs WebVoyager) : un seul agent run ne suffit plus, on parallélise et on garde le meilleur. Implication directe pour rpa_vision_v3 : notre VERIFY post-action devrait être un judge entre plusieurs candidats de grounding, pas un pHash global. + +2. **Mixture-of-Grounding** (Agent-S2, GUI-Actor) : différents modèles de grounding spécialisés pilotés par un routeur adaptatif. C'est exactement la spec **F2** déclarée out-of-scope dans `QW_SUITE_MAI` mais qui devient mainstream. + +3. **Continual learning on-the-fly** (GUI-AiF AAAI 2026, OpenAdapt phase 3) : l'agent apprend pendant le replay. Notre `TargetMemoryStore` est conceptuellement aligné mais sans pipeline d'entraînement. + +4. **MCP-first architecture** : tous les acteurs majeurs (Anthropic, OpenAI, Google, Skyvern, browser-use, Alumnium) exposent ou consomment MCP. Le standard d'interop est tranché. + +5. **Synthesis frameworks** : on n'oppose plus RPA classique et AI agent. Skyvern (Planner-Actor-Validator), Agent-S3 (manager + native coding), Coasty (production-ready), OpenAdapt 3 dépôts. Le vainqueur est celui qui combine déclaratif + LLM + grounding spécialisé. + +6. **Saturation des benchs publics et création de méga-benchs privés** : WebVoyager saturé → Web Bench (5 750 × 452). OSWorld passé humain → futur OSWorld-2 inévitable. + +--- + +## 7. Implications pour rpa_vision_v3 + +### 7.1 Frameworks méritant exploration deeper + +| Framework | Pourquoi | Effort lecture | +|---|---|---| +| **Coasty open-computer-use** (82 % OSWorld, OS) | Architecture production-ready, "remote and local" qui matche notre Léa Windows + serveur Linux | 1–2 j | +| **Agent-S3 bBoN** (72,6 % OSWorld, open) | Best-of-N résout notre Validator laxiste (bug step 10) | 0,5–1 j paper + code | +| **OpenAdapt phase 3** (demo-conditioned fine-tuning) | Template pour brancher `TargetMemoryStore` sur un pipeline d'entraînement | 1 j paper + code | +| **UI-TARS-2 + UI-TARS-desktop** (33 k stars) | Alternative à InfiGUI-G1-3B sur notre serveur grounding | 1 j eval | +| **MCP serveur** (Skyvern, browser-use, Anthropic) | Exposer rpa_vision_v3 en MCP = standard interop healthtech | 2–3 j POC | + +### 7.2 Benchmarks à adopter pour mesurer notre progrès + +1. **ScreenSpot-Pro** (priorité 1) — refaire un bench grounding sur les 5 modèles déjà testés (qwen2.5vl:7b Ollama, qwen3-vl:8b, InfiGUI-G1-3B, UI-TARS-2, qwen3.5). Permet de positionner notre stack sur un référentiel public. + - Notre `BENCH_GROUNDING_INTERNE_2026-05-08` ne contient qu'1 fixture (heartbeat dialog OK/Cancel) — c'est trop pauvre. +2. **WindowsAgentArena** (priorité 2) — adapter 5–10 tâches du WAA "browsers/documents" à notre stack pour avoir un repère agent autonome public. +3. **EasilyBench-1 interne** (priorité 3) — créer un bench fermé à partir des 11 dossiers GHT (workflow `Urgence_aiva_demo` + variantes). Asset commercial : "on a notre propre eval validée par médecin DIM". + +### 7.3 Patterns à formaliser dans la doc (gratuit, zéro code) + +Le doc 10 mai recommandait déjà Policy / Grounding / Safety Gate / Validator. À ajouter : +- **Best-of-N rollouts** (bBoN) comme alternative au pHash VERIFY. +- **Mixture-of-Grounding** comme nom officiel de notre cascade. +- **Screen Tokenizer** comme nom de la suggestion §4.1 du doc 10 mai (log candidats à chaque `_resolve_target`). +- **MCP-first** dans la roadmap interop. + +### 7.4 Mises à jour à porter dans `INSPIRATION_FRAMEWORKS_2026-05-10.md` + +- §3.1 Skyvern : retirer "85,85 % WebVoyager SOTA" — ajouter "85,85 % avant Om Labs/Alumnium/Magnitude/Surfer 2 — Skyvern a lancé Web Bench (5 750 × 452)". +- §4.1 OmniParser : préciser V2.0.1 + patch CVE-2025-55322 + 39,6 % ScreenSpot-Pro + 60 % latence réduite. +- §5 ajouter Coasty, Agent-S3, UI-TARS-2 comme entrants 2026 majeurs. +- §6 ajouter MCP server architecture comme **présent**, pas long-terme. +- §7 ajouter "Best-of-N" et "Continual learning" comme nouveaux patterns convergents. + +--- + +## 8. Sources (avec dates) + +### Benchmarks +- ScreenSpot-Pro paper — https://arxiv.org/abs/2504.07981 (avril 2025, leaderboard MAJ avril 2026) +- ScreenSpot-Pro leaderboard — https://gui-agent.github.io/grounding-leaderboard/ (MAJ 14 avril 2026) +- ScreenSpot-Pro models avg — https://benchlm.ai/benchmarks/screenSpotPro (mai 2026) +- WindowsAgentArena paper — https://huggingface.co/papers/2409.08264 +- WindowsAgentArena GH — https://github.com/microsoft/WindowsAgentArena +- OSWorld leaderboard via Coasty — https://coasty.ai/blog/osworld-benchmark-results-2026-who-actually-wins (mai 2026) +- WebVoyager leaderboard — https://webvoyager.omlabs.xyz/ (avril 2026) +- Online-Mind2Web GH — https://github.com/OSU-NLP-Group/Online-Mind2Web (mars 2025) +- VisualWebBench — https://visualwebbench.github.io/ +- AgentBench GH — https://github.com/THUDM/AgentBench +- Holistic Agent Leaderboard — https://hal.cs.princeton.edu/ + +### Frameworks (delta 10 → 23 mai 2026) +- OpenAdapt — https://github.com/OpenAdaptAI/OpenAdapt (PyPI 4 mars 2026) +- OpenAdapt evals — https://github.com/OpenAdaptAI/openadapt-evals +- Skyvern 2.0 launch — https://www.skyvern.com/blog/skyvern-2-0-state-of-the-art-web-navigation-with-85-8-on-webvoyager-eval/ +- Skyvern Web Bench — https://www.skyvern.com/blog/web-bench-a-new-way-to-compare-ai-browser-agents/ +- OmniParser V2.0.1 release — https://github.com/microsoft/OmniParser/releases (12 sept 2025) +- OmniParser V2 perf — https://www.microsoft.com/en-us/research/articles/omniparser-v2-turning-any-llm-into-a-computer-use-agent/ +- TagUI — https://github.com/aisingapore/TagUI +- Browser Use changelog — https://browser-use.com/changelog (CLI 2.0, BU 2.0 jan 2026, V3 sessions avril 2026) +- Anthropic postmortem Claude — https://www.anthropic.com/engineering/april-23-postmortem (23 avril 2026) +- Anthropic Opus 4.6 — https://www.anthropic.com/news/claude-opus-4-6 +- OpenAI Operator — https://openai.com/index/introducing-operator/ +- OpenAI Operator critique — https://coasty.ai/blog/openai-operator-review-2026-20260504 (4 mai 2026) +- Simular Agent-S2 — https://www.simular.ai/articles/agent-s2 +- Simular Agent-S3 — https://www.simular.ai/articles/agent-s3 +- Simular Agent-S GH — https://github.com/simular-ai/Agent-S +- Microsoft Magma — https://microsoft.github.io/Magma/ +- Microsoft Magma GH — https://github.com/microsoft/Magma +- Microsoft Agent 365 GA — https://www.microsoft.com/en-us/security/blog/2026/05/01/microsoft-agent-365-now-generally-available-expands-capabilities-and-integrations/ (1er mai 2026) +- OS-Atlas — https://github.com/OS-Copilot/OS-Atlas +- UI-TARS-2 paper — https://arxiv.org/abs/2509.02544 (sept 2025) +- UI-TARS GH — https://github.com/bytedance/ui-tars +- UI-TARS-desktop GH — https://github.com/bytedance/UI-TARS-desktop (33,5 k stars mai 2026) +- AGUVIS — https://aguvis-project.github.io/ + +### MCP & adoption +- MCP roadmap 2026 — https://blog.modelcontextprotocol.io/posts/2026-mcp-roadmap/ +- MCP adoption stats — https://www.digitalapplied.com/blog/mcp-adoption-statistics-2026-model-context-protocol (avril 2026) +- MCP 97 M downloads — https://www.digitalapplied.com/blog/mcp-97-million-downloads-model-context-protocol-mainstream (mars 2026) +- The New Stack MCP — https://thenewstack.io/model-context-protocol-roadmap-2026/ + +### Nouveaux entrants & papiers +- Coasty open-cu GH — https://github.com/coasty-ai/open-computer-use +- InfiGUI-G1 AAAI 2026 — https://github.com/InfiXAI/InfiGUI-G1 + https://arxiv.org/abs/2508.05731 +- GUI-Actor — https://microsoft.github.io/GUI-Actor/ +- Alumnium WebVoyager — https://alumnium.ai/blog/webvoyager-benchmark/ +- Magnitude WebVoyager — https://github.com/magnitudedev/webvoyager +- Awesome GUI Agent — https://github.com/showlab/Awesome-GUI-Agent + +--- + +*Document de veille à 23 mai 2026, lecture seule. Toute action (adoption framework, intégration bench, refonte) nécessite une décision explicite de Dom et un spec dédié.* diff --git a/docs/recherche/COMPTE_RENDU_ANCRES_VISUELLES_NOTEPAD_2026-05-24.md b/docs/recherche/COMPTE_RENDU_ANCRES_VISUELLES_NOTEPAD_2026-05-24.md new file mode 100644 index 000000000..a536bc697 --- /dev/null +++ b/docs/recherche/COMPTE_RENDU_ANCRES_VISUELLES_NOTEPAD_2026-05-24.md @@ -0,0 +1,59 @@ +# Compte Rendu : Stabilisation par Ancres Visuelles (Inspiration Coasty/Agent-S3) + +**Date :** 24 mai 2026 +**Objet :** Résolution des régressions "Enregistrer sous" et "Start Button" via la triangulation visuelle. +**Statut :** Spécifications de recherche (Zéro modification de code). + +--- + +## 1. Synthèse de la Problématique +Les échecs actuels de Léa sur les dialogues Windows (Bloc-notes) et le menu Démarrer proviennent d'une dépendance excessive à l'OCR du titre de la fenêtre. Sous Windows 11, ces titres sont instables ou asynchrones. + +La solution identifiée dans les frameworks SOTA (Coasty, Agent-S3) consiste à utiliser des **Ancres Visuelles** : des éléments graphiques invariants qui permettent de trianguler la position des cibles sans lire le texte. + +--- + +## 2. Fiche Ancre : Dialogue "Enregistrer sous" (Notepad Windows 11) + +Cette fiche définit la "signature sémantique" du dialogue de sauvegarde pour permettre à Léa de le reconnaître instantanément, même avant que l'OCR n'ait fini de lire le titre. + +| Élément | Type d'Ancre | Rôle dans la Triangulation | Signature Visuelle (Invariants) | +| :--- | :--- | :--- | :--- | +| **Bouton Fermer (X)** | Ancre de Structure | Définit le coin supérieur droit de la zone de recherche. | Petit rectangle rouge ou gris avec une croix blanche, situé en haut à droite. | +| **Bouton Annuler** | Ancre de Référence | Permet de localiser le bouton "Enregistrer" par proximité. | Bouton textuel situé systématiquement en bas à droite du dialogue. | +| **Champ Nom de fichier** | Ancre de Focus | Indique où l'agent doit cliquer pour saisir le texte. | Rectangle blanc allongé, souvent situé juste au-dessus des boutons d'action. | + +### Stratégie de Triangulation (SANS coordonnée fixe) : +1. **Détection du Dialogue** : Si un nouveau rectangle de ~800x600 pixels apparaît au centre de l'écran avec une bordure fine. +2. **Localisation de l'Ancre de Référence** : Trouver le bouton "Annuler" (bas-droite). +3. **Déduction de la Cible** : Le bouton "Enregistrer" est le bouton immédiatement situé à **gauche** de l'Ancre de Référence (distance ~10-50px). +4. **Validation** : Si le bouton "Annuler" disparaît, le dialogue est fermé -> Succès. + +--- + +## 3. Application au Cas "Start Button" (Boucle de Retries) + +Le menu Démarrer échoue car il n'a pas de titre de fenêtre exploitable par le `DialogHandler` classique. + +### Signature d'Ancre pour le Menu Démarrer : +* **Ancre Primaire** : Le logo Windows dans la barre des tâches (Ancre de déclenchement). +* **Ancre de Validation** : Apparition d'un grand panneau translucide (Mica/Acrylic) ancré en bas de l'écran. +* **Point Critique** : Le menu Démarrer de Windows 11 ne touche pas forcément le bord de l'écran. L'ancre doit être la **Barre de Recherche** interne au menu. +* **Validation de succès** : Si la zone de recherche "Taper ici pour rechercher" devient visible, le menu est considéré comme "Ouvert" (State-Centric Success). + +--- + +## 4. Recommandations pour le Pilotage de Léa + +Pour intégrer ces concepts dans la logique actuelle de Léa sans coder, voici les principes à suivre lors de la création des scénarios : + +1. **Utiliser des Offsets Relatifs** : Dans le VWB, privilégier les clics relatifs à une ancre (ex: "Clic à gauche de 'Annuler'") plutôt que des clics sur un texte "Enregistrer" qui peut varier. +2. **Introduire un "Visual Wait for Anchor"** : Avant de saisir le nom du fichier, forcer l'agent à attendre l'apparition visuelle de l'icône de la fenêtre de dialogue. +3. **Le "pHash de Stabilité"** : Ne pas valider l'action `start_button` tant que le pHash de la zone centrale de l'écran n'a pas arrêté de changer (signe que l'animation d'ouverture est finie). + +--- + +## 5. Conclusion Technique +Le passage à une détection par **Ancres et Triangulation** résout le problème de la latence Windows. Léa devient capable de "comprendre" la structure d'un dialogue Windows sans avoir besoin que l'OS lui fournisse des informations textuelles (souvent en retard). + +*Document d'analyse stratégique. Zéro modification de code effectuée.* diff --git a/docs/recherche/INDEX_REPLAY_SPECS_2026-05-24.md b/docs/recherche/INDEX_REPLAY_SPECS_2026-05-24.md new file mode 100644 index 000000000..5e4e7430d --- /dev/null +++ b/docs/recherche/INDEX_REPLAY_SPECS_2026-05-24.md @@ -0,0 +1,279 @@ +# Index — Specs opérationnelles replay (transport / validator / popups) + +**Date :** 2026-05-24 +**Auteur :** Claude (session principale) à partir des 3 specs ciblées + 6 docs de recherche préalables. +**Public :** humain ou agent qui doit prendre en charge l'un des 3 chantiers de fiabilisation replay post-démo GHT. +**Statut :** lecture seule. Aucune décision figée. Plan d'action proposé à valider par Dom. + +--- + +## 0. Comment lire ce document + +Tu prends en charge un chantier replay → tu lis §1 (TL;DR) et tu sautes directement à la spec correspondante (§3.1, §3.2 ou §3.3). Tu reviens à §2 (croisements) si ton chantier touche les deux autres. Les décisions ouvertes Dom sont consolidées §4. + +Les 3 specs s'appuient sur des **recherches préalables** déjà livrées (`AXE_B1`, `AXE_B1_DEEP`, `AXE_B2`, `AXE_B2_DEEP`, `AXE_D2`, `AXE_D2_DEEP`). Les specs ne refont pas l'étude — elles produisent le contrat opérationnel. + +--- + +## 1. TL;DR + +**3 chantiers replay à mener en parallèle pour fermer les bugs racines post-démo GHT (~3 j homme MVP cumulés)** : + +| # | Chantier | Bug racine fermé | Spec | Code prêt | Effort MVP | +|---|---|---|---|---|---| +| **B1** | Transport + watchdog | Désync 8 mai (9 actions perdues en 33s) | `SPEC_TRANSPORT_CONTRAT.md` | `replay_watchdog.py` ~270 LOC + patches `api_stream.py` | **3h30** | +| **B2** | Validator post-action | Step 10 (clic Imagerie dans Edge, REPORT success=True) | `SPEC_VALIDATOR_MATRICE.md` | package `core/validation/` ~590 LOC (MVP P0 ~190 LOC) | **8h** | +| **D2** | Chaîne popup propre | `_handle_possible_popup` orphelin + auto-dismiss risqué | `SPEC_POPUPS_CATALOGUE.md` | package `core/dialog/` ~700 LOC + 59 entrées catalogue | **1j** | + +**Résultat attendu cumulé** : démo prochaine sans contournement `static_result/static_text`, sans `cancel-replays.sh` manuel, sans pause humaine sur dialogs métier connus. + +**Contraintes invariantes respectées** : +- 100% vision (aucun raccourci système inventé) +- Healthtech (jamais d'auto-accept UAC / Hello / SmartScreen / suppression non déclarée) +- Backward compatible (kill-switches env var par défaut OFF sur chaque chantier) + +--- + +## 2. Croisements entre les 3 specs + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ │ +│ ┌───────────────┐ ┌───────────────┐ │ +│ │ B1 Transport │◄────────│ B2 Validator │ │ +│ │ + Watchdog │ ack │ + 6 Checkers │ │ +│ └───────────────┘ └───────────────┘ │ +│ ▲ │ │ +│ │ purge si paused │ failure_category= │ +│ │ ▼ UNEXPECTED_DIALOG │ +│ │ ┌───────────────┐ │ +│ └──────────────────│ D2 Popup │ │ +│ │ Resolver │ │ +│ └───────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Dépendances et contrats inter-modules + +| Source | Cible | Contrat | Quand | +|---|---|---|---| +| B1 | B2 | Action arrive bien côté serveur (REPORT non perdu) | Prérequis. Sans B1, B2 vote sur du vide. | +| B2 | D2 | `failure_category=UNEXPECTED_DIALOG/NO_VISUAL_CHANGE/WRONG_APPLICATION` → `DialogResolver.resolve()` | Au verdict d'échec d'un Checker | +| D2 | B1 | Si modal détecté pendant qu'une action est en `_retry_pending`, watchdog doit purger | v1.1 du watchdog (déjà partiel via `cancel_replay` ligne 4489) | +| D2 | B2 | `DialogPresenceChecker` (Checker B2) fournit la bbox utilisée par `DialogClosedChecker` | Coordination intra-Validator | +| B2 | B1 | Verdict `WRONG_APPLICATION` (bug step 10) → override `success=True → False` → relance watchdog | Au report client | + +### Orthogonalité + +- **B1 ↔ B2** : techniquement orthogonaux (peuvent être codés en parallèle), mais **les deux ensemble** = fermeture totale du bug du 8 mai (transport + validation). +- **D2** : indépendant de B1 et B2 pour l'implémentation, mais consomme leurs interfaces au runtime. + +--- + +## 3. Résumé par spec + +### 3.1. SPEC_TRANSPORT_CONTRAT.md + +**Chemin** : `/home/dom/ai/rpa_vision_v3/docs/recherche/SPEC_TRANSPORT_CONTRAT.md` +**Volume** : 766 lignes, 169 lignes de tables. +**Recherches sources** : `AXE_B1_REPLAY_TRANSPORT.md`, `AXE_B1_DEEP_WATCHDOG.md`. + +**Contenu** : +- 2 state machines ASCII (serveur PENDING → DISPATCHED → ORPHAN → ACKED → ABANDONED → PAUSE | client POLLING → RECEIVED → DEDUP → EXECUTING → REPORTING → ACKED) avec invariants I1-I6 et C1-C5. +- Contrats JSON DISPATCH / REPORT / re-dispatch / pause / resume / cancel, avec lignes de code source précises (`api_stream.py:626-651` schema, `3354-3359` retry_pending, `4361-4474` resume, `4489` cancel). +- **Matrice 21 cas limites** (a→u) couvrant timeout pré-réponse, déconnexion post-réception, report perdu, watchdog race, client mort, server restart, polls concurrents, idempotence double-clic, pause pendant action en vol, cancel UI, abandon MAX_RESENDS, etc. +- Sémantique d'idempotence à 7 couches + spec `dedup_set` client copy-paste + idempotence par type d'action. +- 18 timeouts/seuils tabulés avec env vars (`RPA_WATCHDOG_ORPHAN_TIMEOUT_S=30`, `RPA_WATCHDOG_SCAN_INTERVAL_S=10`, `RPA_WATCHDOG_MAX_RESENDS=2`, etc.). +- Transitions pause supervisée & resume (5 déclencheurs `paused_need_help`). +- Compatibilité transport polling ↔ SSE (table d'invariance). +- 7 fiches précédents externes (SQS, NATS, Skyvern, browser-use, Anthropic CU, Playwright MCP) + table comparative. + +**Code production-ready dans le doc** : module `agent_v0/server_v1/replay_watchdog.py` (~270 lignes), 4 patches diff unified `api_stream.py`, 8 tests pytest `tests/integration/test_replay_watchdog.py`. + +**Effort intégration** : **3h30** (45min schéma+watchdog, 30min câblage, 1h tests pytest, 1h chasse races sur E2E réel, 15min DETTE). + +--- + +### 3.2. SPEC_VALIDATOR_MATRICE.md + +**Chemin** : `/home/dom/ai/rpa_vision_v3/docs/recherche/SPEC_VALIDATOR_MATRICE.md` +**Volume** : ~1300 lignes denses. +**Recherches sources** : `AXE_B2_VALIDATOR_PATTERN.md`, `AXE_B2_DEEP_VALIDATOR.md`, `AXE_A4_OCR_TEMPLATE_PHASH.md`. + +**Contenu** : +- Matrice principale **27 types d'action × 9 colonnes** (signal primaire / secondaire / fallback / verdicts possibles / latence cible / coût / Checker à utiliser). +- **4 fiches détaillées** pour les cas listés explicitement par Dom : + - `switch_tab` (le bug step 10) : signal primaire = `OcrRoiChecker` ROI 120×40 autour du tab attendu cherchant label exact dans words OCR ; secondaire = pHash de la zone contenu sous les tabs a changé ≥ 5%. + - `close_tab` : `TabAbsenceChecker` + visibilité tab voisin actif. + - `save` : disparition indicateur "modifié" + apparition toast "Enregistré". + - `dialog_button` : disparition du dialog (handoff D2 via `DialogClosedChecker`). +- 5 fiches secondaires (`click_anchor`, `type_text`, `extract_text`, `t2a_decision`, `keyboard_shortcut`). +- **6 Checkers production-ready** : `PixelDiffChecker` (15ms), `OcrRoiChecker` (80ms, **résout step 10**), `TitleBarChecker` (130ms wrapper existant), `JsonSchemaChecker` (10ms), `LlmJudgeChecker` (3s wrapper `verify_with_critic` existant `replay_verifier.py:367`), `TabActiveChecker`/`TabAbsenceChecker`/`SaveSuccessChecker`/`DialogClosedChecker` (nouveaux). +- Confidence scoring + règles d'agrégation multi-checker (`switch_tab` → SUCCESS si primaire ≥ 0.85 OU primaire ≥ 0.65 AND secondaire ≥ 0.70). +- **Anti-patterns table 12 entrées** : pHash global pour switch_tab, title-bar seule pour SPA, `success=True` parce que coords envoyées, SSIM global dialog, etc. +- 6 précédents externes (Skyvern `complete_verify`, browser-use `evaluation` agent, Playwright assertions, Selenium `expected_conditions`, SikuliX `waitVanish`, PyImageSearch). +- Plan d'intégration en 3 étapes graduées (1j MVP / 2 sem couverture complète / 1 mois bench). + +**Réutilisation existant** : `OcrRoiChecker` réutilise le singleton EasyOCR de `TitleVerifier._get_ocr()` (zéro coût d'init). + +**Reproduction offline du bug step 10** : script `repro_bug_step10_validator.py` fourni + test pytest `test_validator_step10.py`, sur capture `visual_workflow_builder/backend/data/anchors/anchor_0438bd2d9bdd_1778161174_full.png`. + +**Wiring** : diff unified format insertion à `api_stream.py:3447-3582` derrière flag `RPA_VALIDATOR_V2_ENABLED=false` (default OFF, aucune régression flag off). + +**Budget latence démo 46 steps** : **+11s cumulés** (vs 33s perdus en pause/reprise step 10). + +**Effort intégration P0** : **8h** (création package 4h + tests 1.5h + patch api_stream 2h + smoke 30min). + +--- + +### 3.3. SPEC_POPUPS_CATALOGUE.md + +**Chemin** : `/home/dom/ai/rpa_vision_v3/docs/recherche/SPEC_POPUPS_CATALOGUE.md` +**Volume** : 1758 lignes après enrichissement. +**Recherches sources** : `AXE_D2_DIALOG_POPUP.md`, `AXE_D2_DEEP_POPUP_CHAIN.md`. + +**Contenu** : +- **Section 2bis — Catalogue compact (format Dom)** avec exactement 5 colonnes : `ID | Titre exact (FR/EN) | Appli source | Boutons attendus | Politique`. +- **Politique trichotomie stricte** : + - **`auto`** = Léa clique un bouton précis (action explicite définie) + - **`pause`** = Léa s'arrête et attend décision humaine + - **`skip`** = Léa ignore le modal (ne clique rien, ne s'arrête pas) +- **59 entrées catalogue** réparties en 5 catégories (A SYSTÈME, B NAVIGATEUR, C MÉTIER, D APP TIERS, E INCONNU) : + - **44 `pause`** : toute la catégorie A SYSTÈME + identification + suppression + warnings cliniques + - **10 `auto`** : save/confirm Easily, dialogs métier connus, dialogs disposables avec clic explicite + - **5 `skip`** : `browser-translate-prompt`, `easily-toast-saved`, `outlook-reminder`, `chrome-update`, `edge-update` +- 10 fiches détaillées des modaux critiques (UAC, Hello, SmartScreen, save unconfirmed, browser perms, etc.) avec capture-type ASCII. +- **Workflow VWB déclaratif** `expected_modal` (YAML + Pydantic schema) avec validateur `system_modals_cannot_be_overridden` qui **rejette toute politique ≠ `pause` sur préfixes `windows-` / `defender-`** — un workflow VWB ne peut pas forcer un UAC en auto. +- Snippet Python `KNOWN_DIALOGS` étendu (781 lignes, syntaxe valide `ast.parse` OK) avec champs `window_title`, `app_source`, `policy` (`auto`/`pause`/`skip`), `declarative_override: bool`. Helpers `get_metadata()` et `can_be_overridden()`. +- Tests offline pytest + protocole de capture. +- 7 précédents externes (Skyvern issue #69, browser-use issue #1996, Anthropic CU human takeover, OpenAI Operator watch mode, AutoIt/Sikuli, pywinauto, Selenium JS alerts). + +**Décisions tranchées par les agents** : +- `_handle_possible_popup` orphelin **supprimé** (0 site d'appel + antipattern Tab+Enter aveugle). +- `_handle_popup_vlm` actif **conservé mais simplifié** → devient client léger d'un nouvel endpoint `POST /api/v1/dialog/resolve`. +- DialogResolver côté **serveur** (pas client) pour mutualiser avec `dialog_handler.py` existant. + +**Couverture estimée** : **~85%** des modaux courants sans intervention humaine. Les 15% restants = pause supervisée par design healthtech. + +**Effort intégration** : **1j MVP** (P0 démo) → 1 sem couverture complète + tests fixtures (P1) → 1 mois bench injection + apprentissage catalogue (P2). + +--- + +## 4. Décisions ouvertes consolidées pour Dom + +Les 3 specs remontent des points qu'un agent ne peut pas trancher seul. Regroupés ici par thème. + +### 4.1. Transport / watchdog (8 décisions — `SPEC_TRANSPORT_CONTRAT.md` §12) + +| ID | Question | Recommandation auteur | +|---|---|---| +| D1 | Persistance `_retry_pending` au restart serveur ? | Non par défaut (cas extrême ; restart démo = redémarrage replay manuel) | +| D2 | Politique d'abandon par type d'action (click vs wait) ? | Différenciée : click MAX_RESENDS=2, extract_text MAX_RESENDS=1 | +| D3 | Retry des actions server-side (`extract_text`, `t2a_decision`) ? | Non (idempotence non garantie : LLM peut diverger) | +| D4 | Purge `_retry_pending` à la complétion workflow ? | **Oui** (recommandé) | +| D5 | `dedup_set` client : taille LRU + clé ? | LRU 50 entrées, clé = `action_id` + `attempt_id` | +| D6 | Génération `attempt_id` côté serveur ? | UUID4 court 8 chars, incrémenté à chaque resend | +| D7 | Backward-compat client v1 ↔ serveur v2 ? | Header `X-Replay-Protocol-Version` côté client | +| D8 | `cancel_in_flight` (annuler action en cours sur le client) ? | **Non** (recommandé) — trop de risques | + +### 4.2. Validator (7 actions où le signal "qui fait foi" reste flou — `SPEC_VALIDATOR_MATRICE.md` §10) + +| Action | Question | +|---|---| +| `paste_and_execute` | Vérif côté Léa Windows ou côté SSH VM (cas NoMachine pixel intermédiaire) ? | +| `screenshot_evidence` | `TitleBarChecker` suffit ou exiger netteté image ? | +| `pause_for_human` mode autonome | Aujourd'hui silencieusement ignorée (`api_stream.py:3011-3017`) — laisser ou changer ? | +| `t2a_decision = NA` | Verdict métier vs erreur LLM (hors-scope médical Claude) — qui décide ? | +| Tab déjà actif au moment du clic | Idempotence (SUCCESS) ou NO_VISUAL_CHANGE ? | +| `drag_drop_anchor` | Whitelist serveur mais pas de handler Léa — implémenter ou retirer du whitelist ? | +| Animations longues > 1s | `wait_after_action_ms` par type d'action vs généralisé ? | + +### 4.3. Popups (5 modaux ambigus — `SPEC_POPUPS_CATALOGUE.md` §10) + +| Modal | Politique proposée | Risque à valider | +|---|---|---| +| `easily-required-field` | `auto` | Peut masquer un bug de grounding réel | +| `outlook-reminder` | `skip` | Risque clinique de masquer un rappel pro | +| `chrome-update` / `edge-update` | `skip` | À confirmer si ne prend pas le focus après inactivité | +| `easily-clinical-warning` | `pause` non surchargeable | Volontairement strict, à valider | +| `browser-perm-microphone` (Easily dictée vocale) | `pause` | Déclaration globale `declared_dialogs` ou par-workflow ? | + +--- + +## 5. Plan d'attaque concret proposé (~3 jours homme cumulés) + +### Vague 1 (jour 1) — Transport + start Validator +- **Matin (3h30)** — Watchdog `replay_watchdog.py` + patches `api_stream.py`. Kill-switch `RPA_WATCHDOG_ENABLED=false` par défaut → activable progressivement. Tests pytest sans Windows. Smoke E2E réel. +- **Après-midi (4h)** — Validator MVP P0 : `PixelDiffChecker` + `OcrRoiChecker` + orchestrateur, sans `LlmJudgeChecker`. Flag `RPA_VALIDATOR_V2_ENABLED=false`. Test offline du bug step 10 sur fixture du 8 mai. + +### Vague 2 (jour 2) — Finir Validator + Popup chain MVP +- **Matin (4h)** — Compléter Validator : `TabActiveChecker`, `SaveSuccessChecker`, `DialogClosedChecker`, `JsonSchemaChecker`. Tests par type d'action. +- **Après-midi (4h)** — Chaîne popup MVP : signatures FR+EN, `ChangeDetector` + `DialogClassifier` + `DialogResolver` côté serveur. Endpoint `/api/v1/dialog/resolve`. Simplification `_handle_popup_vlm` client. + +### Vague 3 (jour 3) — Intégration et démo +- **Matin (3h)** — Coordination Validator → DialogResolver (handoff `UNEXPECTED_DIALOG`). Test bout-en-bout démo MOREL Catherine avec les 3 chantiers actifs. +- **Après-midi (3h)** — Bench latence sur démo réelle (cible : +20s overhead max sur 46 steps). Activation progressive des flags. Mise à jour `DETTE_TECHNIQUE.md` (DETTE-001, 008 closables). + +**Sortie attendue** : démo prochaine `Demo_urgence_3_db` qui tourne **sans** `static_result/static_text`, **sans** `cancel-replays.sh` manuel, **sans** pause humaine sur dialogs métier connus. + +--- + +## 6. Hors-périmètre de ces 3 specs (rappel) + +Ces 3 specs **ne traitent pas** : +- Migration grounding VLM (`AXE_A1`, `AXE_A2`, `AXE_A3` — Qwen3-VL, smart_resize, bench bbox). +- Bug capture client Y `mss.monitors[N]=2560×60` (`AXE_B5_D1_CAPTURE_REMOTE.md` — fix DPI 8 lignes en tête de `main.py`). +- Shadow learning / fine-tuning / memory store (`AXE_C_LEARNING_SHADOW.md`). +- Packaging / code signing / multi-tenant (`AXE_D4_MULTI_TENANT_DEPLOY.md`). +- Veille frameworks externes (`AXE_E_FRAMEWORKS_BENCHMARKS.md`). +- Bug recapture anchor VWB silencieuse (P0 — non couvert par cette vague). +- Bug skip ord 13 orchestration (P0 — non identifié, NOT REPRO 100%). + +Ces sujets sont couverts par les autres docs `docs/recherche/AXE_*.md`. Voir `SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` pour la carte d'ensemble. + +--- + +## 7. Cartographie complète des livrables de recherche + +### Vague 1 — Panorama 13 axes (23 mai 2026) +``` +docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md (synthèse croisée initiale) + +docs/recherche/AXE_A1_VLM_GROUNDING_SOTA.md (état art VLM grounding) +docs/recherche/AXE_A2_SMART_RESIZE_BBOX.md (DETTE-014/010/007/006) +docs/recherche/AXE_A3_BENCH_PROTOCOL.md (script bench reproductible) +docs/recherche/AXE_A4_OCR_TEMPLATE_PHASH.md (cascade déterministe) +docs/recherche/AXE_A5_SCREEN_TOKENIZATION.md (OmniParser / SoM) +docs/recherche/AXE_B1_REPLAY_TRANSPORT.md (SSE / WS / pull-poll) +docs/recherche/AXE_B2_VALIDATOR_PATTERN.md (Planner-Actor-Validator) +docs/recherche/AXE_B4_ORA_VS_REPLAY.md (autonomous vs déclaratif) +docs/recherche/AXE_B5_D1_CAPTURE_REMOTE.md (mss / DXGI / NoMachine) +docs/recherche/AXE_C_LEARNING_SHADOW.md (Shadow + FT + memory) +docs/recherche/AXE_D2_DIALOG_POPUP.md (chaîne dialog handling) +docs/recherche/AXE_D4_MULTI_TENANT_DEPLOY.md (packaging + code signing) +docs/recherche/AXE_E_FRAMEWORKS_BENCHMARKS.md (delta veille + benchmarks) +``` + +### Vague 2 — Approfondissement 3 axes replay (24 mai 2026) +``` +docs/recherche/AXE_B1_DEEP_WATCHDOG.md (watchdog production-ready) +docs/recherche/AXE_B2_DEEP_VALIDATOR.md (Validator package) +docs/recherche/AXE_D2_DEEP_POPUP_CHAIN.md (chaîne popup complète) +``` + +### Vague 3 — Specs opérationnelles (24 mai 2026) +``` +docs/recherche/SPEC_TRANSPORT_CONTRAT.md (contrat dispatch→ack→retry→orphan→resume) +docs/recherche/SPEC_VALIDATOR_MATRICE.md (matrice action → signal qui fait foi) +docs/recherche/SPEC_POPUPS_CATALOGUE.md (catalogue 59 entrées + politique auto/pause/skip) +``` + +### Vague 4 — Index (ce document) +``` +docs/recherche/INDEX_REPLAY_SPECS_2026-05-24.md (point d'entrée brief agent / humain) +``` + +--- + +*Index maintenu par Dom. À mettre à jour quand de nouvelles specs replay sont livrées ou quand une décision §4 est tranchée.* diff --git a/docs/recherche/JOURNAL_SEANCE1_MICRO_APPRENTISSAGE_LEA_2026-05-27.md b/docs/recherche/JOURNAL_SEANCE1_MICRO_APPRENTISSAGE_LEA_2026-05-27.md new file mode 100644 index 000000000..56bd345af --- /dev/null +++ b/docs/recherche/JOURNAL_SEANCE1_MICRO_APPRENTISSAGE_LEA_2026-05-27.md @@ -0,0 +1,109 @@ +# Journal seance 1 micro-apprentissage Lea - 2026-05-27 + +Competence cible: ouvrir le menu Demarrer / Recherche Windows. + +## Demo A - clic logo Windows + +- Session: `sess_20260527T170656_e16163` +- Methode humaine: clic gauche sur la barre des taches / logo Windows. +- Signal utile: + - clic gauche capture a `[755, 1556]`, + - transition observee vers `Rechercher` / `SearchHost.exe`, + - heartbeat avec `active_window_title = Rechercher`. +- Bruit: + - fenetre active initiale: `Acces vocal` (`VoiceAccess.exe`), + - clics de fin de session dans zone systeme / fenetre Lea, + - `window_capture` faux pour le clic barre des taches car la fenetre active `Acces vocal` mesure seulement 60 px de haut. +- Verdict: observation utile, pas preuve propre de competence. + +## Demo B - touche Windows + +- Session: `sess_20260527T171110_ca856a` +- Methode humaine: touche Windows. +- Signal utile: + - transition `Acces vocal` -> `Rechercher` / `SearchHost.exe`, + - heartbeat avec `active_window_title = Rechercher`. +- Bruit: + - aucun evenement clavier `Win` capture dans la trace, + - clics de fin de session dans zone systeme / fenetre Lea, + - `Acces vocal` reste la fenetre active parasite avant/apres le geste. +- Verdict: bonne observation de postcondition, mais capture clavier incomplete. + +## Demo C - Win+S + +- Session: `sess_20260527T171412_737571` +- Methode humaine: raccourci `Win+S`. +- Signal utile: + - transition `Acces vocal` -> `Rechercher` / `SearchHost.exe`, + - heartbeat avec `active_window_title = Rechercher`, + - fenetre `Rechercher` positionnee differemment de la demo B, ce qui confirme un contexte visuel distinct. +- Bruit: + - aucun evenement clavier `Win+S` capture dans la trace, + - clics de fin de session dans zone systeme / fenetre Lea, + - `Acces vocal` reste la fenetre active parasite avant/apres le geste, + - `screenshot_context` vide sur le focus `Rechercher` dans la session serveur. +- Verdict: bonne observation de postcondition, mais methode clavier non apprise explicitement. + +## Points techniques a corriger + +1. Capturer explicitement les touches systeme utiles (`Win`, `Win+S`, `Esc`) ou inferer proprement la methode a partir de la transition. +2. Filtrer les clics de controle de Lea / zone systeme apres la postcondition observee. +3. Ne pas utiliser `window_capture` quand `click_inside_window = false`; preferer coordonnees ecran + moniteur + postcondition. +4. Neutraliser ou ignorer `Acces vocal` comme fenetre parasite. +5. Ne pas promouvoir une competence clavier tant que la methode n'est pas tracee ou declaree par l'humain. + +## Correction appliquee apres Demo C + +- `agent_v0/agent_v1/core/captor.py`: emission de `key_combo ["win"]` sur relachement de la touche Windows seule, emission de `key_combo ["escape"]`, et annulation du `win` seul quand un vrai combo `Win+...` est capture. +- `agent_v0/deploy/windows_client/agent_v1/core/captor.py`: meme correction minimale pour le client Windows deploye. +- `agent_v0/server_v1/stream_processor.py`: conservation explicite de `Win` comme geste systeme actionnable, tout en filtrant encore `Ctrl`/`Alt`/`Shift` seuls. +- `agent_v0/server_v1/stream_processor.py`: ajout des waits post-raccourci pour `Win`, `Win+S` et `Escape`. +- `agent_v0/agent_v1/network/streamer.py`: priorisation des vrais types emis par le captor (`mouse_click`, `key_combo`, `text_input`, `mouse_scroll`) pour eviter une perte sous backpressure. +- `tests/unit/test_keyboard_system_keys.py`: couverture de `Win`, `Win+S`, `Escape`, filtrage serveur et priorite streamer. + +Tests executes: + +- `pytest tests/unit/test_keyboard_system_keys.py tests/unit/test_lea_message_contract.py tests/unit/test_lea_micro_preflight.py tests/unit/test_safety_checks_provider.py tests/integration/test_chat_window_templates.py -q` +- `pytest tests/unit/test_keyboard_system_keys.py tests/integration/test_streamer_buffer_and_purge.py -q` +- `pytest tests/unit/test_keyboard_system_keys.py tests/integration/test_stream_processor.py tests/integration/test_streamer_buffer_and_purge.py -q` +- `python3 -m py_compile agent_v0/agent_v1/core/captor.py agent_v0/deploy/windows_client/agent_v1/core/captor.py agent_v0/server_v1/stream_processor.py agent_v0/agent_v1/network/streamer.py tests/unit/test_keyboard_system_keys.py` +- `git diff --check` + +Risque restant: si Windows / NoMachine / pynput ne remonte toujours pas `Win+S`, il faudra ajouter un hook Windows-only sous flag. On ne doit pas inferer durablement `Win+S` uniquement depuis `SearchHost.exe`. + +## Deploiement runtime + +- `2026-05-27 17:29`: SCP vers `dom@192.168.1.11:C:/rpa_vision/agent_v1/`: + - `core/captor.py` + - `network/streamer.py` +- Sauvegardes Windows creees avec suffixe `.bak_codex_20260527_172924`. +- Verification distante par `findstr`: `_pending_standalone_win` present dans `captor.py`, priorites `mouse_click/key_combo/text_input/mouse_scroll` presentes dans `streamer.py`. +- Serveur streaming `:5005` relance avec `setsid`; PID observe `4124151`, endpoint `/health` OK. +- Healthcheck global: OK fonctionnel sur `:5005`, `:5004`, Ollama et SSH Windows; WARN attendu `qwen2.5vl:7b-rpa` non resident; FAIL non bloquant `systemd:rpa-streaming.service inactive` car serveur relance manuellement hors service systemd. +- `2026-05-27 18:43`: arret distant cible de l'arbre `run_agent_v1.py` via PowerShell/`taskkill /T`, avec fallback `Stop-Process -Force`; verification `remaining_count=0`. +- `tools/lea_healthcheck.py`: correction du diagnostic Windows pour compter les arbres de processus Lea, pas les processus bruts. Une relance normale peut produire un `pythonw.exe` parent et un `pythonw.exe` enfant pour une seule instance Lea. +- `2026-05-27 18:44`: relance Lea confirmee; healthcheck: `1 Lea instance tree(s), 2 run_agent_v1.py process(es)`, flux `/replay/next` actif. +- `2026-05-27 18:45`: micro-demo `sess_20260527T184533_8512ac`. + - OK: transition `Acces vocal` -> `Rechercher` / `SearchHost.exe`. + - OK: saisie captee dans `Rechercher`: `test` + espace + `lea apprentissage`. + - KO: pas de `key_combo`; raw_keys du premier texte montrent seulement `release s` puis `release cmd` avant la saisie. Conclusion: Windows/NoMachine/pynput avale les press de `Win+S` et ne remonte que les releases. +- `2026-05-27 18:49`: correction supplementaire dans `agent_v0/agent_v1/core/captor.py`: inference ciblee `release(s)` puis `release(cmd)` -> emission `key_combo ["win", "s"]`, avec suppression des releases du prochain `text_input`. +- Test ajoute: `test_release_only_windows_shortcut_is_inferred`. +- SCP Windows du captor corrige, sauvegarde `.bak_releaseonly_20260527_184903`, relance via tache `LeaInteractive`; healthcheck: `1 Lea instance tree(s), 2 run_agent_v1.py process(es)`. +- `2026-05-27 18:52`: micro-demo `sess_20260527T185155_98ad9a`. + - OK: `live_events.jsonl` contient `key_combo ["win", "s"]` avec `raw_keys` release-only (`s`, puis `cmd`). + - OK: transition `Acces vocal` -> `Rechercher` / `SearchHost.exe`. + - OK: la saisie arrive ensuite dans `Rechercher` sans polluer le premier `text_input` par les releases `Win+S`. + - Bruit restant: clics de fin de session dans `Rechercher`, zone systeme, puis `pythonw.exe`; a filtrer/rogner apres postcondition. +- `2026-05-27 18:55`: correction serveur dans `agent_v0/server_v1/stream_processor.py`: `_restore_user_events()` restaure aussi `key_combo` depuis `live_events.jsonl`. Sans cela, le fichier consolide `streaming_sessions/*.json` perdait `Win+S` malgre la capture brute correcte. +- Test ajoute: `tests/integration/test_stream_processor.py::TestStreamProcessor::test_restore_user_events_keeps_key_combo`. +- Session consolidee reparee: `data/training/live_sessions/streaming_sessions/sess_20260527T185155_98ad9a.json` contient maintenant 10 events, dont le premier est `key_combo ["win", "s"]`. +- `2026-05-27 18:58`: serveur streaming rebascule sous `systemd --user rpa-streaming.service` au lieu du lancement manuel; endpoint `/health` OK. Healthcheck global: `WARN` uniquement (`qwen2.5vl:7b-rpa` non resident, tache Windows `Ready` mais un arbre Lea actif). + +## Marqueur de succes retenu + +La competence `ouvrir le menu Demarrer` est consideree observee quand: + +- la fenetre active devient `Rechercher` ou `SearchHost.exe`, +- ou le champ `Rechercher` est visible dans un heartbeat, +- sans se baser uniquement sur un clic ou une coordonnee. diff --git a/docs/recherche/RAPPORT_PILOTAGE_CORE_JUDGE_VLM_2026-05-24.md b/docs/recherche/RAPPORT_PILOTAGE_CORE_JUDGE_VLM_2026-05-24.md new file mode 100644 index 000000000..12ba9708b --- /dev/null +++ b/docs/recherche/RAPPORT_PILOTAGE_CORE_JUDGE_VLM_2026-05-24.md @@ -0,0 +1,79 @@ +# Rapport d'Analyse : Pilotage de l'Agent Léa & Architecture "Juge VLM" + +**Date :** 2026-05-24 +**Objet :** Optimisation du Core Central et fiabilisation des interactions OS Windows. +**Statut :** Recherche & Suggestions (Zéro modification de code source). + +--- + +## 1. Diagnostic de l'Architecture de Pilotage Actuelle + +L'analyse du core central (`core/execution/observe_reason_act.py` et `agent_v0/agent_v1/core/executor.py`) révèle une architecture de type **"Open-Loop Execution"** (Exécution en boucle ouverte) : + +1. **Le Serveur (Cerveau)** : Envoie une commande atomique (ex: `click` à `x,y`). +2. **L'Agent (Muscle)** : Exécute la commande et renvoie un rapport immédiat. +3. **Le Critic (Post-Vérification)** : Compare les screenshots avant/après via pHash ou vision. + +### Points de friction identifiés : +* **Asynchronisme de l'OS** : Windows 11 est asynchrone. L'agent renvoie son rapport de succès *avant* que l'effet visuel ne soit stabilisé (ex: animation du menu Démarrer ou ouverture du dialogue "Enregistrer sous"). +* **Frustation du Critic** : Le serveur reçoit une image "en transition" (noire, floue ou incomplète), échoue à valider l'action, et ordonne un `RETRY`. Cela crée la boucle infinie observée sur le `start_button`. +* **Cécité Contextuelle** : L'agent local n'a pas d'autonomie pour dire "Attends, une popup bloque le chemin" ; il obéit aveuglément aux coordonnées reçues. + +--- + +## 2. Deep Dive : Le Concept de "Juge VLM" + +Le **Juge VLM** est l'évolution majeure des frameworks SOTA de 2026 (Agent-S3, Coasty). Il consiste à découpler la **réussite technique** (le clic a été fait) de la **réussite sémantique** (l'objectif est atteint). + +### 2.1. Fonctionnement du Juge VLM +Au lieu de renvoyer des coordonnées, le Juge VLM répond à des questions binaires ou sémantiques sur l'état du système. Il intervient à trois niveaux : + +#### A. Le Juge de Pré-Condition (Visual Guard) +Avant d'exécuter l'action, l'agent interroge le VLM local (Ollama `qwen3-vl:8b`) : +* **Prompt** : *"Est-ce que le bouton 'Enregistrer' est visible et cliquable sans obstacle ?"* +* **Utilité** : Évite de cliquer dans le vide si un dialogue n'est pas encore apparu ou si une fenêtre UAC bloque l'écran. + +#### B. Le Juge de Stabilisation (Visual Anchor) +Après l'action, l'agent ne rend pas la main au serveur tant que le Juge n'a pas validé la transition. +* **Prompt** : *"Le menu Démarrer est-il maintenant ouvert ? Répondre par OUI ou NON."* +* **Utilité** : Absorbe la latence de l'OS. L'agent fait du polling interne jusqu'au "OUI" ou au timeout. Le serveur ne voit que le résultat final stabilisé. + +#### C. Le Juge de Conformité (Semantic Critic) +C'est le niveau le plus élevé, utilisé pour les dialogues complexes. +* **Prompt** : *"L'utilisateur a demandé d'enregistrer le fichier sous le nom 'test.txt'. Est-ce que le dialogue actuel confirme que le fichier sera enregistré dans le dossier 'Documents' ?"* +* **Utilité** : Détecte les erreurs métier que le pixel-matching (pHash) ignore totalement. + +### 2.2. Intégration avec le pattern "Best-of-N" +Le Juge VLM permet d'implémenter le **Best-of-N rollouts**. Si le Juge dit "NON" après une action, l'agent peut tenter localement une variante (ex: presser `Enter` au lieu de cliquer sur `Enregistrer`) sans que le serveur n'ait à gérer la complexité du retry. + +--- + +## 3. Suggestions de Pilotage pour "Léa V3" + +### Suggestion 1 : L'Autonomie de la "Dernière Milliseconde" +L'agent local (`executor.py`) doit cesser d'être un simple terminal. Il doit intégrer une **boucle de rétroaction locale**. +* **Idée** : Si le serveur demande un clic sur "Enregistrer", l'agent local vérifie par OCR/Vision rapide que le texte "Enregistrer" est bien sous le curseur avant de cliquer. S'il ne l'est pas, il attend 500ms et ré-essaie localement. + +### Suggestion 2 : Normalisation sémantique des Dialogues +Utiliser des signatures sémantiques pour les fenêtres Windows. +* **Idée** : Créer un dictionnaire de "DialogStates" (ex: `STATE_SAVE_AS`, `STATE_START_MENU`). Chaque état est défini par un ensemble de mots-clés et d'icônes. Le pilotage devient une suite de transitions d'états : `IDLE -> STATE_START_MENU -> STATE_APP_OPENED`. + +### Suggestion 3 : La Cascade de Modèles (Mixture-of-Grounding) +* **Rapide/Local** : Pour le mouvement de souris et la détection de texte simple (EasyOCR). +* **Lourd/Serveur** : Pour la résolution de cibles complexes (UI-TARS-2 / InfiGUI). +* **Cognitif/Ollama** : Pour le "Juge VLM" et la validation de l'intention. + +--- + +## 4. Conclusion : Pourquoi Léa boucle sur le `start_button` ? + +D'après cette analyse, le blocage est une **crise de confiance entre l'Agent et le Serveur** : +1. L'Agent fait le fallback `Win`. +2. L'Agent envoie le screenshot trop tôt (pendant l'animation Windows). +3. Le Serveur (Critic) voit un écran flou, ne trouve pas le menu, et dit "Échec, recommence". +4. L'Agent reçoit l'ordre de recommencer et boucle. + +**La solution recommandée (sans code)** : Introduire une pause de stabilisation visuelle pilotée par un Juge VLM local qui ne libère l'agent que lorsque le menu est "vrai" à l'écran. + +--- +*Ce rapport est une étude technique destinée à orienter les futures itérations de l'architecture Léa. Aucune modification n'a été apportée aux fichiers sources du projet.* diff --git a/docs/recherche/SPEC_POPUPS_CATALOGUE.md b/docs/recherche/SPEC_POPUPS_CATALOGUE.md new file mode 100644 index 000000000..227f5cd8f --- /dev/null +++ b/docs/recherche/SPEC_POPUPS_CATALOGUE.md @@ -0,0 +1,1758 @@ +# SPEC POPUPS — Catalogue opérationnel modaux + +**Date :** 2026-05-24 +**Auteur :** agent recherche dispatché (Claude Opus 4.7 1M) +**Périmètre :** catalogue **opérationnel** des modaux réels rencontrés ou anticipables côté Léa (Windows, Easily Assure, Edge, Office, apps tierces). Décision binaire **auto / pause humaine** par entrée. Pas une étude théorique — un outil de référence à brancher dans `core/dialog/`. +**Statut :** lecture seule. Aucune modification de code dans ce livrable. Toute mise en code → décision Dom. +**Prérequis lecture :** +- `docs/recherche/AXE_D2_DIALOG_POPUP.md` (taxonomie + matrice modal→action originelle). +- `docs/recherche/AXE_D2_DEEP_POPUP_CHAIN.md` (chaîne `ChangeDetector → DialogClassifier → DialogResolver`). +- `docs/recherche/AXE_B2_DEEP_VALIDATOR.md` (interface `Verdict` / `FailureCategory.UNEXPECTED_DIALOG`). + +--- + +## 1. TL;DR + Politique générale healthtech + +### 1.1. Les 7 règles d'or (immutables) + +| # | Règle | Justification | +|---|---|---| +| 1 | **Tout modal SYSTÈME → pause humaine.** Jamais d'auto-accept UAC / Hello / SmartScreen / Defender / Driver. | RGPD + HDS + risque de répudiation. Un agent qui élève des privilèges seul est inadmissible en healthtech. | +| 2 | **Tout modal d'identification (creds, PIN, biométrie, MFA) → pause humaine.** | Interaction physique requise par construction (Hello, FIDO2). Anti-pattern absolu : cliquer "Annuler" pour passer outre. | +| 3 | **Tout modal de SUPPRESSION / IRRÉVERSIBLE → pause humaine.** Sauf déclaration explicite workflow `expected_modal.action=click_button`. | Une fois supprimé un dossier patient = catastrophe. La déclaration force la responsabilité côté concepteur du workflow. | +| 4 | **Tout modal INCONNU → pause humaine + log + enrichissement catalogue post-démo.** | Anti-pattern proscrit : auto-dismiss aveugle "au cas où". `feedback_failure_is_learning.md`. | +| 5 | **JAMAIS de raccourci système inventé.** Pas de `Escape`, `Win+R`, `Ctrl+X`, `Tab+Enter` ad hoc pour "fermer un popup". | `feedback_100pct_visual.md`. Le récit "Léa comprend visuellement" se démonte en une démo. | +| 6 | **Audit trail systématique.** Tout passage par `DialogResolver` génère un `DialogEvent` persisté (screenshot + OCR + politique + action). | Traçabilité HDS, debug post-démo, alimentation catalogue. | +| 7 | **Cascade verticale OCR → signatures → KNOWN_DIALOGS → VLM compact → ASK_HUMAN.** Le VLM principal n'est JAMAIS appelé pour classifier un modal. | Latence (Qwen2.5-VL = 8-11 s par appel). On garde le budget VLM pour la résolution UI métier. | + +### 1.2. En une phrase + +> Léa auto-traite uniquement ce qu'elle a déjà rencontré ET catalogué côté **métier** ; tout le reste (système, identification, suppression, inconnu) part en pause supervisée, avec un message FR clair en bulle Léa. + +### 1.2.bis. Trichotomie stricte des politiques (`auto` / `pause` / `skip`) + +Chaque modal du catalogue est résolu via **une seule** des trois politiques suivantes : + +- **`auto`** — Léa clique un bouton précis catalogué (action déterministe explicite). Ex. : `easily-overwrite-file` → click `Oui`. +- **`pause`** — Léa s'arrête et attend décision humaine (bulle FR + audit). Toujours utilisé pour SYSTÈME / IDENTIFICATION / SUPPRESSION / INCONNU. +- **`skip`** — Léa **ignore** le modal sans cliquer et poursuit le workflow. Réservé aux toasts non bloquants et notifications passagères qui disparaissent seules. + +Bascule conditionnelle : `pause` → `auto` est possible si le workflow VWB déclare `expected_modal` avec `policy=auto` (champ `declarative_override`). L'inverse n'est jamais autorisé pour les catégories SYSTÈME. + +### 1.3. Couverture cible + +- ~85 % des modaux courants résolus sans humain (sauvegarde, écrasement, OK trivial déclarés). +- ~15 % en pause supervisée (UAC, Hello, SmartScreen, browser permissions non déclarées, OK suspect, inconnu) — **par design**, pas par défaut. + +--- + +## 2. Matrice décision par défaut (compacte) + +| Catégorie | Sécurité (UAC/SmartScreen) | Identification (Hello/CredUI/MFA) | Suppression irréversible | Métier ordinaire (save, overwrite) | Notification non bloquante (toast) | +|---|---|---|---|---|---| +| **Décision défaut** | **pause + escalade** | **pause + escalade** | **pause** sauf workflow déclaratif | **auto** via `KNOWN_DIALOGS` | **auto-dismiss** (clic croix) ou ignore | +| Policy enum | `ESCALATE_SECURITY` | `ESCALATE_SECURITY` | `ASK_HUMAN` | `DECLARATIVE` | `AUTO_DISMISS` | +| Action concrète | `on_pause(event)` + audit critique | identique + tip pré-démo | `on_pause(event)` | InfiGUI/OCR click bouton catalogué | clic visuel croix détectée | +| Latence cible | < 200 ms détection | < 200 ms | < 200 ms | < 200 ms + 3 s click | < 200 ms | + +--- + +## 2bis. Catalogue compact (4 colonnes Dom) + +> **Sémantique stricte des politiques** (rappel) : +> - **`auto`** — Léa clique un bouton précis défini par le catalogue (action explicite). Ex. : `Enregistrer`, `Oui`, `Plus tard`, croix de fermeture. +> - **`pause`** — Léa s'arrête, affiche bulle FR, attend décision humaine. **Toujours** pour SYSTÈME (UAC/Hello/SmartScreen/CredUI/BitLocker), IDENTIFICATION, SUPPRESSION, CLINICAL-WARNING, INCONNU. +> - **`skip`** — Léa ne clique rien, ne s'arrête pas, poursuit la suite du workflow. Réservé aux toasts/notifications **véritablement non bloquants** qui disparaissent seuls. +> - Mention `(auto si workflow déclare expected_modal)` = politique de base = `pause`, surchargeable en `auto` via déclaration explicite VWB (`declarative_override=True`). +> +> Aucune entrée SYSTÈME ne peut jamais être `auto` ni `skip`, même avec déclaration workflow (UI VWB grise ces options). + +### Colonnes + +| ID | Titre exact (FR / EN) | Appli source | Boutons attendus | Politique | +|---|---|---|---|---| +| `windows-uac-elevation` | Contrôle de compte d'utilisateur / User Account Control | Windows (consent.exe) | Oui / Non (ou champ password admin) | `pause` | +| `windows-hello-pin` | Sécurité Windows / Windows Security | Windows (Windows Hello — CredentialUIBroker.exe) | Champ PIN + OK / Annuler | `pause` | +| `windows-hello-fingerprint` | Sécurité Windows / Windows Security | Windows (Windows Hello — biometric service) | (interaction physique) Annuler | `pause` | +| `windows-hello-face` | Sécurité Windows / Windows Security | Windows (Windows Hello Face — WinBioSvc) | (interaction physique) Annuler | `pause` | +| `windows-hello-security-key` | Sécurité Windows / Windows Security | Windows (WebAuthn / FIDO2) | (interaction physique) Annuler | `pause` | +| `defender-smartscreen-app` | Windows a protégé votre PC / Windows protected your PC | Windows (smartscreen.exe) | Informations complémentaires -> Exécuter quand même / Ne pas exécuter | `pause` | +| `defender-smartscreen-url` | Site signalé comme non sûr / This site has been reported as unsafe | Microsoft Edge (SmartScreen URL filter) | Retour à la sécurité / Continuer | `pause` | +| `windows-defender-threat` | Sécurité Windows — Protection contre les menaces / Windows Security — Threat protection | Windows Defender (MsMpEng.exe / SecHealthUI.exe) | Mettre en quarantaine / Autoriser | `pause` | +| `windows-credui-prompt` | Sécurité Windows / Windows Security | Windows (Credential Manager — CredentialUIBroker.exe, ClassName `Credential Dialog Xaml Host`) | Champ user/pass + OK / Annuler | `pause` | +| `windows-bitlocker-key` | Chiffrement de lecteur BitLocker / BitLocker Drive Encryption | Windows (BitLocker — fvenotify.exe) | Champ clé + Déverrouiller | `pause` | +| `windows-update-restart` | Windows Update — Redémarrage requis / Windows Update — Restart required | Windows (Windows Update — MoUsoCoreWorker.exe) | Redémarrer maintenant / Plus tard | `pause` | +| `windows-firewall-allow` | Alerte de sécurité Windows / Windows Security Alert | Windows (Pare-feu Defender — MpDefenderCoreService.exe) | Autoriser l'accès / Annuler | `pause` | +| `windows-driver-install` | Sécurité de Windows / Windows Security | Windows (Plug and Play — drvinst.exe) | Installer / Ne pas installer | `pause` | +| `windows-printer-setup` | Ajouter une imprimante / Add a printer | Windows (Settings — printui.exe / SystemSettings.exe) | Configurer / Annuler | `pause` | +| `windows-network-location` | Réseaux / Networks (panneau latéral) | Windows (Network Connection Flyout) | Domestique / Bureau / Public | `pause` | +| `browser-perm-camera` | (pas de barre de titre — bulle ancrée à la URL bar) | Microsoft Edge / Chrome | Autoriser / Bloquer | `pause` (`auto` si workflow déclare `expected_modal`) | +| `browser-perm-microphone` | (pas de barre de titre — bulle ancrée à la URL bar) | Microsoft Edge / Chrome | Autoriser / Bloquer | `pause` (`auto` si workflow déclare `expected_modal`) | +| `browser-perm-notifications` | (pas de barre de titre — bulle ancrée à la URL bar) | Microsoft Edge / Chrome | Autoriser / Bloquer | `pause` (`auto` si workflow déclare `expected_modal`) | +| `browser-perm-location` | (pas de barre de titre — bulle ancrée à la URL bar) | Microsoft Edge / Chrome | Autoriser / Bloquer | `pause` | +| `browser-perm-midi` | (pas de barre de titre — bulle ancrée à la URL bar) | Microsoft Edge / Chrome | Autoriser / Bloquer | `pause` | +| `browser-perm-clipboard` | (pas de barre de titre — bulle ancrée à la URL bar) | Microsoft Edge / Chrome | Autoriser / Bloquer | `pause` (`auto` si workflow déclare `expected_modal`) | +| `browser-cert-select` | Sélectionner un certificat / Select a certificate | Microsoft Edge / Chrome (mTLS — CryptoAPI) | Liste certificats + OK / Annuler | `pause` | +| `browser-basic-auth` | Se connecter / Sign in | Microsoft Edge / Chrome (HTTP Basic Auth dialog natif) | Champ user/pass + Se connecter / Annuler | `pause` (`auto` si vault Léa + workflow déclare `expected_modal`) | +| `browser-save-password` | (pas de barre de titre — bulle Password Manager ancrée à la URL bar) | Microsoft Edge / Chrome (Password Manager) | Enregistrer / Jamais | `pause` | +| `browser-download-confirm` | (pas de barre de titre — barre de téléchargement bas/haut écran) | Microsoft Edge / Chrome (Downloads bar) | Conserver / Supprimer / Ouvrir | `pause` (`auto` si workflow déclare `expected_modal`) | +| `browser-unsafe-page` | Votre connexion n'est pas privée / Your connection is not private | Microsoft Edge / Chrome (interstitial SSL) | Retour à la sécurité / Avancé -> Continuer | `pause` | +| `browser-beforeunload` | (pas de barre de titre — dialog JS modal au centre) | Microsoft Edge / Chrome (window.onbeforeunload) | Quitter / Annuler | `pause` (`auto` si workflow déclare `expected_modal`) | +| `browser-print-dialog` | Imprimer / Print | Microsoft Edge / Chrome (boîte print intégrée) | Imprimer / Annuler | `pause` (`auto` si workflow déclare `expected_modal`) | +| `browser-translate-prompt` | (pas de barre de titre — bulle ancrée à la URL bar) | Microsoft Edge / Chrome (Translate) | Traduire / Non | `skip` | +| `browser-page-unresponsive` | La page ne répond pas / Page Unresponsive | Microsoft Edge / Chrome (crash dialog) | Attendre / Quitter | `pause` | +| `browser-popup-blocked` | (pas de barre de titre — icône bloqué dans la URL bar) | Microsoft Edge / Chrome (popup blocker) | Toujours autoriser / Continuer à bloquer | `pause` | +| `browser-cookies-banner` | (pas de barre de titre — bandeau site web in-page) | Site Web (HTML/JS in-page banner) | Accepter / Refuser / Personnaliser | `pause` (`auto` si workflow déclare `expected_modal`) | +| `easily-save-unconfirmed` | Easily Assure — Confirmation | Easily Assure | Enregistrer / Ne pas enregistrer / Annuler | `auto` (click `Enregistrer`) | +| `easily-save-as` | Enregistrer sous / Save As | Easily Assure | Champ nom + Enregistrer / Annuler | `auto` (click `Enregistrer`) | +| `easily-overwrite-file` | Confirmer l'enregistrement / Confirm Save As | Easily Assure | Oui / Non | `auto` (click `Oui`) | +| `easily-confirm-action` | Confirmer / Confirm | Easily Assure | Oui / Non | `auto` (click `Oui`) | +| `easily-delete-confirm` | Easily Assure — Suppression | Easily Assure | Oui / Non (ou Supprimer / Annuler) | `pause` (`auto` si workflow déclare `expected_modal` avec `confirm=true`) | +| `easily-send-confirm` | Easily Assure — Envoi | Easily Assure | Envoyer / Annuler | `pause` (`auto` si workflow déclare `expected_modal`) | +| `easily-session-expired` | Easily Assure — Session | Easily Assure | OK + redirection login | `pause` | +| `easily-required-field` | Easily Assure — Validation | Easily Assure | OK | `auto` (click `OK` puis re-grounding du champ manquant) | +| `easily-duplicate-ipp` | Easily Assure — Doublon patient | Easily Assure | Continuer / Annuler / Fusionner | `pause` | +| `easily-clinical-warning` | Easily Assure — Avertissement clinique | Easily Assure | OK / Continuer | `pause` | +| `easily-toast-saved` | (pas de barre de titre — toast in-app coin bas/haut) | Easily Assure (toast non bloquant) | (aucun, disparaît seul) | `skip` | +| `easily-onboarding-tip` | Easily Assure — Astuce | Easily Assure (overlay onboarding) | OK / Suivant / Plus tard | `auto` (click `Plus tard` ou croix) | +| `easily-help-popup` | Aide Easily / Easily Help | Easily Assure (panneau aide) | Suivant / Fermer | `auto` (click `Fermer` ou croix) | +| `app-error-ok` | Erreur / Error | Application métier (générique — Win32 MessageBox) | OK | `auto` (click `OK` puis log) | +| `app-error-suspect` | Erreur / Error (texte contient `irréversible` / `cannot be undone`) | Application métier (générique — blocklist `SUSPECT_TOKENS`) | OK / Annuler | `pause` | +| `office-enable-content` | Avertissement de sécurité — Macros / Security Warning — Macros (bandeau jaune) | Microsoft Word / Excel / PowerPoint (WINWORD.EXE / EXCEL.EXE / POWERPNT.EXE) | Activer le contenu / Annuler | `pause` | +| `word-merge-conflict` | Microsoft Word — Conflit de fusion / Microsoft Word — Merge Conflict | Microsoft Word (WINWORD.EXE) | Conserver la mienne / Conserver leur / Fusionner | `pause` | +| `outlook-reminder` | Rappel — 1 / Reminder — 1 | Microsoft Outlook (OUTLOOK.EXE) | Ignorer / Reporter | `skip` | +| `pdf-open-confirm` | (pas de barre de titre — bulle ancrée à la URL bar / barre de téléchargement) | Microsoft Edge / Chrome (file handler PDF) | Ouvrir / Enregistrer / Annuler | `pause` (`auto` si workflow déclare `expected_modal`) | +| `adobe-reader-update` | Adobe Acrobat Reader — Mise à jour / Adobe Acrobat Reader — Update | Adobe Acrobat Reader DC (AcroRd32.exe / Acrobat.exe) | Installer / Plus tard | `auto` (click `Plus tard`) | +| `chrome-update` | (pas de barre de titre — bulle dans le menu Chrome `⋮`) | Google Chrome (chrome.exe — auto-updater) | Relancer / Plus tard | `skip` | +| `edge-update` | (pas de barre de titre — bulle dans le menu Edge `...`) | Microsoft Edge (msedge.exe — auto-updater) | Redémarrer / Plus tard | `skip` | +| `citrix-receiver-cert` | Citrix Workspace / Citrix Workspace App | Citrix Workspace (CDViewer.exe / SelfService.exe) | OK / Annuler | `pause` | +| `citrix-file-access` | Sécurité Citrix Workspace / Citrix Workspace Security | Citrix Workspace (HDX File Access) | Autoriser / Refuser | `pause` | +| `nomachine-reconnect` | NoMachine — Connexion perdue / NoMachine — Connection lost | NoMachine viewer (nxplayer.exe) | Reconnecter / Quitter | `pause` | +| `nomachine-clipboard-fail` | NoMachine — Notification (toast bas écran) | NoMachine viewer (nxplayer.exe — clipboard sync) | OK | `auto` (click `OK` puis fallback ydotool) | +| `unknown-dialog` | (variable — non catalogué) | (variable — non identifiée) | (variable) | `pause` | + +**Bascules `auto-dismiss` → décision finale** (réf. catalogue § 3) : + +- `browser-translate-prompt` -> **`skip`** (bulle Chrome qui disparaît si on ne fait rien après quelques secondes ; cliquer `Non` ajoute un état d'opt-out persistant indésirable). +- `easily-toast-saved` -> **`skip`** (toast non bloquant qui disparaît seul, ne mérite aucun clic). +- `easily-onboarding-tip` -> **`auto`** (clic explicite `Plus tard` / croix nécessaire — sinon overlay reste affiché). +- `easily-help-popup` -> **`auto`** (clic explicite `Fermer` / croix). +- `app-error-ok` -> **`auto`** (Win32 MessageBox bloquante, exige clic OK pour rendre la main). +- `outlook-reminder` -> **`skip`** (la fenêtre reste affichée mais ne bloque pas Léa, qui poursuit ; voir question ouverte § 10). +- `adobe-reader-update` -> **`auto`** (modal bloquant le focus Reader, clic `Plus tard` requis). +- `chrome-update` -> **`skip`** (bulle de menu non bloquante, aucun clic nécessaire pour continuer). +- `edge-update` -> **`skip`** (idem Chrome). +- `nomachine-clipboard-fail` -> **`auto`** (modal qui bloque la session NoMachine, clic OK requis avant fallback ydotool). + +--- + +## 3. Catalogue principal — ~55 entrées sur 5 catégories + +**Légende colonnes** : +- **ID** : kebab-case stable, à utiliser comme clé dans `KNOWN_DIALOGS` et `expected_modal.id` workflow VWB. +- **Cat** : A=SYSTÈME, B=NAVIGATEUR, C=MÉTIER, D=APP TIERS, E=INCONNU. +- **Signature FR** / **Signature EN** : 2-3 patterns texte à matcher (lowercase, ` in ocr_text.lower()`). +- **Boutons** : labels exacts vus à l'écran (ordre = ordre d'apparition gauche→droite). +- **Trigger** : à quel moment ça apparaît dans un workflow. +- **Décision** : `auto` / `pause` / `déclaratif` (workflow VWB peut basculer `pause` → `auto` via `expected_modal`). +- **Action auto** / **Message pause** : selon décision. +- **Source** : référence externe (URL) ou interne (capture/observation). + +### 3.A. SYSTÈME Windows — toujours pause humaine + +| ID | Cat | Signature FR | Signature EN | Boutons | Trigger | Décision | Action auto | Message pause | Source | +|---|---|---|---|---|---|---|---|---|---| +| `windows-uac-elevation` | A | "contrôle de compte d'utilisateur", "voulez-vous autoriser cette application" | "user account control", "do you want to allow this app" | Oui / Non (ou champ password admin) | install, `runas`, MSI, modif système | **pause** | — | "Windows demande l'élévation des droits administrateur. Merci de cliquer Oui ou Non, j'attends." | [Microsoft UAC docs](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/user-account-control/how-it-works) | +| `windows-hello-pin` | A | "windows hello", "saisissez votre code pin", "entrez votre pin" | "windows hello", "enter your pin" | Champ PIN + OK / Annuler | déverrouillage app, élévation Hello, WebAuthn site | **pause** | — | "Windows Hello demande votre code PIN. Merci de le saisir physiquement, j'attends." | `feedback_auth_dialogs_runtime.md` | +| `windows-hello-fingerprint` | A | "touchez le capteur d'empreintes", "analysez votre doigt", "vérification de votre identité" | "use your fingerprint", "touch the fingerprint sensor" | (interaction physique) | Chrome auto-fill, app sensible | **pause** | — | "Windows Hello demande votre empreinte digitale. Posez votre doigt sur le lecteur, j'attends." | [Token2 FIDO2 doc](https://www.token2.com/site/page/understanding-fido2-authentication-across-different-operating-systems-and-browsers) | +| `windows-hello-face` | A | "regardez la caméra", "reconnaissance faciale" | "look at the camera", "face recognition" | (interaction physique) | login Windows / app | **pause** | — | "Windows Hello demande votre reconnaissance faciale. Regardez la caméra, j'attends." | [Microsoft Hello](https://learn.microsoft.com/en-us/windows/security/identity-protection/hello-for-business/) | +| `windows-hello-security-key` | A | "insérez votre clé de sécurité", "appuyez sur votre clé" | "insert your security key", "touch your security key" | (interaction physique) | WebAuthn FIDO2 | **pause** | — | "Windows demande votre clé de sécurité FIDO2. Insérez/touchez la clé physique, j'attends." | [Microsoft WebAuthn](https://learn.microsoft.com/en-us/windows/security/identity-protection/hello-for-business/webauthn-apis) | +| `defender-smartscreen-app` | A | "windows a protégé votre pc", "smartscreen a empêché", "éditeur inconnu" | "windows protected your pc", "smartscreen prevented", "unknown publisher" | Informations complémentaires → Exécuter quand même / Ne pas exécuter | premier lancement exe non signé (Léa) | **pause** | — | "Windows SmartScreen bloque l'application. Cliquez 'Informations complémentaires' puis 'Exécuter quand même' si vous validez, j'attends." | [MSP360 KB](https://kb.msp360.com/backup/warnings/ms-defender-smart-screen) | +| `defender-smartscreen-url` | A | "smartscreen a signalé", "site potentiellement dangereux" | "smartscreen reported", "potentially unsafe site" | Retour à la sécurité / Continuer | navigation URL suspecte | **pause** | — | "SmartScreen signale ce site comme suspect. Validez la suite manuellement." | [The Windows Club](https://www.thewindowsclub.com/microsoft-defender-smartscreen-prevented-an-unrecognized-app-from-starting) | +| `windows-defender-threat` | A | "menace détectée", "windows defender", "logiciel malveillant" | "threat detected", "malware detected" | Mettre en quarantaine / Autoriser | scan AV en cours | **pause** | — | "Windows Defender signale une menace. Validez l'action manuellement." | [Microsoft Defender](https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/) | +| `windows-credui-prompt` | A | "sécurité windows", "connectez-vous à votre compte", "entrer les informations d'identification" | "windows security", "sign in to your account", "enter your credentials" | Champ user/pass + OK / Annuler | accès partage réseau, app entreprise | **pause** | — | "Windows demande des identifiants. Saisissez-les manuellement, j'attends." | `system_dialog_guard.py:147` ClassName `Credential Dialog Xaml Host` | +| `windows-bitlocker-key` | A | "clé de récupération bitlocker", "déverrouiller le lecteur" | "bitlocker recovery key", "unlock drive" | Champ clé + Déverrouiller | démarrage chiffré, dépannage | **pause** | — | "BitLocker demande la clé de récupération. Saisissez-la manuellement." | [Microsoft BitLocker](https://learn.microsoft.com/en-us/windows/security/operating-system-security/data-protection/bitlocker/) | +| `windows-update-restart` | A | "redémarrer maintenant", "mises à jour disponibles", "redémarrage requis" | "restart now", "updates available" | Redémarrer maintenant / Plus tard | Windows Update push | **pause** | — | "Windows propose un redémarrage pour mise à jour. À gérer hors démo, j'attends." | [Microsoft WU](https://learn.microsoft.com/en-us/windows/deployment/update/) | +| `windows-firewall-allow` | A | "pare-feu windows defender", "autoriser l'accès" | "windows defender firewall", "allow access" | Autoriser l'accès / Annuler | premier réseau d'une app | **pause** | — | "Pare-feu Windows demande l'autorisation réseau. À valider manuellement." | [Microsoft Firewall](https://learn.microsoft.com/en-us/windows/security/operating-system-security/network-security/windows-firewall/) | +| `windows-driver-install` | A | "installer ce pilote", "signature numérique du pilote", "ce pilote n'est pas signé" | "install this driver", "driver signature" | Installer / Ne pas installer | branchement périphérique nouveau | **pause** | — | "Windows propose d'installer un pilote. À valider manuellement." | [Microsoft Driver Signing](https://learn.microsoft.com/en-us/windows-hardware/drivers/install/driver-signing) | +| `windows-printer-setup` | A | "ajouter une imprimante", "nouvelle imprimante détectée" | "add a printer", "new printer detected" | Configurer / Annuler | branchement imprimante | **pause** | — | "Windows propose de configurer une imprimante. À gérer manuellement." | observation interne | +| `windows-network-location` | A | "type d'emplacement réseau", "domestique / bureau / public" | "network location type" | Domestique / Bureau / Public | premier réseau (wifi) | **pause** | — | "Windows demande le type de réseau. À sélectionner manuellement." | [Microsoft Networking](https://learn.microsoft.com/en-us/windows/win32/wlanapi/) | + +### 3.B. NAVIGATEUR (Chrome/Edge/Firefox) — pause par défaut, déclaratif possible + +| ID | Cat | Signature FR | Signature EN | Boutons | Trigger | Décision | Action auto | Message pause | Source | +|---|---|---|---|---|---|---|---|---|---| +| `browser-perm-camera` | B | "souhaite utiliser votre caméra" | "wants to use your camera" | Autoriser / Bloquer | premier accès media | **pause** (auto si workflow declared) | si déclaré : click "Autoriser" | "Le site demande l'accès à la caméra. Validez Autoriser/Bloquer, j'attends." | [BrowserStack](https://www.browserstack.com/docs/automate/selenium/handle-permission-pop-ups) | +| `browser-perm-microphone` | B | "souhaite utiliser votre microphone", "souhaite utiliser votre micro" | "wants to use your microphone" | Autoriser / Bloquer | accès micro | **pause** (auto si declared) | click "Autoriser" | "Le site demande l'accès au micro. Validez, j'attends." | identique | +| `browser-perm-notifications` | B | "souhaite afficher des notifications" | "wants to show notifications" | Autoriser / Bloquer | premier accès notifs | **pause** (auto si declared) | click "Bloquer" recommandé | "Le site demande à afficher des notifications. Validez, j'attends." | [Mozilla MDN](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/permissions/request) | +| `browser-perm-location` | B | "souhaite connaître votre position" | "wants to know your location" | Autoriser / Bloquer | accès géoloc | **pause** | — | "Le site demande votre position GPS. À valider manuellement." | identique | +| `browser-perm-midi` | B | "souhaite utiliser vos appareils midi" | "wants to use your midi devices" | Autoriser / Bloquer | accès MIDI | **pause** | — | "Le site demande l'accès MIDI. À valider manuellement (rare en healthtech)." | identique | +| `browser-perm-clipboard` | B | "souhaite accéder au presse-papiers" | "wants to access clipboard" | Autoriser / Bloquer | copy/paste JS | **pause** (auto si declared) | click "Autoriser" | "Le site demande l'accès presse-papiers. Validez, j'attends." | identique | +| `browser-cert-select` | B | "sélectionner un certificat", "authentification requise" | "select a certificate", "authentication required" | Liste certificats + OK / Annuler | site avec mTLS / CG, Carte CPS | **pause** | — | "Le site demande la sélection d'un certificat client. À choisir manuellement (Carte CPS ?), j'attends." | [Mozilla SSL](https://developer.mozilla.org/en-US/docs/Web/Security/Certificate_Transparency) | +| `browser-basic-auth` | B | "le site demande votre nom d'utilisateur", "connexion requise" | "sign in", "this site is asking you to sign in" | Champ user/pass + Se connecter | site Nginx auth_basic | **pause** sauf vault Léa | si vault dispo : remplir + se connecter | "Le site demande une authentification Basic. Saisissez user/pass, j'attends." | `feedback_auth_dialogs_runtime.md` | +| `browser-save-password` | B | "voulez-vous enregistrer ce mot de passe", "voulez-vous que google chrome enregistre" | "save password", "do you want to save this password" | Enregistrer / Jamais | premier login site | **pause** par défaut sécurité | — | "Chrome propose d'enregistrer le mot de passe. Cliquez Jamais (sécurité), j'attends." | [Chrome Help](https://support.google.com/chrome/answer/95606) | +| `browser-download-confirm` | B | "conserver", "supprimer", "ouvrir" | "keep", "discard", "open" | Conserver / Supprimer | téléchargement déclenché | **déclaratif** (workflow) | si déclaré : click "Conserver" | "Chrome demande quoi faire du téléchargement. À déclarer dans le workflow." | observation interne | +| `browser-unsafe-page` | B | "votre connexion n'est pas privée", "site non sécurisé" | "your connection is not private", "not secure" | Retour à la sécurité / Avancé | cert SSL expiré, self-signed | **pause** | — | "Le navigateur signale un certificat invalide. À valider manuellement (risque)." | [Chrome SSL errors](https://support.google.com/chrome/answer/6098869) | +| `browser-beforeunload` | B | "cette page demande de la fermer", "voulez-vous quitter ce site" | "do you want to leave this site", "leave site" | Quitter / Annuler | nav vers autre URL avec form non sauvé | **déclaratif** | si déclaré : click "Quitter" sinon "Annuler" | "Le site demande confirmation avant fermeture. À déclarer dans le workflow." | [W3C HTML spec](https://html.spec.whatwg.org/multipage/browsing-the-web.html#prompt-to-unload-a-document) | +| `browser-print-dialog` | B | "imprimer", "destination", "pages" | "print", "destination", "pages" | Imprimer / Annuler | Ctrl+P ou JS print() | **déclaratif** | si déclaré : click "Imprimer" sur imprimante configurée | "Boîte d'impression. À déclarer dans le workflow." | [Chrome Print](https://support.google.com/chrome/answer/1069693) | +| `browser-translate-prompt` | B | "traduire cette page", "souhaitez-vous traduire" | "translate this page" | Traduire / Non | page langue ≠ navigateur | **auto-dismiss** | click "Non" | — | [Chrome Translate](https://support.google.com/chrome/answer/173424) | +| `browser-page-unresponsive` | B | "cette page web ne répond pas", "tuer les pages" | "page unresponsive", "kill pages" | Attendre / Quitter | crash app web | **pause** | — | "La page web ne répond plus. À gérer manuellement (attendre ou fermer)." | [Chrome crash](https://support.google.com/chrome/answer/95669) | +| `browser-popup-blocked` | B | "popup bloqué", "fenêtre publicitaire bloquée" | "pop-up blocked" | Toujours autoriser / Continuer à bloquer | site déclenche window.open | **pause** | — | "Le navigateur a bloqué un popup. À valider manuellement." | [Mozilla popup blocker](https://support.mozilla.org/en-US/kb/pop-blocker-settings-exceptions-troubleshooting) | +| `browser-cookies-banner` | B | "accepter les cookies", "ce site utilise des cookies" | "accept cookies", "this site uses cookies" | Accepter / Refuser / Personnaliser | premier accès site UE | **déclaratif** | si déclaré : click "Accepter" ou "Refuser" | "Bannière cookies RGPD. À déclarer dans le workflow." | [RGPD](https://www.cnil.fr/fr/cookies-et-traceurs) | + +### 3.C. MÉTIER Easily Assure / apps cliniques — souvent auto-traitable + +| ID | Cat | Signature FR | Signature EN | Boutons | Trigger | Décision | Action auto | Message pause | Source | +|---|---|---|---|---|---|---|---|---|---| +| `easily-save-unconfirmed` | C | "voulez-vous enregistrer", "enregistrer les modifications", "modifications non sauvegardées" | "do you want to save", "unsaved changes" | Enregistrer / Ne pas enregistrer / Annuler | fermeture onglet ou patient avec data dirty | **auto** | click "Enregistrer" | — | `dialog_handler.KNOWN_DIALOGS["voulez-vous enregistrer"]` | +| `easily-save-as` | C | "enregistrer sous" | "save as" | Champ nom + Enregistrer / Annuler | sauvegarde nouveau document | **auto** | click "Enregistrer" (nom rempli par étape précédente) | — | `dialog_handler.KNOWN_DIALOGS["enregistrer sous"]` | +| `easily-overwrite-file` | C | "voulez-vous le remplacer", "existe déjà", "écraser" | "do you want to replace", "already exists", "overwrite" | Oui / Non | sauvegarde nom existant | **auto** | click "Oui" | — | `dialog_handler.KNOWN_DIALOGS["voulez-vous le remplacer"]` | +| `easily-confirm-action` | C | "confirmer l'enregistrement", "confirmer", "êtes-vous sûr" | "confirm", "are you sure" | Oui / Non | validation étape métier | **auto** | click "Oui" | — | `dialog_handler.KNOWN_DIALOGS["confirmer l'enregistrement"]` | +| `easily-delete-confirm` | C | "voulez-vous supprimer", "confirmer la suppression" | "do you want to delete", "confirm deletion" | Oui / Non / Supprimer | clic poubelle dossier patient | **pause** sauf déclaratif | si déclaré : click "Oui" | "Easily demande confirmation suppression. Action irréversible — à valider manuellement." | observation Anouste + Easily | +| `easily-send-confirm` | C | "voulez-vous envoyer", "confirmer l'envoi" | "do you want to send" | Envoyer / Annuler | validation envoi DPI vers PMSI | **déclaratif** | si déclaré : click "Envoyer" | "Easily demande confirmation d'envoi. À déclarer dans le workflow." | observation Easily | +| `easily-session-expired` | C | "session expirée", "veuillez vous reconnecter", "timeout" | "session expired", "please log in again" | OK + redirection login | inactivité prolongée | **pause** | — | "Session Easily expirée. Reconnectez-vous manuellement, j'attends." | observation interne | +| `easily-required-field` | C | "champ obligatoire manquant", "veuillez remplir", "champ requis" | "required field missing", "please fill" | OK | tentative validation form incomplet | **auto-dismiss** + retour aux retries | click "OK" puis re-grounding champ manquant | — | observation interne | +| `easily-duplicate-ipp` | C | "patient déjà saisi", "doublon ipp", "patient existe déjà" | "duplicate patient", "patient already exists" | Continuer / Annuler / Fusionner | création patient avec IPP existant | **pause** | — | "Easily signale un doublon patient IPP. Décision clinique requise, j'attends." | observation Anouste | +| `easily-clinical-warning` | C | "attention", "avertissement clinique", "vérifier" | "warning", "clinical alert" | OK / Continuer | alerte métier (allergie, contre-indication) | **pause** | — | "Avertissement clinique Easily. Décision médicale requise, j'attends." | observation DIM | +| `easily-toast-saved` | C | "enregistré avec succès", "sauvegardé" | "saved successfully" | (aucun, disparaît seul) | post-save succès | **auto** | ignore (non bloquant) | — | observation interne | +| `easily-onboarding-tip` | C | "astuce", "saviez-vous que", "découvrez" | "did you know", "tip" | OK / Suivant / Plus tard | premier lancement après MAJ | **auto-dismiss** | click "Plus tard" ou croix | — | observation interne | +| `easily-help-popup` | C | "aide", "tutoriel", "guide" | "help", "tutorial" | Suivant / Fermer | clic accidentel sur "?" | **auto-dismiss** | click "Fermer" / croix | — | observation interne | +| `app-error-ok` | C | "erreur", "avertissement", "une erreur s'est produite" | "error", "warning", "an error occurred" | OK | erreur backend non critique | **auto** | click "OK" puis log | — | `dialog_handler.KNOWN_DIALOGS["erreur"]` | +| `app-error-suspect` | C | "données perdues", "supprimé définitivement", "irréversible" | "data lost", "permanently deleted", "irreversible" | OK | erreur critique | **pause** (blocklist `SUSPECT_TOKENS`) | — | "L'application signale une perte de données ou action irréversible. À valider manuellement." | `AXE_D2_DEEP §3.1 SUSPECT_TOKENS_BLOCKLIST` | + +### 3.D. APP TIERS (Office / PDF / Edge / Citrix / NoMachine) — mixte + +| ID | Cat | Signature FR | Signature EN | Boutons | Trigger | Décision | Action auto | Message pause | Source | +|---|---|---|---|---|---|---|---|---|---| +| `office-enable-content` | D | "activer le contenu", "macros désactivées", "avertissement de sécurité" | "enable content", "macros disabled", "security warning" | Activer le contenu / Annuler | ouverture .docx/.xlsx avec macros | **pause** | — | "Office demande l'activation des macros (risque sécurité). À valider manuellement." | [Microsoft Office security](https://learn.microsoft.com/en-us/deployoffice/security/) | +| `word-merge-conflict` | D | "conflit de fusion", "votre version", "version du serveur" | "merge conflict" | Conserver la mienne / Théirs / Fusionner | doc co-édité Office 365 | **pause** | — | "Word signale un conflit de fusion. Décision éditoriale, j'attends." | observation interne | +| `outlook-reminder` | D | "rappel", "outlook", "réunion" | "reminder", "meeting" | Ignorer / Reporter | rendez-vous Outlook arrivant | **auto-dismiss** | click "Ignorer" + log | — | [Microsoft Outlook reminders](https://support.microsoft.com/en-us/office/) | +| `pdf-open-confirm` | D | "voulez-vous ouvrir", "type de fichier" | "do you want to open" | Ouvrir / Enregistrer / Annuler | clic lien PDF | **déclaratif** | si déclaré : click "Ouvrir" | "Le navigateur demande quoi faire du PDF. À déclarer." | observation interne | +| `adobe-reader-update` | D | "mise à jour disponible", "adobe acrobat reader" | "update available", "adobe reader" | Installer / Plus tard | Adobe Reader updater | **auto-dismiss** | click "Plus tard" | — | [Adobe Reader updates](https://helpx.adobe.com/acrobat/using/automatic-update-acrobat-reader.html) | +| `chrome-update` | D | "google chrome mis à jour", "redémarrer chrome" | "google chrome was updated", "relaunch" | Relancer / Plus tard | Chrome auto-update | **auto-dismiss** | click "Plus tard" | — | [Chrome updates](https://support.google.com/chrome/answer/95414) | +| `edge-update` | D | "microsoft edge a été mis à jour", "redémarrer" | "microsoft edge was updated" | Redémarrer / Plus tard | Edge auto-update | **auto-dismiss** | click "Plus tard" | — | [Edge updates](https://support.microsoft.com/en-us/microsoft-edge/) | +| `citrix-receiver-cert` | D | "ajouter ce certificat", "citrix receiver", "workspace app" | "add this account", "citrix workspace" | OK / Annuler | premier accès Citrix | **pause** | — | "Citrix Workspace demande une action. À configurer hors démo." | [Citrix Workspace](https://www.citrix.com/products/citrix-workspace-app/) | +| `citrix-file-access` | D | "autoriser l'accès aux fichiers locaux", "lecture/écriture" | "allow access to local files" | Autoriser / Refuser | session Citrix monte ressource locale | **pause** | — | "Citrix demande l'accès aux fichiers locaux. À valider selon politique." | identique | +| `nomachine-reconnect` | D | "la connexion a été perdue", "tenter de reconnecter" | "connection lost", "try to reconnect" | Reconnecter / Quitter | session NoMachine instable | **pause** | — | "NoMachine a perdu la connexion. À reconnecter manuellement." | observation interne (NoMachine freeze 17 mai) | +| `nomachine-clipboard-fail` | D | "presse-papiers", "clipboard non disponible" | "clipboard not available" | OK | sync clipboard NoMachine cassée | **auto-dismiss** | click "OK" puis fallback ydotool | — | `LESSONS_LEARNED_GHT_2026-05 contournement clipboard` | + +### 3.E. INCONNU / NON CATALOGUÉ + +| ID | Cat | Signature | Boutons | Trigger | Décision | Action | Message pause | Source | +|---|---|---|---|---|---|---|---|---| +| `unknown-dialog` | E | aucun match signature, aucun match `KNOWN_DIALOGS`, VLM classifier fallback → `inconnu` | variable | tout moment, surtout démo live | **pause** par défaut | — | "Un dialogue inattendu est apparu. Je ne le reconnais pas, j'attends votre action. (Catalogue enrichi post-démo.)" | `AXE_D2_DIALOG_POPUP §2.7` | + +--- + +## 4. Fiches détaillées — 10 modaux les plus critiques + +### 4.1. `windows-uac-elevation` (A) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ [Bouclier UAC] Contrôle de compte d'utilisateur X │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ Voulez-vous autoriser cette application à apporter │ +│ des modifications à votre appareil ? │ +│ │ +│ [Icône app] Nom de l'application │ +│ Éditeur vérifié : Microsoft Corporation │ +│ │ +│ Afficher plus de détails │ +│ │ +│ ┌─────────┐ ┌─────────┐ │ +│ │ Oui │ │ Non │ │ +│ └─────────┘ └─────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +- **ID** : `windows-uac-elevation` +- **Catégorie** : A (SYSTÈME) +- **Signature OCR FR** : `"contrôle de compte d'utilisateur"`, `"voulez-vous autoriser cette application"` +- **Signature OCR EN** : `"user account control"`, `"do you want to allow this app"` +- **Détection secondaire** : `ClassName='$$$Secure UAP Dummy Window Class$$$'`, process=`consent.exe` (cf. `system_dialog_guard.py:36-37`) +- **Particularité** : **secure desktop** — l'écran entier est assombri par Windows. Le screenshot Léa peut renvoyer un buffer noir/inaccessible (notamment via SSH ou session distante). +- **Boutons** : `Oui` / `Non` (ou champ password admin si UAC en mode "Always notify"). +- **Trigger** : install MSI, `runas`, modification système, lancement app demandant élévation. +- **Décision** : **`ESCALATE_SECURITY` + pause humaine**. JAMAIS auto-accept. +- **Message bulle Léa** : *"Windows demande l'élévation des droits administrateur. Merci de cliquer Oui ou Non, j'attends."* +- **Action requise pré-démo** : signer le binaire `agent_v1.exe` (cf. `project_code_signing.md`, Azure Artifact Signing ~170€/18m) pour éviter le déclenchement UAC sur Léa elle-même. +- **Source** : [Microsoft UAC docs](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/user-account-control/how-it-works) + `system_dialog_guard.py`. + +--- + +### 4.2. `windows-hello-fingerprint` / `windows-hello-pin` (A) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Sécurité Windows X │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ Vérification de votre identité │ +│ │ +│ [Icône empreinte] Analysez votre doigt sur le lecteur │ +│ d'empreintes digitales │ +│ │ +│ ou │ +│ │ +│ ┌─────────────────────────┐ │ +│ │ Saisir votre code PIN │ │ +│ └─────────────────────────┘ │ +│ │ +│ ┌──────────┐ │ +│ │ Annuler │ │ +│ └──────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +- **ID** : `windows-hello-fingerprint` (ou `windows-hello-pin`) +- **Catégorie** : A (SYSTÈME — Identification) +- **Signature OCR FR** : `"windows hello"`, `"vérification de votre identité"`, `"touchez le capteur d'empreintes"`, `"analysez votre doigt"`, `"saisissez votre code pin"` +- **Signature OCR EN** : `"windows hello"`, `"touch the fingerprint sensor"`, `"enter your pin"` +- **Particularité absolue** : **interaction physique humaine requise par construction** (FIDO2 / WebAuthn). Aucune solution 100% vision ne peut résoudre. +- **Anti-pattern interdit** : tenter de cliquer "Annuler" pour passer outre — ça casse le workflow login. +- **Trigger fréquent en healthtech** : Chrome a stocké le password → auto-fill déclenche Hello. Observé `2026-05-05` démo GHT (`feedback_auth_dialogs_runtime.md`). +- **Décision** : **`ESCALATE_SECURITY` + pause humaine**. +- **Message bulle Léa** : *"Windows Hello demande votre empreinte digitale (ou code PIN). Posez votre doigt sur le lecteur, j'attends."* +- **Action requise pré-démo** : désactiver Windows Hello pour la session, ou utiliser une URL LAN locale sans Basic Auth (cf. workaround GHT `MAQUETTE_URL=http://192.168.1.40:8765`), ou configurer un profil Chrome dédié sans password sauvé. +- **Source** : [Microsoft WebAuthn](https://learn.microsoft.com/en-us/windows/security/identity-protection/hello-for-business/webauthn-apis) + `feedback_auth_dialogs_runtime.md`. + +--- + +### 4.3. `defender-smartscreen-app` (A) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Windows a protégé votre PC X │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ Microsoft Defender SmartScreen a empêché le démarrage │ +│ d'une application non reconnue. L'exécution de cette │ +│ application peut mettre votre PC en danger. │ +│ │ +│ Application : agent_v1.exe │ +│ Éditeur : Éditeur inconnu │ +│ │ +│ [Informations complémentaires] ← lien à cliquer │ +│ │ +│ ┌─────────────┐ │ +│ │ Ne pas exéc.│ │ +│ └─────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +Après clic "Informations complémentaires" : + +``` +│ [Informations complémentaires] │ +│ Application : agent_v1.exe │ +│ Éditeur : Éditeur inconnu │ +│ │ +│ ┌────────────────┐ ┌─────────────┐ │ +│ │ Exécuter qd mê.│ │ Ne pas exéc.│ │ +│ └────────────────┘ └─────────────┘ │ +``` + +- **ID** : `defender-smartscreen-app` +- **Catégorie** : A (SYSTÈME — Sécurité) +- **Signature OCR FR** : `"windows a protégé votre pc"`, `"smartscreen a empêché"`, `"éditeur inconnu"` +- **Signature OCR EN** : `"windows protected your pc"`, `"smartscreen prevented"`, `"unknown publisher"` +- **Détection secondaire** : process=`smartscreen.exe` (cf. `system_dialog_guard.py:46`) +- **Particularité** : la fenêtre initiale ne montre que `Ne pas exécuter`. Le bouton `Exécuter quand même` n'apparaît qu'après clic sur le lien `Informations complémentaires`. Léa ne doit JAMAIS faire ce clic seule (deux étapes de bypass volontaire). +- **Décision** : **`ESCALATE_SECURITY` + pause humaine**. +- **Message bulle Léa** : *"Windows SmartScreen bloque l'application. Cliquez 'Informations complémentaires' puis 'Exécuter quand même' si vous validez, j'attends."* +- **Action requise pré-démo** : signer le binaire `agent_v1.exe`, whitelister le SHA256 dans la console DSI client (cf. `project_code_signing.md`, négociation Anouste SHA256 ~170€/18m Azure Artifact Signing). +- **Source** : [MSP360 SmartScreen KB](https://kb.msp360.com/backup/warnings/ms-defender-smart-screen) + [Microsoft SmartScreen docs](https://learn.microsoft.com/en-us/windows/security/operating-system-security/virus-and-threat-protection/microsoft-defender-smartscreen/). + +--- + +### 4.4. `browser-perm-microphone` / `browser-perm-camera` (B) + +``` +┌────────────────────────────────────────────────────────┐ +│ [🔒] urgence.labs.laurinebazin.design │ +│ ───────────────────────────────────────────── │ +│ 🎤 urgence.labs.laurinebazin.design │ +│ souhaite utiliser votre microphone │ +│ │ +│ ┌────────────┐ ┌─────────┐ │ +│ │ Autoriser │ │ Bloquer │ │ +│ └────────────┘ └─────────┘ │ +└────────────────────────────────────────────────────────┘ +``` + +- **ID** : `browser-perm-microphone` (variantes : `-camera`, `-notifications`, `-location`, `-clipboard`) +- **Catégorie** : B (NAVIGATEUR) +- **Signature OCR FR** : `"souhaite utiliser votre microphone"`, `"autoriser"`, `"bloquer"` +- **Signature OCR EN** : `"wants to use your microphone"`, `"allow"`, `"block"` +- **Particularité** : popup ancré à la URL bar Chrome/Edge (haut gauche), pas centré. Le `ChangeDetector` zone centrale ne le verra **pas** — il faut combiner avec foreground window change ET diff zone haute. +- **Trigger** : premier accès media. Site WebRTC, Easily Assure si elle utilise micro pour dictée vocale. +- **Décision** : + - Par défaut : **`ASK_HUMAN`**. + - Si le workflow VWB déclare `expected_modal.id=browser-perm-microphone, action=click_button, button_label=Autoriser` → **`DECLARATIVE`** (auto). +- **Action auto** : click coordonnées `Autoriser` (résolution via OCR/InfiGUI). +- **Message bulle Léa** (pause) : *"Le site demande l'accès au microphone. Validez Autoriser/Bloquer, j'attends."* +- **Risque sécurité 2026** : CVE-2026-0628 Chrome (extension malveillante détournant permissions). Auto-accept = risque RGPD/HDS. Toujours déclaratif ou pause. +- **Source** : [MDN permissions API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/permissions/request) + `AXE_D2_DIALOG_POPUP §2.4`. + +--- + +### 4.5. `easily-save-unconfirmed` (C) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Easily Assure - Confirmation X │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ Voulez-vous enregistrer les modifications apportées │ +│ au dossier patient MOREL Catherine ? │ +│ │ +│ ┌──────────────┐ ┌──────────────────┐ ┌─────────┐ │ +│ │ Enregistrer │ │ Ne pas enregist. │ │ Annuler │ │ +│ └──────────────┘ └──────────────────┘ └─────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +- **ID** : `easily-save-unconfirmed` +- **Catégorie** : C (MÉTIER) +- **Signature OCR FR** : `"voulez-vous enregistrer"`, `"enregistrer les modifications"` +- **Signature OCR EN** : `"do you want to save"`, `"save changes"` +- **Boutons** : `Enregistrer` / `Ne pas enregistrer` / `Annuler` (3 boutons standard Easily/Office) +- **Trigger** : fermeture onglet/patient avec données dirty. +- **Décision** : **`DECLARATIVE` auto**. Catalogue déjà en place dans `dialog_handler.KNOWN_DIALOGS["voulez-vous enregistrer"]`. +- **Action auto** : InfiGUI click `Enregistrer` (priorité BASSE car fenêtre parent ; les popups modaux comme `confirmer l'enregistrement` matchent AVANT). +- **Fallback OCR direct** : prendre le bouton le plus à gauche labellé "Enregistrer" (parfois "Save" en EN). +- **Source** : `core/grounding/dialog_handler.py:54` + observation Easily Assure runtime. + +--- + +### 4.6. `easily-overwrite-file` (C) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Confirmer l'enregistrement X │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ Le fichier "decision_morel.xlsx" existe déjà. │ +│ Voulez-vous le remplacer ? │ +│ │ +│ ┌─────────┐ ┌─────────┐ │ +│ │ Oui │ │ Non │ │ +│ └─────────┘ └─────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +- **ID** : `easily-overwrite-file` +- **Catégorie** : C (MÉTIER) +- **Signature OCR FR** : `"voulez-vous le remplacer"`, `"existe déjà"`, `"écraser"`, `"remplacer"` +- **Signature OCR EN** : `"do you want to replace"`, `"already exists"`, `"overwrite"` +- **Particularité importante** : ce popup modal s'affiche par-dessus `enregistrer sous`. L'OCR full-screen capte le texte des DEUX dialogs. C'est pour ça que `KNOWN_DIALOGS` ordre les **popups modaux AVANT** les fenêtres parents (`dialog_handler.py:30-35` commentaire). +- **Décision** : **`DECLARATIVE` auto** (sauvegarde T2A Excel = workflow connu). +- **Action auto** : click `Oui` (InfiGUI ou OCR fallback). +- **Risque** : si on cliquait sur le bouton du dialog parent (`Annuler` de "enregistrer sous"), on perdrait la sauvegarde. D'où la priorité haute dans `KNOWN_DIALOGS`. +- **Source** : `dialog_handler.py:38` + observation démo Urgence_aiva. + +--- + +### 4.7. `easily-delete-confirm` (C) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Easily Assure - Suppression X │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ Voulez-vous supprimer le dossier patient │ +│ MOREL Catherine ? │ +│ │ +│ Cette action est irréversible. │ +│ │ +│ ┌─────────┐ ┌─────────┐ │ +│ │ Oui │ │ Non │ │ +│ └─────────┘ └─────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +- **ID** : `easily-delete-confirm` +- **Catégorie** : C (MÉTIER — SUPPRESSION) +- **Signature OCR FR** : `"voulez-vous supprimer"`, `"confirmer la suppression"`, `"irréversible"` +- **Signature OCR EN** : `"do you want to delete"`, `"confirm deletion"`, `"cannot be undone"` +- **Particularité** : déclenche la blocklist `SUSPECT_TOKENS` (`"irréversible"`, `"cannot be undone"`). Mappe à `DialogType.METIER_OK_SUSPECT` par défaut. +- **Décision** : **`ASK_HUMAN`** sauf si workflow déclare explicitement `expected_modal.id=easily-delete-confirm, action=click_button, button_label=Oui, confirm=true`. Le flag `confirm=true` est OBLIGATOIRE pour cette catégorie — sans lui, refus. +- **Message bulle Léa** : *"Easily demande confirmation de suppression. Action irréversible — à valider manuellement, j'attends."* +- **Source** : `AXE_D2_DEEP §3.1 SUSPECT_TOKENS_BLOCKLIST`. + +--- + +### 4.8. `easily-session-expired` (C) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Easily Assure - Session X │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ Votre session a expiré. │ +│ Veuillez vous reconnecter. │ +│ │ +│ ┌──────────┐ │ +│ │ OK │ │ +│ └──────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +- **ID** : `easily-session-expired` +- **Catégorie** : C (MÉTIER — IDENTIFICATION) +- **Signature OCR FR** : `"session expirée"`, `"veuillez vous reconnecter"`, `"timeout"` +- **Signature OCR EN** : `"session expired"`, `"please log in again"` +- **Décision** : **`ASK_HUMAN`** par défaut. Si vault Léa contient credentials Easily ET workflow autorise reconnect → `DECLARATIVE` (click OK puis relogin via vault). +- **Trigger** : inactivité prolongée (workflow long, pause pour LLM T2A 30+ s). +- **Risque** : reconnexion automatique = sécurité moindre. Préférer pause supervisée tant que vault Léa pas généralisé. +- **Message bulle Léa** : *"Session Easily expirée. Reconnectez-vous manuellement, j'attends."* +- **Source** : observation interne + `core/auth/credential_vault.py`. + +--- + +### 4.9. `office-enable-content` (D) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ ⚠ AVERTISSEMENT DE SÉCURITÉ Les macros ont été désactivées│ +│ │ +│ [ Activer le contenu ] │ +└─────────────────────────────────────────────────────────────┘ +``` + +- **ID** : `office-enable-content` +- **Catégorie** : D (APP TIERS — Office) +- **Signature OCR FR** : `"avertissement de sécurité"`, `"activer le contenu"`, `"macros désactivées"` +- **Signature OCR EN** : `"security warning"`, `"enable content"`, `"macros disabled"` +- **Décision** : **`ASK_HUMAN`**. Pas d'auto-accept — les macros peuvent exécuter du code arbitraire. +- **Message bulle Léa** : *"Office demande l'activation des macros (risque sécurité). À valider manuellement."* +- **Particularité workflow Easily** : si on extrait des données vers un classeur Excel pré-existant avec macros (ex. template T2A enrichi), ce dialog apparaît. Préférer un classeur sans macros pour les workflows Léa. +- **Source** : [Microsoft Office security](https://learn.microsoft.com/en-us/deployoffice/security/). + +--- + +### 4.10. `unknown-dialog` (E) + +- **ID** : `unknown-dialog` +- **Catégorie** : E (INCONNU) +- **Signature** : aucun match `SIGNATURES_BY_TYPE`, aucun match `KNOWN_DIALOGS`, VLM classifier fallback → `inconnu` ou aucune réponse. +- **Décision** : **`ASK_HUMAN`** par défaut (politique conservative healthtech). +- **Message bulle Léa** : *"Un dialogue inattendu est apparu. Je ne le reconnais pas, j'attends votre action."* +- **Action post-pause** : `DialogResolver` génère un `DialogEvent` avec screenshot full + OCR text + classification VLM. Ces events sont audités hebdo pour enrichir le catalogue (`SIGNATURES_BY_TYPE` ou `KNOWN_DIALOGS` selon catégorie). +- **Anti-pattern interdit** : auto-dismiss aveugle via Escape ou clic croix. C'est exactement le piège de `_handle_possible_popup` orphelin (cf. `AXE_D2_DEEP §6` : suppression recommandée). +- **Source** : politique conservative healthtech, cf. `feedback_failure_is_learning.md`. + +--- + +## 5. Workflow VWB déclaratif — `expected_modal` + +### 5.1. Format YAML proposé (extensible JSON) + +```yaml +# visual_workflow_builder/workflows/Urgence_aiva_demo.yaml +name: Urgence_aiva_demo +patient: MOREL Catherine + +# Déclaration globale : modaux dont l'apparition est anticipée n'importe quand +declared_dialogs: + browser-perm-microphone: + policy: auto # surcharge le défaut 'pause' du catalogue + action: click_button + button_label: Autoriser + easily-onboarding-tip: + policy: auto + action: click_button + button_label: Plus tard + +steps: + - idx: 1 + type: click_anchor + target: "Enregistrer" + # Déclaration locale : ce step va probablement déclencher ce modal + expected_modal: + id: easily-save-unconfirmed + window_title: "Easily Assure - Confirmation" # optionnel, matching strict + policy: auto + action: click_button + button_label: Oui + timeout_ms: 5000 # délai max d'apparition + + - idx: 2 + type: click_anchor + target: "Supprimer" + # SUPPRESSION : flag confirm=true obligatoire pour autoriser auto + expected_modal: + id: easily-delete-confirm + window_title: "Easily Assure - Suppression" + policy: auto + action: click_button + button_label: Oui + confirm: true # ← OBLIGATOIRE pour catégorie suppression + timeout_ms: 3000 + + - idx: 3 + type: paste_and_execute + # Aucun expected_modal : tout modal inattendu = pause par défaut +``` + +### 5.2. Contrat backend VWB + +**Côté `visual_workflow_builder/backend/workflow_schema.py`** (lecture seule, à câbler) : + +```python +class ExpectedModal(BaseModel): + """Déclaration anticipée d'un modal attendu à un step donné. + + Le workflow peut surcharger la `policy` du catalogue (sauf catégorie + SYSTÈME, jamais surchargeable). Le matching côté DialogResolver + s'appuie d'abord sur `window_title` si fourni (plus strict), sinon + sur les signatures OCR du catalogue. + """ + id: str = Field(description="ID catalogue, ex. 'easily-save-unconfirmed'") + window_title: Optional[str] = None # match strict sur titre fenêtre si fourni + policy: Literal["auto", "pause", "skip"] = "auto" # surcharge du défaut catalogue + action: Literal["click_button", "dismiss", "ignore"] + button_label: Optional[str] = None # requis si action=click_button + confirm: bool = False # OBLIGATOIRE True si catégorie SUPPRESSION + timeout_ms: int = 5000 # délai max d'apparition + + @field_validator("policy") + @classmethod + def system_modals_cannot_be_overridden(cls, v, info): + """Catégorie SYSTÈME : interdiction stricte d'imposer auto/skip via workflow.""" + modal_id = info.data.get("id", "") + system_prefixes = ("windows-", "defender-") + if modal_id.startswith(system_prefixes) and v != "pause": + raise ValueError( + f"expected_modal.policy='{v}' interdit pour '{modal_id}' " + f"(catégorie SYSTÈME — toujours 'pause' en healthtech)." + ) + return v + + @field_validator("confirm") + @classmethod + def suppression_requires_confirm(cls, v, info): + modal_id = info.data.get("id", "") + if modal_id in {"easily-delete-confirm", "easily-purge-confirm"}: + if not v: + raise ValueError( + f"expected_modal.confirm=true OBLIGATOIRE pour '{modal_id}' " + f"(catégorie SUPPRESSION). Refus de l'auto-traitement." + ) + return v + + +class WorkflowStep(BaseModel): + idx: int + type: str + target: Optional[str] = None + expected_modal: Optional[ExpectedModal] = None + # ... autres champs existants + + +class Workflow(BaseModel): + name: str + declared_dialogs: Dict[str, ExpectedModal] = Field(default_factory=dict) + steps: List[WorkflowStep] +``` + +### 5.3. Câblage runtime côté serveur + +**Site d'appel : `agent_v0/server_v1/api_stream.py` — `report_action_result`** (cf. `AXE_D2_DEEP §5.1`) : + +```python +# Construction du workflow_context avec declared_dialogs fusionnés +declared = dict(current_workflow.get("declared_dialogs", {})) +step_declared = (current_step.get("expected_modal") or {}) +if step_declared: + declared[step_declared["id"]] = step_declared + +resolver = _get_dialog_resolver() +event = resolver.check_and_resolve( + img_after, + workflow_context={ + "step_idx": current_step_idx, + "action_id": action_id, + "declared_dialogs": declared, # ← clé : DialogResolver bascule en DECLARATIVE + }, +) +``` + +### 5.4. Frontend VWB — UX proposée + +- Sur chaque étape, un bouton "Ajouter modal attendu" → popup de sélection dans le catalogue (`SIGNATURES_BY_TYPE` exposé en API). +- Catégorie SUPPRESSION → forcer la case à cocher `J'autorise l'auto-traitement de cette suppression irréversible` qui mappe sur `confirm=true`. +- Catégorie SYSTÈME (UAC/Hello/SmartScreen) → impossibilité de déclarer un auto (UI grise les options, message *"Cette catégorie ne peut pas être auto-traitée en healthtech"*). + +--- + +## 6. Snippet Python — `KNOWN_DIALOGS` étendu prêt à coller + +À coller dans **`core/dialog/signatures.py`** (nouveau fichier, cf. `AXE_D2_DEEP §3.1`). Complète `core/grounding/dialog_handler.KNOWN_DIALOGS` existant **sans le remplacer** (single source of truth pour la couche métier). + +```python +"""core/dialog/signatures_catalogue.py — Catalogue complet 55 entrées. + +Étend SIGNATURES_BY_TYPE de signatures.py avec tous les modaux du +SPEC_POPUPS_CATALOGUE.md. Source de vérité pour DialogClassifier. + +Toutes les signatures en MINUSCULES, OCR text doit être .lower() avant +matching. Accents préservés (EasyOCR fr les conserve). + +Ordre du dict = priorité de matching dans chaque catégorie (popups modaux +priorité HAUTE avant fenêtres parents). +""" +from __future__ import annotations + +from typing import Dict, List, Tuple + +# Réutilise les enums de signatures.py (DialogType uniquement — Policy +# remplacé par la trichotomie stricte str "auto" / "pause" / "skip") +from core.dialog.signatures import DialogType + + +# ── Politique stricte par ID modal (trichotomie auto / pause / skip) ───── +# Valeurs strictes : "auto" | "pause" | "skip" +# - "auto" : click_button défini par CATALOGUE[modal_id][2] +# - "pause" : escalade humaine via on_pause(event) +# - "skip" : ignore le modal, poursuit le workflow (toasts non bloquants) +POLICY_BY_MODAL_ID: Dict[str, str] = { + # SYSTÈME — toujours pause (jamais surchargeable par workflow) + "windows-uac-elevation": "pause", + "windows-hello-pin": "pause", + "windows-hello-fingerprint": "pause", + "windows-hello-face": "pause", + "windows-hello-security-key": "pause", + "defender-smartscreen-app": "pause", + "defender-smartscreen-url": "pause", + "windows-defender-threat": "pause", + "windows-credui-prompt": "pause", + "windows-bitlocker-key": "pause", + "windows-update-restart": "pause", + "windows-firewall-allow": "pause", + "windows-driver-install": "pause", + "windows-printer-setup": "pause", + "windows-network-location": "pause", + # NAVIGATEUR — pause par défaut, surchargeable en "auto" via workflow + "browser-perm-camera": "pause", + "browser-perm-microphone": "pause", + "browser-perm-notifications": "pause", + "browser-perm-location": "pause", + "browser-perm-midi": "pause", + "browser-perm-clipboard": "pause", + "browser-cert-select": "pause", + "browser-basic-auth": "pause", + "browser-save-password": "pause", + "browser-download-confirm": "pause", + "browser-unsafe-page": "pause", + "browser-beforeunload": "pause", + "browser-print-dialog": "pause", + "browser-translate-prompt": "skip", + "browser-page-unresponsive": "pause", + "browser-popup-blocked": "pause", + "browser-cookies-banner": "pause", + # MÉTIER Easily — auto sauf SUPPRESSION/IDENTIFICATION/CLINICAL-WARNING + "easily-save-unconfirmed": "auto", + "easily-save-as": "auto", + "easily-overwrite-file": "auto", + "easily-confirm-action": "auto", + "easily-delete-confirm": "pause", # SUPPRESSION + "easily-send-confirm": "pause", + "easily-session-expired": "pause", # IDENTIFICATION + "easily-required-field": "auto", + "easily-duplicate-ipp": "pause", + "easily-clinical-warning": "pause", # CLINICAL-WARNING + "easily-toast-saved": "skip", + "easily-onboarding-tip": "auto", + "easily-help-popup": "auto", + "app-error-ok": "auto", + "app-error-suspect": "pause", + # APP TIERS + "office-enable-content": "pause", + "word-merge-conflict": "pause", + "outlook-reminder": "skip", + "pdf-open-confirm": "pause", + "adobe-reader-update": "auto", + "chrome-update": "skip", + "edge-update": "skip", + "citrix-receiver-cert": "pause", + "citrix-file-access": "pause", + "nomachine-reconnect": "pause", + "nomachine-clipboard-fail": "auto", + # INCONNU + "unknown-dialog": "pause", +} + + +# ── Métadonnées modal (titre fenêtre + appli source + bascule déclarative) +# Le champ `declarative_override` indique si un workflow VWB peut surcharger +# la politique défaut en "auto" via expected_modal. Toujours False pour +# catégorie SYSTÈME (jamais surchargeable en healthtech). +MODAL_METADATA: Dict[str, Dict[str, object]] = { + # SYSTÈME + "windows-uac-elevation": { + "window_title": ["Contrôle de compte d'utilisateur", "User Account Control"], + "app_source": "Windows (consent.exe)", + "declarative_override": False, + }, + "windows-hello-pin": { + "window_title": ["Sécurité Windows", "Windows Security"], + "app_source": "Windows (Windows Hello — CredentialUIBroker.exe)", + "declarative_override": False, + }, + "windows-hello-fingerprint": { + "window_title": ["Sécurité Windows", "Windows Security"], + "app_source": "Windows (Windows Hello — biometric service)", + "declarative_override": False, + }, + "windows-hello-face": { + "window_title": ["Sécurité Windows", "Windows Security"], + "app_source": "Windows (Windows Hello Face — WinBioSvc)", + "declarative_override": False, + }, + "windows-hello-security-key": { + "window_title": ["Sécurité Windows", "Windows Security"], + "app_source": "Windows (WebAuthn / FIDO2)", + "declarative_override": False, + }, + "defender-smartscreen-app": { + "window_title": ["Windows a protégé votre PC", "Windows protected your PC"], + "app_source": "Windows (smartscreen.exe)", + "declarative_override": False, + }, + "defender-smartscreen-url": { + "window_title": ["Site signalé comme non sûr", "This site has been reported as unsafe"], + "app_source": "Microsoft Edge (SmartScreen URL filter)", + "declarative_override": False, + }, + "windows-defender-threat": { + "window_title": ["Sécurité Windows", "Windows Security"], + "app_source": "Windows Defender (MsMpEng.exe / SecHealthUI.exe)", + "declarative_override": False, + }, + "windows-credui-prompt": { + "window_title": ["Sécurité Windows", "Windows Security"], + "app_source": "Windows (Credential Manager — CredentialUIBroker.exe)", + "declarative_override": False, + }, + "windows-bitlocker-key": { + "window_title": ["Chiffrement de lecteur BitLocker", "BitLocker Drive Encryption"], + "app_source": "Windows (BitLocker — fvenotify.exe)", + "declarative_override": False, + }, + "windows-update-restart": { + "window_title": ["Windows Update", "Windows Update"], + "app_source": "Windows (Windows Update — MoUsoCoreWorker.exe)", + "declarative_override": False, + }, + "windows-firewall-allow": { + "window_title": ["Alerte de sécurité Windows", "Windows Security Alert"], + "app_source": "Windows (Pare-feu Defender)", + "declarative_override": False, + }, + "windows-driver-install": { + "window_title": ["Sécurité de Windows", "Windows Security"], + "app_source": "Windows (Plug and Play — drvinst.exe)", + "declarative_override": False, + }, + "windows-printer-setup": { + "window_title": ["Ajouter une imprimante", "Add a printer"], + "app_source": "Windows (Settings — printui.exe)", + "declarative_override": False, + }, + "windows-network-location": { + "window_title": "(pas de barre de titre — flyout Network Connection)", + "app_source": "Windows (Network Connection Flyout)", + "declarative_override": False, + }, + # NAVIGATEUR + "browser-perm-camera": { + "window_title": "(pas de barre de titre — bulle ancrée à la URL bar)", + "app_source": "Microsoft Edge / Chrome", + "declarative_override": True, + }, + "browser-perm-microphone": { + "window_title": "(pas de barre de titre — bulle ancrée à la URL bar)", + "app_source": "Microsoft Edge / Chrome", + "declarative_override": True, + }, + "browser-perm-notifications": { + "window_title": "(pas de barre de titre — bulle ancrée à la URL bar)", + "app_source": "Microsoft Edge / Chrome", + "declarative_override": True, + }, + "browser-perm-location": { + "window_title": "(pas de barre de titre — bulle ancrée à la URL bar)", + "app_source": "Microsoft Edge / Chrome", + "declarative_override": True, + }, + "browser-perm-midi": { + "window_title": "(pas de barre de titre — bulle ancrée à la URL bar)", + "app_source": "Microsoft Edge / Chrome", + "declarative_override": True, + }, + "browser-perm-clipboard": { + "window_title": "(pas de barre de titre — bulle ancrée à la URL bar)", + "app_source": "Microsoft Edge / Chrome", + "declarative_override": True, + }, + "browser-cert-select": { + "window_title": ["Sélectionner un certificat", "Select a certificate"], + "app_source": "Microsoft Edge / Chrome (mTLS — CryptoAPI)", + "declarative_override": False, + }, + "browser-basic-auth": { + "window_title": ["Se connecter", "Sign in"], + "app_source": "Microsoft Edge / Chrome (HTTP Basic Auth)", + "declarative_override": True, + }, + "browser-save-password": { + "window_title": "(pas de barre de titre — bulle Password Manager ancrée à la URL bar)", + "app_source": "Microsoft Edge / Chrome (Password Manager)", + "declarative_override": False, + }, + "browser-download-confirm": { + "window_title": "(pas de barre de titre — barre de téléchargement bas/haut écran)", + "app_source": "Microsoft Edge / Chrome (Downloads bar)", + "declarative_override": True, + }, + "browser-unsafe-page": { + "window_title": ["Votre connexion n'est pas privée", "Your connection is not private"], + "app_source": "Microsoft Edge / Chrome (interstitial SSL)", + "declarative_override": False, + }, + "browser-beforeunload": { + "window_title": "(pas de barre de titre — dialog JS modal au centre)", + "app_source": "Microsoft Edge / Chrome (window.onbeforeunload)", + "declarative_override": True, + }, + "browser-print-dialog": { + "window_title": ["Imprimer", "Print"], + "app_source": "Microsoft Edge / Chrome (boîte print intégrée)", + "declarative_override": True, + }, + "browser-translate-prompt": { + "window_title": "(pas de barre de titre — bulle ancrée à la URL bar)", + "app_source": "Microsoft Edge / Chrome (Translate)", + "declarative_override": False, + }, + "browser-page-unresponsive": { + "window_title": ["La page ne répond pas", "Page Unresponsive"], + "app_source": "Microsoft Edge / Chrome (crash dialog)", + "declarative_override": False, + }, + "browser-popup-blocked": { + "window_title": "(pas de barre de titre — icône bloqué dans la URL bar)", + "app_source": "Microsoft Edge / Chrome (popup blocker)", + "declarative_override": False, + }, + "browser-cookies-banner": { + "window_title": "(pas de barre de titre — bandeau site web in-page)", + "app_source": "Site Web (HTML/JS in-page banner)", + "declarative_override": True, + }, + # MÉTIER Easily + "easily-save-unconfirmed": { + "window_title": "Easily Assure - Confirmation", + "app_source": "Easily Assure", + "declarative_override": True, + }, + "easily-save-as": { + "window_title": ["Enregistrer sous", "Save As"], + "app_source": "Easily Assure", + "declarative_override": True, + }, + "easily-overwrite-file": { + "window_title": ["Confirmer l'enregistrement", "Confirm Save As"], + "app_source": "Easily Assure", + "declarative_override": True, + }, + "easily-confirm-action": { + "window_title": ["Confirmer", "Confirm"], + "app_source": "Easily Assure", + "declarative_override": True, + }, + "easily-delete-confirm": { + "window_title": "Easily Assure - Suppression", + "app_source": "Easily Assure", + "declarative_override": True, # nécessite confirm=true côté workflow + }, + "easily-send-confirm": { + "window_title": "Easily Assure - Envoi", + "app_source": "Easily Assure", + "declarative_override": True, + }, + "easily-session-expired": { + "window_title": "Easily Assure - Session", + "app_source": "Easily Assure", + "declarative_override": True, # uniquement si vault Léa actif + }, + "easily-required-field": { + "window_title": "Easily Assure - Validation", + "app_source": "Easily Assure", + "declarative_override": False, + }, + "easily-duplicate-ipp": { + "window_title": "Easily Assure - Doublon patient", + "app_source": "Easily Assure", + "declarative_override": False, + }, + "easily-clinical-warning": { + "window_title": "Easily Assure - Avertissement clinique", + "app_source": "Easily Assure", + "declarative_override": False, # jamais auto sur warning clinique + }, + "easily-toast-saved": { + "window_title": "(pas de barre de titre — toast in-app coin bas/haut)", + "app_source": "Easily Assure (toast non bloquant)", + "declarative_override": False, + }, + "easily-onboarding-tip": { + "window_title": "Easily Assure - Astuce", + "app_source": "Easily Assure (overlay onboarding)", + "declarative_override": True, + }, + "easily-help-popup": { + "window_title": ["Aide Easily", "Easily Help"], + "app_source": "Easily Assure (panneau aide)", + "declarative_override": True, + }, + "app-error-ok": { + "window_title": ["Erreur", "Error"], + "app_source": "Application métier (générique — Win32 MessageBox)", + "declarative_override": True, + }, + "app-error-suspect": { + "window_title": ["Erreur", "Error"], + "app_source": "Application métier (générique — blocklist SUSPECT_TOKENS)", + "declarative_override": False, + }, + # APP TIERS + "office-enable-content": { + "window_title": ["Avertissement de sécurité - Macros", "Security Warning - Macros"], + "app_source": "Microsoft Word / Excel / PowerPoint", + "declarative_override": False, + }, + "word-merge-conflict": { + "window_title": ["Microsoft Word - Conflit de fusion", "Microsoft Word - Merge Conflict"], + "app_source": "Microsoft Word (WINWORD.EXE)", + "declarative_override": False, + }, + "outlook-reminder": { + "window_title": ["Rappel - 1", "Reminder - 1"], + "app_source": "Microsoft Outlook (OUTLOOK.EXE)", + "declarative_override": False, + }, + "pdf-open-confirm": { + "window_title": "(pas de barre de titre — bulle ancrée à la URL bar / barre de téléchargement)", + "app_source": "Microsoft Edge / Chrome (file handler PDF)", + "declarative_override": True, + }, + "adobe-reader-update": { + "window_title": ["Adobe Acrobat Reader - Mise à jour", "Adobe Acrobat Reader - Update"], + "app_source": "Adobe Acrobat Reader DC (AcroRd32.exe / Acrobat.exe)", + "declarative_override": False, + }, + "chrome-update": { + "window_title": "(pas de barre de titre — bulle dans le menu Chrome)", + "app_source": "Google Chrome (chrome.exe — auto-updater)", + "declarative_override": False, + }, + "edge-update": { + "window_title": "(pas de barre de titre — bulle dans le menu Edge)", + "app_source": "Microsoft Edge (msedge.exe — auto-updater)", + "declarative_override": False, + }, + "citrix-receiver-cert": { + "window_title": ["Citrix Workspace", "Citrix Workspace App"], + "app_source": "Citrix Workspace (CDViewer.exe / SelfService.exe)", + "declarative_override": False, + }, + "citrix-file-access": { + "window_title": ["Sécurité Citrix Workspace", "Citrix Workspace Security"], + "app_source": "Citrix Workspace (HDX File Access)", + "declarative_override": False, + }, + "nomachine-reconnect": { + "window_title": ["NoMachine - Connexion perdue", "NoMachine - Connection lost"], + "app_source": "NoMachine viewer (nxplayer.exe)", + "declarative_override": False, + }, + "nomachine-clipboard-fail": { + "window_title": "NoMachine - Notification", + "app_source": "NoMachine viewer (nxplayer.exe — clipboard sync)", + "declarative_override": False, + }, + # INCONNU + "unknown-dialog": { + "window_title": "(variable - non catalogué)", + "app_source": "(variable - non identifiée)", + "declarative_override": False, + }, +} + + +# ── Signatures texte étendues (lowercase) ──────────────────────────────── +# Format : modal_id → (signatures_fr, signatures_en, button_labels) +CATALOGUE: Dict[str, Tuple[List[str], List[str], List[str]]] = { + # ── A. SYSTÈME ─────────────────────────────────────────────────────── + "windows-uac-elevation": ( + ["contrôle de compte d'utilisateur", "contrôle de compte dutilisateur", + "voulez-vous autoriser cette application"], + ["user account control", "do you want to allow this app", + "do you want to allow the following"], + ["Oui", "Non", "Yes", "No"], + ), + "windows-hello-pin": ( + ["windows hello", "saisissez votre code pin", "saisir votre code pin", + "entrez votre code pin"], + ["windows hello", "enter your pin"], + ["OK", "Annuler", "Cancel"], + ), + "windows-hello-fingerprint": ( + ["touchez le capteur d'empreintes", "analysez votre doigt", + "vérification de votre identité", "lecteur d'empreintes"], + ["touch the fingerprint sensor", "use your fingerprint", + "verify your identity"], + ["Annuler", "Cancel"], + ), + "windows-hello-face": ( + ["regardez la caméra", "reconnaissance faciale"], + ["look at the camera", "face recognition"], + ["Annuler", "Cancel"], + ), + "windows-hello-security-key": ( + ["insérez votre clé de sécurité", "appuyez sur votre clé", + "clé de sécurité fido"], + ["insert your security key", "touch your security key"], + ["Annuler", "Cancel"], + ), + "defender-smartscreen-app": ( + ["windows a protégé votre pc", "windows a protégé votre ordinateur", + "smartscreen a empêché", "éditeur inconnu", + "informations complémentaires", "exécuter quand même"], + ["windows protected your pc", "smartscreen prevented", + "unknown publisher", "run anyway", "more info"], + ["Exécuter quand même", "Ne pas exécuter", "Run anyway", "Don't run"], + ), + "defender-smartscreen-url": ( + ["smartscreen a signalé", "site potentiellement dangereux", + "site signalé comme non sûr"], + ["smartscreen reported", "potentially unsafe site", + "reported as unsafe"], + ["Retour à la sécurité", "Continuer", "Back to safety", "Continue"], + ), + "windows-defender-threat": ( + ["menace détectée", "windows defender", "logiciel malveillant", + "virus détecté"], + ["threat detected", "malware detected", "virus detected"], + ["Mettre en quarantaine", "Autoriser", "Quarantine", "Allow"], + ), + "windows-credui-prompt": ( + ["sécurité windows", "connectez-vous à votre compte", + "entrer les informations d'identification"], + ["windows security", "sign in to your account", + "enter your credentials"], + ["OK", "Annuler", "Cancel"], + ), + "windows-bitlocker-key": ( + ["clé de récupération bitlocker", "déverrouiller le lecteur"], + ["bitlocker recovery key", "unlock drive"], + ["Déverrouiller", "Unlock"], + ), + "windows-update-restart": ( + ["redémarrer maintenant", "mises à jour disponibles", + "redémarrage requis"], + ["restart now", "updates available", "restart required"], + ["Redémarrer maintenant", "Plus tard", "Restart now", "Later"], + ), + "windows-firewall-allow": ( + ["pare-feu windows defender", "autoriser l'accès"], + ["windows defender firewall", "allow access"], + ["Autoriser l'accès", "Annuler", "Allow access", "Cancel"], + ), + "windows-driver-install": ( + ["installer ce pilote", "signature numérique du pilote"], + ["install this driver", "driver signature"], + ["Installer", "Ne pas installer", "Install", "Don't install"], + ), + "windows-printer-setup": ( + ["ajouter une imprimante", "nouvelle imprimante détectée"], + ["add a printer", "new printer detected"], + ["Configurer", "Annuler", "Set up", "Cancel"], + ), + "windows-network-location": ( + ["type d'emplacement réseau", "domestique", "bureau", "public"], + ["network location type", "home", "work", "public"], + ["Domestique", "Bureau", "Public", "Home", "Work", "Public"], + ), + # ── B. NAVIGATEUR ──────────────────────────────────────────────────── + "browser-perm-camera": ( + ["souhaite utiliser votre caméra"], + ["wants to use your camera"], + ["Autoriser", "Bloquer", "Allow", "Block"], + ), + "browser-perm-microphone": ( + ["souhaite utiliser votre microphone", "souhaite utiliser votre micro"], + ["wants to use your microphone"], + ["Autoriser", "Bloquer", "Allow", "Block"], + ), + "browser-perm-notifications": ( + ["souhaite afficher des notifications"], + ["wants to show notifications"], + ["Autoriser", "Bloquer", "Allow", "Block"], + ), + "browser-perm-location": ( + ["souhaite connaître votre position"], + ["wants to know your location"], + ["Autoriser", "Bloquer", "Allow", "Block"], + ), + "browser-perm-midi": ( + ["souhaite utiliser vos appareils midi"], + ["wants to use your midi devices"], + ["Autoriser", "Bloquer", "Allow", "Block"], + ), + "browser-perm-clipboard": ( + ["souhaite accéder au presse-papiers"], + ["wants to access clipboard"], + ["Autoriser", "Bloquer", "Allow", "Block"], + ), + "browser-cert-select": ( + ["sélectionner un certificat", "authentification requise", + "choisissez un certificat"], + ["select a certificate", "authentication required", + "choose a certificate"], + ["OK", "Annuler", "Cancel"], + ), + "browser-basic-auth": ( + ["le site demande votre nom d'utilisateur", "connexion requise", + "authentification requise"], + ["sign in", "this site is asking you to sign in"], + ["Se connecter", "Annuler", "Sign in", "Cancel"], + ), + "browser-save-password": ( + ["voulez-vous enregistrer ce mot de passe", + "voulez-vous que google chrome enregistre"], + ["save password", "do you want to save this password"], + ["Enregistrer", "Jamais", "Save", "Never"], + ), + "browser-download-confirm": ( + ["conserver", "supprimer", "ouvrir"], + ["keep", "discard", "open"], + ["Conserver", "Supprimer", "Keep", "Discard"], + ), + "browser-unsafe-page": ( + ["votre connexion n'est pas privée", "site non sécurisé"], + ["your connection is not private", "not secure"], + ["Retour à la sécurité", "Avancé", "Back to safety", "Advanced"], + ), + "browser-beforeunload": ( + ["cette page demande de la fermer", + "voulez-vous quitter ce site", + "modifications non enregistrées seront perdues"], + ["do you want to leave this site", "leave site", + "changes you made may not be saved"], + ["Quitter", "Annuler", "Leave", "Cancel"], + ), + "browser-print-dialog": ( + ["imprimer", "destination", "pages"], + ["print", "destination", "pages"], + ["Imprimer", "Annuler", "Print", "Cancel"], + ), + "browser-translate-prompt": ( + ["traduire cette page", "souhaitez-vous traduire"], + ["translate this page"], + ["Traduire", "Non", "Translate", "No"], + ), + "browser-page-unresponsive": ( + ["cette page web ne répond pas", "tuer les pages"], + ["page unresponsive", "this page isn't responding", "kill pages"], + ["Attendre", "Quitter", "Wait", "Exit"], + ), + "browser-popup-blocked": ( + ["popup bloqué", "fenêtre publicitaire bloquée"], + ["pop-up blocked"], + ["Toujours autoriser", "Continuer à bloquer", + "Always allow", "Keep blocking"], + ), + "browser-cookies-banner": ( + ["accepter les cookies", "ce site utilise des cookies", + "nous utilisons des cookies"], + ["accept cookies", "this site uses cookies", "we use cookies"], + ["Accepter", "Refuser", "Personnaliser", "Accept", "Reject"], + ), + # ── C. MÉTIER Easily ──────────────────────────────────────────────── + "easily-save-unconfirmed": ( + ["voulez-vous enregistrer", "enregistrer les modifications", + "modifications non sauvegardées"], + ["do you want to save", "unsaved changes"], + ["Enregistrer", "Ne pas enregistrer", "Annuler", + "Save", "Don't save", "Cancel"], + ), + "easily-save-as": ( + ["enregistrer sous"], + ["save as"], + ["Enregistrer", "Annuler", "Save", "Cancel"], + ), + "easily-overwrite-file": ( + ["voulez-vous le remplacer", "existe déjà", "écraser", "remplacer"], + ["do you want to replace", "already exists", "overwrite", "replace"], + ["Oui", "Non", "Yes", "No"], + ), + "easily-confirm-action": ( + ["confirmer l'enregistrement", "confirmer", "êtes-vous sûr"], + ["confirm", "are you sure"], + ["Oui", "Non", "Yes", "No"], + ), + "easily-delete-confirm": ( + ["voulez-vous supprimer", "confirmer la suppression", + "irréversible"], + ["do you want to delete", "confirm deletion", "cannot be undone"], + ["Oui", "Non", "Supprimer", "Delete"], + ), + "easily-send-confirm": ( + ["voulez-vous envoyer", "confirmer l'envoi"], + ["do you want to send"], + ["Envoyer", "Annuler", "Send", "Cancel"], + ), + "easily-session-expired": ( + ["session expirée", "veuillez vous reconnecter", "timeout"], + ["session expired", "please log in again"], + ["OK"], + ), + "easily-required-field": ( + ["champ obligatoire manquant", "veuillez remplir", "champ requis"], + ["required field missing", "please fill", "required field"], + ["OK"], + ), + "easily-duplicate-ipp": ( + ["patient déjà saisi", "doublon ipp", "patient existe déjà"], + ["duplicate patient", "patient already exists"], + ["Continuer", "Annuler", "Fusionner", + "Continue", "Cancel", "Merge"], + ), + "easily-clinical-warning": ( + ["avertissement clinique", "attention", "vérifier", + "allergie connue", "contre-indication"], + ["clinical alert", "warning", "verify", "known allergy"], + ["OK", "Continuer", "Continue"], + ), + "easily-toast-saved": ( + ["enregistré avec succès", "sauvegardé", "modifications enregistrées"], + ["saved successfully", "changes saved"], + [], + ), + "easily-onboarding-tip": ( + ["astuce", "saviez-vous que", "découvrez"], + ["did you know", "tip", "discover"], + ["OK", "Suivant", "Plus tard", "Next", "Later"], + ), + "easily-help-popup": ( + ["aide", "tutoriel", "guide"], + ["help", "tutorial"], + ["Fermer", "Suivant", "Close", "Next"], + ), + "app-error-ok": ( + ["erreur", "avertissement", "une erreur s'est produite"], + ["error", "warning", "an error occurred"], + ["OK"], + ), + "app-error-suspect": ( + ["données perdues", "supprimé définitivement", "irréversible", + "vider la corbeille", "formater", "effacer toutes"], + ["data lost", "permanently deleted", "irreversible", + "empty trash", "format", "erase all"], + ["OK", "Annuler", "Cancel"], + ), + # ── D. APP TIERS ──────────────────────────────────────────────────── + "office-enable-content": ( + ["activer le contenu", "macros désactivées", + "avertissement de sécurité"], + ["enable content", "macros disabled", "security warning"], + ["Activer le contenu", "Annuler", "Enable content", "Cancel"], + ), + "word-merge-conflict": ( + ["conflit de fusion", "votre version", "version du serveur"], + ["merge conflict", "your version", "server version"], + ["Conserver la mienne", "Conserver leur", "Fusionner", + "Keep mine", "Keep theirs", "Merge"], + ), + "outlook-reminder": ( + ["rappel", "réunion", "rendez-vous"], + ["reminder", "meeting", "appointment"], + ["Ignorer", "Reporter", "Dismiss", "Snooze"], + ), + "pdf-open-confirm": ( + ["voulez-vous ouvrir", "type de fichier"], + ["do you want to open"], + ["Ouvrir", "Enregistrer", "Annuler", + "Open", "Save", "Cancel"], + ), + "adobe-reader-update": ( + ["mise à jour disponible", "adobe acrobat reader"], + ["update available", "adobe reader"], + ["Installer", "Plus tard", "Install", "Later"], + ), + "chrome-update": ( + ["google chrome mis à jour", "redémarrer chrome"], + ["google chrome was updated", "relaunch"], + ["Relancer", "Plus tard", "Relaunch", "Later"], + ), + "edge-update": ( + ["microsoft edge a été mis à jour"], + ["microsoft edge was updated"], + ["Redémarrer", "Plus tard", "Restart", "Later"], + ), + "citrix-receiver-cert": ( + ["ajouter ce certificat", "citrix receiver", "workspace app"], + ["add this account", "citrix workspace"], + ["OK", "Annuler", "Cancel"], + ), + "citrix-file-access": ( + ["autoriser l'accès aux fichiers locaux", "lecture/écriture"], + ["allow access to local files"], + ["Autoriser", "Refuser", "Allow", "Deny"], + ), + "nomachine-reconnect": ( + ["la connexion a été perdue", "tenter de reconnecter"], + ["connection lost", "try to reconnect"], + ["Reconnecter", "Quitter", "Reconnect", "Quit"], + ), + "nomachine-clipboard-fail": ( + ["presse-papiers", "clipboard non disponible"], + ["clipboard not available"], + ["OK"], + ), +} + + +# ── Helpers de matching ───────────────────────────────────────────────── + +def match_modal_id(ocr_text: str) -> str | None: + """Retourne l'ID modal qui matche le texte OCR, ou None. + + Ordre du dict CATALOGUE = priorité de matching. Les modaux les plus + spécifiques (popups modaux) doivent être listés avant les fenêtres + parents (cf. dialog_handler.KNOWN_DIALOGS commentaire). + """ + text_lower = ocr_text.lower() + if not text_lower: + return None + for modal_id, (sigs_fr, sigs_en, _btns) in CATALOGUE.items(): + for sig in sigs_fr + sigs_en: + if sig in text_lower: + return modal_id + return None + + +def get_policy(modal_id: str) -> str: + """Retourne la politique stricte pour un modal_id donné. + + Valeurs : "auto" | "pause" | "skip". Default = "pause". + """ + return POLICY_BY_MODAL_ID.get(modal_id, "pause") + + +def get_button_labels(modal_id: str) -> List[str]: + """Retourne les labels de boutons standards pour un modal_id.""" + entry = CATALOGUE.get(modal_id) + return entry[2] if entry else [] + + +def get_metadata(modal_id: str) -> Dict[str, object]: + """Retourne window_title, app_source et declarative_override.""" + return MODAL_METADATA.get(modal_id, { + "window_title": "(inconnu)", + "app_source": "(inconnu)", + "declarative_override": False, + }) + + +def can_be_overridden(modal_id: str) -> bool: + """True si un workflow VWB peut surcharger la politique en 'auto'.""" + meta = MODAL_METADATA.get(modal_id, {}) + return bool(meta.get("declarative_override", False)) +``` + +--- + +## 7. Tests offline — protocole + +### 7.1. Captures à se procurer / générer + +| Modal | Source capture | Méthode acquisition | +|---|---|---| +| `windows-uac-elevation` | runtime Windows réel | lancer un MSI sans droits admin → capture full-screen → `data/dialog_fixtures/uac_fr.png` | +| `windows-hello-fingerprint` | runtime Windows réel | Chrome login site avec password sauvé → Hello popup → capture → `data/dialog_fixtures/hello_fingerprint_fr.png` | +| `defender-smartscreen-app` | runtime Windows réel | lancer exe non signé téléchargé → capture → `data/dialog_fixtures/smartscreen_fr.png` | +| `browser-perm-microphone` | site test WebRTC | https://webrtc.github.io/samples/src/content/devices/input-output/ → capture popup permission | +| `easily-save-unconfirmed` | maquette urgence.labs ou Easily prod | fermer dossier non sauvé → capture → `data/dialog_fixtures/easily_save_fr.png` | +| `easily-overwrite-file` | maquette | sauvegarder T2A avec nom existant → capture | +| `easily-delete-confirm` | maquette | clic suppression dossier → capture | +| `office-enable-content` | Excel avec macros | ouvrir .xlsm → capture | +| `unknown-dialog` | n/a | synthétique : générer un dialog dummy via Python+tkinter avec texte aléatoire | + +### 7.2. Script de test offline + +```python +# scripts/test_popup_catalogue.py +"""Test du catalogue popups en offline (lecture des fixtures PNG). + +Vérifie pour chaque fixture : + 1. ChangeDetector signale is_modal=True + 2. DialogClassifier retourne le bon modal_id via OCR + signature + 3. Policy correspond à la matrice attendue + +Usage : + cd /home/dom/ai/rpa_vision_v3 && source venv_v3/bin/activate + python scripts/test_popup_catalogue.py +""" +from pathlib import Path + +from PIL import Image + +from core.dialog.signatures_catalogue import ( + CATALOGUE, POLICY_BY_MODAL_ID, match_modal_id, get_policy, +) + + +FIXTURES_DIR = Path("data/dialog_fixtures") + +EXPECTED = { + "uac_fr.png": ("windows-uac-elevation", "pause"), + "hello_fingerprint_fr.png": ("windows-hello-fingerprint", "pause"), + "smartscreen_fr.png": ("defender-smartscreen-app", "pause"), + "easily_save_fr.png": ("easily-save-unconfirmed", "auto"), + "easily_overwrite_fr.png": ("easily-overwrite-file", "auto"), + "easily_delete_fr.png": ("easily-delete-confirm", "pause"), + "office_macros_fr.png": ("office-enable-content", "pause"), +} + + +def ocr_pil(img: Image.Image) -> str: + import easyocr + import numpy as np + reader = easyocr.Reader(['fr', 'en'], gpu=True, verbose=False) + return ' '.join(r[1] for r in reader.readtext(np.array(img))) + + +def main(): + results = [] + for fname, (expected_id, expected_policy) in EXPECTED.items(): + path = FIXTURES_DIR / fname + if not path.exists(): + print(f"SKIP {fname} (fixture absente)") + continue + img = Image.open(path) + ocr_text = ocr_pil(img) + detected_id = match_modal_id(ocr_text) + detected_policy = get_policy(detected_id) if detected_id else "none" + ok = (detected_id == expected_id and detected_policy == expected_policy) + results.append((fname, expected_id, detected_id, expected_policy, detected_policy, ok)) + status = "OK" if ok else "FAIL" + print(f"[{status}] {fname}") + print(f" expected: id={expected_id} policy={expected_policy}") + print(f" detected: id={detected_id} policy={detected_policy}") + if not ok: + print(f" ocr_text: {ocr_text[:200]}") + n_ok = sum(1 for r in results if r[5]) + print(f"\n{n_ok}/{len(results)} fixtures matchent") + + +if __name__ == "__main__": + main() +``` + +### 7.3. Test pytest + +```python +# tests/unit/test_popup_catalogue.py +import pytest + +from core.dialog.signatures_catalogue import ( + CATALOGUE, POLICY_BY_MODAL_ID, match_modal_id, get_policy, +) + + +def test_catalogue_keys_consistency(): + """Tous les modal_id de CATALOGUE ont une politique définie.""" + for modal_id in CATALOGUE: + assert modal_id in POLICY_BY_MODAL_ID, f"Politique manquante pour {modal_id}" + + +def test_suppression_modals_default_pause(): + """Tous les modaux de suppression sont 'pause' par défaut.""" + for modal_id in ("easily-delete-confirm",): + assert get_policy(modal_id) == "pause" + + +def test_system_modals_always_pause(): + """Tous les modaux système (UAC/Hello/SmartScreen/Windows*/Defender) sont 'pause'.""" + system_modals = [m for m in CATALOGUE if m.startswith(("windows-", "defender-"))] + for modal_id in system_modals: + assert get_policy(modal_id) == "pause", \ + f"{modal_id} doit être 'pause' (catégorie SYSTÈME), pas {get_policy(modal_id)}" + + +@pytest.mark.parametrize("ocr_text,expected_id", [ + ("Voulez-vous enregistrer les modifications apportées au dossier ?", + "easily-save-unconfirmed"), + ("Contrôle de compte d'utilisateur — voulez-vous autoriser cette application", + "windows-uac-elevation"), + ("Windows a protégé votre PC — Microsoft Defender SmartScreen a empêché", + "defender-smartscreen-app"), + ("urgence.labs souhaite utiliser votre microphone — Autoriser Bloquer", + "browser-perm-microphone"), + ("Voulez-vous le remplacer ? Le fichier existe déjà", + "easily-overwrite-file"), + ("Windows Hello — Touchez le capteur d'empreintes digitales", + "windows-hello-fingerprint"), +]) +def test_match_modal_id_from_ocr(ocr_text, expected_id): + assert match_modal_id(ocr_text) == expected_id + + +def test_unknown_text_returns_none(): + assert match_modal_id("Texte aléatoire qui ne matche rien d'spécial") is None +``` + +Lancement : +```bash +pytest tests/unit/test_popup_catalogue.py -v +``` + +--- + +## 8. Précédents externes — 6 fiches courtes + +### 8.1. Skyvern (`Skyvern-AI/skyvern`) + +- **Approche dialog** : pas de `dialog_handler` nommé. La détection passe indirectement par le **Validator** : un check post-action qui échoue (texte attendu absent, page non chargée) déclenche un rerun du Planner. +- **Issue trace** : commit *"Unblock popup and split chromium preferences for OSS/cloud (#SKY-8474) (#5253)"* — popup management interne au navigateur (Playwright `dialog.accept()`), pas un catalogue. +- **Politique** : 12 catégories d'échec (`FailureCategory`), dont `WRONG_PAGE_STATE` couvre l'apparition d'un modal inattendu. +- **À retenir** : la coordination Validator → DialogHandler (notre `failure_category=UNEXPECTED_DIALOG` → `DialogResolver`) est l'adaptation de leur pattern. +- **Source** : + +### 8.2. browser-use + +- **Approche** : Issue #1996 (mai 2026) *"Need Robust Strategy for Handling Dynamic Popups"* — la communauté demande explicitement une infra générique. **Pas de solution intégrée à mai 2026**. +- **Permission prompts** : repose sur Chromium DevTools Protocol (`Permissions.SetPermission`) pour pré-configurer caméra/micro/notif AVANT lancement — c'est notre `expected_modal.declared_dialogs`. +- **À retenir** : la déclaration anticipée au niveau workflow est l'état de l'art browser, pas une particularité healthtech. +- **Source** : + +### 8.3. Anthropic Computer Use (Claude) + +- **Politique 2026** : *"Computer Use requires asking a human to confirm decisions that might result in meaningful real-world consequences and any tasks requiring affirmative consent"* (cookies, transactions financières, ToS). +- **Mécanisme** : pas de catalogue de signatures texte. Le **modèle lui-même** identifie le besoin de pause via reasoning sur le screenshot. +- **À retenir** : approche "modèle = juge" inadaptée à healthtech à latence + coût. On préfère un catalogue déterministe + VLM compact en fallback. +- **Source** : + + +### 8.4. OpenAI Operator / ChatGPT Agent + +- **Takeover Mode** : *"When Operator encounters a step that requires logging into a website or entering a password, it should pause and prompt you to take over. In takeover mode, Operator does not collect or screenshot information entered by the user."* +- **Watch Mode** : sites sensibles (email, financier) demandent supervision active. +- **À retenir** : + 1. Pas de catalogue public. Détection contextuelle par le modèle principal. + 2. **Screenshot OFF pendant le takeover** — patron à adopter pour Léa : pendant la pause supervisée, suspendre la capture pour éviter de logger des identifiants saisis manuellement. +- **Source** : + + +### 8.5. AutoIt / Sikuli (patterns historiques Windows) + +- **AutoIt `WinWaitActive`** : `WinWaitActive("Open", "", 10)` attend jusqu'à 10 s qu'une fenêtre de titre `Open` devienne active. C'est l'ancêtre déterministe du `ChangeDetector`. +- **Sikuli** : reconnaissance d'image pour cliquer sur des boutons identifiés par leur capture. Approche 100% vision, mais sans signature texte → fragile aux changements de thème. +- **À retenir** : pattern `wait_for_window(title_regex, timeout)` reste pertinent comme **complément** à notre `ChangeDetector` ; on s'en inspire pour la déclaration `expected_modal.timeout_ms`. +- **Source** : + +### 8.6. pywinauto / UIAutomation + +- **Détection programmatique** : `Application().connect(title='Contrôle de compte')` ou via `class_name='$$$Secure UAP Dummy Window Class$$$'`. +- **Notre usage** : `core/system_dialog_guard.py:107` exploite déjà ces ClassName UIA comme **signal complémentaire** (pas source unique car SSH/secure desktop renvoient 0). +- **À retenir** : UIA est fiable sur Windows physique, **inutilisable** en session SSH ou VM headless — d'où la stratégie multi-signal (texte OCR + ClassName UIA + process + diff visuel). +- **Source** : + +### 8.7. Selenium (bonus — JavaScript alerts) + +- **API** : `driver.switch_to.alert.accept()` / `dismiss()`. Limité aux dialogs JS (`alert()`, `confirm()`, `prompt()`). **Ne fonctionne PAS** pour les dialogs OS natifs (Windows, file picker, print). +- **À retenir** : aucune valeur pour notre stack (Léa = OS-level vision, pas browser DOM). Mais c'est le pattern attendu par les workflows VWB qui veulent déclarer `expected_modal` — l'analogie est utile pour expliquer aux développeurs VWB. +- **Source** : + +--- + +## 9. Sources externes (liens cliquables) + +### Microsoft / Windows +- [Microsoft UAC architecture docs](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/user-account-control/how-it-works) +- [Microsoft Windows Hello for Business overview](https://learn.microsoft.com/en-us/windows/security/identity-protection/hello-for-business/) +- [Microsoft WebAuthn APIs](https://learn.microsoft.com/en-us/windows/security/identity-protection/hello-for-business/webauthn-apis) +- [Microsoft Defender SmartScreen docs](https://learn.microsoft.com/en-us/windows/security/operating-system-security/virus-and-threat-protection/microsoft-defender-smartscreen/) +- [MSP360 SmartScreen KB](https://kb.msp360.com/backup/warnings/ms-defender-smart-screen) +- [The Windows Club SmartScreen](https://www.thewindowsclub.com/microsoft-defender-smartscreen-prevented-an-unrecognized-app-from-starting) +- [NinjaOne UAC behavior Windows 11](https://www.ninjaone.com/blog/change-uac-behavior-for-administrators-in-windows-11/) + +### Frameworks computer-use +- [Skyvern GitHub](https://github.com/Skyvern-AI/skyvern) +- [browser-use issue #1996 popups](https://github.com/browser-use/browser-use/issues/1996) +- [Anthropic Computer Use docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool) +- [Anthropic Claude 4 System Card](https://www.anthropic.com/claude-4-system-card) +- [OpenAI Operator introduction](https://openai.com/index/introducing-operator/) +- [OpenAI ChatGPT Agent help](https://help.openai.com/en/articles/11752874-chatgpt-agent) +- [WorkOS — Securing AI agents authentication patterns](https://workos.com/blog/securing-ai-agents-operator-models-and-authentication) + +### Browser permissions +- [MDN permissions.request API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/permissions/request) +- [BrowserStack Selenium handling permission popups](https://www.browserstack.com/docs/automate/selenium/handle-permission-pop-ups) +- [Selenium alerts/prompts/confirmations](https://www.selenium.dev/documentation/webdriver/interactions/alerts/) +- [Mozilla popup blocker settings](https://support.mozilla.org/en-US/kb/pop-blocker-settings-exceptions-troubleshooting) + +### FIDO2 / WebAuthn / Auth +- [Token2 FIDO2 across OS/browsers](https://www.token2.com/site/page/understanding-fido2-authentication-across-different-operating-systems-and-browsers) +- [Duo Universal Prompt Enrollment](https://guide.duo.com/universal-enrollment/) + +### Automation patterns +- [pywinauto application module docs](https://pywinauto.readthedocs.io/en/latest/code/pywinauto.application.html) +- [AutoIt window titles and text](https://www.autoitscript.com/autoit3/docs/intro/windowsbasic.htm) +- [Netwoven Sikuli Windows authentication popup](https://netwoven.com/custom-development/automate-windows-authentication-popup-in-selenium-using-sikuli/) + +### Sécurité 2026 +- [CVE-2026-0628 Chrome Gemini Live panel](https://news.corksafetyalerts.com/chrome-flaw-allowed-extensions-to-hijack-googles-ai-assistant-camera-and-microphone/) +- [AI-powered phishing leveraging hardware access](https://www.scworld.com/brief/ai-powered-phishing-campaign-leverages-hardware-access-for-data-theft) + +### Documents internes (lecture seule) +- `docs/recherche/AXE_D2_DIALOG_POPUP.md` +- `docs/recherche/AXE_D2_DEEP_POPUP_CHAIN.md` +- `docs/recherche/AXE_B2_DEEP_VALIDATOR.md` +- `core/grounding/dialog_handler.py` (`KNOWN_DIALOGS` historique) +- `agent_v0/agent_v1/core/system_dialog_guard.py` (détection multi-signal) +- `memory/feedback_popup_vlm.md` +- `memory/feedback_auth_dialogs_runtime.md` +- `memory/feedback_lea_reflexes_catalog.md` +- `memory/feedback_100pct_visual.md` +- `memory/feedback_failure_is_learning.md` + +--- + +## 10. Hors-périmètre de ce doc (questions ouvertes pour Dom) + +Les décisions auto/pause des entrées ci-dessous sont **ambiguës** et méritent validation explicite Dom avant câblage : + +| Modal | Décision proposée | Question ouverte | +|---|---|---| +| `easily-required-field` | `AUTO_DISMISS` | Risque de masquer un bug workflow (champ jamais rempli car résolution UI cassée). Alternative : pause + re-grounding du champ. | +| `easily-session-expired` | `ASK_HUMAN` | Si vault Léa généralisé, peut-on basculer en `DECLARATIVE` automatique ? Implique rotation tokens + audit accru. | +| `browser-perm-microphone` (Easily Assure dictée vocale) | `ASK_HUMAN` par défaut | Si Easily ajoute dictée, faut-il déclarer le permission micro en `declared_dialogs` global ou par workflow ? | +| `easily-clinical-warning` | `ASK_HUMAN` | Toujours pause ou auto-dismiss si message non bloquant (toast) avec catalogue spécifique allergies/contre-indications ? Risque clinique. | +| `outlook-reminder` | `AUTO_DISMISS` | Si Léa minimise tous les rappels pendant 1 h, risque de masquer un rappel professionnel important. Préférer notification serveur ? | +| `app-error-ok` | `AUTO_DISMISS` | Catch-all dangereux. À limiter aux messages catalogués explicitement (whitelist plutôt que blocklist). | + +### Liens transversaux + +- **`spec_transport`** (à venir si jalonné) : la détection modal post-action repose sur un transport fiable. Si SSE/WebSocket adopté (cf. `AXE_B1`), le `DialogEvent` peut être pushé en temps réel au dashboard plutôt que via long-poll. +- **`spec_validator`** (`AXE_B2_DEEP_VALIDATOR.md`) : interface `FailureCategory.UNEXPECTED_DIALOG` → handoff `DialogResolver`. Contrat clair, déjà documenté. +- **Vault credentials Léa** (`core/auth/credential_vault.py`) : permet long terme de basculer certains modaux auth en `DECLARATIVE` avec audit (`browser-basic-auth`, `easily-session-expired`). + +--- + +*Document opérationnel destiné à devenir la source de vérité du package `core/dialog/`. Lecture seule. Toute mise en code → décision Dom + chirurgie itérative supervisée (CLAUDE.md projet).* diff --git a/docs/recherche/SPEC_TRANSPORT_CONTRAT.md b/docs/recherche/SPEC_TRANSPORT_CONTRAT.md new file mode 100644 index 000000000..d285e9f78 --- /dev/null +++ b/docs/recherche/SPEC_TRANSPORT_CONTRAT.md @@ -0,0 +1,766 @@ +# SPEC TRANSPORT — Contrat dispatch / ack / retry / orphan / resume + +**Date :** 2026-05-24 +**Auteur :** Claude (recherche dispatchée, lecture seule sur code) +**Statut :** spécification contractuelle. Aucune modif code. Toutes les sections sont en tables ou state machines, prose minimale. +**Pré-requis :** +- `docs/recherche/AXE_B1_REPLAY_TRANSPORT.md` (transport SSE/WebSocket — choix techno) +- `docs/recherche/AXE_B1_DEEP_WATCHDOG.md` (implémentation watchdog) +- `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` (bug 9 actions perdues) +- `docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` §4 + +Ce doc fige **le contrat** entre `api_stream.py` (FastAPI Linux) et `agent_v1/core/executor.py` + `agent_v1/network/streamer.py` (Léa Windows). Il est **isomorphe poll ↔ SSE** (cf. §9). + +--- + +## 1. TL;DR + diagramme d'ensemble + +Une action visuelle est un **message borné par 2 IDs** : `action_id` (unique par step de replay, déterministe) + `attempt_id` (UUID, incrémenté à chaque re-dispatch transport). Le serveur tient une **mini-visibility-timeout in-memory** (`_retry_pending`). Le client tient un **LRU `dedup_set`** des `attempt_id` récemment exécutés. La pause supervisée (`paused_need_help`) est l'état absorbant en cas d'épuisement des essais ou de signal explicite (`system_dialog`, `wrong_window`, `target_not_found`). Le contrat est identique en polling (transport actuel) et en SSE (cible AXE_B1). + +``` + ┌───────────── server (Linux, FastAPI :5005) ─────────────┐ + │ │ + _replay_queues │ ┌──────────┐ DISPATCH ┌─────────────┐ │ + [session_id] │ │ PENDING │ ──────────────►│ DISPATCHED │ │ + │ └──────────┘ │ (_retry_ │ │ + │ ▲ │ pending) │ │ + │ │ repush head └─────┬───────┘ │ + │ │ (watchdog) │ REPORT(success) │ + │ ┌────┴─────┐ age>30s, resent │ verify OK │ + │ │ ORPHAN │ ◄─── timeout ◄────────┤ │ + │ │ (resent_ │ │ │ + │ │ count++)│ ▼ │ + │ └──────────┘ ┌──────────┐ │ + │ │ resent ≥ MAX │ ACKED │ │ + │ ▼ └──────────┘ │ + │ ┌──────────┐ REPORT(fail+system_dialog/wrong_window) │ + │ │ ABANDONED│◄──┬────────────────┐ │ + │ └──────────┘ │ │ │ + │ │ ▼ ▼ │ + │ │ ┌──────────────┐ ┌──────────┐ │ + │ └──►│ PAUSE_NEED_ │ │ FAILED │ │ + │ │ HELP │ │ (retry │ │ + │ └─────┬────────┘ │ budget │ │ + │ │ /resume │ out) │ │ + │ ▼ └──────────┘ │ + │ (resume_action en tête queue) │ + └──────────────────────────────────────────────────────────┘ + ▲ │ + │ POST /replay/result │ GET /replay/next (poll) + │ │ ou SSE event + │ ▼ + ┌───────────┴──────────── client Léa (Windows) ────────────┐ + │ ┌──────────┐ parse ┌──────────┐ execute_replay_ │ + │ │ RECEIVED │──────────►│ DEDUP │ action() │ + │ └──────────┘ │ CHECK │ │ + │ └────┬─────┘ │ + │ hit LRU │ miss │ + │ │ ▼ │ + │ │ ┌──────────┐ │ + │ │ │EXECUTING │ │ + │ │ └────┬─────┘ │ + │ ▼ ▼ │ + │ ┌──────────────┐ │ + │ │ REPORTING │ → POST /replay/result │ + │ └──────────────┘ │ + └──────────────────────────────────────────────────────────┘ +``` + +--- + +## 2. State machine serveur — une action dans `_retry_pending` + +``` + start + │ + ▼ + ┌───────────────────────────────────────────────────────────────┐ + │ PENDING │ + │ présent dans _replay_queues[session_id], pas encore │ + │ extrait par get_next_action │ + └───────────────────────┬───────────────────────────────────────┘ + │ get_next_action → pop queue + write + │ _retry_pending[action_id]={dispatched_at=now} + │ + log [REPLAY] DISPATCH + ▼ + ┌───────────────────────────────────────────────────────────────┐ + │ DISPATCHED │ + │ dans _retry_pending, attente du REPORT │ + │ invariant : action absente de _replay_queues │ + └───┬───────────────────────┬───────────────────────────────────┘ + │ REPORT(success=True) │ REPORT(success=False, error=*) + │ verify OK ou skip │ watchdog scan age>ORPHAN_TIMEOUT + │ │ + │ pop _retry_pending │ resent_count < MAX_RESENDS + │ completed_actions++ │ → repush head + resent++ + │ current_idx++ │ → dispatched_at=0 + ▼ │ → log [BUS] lea:dispatch_ + ┌────────┐ │ orphan_resent + │ ACKED │ │ + │ (term) │ │ resent_count ≥ MAX_RESENDS + └────────┘ │ → pop _retry_pending + │ → log [BUS] lea:dispatch_ + │ orphan_giveup + │ → ABANDONED + ▼ + ┌─────────────────┐ watchdog repush + │ ORPHAN_RESENT │ ───────────► PENDING + │ (transitoire) │ (avec resent++) + └─────────────────┘ + + REPORT(verify failed AND retry_count/cancel): + purge _retry_pending par replay_id (api_stream.py:4489) + _replay_queues[session_id] = [] + state.status = "cancelled" +``` + +**Invariants serveurs :** + +| # | Invariant | Garanti par | +|---|---|---| +| I1 | Une action en `DISPATCHED` est absente de `_replay_queues` | pop atomique sous `_replay_lock` (api_stream.py:3346-3348) | +| I2 | `action_id` unique dans `_retry_pending` à un instant t | clé dict + check `if action_id_sent not in _retry_pending` (api_stream.py:3354) | +| I3 | `report_action_result.pop(action_id)` est idempotent | `pop(key, None)` retourne None si déjà acquitté (api_stream.py:3491) | +| I4 | Cancel purge bien `_retry_pending` pour ce replay | iter `_retry_pending.items() if v["replay_id"]==replay_id` (api_stream.py:4489-4491) | +| I5 | Watchdog re-check sous lock avant repush | pattern `if aid not in self._retry_pending: skip` (AXE_B1_DEEP §3) | +| I6 | Pause `paused_need_help` ne distribue aucune action | `get_next_action` retourne `replay_paused=True` (api_stream.py:2951) | + +--- + +## 3. State machine client Léa — une action côté `executor.py` + +``` + ┌──────────────┐ + │ POLLING │ thread `_poll_loop`, every 1s (+backoff) + │ (idle) │ GET /replay/next?session_id&machine_id + └──────┬───────┘ + │ HTTP 200 + action ≠ null + ▼ + ┌──────────────┐ + │ RECEIVED │ data["action"] parsé + └──────┬───────┘ + │ + ┌───────────────┴────────────────┐ + │ attempt_id ∈ dedup_set ? │ + ▼ ▼ + OUI : SKIP NON + (log warning, ack synthetique) │ + ▼ + ┌──────────────┐ + │ EXECUTING │ execute_replay_action() + │ │ ├─ pre-check window + │ │ ├─ resolve target visuel + │ │ ├─ click / type / key + │ │ └─ screenshot_after + └──────┬───────┘ + │ + ┌─────────────────┼──────────────────┐ + │ │ │ + success=True success=False system_dialog + warning=None error=* détecté + │ │ │ + ▼ ▼ ▼ + ┌─────────────────────────────────┐ + │ REPORTING │ + │ POST /replay/result + retry │ + │ (timeout=10s, allow_redirects=False) + └─────────────────┬────────────────┘ + │ + ┌──────────┴──────────┐ + HTTP 200 fail/timeout + │ │ + ▼ ▼ + ┌──────────┐ ┌──────────────┐ + │ ACKED │ │ REPORT_RETRY │ (PAS implémenté + │ + LRU │ │ (in-memory) │ v1 ; à v2) + │ store │ └──────┬───────┘ + │ attempt │ │ + │ _id │ ▼ + └──────────┘ (perte report : serveur watchdog + rattrapera via orphan) + + Pendant POLLING : si data.replay_paused → afficher PauseDialog + Pendant EXECUTING : timeout par étape gérée par execute_replay_action + (resolve serveur 30s, _wait_for_screen_change 1000ms+, capture 0.5s) +``` + +**Invariants clients :** + +| # | Invariant | Garanti par | +|---|---|---| +| C1 | Une action reçue est TOUJOURS reportée (succès ou échec) | try/except global executor.py:2429-2503, fallback `result={success:False, error=…}` | +| C2 | Un seul `poll_and_execute` à la fois | `self._replay_lock.acquire(blocking=False)` executor.py:2291 | +| C3 | Pas de blocage event tray UI pendant exécution | thread dédié `_poll_loop` | +| C4 | Idempotence côté action (v2) | dedup_set LRU bornée 256 entrées sur `attempt_id` (§6.2) — **À AJOUTER** | +| C5 | Pause UI déclenchée uniquement sur signal serveur explicite | `data.get("replay_paused")` executor.py:2346 | + +--- + +## 4. Contrats JSON + +### 4.1. Payload DISPATCH serveur → client (GET /replay/next OU SSE event) + +**Cas nominal : action visuelle** + +| Champ | Type | Obligatoire | Description | Source code | +|---|---|---|---|---| +| `action` | object \| null | oui | l'action ou `null` si rien | api_stream.py:3436 | +| `session_id` | str | oui | session active | api_stream.py:3438 | +| `machine_id` | str | oui | machine cible | api_stream.py:3439 | +| `action.action_id` | str | oui | identifiant unique step, ex. `step_4c0663941f22` | DB workflow + suffixes `_retry1/_resume` | +| `action.attempt_id` | str | **À AJOUTER** | UUID hex 16, nouveau à chaque dispatch (initial OU resend) | n/a, v2 | +| `action.type` | enum | oui | `click`/`type`/`key_combo`/`wait`/`scroll`/`pause_for_human`/`extract_text`/... | core/executor.py:2422 | +| `action.target_spec` | object | si visuelle | `{by_text,vlm_description,anchor_image_base64,resolve_order,window_title,uia_target}` | api_stream.py:3364 | +| `action.parameters` | object | dep. type | `{text,keys,duration_ms,condition,…}` | dépend du type | +| `action.expected_window_before` | str | non | titre fenêtre attendue avant clic | api_stream.py:3366 | +| `action.expected_window_title` | str | non | titre fenêtre attendue après clic | api_stream.py:3369 | +| `action.success_strict` | bool | non | mode strict (skip OCR fuzzy) | api_stream.py:3387 | +| `action.intention` | str | non | description humaine | api_stream.py:3379 | +| `action.monitor_resolution` | object | oui (QW1) | `{idx,offset_x,offset_y,w,h,source}` | api_stream.py:3403 | +| `action.from_node` | str | non | id node WorkflowGraph (active pre-check) | api_stream.py:3229 | +| `action.dispatch_meta` | object | **À AJOUTER** | `{first_dispatched_at,resent_count,last_resent_at}` pour visibilité client | v2 | +| `precheck` | object | non | résultat pre-check serveur `{match,similarity,popup_detected}` | api_stream.py:3441 | +| `server_busy` | bool | non | lock occupé, retry plus tard | api_stream.py:2944 | +| `replay_paused` | bool | non | replay en pause supervisée | api_stream.py:2960 | +| `pause_message` | str | si paused | message à afficher dans bulle | api_stream.py:2961 | +| `replay_id` | str | si paused | pour ack ciblé via /resume | api_stream.py:2962 | +| `auth_detected` | bool | non | injection automatique d'actions d'auth | api_stream.py:3304 | + +**Exemple complet (v2 cible) :** +```json +{ + "action": { + "action_id": "step_4c0663941f22", + "attempt_id": "a8f3c2d1e9b4f720", + "type": "click", + "target_spec": { + "by_text": "Imagerie", + "resolve_order": ["ocr","template","vlm"], + "anchor_image_base64": "iVBORw0KGgo…", + "window_title": "MOREL Catherine — Easily Assure", + "uia_target": null + }, + "parameters": {}, + "expected_window_before": "MOREL Catherine — Easily Assure", + "expected_window_title": "MOREL Catherine — Easily Assure", + "success_strict": true, + "intention": "Cliquer onglet Imagerie", + "monitor_resolution": {"idx":1,"offset_x":0,"offset_y":0,"w":2560,"h":1600,"source":"action_hint"}, + "from_node": "node_tab_imagerie", + "dispatch_meta": { + "first_dispatched_at": 1779015600.123, + "resent_count": 0, + "last_resent_at": 0.0 + } + }, + "session_id": "sess_demo_42", + "machine_id": "DESKTOP-58D5CAC" +} +``` + +**Cas pause supervisée :** +```json +{ + "action": null, + "session_id": "sess_demo_42", + "machine_id": "DESKTOP-58D5CAC", + "replay_paused": true, + "pause_message": "Je n'y arrive pas (« Coller ou saisir... »)", + "replay_id": "replay_free_68ca51ab" +} +``` + +**Cas idle / server_busy :** +```json +{"action": null, "session_id":"sess_demo_42", "machine_id":"…", "server_busy": true} +``` + +### 4.2. Payload REPORT client → serveur (POST /replay/result) + +| Champ | Type | Obligatoire | Description | Source code | +|---|---|---|---|---| +| `session_id` | str | oui | | api_stream.py:628 | +| `action_id` | str | oui | identifiant de l'action acquittée | api_stream.py:629 | +| `attempt_id` | str | **À AJOUTER** | echo du `attempt_id` reçu (corrélation watchdog ↔ client) | v2 | +| `success` | bool | oui | résultat global | api_stream.py:630 | +| `error` | str \| null | dep. success | message court (`target_not_found`, `system_dialog:uac_consent`, …) | api_stream.py:631 | +| `warning` | str \| null | non | `no_screen_change`/`popup_handled`/`visual_resolve_failed`/`wrong_window` | api_stream.py:632 | +| `screenshot_after` | str \| null | recommandé | base64 PNG ou path | api_stream.py:634 | +| `screenshot_before` | str \| null | recommandé (clic) | base64 PNG du frame pre-action (Critic) | api_stream.py:635 | +| `actual_position` | object | si visuel | `{x_pct: float, y_pct: float}` coords cliquées | api_stream.py:636 | +| `resolution_method` | str | si visuel | `server_resolve_hybrid`/`template_match`/... | api_stream.py:638 | +| `resolution_score` | float | si visuel | 0.0–1.0 | api_stream.py:639 | +| `resolution_elapsed_ms` | float | si visuel | latence cascade | api_stream.py:640 | +| `target_description` | str | si fail | description humaine pour bulle pause | api_stream.py:642 | +| `target_spec` | object | si fail | echo target_spec pour reconstruction | api_stream.py:643 | +| `correction` | object | si pédagogique | `{x_pct,y_pct,uia_snapshot,crop_b64}` mode supervisé | api_stream.py:645 | +| `system_dialog` | object | si dialog | `{category,matched_signal,matched_value,reason,context}` | api_stream.py:650 | +| `needs_human` | bool | non | force pause supervisée | api_stream.py:651 | + +**Exemple succès :** +```json +{ + "session_id": "sess_demo_42", + "action_id": "step_4c0663941f22", + "attempt_id": "a8f3c2d1e9b4f720", + "success": true, + "actual_position": {"x_pct":0.2305,"y_pct":0.2805}, + "resolution_method": "server_resolve_hybrid_text_direct", + "resolution_score": 0.80, + "resolution_elapsed_ms": 412.7, + "screenshot_before": "iVBO…", + "screenshot_after": "iVBO…" +} +``` + +**Exemple échec target_not_found :** +```json +{ + "session_id": "sess_demo_42", + "action_id": "step_36346c1c40b9", + "attempt_id": "b9e4d3c2f0a5e831", + "success": false, + "error": "target_not_found", + "warning": "visual_resolve_failed", + "target_description": "Coller ou saisir le dossier patient", + "target_spec": {"by_text":"Coller ou saisir le dossier patient", "…":"…"}, + "screenshot_after": "iVBO…" +} +``` + +**Exemple system_dialog (UAC) :** +```json +{ + "session_id":"sess_demo_42", + "action_id":"step_xxx", + "attempt_id":"…", + "success": false, + "error": "system_dialog:uac_consent", + "system_dialog": { + "category": "uac_consent", + "matched_signal": "window_title", + "matched_value": "Contrôle de compte d'utilisateur", + "reason": "UAC consent prompt blocking click", + "context": "handle_popup_vlm" + }, + "needs_human": true, + "screenshot_after": "iVBO…" +} +``` + +### 4.3. Payload re-dispatch (orphan resent) + +Identique au DISPATCH normal **sauf** `dispatch_meta` enrichi : + +```json +{ + "action": { + "action_id": "step_4c0663941f22", // INCHANGÉ + "attempt_id": "c2d5e8f1a3b7c049", // NOUVEAU (UUID frais) + "type": "click", + "...": "…", + "dispatch_meta": { + "first_dispatched_at": 1779015600.123, + "resent_count": 1, + "last_resent_at": 1779015630.987, + "resend_reason": "orphan_timeout" + } + } +} +``` + +**Règle :** `action_id` reste stable (preserve idempotence côté serveur via `_retry_pending`). **Seul `attempt_id` change** (permet au client de distinguer un vrai re-dispatch d'un doublon réseau). + +### 4.4. Payload escalation pause supervisée + +Envoyé par le serveur dans la réponse au prochain poll après bascule `paused_need_help` (cf. §4.1 cas pause). Le client doit afficher la bulle et arrêter d'exécuter jusqu'à reception d'un nouveau dispatch (qui sera l'action de resume). + +**Payload /replay/{replay_id}/resume (POST) :** corps optionnel `{"acknowledged_check_ids": ["chk_1","chk_2"]}` (QW4). Réponse : +```json +{ + "status": "resumed", + "replay_id": "replay_free_68ca51ab", + "session_id": "sess_demo_42", + "remaining_actions": 12 +} +``` + +**Payload /replay/{replay_id}/cancel (POST) :** corps vide. Réponse : +```json +{"status": "cancelled", "replay_id": "…", "session_id": "…"} +``` + +--- + +## 5. Matrice des cas limites — la table principale du document + +Notation : **S** = état serveur (PENDING/DISPATCHED/ORPHAN/ACKED/ABANDONED/PAUSE), **C** = état client (POLLING/RECEIVED/EXECUTING/REPORTING/IDLE/DEAD). + +| # | Scénario | État serveur (avant→après) | État client (avant→après) | Comportement attendu (v2 contrat) | Risque idempotence | Status code / contournement | +|---|---|---|---|---|---|---| +| **a** | Client coupe AVANT réception réponse `/replay/next` (bug 8 mai) | DISPATCHED→ORPHAN→PENDING→DISPATCHED | POLLING→POLLING (timeout) | Watchdog détecte age>30s, re-dispatch (`attempt_id` neuf, `resent_count=1`). Bulle "action retentée" facultative. | **Faible** : action_id stable, client n'a JAMAIS exécuté → pas de double-effet. | OK via AXE_B1_DEEP §3 | +| **b** | Client coupe APRÈS réception, AVANT exécution | DISPATCHED→ORPHAN→PENDING→DISPATCHED | RECEIVED→DEAD | Watchdog re-dispatch. Si client revit, reçoit nouvelle attempt → exécute. Si client mort, watchdog finit en ABANDONED. | **Faible** : action perdue avant tout effet. | OK | +| **c** | Client exécute, coupe AVANT envoi report | DISPATCHED→ORPHAN→PENDING→DISPATCHED | EXECUTING→REPORTING→DEAD (avant POST) | Watchdog re-dispatch. **2e exécution probable** côté client. Idempotence action requise (§6.3). | **ÉLEVÉ** : double clic, double saisie possibles. Cible critique : `type` → préfixer `Ctrl+A`. | dedup_set côté client (§6.2) BLOQUE la 2e si même `action_id` reçu < 256 messages | +| **d** | Client report success, serveur ne reçoit pas (HTTP 502, timeout serveur côté POST) | DISPATCHED→ORPHAN→PENDING | EXECUTING→REPORTING(echec POST)→IDLE | Client doit **retenter** le POST (boucle interne avec backoff). v1 : un seul essai (executor.py:2476 timeout=10s, pas de retry). **À AJOUTER v2** : retry 3× backoff [1,3,7]s. Sinon watchdog re-dispatch + dedup_set côté client→ack synthétique. | Moyen | À ajouter retry POST côté client | +| **e** | Client report success=false, retry budget restant | DISPATCHED→FAILED(verify)→PENDING(retry_N+1) | EXECUTING→REPORTING→POLLING | `_schedule_retry` crée nouvelle entrée `{action_id}_retry{N+1}` (replay_engine.py:2604), repush head. `verify_failed` ne consomme PAS le budget orphan. | Géré | OK | +| **f** | Watchdog re-dispatch ALORS QUE client envoie report tardif | DISPATCHED→ORPHAN→repush en cours | EXECUTING→REPORTING(en vol) | Race window. Re-check sous lock dans watchdog `if aid not in _retry_pending: skip` (AXE_B1_DEEP §3 _scan_once). Si pop arrive 1er : repush skip. Si repush arrive 1er : pop ignoré (`no_active_replay`). | Faible | Géré par I3 + I5 | +| **g** | Watchdog re-dispatch, mais client a déjà ré-exécuté (cas c+f combinés) | DISPATCHED→ORPHAN→repush | EXECUTING(1)→REPORTING(1)→RECEIVED(2)→EXECUTING(2) | dedup_set client détecte 2e `action_id` identique → log warning + ack synthétique `{success:true, warning:"already_executed"}`. **Sans dedup** : double exécution = bug applicatif. | **CRITIQUE sans dedup** | dedup_set v2 obligatoire | +| **h** | Client mort silencieux (Léa crash, NoMachine freeze) | DISPATCHED→ORPHAN→PENDING→…→ABANDONED→PAUSE | DEAD | Watchdog MAX_RESENDS=2 puis ABANDONED. **Hook v1.1** : si ≥2 give-ups en 60s sur même session → bascule replay_state.status=paused_need_help + message "Léa ne répond plus" (AXE_B1_DEEP §6 R4). | OK avec hook | À ajouter hook dead_client_signal | +| **i** | Serveur restart pendant actions en `_retry_pending` | TOUT en mémoire → PERTE | POLLING → reçoit 404 / 503 / ConnectionError | `_retry_pending` est in-memory. Au restart : queue vide, replay_state perdu (sauf si persisté en DB — vérifier). Client backoff exponentiel ; quand serveur revient, replay_state restaurable depuis SQLite mais `_retry_pending` non. **Décision v1** : rebuild best-effort — `_replay_states` sauvegardés en SQLite ont les `completed_actions`, on relance depuis `current_action_index+1`. Pas de rejeu des actions en vol. **Lacune connue** : si action mid-flight ; à valider avec Dom. | n/a (état perdu) | **À TRANCHER avec Dom** : persistance _retry_pending ? | +| **j** | Polls clients simultanés en course (2 process Léa, ou retry rapide) | DISPATCHED (1 seul vainqueur du lock) | 2× POLLING | `_replay_lock.acquire(timeout=4.5)` : 1er gagne, 2e reçoit `{server_busy:true}` (api_stream.py:2944). Client backoff. Ordre des steps préservé (lock global). | Faible | OK | +| **k** | Action arrivée 2× côté client (double-clic même bouton) | DISPATCHED (attempt_1) puis DISPATCHED (attempt_2) | RECEIVED→DEDUP_CHECK→SKIP (2e) | dedup_set client = LRU 256 sur `action_id`. 2e réception → ack synthétique success=true warning="already_executed", PAS de ré-exécution. | Bloqué côté client | dedup_set v2 | +| **l** | Pause supervisée serveur déclenchée pendant action en vol | DISPATCHED→PAUSE | EXECUTING→REPORTING (résultat ignoré côté serveur ?) | Le serveur applique la pause sur le step SUIVANT (boucle `while queue` voit `paused`). L'action en vol s'achève normalement, report traité (pop+verify), puis prochain poll → `replay_paused=true`. **PAS de cancel de l'action en vol** (pas de protocole serveur→client pour interrompre une action en cours). | Faible | OK | +| **m** | Cancel replay côté VWB UI pendant action en vol | DISPATCHED→cancelled (purge _retry_pending) | EXECUTING→REPORTING | Cancel purge `_retry_pending` par replay_id (api_stream.py:4489) ET vide `_replay_queues[session_id]`. Le report tardif arrive : `pop(action_id)→None` → réponse `no_active_replay` (api_stream.py:3488). Client log info. Pas d'erreur. | Faible | OK | +| **n** | Cap MAX_RESENDS atteint | ORPHAN→ABANDONED | DEAD ou EXECUTING (cas g) | Log `[BUS] lea:dispatch_orphan_giveup`. v1 = action perdue silencieusement, replay continue (peut bloquer step suivant si dépendance). **Politique v2 :** si action critique (type ∈ {click,type,t2a_decision}) → bascule `paused_need_help` immédiatement avec message "Léa n'a pas répondu, vérifie". Si non critique (wait,scroll) → log seul, continue. | n/a | **À TRANCHER** : seuil par type ? | +| **o** | Action non-visuelle (`extract_text`, `t2a_decision`) vs visuelle (`click`) | Non-visuelle : pas de DISPATCHED, exécutée *server-side* dans la même boucle `get_next_action` (api_stream.py:3132-3197) | Jamais reçue par le client | **Contrats distincts** : non-visuelles n'entrent JAMAIS dans `_retry_pending`. Le watchdog n'a rien à scanner pour elles. Si extract_text plante (Ollama 503), `queue.pop(0)` + log warning + continue (api_stream.py:3195) → action serveur perdue silencieusement. **Risque pas couvert par watchdog.** | n/a | **À TRANCHER** : retry serveur sur actions serveur ? séparé du watchdog actions visuelles | +| **p** | Workflow se termine alors qu'action est encore en `_retry_pending` | DISPATCHED en cours → workflow.completed | n/a | `_replay_states[replay_id].status = "completed"`. Si une action est encore en `_retry_pending`, le watchdog la verra orphan ; au resend, `get_next_action` ne trouvera pas de `owning_replay` (status `running` requis ligne 2974) → queue vide retournée. **Fuite mémoire** : entrée `_retry_pending` jamais purgée tant que pas de cancel ou age > MAX_RESENDS. **Mitigation** : ajouter purge `_retry_pending` sur transition vers completed/error/failed (analogue à cancel ligne 4489). | Faible (mais leak) | **À AJOUTER v2** : purge à la complétion | +| **q** | Précheck "wait" injecté (popup détectée) — n'est PAS une action workflow | Pas dans `_retry_pending` (action synthétique) | RECEIVED→EXECUTING(wait 2000ms)→REPORTING(success=true) | wait_action a un `action_id=precheck_wait_<6hex>` non stockée côté serveur. Le report arrive : `pop(action_id)→None`, action ignorée gracieusement. | Faible | OK | +| **r** | Replay paused, client continue à poller | PAUSE | POLLING reçoit `replay_paused:true` à chaque tick | Client affiche bulle 1 fois (dedup sur `_last_pause_msg_shown` executor.py:2351), continue à poller. CPU loss négligeable. | Faible | OK | +| **s** | Reverse-proxy NPM bufferise SSE | DISPATCHED, event jamais reçu | POLLING/SSE silencieux | `X-Accel-Buffering: no` côté server response. Ping 15s force flush. AXE_B1 §4 §8 risques. | Faible avec headers | OK avec headers | +| **t** | NoMachine timeout idle (>60s) | DISPATCHED dormant | SSE→reconnect via Last-Event-ID | sseclient-py auto-reconnect, `Last-Event-ID` header repris au reconnect → serveur peut sauter les events déjà acquittés. v1 polling : pas de Last-Event-ID, juste réacheminement via watchdog. | Faible | OK | +| **u** | Bearer token expire/révoqué pendant un replay actif | DISPATCHED en attente | POLLING reçoit 401 | Client doit re-auth (hors scope v1 : tokens longue durée). v1 : crash + tray notification. Watchdog côté serveur continue à scanner — actions partent en ABANDONED après MAX. | Faible | hors scope v1 | + +--- + +## 6. Sémantique d'idempotence + +### 6.1. Couches d'idempotence + +| Couche | Mécanisme | Effet | Implémenté ? | +|---|---|---|---| +| **Serveur — pop sur report** | `_retry_pending.pop(action_id, None)` retourne None silencieux si déjà acquitté | report en double n'augmente pas `completed_actions` 2× | ✅ api_stream.py:3491 | +| **Serveur — re-check watchdog** | `if aid not in _retry_pending: skip` sous lock | re-dispatch annulé si report arrivé entre snapshot et repush | ✅ AXE_B1_DEEP §3 | +| **Serveur — cancel purge** | itération `_retry_pending` par replay_id | aucun ghost-resend après cancel | ✅ api_stream.py:4489 | +| **Action — `action_id` stable** | identifiant unique step (`step_` puis suffixes `_retry{N}` ou `_resume`) | clé du `pop` côté serveur, clé du dedup côté client | ✅ DB workflow + replay_engine.py:2609 | +| **Action — `attempt_id` rotatif** | UUID nouveau à chaque DISPATCH (initial + chaque resend) | distingue un re-dispatch légitime d'un doublon réseau, permet stats orphan | ❌ **À AJOUTER v2** | +| **Client — dedup_set LRU 256** | `set` bornée de `(action_id)` ou `(action_id, attempt_id)` récemment exécutés | bloque ré-exécution en cas g/k | ❌ **À AJOUTER v2 obligatoire** | +| **Action — idempotence intrinsèque** | clear field avant `type`, idempotence native du `click` sur tab actif | minimise dégât en cas de double exécution résiduelle | ⚠ **À documenter dans VWB**, pas dans code | + +### 6.2. Spec dedup_set client (v2) + +```python +# agent_v1/core/executor.py — à ajouter +from collections import OrderedDict + +class ActionDedupSet: + """LRU bornée d'action_id récemment exécutées. + Bloque ré-exécution si action arrive 2 fois (orphan resent + double réseau). + """ + def __init__(self, max_size: int = 256): + self._store: OrderedDict[str, float] = OrderedDict() # action_id → ts + self._max = max_size + + def seen(self, action_id: str) -> bool: + if action_id in self._store: + # Touch (LRU) + self._store.move_to_end(action_id) + return True + return False + + def mark(self, action_id: str) -> None: + self._store[action_id] = time.time() + self._store.move_to_end(action_id) + if len(self._store) > self._max: + self._store.popitem(last=False) +``` + +**Usage dans `poll_and_execute_inner` AVANT `execute_replay_action` :** +```python +if self._dedup.seen(action.get("action_id","")): + logger.warning(f"[DEDUP] action {action.get('action_id')} déjà exécutée — ack synthétique") + self._post_synthetic_ack(action, server_url, replay_result_url, + success=True, warning="already_executed") + return True +self._dedup.mark(action.get("action_id","")) +# … execute_replay_action(action) … +``` + +### 6.3. Idempotence intrinsèque par type d'action + +| Type | Idempotent nativement ? | Mitigation si exécuté 2× | +|---|---|---| +| `click` sur tab/bouton actif | OUI (le tab reste actif) | aucune | +| `click` sur bouton "Submit"/"Valider" | NON (double formulaire) | **dedup_set CRITIQUE** + dialog confirm côté app | +| `type` texte | NON (double saisie) | préfixer `Ctrl+A` (clear) + dedup_set | +| `keyboard_shortcut` Ctrl+S | dep. (1 sauvegarde = 1 dialog) | dedup_set | +| `keyboard_shortcut` Ctrl+V | NON (double collage) | dedup_set + clear avant | +| `scroll` | OUI mais déplace 2× | tolérable, dedup_set conseillé | +| `wait` | OUI | aucun risque | +| `extract_text` (server-side) | OUI (lecture pure) | n/a | +| `t2a_decision` (server-side, LLM) | OUI mais re-coût LLM ($/temps) | retry serveur, pas client | + +--- + +## 7. Timeouts et seuils + +| Nom | Défaut | Env var | Effet | Source | +|---|---:|---|---|---| +| `client_poll_timeout` | 30 s | non, en dur | `requests.get(/replay/next, timeout=30)` côté Léa | executor.py:2320 | +| `client_report_timeout` | 10 s | non, en dur | `requests.post(/replay/result, timeout=10)` | executor.py:2480 | +| `client_resolve_timeout` | 30 s | non, en dur | appel serveur `/resolve_target` | executor.py:1898 | +| `server_replay_lock_timeout` | 4.5 s | non, en dur | `_async_replay_lock(timeout=4.5)` → 503 ou server_busy | api_stream.py:539, 2938 | +| `server_action_server_side_timeout` | 180 s | non, en dur | `asyncio.wait_for(extract_text/t2a, 180)` | api_stream.py:3141 | +| `server_paste_and_execute_timeout` | 30 s | non, en dur | paste+execute ydotool | api_stream.py:3192 | +| `server_precheck_timeout` | 0.5 s | non, en dur | CLIP embed pre-check | api_stream.py:3250 | +| `heartbeat_max_age` | varie | `RPA_HEARTBEAT_MAX_AGE_SECONDS` | utilité pre-check | api_stream.py:3235 | +| `WATCHDOG_SCAN_INTERVAL_S` | 10 s | `RPA_WATCHDOG_SCAN_INTERVAL_S` | période scan orphan | AXE_B1_DEEP §11 | +| `WATCHDOG_ORPHAN_TIMEOUT_S` | 30 s | `RPA_WATCHDOG_ORPHAN_TIMEOUT_S` | age sans report → orphan | AXE_B1_DEEP §11 | +| `WATCHDOG_MAX_RESENDS` | 2 | `RPA_WATCHDOG_MAX_RESENDS` | give-up après N resends | AXE_B1_DEEP §11 | +| `WATCHDOG_REPUSH_POSITION` | `head` | `RPA_WATCHDOG_REPUSH_POSITION` | head/tail | AXE_B1_DEEP §11 | +| `WATCHDOG_ENABLED` | `1` | `RPA_WATCHDOG_ENABLED` | kill-switch | AXE_B1_DEEP §11 | +| `REPLAY_STATE_TTL_SECONDS` | varie | `RPA_REPLAY_STATE_TTL` | purge states finis | api_stream.py:726 | +| `MAX_REPLAY_STATES` | n | en dur | borne `_replay_states` | api_stream.py:735 | +| `MAX_RETRIES_PER_ACTION` | 3 | en dur | budget retry métier `_schedule_retry` | replay_engine.py:2591 | +| `_poll_backoff_min` | varie | n/a | reset après HTTP 200 | executor.py:2334 | +| `_poll_backoff_max` | varie | n/a | plafond backoff exponentiel | executor.py:2326 | +| `_poll_backoff_factor` | 2.0 | n/a | facteur multiplicatif | executor.py:2326 | +| `SSE_PING_INTERVAL_S` (cible) | 15 | env futur | heartbeat SSE | AXE_B1 §4 | + +**Cohérence des seuils :** +- `client_poll_timeout (30s) > server_replay_lock_timeout (4.5s)` → OK, le client attend bien la réponse server_busy. +- `server_action_server_side_timeout (180s) > client_poll_timeout (30s)` → SI extract_text dure 35s sans dispatcher d'action visuelle entre temps, le client coupe MAIS le serveur continue ; au prochain poll le serveur a fini, dispatche l'action visuelle suivante. **Pas de perte tant que l'action visuelle dispatch est rapide après extract_text.** Bug 8 mai = extract_text + dispatch click dans la MÊME réponse → 5s timeout dépassé → fix `timeout=30` adopté. +- `WATCHDOG_ORPHAN_TIMEOUT_S (30s) > client_poll_timeout (30s)` → frontière dangereuse. **Recommandation : remonter à 45s** pour laisser le temps au client de retenter au moins 1 poll naturellement avant que le watchdog résende. + +--- + +## 8. Transitions vers pause supervisée et resume + +### 8.1. Déclencheurs `status = "paused_need_help"` + +| Déclencheur | Source | État avant | État après | Champ enrichi | +|---|---|---|---|---| +| `pause_for_human` en mode supervised ou safety_checks présents | api_stream.py:3066-3111 | running | paused_need_help | `safety_checks`, `pause_payload`, `failed_action.reason="user_request"` | +| Report `system_dialog:*` (UAC/CredUI/SmartScreen) | api_stream.py:3785-3870 | running | paused_need_help | `failed_action.reason="system_dialog"`, message contextualisé | +| Report `warning="wrong_window"` | api_stream.py:3872-3920 | running | paused_need_help | `failed_action.reason="wrong_window"` | +| Report `success=false` + `error="target_not_found"` après MAX_RETRIES_PER_ACTION (3) | api_stream.py:3949-4030 | running | paused_need_help | `failed_action.target_description` | +| Hook v1.1 dead client signal (2+ giveups en 60s) | À AJOUTER (AXE_B1_DEEP §6 R4) | running | paused_need_help | `failed_action.reason="dead_client"` | +| Hook v2 (cas n) : N orphan giveup sur action type critique | À DÉCIDER avec Dom | running | paused_need_help | `failed_action.reason="orphan_max_resends"` | + +### 8.2. État de `_retry_pending` au moment de la pause + +- **Pause via `pause_for_human`** : aucune action en vol (pause arrive *avant* dispatch). +- **Pause via report failed** : l'action qui a déclenché la pause vient d'être `pop`pée. `_retry_pending` est **vide pour cet action_id** (déjà acquittée). Aucune purge supplémentaire nécessaire. +- **Pause via watchdog hook (v1.1)** : `_retry_pending` peut contenir des entrées orphelines avec age > MAX. **Politique :** purger en transition (à ajouter dans le hook). + +### 8.3. /resume — reconstruction de l'action + +`resume_replay` (api_stream.py:4361-4474) : +1. Vérifie state.status == `paused_need_help` (sinon 409). +2. Vérifie acquittement safety_checks required (sinon 400). +3. Reset state : `status="running"`, `failed_action=None`, `pause_message=None`, `safety_checks=[]`. +4. Reconstruit l'action : + - Priorité 1 : `failed_action.original_action` si présent. + - Priorité 2 : `_retry_pending.pop(failed_action.action_id, {}).get("action")`. + - Priorité 3 : minimum `{action_id, type, target_spec, visual_mode}`. +5. Nouveau `action_id = "{original}_resume"`. +6. Enregistre dans `_retry_pending[resume_id] = {action,retry_count:0,replay_id,reason:"resume_after_pause"}`. +7. Insère en tête `_replay_queues[session_id]`. + +**Lacune v1 :** le nouveau `action_id` (`_resume`) **n'a pas de `attempt_id`** explicite. Au prochain dispatch, le watchdog démarre le compteur à 0. Cohérent. + +### 8.4. Event bus `[BUS]` + +| Event | Quand | Payload (log structuré) | Source | +|---|---|---|---| +| `[BUS] lea:safety_checks_generated` | Pause `pause_for_human` avec checks | `replay= count=N sources=[…]` | api_stream.py:3081 | +| `[BUS] lea:monitor_routed` | Dispatch action visuelle (résolution monitor) | `replay= action= idx=N source=<…>` | api_stream.py:3419 | +| `[BUS] lea:dispatch_orphan_resent` (v1.1) | Watchdog repush | `action_id=X resent=N/MAX age=Ts session machine replay` | AXE_B1_DEEP §3 | +| `[BUS] lea:dispatch_orphan_giveup` (v1.1) | Watchdog abandon | `action_id=X resent=N age_total=Ts session machine replay` | AXE_B1_DEEP §3 | +| `[BUS] lea:dead_client_signal` (v2) | Hook ≥2 giveups/60s | `session= dead_count=N period=60s` | À AJOUTER | + +Tous les events sont consommables via `journalctl --user -u rpa-streaming -f | grep '\[BUS\]'`. Pas de bus pub/sub réel (pattern QW1/QW4 = log structuré). + +--- + +## 9. Compatibilité polling actuel ↔ futur SSE + +Le contrat des §4 / §5 / §6 / §8 est **invariant** par rapport au transport. Voici ce qui change vs ce qui ne change pas : + +| Aspect | Polling (v1 actuel) | SSE (cible AXE_B1) | Change ? | +|---|---|---|---| +| Endpoint dispatch | `GET /api/v1/traces/stream/replay/next` (1 réponse JSON) | `GET /api/v1/traces/stream/replay/events` (stream `text/event-stream`) | OUI | +| Format payload action | JSON dans body | JSON dans `data:` field d'un `ServerSentEvent` (event=`action`) | NON (même schéma) | +| Endpoint report | `POST /api/v1/traces/stream/replay/result` | identique | NON | +| ID corrélation | `action_id` + (v2) `attempt_id` | identique + `id:` SSE = `action_id` | NON | +| Détection déco client | Indirecte (pas de poll suivant) | `await request.is_disconnected()` immédiat | OUI (gain) | +| Détection déco serveur | Timeout client 30s | `EventSource.onerror` + reconnect natif | OUI (gain) | +| Reprise après reconnect | Pas de Last-Event-ID, watchdog seul | `Last-Event-ID` header automatique côté sseclient-py | OUI (gain) | +| Watchdog `_retry_pending` | **ACTIF** | **ACTIF** (ceinture+bretelles, cf. AXE_B1_DEEP §12) | NON | +| dedup_set client | **ACTIF v2** | **ACTIF v2** | NON | +| `_replay_lock` serveur | Tient pendant exécution serveur (extract_text…) | Idem (les actions server-side restent dans la même boucle) | NON | +| Bulle pause client | Reçue via `replay_paused:true` au prochain poll | Reçue via event `event=paused` ou via `replay_paused:true` dans event `action` | NON (même UX) | + +**Flag de bascule :** `RPA_REPLAY_TRANSPORT=poll|sse` côté client (executor.py choisit poll vs `replay_subscriber.py`) et serveur (les 2 endpoints coexistent — pas de mutual exclusion). Permet rollback 1-ligne. + +**Garantie de migration :** un client v2 polling et un client v2 SSE consomment **strictement le même contrat de message**. Le watchdog serveur scanne `_retry_pending` indépendamment du transport. Tous les invariants I1–I6 et C1–C5 tiennent identiquement. + +**Seul écart pratique :** en SSE, `WATCHDOG_ORPHAN_TIMEOUT_S` peut descendre à 15s (déconnexion détectée plus tôt). En polling, garder 30s (laisser une chance au polling naturel). + +--- + +## 10. Précédents externes — fiches courtes + +### 10.1. AWS SQS — visibility timeout +- **Contrat :** message reçu devient *invisible* pour `VisibilityTimeout` secondes (défaut 30s). Si pas `DeleteMessage` avant expiration → redevient visible, redélivrable. +- **Modèle :** at-least-once delivery (standard queues), exactly-once (FIFO via `MessageDeduplicationId`). +- **Idempotence :** **côté consommateur obligatoire** (chez AWS « your processing logic must be idempotent »). DLQ pour les empoisonnés. +- **Cap :** `ChangeMessageVisibility` pour étendre dynamiquement. Limite dure 12h. +- **Notre mapping :** `_retry_pending[action_id] = {dispatched_at}` = visibility timeout in-memory. `WATCHDOG_ORPHAN_TIMEOUT_S = 30s` = `VisibilityTimeout`. `WATCHDOG_MAX_RESENDS = 2` = `maxReceiveCount` avant DLQ. ABANDONED = DLQ équivalent (mais sans queue physique, juste log). +- **Source :** [SQS visibility timeout doc](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html) (consulté 2026-05-24) + +### 10.2. NATS JetStream — pull consumer ack +- **Contrat :** `AckExplicit` par défaut. `AckWait` (défaut 30s) = délai avant redélivrance. `MaxDeliver` = N attempts max. `MaxAckPending` = window flow control (défaut 1000). +- **NAK :** redélivrance immédiate (ou `nakWithDelay`). +- **Backoff :** liste `[5s, 30s, 300s, …]` qui *override* `AckWait`. Si liste plus courte que `MaxDeliver`, dernier délai répété. +- **Notre mapping :** `AckWait` = `WATCHDOG_ORPHAN_TIMEOUT_S`. `MaxDeliver` = `WATCHDOG_MAX_RESENDS+1`. **Pas de NAK explicite chez nous** : un report success=false suit la voie retry métier (`_schedule_retry`), pas la voie transport. **Pas de backoff** dans la v1 du watchdog (justification AXE_B1_DEEP §5 : démo médicale, réactivité prime). Adoptable si besoin. +- **Source :** [NATS JetStream Consumers doc](https://docs.nats.io/nats-concepts/jetstream/consumers) (consulté 2026-05-24) + +### 10.3. Skyvern — `execute_step` + `handle_failed_step` +- **Contrat :** boucle récursive `execute_step` (forge/agent.py lignes 1094–1577). À chaque step : + - `step.status == failed` → `handle_failed_step()` retourne *next step* (retry) ou *None* (terminal). + - `step.status == completed` → `handle_completed_step()` décide advance vs verify vs finalize. +- **Cap :** `max_steps_per_run` global, hiérarchie task → org → settings (ligne 1169-1176). +- **Idempotence :** PR récente a *retiré* le retry interne du `fail_task` (transition status uniquement). Skyvern délègue le retry au LLM via re-emit du prochain action_use. +- **Différence avec nous :** Skyvern = monolithe local (browser CDP), pas de transport HTTP entre dispatcher et exécuteur. Notre cas nécessite un layer transport en plus, d'où `_retry_pending` qui n'a pas d'équivalent direct. +- **Source :** [Skyvern agent.py main](https://github.com/Skyvern-AI/skyvern/blob/main/skyvern/forge/agent.py), [PR #434 better catch exceptions](https://github.com/Skyvern-AI/skyvern/pull/434) + +### 10.4. browser-use — action_id + idempotency guard +- **Contrat :** `max_failures` config (défaut 3). Action cache court-terme keyed sur `(command, selector, value)` pour éviter side-effects dupliqués si retry rapide. +- **Pattern d'idempotency key :** « *deterministic key before execution, generated from workflow run ID, step index, and action type* » (cf. mightybot blog). +- **Notre mapping :** `action_id` déterministe = `step_` + suffixes. dedup_set client = équivalent action cache court-terme. +- **Différence :** browser-use est intra-process (loop Python contrôle Chromium via CDP local). Notre cas inter-process inter-machine. +- **Source :** [browser-use AGENTS.md](https://github.com/browser-use/browser-use/blob/main/AGENTS.md), [Idempotent AI agents — buildmvpfast 2026](https://www.buildmvpfast.com/blog/idempotent-ai-agent-retry-safe-patterns-production-workflow-2026) + +### 10.5. Anthropic Computer Use SDK — tool_use_id binding +- **Contrat :** chaque `tool_use` retourné par Claude a un `id` ; le code applicatif doit retourner un `tool_result` avec `tool_use_id` identique (loop.py lignes 234-254). +- **Retry :** uniquement au niveau API (`max_retries=4` côté client Anthropic, ligne 182). **Pas de retry au niveau tool execution** — c'est le modèle qui re-décide au prochain tour. +- **Idempotence :** non garantie par le SDK. Délégué à l'application (« deduplication or idempotency key handling visible in this loop : none »). +- **Notre mapping :** `tool_use_id` ↔ `action_id`. Mais notre boucle est *server-driven* (queue d'actions pré-compilée par VWB), pas LLM-driven. Plus déterministe, donc plus simple à idempotenter. +- **Source :** [computer-use-demo/loop.py main](https://github.com/anthropics/claude-quickstarts/blob/main/computer-use-demo/computer_use_demo/loop.py), [Tool use Claude API docs](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/overview) + +### 10.6. Playwright MCP — SSE remote transport +- **Contrat :** transport stdio (local) OU HTTP/SSE (remote). Tools/list au handshake, tool/call par event SSE descendant, tool/result POST remontant. +- **Issue connue 2026 :** « SSE stream disconnected » après idle (cline/cline #8367). Mitigation = ping applicatif. +- **Timeout :** 30s par défaut sur CDP endpoint connect. +- **Notre mapping :** très proche du pattern cible AXE_B1 §4 (SSE descendant + POST ack). Confirme robustesse du choix techno. Précaution : prévoir reconnect natif (sseclient-py). +- **Source :** [microsoft/playwright-mcp](https://github.com/microsoft/playwright-mcp), [cline issue #8367 SSE disconnect](https://github.com/cline/cline/issues/8367) + +### 10.7. Synthèse comparative + +| Système | ID corrélation | Visibility/Ack timeout | Max retry transport | Dedup client | Modèle delivery | +|---|---|---|---|---|---| +| **AWS SQS std** | MessageId + ReceiptHandle | VisibilityTimeout 30s | maxReceiveCount (DLQ) | obligatoire app | at-least-once | +| **NATS JetStream** | StreamSeq + ConsumerSeq | AckWait 30s | MaxDeliver | obligatoire app | at-least-once | +| **Skyvern** | step.step_id | n/a (monolithe) | max_steps_per_run | n/a | exactly-once local | +| **browser-use** | (cmd, selector, value) | n/a | max_failures=3 | action cache | exactly-once local | +| **Anthropic CU** | tool_use_id | n/a | max_retries client (API) | non garanti | exactly-once par tour | +| **Playwright MCP** | request_id | 30s CDP | n/a (LLM décide) | non garanti | best-effort | +| **Nous (v2 cible)** | `action_id` + `attempt_id` | ORPHAN_TIMEOUT 30s | MAX_RESENDS=2 | dedup_set 256 LRU | at-least-once + dedup → effectif exactly-once | + +--- + +## 11. Sources + +### Code interne (lecture seule, lignes vérifiées 2026-05-24) +- `agent_v0/server_v1/api_stream.py:520-559` — `_replay_lock`, `_async_replay_lock`, `_replay_queues`, `_replay_states`, `_machine_replay_target` +- `agent_v0/server_v1/api_stream.py:626-651` — `ReplayResultReport` Pydantic schema +- `agent_v0/server_v1/api_stream.py:2906-3443` — `get_next_action` (DISPATCH path) +- `agent_v0/server_v1/api_stream.py:3132-3197` — actions server-side `extract_text/t2a_decision/...` +- `agent_v0/server_v1/api_stream.py:3354-3359` — création `_retry_pending` (à enrichir AXE_B1_DEEP §4.1) +- `agent_v0/server_v1/api_stream.py:3446-3491` — `report_action_result`, pop idempotent +- `agent_v0/server_v1/api_stream.py:3785-3870` — bascule `paused_need_help` sur system_dialog +- `agent_v0/server_v1/api_stream.py:4361-4474` — `resume_replay` + safety_checks +- `agent_v0/server_v1/api_stream.py:4477-4494` — `cancel_replay` + purge +- `agent_v0/server_v1/replay_engine.py:2583-2642` — `_schedule_retry` (retry métier, distinct du retry transport) +- `agent_v0/agent_v1/core/executor.py:2275-2503` — `poll_and_execute` + `_poll_and_execute_inner` +- `agent_v0/agent_v1/core/executor.py:2308-2321` — `requests.get(/replay/next, timeout=30)` (fix 8 mai) +- `agent_v0/agent_v1/core/executor.py:2476-2501` — `requests.post(/replay/result, timeout=10)` +- `agent_v0/agent_v1/network/streamer.py:1-120` — streaming events/screenshots (canal séparé du replay) + +### Docs internes +- `docs/recherche/AXE_B1_REPLAY_TRANSPORT.md` (2026-05-23) — choix SSE vs WebSocket, pseudo-code endpoint +- `docs/recherche/AXE_B1_DEEP_WATCHDOG.md` (2026-05-24) — implémentation watchdog complète +- `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` — diagnostic 9 actions perdues, racine du contrat +- `docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` §4 — synthèse replay +- `docs/LESSONS_LEARNED_GHT_2026-05.md` — bugs P0 post-démo +- `docs/SMOKE_TEST_FINALIZE_REPLAY_2026-05-20.md` — contrat finalize → replay + +### Sources externes (consultées 2026-05-24) + +**Patterns queue / visibility timeout / idempotence** +- [Amazon SQS visibility timeout](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html) +- [Amazon SQS exactly-once processing FIFO](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues-exactly-once-processing.html) +- [Amazon SQS message deduplication ID](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/using-messagededuplicationid-property.html) +- [NATS JetStream Consumers](https://docs.nats.io/nats-concepts/jetstream/consumers) +- [NATS JetStream Model Deep Dive](https://docs.nats.io/using-nats/developer/develop_jetstream/model_deep_dive) +- [How to Handle SQS Visibility Timeout (oneuptime 2026-01-27)](https://oneuptime.com/blog/post/2026-01-27-sqs-message-visibility-timeout/view) +- [Achieving idempotency in AWS serverless (Albaqali)](https://qasimalbaqali.medium.com/achieving-idempotency-in-the-aws-serverless-space-d0671a521479) + +**Frameworks RPA / Computer Use** +- [Skyvern forge/agent.py main](https://github.com/Skyvern-AI/skyvern/blob/main/skyvern/forge/agent.py) — `execute_step` 1094-1577 +- [Skyvern webeye/actions/handler.py](https://github.com/Skyvern-AI/skyvern/blob/main/skyvern/webeye/actions/handler.py) +- [Skyvern PR #434 better catch exceptions](https://github.com/Skyvern-AI/skyvern/pull/434) +- [Skyvern retry run webhook docs](https://www.skyvern.com/docs/api-reference/api-reference/agent/retry-run-webhook) +- [Anthropic computer-use-demo loop.py](https://github.com/anthropics/claude-quickstarts/blob/main/computer-use-demo/computer_use_demo/loop.py) — dispatch 234-254 +- [Anthropic Tool use overview docs](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/overview) +- [Microsoft Playwright MCP](https://github.com/microsoft/playwright-mcp) +- [Cline SSE disconnect issue #8367](https://github.com/cline/cline/issues/8367) +- [browser-use AGENTS.md main](https://github.com/browser-use/browser-use/blob/main/AGENTS.md) +- [browser-use issue #3615 agent not stopping](https://github.com/browser-use/browser-use/issues/3615) + +**Patterns idempotence agents AI** +- [Idempotent AI Agents — buildmvpfast 2026](https://www.buildmvpfast.com/blog/idempotent-ai-agent-retry-safe-patterns-production-workflow-2026) +- [Fault-Tolerant AI Agent Pipelines — MightyBot](https://mightybot.ai/blog/fault-tolerant-ai-agent-pipelines/) +- [Action Verification and Retries in LLM Agent Loops — ingramhaus](https://ingramhaus.com/action-verification-and-retries-in-llm-agent-execution-loops) +- [Idempotency in Distributed Systems — aloknecessary](https://aloknecessary.github.io/blogs/idempotency-distributed-systems/) +- [Stripe API idempotent requests](https://docs.stripe.com/api/idempotent_requests) + +**Transport SSE / FastAPI** +- [sse-starlette GitHub](https://github.com/sysid/sse-starlette) +- [FastAPI lifespan events](https://fastapi.tiangolo.com/advanced/events/) +- [Stop streaming response when client disconnects (FastAPI #7572)](https://github.com/fastapi/fastapi/discussions/7572) + +--- + +## 12. Décisions non tranchables sans Dom (sortir explicitement du contrat v2) + +Ces points sont identifiés mais demandent un arbitrage produit : + +| # | Cas limite | Question | Recommandation Claude | +|---|---|---|---| +| D1 | Cas (i) — restart serveur pendant actions en `_retry_pending` | Faut-il persister `_retry_pending` (SQLite) pour rebuild ? Ou accepter perte transport au restart ? | **Accepter perte v2** : le restart serveur est volontaire (`systemctl restart`), Pauline relance le replay depuis VWB. Surcoût persistance > bénéfice. | +| D2 | Cas (n) — politique abandon | Bascule en `paused_need_help` après MAX_RESENDS atteint ? Pour quels types d'action ? | **OUI pour `click`/`type`/`t2a_decision`** (critiques). **Log seul pour `wait`/`scroll`** (continuer). À ajouter dans hook watchdog v1.1. | +| D3 | Cas (o) — actions server-side perdues | Retry serveur sur `extract_text` qui timeout ? Watchdog dédié actions server-side ? | **Différer** : v1 = un seul try + log warning, comportement actuel acceptable. v2 envisageable si bench Ollama montre instabilités fréquentes. | +| D4 | Cas (p) — purge `_retry_pending` à la complétion workflow | Ajouter purge automatique en transition vers `completed/error/failed` ? | **OUI**, simple à ajouter analogue à cancel (api_stream.py:4489). | +| D5 | dedup_set côté client | Implémenter v2 obligatoire ? Quelle taille LRU ? Inclure `attempt_id` ou juste `action_id` ? | **OUI obligatoire v2.** Taille 256 (couvre largement les workflows GHT 50 steps). Key = `action_id` seul (le `attempt_id` n'apporte rien côté dedup — l'objectif est de bloquer la double exécution même action). | +| D6 | `attempt_id` côté serveur | Générer UUID à chaque dispatch (initial + resend) ? Stocker l'historique ? | **OUI v2.** Génération à chaque DISPATCH dans `get_next_action`. Pas d'historique nécessaire (logs structurés `[BUS] lea:dispatch_orphan_resent` suffisent). | +| D7 | Migration backward-compat | Si client v1 (sans dedup_set, sans `attempt_id` echo) parle à serveur v2, casse-t-il ? | **NON** : `attempt_id` est optionnel côté serveur (toléré absent). dedup_set est purement défensif côté client. Migration progressive sans rupture. | +| D8 | Cas (l) — protocole d'interruption serveur→client d'une action en vol | Ajouter mécanisme `cancel_in_flight` ? | **NON v1, v2** : pas nécessaire pour la démo. Pause supervisée sur step suivant suffit. | + +--- + +## 13. Liens vers autres specs en cours + +- **`spec_validator`** (à venir) — un Validator strict (sémantique post-action) ne peut être fiable que si toutes les actions arrivent. **SPEC_TRANSPORT est prérequis logique de spec_validator.** Le contrat REPORT.warning peut s'enrichir de codes de Validator (`semantic_fail`, `expected_text_not_found`) sans casser ce contrat. +- **`spec_popups`** (à venir) — la détection popup côté serveur (pre-check) ET côté client (DialogHandler) émet des actions synthétiques `wait` ou des reports `warning="popup_handled"`. **Cas (q) du §5 documente la non-interférence.** Le contrat dialog/popup s'imbrique sur les mêmes endpoints sans extension. +- **AXE_B2 (Validator)** : couvre le côté `verify_action` + Critic sémantique (déjà partiellement codé dans `replay_verifier.py`). À spécifier en parallèle. +- **AXE_B4 (ORA Observe-Reason-Act)** : pousse aussi dans `_replay_queues` → bénéficie gratuitement du watchdog et du contrat. +- **AXE_D2 (Dialog/Popup)** : `system_dialog` + `wrong_window` + DialogHandler — branches bascule pause supervisée déjà tracées §8.1. + +--- + +*Document de spécification contractuelle. Lecture seule sur code, aucune modification. À valider par Dom avant implémentation v2 (dedup_set, attempt_id, hooks watchdog).* diff --git a/docs/recherche/SPEC_VALIDATOR_MATRICE.md b/docs/recherche/SPEC_VALIDATOR_MATRICE.md new file mode 100644 index 000000000..4a58abf40 --- /dev/null +++ b/docs/recherche/SPEC_VALIDATOR_MATRICE.md @@ -0,0 +1,1319 @@ +# SPEC OPÉRATIONNELLE — Matrice « action → signal qui fait foi » + +**Date :** 2026-05-24 +**Auteur :** agent recherche dispatché (Claude Opus 4.7 1M) +**Statut :** spec opérationnelle, lecture seule, **AUCUNE modification de code**. +**Parents :** +- `docs/recherche/AXE_B2_VALIDATOR_PATTERN.md` (architecture Skyvern + design Validator pluggable) +- `docs/recherche/AXE_B2_DEEP_VALIDATOR.md` (code Checkers production-ready, wiring `api_stream.py`) + +**Périmètre.** Pour chaque type d'action exécutable côté Léa (cf. `_ALLOWED_ACTION_TYPES` +`replay_engine.py:35-48` et `reference_vwb_action_types.md`), **dire précisément quel signal +doit faire foi** pour valider qu'elle a réussi. Pas une étude théorique, une matrice +opérationnelle : ce qu'il faut **mesurer**, sur **quelle zone**, avec **quel seuil**, +**combien de millisecondes** pour le décider, et **quel Checker** du package `core/validation/` +implémenter. + +Pas de code à appliquer ici — uniquement des snippets copy-paste pour les nouveaux Checkers +qui sortent de ce que B2/B2_DEEP a déjà décrit. + +--- + +## 1. TL;DR — l'insight central et le tableau-résumé + +### 1.1. Insight + +> **Le pixel-diff global ment, le titre fenêtre est aveugle aux SPA, l'OCR-ROI ne suffit +> pas seul.** Chaque type d'action a un signal *spatial et sémantique* qui prouve son +> effet. Ce signal est différent selon l'action : +> +> - **click_anchor / switch_tab** → le **mot attendu** apparaît dans une ROI 80 px autour +> du point cliqué ET l'indicateur visuel d'activation (souligné, fond surligné, mise +> en gras) change dans une bande étroite sous le label. +> - **close_tab** → le label de l'onglet *fermé* **disparaît** de la barre de tabs +> (SSIM-ROI before/after sur la barre + OCR confirme absence). +> - **save** → une **transition d'état** observable : disparition d'un marqueur +> « modifié » (étoile dans la title-bar, indicateur dirty) OU apparition d'un toast +> « Enregistré » sous 3 s. +> - **dialog_button** → le **dialog disparaît** (zone modale du screenshot before n'est +> plus là dans after) ET le focus retourne à l'app underlying. +> +> Ne **jamais** se fier au seul `success=True` rapporté par l'agent : c'est la cause +> exacte du bug step 10 démo GHT (`REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md`). + +### 1.2. Tableau-résumé une page + +| Action VWB / cas | Signal **primaire** (fait foi) | Latence cible | Coût | +|---|---|---:|---| +| **`click_anchor`** | OcrRoiChecker 80 px : mot attendu présent ET aucun token suspect (`https`, `edge`) | 80 ms | OCR | +| **`switch_tab`** (cas explicite Dom) | OcrRoiChecker 120×40 sur barre de tabs + SSIM-ROI bande sous-tab > 0.05 | 100 ms | OCR + SSIM | +| **`close_tab`** (cas explicite Dom) | TabAbsenceChecker : label cible **absent** de la barre de tabs after | 120 ms | OCR | +| **`save`** (cas explicite Dom) | SaveSuccessChecker : `*` disparu de title-bar OU toast « Enregistré » détecté ≤ 3 s | 150 ms (poll) | OCR title + ROI bas | +| **`dialog_button`** (cas explicite Dom) | DialogClosedChecker : la modale du before n'est plus dans after (SSIM-ROI + OCR absence) | 100 ms | SSIM + OCR | +| **`double_click_anchor`** | TitleBarChecker (changement titre fenêtre) + OcrRoiChecker | 200 ms | OCR title + OCR ROI | +| **`right_click_anchor`** | DialogPresenceChecker : menu contextuel apparu sous le curseur | 150 ms | OCR + SSIM | +| **`hover_anchor`** | PixelDiffChecker ROI ≤ 200 px autour du curseur : tooltip a changé | 30 ms | SSIM | +| **`type_text`** / **`type_secret`** | OcrRoiChecker 120 px : texte tapé visible dans la zone du caret | 120 ms | OCR | +| **`keyboard_shortcut`** | dépend du raccourci : voir §4.5 (Ctrl+S → SaveSuccessChecker, Ctrl+W → close_tab, etc.) | variable | variable | +| **`scroll_to_anchor`** | OcrRoiChecker : ancre visible dans la viewport | 100 ms | OCR | +| **`wait_for_anchor`** | OcrRoiChecker : ancre présente (sinon CONTINUE poll) | 100 ms | OCR | +| **`extract_text`** / **`extract_text_scroll`** | JsonSchemaChecker : str non vide + langue fr + len > 50 | 10 ms | déterministe | +| **`extract_table`** | JsonSchemaChecker : ≥ 1 row, schema headers attendus si fournis | 10 ms | déterministe | +| **`t2a_decision`** | JsonSchemaChecker strict : `decision ∈ enum`, `justification` non vide | 10 ms | déterministe | +| **`paste_and_execute`** | PixelDiffChecker ROI input (caret) + escalation LlmJudge si ambigu | 50 ms + escalation | SSIM | +| **`screenshot_evidence`** | TitleBarChecker (la bonne app est devant) — pas de side-effect | 130 ms | OCR title | +| **`pause_for_human`** | hors-scope (QW4 ChecklistPanel, déjà câblé) | 0 | — | +| **`db_save_data`** / **`db_read_data`** / **`import_excel`** | JsonSchemaChecker : SELECT vérifie nb_rows attendu | <50 ms | SQLite | +| **`visual_condition`** | LlmJudgeChecker sur la condition formulée | 2.5 s | LLM-judge | +| **`drag_drop_anchor`** | OcrRoiChecker ROI destination + PixelDiffChecker zone source | 200 ms | OCR + SSIM | +| **`select_option`** (combobox) | OcrRoiChecker : valeur choisie visible dans le champ collapsed | 100 ms | OCR | +| **`check_checkbox`** / **`radio_button`** | PixelDiffChecker 24×24 px autour du widget : transition cochée/décochée | 20 ms | SSIM | +| **`open_app`** / **`open_url`** | TitleBarChecker : nouveau title attendu présent | 130 ms | OCR title | + +**Latence cumulée budget démo MOREL 46 steps** : ~10 s ajoutés, négligeable face aux 30-60 s +gagnés en évitant un blocage step 10 (cf. `AXE_B2_DEEP_VALIDATOR.md` §4). + +--- + +## 2. Matrice principale dense + +Aligné avec `_ALLOWED_ACTION_TYPES` (`replay_engine.py:35-48`), TYPE_MAP VWB→Léa +(`dag_execute.py:1172-1216`) et la palette frontend (`types.ts:98-274`). + +| Action VWB | Type Léa transmis | Signal **primaire** (fait foi) | Signal **secondaire** (confirmation) | Fallback si primaire indisponible | Verdicts possibles | Latence cible | Coût | Checker `core/validation/` | +|---|---|---|---|---|---|---:|---|---| +| `click_anchor` | `click` | **OcrRoiChecker** 80 px : `by_text` présent dans crop autour de `actual_position` | TitleBarChecker (titre fenêtre stable, app correcte) | LlmJudgeChecker `verify_with_critic` (escalation 2.5 s) | COMPLETE, WRONG_APPLICATION, WRONG_TARGET, OCR_TEXT_MISSING, UI_LOADING | 80 ms (+2.5 s rare) | OCR EasyOCR | `checkers/ocr_roi.py` | +| `double_click_anchor` | `click button="double"` | **TitleBarChecker** (changement titre = nouvelle vue/fenêtre) | OcrRoiChecker 80 px sur zone double-cliquée | LlmJudgeChecker | COMPLETE, NO_VISUAL_CHANGE, WRONG_TARGET | 200 ms | OCR title + OCR ROI | `checkers/title_bar.py` + `ocr_roi.py` | +| `right_click_anchor` | `click button="right"` | **DialogPresenceChecker** : menu contextuel apparu sous (cx, cy) | OcrRoiChecker sur items menu si `expected_menu_items` fourni | PixelDiffChecker ROI 300×300 autour curseur | COMPLETE, NO_VISUAL_CHANGE, UNEXPECTED_DIALOG | 150 ms | OCR + SSIM | `checkers/dialog_presence.py` (P1) | +| `hover_anchor` | aucun mapping direct (Léa : `move`) | **PixelDiffChecker ROI** 200 px autour curseur : SSIM < 0.98 (tooltip apparu) | OcrRoiChecker si `expected_tooltip_text` fourni | — | COMPLETE, NO_VISUAL_CHANGE | 30 ms | SSIM | `checkers/pixel_diff.py` (existant) | +| `drag_drop_anchor` | `drag` (whitelisté, pas de handler Léa actuel) | **OcrRoiChecker** ROI destination 80 px : item attendu visible | PixelDiffChecker zone source (item parti) | LlmJudgeChecker | COMPLETE, WRONG_TARGET, NO_VISUAL_CHANGE | 200 ms | OCR + SSIM | `ocr_roi.py` + `pixel_diff.py` | +| `scroll_to_anchor` | `scroll` | **OcrRoiChecker** : ancre attendue (`by_text`) visible dans la viewport | PixelDiffChecker global change_pct > 5% | — | COMPLETE, NO_VISUAL_CHANGE (déjà en bas) | 100 ms | OCR | `ocr_roi.py` | +| `focus_anchor` | `focus_anchor` (whitelist côté serveur partielle) | **PixelDiffChecker** 30 px autour widget : caret/highlight apparu | OcrRoiChecker confirmation label widget | — | COMPLETE, WRONG_TARGET | 30 ms | SSIM | `pixel_diff.py` | +| `type_text` | `type` | **OcrRoiChecker** 120 px autour caret : texte tapé présent (ratio tokens ≥ 50%) | PixelDiffChecker ROI input (a changé) | LlmJudgeChecker si texte > 50 c | COMPLETE, OCR_TEXT_MISSING (typing failed), SCHEMA_INVALID (caractères spéciaux ratés) | 120 ms | OCR | `ocr_roi.py` (radius_px=120) | +| `type_secret` | `type` | **PixelDiffChecker** ROI input : un input s'est rempli (jamais OCR sur le contenu) | — | — | COMPLETE, NO_VISUAL_CHANGE | 20 ms | SSIM | `pixel_diff.py` | +| `keyboard_shortcut` (`Ctrl+S`) | `key_combo` | **SaveSuccessChecker** (§4.3) | TitleBarChecker | — | voir §4.3 | 150 ms | OCR + ROI | `checkers/save_success.py` (NEW §5.2) | +| `keyboard_shortcut` (`Ctrl+W`, `Ctrl+F4`) | `key_combo` | **TabAbsenceChecker** (§4.2) | — | — | voir §4.2 | 120 ms | OCR | `checkers/tab_active.py` (NEW §5.1) | +| `keyboard_shortcut` (`Ctrl+Tab`, `Ctrl+PgDn`) | `key_combo` | **TabActiveChecker** (§4.1) | — | — | voir §4.1 | 120 ms | OCR + SSIM | `checkers/tab_active.py` (NEW §5.1) | +| `keyboard_shortcut` (`Home`, `End`, `PgUp/Dn`) | `key_combo` | **PixelDiffChecker** global change_pct > 5% (scroll observable) | — | — | COMPLETE, NO_VISUAL_CHANGE | 15 ms | SSIM | `pixel_diff.py` | +| `keyboard_shortcut` (`Alt+F4`, `Esc`) | `key_combo` | **DialogClosedChecker** (§4.4) si dialog avant ; sinon TitleBarChecker | — | LlmJudgeChecker | voir §4.4 | 100 ms | SSIM + OCR | `checkers/dialog_closed.py` (NEW §5.3) | +| `keyboard_shortcut` (autre) | `key_combo` | **TitleBarChecker** | LlmJudgeChecker | — | COMPLETE, NO_VISUAL_CHANGE | 130 ms | OCR title | `title_bar.py` | +| `wait_for_anchor` | `wait` | **OcrRoiChecker** : ancre présente | PixelDiffChecker (écran stable, fin animation) | — | COMPLETE, CONTINUE (poll), UI_LOADING | 100 ms | OCR | `ocr_roi.py` | +| `extract_text` | server-side | **JsonSchemaChecker** : `str`, `len > 50`, ratio lettres ≥ 0.5 | LlmJudgeChecker plausibilité si `len < 50` | — | COMPLETE, SCHEMA_INVALID | 10 ms (+2.5 s rare) | déterministe | `checkers/json_schema.py` | +| `extract_text_scroll` | server-side | JsonSchemaChecker + concat check | LlmJudgeChecker si plusieurs pages | — | COMPLETE, SCHEMA_INVALID | 10 ms | déterministe | `json_schema.py` | +| `extract_table` | server-side | **JsonSchemaChecker** : `list[str]`, `len ≥ 1`, headers attendus si fournis | OcrRoiChecker re-check headers | — | COMPLETE, SCHEMA_INVALID | 10 ms | déterministe | `json_schema.py` | +| `screenshot_evidence` | (action passive) | **TitleBarChecker** (bonne app devant) | — | — | COMPLETE (passive) | 130 ms | OCR title | `title_bar.py` | +| `t2a_decision` | server-side | **JsonSchemaChecker** strict (decision ∈ enum, justification ≥ 10 c, confiance ∈ [0,1]) | — | — | COMPLETE, SCHEMA_INVALID | 10 ms | déterministe | `json_schema.py` | +| `pause_for_human` | server-side | **Checklist QW4** SafetyChecksProvider | — | — | (hors-scope Validator) | 0 ms | — | — | +| `visual_condition` | server-side | **LlmJudgeChecker** sur condition formulée | — | — | COMPLETE, TERMINATE | 2.5 s | LLM | `llm_judge.py` | +| `db_save_data` | server-side | **JsonSchemaChecker** + SELECT count rows attendus | — | — | COMPLETE, SCHEMA_INVALID | <50 ms | SQLite | `json_schema.py` extension | +| `db_read_data` / `import_excel` | server-side | **JsonSchemaChecker** rows ≥ 1 | — | — | COMPLETE, SCHEMA_INVALID | <50 ms | SQLite | `json_schema.py` | +| `db_foreach` | server-side | (boucle, validation par itération via les actions internes) | — | — | — | n/a | — | — | +| `paste_and_execute` | server-side bypass `ydotool` | **PixelDiffChecker** ROI input | OcrRoiChecker sur contenu collé si possible | LlmJudgeChecker | COMPLETE, NO_VISUAL_CHANGE | 50 ms (+2.5 s rare) | SSIM | `pixel_diff.py` | +| `ai_ocr` / `ai_summarize` / `ai_extract` / `ai_classify` / `ai_analyze_text` / `ai_custom` | server-side | JsonSchemaChecker (str/dict non vide) + LlmJudgeChecker plausibilité | — | — | COMPLETE, SCHEMA_INVALID | 10 ms (+2.5 s) | déterministe + LLM | `json_schema.py` + `llm_judge.py` | +| `llm_generate` / `llm_analyze` / `llm_translate` / `llm_extract_data` | server-side | JsonSchemaChecker (str non vide) | — | — | COMPLETE, SCHEMA_INVALID | 10 ms | déterministe | `json_schema.py` | +| `verify_element_exists` | server-side ou client | **OcrRoiChecker** sur ancre | PixelDiffChecker | — | COMPLETE, OCR_TEXT_MISSING | 100 ms | OCR | `ocr_roi.py` | +| `verify_text_content` | server-side ou client | **OcrRoiChecker** stricte sur `expected_text` | — | — | COMPLETE, OCR_TEXT_MISSING | 100 ms | OCR | `ocr_roi.py` | +| `download_to_folder` | server-side | **FileSystemChecker** : fichier présent dans dossier cible | — | — | COMPLETE, NO_VISUAL_CHANGE | 20 ms | FS | nouveau, trivial | +| `file_list_dir` / `file_create_dir` / `file_move` / `file_copy` | server-side | FileSystemChecker équivalent | — | — | COMPLETE | <20 ms | FS | trivial | + +**Note sur les actions whitelisted mais sans handler Léa** : `double_click`, `right_click`, +`drag`, `file_open`, `file_save`, `file_close`, `file_new`, `file_dialog` sont dans +`_ALLOWED_ACTION_TYPES` mais retournent « Type d'action inconnu » côté Léa (cf. +`reference_vwb_action_types.md`). Le Validator ne doit donc **jamais** être appelé sur ces +types — sauf si elles sont remappées par TYPE_MAP (ce qui est le cas pour `double_click` +et `right_click` via `click + button=...`). + +--- + +## 3. Fiches détaillées — les 4 cas explicitement listés par Dom + +### 3.1. Fiche `switch_tab` (changer d'onglet dans l'app — cas du bug step 10) + +#### Description précise + +Action visant à changer d'onglet dans une app multi-tab (Easily Assure, navigateur, IDE, +PDV). + +Trois sous-cas : + +- **Tab d'application native** : barre de tabs en haut d'une fenêtre Windows (Edge, VS Code). +- **Tab interne d'une SPA** : `` en plein contenu (Easily Assure, JIRA). +- **Tab MDI** : sous-fenêtre dans une app MDI (Outlook, Excel). + +L'action transmise à Léa est un `click` avec `by_text=` (cas commun +`click_anchor` → `click`). Aucun type `switch_tab` natif dans VWB aujourd'hui — c'est +toujours un `click_anchor` dont l'ancre est un tab. + +#### Signaux disponibles + +| Côté | Signal | Disponibilité | +|---|---|---| +| **Client (Léa Windows)** | screenshot_after (PNG), `actual_position` (x_pct, y_pct effectivement cliqué), `keystroke_count`, `mouse_delta_ms` | ✅ déjà câblé via `report_action_result` | +| **Client (Léa Windows)** | titre fenêtre `GetForegroundWindow().title` | ⚠ pas remonté actuellement — à ajouter au REPORT (déjà fait pour pHash via `core/grounding/title_verifier.py:25-175`) | +| **Serveur** | screenshot_before, target_spec (`by_text`), anchor.bbox (cosmétique, non utilisé en mode strict) | ✅ déjà disponible | +| **Serveur** | OCR docTR singleton sur screenshot_after | ✅ déjà chargé (`title_verifier._get_ocr`) | + +#### Algorithme de validation (pseudocode 12 lignes) + +```python +def validate_switch_tab(action, result, screenshot_after, ctx) -> ValidationResult: + # 1. ROI 120×40 px centrée sur (actual_position.x_pct, actual_position.y_pct) + roi = crop(screenshot_after, center=actual_pos, w=120, h=40) + text_in_roi = ocr(roi).lower() + expected = action["by_text"].lower() + + # 2. PRIORITÉ ABSOLUE : token suspect navigateur/système → bug step 10 + for sus in {"https", "edge", "chrome", "favoris", "barre d'adresse"}: + if sus in text_in_roi and sus not in expected: + return TERMINATE, WRONG_APPLICATION, conf=0.9 + + # 3. Match exact ou partiel (accents/casse normalisés) + if strip_accents(expected) in strip_accents(text_in_roi): + # 4. CONFIRMATION : indicateur visuel d'activation (underline, bg active) + # bande 4 px sous le label dans le screenshot_after vs before + diff_band = ssim_roi(screenshot_before, screenshot_after, + bbox=(actual_pos.x-30, actual_pos.y+18, 60, 4)) + if diff_band < 0.95: # la bande a changé → tab activé + return COMPLETE, conf=0.95 + return COMPLETE, conf=0.75 # match texte OK, animation peut-être ratée + return CONTINUE, OCR_TEXT_MISSING, conf=0.4 # → escalation LLM judge +``` + +#### Cas limites + +- **Animation tab** : Easily Assure n'anime pas, mais Edge/Chrome ont une animation 200 ms + sur l'underline. → Bande étroite testée 200 ms après le clic (`screenshot_after` doit + être pris ≥ 200 ms après l'action ; à confirmer côté agent Léa). +- **Lazy load** : Easily ne charge pas le contenu du tab tant qu'on clique pas. Si on + vérifie le contenu sous les tabs, on peut tomber sur un spinner. **Ne valider que la + zone du tab lui-même**, pas le contenu sous. +- **Popup intermédiaire** : Easily peut afficher « Voulez-vous sauvegarder les modifications + ? » avant de switcher. Le Validator doit alors retourner CONTINUE avec + `UNEXPECTED_DIALOG` → handoff DialogHandler (chaîne D2). Détection : DialogPresenceChecker + parallèle. +- **Tab déjà actif** : si on clique sur le tab actuellement actif, le screenshot ne change + pas. → Vérifier d'abord si `expected` est déjà dans la zone active *avant* le clic + (champ `screenshot_before` exploité, optimisation P1). + +#### Reproductible offline ? + +✅ **Oui**. Fixture déjà disponible : +`/home/dom/ai/rpa_vision_v3/visual_workflow_builder/backend/data/anchors/anchor_0438bd2d9bdd_1778161174_full.png` + +Cf. `AXE_B2_DEEP_VALIDATOR.md` §7 (snippet `scripts/repro_bug_step10_validator.py`) +qui démontre : +- SCENARIO 1 (clic dans URL bar Edge) → TERMINATE / WRONG_APPLICATION +- SCENARIO 2 (clic correct Imagerie) → COMPLETE + +--- + +### 3.2. Fiche `close_tab` (fermer un onglet) + +#### Description précise + +Action de fermeture d'un onglet via : +- Clic sur le « × » de l'onglet (`click_anchor` avec `by_text="×"` + bbox) +- Raccourci clavier `Ctrl+W` (Chrome, Edge, VS Code) ou `Ctrl+F4` (apps MDI Windows) + +Le tab fermé **disparaît visuellement** de la barre de tabs, et un autre tab prend le +focus (ou le navigateur se ferme si c'était le dernier). + +#### Signaux disponibles + +| Côté | Signal | Disponibilité | +|---|---|---| +| **Client** | screenshot_after, `tab_label_attendu` (variable workflow ou paramètre step) | ✅ | +| **Serveur** | screenshot_before, OCR docTR, expected `tab_label_attendu` | ✅ | +| **Serveur** | nombre de tabs détectés before/after (compté via OCR sur barre de tabs) | déterministe une fois OCR appliqué | + +#### Algorithme de validation (pseudocode 14 lignes) + +```python +def validate_close_tab(action, result, screenshot_before, screenshot_after, ctx) -> ValidationResult: + # 1. ROI barre de tabs (haut de la fenêtre app, hauteur 50 px) + # Stratégie : on prend la zone autour de actual_pos.y, largeur full + roi_before = crop(screenshot_before, y=actual_pos.y-25, h=50, x=0, w=full_w) + roi_after = crop(screenshot_after, y=actual_pos.y-25, h=50, x=0, w=full_w) + + # 2. OCR liste des labels visibles + labels_before = set(strip_accents(t.lower()) for t in ocr_words(roi_before)) + labels_after = set(strip_accents(t.lower()) for t in ocr_words(roi_after)) + target = strip_accents(ctx["tab_label_attendu"].lower()) + + # 3. Le label cible doit être DANS before et ABSENT de after + in_before = target in labels_before + in_after = target in labels_after + + if in_before and not in_after: + # 4. SSIM-ROI sur la barre confirme un changement + ssim_score = ssim_roi(roi_before, roi_after) + if ssim_score < 0.95: + return COMPLETE, conf=0.92 + return COMPLETE, conf=0.70 # OCR dit OK mais SSIM trop stable, bizarre + if in_after: + return TERMINATE, WRONG_TARGET, conf=0.85, + reason="Le tab attendu est toujours présent après close" + if not in_before: + return CONTINUE, conf=0.3, + reason="Label cible absent du before — ROI ou paramètre erroné" +``` + +#### Cas limites + +- **Dernier tab fermé** : navigateur se ferme → écran change radicalement, OCR after ne + trouve plus la barre de tabs. → Fallback : si `before contains tab` et + `after.window_title != before.window_title` → COMPLETE. +- **Confirmation « modifications non sauvegardées »** : popup intermédiaire identique à + switch_tab → routing vers DialogHandler. +- **Tab épinglé** : un tab pinned ne se ferme pas via × visible mais via menu contextuel. + Si l'action `click` est censée fermer un pinned tab → comportement non spécifié, à + TERMINATE conservativement. +- **Multi-window** : si le tab fermé était dans une fenêtre secondaire, le screenshot + serveur (capturé via `mss.monitors[1]`) peut ne pas voir la bonne fenêtre. Couvert par + la dette ouverte « coord client Léa Y cassé » (`LESSONS_LEARNED_GHT_2026-05.md`). + +#### Reproductible offline ? + +🟡 **Partiellement**. Pas de fixture before/after dédiée close_tab dans le repo. Création +nécessaire : capturer 2 screenshots Easily (before avec 5 tabs, after avec 4 tabs). +À demander dans le smoke test P0. + +--- + +### 3.3. Fiche `save` (enregistrer) + +#### Description précise + +Action de sauvegarde via : +- Raccourci `Ctrl+S` (universel) +- Clic sur bouton « Enregistrer » (`click_anchor` avec `by_text="Enregistrer"`) +- Menu Fichier > Enregistrer + +Trois signaux observables typiques : +1. **Disparition d'un marqueur « modifié »** : la title-bar passe de `Mon doc * - App` à + `Mon doc - App` (étoile, dot, ou `[modified]` retiré). +2. **Apparition d'un toast** : « Enregistré », « Saved », « Modifications enregistrées » + dans une bande inférieure ou supérieure, généralement 1-3 s avant de disparaître. +3. **Disparition d'un bouton « Enregistrer » désactivé→désactivé** : certaines apps (Easily) + désactivent le bouton après save jusqu'à la prochaine modification. + +#### Signaux disponibles + +| Côté | Signal | Disponibilité | +|---|---|---| +| **Serveur** | screenshot_before/after | ✅ | +| **Serveur** | OCR title-bar (déjà câblé `TitleVerifier`) | ✅ `core/grounding/title_verifier.py:80` | +| **Serveur** | OCR bande inférieure (zone toasts, ~150 px depuis le bas) | nouveau crop | +| **Client** | aucune télémétrie spécifique save | — | + +#### Algorithme de validation (pseudocode 15 lignes) + +```python +def validate_save(action, result, screenshot_before, screenshot_after, ctx) -> ValidationResult: + # SIGNAL 1 — marqueur « modifié » disparu de la title-bar + title_before = ocr_title_bar(screenshot_before) # ~40 px du haut + title_after = ocr_title_bar(screenshot_after) + dirty_markers = ["*", "•", "[modified]", "[modifié]", " - modifié"] + had_marker = any(m in title_before for m in dirty_markers) + still_dirty = any(m in title_after for m in dirty_markers) + if had_marker and not still_dirty: + return COMPLETE, conf=0.92, evidence={"signal": "dirty_marker_cleared"} + + # SIGNAL 2 — toast « Enregistré » détecté en bas/haut de la fenêtre + # NB: poll 3 captures espacées de 500 ms si screenshot_after est unique + toast_keywords = ["enregistré", "enregistrement", "saved", "modifications enregistrées", + "sauvegardé", "successfully"] + for region in [crop_top_30pct(screenshot_after), crop_bottom_15pct(screenshot_after)]: + text = strip_accents(ocr(region).lower()) + if any(kw in text for kw in toast_keywords): + return COMPLETE, conf=0.88, evidence={"signal": "toast", "matched_kw": kw} + + # SIGNAL 3 — si on connaît le bouton Enregistrer (ancré), check son état + if ctx.get("save_button_anchor"): + is_disabled = check_button_disabled(screenshot_after, ctx["save_button_anchor"]) + if is_disabled and ctx.get("save_button_enabled_before"): + return COMPLETE, conf=0.80, evidence={"signal": "save_button_disabled"} + + # AUCUN SIGNAL : action peut être un échec silencieux OU app sans marqueur explicite + return CONTINUE, conf=0.4, reason="Aucun signal de save observable — escalation LLM" +``` + +#### Cas limites + +- **App sans dirty marker ni toast** : Easily Assure n'affiche peut-être ni l'un ni + l'autre. → Le Validator passe en `CONTINUE conf=0.4` → escalation LlmJudgeChecker qui + demande au VLM « Y a-t-il un indice que la sauvegarde a réussi ? ». À benchmarker sur + fixtures Easily réelles. +- **Save échoue silencieusement** (espace disque, permission) : aucun toast n'apparaît, + ou un toast d'erreur (« Erreur d'enregistrement »). Le Validator doit aussi rechercher + les keywords d'erreur (`erreur`, `échec`, `failed`, `permission`) → TERMINATE + WRONG_TARGET avec haute conf si trouvé. +- **Save async** : certaines apps (Office 365, Notion) sauvent en background, indicateur + passe à `Sauvegarde en cours...` puis `Enregistré`. Polling 3 captures × 500 ms. +- **Save = nouvelle fenêtre « Enregistrer sous »** : si le doc n'avait jamais été sauvé, + Ctrl+S ouvre un dialog. → Validator détecte UNEXPECTED_DIALOG → routing vers + DialogHandler pour remplir le nom de fichier. + +#### Reproductible offline ? + +🟡 **Partiellement**. Fixtures nécessaires : +- Capture Easily *before* save (avec marqueur dirty) +- Capture Easily *after* save (sans marqueur, ou avec toast) +- Capture Easily *error* (toast d'erreur) + +Sinon, on peut tester sur fixtures Office/VS Code disponibles publiquement. + +--- + +### 3.4. Fiche `dialog_button` (cliquer un bouton dans un dialog) + +#### Description précise + +Action de clic sur un bouton standard d'une boîte de dialogue modale (OK, Annuler, Oui, +Non, Confirmer, Fermer). Le dialog peut être : +- Standard Windows (MessageBox, UAC, Windows Hello, Save As) +- Custom in-app (modal Bootstrap, modal Easily Assure, modal Citrix) + +L'effet observable : **le dialog disparaît** de l'écran et le focus retourne à la fenêtre +underlying. + +#### Signaux disponibles + +| Côté | Signal | Disponibilité | +|---|---|---| +| **Serveur** | screenshot_before (avec dialog visible), screenshot_after | ✅ | +| **Serveur** | bbox du dialog dans before (détectée par DialogPresenceChecker, OmniParser, ou heuristique « rectangle sombre + bouton » `feedback_popup_vlm.md`) | partiel — à câbler | +| **Client** | titre fenêtre (`GetForegroundWindow().title` après clic) | ⚠ à remonter dans REPORT | + +#### Algorithme de validation (pseudocode 12 lignes) + +```python +def validate_dialog_button(action, result, screenshot_before, screenshot_after, ctx) -> ValidationResult: + # 1. Détecter la bbox du dialog dans screenshot_before + # Heuristique : ROI centrale 60% width × 50% height OU bbox fournie par + # DialogPresenceChecker upstream (sauvée dans replay_state) + dialog_bbox = ctx.get("dialog_bbox") or detect_dialog_heuristic(screenshot_before) + if dialog_bbox is None: + return CONTINUE, conf=0.3, reason="Pas de dialog détecté avant — contexte manquant" + + # 2. Comparer screenshot_after sur la même bbox + before_crop = crop(screenshot_before, dialog_bbox) + after_crop = crop(screenshot_after, dialog_bbox) + ssim_score = ssim(before_crop, after_crop) + + # 3. Si SSIM > 0.95 → le dialog est toujours là (le bouton n'a rien fait) + if ssim_score > 0.95: + return TERMINATE, WRONG_TARGET, conf=0.85, + reason="Dialog toujours visible après clic" + + # 4. OCR confirmation : le titre du dialog (souvent en haut) n'est plus présent + dialog_title_before = ocr_top_strip(before_crop) + text_after_full = ocr(screenshot_after).lower() + if dialog_title_before and strip_accents(dialog_title_before.lower()) not in text_after_full: + return COMPLETE, conf=0.92 + + # 5. Dialog parti mais peut-être remplacé par un autre (cascade Easily) + return COMPLETE, conf=0.75, reason="Dialog disparu, à vérifier nouveau dialog éventuel" +``` + +#### Cas limites + +- **Cascade de dialogs** : cliquer OK sur le 1er ouvre un 2e dialog (très fréquent dans + Easily). → Validator dit COMPLETE pour le 1er, le step suivant doit gérer le 2e. +- **Bouton « Annuler » mais l'app a quand même sauvé** : sémantique du dialog # signal + visuel. Le Validator ne peut pas juger l'intention métier — il vérifie seulement la + disparition. La vérification métier est de la responsabilité du Planner (VWB). +- **Dialog non modal** (notification, toast) : techniquement pas un dialog_button. Si + l'action est `click` sur un bouton non modal, le SSIM dans la bbox restera élevé + parce que le reste de l'app n'a pas bougé. → Heuristique : `dialog_bbox` doit être au + centre et < 80% écran, sinon ce n'est pas un vrai dialog modal. +- **UAC / Windows Hello / Sécurité Windows** : ces dialogs sont **hors fenêtre app** + (système). Le pattern `OcrRoiChecker` les détecte via SUSPECT_TOKENS (« sécurité + windows », « user account control » — cf. `AXE_B2_DEEP_VALIDATOR.md` §3.3 ligne 257). + Si présents dans before → routing spécifique (`feedback_auth_dialogs_runtime.md`). + +#### Reproductible offline ? + +🟡 **Partiellement**. Fixtures nécessaires : +- Capture Easily *with* dialog (confirmation modifications non sauvées) +- Capture Easily *after* OK clicked + +Couverture indirecte par fixtures UAC standard Windows (à acquérir lors de smoke P1). + +--- + +## 4. Fiches secondaires (action standard, validation déjà couverte ou simple) + +### 4.1. `click_anchor` (cas non-tab) + +Couvert exhaustivement dans `AXE_B2_DEEP_VALIDATOR.md` §3.3 (`OcrRoiChecker`). Signal +primaire = OCR ROI 80 px ; détection SUSPECT_TOKENS pour bug step 10 ; match expected. + +### 4.2. `type_text` + +Variante d'`OcrRoiChecker` avec `radius_px=120` (zone input plus large que zone bouton). + +```python +# Variante d'OcrRoiChecker pour type_text +checker_type_text = OcrRoiChecker( + ocr_fn=easyocr_singleton, + radius_px=120, # input plus large + expected_min_confidence=0.85, # match plus tolérant (caret peut couper un caractère) +) +# action["by_text"] = texte que Léa devait taper (ex. "25003284") +# ROI autour de actual_position == zone du champ input +# verdict COMPLETE si tokens du texte tapé présents à 50%+ +``` + +### 4.3. `extract_text` / `extract_text_scroll` + +`JsonSchemaChecker` (`AXE_B2_DEEP_VALIDATOR.md` §3.5). Schema : + +```python +class ExtractTextResult(BaseModel): + value: str = Field(min_length=1, max_length=50000) + + @field_validator("value") + @classmethod + def must_have_letters(cls, v: str) -> str: + if not any(c.isalpha() for c in v): + raise ValueError("aucune lettre — vraisemblablement vide") + return v +``` + +Validation déterministe < 10 ms. Si `len(value) < 50` → escalation LlmJudge pour +plausibilité. + +### 4.4. `t2a_decision` + +Schema strict (cf. `AXE_B2_DEEP_VALIDATOR.md` §3.5) : + +```python +class T2aDecisionResult(BaseModel): + decision: Literal["UHCD", "FORFAIT", "FORFAIT_URGENCE", "NA", "INCONNU"] + justification: str = Field(min_length=10, max_length=5000) + confidence: Optional[float] = Field(default=None, ge=0.0, le=1.0) +``` + +### 4.5. `keyboard_shortcut` + +Routing selon le raccourci (table de correspondance dans le Validator) : + +| Raccourci (`keys`) | Checker primaire | Notes | +|---|---|---| +| `["ctrl", "s"]` | `SaveSuccessChecker` (§5.2) | universal save | +| `["ctrl", "w"]` ou `["ctrl", "f4"]` | `TabAbsenceChecker` | fermeture tab | +| `["ctrl", "tab"]` ou `["ctrl", "page_down"]` | `TabActiveChecker` | navigation tab | +| `["ctrl", "home"]` ou `["ctrl", "end"]` | `PixelDiffChecker` global | scroll observable | +| `["alt", "f4"]` | `DialogClosedChecker` ou TitleBarChecker (fenêtre fermée) | | +| `["escape"]` | `DialogClosedChecker` si dialog avant ; sinon NoOpChecker | | +| `["ctrl", "c"]` / `["ctrl", "x"]` / `["ctrl", "v"]` | clipboard, **pas de check visuel** (sauf pour `v` qui appelle OcrRoi) | | +| `["enter"]` / `["tab"]` (focus next) | `PixelDiffChecker` ROI 100 px autour caret | | +| autre | `TitleBarChecker` | fallback générique | + +Code de dispatch : + +```python +def select_checker_for_key_combo(action: dict) -> ActionChecker: + keys = [k.lower() for k in action.get("keys", [])] + keys_set = set(keys) + if keys_set == {"ctrl", "s"}: return _save_checker + if keys_set in ({"ctrl", "w"}, {"ctrl", "f4"}): return _tab_absence_checker + if keys_set in ({"ctrl", "tab"}, {"ctrl", "page_down"}, {"ctrl", "page_up"}): + return _tab_active_checker + if keys_set & {"home", "end", "page_up", "page_down"}: + return _pixel_diff_checker + if keys_set == {"alt", "f4"} or keys_set == {"escape"}: + return _dialog_closed_checker + return _title_bar_checker +``` + +--- + +## 5. Nouveaux Checkers à ajouter au package `core/validation/` + +Tous s'inscrivent dans l'architecture posée par `AXE_B2_DEEP_VALIDATOR.md` §2 : Protocol +`ActionChecker`, retournent `ValidationResult`, exceptions catchées au niveau orchestrateur. + +### 5.1. `core/validation/checkers/tab_active.py` (NEW) + +Pour `switch_tab` (via `click_anchor` sur un tab) et `keyboard_shortcut` Ctrl+Tab. +Combine OcrRoiChecker + détection bande indicateur d'activation. + +```python +# core/validation/checkers/tab_active.py +"""TabActiveChecker / TabAbsenceChecker — pour switch_tab et close_tab. + +TabActiveChecker : confirme que le tab visé est activé (label présent + bande +indicateur a changé sous le label dans une ROI étroite). +TabAbsenceChecker : confirme qu'un tab a disparu de la barre de tabs. +""" +from __future__ import annotations +import time +import unicodedata +from typing import Any, Callable, Dict, Optional + +from core.validation.result import ValidationResult, Verdict, FailureCategory + + +def _strip_accents(s: str) -> str: + return "".join( + c for c in unicodedata.normalize("NFKD", s) if not unicodedata.combining(c) + ).lower().strip() + + +class TabActiveChecker: + """Vérifie qu'un tab a été activé après un click_anchor sur un tab. + + Signal 1 (OBLIGATOIRE) : label du tab attendu présent dans ROI 120×40 autour + du point cliqué. + Signal 2 (RENFORCEMENT) : bande 4 px sous le label a changé entre before/after + (indicateur d'activation : underline, fond, bold). + + Si signal 1 OK + signal 2 OK → COMPLETE conf=0.95 + Si signal 1 OK + signal 2 manquant → COMPLETE conf=0.75 + Si signal 1 KO → CONTINUE (escalation LLM judge) + """ + name = "tab_active" + budget_ms = 150.0 + + SUSPECT_TOKENS = ( + "edge", "chrome", "firefox", "https", "http", ".com", ".fr", + "favoris", "barre d'adresse", "sécurité windows", + ) + + def __init__( + self, + ocr_fn: Callable, + roi_w_px: int = 120, + roi_h_px: int = 40, + underline_strip_offset_y_px: int = 18, # px sous le centre du label + underline_strip_h_px: int = 4, + ssim_change_threshold: float = 0.95, # < 0.95 = bande a changé + ): + self._ocr = ocr_fn + self._w = roi_w_px + self._h = roi_h_px + self._strip_dy = underline_strip_offset_y_px + self._strip_h = underline_strip_h_px + self._ssim_thr = ssim_change_threshold + + def check(self, action, result, screenshot_before, screenshot_after, context) -> ValidationResult: + t0 = time.time() + target_spec = action.get("target_spec") or {} + expected = action.get("by_text") or target_spec.get("by_text") or context.get("expected_text", "") + actual_pos = result.get("actual_position") or {} + x_pct = actual_pos.get("x_pct") or action.get("x_pct") + y_pct = actual_pos.get("y_pct") or action.get("y_pct") + + if not screenshot_after or x_pct is None or y_pct is None or not expected: + return ValidationResult( + verdict=Verdict.CONTINUE, confidence=0.2, + check_used=self.name, elapsed_ms=(time.time() - t0) * 1000, + reasoning="ROI indéfinie pour TabActiveChecker", + ) + + from PIL import Image + from agent_v0.server_v1.replay_verifier import ReplayVerifier + rv = ReplayVerifier() + img_after = rv._load_single_image(screenshot_after) + w, h = img_after.size + cx, cy = int(x_pct * w), int(y_pct * h) + + # ROI 120×40 centrée sur le clic + roi = img_after.crop(( + max(0, cx - self._w // 2), max(0, cy - self._h // 2), + min(w, cx + self._w // 2), min(h, cy + self._h // 2), + )) + + try: + text = self._ocr(roi) or "" + except Exception as exc: + return ValidationResult( + verdict=Verdict.CONTINUE, confidence=0.1, + check_used=self.name, elapsed_ms=(time.time() - t0) * 1000, + reasoning=f"OCR erreur: {exc}", + ) + + text_norm = _strip_accents(text) + expected_norm = _strip_accents(expected) + + # Signal négatif : token suspect → WRONG_APPLICATION + for sus in self.SUSPECT_TOKENS: + if sus in text_norm and sus not in expected_norm: + return ValidationResult( + verdict=Verdict.TERMINATE, confidence=0.9, + check_used=self.name, elapsed_ms=(time.time() - t0) * 1000, + failure_category=FailureCategory.WRONG_APPLICATION, + reasoning=f"Token suspect '{sus}' dans ROI tab — hors-app", + raw_evidence={"roi_text": text[:200], "expected": expected}, + ) + + # Signal positif : label attendu présent + if expected_norm not in text_norm: + return ValidationResult( + verdict=Verdict.CONTINUE, confidence=0.4, + check_used=self.name, elapsed_ms=(time.time() - t0) * 1000, + failure_category=FailureCategory.OCR_TEXT_MISSING, + reasoning=f"Label '{expected[:40]}' absent de la ROI tab", + raw_evidence={"roi_text": text[:200]}, + ) + + # Renforcement : SSIM bande indicateur 4 px sous le label + ssim_score = None + if screenshot_before: + try: + img_before = rv._load_single_image(screenshot_before) + strip_bbox = ( + max(0, cx - 30), max(0, cy + self._strip_dy), + min(w, cx + 30), min(h, cy + self._strip_dy + self._strip_h), + ) + strip_before = img_before.crop(strip_bbox) + strip_after = img_after.crop(strip_bbox) + ssim_score = self._ssim_pair(strip_before, strip_after) + except Exception: + ssim_score = None + + elapsed_ms = (time.time() - t0) * 1000 + if ssim_score is not None and ssim_score < self._ssim_thr: + return ValidationResult( + verdict=Verdict.COMPLETE, confidence=0.95, + check_used=self.name, elapsed_ms=elapsed_ms, + reasoning=f"Label '{expected}' trouvé + bande indicateur a changé (SSIM={ssim_score:.2f})", + raw_evidence={"roi_text": text[:200], "ssim_strip": ssim_score}, + ) + return ValidationResult( + verdict=Verdict.COMPLETE, confidence=0.75, + check_used=self.name, elapsed_ms=elapsed_ms, + reasoning=f"Label '{expected}' trouvé (sans renfort SSIM)", + raw_evidence={"roi_text": text[:200], "ssim_strip": ssim_score}, + ) + + @staticmethod + def _ssim_pair(img_a, img_b) -> float: + """SSIM grayscale entre 2 PIL.Image.""" + import numpy as np + from skimage.metrics import structural_similarity as ssim + a = np.array(img_a.convert("L")) + b = np.array(img_b.convert("L")) + if a.shape != b.shape: + return 1.0 # ne peut pas comparer → on suppose pas de changement + return float(ssim(a, b)) + + +class TabAbsenceChecker: + """Vérifie qu'un tab a DISPARU de la barre de tabs (close_tab).""" + name = "tab_absence" + budget_ms = 150.0 + + def __init__(self, ocr_fn: Callable, tab_bar_h_px: int = 50): + self._ocr = ocr_fn + self._h = tab_bar_h_px + + def check(self, action, result, screenshot_before, screenshot_after, context) -> ValidationResult: + t0 = time.time() + target_label = context.get("tab_label_attendu") or action.get("by_text", "") + actual_pos = result.get("actual_position") or {} + y_pct = actual_pos.get("y_pct") or action.get("y_pct") + + if not screenshot_before or not screenshot_after or not target_label or y_pct is None: + return ValidationResult( + verdict=Verdict.CONTINUE, confidence=0.2, + check_used=self.name, elapsed_ms=(time.time() - t0) * 1000, + reasoning="Contexte insuffisant (label, before, after, y_pct requis)", + ) + + from agent_v0.server_v1.replay_verifier import ReplayVerifier + rv = ReplayVerifier() + img_b = rv._load_single_image(screenshot_before) + img_a = rv._load_single_image(screenshot_after) + w, h = img_a.size + cy = int(y_pct * h) + + # ROI : barre de tabs (largeur full, hauteur tab_bar_h centrée sur le clic) + bbox = (0, max(0, cy - self._h // 2), w, min(h, cy + self._h // 2)) + text_before = _strip_accents(self._ocr(img_b.crop(bbox)) or "") + text_after = _strip_accents(self._ocr(img_a.crop(bbox)) or "") + target_norm = _strip_accents(target_label) + + in_before = target_norm in text_before + in_after = target_norm in text_after + elapsed_ms = (time.time() - t0) * 1000 + + if in_before and not in_after: + return ValidationResult( + verdict=Verdict.COMPLETE, confidence=0.92, + check_used=self.name, elapsed_ms=elapsed_ms, + reasoning=f"Tab '{target_label}' disparu de la barre", + raw_evidence={"text_before": text_before[:200], "text_after": text_after[:200]}, + ) + if in_after: + return ValidationResult( + verdict=Verdict.TERMINATE, confidence=0.85, + check_used=self.name, elapsed_ms=elapsed_ms, + failure_category=FailureCategory.WRONG_TARGET, + reasoning=f"Tab '{target_label}' toujours présent après close", + raw_evidence={"text_after": text_after[:200]}, + ) + return ValidationResult( + verdict=Verdict.CONTINUE, confidence=0.3, + check_used=self.name, elapsed_ms=elapsed_ms, + reasoning=f"Tab '{target_label}' absent même du before — ROI ou label erroné", + raw_evidence={"text_before": text_before[:200]}, + ) +``` + +### 5.2. `core/validation/checkers/save_success.py` (NEW) + +Pour `save` (Ctrl+S ou clic bouton Enregistrer). + +```python +# core/validation/checkers/save_success.py +"""SaveSuccessChecker — confirme qu'une action save a eu l'effet voulu. + +Signaux dans l'ordre de priorité : +1. Marqueur « modifié » disparu de la title-bar (`*`, `•`, `[modified]`) +2. Toast « Enregistré » / « Saved » apparu dans bande haute ou basse +3. Keyword d'erreur (`erreur`, `failed`) → TERMINATE +""" +from __future__ import annotations +import time +import unicodedata +from typing import Any, Callable, Dict, Optional + +from core.validation.result import ValidationResult, Verdict, FailureCategory + + +def _strip_accents(s: str) -> str: + return "".join( + c for c in unicodedata.normalize("NFKD", s) if not unicodedata.combining(c) + ).lower().strip() + + +class SaveSuccessChecker: + name = "save_success" + budget_ms = 200.0 + + DIRTY_MARKERS = ("*", "•", "[modified]", "[modifié]", " - modifié", " modifié") + SUCCESS_KEYWORDS = ( + "enregistré", "enregistrement effectué", "modifications enregistrées", + "saved", "sauvegardé", "successfully saved", "document enregistré", + ) + ERROR_KEYWORDS = ( + "erreur d'enregistrement", "échec enregistrement", "save failed", + "permission denied", "disk full", "impossible d'enregistrer", + ) + + def __init__( + self, + ocr_fn: Callable, + title_bar_h_px: int = 40, + toast_top_h_px: int = 200, + toast_bottom_h_px: int = 150, + ): + self._ocr = ocr_fn + self._title_h = title_bar_h_px + self._toast_top_h = toast_top_h_px + self._toast_bot_h = toast_bottom_h_px + + def check(self, action, result, screenshot_before, screenshot_after, context) -> ValidationResult: + t0 = time.time() + if not screenshot_after: + return ValidationResult( + verdict=Verdict.CONTINUE, confidence=0.2, + check_used=self.name, elapsed_ms=(time.time() - t0) * 1000, + reasoning="screenshot_after manquant", + ) + + from agent_v0.server_v1.replay_verifier import ReplayVerifier + rv = ReplayVerifier() + img_a = rv._load_single_image(screenshot_after) + w, h = img_a.size + + # SIGNAL 3 : keyword d'erreur (priorité, on veut détecter tôt) + full_text = _strip_accents(self._ocr(img_a) or "") + for err in self.ERROR_KEYWORDS: + if err in full_text: + return ValidationResult( + verdict=Verdict.TERMINATE, confidence=0.92, + check_used=self.name, elapsed_ms=(time.time() - t0) * 1000, + failure_category=FailureCategory.WRONG_TARGET, + reasoning=f"Keyword d'erreur détecté : '{err}'", + raw_evidence={"matched_error": err}, + ) + + # SIGNAL 1 : marqueur dirty disparu + if screenshot_before: + img_b = rv._load_single_image(screenshot_before) + title_before = (self._ocr(img_b.crop((0, 0, w, self._title_h))) or "").lower() + title_after = (self._ocr(img_a.crop((0, 0, w, self._title_h))) or "").lower() + had_marker = any(m in title_before for m in self.DIRTY_MARKERS) + still_dirty = any(m in title_after for m in self.DIRTY_MARKERS) + if had_marker and not still_dirty: + return ValidationResult( + verdict=Verdict.COMPLETE, confidence=0.92, + check_used=self.name, elapsed_ms=(time.time() - t0) * 1000, + reasoning="Marqueur dirty disparu de la title-bar", + raw_evidence={"title_before": title_before[:120], "title_after": title_after[:120]}, + ) + + # SIGNAL 2 : toast success en haut ou bas + for region_name, bbox in [ + ("toast_top", (0, 0, w, self._toast_top_h)), + ("toast_bottom", (0, h - self._toast_bot_h, w, h)), + ]: + text = _strip_accents(self._ocr(img_a.crop(bbox)) or "") + for kw in self.SUCCESS_KEYWORDS: + if kw in text: + return ValidationResult( + verdict=Verdict.COMPLETE, confidence=0.88, + check_used=self.name, elapsed_ms=(time.time() - t0) * 1000, + reasoning=f"Toast '{kw}' détecté ({region_name})", + raw_evidence={"matched_kw": kw, "region": region_name}, + ) + + # Aucun signal → escalation LLM + return ValidationResult( + verdict=Verdict.CONTINUE, confidence=0.4, + check_used=self.name, elapsed_ms=(time.time() - t0) * 1000, + reasoning="Aucun signal de save observable — escalation LLM judge", + ) +``` + +### 5.3. `core/validation/checkers/dialog_closed.py` (NEW) + +Pour `dialog_button` (clic OK/Annuler/Oui/Non) et raccourcis Esc/Alt+F4 sur un dialog. + +```python +# core/validation/checkers/dialog_closed.py +"""DialogClosedChecker — confirme qu'un dialog modal a été fermé. + +Stratégie : SSIM-ROI sur la bbox du dialog (issue de screenshot_before). +Si SSIM < 0.95 (zone a changé) ET le titre du dialog n'est plus dans after → COMPLETE. +Si SSIM > 0.95 (zone identique) → TERMINATE (le bouton n'a rien fait). +""" +from __future__ import annotations +import time +import unicodedata +from typing import Any, Callable, Dict, Optional + +from core.validation.result import ValidationResult, Verdict, FailureCategory + + +def _strip_accents(s: str) -> str: + return "".join( + c for c in unicodedata.normalize("NFKD", s) if not unicodedata.combining(c) + ).lower().strip() + + +class DialogClosedChecker: + name = "dialog_closed" + budget_ms = 200.0 + + def __init__( + self, + ocr_fn: Callable, + ssim_change_threshold: float = 0.95, + # ROI heuristique par défaut : centre 60% x 50% + default_bbox_rel: tuple = (0.20, 0.25, 0.80, 0.75), + ): + self._ocr = ocr_fn + self._ssim_thr = ssim_change_threshold + self._default_bbox = default_bbox_rel + + def check(self, action, result, screenshot_before, screenshot_after, context) -> ValidationResult: + t0 = time.time() + if not screenshot_before or not screenshot_after: + return ValidationResult( + verdict=Verdict.CONTINUE, confidence=0.2, + check_used=self.name, elapsed_ms=(time.time() - t0) * 1000, + reasoning="screenshots before/after requis", + ) + + from agent_v0.server_v1.replay_verifier import ReplayVerifier + rv = ReplayVerifier() + img_b = rv._load_single_image(screenshot_before) + img_a = rv._load_single_image(screenshot_after) + w, h = img_a.size + + # bbox du dialog : passée par contexte (issue de DialogPresenceChecker upstream) + # ou heuristique centrale par défaut + dialog_bbox = context.get("dialog_bbox") + if dialog_bbox is None: + x0, y0, x1, y1 = self._default_bbox + dialog_bbox = (int(x0 * w), int(y0 * h), int(x1 * w), int(y1 * h)) + + crop_b = img_b.crop(dialog_bbox) + crop_a = img_a.crop(dialog_bbox) + + # SSIM sur la bbox + import numpy as np + from skimage.metrics import structural_similarity as ssim + arr_b = np.array(crop_b.convert("L")) + arr_a = np.array(crop_a.convert("L")) + ssim_score = float(ssim(arr_b, arr_a)) if arr_b.shape == arr_a.shape else 1.0 + + elapsed_ms = (time.time() - t0) * 1000 + + # SSIM élevé → dialog toujours là + if ssim_score > self._ssim_thr: + return ValidationResult( + verdict=Verdict.TERMINATE, confidence=0.85, + check_used=self.name, elapsed_ms=elapsed_ms, + failure_category=FailureCategory.WRONG_TARGET, + reasoning=f"Dialog toujours présent (SSIM={ssim_score:.2f} > {self._ssim_thr})", + raw_evidence={"ssim": ssim_score, "bbox": list(dialog_bbox)}, + ) + + # SSIM faible : confirmer par OCR — titre du dialog disparu du after ? + title_strip = crop_b.crop((0, 0, crop_b.width, max(20, crop_b.height // 4))) + title_before = _strip_accents(self._ocr(title_strip) or "")[:80] + full_after = _strip_accents(self._ocr(img_a) or "") + + if title_before and len(title_before) > 5 and title_before not in full_after: + return ValidationResult( + verdict=Verdict.COMPLETE, confidence=0.92, + check_used=self.name, elapsed_ms=elapsed_ms, + reasoning=f"Dialog fermé (SSIM={ssim_score:.2f}, titre absent du after)", + raw_evidence={"ssim": ssim_score, "dialog_title_before": title_before}, + ) + + return ValidationResult( + verdict=Verdict.COMPLETE, confidence=0.75, + check_used=self.name, elapsed_ms=elapsed_ms, + reasoning=f"Dialog disparu (SSIM={ssim_score:.2f}, titre OCR ambigu)", + raw_evidence={"ssim": ssim_score}, + ) +``` + +### 5.4. Câblage dans le `Validator` (extension du dispatcher §6.1 de `AXE_B2_DEEP_VALIDATOR.md`) + +```python +# Pseudo-init lazy à intégrer dans api_stream.py:3447 (cf. AXE_B2_DEEP §6.1) +from core.validation.checkers.tab_active import TabActiveChecker, TabAbsenceChecker +from core.validation.checkers.save_success import SaveSuccessChecker +from core.validation.checkers.dialog_closed import DialogClosedChecker + +_validator_v2 = Validator( + checkers={ + # click_anchor générique + "click": [OcrRoiChecker(ocr_fn=_ocr, radius_px=80)], + # type_text + "type": [OcrRoiChecker(ocr_fn=_ocr, radius_px=120)], + # key_combo — dispatch interne par select_checker_for_key_combo (cf. §4.5) + # Implémentation : un MetaChecker qui choisit le sous-checker selon action["keys"] + "key_combo": [KeyComboDispatcher( + save_checker=SaveSuccessChecker(_ocr), + tab_active_checker=TabActiveChecker(_ocr), + tab_absence_checker=TabAbsenceChecker(_ocr), + dialog_closed_checker=DialogClosedChecker(_ocr), + pixel_diff_checker=PixelDiffChecker(_replay_verifier), + title_bar_checker=TitleBarChecker(), + )], + # extract_*, t2a_decision, db_* + "extract_text": [JsonSchemaChecker()], + "extract_text_scroll": [JsonSchemaChecker()], + "extract_table": [JsonSchemaChecker()], + "t2a_decision": [JsonSchemaChecker()], + # screenshot_evidence (passive) + "screenshot_evidence": [TitleBarChecker()], + # paste_and_execute (bypass NoMachine) + "paste_and_execute": [PixelDiffChecker(_replay_verifier)], + }, + default_checkers=[PixelDiffChecker(_replay_verifier)], + escalation_checker=LlmJudgeChecker(_replay_verifier), + accept_confidence=0.7, + escalate_below_confidence=0.55, +) +``` + +Le `KeyComboDispatcher` est un mini-router qui implémente `ActionChecker` : + +```python +# core/validation/checkers/key_combo_dispatcher.py +"""KeyComboDispatcher — route un keyboard_shortcut vers le sous-checker adapté.""" +class KeyComboDispatcher: + name = "key_combo_dispatcher" + budget_ms = 250.0 + + def __init__(self, save_checker, tab_active_checker, tab_absence_checker, + dialog_closed_checker, pixel_diff_checker, title_bar_checker): + self._save = save_checker + self._tab_active = tab_active_checker + self._tab_absence = tab_absence_checker + self._dialog_closed = dialog_closed_checker + self._pixel = pixel_diff_checker + self._title = title_bar_checker + + def check(self, action, result, screenshot_before, screenshot_after, context): + keys = {k.lower() for k in action.get("keys", [])} + # Routing + if keys == {"ctrl", "s"}: + return self._save.check(action, result, screenshot_before, screenshot_after, context) + if keys in ({"ctrl", "w"}, {"ctrl", "f4"}): + return self._tab_absence.check(action, result, screenshot_before, screenshot_after, context) + if keys in ({"ctrl", "tab"}, {"ctrl", "page_down"}, {"ctrl", "page_up"}): + return self._tab_active.check(action, result, screenshot_before, screenshot_after, context) + if keys & {"home", "end", "page_up", "page_down"}: + return self._pixel.check(action, result, screenshot_before, screenshot_after, context) + if keys == {"alt", "f4"} or keys == {"escape"}: + return self._dialog_closed.check(action, result, screenshot_before, screenshot_after, context) + # fallback : titre fenêtre + return self._title.check(action, result, screenshot_before, screenshot_after, context) +``` + +--- + +## 6. Confidence scoring et agrégation + +### 6.1. Règles par type d'action + +| Action | Acceptation SUCCESS direct | Escalation LLM judge | Échec direct (TERMINATE) | +|---|---|---|---| +| `click_anchor` | primaire ≥ 0.85 | 0.50 ≤ primaire < 0.85 | primaire = TERMINATE conf ≥ 0.85 (suspect token) | +| `switch_tab` (`TabActiveChecker`) | primaire ≥ 0.85 OU (label OK 0.75 + SSIM bande change) | 0.50 ≤ primaire < 0.85 | suspect token conf ≥ 0.85 | +| `close_tab` (`TabAbsenceChecker`) | primaire ≥ 0.85 (label disparu) | 0.50 ≤ primaire < 0.85 | label encore là conf ≥ 0.80 | +| `save` (`SaveSuccessChecker`) | primaire ≥ 0.85 (marqueur OU toast) | 0.40 ≤ primaire < 0.85 (aucun signal) | keyword erreur conf ≥ 0.90 | +| `dialog_button` (`DialogClosedChecker`) | primaire ≥ 0.85 (SSIM change + titre absent) | 0.55 ≤ primaire < 0.85 | SSIM stable conf ≥ 0.80 | +| `type_text` | OCR ROI ≥ 0.80 | 0.50 ≤ primaire < 0.80 | OCR vide conf ≥ 0.80 | +| `extract_text` / `t2a_decision` | schema valide conf = 0.95 | n/a (schema déterministe) | schema invalide conf = 0.90 | +| `keyboard_shortcut` (autre) | TitleBarChecker conf ≥ 0.70 | 0.40 ≤ primaire < 0.70 | rare | +| `paste_and_execute` | SSIM-ROI change conf ≥ 0.70 | 0.40 ≤ primaire < 0.70 | SSIM stable conf ≥ 0.75 | +| `screenshot_evidence` | TitleBarChecker conf ≥ 0.50 (action passive) | rarement | jamais | +| `visual_condition` | LlmJudge conf ≥ 0.70 | n/a (déjà LLM) | LlmJudge dit faux conf ≥ 0.80 | + +### 6.2. Stratégie d'agrégation multi-checker + +Quand plusieurs checkers s'exécutent en cascade (ex. `double_click_anchor` = TitleBarChecker +puis OcrRoiChecker) : + +```python +# Pseudocode logique d'agrégation +def aggregate(results: list[ValidationResult]) -> ValidationResult: + # Règle 1 : si UN checker dit TERMINATE conf ≥ 0.85 → TERMINATE + for r in results: + if r.verdict == Verdict.TERMINATE and r.confidence >= 0.85: + return r + # Règle 2 : si UN checker dit COMPLETE conf ≥ accept_threshold → COMPLETE + completes = [r for r in results if r.verdict == Verdict.COMPLETE and r.confidence >= 0.7] + if completes: + return max(completes, key=lambda r: r.confidence) + # Règle 3 : tous CONTINUE → escalation LLM + return None # → escalation +``` + +### 6.3. Seuils par défaut + +- `accept_confidence = 0.70` (Validator orchestrateur) +- `escalate_below_confidence = 0.55` +- Plafond `LlmJudgeChecker` : `0.9` (jamais 1.0, on garde une marge d'incertitude) + +--- + +## 7. Anti-patterns à éviter + +| Anti-pattern | Cas concret | Pourquoi c'est faux | Solution | +|---|---|---|---| +| **pHash global** comme signal `switch_tab` | bug step 10 GHT : pHash voit du mouvement (URL bar Edge change pixel), conclut SUCCESS | Aucune information *spatiale* — un clic dans la URL bar change l'écran sans switcher le tab | OcrRoiChecker 80 px (`AXE_B2_DEEP_VALIDATOR.md` §3.3) | +| **Title-bar seule** sur SPA | switch_tab dans Easily Assure (SPA Edge) — le titre Edge ne change pas | Les SPA ne changent pas le titre fenêtre du navigateur lors d'un switch interne | OCR ROI sur tab + SSIM bande indicateur | +| **`actual_position` retourné par l'agent = success** | Léa renvoie `actual_position=(0.23, 0.155)` et `success=True` même quand elle a cliqué dans la URL bar | L'agent ne sait pas distinguer un clic réussi d'un clic raté — il sait juste qu'il a envoyé un MouseEvent | Override server-side avec OcrRoiChecker | +| **Vérifier le contenu sous les tabs** pour `switch_tab` | Easily lazy-load → on voit un spinner → conclut UI_LOADING / faux échec | Le contenu n'est pas synchrone avec l'activation du tab | Ne vérifier que la zone du tab lui-même (indicateur d'activation) | +| **SSIM global pour `dialog_button`** | Si on compare le screenshot entier before/after, le SSIM est dominé par la zone underlying (90% inchangée) → SSIM = 0.97 même si dialog parti | Le signal pertinent est local à la bbox du dialog | SSIM-ROI sur `dialog_bbox` | +| **Pixel-diff global pour `save`** | Save peut ne changer que 4 px (marqueur dirty disparu) → pHash dit « écran identique » | Le signal de save est extrêmement localisé | SaveSuccessChecker OCR title-bar + ROI toasts | +| **Vérifier `success=True` envoyé par client** sans cross-check serveur | bug 8 mai : Léa fait timeout, action perdue, mais ZÉRO REPORT remonte ; ou pire, REPORT success=True après clic raté | Le client ne peut pas observer ses propres erreurs spatiales (il a cliqué là où on lui a dit) | Server-side Validator obligatoire | +| **OCR sur image entière** pour valider un clic local | OCR EasyOCR sur 2560×1600 = 1-2 s, et on perd la localisation (le texte attendu existe peut-être ailleurs) | Localisation perdue + coût élevé | OCR sur crop ROI | +| **LLM judge en SUCCESS path** | Appeler `verify_with_critic` à chaque action = +80 s sur 40 steps | Coût rédhibitoire en démo | LLM judge en escalation uniquement (cf. `AXE_B2_DEEP_VALIDATOR.md` §4) | +| **Confondre `t2a_decision = NA`** avec échec | `decision="NA"` est un verdict métier valide (cas sans T2A), pas une erreur | Le LLM peut renvoyer "NA" légitimement | Schema accepte "NA" comme valeur valide | +| **Ne pas catcher l'exception OCR** | OCR crash sur crop 0×0 ou mémoire → tout le Validator crash → on perd la trace du verdict | Robustesse | Try/except dans chaque Checker (cf. `AXE_B2_DEEP_VALIDATOR.md` §3.7 ligne 720 `try: checker.check`) | +| **Considérer `paste_and_execute` comme `type_text`** | `ydotool Ctrl+V` ne tape pas char par char, l'OCR ROI peut voir un texte différent | Le bypass NoMachine colle d'un coup, vérification par diff pixel suffit | PixelDiffChecker primaire | +| **Verdict CONTINUE infini** | Si tous les checkers disent CONTINUE et l'escalation aussi, on est coincé | Boucle | `route_verdict` doit imposer `max_rechecks=2` puis TERMINATE (cf. `AXE_B2_DEEP_VALIDATOR.md` §5) | + +--- + +## 8. Précédents externes (fiches courtes) + +### 8.1. Skyvern — `complete_verify` générique + +- **Approche** : 1 seul check VLM par step, prompt Jinja2 `check-user-goal.j2`, sortie JSON + stricte `{is_complete, is_terminate}`. +- **Apport** : modèle Planner-Actor-Validator formalisé, prompt verbatim disponible. +- **Limite pour nous** : 1 appel LLM par step = 2-3 s × 46 steps = 90-140 s. Inapplicable + en démo sans matrice par type d'action. +- **Adoption** : on garde le pattern Validator-as-component mais on remplace le LLM par + des Checkers spécialisés sauf en escalation. +- Source : [`agent.py` ligne 2609](https://github.com/Skyvern-AI/skyvern/blob/main/skyvern/forge/agent.py), + prompt [`check-user-goal.j2`](https://raw.githubusercontent.com/Skyvern-AI/skyvern/main/skyvern/forge/prompts/skyvern/check-user-goal.j2). + +### 8.2. browser-use — agentic judge + +- **Approche** : LLM judge (gemini-2.5-flash) appelé après `done`, sortie JSON + `{verdict, failure_reason}`, philosophie « simple prompts, absolute True/False ». +- **Apport** : démonstration que les prompts simples + verdicts binaires battent les + rubrics complexes (87% accord humain). +- **Limite pour nous** : appliqué qu'en fin d'agent, pas par step ; pas de matrice par + type d'action. +- **Adoption** : on retient la philosophie binaire pour `LlmJudgeChecker` (déjà fait dans + `replay_verifier.verify_with_critic` qui force `semantic_verified ∈ {True, False, None}`). +- Source : [browser-use evaluation system](https://browser-use.com/posts/our-browser-agent-evaluation-system). + +### 8.3. Playwright Python — `expect()` assertions + +- **Approche** : assertions auto-retry avec timeout par défaut 5 s. Méthodes principales : + `to_have_text()`, `to_be_visible()`, `to_be_disabled()`, `to_have_url()`, + `to_have_title()`. +- **Apport** : équivalent direct de notre matrice par type d'action — chaque action a + une assertion typée. +- **Mapping pour notre cas** : + - `to_have_text(label)` → `OcrRoiChecker(by_text=label)` (notre version sans DOM) + - `to_be_visible(selector)` → `OcrRoiChecker` ou `TitleBarChecker` + - `to_have_url(pattern)` → bug step 10 : `OcrRoiChecker` URL bar avec SUSPECT_TOKENS + - `to_have_title(re.compile(r'.*Easily.*'))` → `TitleBarChecker` + - `to_be_hidden(dialog)` → `DialogClosedChecker` +- **Limite** : repose sur DOM, pas applicable à Easily Assure (pas d'API accessibility). +- Sources : [Playwright Python LocatorAssertions](https://playwright.dev/python/docs/api/class-locatorassertions), + [Playwright Assertions guide BrowserStack 2026](https://www.browserstack.com/guide/playwright-assertions). + +### 8.4. Selenium `WebDriverWait` + `expected_conditions` + +- **Approche** : pattern `wait.until(EC.condition(...))` avec polling 500 ms jusqu'à + timeout. Conditions clés : + - `visibility_of_element_located` (apparition) + - `invisibility_of_element_located` (disparition — cas `close_tab`, `dialog_button`) + - `staleness_of` (élément retiré du DOM — pas applicable nous) + - `text_to_be_present_in_element` (notre `OcrRoiChecker` équivalent) + - `element_to_be_clickable` (pas applicable — nous, c'est avant l'action) +- **Apport** : pattern de polling avec timeout, à adopter pour `wait_for_anchor` et + `pause_for_human` (déjà câblé QW4). +- **Mapping** : `invisibility_of_element_located` → `DialogClosedChecker` / + `TabAbsenceChecker`. +- Sources : [Selenium 4.44 expected_conditions Python](https://www.selenium.dev/selenium/docs/api/py/selenium_webdriver_support/selenium.webdriver.support.expected_conditions.html), + [Selenium waits documentation](https://www.selenium.dev/documentation/webdriver/support_features/expected_conditions/). + +### 8.5. SikuliX — `exists()`, `wait()`, `waitVanish()`, `onAppear()`, `onVanish()` + +- **Approche** : visual pattern matching, `exists(image, timeout)` retourne `Match|None`, + `waitVanish(image, timeout)` attend qu'une image disparaisse, `onAppear/onVanish/onChange` + registre des observateurs sur une `Region`. +- **Apport** : précédent historique direct du « 100% vision ». Le pattern `waitVanish` est + exactement notre `DialogClosedChecker` / `TabAbsenceChecker`. La notion de `Region` + (ROI typée) est aussi celle qu'on adopte. +- **Limite** : pas de notion de confidence multi-signal, pas de LLM judge en escalation. +- **Adoption** : on calque les noms (`Active`, `Absence`, `Closed`, `Presence`) sur la + sémantique SikuliX (`Appear`, `Vanish`) pour cohérence. +- Sources : [SikuliX Region documentation](https://sikulix-2014.readthedocs.io/en/latest/region.html), + [SikuliX FAQ wait/exists](https://answers.launchpad.net/sikuli/+question/693582). + +### 8.6. (Bonus) PyImageSearch / AutoIt patterns post-action + +- **Approche** : screenshot diff + template re-match. Pas formalisé en framework. +- **Apport** : confirme que la combinaison « pixel diff (rapide) + template match + (localisation) » est le couteau-suisse historique. +- **Adoption** : on garde `PixelDiffChecker` comme pré-filtre 15 ms (déjà existant via + `replay_verifier.verify_action`). + +--- + +## 9. Liens avec autres specs ouvertes (à valider par Dom) + +### 9.1. Lien avec `spec_transport` (à rédiger ou en cours) + +Le Validator agit **après** réception du REPORT du client. Si la couche transport perd un +REPORT (cas du timeout client 5 s, bug 8 mai cause #1), le Validator **ne tourne jamais**. + +Implications : +- Le watchdog `_retry_pending` (`AXE_B1_DEEP_WATCHDOG.md`) est **prérequis** pour que le + Validator soit utile dans tous les cas. +- Si on passe en SSE/WebSocket (fix structurel `REPLAY_BLOCAGE_NOTES_MEDICALES §5`), le + REPORT devient un message push fiable, le Validator tourne systématiquement. +- En attendant, prévoir un check serveur-only « si pas de REPORT après 30 s, capturer + screenshot serveur + valider hors-client » (mais cela suppose un screenshot serveur + via VNC/RDP/mss côté Léa — pas en place aujourd'hui). + +### 9.2. Lien avec `spec_popups` (chaîne D2, à rédiger ou en cours) + +Le `FailureCategory.UNEXPECTED_DIALOG` du Validator est l'entrée du dispatcher D2 : + +```python +# route_verdict (cf. AXE_B2_DEEP_VALIDATOR.md §5) +if fc == FailureCategory.UNEXPECTED_DIALOG: + return {"action": "handoff_dialog_handler", ...} +``` + +Implications : +- Le `DialogPresenceChecker` (P1) DOIT être implémenté avant que le routing + `handoff_dialog_handler` soit effectif. +- Pour `dialog_button` (cette spec §3.4), on suppose que la **bbox du dialog est déjà + connue** (issue de DialogPresenceChecker upstream, sauvée dans `replay_state`). Sinon + fallback heuristique centrale 60×50%. +- Cascade Easily Assure (popup intermédiaire avant switch_tab) : à traiter dans D2, pas + ici. + +### 9.3. Lien avec watchdog `_retry_pending` (`AXE_B1`) + +Couvert §9.1. Le Validator + le watchdog sont **orthogonaux** : le watchdog corrige la +cause primaire (HTTP timeout silencieux), le Validator corrige la cause aggravante (clic +hors-zone validé success=True). + +--- + +## 10. Cas explicitement non couverts (besoin de décision Dom) + +| Action | Pourquoi flou | Décision attendue | +|---|---|---| +| `paste_and_execute` (bypass NoMachine VM Citrix) | Le ydotool injecte dans la VM, le screenshot serveur ne voit pas forcément le résultat (NoMachine pixel intermédiaire) | OCR-ROI ou check via SSH dans la VM ? | +| `screenshot_evidence` | Action passive sans effet UI — comment valider la "qualité" du screenshot ? | Suffit-il de vérifier que la bonne app est devant (TitleBar) ou exiger un crop net (Laplacian variance > 100) ? | +| `pause_for_human` en mode autonome | Aujourd'hui silencieusement ignorée (`api_stream.py:3011-3017`). Si autonome, le Validator doit-il forcer SUCCESS sans vérification ? Ou pause pour humain quand même via SafetyChecksProvider QW4 ? | Stratégie globale autonome vs supervisé | +| `t2a_decision` retournant `decision="NA"` | Verdict métier valide vs erreur du LLM (cas DPI incomplet) | Le Validator doit-il escalader vers un LlmJudgeChecker spécifique « ce DPI permet-il une T2A » ? Hors-scope médical Claude (mémoire `feedback_anonymisation_stricte.md` + Amina) | +| Tab **déjà actif** au moment du clic | Le screenshot ne change pas (idempotent) — le Validator dit NO_VISUAL_CHANGE alors que c'est OK | Pré-check : si `expected` déjà dans la zone active *avant* le clic → COMPLETE direct. Optimisation P1. | +| `drag_drop_anchor` | Le whitelist serveur l'accepte mais Léa n'a pas de handler — cas test ? | Désactiver l'action côté VWB tant que le handler n'existe pas, ou ajouter le handler Léa | +| Fenêtre cachée derrière une autre (off-screen partial) | OCR voit le texte sur le screenshot mais la fenêtre n'a peut-être pas le focus pour réagir aux events | Ajouter une vérif `GetForegroundWindow().title` au REPORT | +| Animation longue (>1 s) | Le screenshot_after est pris trop tôt, l'effet n'est pas encore observable | Le `wait_after_action_ms` configurable par type d'action (déjà partiellement câblé pour SCROLL_PAUSE_MS=500 ms `replay_engine.py:64`) | +| Multi-écran (`monitor_index`) | Le crop ROI est calculé sur le screenshot du moniteur principal — si l'action est sur moniteur 2, on rate | Couvert par QW1 MonitorRouter, à confirmer compatibilité avec OCR-ROI | + +--- + +## 11. Sources cliquables + +### Frameworks 2026 (Validator pattern) + +- [Skyvern `agent.py` `complete_verify`](https://github.com/Skyvern-AI/skyvern/blob/main/skyvern/forge/agent.py) +- [Skyvern prompt `check-user-goal.j2`](https://raw.githubusercontent.com/Skyvern-AI/skyvern/main/skyvern/forge/prompts/skyvern/check-user-goal.j2) +- [Skyvern prompt `check-user-goal-with-termination.j2`](https://raw.githubusercontent.com/Skyvern-AI/skyvern/main/skyvern/forge/prompts/skyvern/check-user-goal-with-termination.j2) +- [Skyvern 2.0 blog post (WebVoyager 85.85%)](https://www.skyvern.com/blog/skyvern-2-0-state-of-the-art-web-navigation-with-85-8-on-webvoyager-eval/) +- [browser-use evaluation system blog](https://browser-use.com/posts/our-browser-agent-evaluation-system) +- [OpenAdapt architecture wiki](https://github.com/OpenAdaptAI/OpenAdapt/wiki/OpenAdapt-Architecture-\(draft\)) + +### Patterns assertion test (équivalent Validator) + +- [Playwright Python LocatorAssertions](https://playwright.dev/python/docs/api/class-locatorassertions) +- [Playwright assertions guide](https://www.browserstack.com/guide/playwright-assertions) +- [Selenium 4.44 `expected_conditions` (Python)](https://www.selenium.dev/selenium/docs/api/py/selenium_webdriver_support/selenium.webdriver.support.expected_conditions.html) +- [Selenium waits documentation](https://www.selenium.dev/documentation/webdriver/support_features/expected_conditions/) +- [SikuliX Region documentation](https://sikulix-2014.readthedocs.io/en/latest/region.html) +- [SikuliX `exists` / `wait` FAQ](https://answers.launchpad.net/sikuli/+question/693582) +- [SikuliX `waitVanish`](https://answers.launchpad.net/sikuli/+question/176416) + +### Image similarity / pixel diff + +- [scikit-image SSIM](https://scikit-image.org/docs/stable/api/skimage.metrics.html#skimage.metrics.structural_similarity) +- [Pydantic v2 validation](https://docs.pydantic.dev/latest/concepts/json/) +- [Screenshot Comparison Algorithms — Wopee.io](https://wopee.io/blog/screenshot-comparison-algorithms-visual-testing/) + +### Doc interne (lecture seule) + +- Parent : `docs/recherche/AXE_B2_VALIDATOR_PATTERN.md` +- Parent (deep) : `docs/recherche/AXE_B2_DEEP_VALIDATOR.md` +- OCR / template / pHash : `docs/recherche/AXE_A4_OCR_TEMPLATE_PHASH.md` +- Synthèse : `docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` +- Bug archétype : `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` +- Reference action_types : `~/.claude/projects/-home-dom-ai-rpa-vision-v3/memory/reference_vwb_action_types.md` +- Reference templating : `~/.claude/projects/-home-dom-ai-rpa-vision-v3/memory/reference_vwb_templating.md` +- Whitelist serveur : `agent_v0/server_v1/replay_engine.py:35-48` +- Title verifier : `core/grounding/title_verifier.py:25-175` +- OCR-DIRECT : `agent_v0/server_v1/resolve_engine.py:1447-1527` + +--- + +*Spec opérationnelle, lecture seule. Aucune modification de code appliquée. Décision +d'implémentation des nouveaux Checkers relève de Dom au cas par cas, en cohérence avec +le calendrier P0/P1/P2 défini par `AXE_B2_DEEP_VALIDATOR.md` §11.* diff --git a/docs/specs/Q-P1-agentchat-shadow-spec.md b/docs/specs/Q-P1-agentchat-shadow-spec.md new file mode 100644 index 000000000..c5dcd0597 --- /dev/null +++ b/docs/specs/Q-P1-agentchat-shadow-spec.md @@ -0,0 +1,274 @@ +# Spec technique — Raccord agent-chat (5004) → Shadow (5005) + +**Ticket:** Q-P1-agentchat +**Date:** 2026-06-01 + +## 1. État des lieux + +| Composant | Fichier | État | +|---|---|---| +| Endpoints Shadow | `agent_v0/server_v1/api_stream.py` L2415-2650 | **Existent**, fonctionnels | +| ShadowObserver | `core/workflow/shadow_observer.py` | **Existe**, get_shared_observer() | +| ShadowValidator | `core/workflow/shadow_validator.py` | **Existe**, apply_feedback + build_workflow_ir | +| Agent-chat intent | `agent_chat/intent_parser.py` | "apprends-moi" → **IntentType.EXECUTE** (L120) | +| Agent-chat caller | `agent_chat/app.py` | **AUCUN appel Shadow** — endpoints orphelins | +| Persist competence | `api_stream.py` | **N'existe pas** — tâche séparée | + +## 2. Contrats d'endpoints Shadow (port 5005) + +### 2.1 `POST /api/v1/shadow/start` +```json +// Request body +{"session_id": "sess_xxx"} +// Response 200 +{"status": "shadow_started", "session_id": "sess_xxx", "message": "..."} +``` +Appelle `observer.start(session_id)`. Aucune autre dépendance. + +### 2.2 `POST /api/v1/shadow/stop` +```json +// Request body +{"session_id": "sess_xxx"} +// Response 200 +{ + "status": "shadow_stopped", + "session_id": "sess_xxx", + "steps_count": N, + "understanding": [{"step": 1, "intent": "...", "confidence": 0.x, ...}] +} +``` +Appelle `observer.stop(session_id)` puis `observer.get_understanding()`. + +### 2.3 `GET /api/v1/shadow/{session_id}/understanding` +Retourne steps, current_step, notifications (optionnel `?since_ts=...`). + +### 2.4 `POST /api/v1/shadow/feedback` +```json +{ + "session_id": "sess_xxx", + "action": "validate|correct|undo|cancel|merge_next|split", + "step_index": 1, // optionnel sauf pour correct/undo/merge/split + "new_intent": "...", // requis si action=correct + "at_event_index": 5 // requis si action=split +} +``` +Appelle `validator.apply_feedback()`. Retourne `{"status": "feedback_applied|feedback_rejected", "result": {...}}`. + +### 2.5 `POST /api/v1/shadow/build` +```json +{"session_id": "sess_xxx", "name": "Nom workflow", "domain": "generic", "require_all_validated": false} +``` +Retourne `{"status": "workflow_built", "workflow_ir": {...}}` ou `{"status": "cancelled"}`. + +### Auth +Tous les endpoints exigent `Authorization: Bearer ` (sauf si `RPA_AUTH_DISABLED=true`). Agent-chat utilise déjà `_streaming_headers()` pour ses appels replay — réutiliser le même mécanisme. + +## 3. Résolution session_id + +**Problème :** le ShadowObserver est indexé par `session_id`, mais les événements sont streamés par l'agent V1 (sur la machine de Dom) via `POST /api/v1/traces/stream/event`. Le `session_id` du Shadow start/stop doit correspondre au `session_id` utilisé par l'agent V1 pour streamer ses événements. + +**Mécanisme existant :** le StreamProcessor auto-enregistre les sessions à la réception du premier event (`_ensure_session_registered`). Le `session_id` est donc déterminé par l'agent V1 côté client. + +**Solution retenue :** agent-chat ne génère pas de session_id. Il utilise la **session active** de la machine cible. + +### 3.1 Résolution machine_id + +Agent-chat appelle `GET /api/v1/traces/stream/machines` (déjà implémenté dans `_fetch_connected_machines()`, app.py L193). En mode mono-machine (cas Dom seul), une seule machine est connectée → `machine_id` = celle de la liste. + +### 3.2 Résolution session_id (flux) + +``` +1. Dom: "Léa, observe ce que je fais" +2. Agent-chat → GET /api/v1/traces/stream/machines + → machine_id = "DESKTOP-XXX" (seule machine connectée) +3. Agent-chat → GET /api/v1/traces/stream/sessions?machine_id=DESKTOP-XXX + → session_active = sessions non-finalisées [0].session_id + → Si aucune session active, agent-chat dit "Je ne vois pas de session active, + commence par faire une action d'abord" +4. Agent-chat → POST /api/v1/shadow/start {"session_id": session_active} +``` + +**Alternative (plus simple) :** si l'agent V1 stream déjà des événements (session existe), le ShadowObserver peut auto-start via `observe_event` (L355 de shadow_observer.py : "Auto-start si pas encore démarré"). Dans ce cas, agent-chat peut appeler `/shadow/start` avec le session_id résolu pour être explicite, mais ce n'est pas strictement nécessaire — les événements alimenteront l'observer même sans start explicite. + +**Décision :** start explicite requis pour la sémantique ("Léa, observe" = début intentionnel) et pour que `observer.stop()` puisse finaliser le segment en cours proprement. + +## 4. Flux complet spécifié + +``` +┌─────────────────────────────────────────────────────┐ +│ Phase 1 : DÉMARRAGE OBSERVATION │ +│ │ +│ Dom → Agent-chat (5004): "Léa, observe ce que je fais"│ +│ ↓ │ +│ Agent-chat: │ +│ 1. IntentParser.parse() → IntentType.EXECUTE │ +│ workflow_hint = "observe ce que je fais" │ +│ (pattern "apprends-moi" ou "observe" détecté) │ +│ 2. Résoudre session_id: │ +│ GET /api/v1/traces/stream/sessions │ +│ → session_active = première session non-finalisée│ +│ → Si aucune: "Aucune session active détectée" │ +│ 3. POST /api/v1/shadow/start │ +│ {"session_id": session_active} │ +│ headers = _streaming_headers() │ +│ 4. Si erreur 404 → session inactive │ +│ Si erreur 401 → token manquant │ +│ Si connexion error → "Streaming server injoignable"│ +│ 5. Réponse Dom: "J'observe. Fais ta tâche." │ +│ │ +├─────────────────────────────────────────────────────┤ +│ Phase 2 : DOM EFFECTUE L'ACTION │ +│ │ +│ Agent V1 (machine Dom) → Streaming server (5005): │ +│ POST /api/v1/traces/stream/event (en continu) │ +│ → worker.process_event_direct() │ +│ → shadow_observe_event(session_id, event) │ +│ → ShadowObserver.observe_event() (incrémental) │ +│ │ +│ (Aucune action requise d'agent-chat pendant cette │ +│ phase — les événements sont streamés directement) │ +│ │ +├─────────────────────────────────────────────────────┤ +│ Phase 3 : ARRÊT OBSERVATION │ +│ │ +│ Dom → Agent-chat: "Léa, c'est fini" │ +│ ↓ │ +│ Agent-chat: │ +│ 1. IntentParser.parse() → IntentType.EXECUTE │ +│ (détecté par verbe "fini" + contexte shadow) │ +│ OU par contexte conversation (pending shadow) │ +│ 2. POST /api/v1/shadow/stop │ +│ {"session_id": session_active} │ +│ 3. Retour: {understanding: [...], steps_count: N} │ +│ 4. Affiche à Dom les étapes comprises: │ +│ "J'ai observé N étapes:" │ +│ 1. Ouvrir le Bloc-notes (confiance 0.8) │ +│ 2. Saisir du texte (confiance 0.5) │ +│ ... │ +│ 5. Demande validation: "C'est correct ?" │ +│ │ +├─────────────────────────────────────────────────────┤ +│ Phase 4 : FEEDBACK (N tours) │ +│ │ +│ Dom → Agent-chat: "L'étape 2, c'est 'Rechercher X'" │ +│ ↓ │ +│ Agent-chat: │ +│ 1. Parse la correction → action=correct │ +│ step_index=2, new_intent="Rechercher X" │ +│ 2. POST /api/v1/shadow/feedback │ +│ {session_id, action:"correct", │ +│ step_index:2, new_intent:"Rechercher X"} │ +│ 3. Réaffiche les étapes mises à jour │ +│ 4. Redemande: "Et là, c'est bon ?" │ +│ │ +│ Si Dom dit "oui" → action=validate pour chaque étape │ +│ Si Dom dit "supprime l'étape 3" → action=undo │ +│ Si Dom dit "annule tout" → action=cancel │ +│ │ +├─────────────────────────────────────────────────────┤ +│ Phase 5 : BUILD + PERSIST │ +│ │ +│ Après validation complète: │ +│ 1. POST /api/v1/shadow/build │ +│ {session_id, name:"Nom auto ou donné par Dom", │ +│ domain:"generic", require_all_validated:false} │ +│ 2. Retour: {workflow_ir: {...}} │ +│ 3. POST /api/v1/lea/competences/candidate/persist │ +│ ← N'EXISTE PAS ENCORE (tâche séparée) │ +│ À implémenter : reçoit workflow_ir + machine_id │ +│ → crée YAML dans data/competences/candidate/ │ +│ 4. Réponse Dom: "Tâche apprise et sauvegardée" │ +│ 5. observer.reset(session_id) via call Shadow │ +│ (nettoyage état mémoire) │ +└─────────────────────────────────────────────────────┘ +``` + +## 5. Gestion des intentions agent-chat + +### Détection existante (intent_parser.py) + +| Phrase type | Pattern | IntentType actuel | +|---|---|---| +| "apprends-moi à ..." | `r"(?:apprends|apprenez)[- ]moi\s+(.+)"` | EXECUTE | +| "observe ce que je fais" | **NON DÉTECTÉ** — pas de pattern | UNKNOWN → CLARIFY | + +### Modifications requises dans `agent_chat/intent_parser.py` + +Ajouter une nouvelle intention `IntentType.LEARN` (ou `SHADOW`) dans l'enum, avec les patterns : + +```python +IntentType.LEARN = "learn" # Démarrer/arrêter une session d'observation Shadow +``` + +Patterns à ajouter dans `INTENT_PATTERNS[IntentType.LEARN]` : +- `r"(?:observe|regarde)[ -]?(?:moi|ce que je fais)"` +- `r"(?:apprend|apprenez)[- ]moi"` +- `r"(?:montre[- ]moi\s+comment|fais\s+comme\s+moi)"` +- `r"(?:c'est\s+fini|j'ai\s+fini|stop\s+observation)"` (pour le stop, détectable aussi par contexte) + +### Ou approche contextuelle (recommandée) + +Ne pas créer de nouveau IntentType. Utiliser un **flag de contexte** dans le ConversationManager : + +```python +# Quand Dom dit "observe ce que je fais" (EXECUTE, non-match workflow) +if result.get("teach_me") and not matcher: + conversation_manager.set_session_flag(session, "shadow_pending") + +# Au prochain message, si flag shadow_pending: +if conversation_manager.has_session_flag(session, "shadow_pending"): + # Dom dit "oui", "c'est parti" → start shadow + # Dom dit autre chose → interpréter comme contexte shadow +``` + +**Décision recommandée :** approche contextuelle. Elle évite de polluer l'enum et permet de gérer le cycle start/stop comme un sous-état de la conversation, pas comme une intention globale. + +## 6. Gestion des erreurs + +| Erreur | Endpoint | Réponse agent-chat à Dom | +|---|---|---| +| Streaming server injoignable (ConnectionError) | start/stop/feedback/build | "Je n'arrive pas à joindre le serveur de streaming. Vérifie qu'il tourne (port 5005)." | +| 401 Unauthorized | start/stop/feedback/build | "Problème d'authentification — le token API est invalide." | +| 404 Not Found (session inactive) | start/stop | "Je ne trouve pas de session active. Commence par faire une action pour que je puisse observer." | +| 400 Bad Request (build sans étapes validées) | build | "Impossible de construire le workflow — aucune étape n'a été validée." | +| Timeout (>15s) | tout | "Le serveur met trop de temps à répondre. Réessaie." | +| Session déjà active (double start) | start | No-op côté observer (reset implicite) — informer Dom: "J'observe déjà cette session." | +| Stop sans session active | stop | "Je n'étais pas en mode observation." | + +## 7. Paramètres par appel + +| Appel | Méthode | Body | Headers | Timeout | +|---|---|---|---|---| +| Résoudre session | GET | (query params) | `_streaming_headers()` | 3s | +| Shadow start | POST | `{"session_id": "..."}` | `_streaming_headers()` | 5s | +| Shadow stop | POST | `{"session_id": "..."}` | `_streaming_headers()` | 5s | +| Shadow feedback | POST | `{"session_id": "...", "action": "...", ...}` | `_streaming_headers()` | 5s | +| Shadow build | POST | `{"session_id": "...", "name": "...", ...}` | `_streaming_headers()` | 10s | +| Persist | POST | `{"workflow_ir": {...}, "machine_id": "..."}` | `_streaming_headers()` | 10s | + +Tous les appels utilisent `STREAMING_SERVER_URL` (déjà configuré via `RPA_STREAMING_URL`, défaut `http://localhost:5005`). + +## 8. Ce qui n'est PAS dans le scope + +- **Endpoint `/persist`** : n'existe pas. Tâche séparée (~80-120 lignes, cf. handoff). +- **Bouton dashboard** : interdit par contrainte. +- **Canvas/nodes VWB** : interdit par contrainte. +- **Génération de session_id par agent-chat** : le session_id est résolu côté serveur. +- **Multi-machine** : en mono-machine Dom, la résolution est triviale (seule machine connectée). Multi-machine = évolution future. + +## 9. Fichiers à modifier + +| Fichier | Modification | +|---|---| +| `agent_chat/app.py` | Ajouter fonctions `_shadow_start()`, `_shadow_stop()`, `_shadow_feedback()`, `_shadow_build()` + logique de détection dans `api_chat()` | +| `agent_chat/intent_parser.py` | Ajouter patterns de détection "observe"/"c'est fini" (shadow) OU approche contextuelle via ConversationManager | +| `agent_chat/conversation_manager.py` | Ajouter `set_session_flag` / `has_session_flag` pour état `shadow_session` | + +## 10. Implémentation suggérée (ordre) + +1. **Fonctions callers Shadow** dans `app.py` — fonctions pures qui appellent les endpoints (analogues à `_try_streaming_server_replay`) +2. **Détection intention shadow** — patterns + contexte dans `api_chat()` +3. **Cycle start → stop → understanding** — afficher les étapes à Dom +4. **Cycle feedback** — parser les corrections de Dom et appeler `/shadow/feedback` +5. **Build** — appeler `/shadow/build` après validation complète +6. **Persist** — brancher quand l'existe (tâche séparée)