docs: add POC specs, handoffs, and research notes
This commit is contained in:
402
docs/recherche/AXE_A1_VLM_GROUNDING_SOTA.md
Normal file
402
docs/recherche/AXE_A1_VLM_GROUNDING_SOTA.md
Normal file
@@ -0,0 +1,402 @@
|
||||
# Axe A1 — État de l'art VLM Grounding UI (2025-2026)
|
||||
|
||||
**Date :** 2026-05-23
|
||||
**Auteur :** Agent recherche dispatché (Claude Opus 4.7 1M)
|
||||
**Périmètre :** modèles VLM de grounding d'éléments UI graphiques, focus 2025-2026, candidats déployables sur RTX 5070 12 GB VRAM, healthtech (licence permissive).
|
||||
**Source maître interne :** [`SYNTHESE_TECHNOS_REPLAY_2026-05-23.md`](../SYNTHESE_TECHNOS_REPLAY_2026-05-23.md), [`MIGRATION_VLM_PLAN_2026-05-09.md`](../MIGRATION_VLM_PLAN_2026-05-09.md), [`HISTORIQUE_VLM_IMPLEMENTATIONS_2026-05-08.md`](../HISTORIQUE_VLM_IMPLEMENTATIONS_2026-05-08.md)
|
||||
|
||||
> Recherche documentaire — aucun test runtime. Chaque chiffre vient d'un papier, fiche HF ou leaderboard linké. Les scores ScreenSpot-Pro varient parfois de ±3 points entre sources (papier vs leaderboard tiers vs reproduction utilisateur). On affiche systématiquement le chiffre déclaré par les auteurs ou la fiche HF officielle.
|
||||
|
||||
---
|
||||
|
||||
## 1. TL;DR
|
||||
|
||||
1. **Le SOTA open-source 7B sur ScreenSpot-Pro a doublé en 12 mois** : 18.9 % (OS-Atlas-7B, oct 2024) → 61.6 % (UI-TARS-1.5-7B, avr 2025) → 51.9 % (InfiGUI-G1-7B, aoû 2025, AAAI 2026 Oral). Les fermés (GPT-5.2 à 86 %) creusent encore l'écart mais inutilisables on-premise.
|
||||
2. **Notre InfiGUI-G1-3B actuel (45.2 % SSPro / 91.1 % SSv2) reste compétitif** pour 3 GB VRAM 4-bit. Le ratio perf/VRAM est excellent. Migration vers le 7B (51.9 % SSPro) faisable sans changer d'architecture (même `Qwen2_5_VLForConditionalGeneration`).
|
||||
3. **Qwen3-VL-8B-Instruct (oct 2025, Apache 2.0) ne résout PAS le bug d'échelle bbox seul** : même convention post-resize que Qwen2.5-VL. Le fix est dans le **backend** (vLLM/Transformers in-process expose `resized_height/resized_width`), pas dans le modèle.
|
||||
4. **Approche coordinate-free montante** (GUI-Actor, MolmoPoint-GUI, InfiGUI-G1) : la cible n'est plus du texte JSON mais un token de patch visuel ou des grounding-tokens. Élimine structurellement le bug d'échelle. Mais demande un fork Transformers ou un head custom.
|
||||
|
||||
**Recommandation top 3 pour notre cas (12 GB VRAM, healthtech, licence commerciale OK) :**
|
||||
|
||||
| # | Modèle | Pourquoi |
|
||||
|---|---|---|
|
||||
| 1 | **`InfiX-ai/InfiGUI-G1-7B`** ([HF](https://huggingface.co/InfiX-ai/InfiGUI-G1-7B), Apache 2.0) | Continuité totale avec notre stack `core/grounding/`, +5 pts SSPro vs G1-3B, tient en 4-bit NF4 (~6 GB), même format point post-resize que G1-3B donc le bug d'échelle est déjà géré côté `_smart_resize` |
|
||||
| 2 | **`Hcompany/Holo1.5-7B`** ([HF](https://huggingface.co/Hcompany/Holo1.5-7B), Apache 2.0) | Qwen2.5-VL-7B-Instruct base, **57.9 % SSPro / 93.3 % SSv2**, natif 3840×2160 (utile fenêtres 2560×1600 Easily), entraîné GRPO sur UI réelles |
|
||||
| 3 | **`ByteDance-Seed/UI-TARS-1.5-7B`** ([HF](https://huggingface.co/ByteDance-Seed/UI-TARS-1.5-7B), Apache 2.0) | **61.6 % SSPro / 94.2 % SSv2** déclarés (mais reproduction utilisateur à ~40-48 % selon [issue #215](https://github.com/bytedance/UI-TARS/issues/215)) — fallback si InfiGUI déçoit en réel |
|
||||
|
||||
---
|
||||
|
||||
## 2. Table comparative complète
|
||||
|
||||
Légende :
|
||||
- VRAM : approximation pour inférence single-batch, base BF16 sans optim. `(4-bit ≈ /3)` pour quantif NF4 type bitsandbytes.
|
||||
- SS = ScreenSpot v1 (1200+ instructions, multi-OS), SSv2 = re-annoté par OS-Atlas (11% corrections), SSPro = professional high-res (1581 instructions, 23 apps, 5 industries, 3 OS, papier ICLR 2025).
|
||||
- Conv. coord : `0-1000` = normalisé 0-1000 indép. taille image (Qwen2-VL natif). `post-resize` = bbox dans la résolution **après smart_resize** côté modèle (Qwen2.5-VL). `point-token` = grounding via attention sur tokens visuels, pas de texte coord. `abs` = pixel image originale.
|
||||
- "non trouvé" = pas de chiffre publié dans les sources consultées.
|
||||
|
||||
| Modèle | Params | VRAM BF16 | SS | SSv2 | SSPro | Sortie | Conv. coord | vLLM | Transformers | Licence | Release | HF |
|
||||
|---|---:|---:|---:|---:|---:|---|---|:-:|:-:|---|---|---|
|
||||
| **InfiGUI-G1-3B** ⭐ *(actuel)* | 3B | ~6 GB (~3 GB 4-bit) | 90.3 | 91.1 | 45.2 | point JSON | post-resize | ✅ | ✅ | Apache 2.0 | 2025-08-11 | [InfiX-ai/InfiGUI-G1-3B](https://huggingface.co/InfiX-ai/InfiGUI-G1-3B) |
|
||||
| **InfiGUI-G1-7B** | 7B | ~14 GB (~6 GB 4-bit) | non trouvé | 93.5 | 51.9 | point JSON | post-resize | ✅ | ✅ | Apache 2.0 | 2025-08-11 | [InfiX-ai/InfiGUI-G1-7B](https://huggingface.co/InfiX-ai/InfiGUI-G1-7B) |
|
||||
| **InfiGUI-R1-3B** *(prédécesseur)* | 3B | ~6 GB | 87.5 | non trouvé | 35.7 | point JSON | post-resize | ✅ | ✅ | Apache 2.0 | 2025-04-20 | [InfiX-ai/InfiGUI-R1-3B](https://huggingface.co/InfiX-ai/InfiGUI-R1-3B) |
|
||||
| **UI-TARS-1.5-7B** | 7B | ~14 GB | non trouvé | 94.2 | 61.6 *(48 reprod.)* | action DSL `click(x,y)` | abs px | ✅ | ✅ | Apache 2.0 | 2025-04-16 | [ByteDance-Seed/UI-TARS-1.5-7B](https://huggingface.co/ByteDance-Seed/UI-TARS-1.5-7B) |
|
||||
| **Qwen3-VL-8B-Instruct** | 9B | ~18 GB (~6 GB 4-bit) | ~94 (déclaré) | non trouvé | 54.6 *(leaderboard llm-stats)* / 61.8 *(papier)* | bbox_2d ou point JSON | post-resize (multiples 32) | ✅ (vllm≥0.11) | ✅ | Apache 2.0 | 2025-10-15 | [Qwen/Qwen3-VL-8B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-8B-Instruct) |
|
||||
| **Qwen3-VL-4B-Instruct** | 4B | ~8 GB | non trouvé | non trouvé | 59.5 *(leaderboard)* | bbox_2d ou point JSON | post-resize | ✅ | ✅ | Apache 2.0 | 2025-10-15 | [Qwen/Qwen3-VL-4B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct) |
|
||||
| **Qwen2.5-VL-7B-Instruct** *(legacy)* | 7B | ~14 GB | 88.8 | 88.8 | 26.8 | bbox_2d JSON | post-resize (multiples 28) | ✅ | ✅ | Apache 2.0 | 2025-01 | [Qwen/Qwen2.5-VL-7B-Instruct](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct) |
|
||||
| **Holo1.5-7B** ⭐ | 7B | ~14 GB | non trouvé | 93.31 | 57.94 | non documenté (probable point) | non documenté | ✅ | ✅ | Apache 2.0 | 2025-09 | [Hcompany/Holo1.5-7B](https://huggingface.co/Hcompany/Holo1.5-7B) |
|
||||
| **Holo1.5-3B** | 3B | ~6 GB | non trouvé | non trouvé | non trouvé | idem | idem | ✅ | ✅ | Apache 2.0 | 2025-09 | [Hcompany/Holo1.5-3B](https://huggingface.co/Hcompany/Holo1.5-3B) |
|
||||
| **Holo1-7B** *(v1)* | 7B | ~14 GB | non trouvé (avg UI 76.2) | non trouvé | non trouvé | non documenté | non documenté | ✅ | ✅ | Apache 2.0 | 2025-06 | [Hcompany/Holo1-7B](https://huggingface.co/Hcompany/Holo1-7B) |
|
||||
| **OS-Atlas-Base-7B** | 8B | ~16 GB | 82.5 *(papier)* | 85.1 *(InfiGUI eval)* | 18.9 | bbox + point JSON | 0-1000 normalisé | ✅ | ✅ | Apache 2.0 | 2024-10-30 | [OS-Copilot/OS-Atlas-Base-7B](https://huggingface.co/OS-Copilot/OS-Atlas-Base-7B) |
|
||||
| **OS-Atlas-Base-4B** | 4B | ~8 GB | non trouvé | non trouvé | non trouvé | bbox + point JSON | 0-1000 normalisé | ✅ | ✅ | Apache 2.0 | 2024-10-30 | [OS-Copilot/OS-Atlas-Base-4B](https://huggingface.co/OS-Copilot/OS-Atlas-Base-4B) |
|
||||
| **UGround-V1-7B** ⭐ | 7B | ~14 GB | 86.3 | non trouvé | non trouvé (probable ~36 papier) | point `(x,y)` | 0-1000 normalisé | ✅ | ✅ | Apache 2.0 | 2024-10-07 / révisé 2025-01 | [osunlp/UGround-V1-7B](https://huggingface.co/osunlp/UGround-V1-7B) |
|
||||
| **UGround-V1-2B** | 2B | ~4 GB | non trouvé | non trouvé | non trouvé | point `(x,y)` | 0-1000 | ✅ | ✅ | Apache 2.0 | 2025-01 | [osunlp/UGround-V1-2B](https://huggingface.co/osunlp/UGround-V1-2B) |
|
||||
| **UGround-V1-72B** | 72B | ~144 GB | non trouvé | non trouvé | 34.5 *(papier SSPro orig.)* | point `(x,y)` | 0-1000 | ✅ | ✅ | Apache 2.0 | 2025-01 | [osunlp/UGround-V1-72B](https://huggingface.co/osunlp/UGround-V1-72B) |
|
||||
| **Magma-8B** | 9B | ~18 GB | mobile 59.5 / desktop 64.1 / web 60.6 | non trouvé | non trouvé | Set-of-Mark + bbox | non documenté | ⚠️ fork transfo | ✅ (fork) | **MIT** | 2025-02-18 | [microsoft/Magma-8B](https://huggingface.co/microsoft/Magma-8B) |
|
||||
| **GUI-Actor-7B-Qwen2.5-VL** | 8B | ~16 GB | non trouvé | 92.1 | 44.6 | **special token attention head** → `topk_points` normalisés | normalisé 0-1 (sans texte coord) | ⚠️ pas mentionné | ✅ (fork) | **MIT** | 2025-06-03 | [microsoft/GUI-Actor-7B-Qwen2.5-VL](https://huggingface.co/microsoft/GUI-Actor-7B-Qwen2.5-VL) |
|
||||
| **GUI-Actor-7B-Qwen2-VL** | 8B | ~16 GB | non trouvé | 89.5 | 40.7 | idem | idem | ⚠️ | ✅ (fork) | MIT | 2025-06 | [microsoft/GUI-Actor-7B-Qwen2-VL](https://huggingface.co/microsoft/GUI-Actor-7B-Qwen2-VL) |
|
||||
| **MolmoPoint-GUI-8B** ⭐ | 9B | ~18 GB | non trouvé | non trouvé | **61.1** (open SOTA) | grounding-tokens `[id,img,x,y]` | abs px | ❌ (logits processor custom) | ✅ | Apache 2.0 | 2026-03-18 | [allenai/MolmoPoint-GUI-8B](https://huggingface.co/allenai/MolmoPoint-GUI-8B) |
|
||||
| **AGUVIS-7B-720P** | 8B | ~16 GB | 84.4 *(papier)* | non trouvé | 22.9 | bbox + action plan | non documenté (probable post-resize Qwen2-VL) | ✅ | ✅ | non trouvé (probable Apache via base) | 2024-12 | [xlangai/Aguvis-7B-720P](https://huggingface.co/xlangai/Aguvis-7B-720P) |
|
||||
| **ShowUI-2B** | 2B | ~4 GB | 75.1 | non trouvé | 7.7 | point + action dict | normalisé 0-1 | ✅ | ✅ | **MIT** | 2024-11-26 | [showlab/ShowUI-2B](https://huggingface.co/showlab/ShowUI-2B) |
|
||||
| **CogAgent-9B-20241220** | 14B (9B lang + 5B vision) | ~28 GB | leader cité, score précis non publié | non trouvé | non trouvé | `CLICK(box=[x1,y1,x2,y2])` action DSL | non documenté (probable abs sur 1120×1120) | ⚠️ partiel | ✅ | **Other** (custom, non-Apache) | 2024-12-20 | [zai-org/cogagent-9b-20241220](https://huggingface.co/zai-org/cogagent-9b-20241220) |
|
||||
| **SeeClick** *(historique)* | 9.6B | ~19 GB | 53.4 | non trouvé | <10 (papier ScreenSpot-Pro) | bbox via Qwen-VL | non documenté | ❌ | ✅ | Apache 2.0 | 2024-04 (ACL 2024) | [cckevinn/SeeClick](https://huggingface.co/cckevinn/SeeClick) |
|
||||
| **GUI-G2-7B** | 7B | ~14 GB | SOTA déclaré | SOTA déclaré | SOTA déclaré (Gaussian reward GRPO) | non documenté | non documenté | ✅ | ✅ | non trouvé | 2026-01 (AAAI 2026) | [inclusionAI/GUI-G2-7B](https://huggingface.co/inclusionAI/GUI-G2-7B) |
|
||||
| **GPT-5.2** *(fermé)* | n/a | n/a (cloud) | — | — | **86.3** | n/a | n/a | n/a | n/a | OpenAI propriétaire | 2026 | n/a |
|
||||
| **Gemini 3 Pro** *(fermé)* | n/a | n/a (cloud) | — | — | 72.7 | n/a | n/a | n/a | n/a | Google propriétaire | 2026 | n/a |
|
||||
|
||||
**Sources des scores principaux :** [ScreenSpot-Pro leaderboard llm-stats](https://llm-stats.com/benchmarks/screenspot-pro), [papier ScreenSpot-Pro arXiv:2504.07981](https://arxiv.org/abs/2504.07981), [InfiGUI-G1 paper arXiv:2508.05731](https://arxiv.org/abs/2508.05731), fiches HF citées colonne droite.
|
||||
|
||||
---
|
||||
|
||||
## 3. Fiches détaillées par modèle
|
||||
|
||||
### 3.1. InfiGUI-G1-3B / 7B (notre modèle actuel + upgrade direct)
|
||||
|
||||
- **Repo HF :** [InfiX-ai/InfiGUI-G1-3B](https://huggingface.co/InfiX-ai/InfiGUI-G1-3B), [InfiX-ai/InfiGUI-G1-7B](https://huggingface.co/InfiX-ai/InfiGUI-G1-7B)
|
||||
- **Papier :** [arXiv:2508.05731](https://arxiv.org/abs/2508.05731) — *InfiGUI-G1: Advancing GUI Grounding with Adaptive Exploration Policy Optimization*, AAAI 2026 Oral
|
||||
- **GitHub :** [InfiXAI/InfiGUI-G1](https://github.com/InfiXAI/InfiGUI-G1)
|
||||
- **Release :** 2025-08-11
|
||||
- **Licence :** Apache 2.0
|
||||
- **Base :** Qwen2.5-VL-3B (resp. 7B), GRPO via AEPO (Adaptive Exploration Policy Optimization)
|
||||
- **Bench :** ScreenSpot 90.3 (3B) / 92.5+ (7B), SSv2 91.1 / 93.5, **SSPro 45.2 (3B) / 51.9 (7B)**, MMBench-GUI L2 73.4 (3B) / 80.8 (7B)
|
||||
- **Sortie :** point JSON `[{"point_2d": [x, y]}, …]`, coordonnées **post-resize** (le prompt expose `{new_width}x{new_height}` au modèle, mapping à faire client side)
|
||||
- **Code grounding minimal :**
|
||||
```python
|
||||
# Sortie typique
|
||||
# [{"point_2d": [421, 612], "label": "OK button"}]
|
||||
# Mapping coords:
|
||||
original_x = int(coords[0] / new_width * original_width)
|
||||
original_y = int(coords[1] / new_height * original_height)
|
||||
```
|
||||
- **Pourquoi pertinent chez nous :** déjà câblé (`core/grounding/server.py` + `infigui_worker.py` + `infigui_server.py`), `_smart_resize` factor 28 calibré. Passage 3B → 7B = changement de `MODEL_ID` (env `GROUNDING_MODEL`). VRAM 4-bit ≈ 6 GB, tient sur RTX 5070.
|
||||
|
||||
### 3.2. UI-TARS-1.5-7B (ByteDance)
|
||||
|
||||
- **Repo HF :** [ByteDance-Seed/UI-TARS-1.5-7B](https://huggingface.co/ByteDance-Seed/UI-TARS-1.5-7B)
|
||||
- **Papier :** [arXiv:2501.12326](https://arxiv.org/abs/2501.12326), UI-TARS-2 technical report [arXiv:2509.02544](https://arxiv.org/html/2509.02544v1)
|
||||
- **GitHub :** [bytedance/UI-TARS](https://github.com/bytedance/ui-tars), desktop [bytedance/UI-TARS-desktop](https://github.com/bytedance/UI-TARS-desktop)
|
||||
- **Release :** 2025-04-16
|
||||
- **Licence :** Apache 2.0
|
||||
- **Bench déclarés :** SSv2 94.2, **SSPro 61.6** — MAIS [issue #215](https://github.com/bytedance/UI-TARS/issues/215) signale reproduction à ~40-48 % selon prompt
|
||||
- **Sortie :** action DSL natif `click(start_box='[x1,y1,x2,y2]')`, coordonnées **pixels absolues** sur image originale
|
||||
- **Note :** UI-TARS-2 (sept 2025) existe mais pas open-source à date des sources consultées (technical report only). Continuer sur 1.5.
|
||||
- **Risque :** asymétrie déclaré/reproduit. Tester localement avant migration.
|
||||
|
||||
### 3.3. Qwen3-VL-8B-Instruct (cible migration plan 9 mai)
|
||||
|
||||
- **Repo HF :** [Qwen/Qwen3-VL-8B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-8B-Instruct), quantif [cpatonn/Qwen3-VL-8B-Instruct-AWQ-8bit](https://huggingface.co/cpatonn/Qwen3-VL-8B-Instruct-AWQ-8bit), [cyankiwi/Qwen3-VL-8B-Instruct-AWQ-4bit](https://huggingface.co/cyankiwi/Qwen3-VL-8B-Instruct-AWQ-4bit)
|
||||
- **GitHub :** [QwenLM/Qwen3-VL](https://github.com/QwenLM/Qwen3-VL)
|
||||
- **Release :** 2025-10-15
|
||||
- **Licence :** Apache 2.0
|
||||
- **Bench :** [llm-stats SSPro 54.6](https://llm-stats.com/benchmarks/screenspot-pro) (rank 16), mais codersera blog cite ~94 % ScreenSpot et 61.8 % SSPro pour la variante computer-use de Qwen3-VL
|
||||
- **Sortie :** flexible, supporte `bbox_2d` ET `point` selon prompt. Conv. **post-resize multiples de 32** (≠ Qwen2.5-VL qui était multiples de 28 — **DETTE-014 du repo s'aligne désormais sur cette nouvelle factor 32**)
|
||||
- **Resize :** le modèle expose `resized_width` et `resized_height` en paramètres directs (cf. GitHub Qwen3-VL "Directly set resized_height and resized_width. These values will be rounded to the nearest multiple of 32")
|
||||
- **Support vLLM :** `vllm>=0.11.0` requis
|
||||
- **Pourquoi vigilant :** le bench llm-stats positionne Qwen3-VL-8B-Instruct à 54.6 % SSPro, **moins bon que InfiGUI-G1-7B (51.9 %)... attendez non, 54.6 > 51.9**. À 3 pts d'écart, dans la marge d'erreur protocole. Le 4B (59.5 % rank 12) est curieusement meilleur que le 8B (54.6 %), à investiguer.
|
||||
|
||||
### 3.4. Holo1.5-7B (H Company)
|
||||
|
||||
- **Repo HF :** [Hcompany/Holo1.5-7B](https://huggingface.co/Hcompany/Holo1.5-7B), variantes [3B](https://huggingface.co/Hcompany/Holo1.5-3B) et [72B](https://huggingface.co/Hcompany/Holo1.5-72B)
|
||||
- **Blog :** [HF blog Holo1](https://huggingface.co/blog/Hcompany/holo1), [GRPO for GUI Grounding](https://huggingface.co/blog/HelloKKMe/grounding-r1)
|
||||
- **Papier (Holo1) :** [arXiv:2506.02865](https://arxiv.org/pdf/2506.02865) *Surfer-H Meets Holo1*
|
||||
- **Release :** v1 juin 2025, v1.5 septembre 2025
|
||||
- **Licence :** Apache 2.0
|
||||
- **Base :** Qwen2.5-VL-7B-Instruct
|
||||
- **Bench :** **SSv2 93.31 %, SSPro 57.94 %**, WebClick 90.24 % — natif 3840×2160
|
||||
- **Sortie :** non documenté dans la fiche HF directement (probable point format Qwen2.5-VL-like)
|
||||
- **Pourquoi pertinent :** entraîné spécifiquement multi-environnements (web + desktop + mobile) avec GRPO, score SSPro très solide pour Apache 2.0. Probable swap drop-in dans `core/grounding/server.py` (même architecture Qwen2_5_VLForConditionalGeneration).
|
||||
|
||||
### 3.5. UGround-V1 (OSU NLP, ICLR'25 Oral)
|
||||
|
||||
- **Repo HF :** [osunlp/UGround-V1-7B](https://huggingface.co/osunlp/UGround-V1-7B), [2B](https://huggingface.co/osunlp/UGround-V1-2B), [72B](https://huggingface.co/osunlp/UGround-V1-72B)
|
||||
- **Papier :** [arXiv:2410.05243](https://arxiv.org/abs/2410.05243), ICLR 2025 Oral
|
||||
- **GitHub :** [OSU-NLP-Group/UGround](https://github.com/OSU-NLP-Group/UGround)
|
||||
- **Licence :** Apache 2.0
|
||||
- **Base :** Qwen2-VL-7B-Instruct
|
||||
- **Bench :** ScreenSpot 86.3 % moyenne (texte/icône desktop/mobile/web 76-93 %), UGround-V1-72B cité 34.5 % sur SSPro (papier original SSPro)
|
||||
- **Sortie :** point unique `(x, y)` en string, **convention normalisée [0, 1000)** indépendante de l'image (héritage Qwen2-VL)
|
||||
- **Avantage :** convention 0-1000 = pas de bug d'échelle post-resize. Le modèle a appris à raisonner dans un espace canonique.
|
||||
- **Inconvénient :** absence d'évaluation SSPro publique pour le 7B (à part le 72B). Compatible bbox = non (point only).
|
||||
|
||||
### 3.6. OS-Atlas (UCSD / Shanghai AI Lab)
|
||||
|
||||
- **Repo HF :** [OS-Copilot/OS-Atlas-Base-7B](https://huggingface.co/OS-Copilot/OS-Atlas-Base-7B), [Base-4B](https://huggingface.co/OS-Copilot/OS-Atlas-Base-4B), [Pro-7B](https://huggingface.co/OS-Copilot/OS-Atlas-Pro-7B), [Pro-4B](https://huggingface.co/OS-Copilot/OS-Atlas-Pro-4B)
|
||||
- **Papier :** [arXiv:2410.23218](https://arxiv.org/abs/2410.23218), NeurIPS 2024
|
||||
- **GitHub :** [OS-Copilot/OS-Atlas](https://github.com/OS-Copilot/OS-Atlas)
|
||||
- **Release :** 2024-10-30
|
||||
- **Licence :** Apache 2.0
|
||||
- **Base Base-7B :** Qwen2-VL-7B-Instruct, dataset 13M éléments GUI cross-platform
|
||||
- **Bench :** ScreenSpot 82.5 % avg, SSv2 85.1 %, **SSPro 18.9 %** *(plus bas que tous les modèles 2025)*
|
||||
- **Sortie :** bbox + point JSON, **normalisé 0-1000** (Qwen2-VL natif)
|
||||
- **Statut :** point de référence historique. Surclassé par tous les modèles 2025 sur SSPro. À retenir uniquement pour SSv2 / desktop low-res "tabs simples".
|
||||
|
||||
### 3.7. AGUVIS-7B (Salesforce + HKU)
|
||||
|
||||
- **Repo HF :** [xlangai/Aguvis-7B-720P](https://huggingface.co/xlangai/Aguvis-7B-720P)
|
||||
- **Papier :** voir [paper list OSU](https://github.com/OSU-NLP-Group/GUI-Agents-Paper-List/blob/main/paper_by_key/paper_visual_grounding.md)
|
||||
- **Release :** 2024-12
|
||||
- **Licence :** non explicite sur fiche HF (probable Apache 2.0 via base)
|
||||
- **Base :** Qwen2-VL
|
||||
- **Bench :** ScreenSpot 84.4 % papier, **SSPro 22.9 %**
|
||||
- **Sortie :** bbox + action plan (training en 2 étapes : grounding puis action)
|
||||
- **Statut :** intéressant historiquement (pure-vision unified framework Salesforce). Score SSPro faible vs cohorte 2025. Pas prioritaire.
|
||||
|
||||
### 3.8. Magma-8B (Microsoft, CVPR 2025)
|
||||
|
||||
- **Repo HF :** [microsoft/Magma-8B](https://huggingface.co/microsoft/Magma-8B)
|
||||
- **Papier :** [arXiv:2502.13130](https://arxiv.org/abs/2502.13130), CVPR 2025
|
||||
- **GitHub :** [microsoft/Magma](https://github.com/microsoft/Magma)
|
||||
- **Release :** 2025-02-18
|
||||
- **Licence :** **MIT** (très permissive)
|
||||
- **Bench :** ScreenSpot mobile 59.5 / desktop 64.1 / web 60.6 — **pas d'éval SSPro publiée**
|
||||
- **Sortie :** Set-of-Mark (marques numérotées sur image) + Trace-of-Mark (vidéo). Hybride GUI + robotique
|
||||
- **Inconvénient majeur :** nécessite **fork custom de Transformers** (`git+https://github.com/jwyang/transformers.git@dev/jwyang-v4.48.2`), pas de support vLLM standard
|
||||
- **Pertinent si :** intérêt cross-domaine (GUI + robotique). Pour pure GUI, autres modèles font mieux.
|
||||
|
||||
### 3.9. GUI-Actor-7B-Qwen2.5-VL (Microsoft, NeurIPS 2025)
|
||||
|
||||
- **Repo HF :** [microsoft/GUI-Actor-7B-Qwen2.5-VL](https://huggingface.co/microsoft/GUI-Actor-7B-Qwen2.5-VL), variante [Qwen2-VL](https://huggingface.co/microsoft/GUI-Actor-7B-Qwen2-VL)
|
||||
- **Papier :** [arXiv:2506.03143](https://arxiv.org/abs/2506.03143), NeurIPS 2025 — *GUI-Actor: Coordinate-Free Visual Grounding for GUI Agents*
|
||||
- **GitHub :** [microsoft/GUI-Actor](https://github.com/microsoft/GUI-Actor)
|
||||
- **Release :** 2025-06-03
|
||||
- **Licence :** MIT
|
||||
- **Bench :** SSv2 92.1, **SSPro 44.6** (sans verifier), avec verifier monte
|
||||
- **Sortie :** **coordinate-free** — attention-based action head qui pointe directement vers les patches visuels. Output décodé en `topk_points` (coordonnées normalisées 0-1, sans génération texte)
|
||||
- **Avantage théorique majeur :** élimine structurellement le bug d'échelle. Le modèle aligne directement un token spécial avec les patches visuels pertinents.
|
||||
- **Inconvénient :** demande fork custom (`Qwen2_5_VLForConditionalGenerationWithPointer`), pas de support vLLM standard mentionné.
|
||||
- **Intérêt R&D :** valider la direction "coordinate-free" comme architecturale pour la v2 grounding.
|
||||
|
||||
### 3.10. MolmoPoint-GUI-8B (Allen AI, mars 2026)
|
||||
|
||||
- **Repo HF :** [allenai/MolmoPoint-GUI-8B](https://huggingface.co/allenai/MolmoPoint-GUI-8B)
|
||||
- **Blog :** [MolmoPoint blog Ai2](https://allenai.org/blog/molmopoint)
|
||||
- **Papier :** voir blog (référence papier non explicite dans nos sources)
|
||||
- **GitHub :** [allenai/molmo2](https://github.com/allenai/molmo2)
|
||||
- **Release :** mars 2026
|
||||
- **Licence :** Apache 2.0 (recherche/éducation, Responsible Use Guidelines Ai2)
|
||||
- **Base :** Qwen3-8B + MolmoPoint-8B finetuning
|
||||
- **Bench :** **SSPro 61.1 (SOTA open)**, OSWorldG 70.0
|
||||
- **Sortie :** grounding-tokens `[object_id, image_num, x, y]`, **coords pixels absolues** (pas post-resize !)
|
||||
- **Données training :** MolmoPoint-GUISyn = 36k screenshots synthétiques HR (desktop + web + mobile)
|
||||
- **Inconvénient :** **pas de support vLLM** (logits processor custom requis), single-image only, pas de support training prod
|
||||
- **Note pour nous :** **score SSPro le plus élevé parmi les open-source**, et conv. coord absolue = AUCUN bug d'échelle. Mais intégration plus lourde (custom logits processor).
|
||||
|
||||
### 3.11. CogAgent-9B-20241220 (Zhipu / THUDM)
|
||||
|
||||
- **Repo HF :** [zai-org/cogagent-9b-20241220](https://huggingface.co/zai-org/cogagent-9b-20241220)
|
||||
- **Papier :** [arXiv:2312.08914](https://arxiv.org/abs/2312.08914) (v1), v2 dec 2024 sans papier dédié
|
||||
- **GitHub :** [zai-org/CogAgent](https://github.com/zai-org/CogAgent)
|
||||
- **Release :** 2024-12-20 (v2)
|
||||
- **Licence :** **Other** (Custom Zhipu License, non Apache — vérifier compat commerciale healthtech !)
|
||||
- **Base :** GLM-4V-9B (14B total : 9B language + 5B vision)
|
||||
- **Bench :** "leader cité" sur ScreenSpot vs GPT-4o/Claude/SeeClick mais **chiffre SSPro précis non publié dans les sources consultées**
|
||||
- **Sortie :** action DSL `CLICK(box=[[x1,y1,x2,y2]], element_info='...')`, conv. probablement absolue sur 1120×1120
|
||||
- **Risque licence :** "Other" custom, à valider ligne par ligne avant production commerciale.
|
||||
|
||||
### 3.12. ShowUI-2B (Show Lab, CVPR 2025)
|
||||
|
||||
- **Repo HF :** [showlab/ShowUI-2B](https://huggingface.co/showlab/ShowUI-2B)
|
||||
- **Papier :** [arXiv:2411.17465](https://arxiv.org/abs/2411.17465), CVPR 2025
|
||||
- **GitHub :** [showlab/ShowUI](https://github.com/showlab/ShowUI)
|
||||
- **Release :** 2024-11-26
|
||||
- **Licence :** MIT
|
||||
- **Base :** Qwen2-VL-2B-Instruct
|
||||
- **Bench :** ScreenSpot 75.1 % (zéro-shot), **SSPro 7.7 %** *(très faible — modèle 2B léger)*
|
||||
- **Sortie :** point normalisé 0-1 + action dict structuré
|
||||
- **Pertinent si :** contrainte VRAM extrême (4 GB), workflow simple, fenêtres low-res. Pas pour Easily 2560×1600.
|
||||
- **Successeur FocusUI** ([CVPR 2026](https://github.com/showlab/FocusUI)) : framework token pruning sur Qwen2.5-VL / Qwen3-VL multi-sizes, outperforme SOTA précédents.
|
||||
|
||||
### 3.13. SeeClick (référence historique, ACL 2024)
|
||||
|
||||
- **Repo HF :** [cckevinn/SeeClick](https://huggingface.co/cckevinn/SeeClick)
|
||||
- **Papier :** [arXiv:2401.10935](https://arxiv.org/abs/2401.10935), ACL 2024
|
||||
- **GitHub :** [njucckevin/SeeClick](https://github.com/njucckevin/SeeClick)
|
||||
- **Release :** 2024-04
|
||||
- **Licence :** Apache 2.0
|
||||
- **Base :** Qwen-VL ≈9.6B + LoRA finetune
|
||||
- **Bench :** ScreenSpot 53.4 % moyenne (Windows text 55.7, Windows icon/widget 32.5), **SSPro <10 % (papier SSPro orig.)**
|
||||
- **Statut chez nous :** déjà testé, retiré de `intelligent_executor.py` au commit `d1b556b6c` (avril 2026, "cassé"). À NE PAS réutiliser.
|
||||
|
||||
### 3.14. GUI-G2-7B (Zhejiang Univ / inclusionAI, AAAI 2026)
|
||||
|
||||
- **Repo HF :** [inclusionAI/GUI-G2-7B](https://huggingface.co/inclusionAI/GUI-G2-7B)
|
||||
- **GitHub :** [ZJU-REAL/GUI-G2](https://github.com/ZJU-REAL/GUI-G2)
|
||||
- **Papier :** AAAI 2026 *GUI-G²: Gaussian Reward Modeling for GUI Grounding*
|
||||
- **Innovation :** Gaussian reward modeling pour RL — récompense continue scalée selon la taille de l'élément cible (≠ binaire). Pertinent pour icônes petites en haute-res (cas Easily Assure).
|
||||
- **Bench :** SOTA sur ScreenSpot/SSv2/SSPro déclaré (papier InfiGUI cite GUI-G2-7B à 47.5 % SSPro)
|
||||
- **Statut :** récent (jan 2026), à surveiller mais pas encore largement reproduit publiquement.
|
||||
|
||||
### 3.15. UI-Venus (inclusionAI, 2025-2026)
|
||||
|
||||
- **GitHub :** [inclusionAI/UI-Venus](https://github.com/inclusionAI/UI-Venus)
|
||||
- **Statut :** signalé dans recherche comme native UI agent screenshot-only. Pas d'évaluation détaillée trouvée dans nos sources.
|
||||
|
||||
### 3.16. Florence-2 (Microsoft) — hors scope GUI
|
||||
|
||||
- **Note :** modèle 0.27B encodant les coords comme tokens, **non entraîné sur UI** (object/phrase grounding général). Cité pour complétude — **PAS adapté** au cas GUI, à éliminer.
|
||||
|
||||
---
|
||||
|
||||
## 4. Analyse Qwen3-VL vs InfiGUI-G1 vs OS-Atlas vs Magma sur notre cas usage
|
||||
|
||||
Périmètre concret : Windows desktop Easily Assure, fenêtre 2560×1600 souvent croppée par mss à 2560×60 (bug DETTE séparé), 22+ steps mixant tabs, dropdowns, dialogues modaux, boutons de toolbar, champs de saisie.
|
||||
|
||||
| Critère | InfiGUI-G1-7B (upgrade direct) | Qwen3-VL-8B-Instruct (plan migration) | OS-Atlas-Base-7B (référence 2024) | Magma-8B (Microsoft hybrid) |
|
||||
|---|---|---|---|---|
|
||||
| **VRAM 4-bit RTX 5070 12 GB** | ~6 GB ✅ | ~6 GB ✅ | ~6 GB ✅ | ~8 GB ⚠️ (fork transfo) |
|
||||
| **ScreenSpot-Pro (SSPro)** | 51.9 ✅ | 54.6 ✅ | 18.9 ❌ | non publié SSPro |
|
||||
| **Convention coords** | post-resize (factor 28) — `_smart_resize` déjà en place | post-resize (factor 32) — DETTE-014 à recaler | 0-1000 normalisé — **pas de bug d'échelle** | SoM/marks, complexe |
|
||||
| **Bug d'échelle bbox_2d évité ?** | non par construction, mais `_smart_resize` côté serveur OK si bien calibré | non par construction, idem (factor 32 ≠ 28 → recalibration) | **OUI** (0-1000 indépendant) | non documenté |
|
||||
| **Format sortie** | point JSON `point_2d` | bbox_2d OU point JSON | bbox + point JSON | SoM (numéros sur image) |
|
||||
| **vLLM support** | ✅ natif | ✅ (vllm≥0.11) | ✅ | ❌ fork custom |
|
||||
| **Continuité code existant** | **maximale** — même architecture `Qwen2_5_VLForConditionalGeneration`, mêmes prompts, juste `MODEL_ID` à changer | moyenne — Qwen3-VL = nouvelle architecture, factor 32 ≠ 28, prompts à adapter (think:false, num_predict≥128) | bonne (Qwen2-VL base) — mais format coord 0-1000 → tout le parsing à refaire | faible — fork transfo, head SoM, parser custom |
|
||||
| **Healthtech licence commerciale** | ✅ Apache 2.0 | ✅ Apache 2.0 | ✅ Apache 2.0 | ✅ MIT (encore plus permissive) |
|
||||
| **Risque démo (Easily 2560×1600)** | bas | moyen (recalage factor 32 + DETTE-014 + nouveaux prompts) | élevé (SSPro 18.9 = grosses erreurs sur dialogues complexes) | élevé (intégration custom) |
|
||||
| **Effort migration** | ~1 jour | ~3-5 jours | ~2-3 jours (réécrire parser 0-1000) | ~1 semaine + intégration spéciale |
|
||||
|
||||
**Conclusion comparative :** **InfiGUI-G1-7B est l'upgrade le plus rapide et le moins risqué**. Qwen3-VL-8B est techniquement aussi bon mais demande de recalibrer `_smart_resize` (DETTE-014 documente déjà le piège factor 28 vs 32). OS-Atlas perd 30+ pts SSPro vs cohorte 2025 mais offre la convention 0-1000 qui élimine le bug d'échelle. Magma intéressant en R&D, pas en production court terme.
|
||||
|
||||
---
|
||||
|
||||
## 5. Bug d'échelle bbox_2d : quels modèles l'évitent
|
||||
|
||||
Rappel du bug (cf. [`MIGRATION_VLM_PLAN_2026-05-09.md`](../MIGRATION_VLM_PLAN_2026-05-09.md) §1.2) : les coordonnées renvoyées sont dans la résolution **post-`smart_resize`** appliquée par le modèle, mais le code prod divise par `orig_w` au lieu de `resized_w` → toutes les coords shiftées top-left. Ollama n'expose pas `resized_dimensions`, d'où impossibilité de fixer côté client.
|
||||
|
||||
### Modèles SANS bug d'échelle (par construction)
|
||||
|
||||
1. **UGround-V1 (toutes tailles)** — sortie en `[0, 1000)` normalisé, parser officiel `actual_x = (x / 1000) * image_width`. Le modèle a appris à raisonner dans un espace canonique indépendant de la résolution réelle.
|
||||
2. **OS-Atlas-Base-7B / Base-4B** — sortie normalisée 0-1000 (héritage Qwen2-VL). Pas d'aller-retour resize → coord.
|
||||
3. **MolmoPoint-GUI-8B** — sortie en pixels absolus (le grounding-token est décodé en (x, y) image originale). Aucune transformation à faire côté client.
|
||||
4. **GUI-Actor-7B-Qwen2.5-VL** — sortie en `topk_points` normalisés 0-1, sans texte coordonnées (attention head sur patches visuels). **Architecturalement coordinate-free** = élimination radicale du bug.
|
||||
5. **UI-TARS-1.5-7B** — sortie en pixels absolus dans le DSL `click(start_box='[x1,y1,x2,y2]')`. Documenté ainsi, mais le modèle a un smart_resize interne dont la cohérence avec son DSL est à vérifier en réel (issue #215 GitHub suggère reproduction inconstante).
|
||||
|
||||
### Modèles AVEC bug d'échelle latent (à gérer côté client)
|
||||
|
||||
6. **InfiGUI-G1-3B / 7B** — `point_2d` post-resize, mais la fiche HF expose **explicitement** `{new_width}x{new_height}` dans le prompt et fournit le mapping. Si on lit la doc, pas de surprise. Notre `core/grounding/server.py` a déjà `_smart_resize` calibré.
|
||||
7. **Qwen3-VL-8B-Instruct** — `bbox_2d` post-resize (factor **32**, pas 28 !). Avec backend in-process (vLLM ou Transformers), on peut passer `resized_width/resized_height` au modèle. Avec Ollama → impossible (cf. plan migration).
|
||||
8. **Qwen2.5-VL-7B-Instruct** *(legacy)* — racine du bug actuel chez nous via Ollama. À abandonner.
|
||||
9. **AGUVIS-7B-720P** — `720P` dans le nom suggère resize fixe vers 720p, mais convention coord non documentée.
|
||||
|
||||
### Recommandation
|
||||
|
||||
Pour éliminer **définitivement** le bug d'échelle :
|
||||
- **Court terme (continuité code)** : passer en backend Transformers in-process avec exposition explicite de `resized_width/resized_height` (déjà en place dans `core/grounding/server.py` pour InfiGUI). Migrer 3B → 7B.
|
||||
- **Moyen terme (architecture)** : évaluer GUI-Actor ou MolmoPoint-GUI en R&D pour l'approche coordinate-free / absolue.
|
||||
|
||||
---
|
||||
|
||||
## 6. Recommandation actionnable
|
||||
|
||||
Si on devait migrer maintenant pour la démo cliente suivante (post-GHT) :
|
||||
|
||||
### Option A — Continuité chirurgicale (recommandée)
|
||||
|
||||
**Modèle :** `InfiX-ai/InfiGUI-G1-7B`
|
||||
**Backend :** Transformers in-process via `core/grounding/server.py` (déjà en place), changer `MODEL_ID` (env `GROUNDING_MODEL`)
|
||||
**Effort :** ~1 jour
|
||||
**Gain attendu :** +6.7 pts SSPro vs InfiGUI-G1-3B actuel (45.2 → 51.9), même format sortie, `_smart_resize` factor 28 inchangé
|
||||
**Risque :** bas — même architecture, mêmes prompts, juste +3 GB VRAM (~6 GB en 4-bit NF4, tient)
|
||||
|
||||
### Option B — Saut SOTA open (R&D parallèle)
|
||||
|
||||
**Modèle :** `allenai/MolmoPoint-GUI-8B` (SSPro 61.1, open SOTA)
|
||||
**Backend :** Transformers in-process avec logits processor custom (pas vLLM)
|
||||
**Effort :** ~3-5 jours (intégration spéciale, training/eval pipelines)
|
||||
**Gain attendu :** +15 pts SSPro vs actuel, **convention coord absolue → ZÉRO bug d'échelle**
|
||||
**Risque :** moyen — pas de vLLM, single-image, intégration non standard
|
||||
|
||||
### Option C — Aligner sur le plan migration existant (Qwen3-VL)
|
||||
|
||||
**Modèle :** `Qwen/Qwen3-VL-8B-Instruct` (cible documentée dans [`MIGRATION_VLM_PLAN_2026-05-09.md`](../MIGRATION_VLM_PLAN_2026-05-09.md))
|
||||
**Backend :** vLLM ≥0.11 (déjà câblé `resolve_engine.py:785-816`) ou Transformers
|
||||
**Effort :** ~3-5 jours
|
||||
**Gain attendu :** +9.4 pts SSPro (54.6 vs 45.2), `resized_width/resized_height` passable explicitement
|
||||
**Risque :** moyen — factor 32 ≠ 28 (DETTE-014), nouveaux prompts (`think:false`, num_predict≥128), nouvelle architecture Qwen3-VL
|
||||
|
||||
### Choix recommandé : **A maintenant, B en R&D parallèle, C reporté tant que A fonctionne**
|
||||
|
||||
Raisons :
|
||||
- A minimise le risque court terme et capitalise sur l'infra `core/grounding/` déjà investie depuis 2026-04-26.
|
||||
- B teste l'hypothèse "coordinate-free / absolue" qui pourrait être le pattern d'avenir.
|
||||
- C demande de recalibrer le smart_resize sur factor 32 (DETTE-014 explicite), opération à faire UNE fois et qui mérite le timing post-démo.
|
||||
|
||||
**Question ouverte pour Dom :** est-ce que l'écart 45.2 → 51.9 SSPro (option A) suffit pour débloquer les cas Easily où le grounding échoue actuellement ? Si la cause primaire est transport (cf. diagnostic 8 mai, [`REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md`](../REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md)), un modèle SOTA ne corrigera rien.
|
||||
|
||||
---
|
||||
|
||||
## 7. Sources
|
||||
|
||||
### Benchmarks et leaderboards
|
||||
|
||||
- [ScreenSpot-Pro paper arXiv:2504.07981](https://arxiv.org/abs/2504.07981) — *ScreenSpot-Pro: GUI Grounding for Professional High-Resolution Computer Use* (ICLR 2025)
|
||||
- [ScreenSpot-Pro leaderboard llm-stats](https://llm-stats.com/benchmarks/screenspot-pro) — leaderboard tiers (21 modèles, GPT-5.2 leader 86.3 %)
|
||||
- [gui-agent.github.io grounding-leaderboard](https://gui-agent.github.io/grounding-leaderboard/) — infra leaderboard académique
|
||||
- [ScreenSpot-Pro GitHub likaixin2000](https://github.com/likaixin2000/ScreenSpot-Pro-GUI-Grounding) — repo officiel benchmark
|
||||
- [HF blog Ziyang ScreenSpot-Pro](https://huggingface.co/blog/Ziyang/screenspot-pro) — annonce HF du benchmark
|
||||
- [WindowsAgentArena Microsoft](https://microsoft.github.io/WindowsAgentArena/) — environnement Windows benchmark
|
||||
- [Awesome Agents Computer Use leaderboard](https://awesomeagents.ai/leaderboards/computer-use-leaderboard/) — leaderboard tiers
|
||||
- [OSU GUI-Agents Paper List](https://github.com/OSU-NLP-Group/GUI-Agents-Paper-List/blob/main/paper_by_key/paper_visual_grounding.md) — recensement papiers
|
||||
|
||||
### Modèles open-source (Apache / MIT) — repos et papiers
|
||||
|
||||
- **InfiGUI-G1** : [HF 3B](https://huggingface.co/InfiX-ai/InfiGUI-G1-3B), [HF 7B](https://huggingface.co/InfiX-ai/InfiGUI-G1-7B), [paper arXiv:2508.05731](https://arxiv.org/abs/2508.05731), [GitHub InfiXAI/InfiGUI-G1](https://github.com/InfiXAI/InfiGUI-G1)
|
||||
- **InfiGUI-R1** : [paper arXiv:2504.14239](https://arxiv.org/abs/2504.14239), [GitHub InfiXAI/InfiGUI-R1](https://github.com/InfiXAI/InfiGUI-R1)
|
||||
- **UI-TARS-1.5** : [HF ByteDance-Seed/UI-TARS-1.5-7B](https://huggingface.co/ByteDance-Seed/UI-TARS-1.5-7B), [GitHub bytedance/UI-TARS](https://github.com/bytedance/ui-tars), [GitHub UI-TARS-desktop](https://github.com/bytedance/UI-TARS-desktop), [paper arXiv:2501.12326](https://arxiv.org/abs/2501.12326), [UI-TARS-2 tech report arXiv:2509.02544](https://arxiv.org/html/2509.02544v1)
|
||||
- **Qwen3-VL** : [HF Qwen/Qwen3-VL-8B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-8B-Instruct), [GitHub QwenLM/Qwen3-VL](https://github.com/QwenLM/Qwen3-VL), [HF Qwen/Qwen3-VL-4B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct)
|
||||
- **Qwen2.5-VL** : [HF Qwen/Qwen2.5-VL-7B-Instruct](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct), [discussion #13 bbox_2d resize bug](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct/discussions/13)
|
||||
- **Holo1 / Holo1.5** : [HF Hcompany/Holo1.5-7B](https://huggingface.co/Hcompany/Holo1.5-7B), [3B](https://huggingface.co/Hcompany/Holo1.5-3B), [HF blog Holo1](https://huggingface.co/blog/Hcompany/holo1), [paper Surfer-H arXiv:2506.02865](https://arxiv.org/pdf/2506.02865), [HF blog GRPO grounding-r1](https://huggingface.co/blog/HelloKKMe/grounding-r1)
|
||||
- **UGround** : [HF osunlp/UGround-V1-7B](https://huggingface.co/osunlp/UGround-V1-7B), [2B](https://huggingface.co/osunlp/UGround-V1-2B), [72B](https://huggingface.co/osunlp/UGround-V1-72B), [paper arXiv:2410.05243](https://arxiv.org/abs/2410.05243), [GitHub OSU-NLP-Group/UGround](https://github.com/OSU-NLP-Group/UGround)
|
||||
- **OS-Atlas** : [HF OS-Copilot/OS-Atlas-Base-7B](https://huggingface.co/OS-Copilot/OS-Atlas-Base-7B), [Base-4B](https://huggingface.co/OS-Copilot/OS-Atlas-Base-4B), [Pro-7B](https://huggingface.co/OS-Copilot/OS-Atlas-Pro-7B), [paper arXiv:2410.23218](https://arxiv.org/abs/2410.23218), [GitHub OS-Copilot/OS-Atlas](https://github.com/OS-Copilot/OS-Atlas)
|
||||
- **AGUVIS** : [HF xlangai/Aguvis-7B-720P](https://huggingface.co/xlangai/Aguvis-7B-720P)
|
||||
- **Magma** : [HF microsoft/Magma-8B](https://huggingface.co/microsoft/Magma-8B), [paper arXiv:2502.13130](https://arxiv.org/abs/2502.13130), [GitHub microsoft/Magma](https://github.com/microsoft/Magma), [Microsoft blog](https://www.microsoft.com/en-us/research/blog/magma-a-foundation-model-for-multimodal-ai-agents-across-digital-and-physical-worlds/)
|
||||
- **GUI-Actor** : [HF microsoft/GUI-Actor-7B-Qwen2.5-VL](https://huggingface.co/microsoft/GUI-Actor-7B-Qwen2.5-VL), [Qwen2-VL variant](https://huggingface.co/microsoft/GUI-Actor-7B-Qwen2-VL), [paper arXiv:2506.03143](https://arxiv.org/abs/2506.03143), [GitHub microsoft/GUI-Actor](https://github.com/microsoft/GUI-Actor), [project page](https://microsoft.github.io/GUI-Actor/)
|
||||
- **MolmoPoint-GUI** : [HF allenai/MolmoPoint-GUI-8B](https://huggingface.co/allenai/MolmoPoint-GUI-8B), [blog Ai2 MolmoPoint](https://allenai.org/blog/molmopoint), [GitHub allenai/molmo2](https://github.com/allenai/molmo2), [MolmoWeb blog](https://allenai.org/blog/molmoweb)
|
||||
- **CogAgent v2** : [HF zai-org/cogagent-9b-20241220](https://huggingface.co/zai-org/cogagent-9b-20241220), [GitHub zai-org/CogAgent](https://github.com/zai-org/CogAgent), [paper v1 arXiv:2312.08914](https://arxiv.org/abs/2312.08914), [MarkTechPost announcement](https://www.marktechpost.com/2024/12/25/tsinghua-university-researchers-just-open-sourced-cogagent-9b-20241220-the-latest-version-of-cogagent/)
|
||||
- **ShowUI / FocusUI** : [HF showlab/ShowUI-2B](https://huggingface.co/showlab/ShowUI-2B), [paper arXiv:2411.17465](https://arxiv.org/abs/2411.17465), [GitHub showlab/ShowUI](https://github.com/showlab/showui), [GitHub showlab/FocusUI](https://github.com/showlab/FocusUI)
|
||||
- **SeeClick** : [HF cckevinn/SeeClick](https://huggingface.co/cckevinn/SeeClick), [paper arXiv:2401.10935](https://arxiv.org/abs/2401.10935), [GitHub njucckevin/SeeClick](https://github.com/njucckevin/SeeClick)
|
||||
- **GUI-G2** : [HF inclusionAI/GUI-G2-7B](https://huggingface.co/inclusionAI/GUI-G2-7B), [GitHub ZJU-REAL/GUI-G2](https://github.com/ZJU-REAL/GUI-G2)
|
||||
- **OmniParser V2** : [GitHub microsoft/OmniParser](https://github.com/microsoft/omniparser), [Microsoft Research V2 blog](https://www.microsoft.com/en-us/research/articles/omniparser-v2-turning-any-llm-into-a-computer-use-agent/)
|
||||
|
||||
### Annexes
|
||||
|
||||
- [Codersera Qwen3-VL Instruct vs Thinking guide 2026](https://codersera.com/blog/qwen3-vl-8b-instruct-vs-qwen3-vl-8b-thinking-2025-guide/)
|
||||
- [Skywork blog Qwen3-VL GUI Automation 2025](https://skywork.ai/blog/llm/qwen3-vl-gui-automation-2025-visual-agent-revolution/)
|
||||
- [BinaryVerse Qwen3-VL benchmarks](https://binaryverseai.com/qwen3-vl-benchmarks-local-installation-guide-use/)
|
||||
- [The Decoder Qwen3-VL videos](https://the-decoder.com/qwen3-vl-can-scan-two-hour-videos-and-pinpoint-nearly-every-detail/)
|
||||
- [HF discussion #13 Qwen2.5-VL bbox resize bug](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct/discussions/13) — racine documentée du bug que nous vivons
|
||||
- [GitHub QwenLM/Qwen3-VL issue #1831 — image zoom factor 32 vs 28](https://github.com/QwenLM/Qwen3-VL/issues/1831) — directement lié à notre DETTE-014
|
||||
|
||||
---
|
||||
|
||||
## 8. Liens avec autres axes de recherche du projet
|
||||
|
||||
| Axe | Lien |
|
||||
|---|---|
|
||||
| **A2 — smart_resize** | Le choix de modèle conditionne le `factor` à utiliser : Qwen2.5-VL = 28, Qwen3-VL = 32, OS-Atlas/UGround = pas de smart_resize (espace 0-1000). DETTE-014 du repo (`feedback_reread_before_code.md`) doit être recalibrée selon le modèle final retenu. |
|
||||
| **A3 — Bench grounding bbox cible** | Le test à refaire (`MIGRATION_VLM_PLAN_2026-05-09.md` §5) doit inclure les 3 candidats top : InfiGUI-G1-7B, Qwen3-VL-8B-Instruct, Holo1.5-7B, sur la fixture `heartbeat_1773792436.png` 2560×1600. Critère : OK button à cx ≈ 0.45-0.55. |
|
||||
| **B2 — Validator (Planner-Actor-Validator)** | GUI-Actor inclut un grounding verifier pour évaluer les candidats. MolmoPoint-GUI retourne topk points. Pattern à intégrer dans notre `replay_verifier.py` actuellement laxiste (cf. synthèse §5.2). |
|
||||
| **B3 — Coordinate-free architecture** | GUI-Actor (NeurIPS 2025) et MolmoPoint-GUI (Ai2 2026) ouvrent une voie post-coordonnée. À explorer pour v2 grounding, indépendant de l'urgence démo. |
|
||||
| **Démo GHT post-mortem** | Le bug primaire de la démo 8-19 mai était transport HTTP, pas grounding (cf. [`LESSONS_LEARNED_GHT_2026-05.md`](../LESSONS_LEARNED_GHT_2026-05.md)). Migrer le VLM n'a de sens qu'après stabilisation transport (5 bugs P0 toujours ouverts). |
|
||||
|
||||
---
|
||||
|
||||
*Document destiné à informer la décision de migration VLM post-démo GHT. Pas de modification de code. La décision opérationnelle (option A / B / C) doit être validée par Dom.*
|
||||
759
docs/recherche/AXE_A2_SMART_RESIZE_BBOX.md
Normal file
759
docs/recherche/AXE_A2_SMART_RESIZE_BBOX.md
Normal file
@@ -0,0 +1,759 @@
|
||||
# Axe A2 — Doctrine smart_resize et convention bbox_2d (RPA Vision V3)
|
||||
|
||||
**Date :** 2026-05-23
|
||||
**Auteur :** Claude (sous-agent recherche, contexte mission Dom)
|
||||
**Périmètre :** clore techniquement DETTE-014, DETTE-010, DETTE-007, DETTE-006 par une doctrine officielle de preprocessing image et de parsing de coordonnées, par famille de modèle VLM grounding.
|
||||
**Statut :** lecture seule, aucune modification de code. Synthèse de sources officielles HuggingFace, QwenLM, vLLM, Ollama, InfiX-AI, OS-Copilot.
|
||||
|
||||
---
|
||||
|
||||
## 1. TL;DR
|
||||
|
||||
Trois familles de modèles utilisent les chiffres suivants pour preprocesser l'image et exprimer les coordonnées de sortie :
|
||||
|
||||
| Famille | `patch_size` | `spatial_merge_size` | **Factor image** | Convention output |
|
||||
|---|---:|---:|---:|---|
|
||||
| **Qwen2-VL / Qwen2.5-VL** (et tous fine-tunes — InfiGUI-G1-3B, UI-TARS-1.5-7B, OS-Atlas-Base-7B, SeeClick) | **14** | 2 | **28** | bbox/point en **pixel absolu post-smart_resize** (sauf OS-Atlas qui normalise sur 1000) |
|
||||
| **Qwen3-VL** (Instruct / Thinking, dense + MoE) | **16** | 2 | **32** | bbox/point en pixel absolu post-smart_resize, format `[x1,y1,x2,y2]` ou `point_2d` |
|
||||
| **OS-Atlas-Base-4B** (backbone InternVL-2) | tiles 448×448 | dynamic_preprocess | n/a (max 6 tiles) | bbox/point **normalisés [0, 1000]** dans des balises `<box>`/`<point>` |
|
||||
|
||||
Trois constantes officielles, communes à toutes les variantes Qwen-VL via `qwen_vl_utils` :
|
||||
- `IMAGE_MIN_TOKEN_NUM = 4` → `min_pixels = 4 × factor²`
|
||||
- `IMAGE_MAX_TOKEN_NUM = 16384` → `max_pixels = 16384 × factor²`
|
||||
- `MAX_RATIO = 200`
|
||||
|
||||
**Le bug d'échelle bbox_2d documenté (cf. `MIGRATION_VLM_PLAN_2026-05-09.md`) a deux racines distinctes** :
|
||||
|
||||
1. **Ollama opaque** : Ollama applique son propre redimensionnement interne (factor 28 pour qwen2.5-vl, factor 32 pour qwen3-vl) sans permettre au client de passer `resized_width` / `resized_height`, ni de récupérer la taille effective post-resize utilisée par le modèle. Conséquence : impossible de re-normaliser les coordonnées correctement. Ce n'est pas un bug réparable côté client — c'est une **limitation de protocole**. La citation mainteneur de la discussion HF #13 est exacte : « *there is no option (at least i don't found any) to set "resized_width": img_width, "resized_height": img_height* ».
|
||||
|
||||
2. **Code prod RPA Vision V3** : `resolve_engine.py:925` (`small_w, small_h = orig_w, orig_h # pas de redimensionnement`) puis `parse_bbox_to_norm(content, small_w, small_h)` divise les coordonnées du modèle par la taille PRE-resize au lieu de la taille POST-resize. C'est documenté dans `core/grounding/bbox_parser.py:10-13` (DETTE-006 ouverte). Côté Ollama, **même corrigé, ça ne marchera pas tant qu'on n'a pas la taille post-resize d'Ollama** ; côté vLLM/Transformers, ça marche dès qu'on impose nos propres `resized_height/resized_width`.
|
||||
|
||||
**Recommandation d'unification (DETTE-007)** :
|
||||
|
||||
- Conserver `core/grounding/smart_resize.py` comme **module de référence** (formule officielle correcte).
|
||||
- Le corriger pour exposer la famille de modèle : `smart_resize_for_family(model_name, orig_h, orig_w) -> (resized_h, resized_w)` avec dispatch automatique `factor=28` (Qwen2-VL / Qwen2.5-VL / InfiGUI / UI-TARS / OS-Atlas-7B) vs `factor=32` (Qwen3-VL).
|
||||
- Supprimer les deux copies inline (`core/grounding/server.py:15-26` et `core/grounding/infigui_worker.py:99-101` — cette dernière est tronquée et ne clampe pas MIN/MAX_PIXELS, source probable de désalignement sur petites images).
|
||||
- Ajouter `parse_bbox_for_family(model_name, raw_output, resized_w, resized_h, orig_w, orig_h) -> (x_pct, y_pct)` qui encapsule la conversion (pixel post-resize → pourcentage de l'image originale envoyée).
|
||||
- Migrer `resolve_engine.py` (4 sites bbox + `_locate_popup_button`) pour appeler ces deux fonctions au lieu de l'arithmétique inline (corrige DETTE-006).
|
||||
- **Abandonner Ollama pour le grounding bbox** tant qu'il n'expose pas les dimensions post-resize. Cible vLLM ou Transformers in-process avec passage explicite de `resized_width`/`resized_height` (cf. §4).
|
||||
|
||||
---
|
||||
|
||||
## 2. Doctrine par famille de modèle
|
||||
|
||||
### 2.1. Qwen2-VL / Qwen2.5-VL (et tous les fine-tunes : InfiGUI-G1-3B, UI-TARS-1.5-7B, OS-Atlas-Base-7B, SeeClick)
|
||||
|
||||
**Architecture vision** (source : Qwen2.5-VL technical report, arXiv 2502.13923) :
|
||||
- `patch_size = 14`
|
||||
- `spatial_merge_size = 2`
|
||||
- → Factor image = `14 × 2 = 28`
|
||||
|
||||
**Pourquoi 28 et pas 14 ?** Le ViT découpe l'image en patches de 14×14 px (stride 14). Puis le projector multimodal fusionne les patches voisins par groupes de `spatial_merge_size × spatial_merge_size = 2×2`. Donc les dimensions H et W doivent être divisibles par `14 × 2 = 28` pour que la fusion soit propre.
|
||||
|
||||
**Constantes officielles** (verbatim depuis `qwen-vl-utils/src/qwen_vl_utils/vision_process.py`, repo Qwen3-VL `main`) :
|
||||
|
||||
```python
|
||||
MAX_RATIO = 200
|
||||
SPATIAL_MERGE_SIZE = 2
|
||||
IMAGE_MIN_TOKEN_NUM = 4
|
||||
IMAGE_MAX_TOKEN_NUM = 16384
|
||||
```
|
||||
|
||||
Pour Qwen2.5-VL avec `image_patch_size = 14` :
|
||||
- `factor = 14 × 2 = 28`
|
||||
- `min_pixels = 4 × 28² = 3136`
|
||||
- `max_pixels = 16384 × 28² = 12 845 056`
|
||||
|
||||
**Snippet officiel `smart_resize`** (verbatim) :
|
||||
|
||||
```python
|
||||
import math
|
||||
from typing import Optional, Tuple
|
||||
|
||||
MAX_RATIO = 200
|
||||
IMAGE_MIN_TOKEN_NUM = 4
|
||||
IMAGE_MAX_TOKEN_NUM = 16384
|
||||
|
||||
|
||||
def round_by_factor(number: int, factor: int) -> int:
|
||||
"""Returns the closest integer to 'number' that is divisible by 'factor'."""
|
||||
return round(number / factor) * factor
|
||||
|
||||
|
||||
def ceil_by_factor(number: int, factor: int) -> int:
|
||||
"""Returns the smallest integer greater than or equal to 'number' that is divisible by 'factor'."""
|
||||
return math.ceil(number / factor) * factor
|
||||
|
||||
|
||||
def floor_by_factor(number: int, factor: int) -> int:
|
||||
"""Returns the largest integer less than or equal to 'number' that is divisible by 'factor'."""
|
||||
return math.floor(number / factor) * factor
|
||||
|
||||
|
||||
def smart_resize(
|
||||
height: int,
|
||||
width: int,
|
||||
factor: int,
|
||||
min_pixels: Optional[int] = None,
|
||||
max_pixels: Optional[int] = None,
|
||||
) -> Tuple[int, int]:
|
||||
"""Rescales the image so that the following conditions are met:
|
||||
1. Both dimensions (height and width) are divisible by 'factor'.
|
||||
2. The total number of pixels is within the range ['min_pixels', 'max_pixels'].
|
||||
3. The aspect ratio of the image is maintained as closely as possible.
|
||||
"""
|
||||
max_pixels = max_pixels if max_pixels is not None else (IMAGE_MAX_TOKEN_NUM * factor ** 2)
|
||||
min_pixels = min_pixels if min_pixels is not None else (IMAGE_MIN_TOKEN_NUM * factor ** 2)
|
||||
assert max_pixels >= min_pixels, "The max_pixels of image must be greater than or equal to min_pixels."
|
||||
if max(height, width) / min(height, width) > MAX_RATIO:
|
||||
raise ValueError(
|
||||
f"absolute aspect ratio must be smaller than {MAX_RATIO}, got {max(height, width) / min(height, width)}"
|
||||
)
|
||||
h_bar = max(factor, round_by_factor(height, factor))
|
||||
w_bar = max(factor, round_by_factor(width, factor))
|
||||
if h_bar * w_bar > max_pixels:
|
||||
beta = math.sqrt((height * width) / max_pixels)
|
||||
h_bar = floor_by_factor(height / beta, factor)
|
||||
w_bar = floor_by_factor(width / beta, factor)
|
||||
elif h_bar * w_bar < min_pixels:
|
||||
beta = math.sqrt(min_pixels / (height * width))
|
||||
h_bar = ceil_by_factor(height * beta, factor)
|
||||
w_bar = ceil_by_factor(width * beta, factor)
|
||||
return h_bar, w_bar
|
||||
```
|
||||
|
||||
**Convention coord output Qwen2.5-VL** : pixel absolu dans la résolution **post-smart_resize**. Confirmé verbatim par le mainteneur Qwen sur la discussion HF #13 de Qwen2.5-VL-7B-Instruct :
|
||||
|
||||
> "The bbox_2d coordinates are x1, y1, x2, y2 rather than x,y,w,h. And they will be relative to your resized image size if you are resizing."
|
||||
|
||||
Pour reconvertir en coord originale :
|
||||
```python
|
||||
x_orig = x_post_resize * (orig_w / resized_w)
|
||||
y_orig = y_post_resize * (orig_h / resized_h)
|
||||
```
|
||||
|
||||
### 2.2. Qwen3-VL (Instruct / Thinking, dense + MoE)
|
||||
|
||||
**Architecture vision** (source : `transformers/main/model_doc/qwen3_vl`, classe `Qwen3VLVisionConfig`) :
|
||||
- `patch_size = 16` (au lieu de 14)
|
||||
- `spatial_merge_size = 2`
|
||||
- → Factor image = `16 × 2 = 32`
|
||||
|
||||
**Constantes officielles** : identiques à Qwen2.5-VL côté `qwen_vl_utils` (`IMAGE_MIN_TOKEN_NUM=4`, `IMAGE_MAX_TOKEN_NUM=16384`, `MAX_RATIO=200`). Seul le facteur change.
|
||||
|
||||
Pour Qwen3-VL avec `image_patch_size = 16` :
|
||||
- `factor = 16 × 2 = 32`
|
||||
- `min_pixels = 4 × 32² = 4096`
|
||||
- `max_pixels = 16384 × 32² = 16 777 216`
|
||||
|
||||
**API officielle Qwen3-VL** (verbatim README qwen-vl-utils) :
|
||||
|
||||
```python
|
||||
from qwen_vl_utils import process_vision_info
|
||||
|
||||
images, videos, video_kwargs = process_vision_info(
|
||||
messages,
|
||||
image_patch_size=16, # ← Qwen3-VL utilise 16
|
||||
return_video_kwargs=True,
|
||||
return_video_metadata=True,
|
||||
)
|
||||
```
|
||||
|
||||
Le paramètre `image_patch_size` est exposé pour permettre à `process_vision_info` de calculer `factor = image_patch_size * SPATIAL_MERGE_SIZE` dynamiquement. **Si tu utilises Qwen3-VL avec un module `smart_resize` figé sur `factor=28`, l'image envoyée n'est PAS divisible par 32, donc le processor va re-resizer derrière toi à une taille que tu ne connais pas.** C'est exactement la racine documentée de DETTE-014 (`core/grounding/smart_resize.py` est calé sur factor 28).
|
||||
|
||||
**Note importante** : l'issue [QwenLM/Qwen3-VL#1831](https://github.com/QwenLM/Qwen3-VL/issues/1831) signale que **`image_zoom_in_qwen3vl.py` utilise factor=32** alors que le rapport technique Qwen2.5-VL parle de 28. Confusion levée : c'est bien `32` pour Qwen3-VL (`patch_size=16`) et `28` pour Qwen2.5-VL (`patch_size=14`). Le rapport technique 2502.13923 décrit la famille Qwen2.5-VL, pas Qwen3-VL.
|
||||
|
||||
**Convention coord output Qwen3-VL** : pixel absolu post-smart_resize, idem Qwen2.5-VL. Issue [QwenLM/Qwen3-VL#1486](https://github.com/QwenLM/Qwen3-VL/issues/1486) mentionne aussi une variante 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 : <think>...</think>[{"point_2d": [x, y]}, ...]
|
||||
où (x, y) est exprimé en pixel post-smart_resize.
|
||||
```
|
||||
|
||||
Le `point_2d` est le centre de l'élément (pas une bbox). Conversion en pixel original :
|
||||
```python
|
||||
x_orig = x_point * (orig_w / resized_w)
|
||||
y_orig = y_point * (orig_h / resized_h)
|
||||
```
|
||||
|
||||
**MAX_IMAGE_PIXELS officiel InfiGUI** : `5600 * 28 * 28 = 4 390 400` (carte HF). C'est ce qui est utilisé par `core/grounding/server.py:11` et `core/grounding/infigui_worker.py:50` — cohérent avec la doctrine.
|
||||
|
||||
**Bug local DETTE-006/DETTE-007** : `core/grounding/infigui_worker.py:99-101` calcule rH/rW avec un `round_by_factor` simple **sans clamper sur MIN_PIXELS et MAX_PIXELS**. Sur une image en-dessous de 56×56 px (cas heartbeat 2560×60 cité dans `LESSONS_LEARNED_GHT_2026-05.md`), c'est une bombe : le ratio aspect dépasse `MAX_RATIO=200` et toute la logique de smart_resize tombe à plat. À corriger.
|
||||
|
||||
### 2.4. UI-TARS-1.5-7B (ancien modèle grounding, remplacé par InfiGUI)
|
||||
|
||||
**Architecture** : fine-tune de Qwen2.5-VL-7B. Mêmes conventions que Qwen2.5-VL : `patch_size=14`, `factor=28`.
|
||||
|
||||
**Convention output UI-TARS** : format d'action structuré `Thought:/Action: click(start_box='(x1, y1)')`. Coordonnées en **pixel absolu post-resize**, identique à Qwen2.5-VL. Le prompt officiel est récupérable via `git show 9da589c8c:core/grounding/server.py` (commit historique avant remplacement par InfiGUI).
|
||||
|
||||
Sur la doctrine smart_resize : aucune différence opérationnelle avec Qwen2.5-VL. Si on veut le réactiver, c'est interchangeable avec InfiGUI sous la doctrine factor=28.
|
||||
|
||||
### 2.5. OS-Atlas (Base-4B et Base-7B)
|
||||
|
||||
**Architecture** :
|
||||
- OS-Atlas-Base-4B : fine-tune **InternVL-2** (PAS Qwen-VL). Preprocessing différent : `dynamic_preprocess` avec `max_dynamic_patch=6` tiles 448×448 + thumbnail global. **Pas de smart_resize.**
|
||||
- OS-Atlas-Base-7B : fine-tune de **Qwen2-VL-7B-Instruct**. Donc `patch_size=14`, `factor=28`, conventions Qwen-VL.
|
||||
|
||||
**Convention output OS-Atlas** (les deux versions) : coordonnées **normalisées dans [0, 1000]**, format structuré entre balises spéciales :
|
||||
```
|
||||
<|object_ref_start|>language switch<|object_ref_end|><|box_start|>(576,12),(592,42)<|box_end|>
|
||||
```
|
||||
ou pour les points :
|
||||
```
|
||||
<|object_ref_start|>...<|object_ref_end|><|point_start|>(x,y)<|point_end|>
|
||||
```
|
||||
|
||||
Conversion :
|
||||
```python
|
||||
x_orig = (x_normalized / 1000) * orig_w
|
||||
y_orig = (y_normalized / 1000) * orig_h
|
||||
```
|
||||
|
||||
**Pas de bug d'échelle bbox_2d** sur OS-Atlas — la normalisation 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 `</think>` | [0, resized_w] × [0, resized_h] | `"point_2d"\s*:\s*\[(\d+),\s*(\d+)\]` | idem |
|
||||
| UI-TARS-1.5-7B | `click(start_box='(x,y)')` | [0, resized_w] × [0, resized_h] | `start_box='\((\d+),\s*(\d+)\)'` | idem |
|
||||
| OS-Atlas-Base-7B | `<\|box_start\|>(x1,y1),(x2,y2)<\|box_end\|>` | **[0, 1000]** | `<\|box_start\|>\((\d+),(\d+)\),\((\d+),(\d+)\)<\|box_end\|>` | `(x / 1000) * orig_w` |
|
||||
| OS-Atlas-Base-4B | idem 7B | [0, 1000] | idem | idem |
|
||||
|
||||
**Règle de division universelle** : on divise par la taille **dans laquelle le modèle a vu l'image**, et on multiplie par la taille **de l'image originale envoyée**. Pour les modèles pixel-absolu post-resize, c'est `resized_w/h`. Pour les modèles normalisés 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 <think>...</think>).
|
||||
resized_w, resized_h: dimensions effectivement utilisées par le modèle.
|
||||
orig_w, orig_h: dimensions de l'image originale envoyée au client.
|
||||
|
||||
Returns:
|
||||
(x_pct, y_pct) dans [0, 1] OU (None, None) si non parsable.
|
||||
"""
|
||||
family = _detect_family(model_name)
|
||||
range_kind = _OUTPUT_RANGE_BY_FAMILY[family]
|
||||
|
||||
# Nettoyer le thinking et fences markdown
|
||||
body = raw_output.split("</think>")[-1] if "</think>" in raw_output else raw_output
|
||||
body = body.replace("```json", "").replace("```", "").strip()
|
||||
|
||||
cx_px = cy_px = None
|
||||
|
||||
# Cas OS-Atlas : balises <|box_start|>(x1,y1),(x2,y2)<|box_end|>
|
||||
if range_kind == "normalized_1000":
|
||||
m = re.search(
|
||||
r"<\|box_start\|>\((\d+),\s*(\d+)\),\s*\((\d+),\s*(\d+)\)<\|box_end\|>",
|
||||
body,
|
||||
)
|
||||
if m:
|
||||
x1, y1, x2, y2 = (int(g) for g in m.groups())
|
||||
cx_norm = (x1 + x2) / 2 # dans [0, 1000]
|
||||
cy_norm = (y1 + y2) / 2
|
||||
return cx_norm / 1000.0, cy_norm / 1000.0
|
||||
m = re.search(
|
||||
r"<\|point_start\|>\((\d+),\s*(\d+)\)<\|point_end\|>",
|
||||
body,
|
||||
)
|
||||
if m:
|
||||
return int(m.group(1)) / 1000.0, int(m.group(2)) / 1000.0
|
||||
return None, None
|
||||
|
||||
# Cas Qwen-VL famille : pixel post-resize, plusieurs formats
|
||||
# Format 1 : InfiGUI/UI-TARS point_2d
|
||||
m = re.search(r'"point_2d"\s*:\s*\[\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*\]', body)
|
||||
if m:
|
||||
cx_px = float(m.group(1))
|
||||
cy_px = float(m.group(2))
|
||||
|
||||
# Format 2 : Qwen2.5-VL / Qwen3-VL bbox_2d [x1,y1,x2,y2]
|
||||
if cx_px is None:
|
||||
m = re.search(r'"bbox_2d"\s*:\s*\[([^\]]+)\]', body)
|
||||
if m:
|
||||
coords = [float(v.strip()) for v in m.group(1).split(",")]
|
||||
if len(coords) == 2:
|
||||
cx_px, cy_px = coords[0], coords[1]
|
||||
elif len(coords) >= 4:
|
||||
cx_px = (coords[0] + coords[2]) / 2
|
||||
cy_px = (coords[1] + coords[3]) / 2
|
||||
|
||||
# Format 3 : UI-TARS click(start_box='(x, y)')
|
||||
if cx_px is None:
|
||||
m = re.search(r"start_box\s*=\s*['\"]?\(\s*(\d+)\s*,\s*(\d+)\s*\)", body)
|
||||
if m:
|
||||
cx_px = float(m.group(1))
|
||||
cy_px = float(m.group(2))
|
||||
|
||||
# Format 4 : array brut [x, y] ou [x1, y1, x2, y2] (fallback)
|
||||
if cx_px is None:
|
||||
m = re.search(
|
||||
r"\[\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)"
|
||||
r"(?:\s*,\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?))?\s*\]",
|
||||
body,
|
||||
)
|
||||
if m:
|
||||
vals = [float(v) for v in m.groups() if v is not None]
|
||||
if len(vals) >= 4:
|
||||
cx_px = (vals[0] + vals[2]) / 2
|
||||
cy_px = (vals[1] + vals[3]) / 2
|
||||
elif len(vals) == 2:
|
||||
cx_px = vals[0]
|
||||
cy_px = vals[1]
|
||||
|
||||
if cx_px is None or cy_px is None:
|
||||
return None, None
|
||||
|
||||
# IMPORTANT : on divise par resized_w/h (taille POST-resize) et non orig_w/h.
|
||||
# C'est le fix DETTE-006.
|
||||
x_pct = cx_px / resized_w
|
||||
y_pct = cy_px / resized_h
|
||||
|
||||
if not (0.0 <= x_pct <= 1.0 and 0.0 <= y_pct <= 1.0):
|
||||
return None, None
|
||||
|
||||
return x_pct, y_pct
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Recommandation de refactor `resolve_engine.py`
|
||||
|
||||
### 7.1. Sites à modifier
|
||||
|
||||
D'après l'audit déjà réalisé dans le repo (DETTE-006 dans `bbox_parser.py:10-13` et `MIGRATION_VLM_PLAN_2026-05-09.md` §4) :
|
||||
|
||||
| Site | Ligne | Modification |
|
||||
|---|---|---|
|
||||
| `_resolve_by_grounding` initialisation | 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.*
|
||||
895
docs/recherche/AXE_A3_BENCH_PROTOCOL.md
Normal file
895
docs/recherche/AXE_A3_BENCH_PROTOCOL.md
Normal file
@@ -0,0 +1,895 @@
|
||||
# AXE A3 — Protocole de bench grounding VLM (post smart_resize)
|
||||
|
||||
**Date** : 2026-05-23
|
||||
**Auteur** : Claude (session dispatchée AXE A3)
|
||||
**Contexte** : `MIGRATION_VLM_PLAN_2026-05-09.md` §2 a relevé un bug d'échelle `bbox_2d` (cx ≈ 0.17 au lieu de ~0.45-0.55) sur 4 configs Ollama. Le module `core/grounding/smart_resize.py` a été commité (`0d7bcd18a`) mais **jamais reverifié** par un bench end-to-end. DETTE-014 indique qu'il est mal calé (factor 28 vs 32). Ce protocole comble le trou méthodologique.
|
||||
**Statut** : protocole + script prêts. Aucune exécution réelle réalisée par Claude — Dom décide quand lancer.
|
||||
|
||||
---
|
||||
|
||||
## 1. TL;DR
|
||||
|
||||
1. **Une fixture** (`heartbeat_1773792436.png` 2560×1600, dialog OK/Cancel) sur laquelle on connaît la vérité-terrain (bouton OK ≈ mid-screen, `cx ≈ 0.50`).
|
||||
2. **Trois backends** : Ollama (`qwen2.5vl:7b` = baseline buggy, `qwen3-vl:8b` JSON-explicit), vLLM (`Qwen3-VL-8B-Instruct` avec `resized_width/height` natifs), Transformers in-process (`InfiGUI-G1-3B` actuel + 1-2 SOTA optionnels OS-Atlas/Magma).
|
||||
3. **Pour chaque modèle** : déchargement VRAM → 1 cold → 10 warm → mesure latence, VRAM pic, format brut, parse OK, `cx_pct` mesuré.
|
||||
4. **Critère go/no-go** : `cx_pct ∈ [0.40, 0.60]` ET `cy_pct ∈ [0.40, 0.60]` (le bouton OK est au centre du dialog, le dialog est centré écran) ET parse regex prod OK ET latence cold < 12 s.
|
||||
5. **Livrable** : un CSV `/tmp/bench_grounding_2026-05-23.csv` + overlay PNG par modèle pour validation visuelle.
|
||||
|
||||
**Go / no-go pour la migration AXE_A2** : si **aucun** modèle ne passe `cx_pct ∈ [0.40, 0.60]`, le bug n'est PAS uniquement `smart_resize` mais aussi côté preprocessing/prompt → escalader avant de remplacer la prod.
|
||||
|
||||
---
|
||||
|
||||
## 2. Protocole détaillé
|
||||
|
||||
### 2.1. Preprocessing image (par backend)
|
||||
|
||||
| Backend | Resize | resized_w/h passé au modèle | Coords retournées en |
|
||||
|---|---|---|---|
|
||||
| **Ollama** (qwen2.5vl, qwen3-vl) | Implicite côté serveur, opaque | NON (non supporté) | Pixels post-resize OLLAMA (inconnu) ⚠ |
|
||||
| **vLLM** (qwen3-vl-8b) | Côté client via `smart_resize` officiel | OUI (via `min_pixels`/`max_pixels` extra body) | Pixels post-resize CLIENT (connu) |
|
||||
| **Transformers** (InfiGUI, OS-Atlas, Magma) | Côté script via `core/grounding/smart_resize.py` | OUI (passé au `processor`) | Pixels post-resize CLIENT (connu) |
|
||||
|
||||
**Côté script** : on calcule `(rH, rW) = smart_resize(H, W)` sur l'image originale en utilisant `core.grounding.smart_resize` (factor 28 par défaut, à challenger après lecture du `probe_qwen3vl_processor.py` qui dump le factor effectif via `patch_size × merge_size`). On envoie l'image **redimensionnée** au backend (sauf Ollama qui re-resize de toute façon).
|
||||
|
||||
### 2.2. Prompt (par famille de modèle)
|
||||
|
||||
- **Qwen2.5-VL** (baseline) : `Detect 'OK button' in this image with a bounding box.` (prompt actuel `resolve_engine.py:942`)
|
||||
- **Qwen3-VL Instruct** : prompt JSON explicite obligatoire — `Locate the "OK" button. Return ONLY this JSON: {"bbox_2d":[x1,y1,x2,y2],"label":"OK"}.` (Sans cette directive, sortie liste nue cf. MIGRATION_VLM_PLAN §2)
|
||||
- **InfiGUI-G1-3B** : prompt du worker existant (`infigui_worker.py:130-135`) — `The screen's resolution is {rW}x{rH}. Locate the UI element(s) for "OK button", output the coordinates using JSON format: [{"point_2d": [x, y]}, ...]` + system avec `<think>`
|
||||
- **OS-Atlas-Base-7B** (Qwen2-VL-7B-FT) : `In the image, please find the bbox of "OK button". Output format: [[x1,y1,x2,y2]] with each value in [0,1000].`
|
||||
- **Magma-8B** : pas dans le périmètre v0 (Set-of-Mark requiert SomEngine, hors A3) — documenter comme extension.
|
||||
|
||||
### 2.3. Parsing sortie
|
||||
|
||||
Réutiliser `core.grounding.bbox_parser.parse_bbox_to_norm(content, divisor_w, divisor_h)`. **CLEF du bench** : appeler avec `divisor_w = rW` et `divisor_h = rH` (post-resize), pas avec `orig_w`/`orig_h`. C'est le fix qu'AXE_A2 documente comme nécessaire.
|
||||
|
||||
Pour OS-Atlas (sortie 0-1000), parser à part puis convertir : `cx_pct = (x1 + x2) / 2 / 1000`.
|
||||
|
||||
### 2.4. Métriques mesurées (par modèle, en sortie CSV)
|
||||
|
||||
| Colonne | Méthode |
|
||||
|---|---|
|
||||
| `model` | nom + backend |
|
||||
| `cold_s` | 1er appel après `unload_all` (Ollama) ou après reload process (Transformers) |
|
||||
| `warm_avg_s`, `warm_p50_s`, `warm_p95_s` | sur 10 runs warm (même image, même prompt) |
|
||||
| `vram_pic_mib` | sample `nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits` toutes 200 ms pendant l'appel, max retenu |
|
||||
| `raw_output` | premier 250 char de la réponse brute |
|
||||
| `format_detected` | `bbox_2d` / `point_2d` / `xy_json` / `raw_array` / `unknown` |
|
||||
| `parse_ok` | bool, parser regex prod renvoie un (x, y) |
|
||||
| `cx_pct`, `cy_pct` | coords normalisées calculées avec `divisor = (rW, rH)` |
|
||||
| `validated` | bool, `cx_pct ∈ [0.40, 0.60]` ET `cy_pct ∈ [0.40, 0.60]` |
|
||||
| `error` | string si exception/timeout |
|
||||
|
||||
### 2.5. Overlay PNG pour validation visuelle
|
||||
|
||||
Pour chaque modèle qui retourne un (cx_pct, cy_pct), générer `bench_grounding_<model>.png` = fixture + croix rouge au point retourné + texte `<model> cx=0.XX cy=0.YY`. Dom regarde les overlays côte-à-côte.
|
||||
|
||||
---
|
||||
|
||||
## 3. Script Python autonome
|
||||
|
||||
À créer en `/home/dom/ai/rpa_vision_v3/tools/bench_grounding_2026-05-23.py`. Code prêt à coller :
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""Bench grounding VLM — AXE A3 post smart_resize (2026-05-23).
|
||||
|
||||
Objectif : refaire le bench bbox_2d du 8 mai 2026 après commit du module
|
||||
`core/grounding/smart_resize.py` (0d7bcd18a) sur la fixture interne du projet.
|
||||
Mesure latence cold/warm, VRAM pic, format brut, parse OK, cx_pct/cy_pct
|
||||
mesuré contre vérité-terrain visuelle (bouton OK ≈ centre écran).
|
||||
|
||||
Usage :
|
||||
.venv/bin/python tools/bench_grounding_2026-05-23.py
|
||||
[--models qwen25vl_ollama,qwen3vl_ollama,qwen3vl_vllm,infigui_tx]
|
||||
[--warm 10]
|
||||
[--out /tmp/bench_grounding_2026-05-23.csv]
|
||||
[--overlay-dir /tmp/bench_grounding_overlays]
|
||||
|
||||
Pré-requis runtime (à confirmer avant lancement, cf. §4) :
|
||||
- Ollama tourne sur :11434 avec qwen2.5vl:7b et qwen3-vl:8b pull
|
||||
- vLLM tourne sur :8100 avec Qwen3-VL-8B-Instruct (cf. §4)
|
||||
- Transformers : .venv contient transformers + bitsandbytes
|
||||
- nvidia-smi accessible (mesure VRAM)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
import requests
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
# Ajout du repo au path pour réutiliser core.grounding.*
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
from core.grounding.smart_resize import smart_resize # noqa: E402
|
||||
from core.grounding.bbox_parser import parse_bbox_to_norm # noqa: E402
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
|
||||
FIXTURE_PATH = REPO_ROOT / "data/training/live_sessions/bg_DESKTOP-58D5CAC_windows/shots/heartbeat_1773792436.png"
|
||||
TARGET_LABEL = "OK button"
|
||||
# Vérité-terrain manuelle (cf. §7) : bouton OK approximativement au centre
|
||||
GT_CX_RANGE = (0.40, 0.60)
|
||||
GT_CY_RANGE = (0.40, 0.60)
|
||||
|
||||
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
|
||||
VLLM_URL = os.environ.get("VLLM_URL", "http://localhost:8100")
|
||||
|
||||
# Une entrée = un modèle à bencher. Le champ "runner" identifie l'appel à faire.
|
||||
MODEL_CATALOG: dict[str, dict[str, Any]] = {
|
||||
"qwen25vl_ollama": {
|
||||
"label": "qwen2.5vl:7b (Ollama, baseline buggy)",
|
||||
"runner": "ollama",
|
||||
"ollama_model": "qwen2.5vl:7b",
|
||||
"prompt_style": "qwen25_bbox",
|
||||
"num_predict": 100,
|
||||
"think": None, # n/a
|
||||
},
|
||||
"qwen3vl_ollama": {
|
||||
"label": "qwen3-vl:8b (Ollama, prompt JSON explicite)",
|
||||
"runner": "ollama",
|
||||
"ollama_model": "qwen3-vl:8b",
|
||||
"prompt_style": "qwen3_bbox_explicit",
|
||||
"num_predict": 128,
|
||||
"think": False,
|
||||
},
|
||||
"qwen3vl_vllm": {
|
||||
"label": "Qwen3-VL-8B-Instruct (vLLM, resized_w/h natif)",
|
||||
"runner": "vllm",
|
||||
"vllm_model": "Qwen/Qwen3-VL-8B-Instruct",
|
||||
"prompt_style": "qwen3_bbox_explicit",
|
||||
"max_tokens": 128,
|
||||
},
|
||||
"infigui_tx": {
|
||||
"label": "InfiGUI-G1-3B (Transformers, prod)",
|
||||
"runner": "transformers_infigui",
|
||||
"model_id": "InfiX-ai/InfiGUI-G1-3B",
|
||||
"prompt_style": "infigui_point",
|
||||
},
|
||||
# Extensions facultatives (AXE A1) — à activer via --models
|
||||
"os_atlas_tx": {
|
||||
"label": "OS-Atlas-Base-7B (Transformers, SOTA grounding)",
|
||||
"runner": "transformers_qwen2vl",
|
||||
"model_id": "OS-Copilot/OS-Atlas-Base-7B",
|
||||
"prompt_style": "os_atlas_bbox_1000",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Prompts par style
|
||||
# ============================================================================
|
||||
|
||||
def build_prompt(style: str, rW: int, rH: int, label: str) -> tuple[str, str]:
|
||||
"""Retourne (system, user). System "" si pas utile."""
|
||||
if style == "qwen25_bbox":
|
||||
return (
|
||||
"You locate UI elements on screenshots. Return coordinates.",
|
||||
f"Detect '{label}' in this image with a bounding box.",
|
||||
)
|
||||
if style == "qwen3_bbox_explicit":
|
||||
return (
|
||||
"You are a UI element locator. Output raw JSON only. No explanation.",
|
||||
f'Locate the "{label}" in this {rW}x{rH} screenshot. '
|
||||
f'Return ONLY this JSON object: '
|
||||
f'{{"bbox_2d":[x1,y1,x2,y2],"label":"{label}"}}',
|
||||
)
|
||||
if style == "infigui_point":
|
||||
return (
|
||||
"You FIRST think about the reasoning process as an internal monologue "
|
||||
"and then provide the final answer.\n"
|
||||
"The reasoning process MUST BE enclosed within <think> </think> tags.",
|
||||
f'The screen\'s resolution is {rW}x{rH}.\n'
|
||||
f'Locate the UI element(s) for "{label}", '
|
||||
f'output the coordinates using JSON format: '
|
||||
f'[{{"point_2d": [x, y]}}, ...]',
|
||||
)
|
||||
if style == "os_atlas_bbox_1000":
|
||||
return (
|
||||
"",
|
||||
f'In the image, please find the bbox of "{label}". '
|
||||
f'Output the bounding boxes in this format: [[x1,y1,x2,y2]], '
|
||||
f'where each value is normalized in [0, 1000].',
|
||||
)
|
||||
raise ValueError(f"prompt_style inconnu : {style}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# VRAM monitoring (nvidia-smi sampling)
|
||||
# ============================================================================
|
||||
|
||||
class VRAMSampler:
|
||||
"""Sample nvidia-smi en thread, expose pic en MiB."""
|
||||
|
||||
def __init__(self, interval_s: float = 0.2):
|
||||
self.interval = interval_s
|
||||
self.peak_mib = 0
|
||||
self._stop = threading.Event()
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
|
||||
def _loop(self) -> None:
|
||||
while not self._stop.is_set():
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
["nvidia-smi", "--query-gpu=memory.used",
|
||||
"--format=csv,noheader,nounits", "-i", "0"],
|
||||
timeout=2,
|
||||
).decode().strip()
|
||||
mib = int(out.splitlines()[0])
|
||||
self.peak_mib = max(self.peak_mib, mib)
|
||||
except Exception:
|
||||
pass
|
||||
self._stop.wait(self.interval)
|
||||
|
||||
def start(self) -> None:
|
||||
self.peak_mib = 0
|
||||
self._stop.clear()
|
||||
self._thread = threading.Thread(target=self._loop, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def stop(self) -> int:
|
||||
self._stop.set()
|
||||
if self._thread:
|
||||
self._thread.join(timeout=2)
|
||||
return self.peak_mib
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Runners par backend
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class CallResult:
|
||||
elapsed_s: float
|
||||
raw: str = ""
|
||||
error: str = ""
|
||||
vram_pic_mib: int = 0
|
||||
|
||||
|
||||
def _encode_image(img: Image.Image) -> str:
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="JPEG", quality=85)
|
||||
return base64.b64encode(buf.getvalue()).decode("ascii")
|
||||
|
||||
|
||||
def _ollama_unload_all() -> None:
|
||||
try:
|
||||
ps = requests.get(f"{OLLAMA_URL}/api/ps", timeout=5).json()
|
||||
for m in ps.get("models", []):
|
||||
requests.post(
|
||||
f"{OLLAMA_URL}/api/generate",
|
||||
json={"model": m["name"], "prompt": "", "keep_alive": 0, "stream": False},
|
||||
timeout=10,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(2)
|
||||
|
||||
|
||||
def run_ollama(cfg: dict, img: Image.Image, rW: int, rH: int) -> CallResult:
|
||||
system, user = build_prompt(cfg["prompt_style"], rW, rH, TARGET_LABEL)
|
||||
img_resized = img.resize((rW, rH))
|
||||
img_b64 = _encode_image(img_resized)
|
||||
messages = []
|
||||
if system:
|
||||
messages.append({"role": "system", "content": system})
|
||||
messages.append({"role": "user", "content": user, "images": [img_b64]})
|
||||
options: dict[str, Any] = {
|
||||
"temperature": 0.1,
|
||||
"num_predict": cfg.get("num_predict", 128),
|
||||
}
|
||||
payload = {
|
||||
"model": cfg["ollama_model"],
|
||||
"messages": messages,
|
||||
"stream": False,
|
||||
"options": options,
|
||||
}
|
||||
if cfg.get("think") is False:
|
||||
payload["think"] = False
|
||||
sampler = VRAMSampler()
|
||||
sampler.start()
|
||||
t0 = time.perf_counter()
|
||||
try:
|
||||
resp = requests.post(f"{OLLAMA_URL}/api/chat", json=payload, timeout=120)
|
||||
elapsed = time.perf_counter() - t0
|
||||
vram = sampler.stop()
|
||||
if resp.status_code != 200:
|
||||
return CallResult(elapsed, error=f"HTTP_{resp.status_code}", vram_pic_mib=vram)
|
||||
content = resp.json().get("message", {}).get("content", "")
|
||||
return CallResult(elapsed, raw=content, vram_pic_mib=vram)
|
||||
except Exception as e:
|
||||
sampler.stop()
|
||||
return CallResult(time.perf_counter() - t0, error=f"NET:{type(e).__name__}")
|
||||
|
||||
|
||||
def run_vllm(cfg: dict, img: Image.Image, rW: int, rH: int) -> CallResult:
|
||||
system, user = build_prompt(cfg["prompt_style"], rW, rH, TARGET_LABEL)
|
||||
img_resized = img.resize((rW, rH))
|
||||
img_b64 = _encode_image(img_resized)
|
||||
messages = []
|
||||
if system:
|
||||
messages.append({"role": "system", "content": system})
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": user},
|
||||
{"type": "image_url",
|
||||
"image_url": {"url": f"data:image/jpeg;base64,{img_b64}"}},
|
||||
],
|
||||
})
|
||||
payload = {
|
||||
"model": cfg["vllm_model"],
|
||||
"messages": messages,
|
||||
"temperature": 0.1,
|
||||
"max_tokens": cfg.get("max_tokens", 128),
|
||||
# vLLM Qwen-VL extension : passer min/max pixels en kwargs
|
||||
# (cf. github QwenLM/Qwen3-VL issue #1434 — peut être ignoré selon version vllm)
|
||||
"mm_processor_kwargs": {"min_pixels": rW * rH, "max_pixels": rW * rH},
|
||||
}
|
||||
sampler = VRAMSampler()
|
||||
sampler.start()
|
||||
t0 = time.perf_counter()
|
||||
try:
|
||||
resp = requests.post(
|
||||
f"{VLLM_URL}/v1/chat/completions", json=payload, timeout=120,
|
||||
)
|
||||
elapsed = time.perf_counter() - t0
|
||||
vram = sampler.stop()
|
||||
if resp.status_code != 200:
|
||||
return CallResult(elapsed, error=f"HTTP_{resp.status_code}",
|
||||
raw=resp.text[:250], vram_pic_mib=vram)
|
||||
content = (resp.json().get("choices", [{}])[0]
|
||||
.get("message", {}).get("content", ""))
|
||||
return CallResult(elapsed, raw=content, vram_pic_mib=vram)
|
||||
except Exception as e:
|
||||
sampler.stop()
|
||||
return CallResult(time.perf_counter() - t0, error=f"NET:{type(e).__name__}")
|
||||
|
||||
|
||||
# Pour les runners Transformers, on délègue au worker existant (subprocess)
|
||||
# pour ne pas surcharger ce script en deps lourdes. Variante one-shot.
|
||||
def run_transformers_infigui(cfg: dict, img: Image.Image, rW: int, rH: int) -> CallResult:
|
||||
"""Appel one-shot via core/grounding/infigui_worker.py.
|
||||
|
||||
Note : InfiGUI utilise un système Unix-socket persistant en prod
|
||||
(`core/grounding/infigui_server.py`). Pour le bench cold, on lance
|
||||
le worker en subprocess one-shot (charge le modèle à chaque cold).
|
||||
Pour les warms, on bench via le socket si dispo, sinon en subprocess
|
||||
(et la mesure cold/warm sera identique — à documenter dans le CSV).
|
||||
"""
|
||||
req = {
|
||||
"image_path": str(FIXTURE_PATH),
|
||||
"target": TARGET_LABEL,
|
||||
"description": "",
|
||||
}
|
||||
sampler = VRAMSampler()
|
||||
sampler.start()
|
||||
t0 = time.perf_counter()
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[sys.executable, "-m", "core.grounding.infigui_worker"],
|
||||
input=json.dumps(req),
|
||||
capture_output=True, text=True,
|
||||
timeout=180, cwd=str(REPO_ROOT),
|
||||
)
|
||||
elapsed = time.perf_counter() - t0
|
||||
vram = sampler.stop()
|
||||
if proc.returncode != 0:
|
||||
return CallResult(elapsed, error=f"PROC_{proc.returncode}",
|
||||
raw=(proc.stderr or "")[:250], vram_pic_mib=vram)
|
||||
# Le worker écrit sur stdout du JSON final
|
||||
out = proc.stdout.strip().splitlines()[-1] if proc.stdout.strip() else "{}"
|
||||
return CallResult(elapsed, raw=out, vram_pic_mib=vram)
|
||||
except Exception as e:
|
||||
sampler.stop()
|
||||
return CallResult(time.perf_counter() - t0, error=f"EXC:{type(e).__name__}")
|
||||
|
||||
|
||||
def run_transformers_qwen2vl(cfg: dict, img: Image.Image, rW: int, rH: int) -> CallResult:
|
||||
"""Stub pour OS-Atlas / autres Qwen2-VL fine-tunés.
|
||||
|
||||
À étoffer si Dom décide d'inclure OS-Atlas dans la 1ère salve. Procédure :
|
||||
charger le modèle via Qwen2_5_VLForConditionalGeneration + AutoProcessor,
|
||||
smart_resize côté script, prompt `os_atlas_bbox_1000`, parser 0-1000.
|
||||
|
||||
Implémentation effective hors périmètre v0 — retourne un CallResult vide.
|
||||
"""
|
||||
return CallResult(0.0, error="NOT_IMPLEMENTED_V0")
|
||||
|
||||
|
||||
RUNNERS = {
|
||||
"ollama": run_ollama,
|
||||
"vllm": run_vllm,
|
||||
"transformers_infigui": run_transformers_infigui,
|
||||
"transformers_qwen2vl": run_transformers_qwen2vl,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Parsing & validation
|
||||
# ============================================================================
|
||||
|
||||
def detect_format(content: str) -> str:
|
||||
if '"bbox_2d"' in content:
|
||||
return "bbox_2d"
|
||||
if '"point_2d"' in content:
|
||||
return "point_2d"
|
||||
if '"x_pct"' in content and '"y_pct"' in content:
|
||||
return "xy_pct"
|
||||
if re.search(r'"x"\s*:\s*[\d.]+.*?"y"\s*:\s*[\d.]+', content, re.S):
|
||||
return "xy_json"
|
||||
if re.search(r'\[\s*\[\s*\d+\s*,', content):
|
||||
return "raw_2d_array" # OS-Atlas 0-1000 style
|
||||
if re.search(r'\[\s*[\d.]+\s*,', content):
|
||||
return "raw_array"
|
||||
return "unknown"
|
||||
|
||||
|
||||
def parse_coords(content: str, rW: int, rH: int, prompt_style: str
|
||||
) -> tuple[Optional[float], Optional[float]]:
|
||||
"""Retourne (cx_pct, cy_pct) normalisés ∈ [0, 1]."""
|
||||
# OS-Atlas-style : 0-1000
|
||||
if prompt_style == "os_atlas_bbox_1000":
|
||||
m = re.search(r'\[\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\]', content)
|
||||
if m:
|
||||
x1, y1, x2, y2 = [int(v) for v in m.groups()]
|
||||
return (x1 + x2) / 2 / 1000.0, (y1 + y2) / 2 / 1000.0
|
||||
return None, None
|
||||
|
||||
# InfiGUI point_2d (parser dédié, sortie en pixels post-resize rW/rH)
|
||||
if prompt_style == "infigui_point":
|
||||
m = re.search(r'"point_2d"\s*:\s*\[(\d+)\s*,\s*(\d+)\]', content)
|
||||
if m:
|
||||
x, y = int(m.group(1)), int(m.group(2))
|
||||
return x / rW, y / rH
|
||||
# Le worker InfiGUI renvoie directement {"x": .., "y": ..} en pixels
|
||||
# source résolution. On essaie aussi ce format.
|
||||
m2 = re.search(r'"x"\s*:\s*(\d+).*?"y"\s*:\s*(\d+)', content, re.S)
|
||||
if m2:
|
||||
x, y = int(m2.group(1)), int(m2.group(2))
|
||||
# ici x/y sont en pixels image source — diviser par fixture size
|
||||
return x / 2560.0, y / 1600.0
|
||||
return None, None
|
||||
|
||||
# Cas général : parser regex prod avec divisor post-resize
|
||||
return parse_bbox_to_norm(content, rW, rH)
|
||||
|
||||
|
||||
def is_validated(cx: Optional[float], cy: Optional[float]) -> bool:
|
||||
if cx is None or cy is None:
|
||||
return False
|
||||
return (GT_CX_RANGE[0] <= cx <= GT_CX_RANGE[1]
|
||||
and GT_CY_RANGE[0] <= cy <= GT_CY_RANGE[1])
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Overlay PNG pour validation visuelle
|
||||
# ============================================================================
|
||||
|
||||
def draw_overlay(img: Image.Image, cx: float, cy: float, label: str,
|
||||
out_path: Path) -> None:
|
||||
overlay = img.copy().convert("RGB")
|
||||
draw = ImageDraw.Draw(overlay)
|
||||
W, H = overlay.size
|
||||
px, py = int(cx * W), int(cy * H)
|
||||
r = 30
|
||||
draw.ellipse([px - r, py - r, px + r, py + r], outline="red", width=5)
|
||||
draw.line([px - r * 2, py, px + r * 2, py], fill="red", width=3)
|
||||
draw.line([px, py - r * 2, px, py + r * 2], fill="red", width=3)
|
||||
try:
|
||||
font = ImageFont.truetype(
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 28)
|
||||
except OSError:
|
||||
font = ImageFont.load_default()
|
||||
txt = f"{label}\ncx={cx:.3f} cy={cy:.3f}"
|
||||
draw.rectangle([10, 10, 700, 90], fill="white")
|
||||
draw.text((20, 15), txt, fill="black", font=font)
|
||||
overlay.save(out_path)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Bench main
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class ModelStats:
|
||||
key: str
|
||||
label: str
|
||||
cold_s: float = 0.0
|
||||
warm_times: list[float] = field(default_factory=list)
|
||||
vram_peaks: list[int] = field(default_factory=list)
|
||||
raw_first: str = ""
|
||||
format_detected: str = "unknown"
|
||||
parse_ok: bool = False
|
||||
cx: Optional[float] = None
|
||||
cy: Optional[float] = None
|
||||
validated: bool = False
|
||||
error: str = ""
|
||||
|
||||
|
||||
def bench_one_model(key: str, cfg: dict, img: Image.Image, rW: int, rH: int,
|
||||
warm_runs: int, overlay_dir: Optional[Path]) -> ModelStats:
|
||||
stats = ModelStats(key=key, label=cfg["label"])
|
||||
runner = RUNNERS[cfg["runner"]]
|
||||
|
||||
print(f"\n══════ {cfg['label']} ══════")
|
||||
|
||||
# Déchargement VRAM avant cold (Ollama seulement — Transformers/vLLM
|
||||
# gardent le modèle, c'est attendu)
|
||||
if cfg["runner"] == "ollama":
|
||||
_ollama_unload_all()
|
||||
|
||||
# Cold
|
||||
print(f" [cold]", end=" ", flush=True)
|
||||
r0 = runner(cfg, img, rW, rH)
|
||||
stats.cold_s = r0.elapsed_s
|
||||
if r0.error:
|
||||
stats.error = r0.error
|
||||
print(f"❌ {r0.error}")
|
||||
return stats
|
||||
stats.raw_first = r0.raw[:250]
|
||||
stats.format_detected = detect_format(r0.raw)
|
||||
cx, cy = parse_coords(r0.raw, rW, rH, cfg["prompt_style"])
|
||||
stats.parse_ok = (cx is not None and cy is not None)
|
||||
stats.cx, stats.cy = cx, cy
|
||||
stats.validated = is_validated(cx, cy)
|
||||
stats.vram_peaks.append(r0.vram_pic_mib)
|
||||
print(f"{stats.cold_s:.2f}s | fmt={stats.format_detected} | "
|
||||
f"cx={cx} cy={cy} | val={stats.validated}")
|
||||
|
||||
# Warm
|
||||
print(f" [warm × {warm_runs}]", end=" ", flush=True)
|
||||
for i in range(warm_runs):
|
||||
rN = runner(cfg, img, rW, rH)
|
||||
if rN.error:
|
||||
print(f" run{i}=❌{rN.error}", end="")
|
||||
continue
|
||||
stats.warm_times.append(rN.elapsed_s)
|
||||
stats.vram_peaks.append(rN.vram_pic_mib)
|
||||
print()
|
||||
if stats.warm_times:
|
||||
print(f" warm avg={statistics.mean(stats.warm_times):.2f}s | "
|
||||
f"p95={sorted(stats.warm_times)[int(len(stats.warm_times)*0.95)-1]:.2f}s")
|
||||
|
||||
# Overlay (si parse OK)
|
||||
if overlay_dir and cx is not None and cy is not None:
|
||||
overlay_dir.mkdir(parents=True, exist_ok=True)
|
||||
draw_overlay(img, cx, cy, cfg["label"],
|
||||
overlay_dir / f"bench_{key}.png")
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def write_csv(all_stats: list[ModelStats], out_path: Path) -> None:
|
||||
with out_path.open("w", newline="") as f:
|
||||
w = csv.writer(f)
|
||||
w.writerow([
|
||||
"key", "label", "cold_s",
|
||||
"warm_avg_s", "warm_p50_s", "warm_p95_s",
|
||||
"vram_pic_mib",
|
||||
"format", "parse_ok", "cx_pct", "cy_pct", "validated",
|
||||
"raw_first", "error",
|
||||
])
|
||||
for s in all_stats:
|
||||
warm_avg = statistics.mean(s.warm_times) if s.warm_times else 0.0
|
||||
warm_p50 = statistics.median(s.warm_times) if s.warm_times else 0.0
|
||||
warm_p95 = (sorted(s.warm_times)[int(len(s.warm_times)*0.95)-1]
|
||||
if len(s.warm_times) > 1 else (s.warm_times[0]
|
||||
if s.warm_times else 0.0))
|
||||
vram = max(s.vram_peaks) if s.vram_peaks else 0
|
||||
w.writerow([
|
||||
s.key, s.label, f"{s.cold_s:.2f}",
|
||||
f"{warm_avg:.2f}", f"{warm_p50:.2f}", f"{warm_p95:.2f}",
|
||||
vram,
|
||||
s.format_detected, s.parse_ok,
|
||||
f"{s.cx:.4f}" if s.cx is not None else "",
|
||||
f"{s.cy:.4f}" if s.cy is not None else "",
|
||||
s.validated, s.raw_first.replace("\n", " "), s.error,
|
||||
])
|
||||
|
||||
|
||||
def print_summary(all_stats: list[ModelStats]) -> None:
|
||||
print("\n\n══════════════════ SYNTHÈSE ══════════════════")
|
||||
print("| Modèle | Cold (s) | Warm avg | VRAM MiB | Fmt | cx | cy | VAL |")
|
||||
print("|---|---:|---:|---:|---|---:|---:|:---:|")
|
||||
for s in all_stats:
|
||||
warm_avg = statistics.mean(s.warm_times) if s.warm_times else 0.0
|
||||
vram = max(s.vram_peaks) if s.vram_peaks else 0
|
||||
cx_s = f"{s.cx:.3f}" if s.cx is not None else "—"
|
||||
cy_s = f"{s.cy:.3f}" if s.cy is not None else "—"
|
||||
mark = "✅" if s.validated else ("⚠" if s.parse_ok else "❌")
|
||||
print(f"| {s.label[:40]} | {s.cold_s:.1f} | {warm_avg:.1f} | "
|
||||
f"{vram} | {s.format_detected} | {cx_s} | {cy_s} | {mark} |")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--models", default="qwen25vl_ollama,qwen3vl_ollama,qwen3vl_vllm,infigui_tx",
|
||||
help="liste séparée par virgules (clés dans MODEL_CATALOG)")
|
||||
ap.add_argument("--warm", type=int, default=10)
|
||||
ap.add_argument("--out", default="/tmp/bench_grounding_2026-05-23.csv")
|
||||
ap.add_argument("--overlay-dir", default="/tmp/bench_grounding_overlays")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not FIXTURE_PATH.exists():
|
||||
print(f"ERROR: fixture absente — {FIXTURE_PATH}")
|
||||
return 2
|
||||
|
||||
img = Image.open(FIXTURE_PATH).convert("RGB")
|
||||
W, H = img.size
|
||||
rH, rW = smart_resize(H, W)
|
||||
print(f"Fixture : {FIXTURE_PATH}")
|
||||
print(f" source : {W}×{H}")
|
||||
print(f" smart_resize() → {rW}×{rH} (factor=28)")
|
||||
print(f" vérité-terrain : cx ∈ {GT_CX_RANGE}, cy ∈ {GT_CY_RANGE}")
|
||||
print(f" cible : '{TARGET_LABEL}'")
|
||||
|
||||
keys = [k.strip() for k in args.models.split(",") if k.strip()]
|
||||
unknown = [k for k in keys if k not in MODEL_CATALOG]
|
||||
if unknown:
|
||||
print(f"ERROR: modèles inconnus : {unknown}")
|
||||
print(f"Catalog : {list(MODEL_CATALOG)}")
|
||||
return 2
|
||||
|
||||
overlay_dir = Path(args.overlay_dir) if args.overlay_dir else None
|
||||
all_stats: list[ModelStats] = []
|
||||
for k in keys:
|
||||
try:
|
||||
stats = bench_one_model(
|
||||
k, MODEL_CATALOG[k], img, rW, rH, args.warm, overlay_dir)
|
||||
all_stats.append(stats)
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n⚠ Interrompu pendant {k}")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"\n❌ Crash {k}: {e}")
|
||||
all_stats.append(ModelStats(key=k, label=MODEL_CATALOG[k]["label"],
|
||||
error=f"crash:{e}"))
|
||||
|
||||
out = Path(args.out)
|
||||
write_csv(all_stats, out)
|
||||
print(f"\nCSV écrit : {out}")
|
||||
if overlay_dir:
|
||||
print(f"Overlays : {overlay_dir}/")
|
||||
|
||||
print_summary(all_stats)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
```
|
||||
|
||||
**Vérification syntaxique** : imports cohérents avec `core/grounding/smart_resize.py` (export `smart_resize`) et `core/grounding/bbox_parser.py` (export `parse_bbox_to_norm`). Pas de dépendance externe hors `requests`, `Pillow` (déjà dans `.venv`). `subprocess` pour Transformers worker + nvidia-smi. **À tester par Dom** avec `--models qwen25vl_ollama` seul d'abord pour valider l'I/O.
|
||||
|
||||
---
|
||||
|
||||
## 4. Procédure d'install pour Dom
|
||||
|
||||
### 4.1. Prérequis Ollama
|
||||
|
||||
```bash
|
||||
# Vérifier que les 2 modèles sont pull
|
||||
ollama list | grep -E "qwen2.5vl|qwen3-vl"
|
||||
# Si manquants :
|
||||
ollama pull qwen2.5vl:7b # ~8 GB
|
||||
ollama pull qwen3-vl:8b # ~6 GB
|
||||
```
|
||||
|
||||
### 4.2. Prérequis vLLM (option recommandée)
|
||||
|
||||
`vLLM` est listé dans `MIGRATION_VLM_PLAN_2026-05-09.md` §3 comme cible. Démarrage suggéré dans un terminal séparé (ou un service systemd dédié, voir `tools/start_grounding_server.sh` pour le pattern existant) :
|
||||
|
||||
```bash
|
||||
# Dans venv_v3 (créer pip install vllm si manquant)
|
||||
cd ~/ai/rpa_vision_v3 && source venv_v3/bin/activate
|
||||
pip install --upgrade vllm # >= 0.6.5 pour Qwen3-VL
|
||||
|
||||
python -m vllm.entrypoints.openai.api_server \
|
||||
--model Qwen/Qwen3-VL-8B-Instruct \
|
||||
--host 0.0.0.0 --port 8100 \
|
||||
--gpu-memory-utilization 0.55 \
|
||||
--max-model-len 8192 \
|
||||
--limit-mm-per-prompt image=1 \
|
||||
--mm-processor-kwargs '{"min_pixels": 100352, "max_pixels": 1003520}'
|
||||
```
|
||||
|
||||
**Note** : le hardware n'a que 12 GB VRAM. Si Ollama tourne en parallèle avec un autre modèle chargé, prévoir `unload_all` Ollama avant lancement vLLM (`for m in $(ollama ps | awk 'NR>1 {print $1}'); do curl -X POST localhost:11434/api/generate -d "{\"model\":\"$m\",\"keep_alive\":0}"; done`).
|
||||
|
||||
### 4.3. Modèles optionnels (HuggingFace)
|
||||
|
||||
```bash
|
||||
# OS-Atlas (si on l'active dans --models)
|
||||
huggingface-cli download OS-Copilot/OS-Atlas-Base-7B
|
||||
# Magma (extension future)
|
||||
huggingface-cli download microsoft/Magma-8B
|
||||
```
|
||||
|
||||
Les modèles HF se cachent automatiquement dans `~/.cache/huggingface/hub/`. Comptez ~15 GB par modèle 7-8B en bf16, ~4 GB en 4-bit NF4 (notre stack quantize à la volée pour InfiGUI).
|
||||
|
||||
### 4.4. Lancement bench
|
||||
|
||||
```bash
|
||||
cd ~/ai/rpa_vision_v3 && source venv_v3/bin/activate
|
||||
|
||||
# Salve baseline (sans vLLM, sans OS-Atlas)
|
||||
.venv/bin/python tools/bench_grounding_2026-05-23.py \
|
||||
--models qwen25vl_ollama,qwen3vl_ollama,infigui_tx \
|
||||
--warm 10
|
||||
|
||||
# Salve complète (vLLM démarré préalablement)
|
||||
.venv/bin/python tools/bench_grounding_2026-05-23.py \
|
||||
--models qwen25vl_ollama,qwen3vl_ollama,qwen3vl_vllm,infigui_tx \
|
||||
--warm 10
|
||||
|
||||
# Avec OS-Atlas (nécessite implémentation effective de run_transformers_qwen2vl)
|
||||
.venv/bin/python tools/bench_grounding_2026-05-23.py \
|
||||
--models qwen25vl_ollama,qwen3vl_vllm,infigui_tx,os_atlas_tx \
|
||||
--warm 10
|
||||
```
|
||||
|
||||
Durée estimée pour la salve baseline : 1 cold + 10 warm × 3 modèles. Si cold ≈ 11s et warm ≈ 2s : ~2 min/modèle, total ~7-10 min.
|
||||
|
||||
---
|
||||
|
||||
## 5. Modèles candidats avec configs précises
|
||||
|
||||
### 5.1. `qwen2.5vl:7b` (Ollama, baseline buggy attendue)
|
||||
|
||||
- Backend : Ollama HTTP `/api/chat`
|
||||
- Prompt : `Detect 'OK button' in this image with a bounding box.` (prompt actuel `resolve_engine.py:942`)
|
||||
- Options : `temperature=0.1`, `num_predict=100`
|
||||
- **Attendu** : bbox_2d en pixels post-resize Ollama (opaque) → `cx_pct ≈ 0.17` (bug confirmé 8 mai)
|
||||
- Rôle dans le bench : **témoin du bug**, doit échouer pour confirmer que le bug est reproductible et que le bench discrimine.
|
||||
|
||||
### 5.2. `qwen3-vl:8b` (Ollama, prompt JSON explicite)
|
||||
|
||||
- Backend : Ollama HTTP `/api/chat`
|
||||
- Prompt système : `You are a UI element locator. Output raw JSON only. No explanation.`
|
||||
- Prompt user : `Locate the "OK button" in this {rW}x{rH} screenshot. Return ONLY this JSON object: {"bbox_2d":[x1,y1,x2,y2],"label":"OK button"}`
|
||||
- Options : `temperature=0.1`, `num_predict=128`, `think:false`
|
||||
- ⚠ Note web search : Ollama issue #14798 — `think:false` est **silencieusement ignoré** pour qwen3-vl:8b (template bare). Vérifier sur les outputs si `<think>...</think>` apparaît. Workaround : préfixer le user prompt par `/no_think`.
|
||||
- **Attendu** : si `smart_resize` côté script correspond au resize interne Ollama, `cx ≈ 0.50`. Sinon, même bug que qwen2.5vl.
|
||||
|
||||
### 5.3. `Qwen3-VL-8B-Instruct` (vLLM, cible migration)
|
||||
|
||||
- Backend : vLLM OpenAI-compat sur :8100
|
||||
- Image envoyée **déjà resize côté client** au `smart_resize`-output → vLLM ne re-resize plus (à confirmer via `mm_processor_kwargs={"min_pixels": rW*rH, "max_pixels": rW*rH}` pour forcer no-op).
|
||||
- Prompt : idem 5.2
|
||||
- `max_tokens=128`, `temperature=0.1`
|
||||
- **Attendu** : `cx_pct ∈ [0.40, 0.60]` si la chaîne resize+prompt+parse est cohérente. C'est la config qui valide AXE_A2.
|
||||
|
||||
### 5.4. `InfiGUI-G1-3B` (Transformers, prod actuelle)
|
||||
|
||||
- Backend : `core/grounding/infigui_worker.py` en subprocess (one-shot) OU socket Unix via `rpa-grounding.service` si actif.
|
||||
- Prompt : du worker, sortie `point_2d` (pas `bbox_2d`).
|
||||
- **Spécificité** : le worker écrit (x, y) en pixels **source** déjà (re-multiplie par `W/rW, H/rH` ligne 174-175). Donc `cx_pct = x_returned / 2560`, `cy_pct = y_returned / 1600` direct. Vérifier au runtime.
|
||||
- **Attendu** : `cx_pct ≈ 0.50` (le worker est testé en prod, c'est la baseline qui marche déjà selon `HISTORIQUE_VLM_IMPLEMENTATIONS_2026-05-08.md`).
|
||||
|
||||
### 5.5. `OS-Atlas-Base-7B` (Transformers, candidate SOTA, AXE A1)
|
||||
|
||||
- Backend : Transformers Qwen2.5-VL chargé en 4-bit NF4 (à implémenter, runner `transformers_qwen2vl` est un stub v0).
|
||||
- Prompt : `In the image, please find the bbox of "OK button". Output the bounding boxes in this format: [[x1,y1,x2,y2]], where each value is normalized in [0, 1000].`
|
||||
- Output 0-1000 normalisé → conversion directe `cx_pct = (x1 + x2) / 2 / 1000`.
|
||||
- **Attendu** : SOTA sur ScreenSpot/ScreenSpot-Pro selon HF README → `cx_pct ∈ [0.40, 0.60]` probable.
|
||||
|
||||
### 5.6. `Magma-8B` (extension future, non v0)
|
||||
|
||||
Magma utilise **Set-of-Mark** : il faut détecter d'abord les éléments (SomEngine ou OmniParser) puis lui demander de choisir un numéro. Pas directement comparable aux 5 candidats ci-dessus. À benchmarker dans un AXE A4 séparé.
|
||||
|
||||
---
|
||||
|
||||
## 6. Critère de validation success (matrice)
|
||||
|
||||
| Modèle | Latence cold | Warm avg | cx_pct ∈ [0.40, 0.60] | cy_pct ∈ [0.40, 0.60] | Parse regex prod | Verdict |
|
||||
|---|---|---|---|---|---|---|
|
||||
| qwen25vl_ollama | < 12 s | < 12 s | ❌ (attendu 0.17) | ? | ✅ | **Témoin OK si tout sauf cx pass** |
|
||||
| qwen3vl_ollama | < 5 s | < 3 s | ✅ ou ❌ selon resize | ✅ | ✅ si prompt JSON | go si ✅ |
|
||||
| qwen3vl_vllm | < 8 s | < 3 s | ✅ requis | ✅ requis | ✅ requis | **CIBLE migration AXE_A2** |
|
||||
| infigui_tx | < 15 s | < 4 s | ✅ requis | ✅ requis | ✅ (point_2d) | Baseline prod |
|
||||
| os_atlas_tx | < 15 s | < 5 s | ✅ requis | ✅ requis | ✅ (raw_2d 0-1000) | Candidat upgrade |
|
||||
|
||||
**Verdict global AXE_A3** :
|
||||
- Si `qwen3vl_vllm` ET `infigui_tx` passent ✅ → migration vers vLLM Qwen3-VL est **safe**, AXE_A2 peut être considéré comme résolu.
|
||||
- Si **seul `infigui_tx` passe** → la prod actuelle est valide, vLLM Qwen3-VL n'apporte rien à part la latence — décision business sur la migration.
|
||||
- Si **rien ne passe** → le bug n'est PAS dans `smart_resize` seul. Investiguer `divisor_w/h` côté `parse_bbox_to_norm`, et la chaîne window crop (`window_rect` line 916-919) qui pourrait introduire un offset.
|
||||
|
||||
---
|
||||
|
||||
## 7. Vérité terrain manuelle pour Dom
|
||||
|
||||
Avant tout bench, Dom doit confirmer visuellement où est le bouton OK sur la fixture. Trois méthodes au choix :
|
||||
|
||||
### Méthode 1 : aperçu rapide via xdg-open
|
||||
|
||||
```bash
|
||||
xdg-open /home/dom/ai/rpa_vision_v3/data/training/live_sessions/bg_DESKTOP-58D5CAC_windows/shots/heartbeat_1773792436.png
|
||||
```
|
||||
|
||||
Ouvrir l'image, identifier le bouton OK à l'œil, estimer `cx_pct ≈ pixel_x / 2560` et `cy_pct ≈ pixel_y / 1600`.
|
||||
|
||||
### Méthode 2 : overlay grille via PIL (one-liner)
|
||||
|
||||
```bash
|
||||
.venv/bin/python -c "
|
||||
from PIL import Image, ImageDraw
|
||||
img = Image.open('data/training/live_sessions/bg_DESKTOP-58D5CAC_windows/shots/heartbeat_1773792436.png').convert('RGB')
|
||||
draw = ImageDraw.Draw(img)
|
||||
W, H = img.size
|
||||
# Grille de référence en %
|
||||
for pct in [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]:
|
||||
x = int(pct * W)
|
||||
draw.line([(x, 0), (x, H)], fill='cyan', width=2)
|
||||
draw.text((x + 5, 10), f'{pct:.1f}', fill='cyan')
|
||||
y = int(pct * H)
|
||||
draw.line([(0, y), (W, y)], fill='cyan', width=2)
|
||||
draw.text((10, y + 5), f'{pct:.1f}', fill='cyan')
|
||||
img.save('/tmp/heartbeat_grid.png')
|
||||
print('grille → /tmp/heartbeat_grid.png')
|
||||
"
|
||||
xdg-open /tmp/heartbeat_grid.png
|
||||
```
|
||||
|
||||
Dom regarde où est le bouton OK et note `(cx_gt, cy_gt)`. Si OK n'est pas dans `[0.40, 0.60]² ` après cette vérif, **ajuster `GT_CX_RANGE` / `GT_CY_RANGE` dans le script** avant tout bench.
|
||||
|
||||
### Méthode 3 : crop autour de la zone OK
|
||||
|
||||
```bash
|
||||
.venv/bin/python -c "
|
||||
from PIL import Image
|
||||
img = Image.open('data/training/live_sessions/bg_DESKTOP-58D5CAC_windows/shots/heartbeat_1773792436.png')
|
||||
# Crop la zone supposée 0.4-0.6 × 0.4-0.6
|
||||
W, H = img.size
|
||||
img.crop((int(0.35*W), int(0.35*H), int(0.65*W), int(0.65*H))).save('/tmp/heartbeat_center_crop.png')
|
||||
"
|
||||
xdg-open /tmp/heartbeat_center_crop.png
|
||||
```
|
||||
|
||||
Si le bouton OK est visible dans ce crop, vérité-terrain `[0.40, 0.60]` est valide.
|
||||
|
||||
---
|
||||
|
||||
## 8. Extensions futures (autres fixtures à ajouter)
|
||||
|
||||
Une seule fixture = sous-dimensionné pour conclure. Au minimum 3 fixtures additionnelles, idéalement issues du replay réel post-démo :
|
||||
|
||||
| Fixture | Origine | Cible | cx_gt approx | Difficulté |
|
||||
|---|---|---|---|---|
|
||||
| **Tabs Easily Assure** | replay 8 mai bug step 10 | "Imagerie" / "Notes médicales" / "Synthèse Urgences" (3 cas) | par tab | discriminer 3 textes dans une barre — confondus en center-of-line (REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md §4.2) |
|
||||
| **Popup IPP recherche** | recording VWB urgence_aiva_demo | bouton "Rechercher" | mid-screen | popup centré, peut avoir plusieurs occurrences du même texte (cf. `original_position` y_relative pour désambiguer) |
|
||||
| **Bandeau outils Easily** | shot quelconque | icône "disquette" (sans texte) | top-left de la barre | grounding visuel pur (pas d'OCR), c'est le cas pour lequel le grounding VLM est censé exister |
|
||||
|
||||
Ces extensions justifieraient un **AXE A3.2 — Multi-fixture grounding bench** une fois A3.1 (cette spec) validé. Le script ci-dessus est déjà conçu pour accepter une liste de fixtures (refacto trivial : `FIXTURE_PATH` + `TARGET_LABEL` + `GT_*_RANGE` → liste de tuples, boucle externe).
|
||||
|
||||
---
|
||||
|
||||
## 9. Dépendances et liens avec autres AXES
|
||||
|
||||
- **AXE A1 (sélection modèles)** : alimente la section §5 — si A1 retient OS-Atlas-Pro-7B ou ShowUI-2B en lieu et place de OS-Atlas-Base-7B, ajuster `MODEL_CATALOG`.
|
||||
- **AXE A2 (smart_resize calibration)** : ce bench est la **validation end-to-end** d'A2. Tant qu'A2 n'a pas tranché entre factor=28 (Qwen2-VL) et factor=32 (Qwen3-VL), lancer A3 d'abord avec `factor=28` (état actuel `core/grounding/smart_resize.py`) puis re-bench avec `factor=32` (modifier `FACTOR_DEFAULT` du module ou patcher le script).
|
||||
- **Hors scope A3** : SomEngine + Magma (Set-of-Mark) ; ces 2 stratégies de grounding nécessitent un détecteur amont (cf. `_resolve_by_som` dans `resolve_engine.py:1095+`) et un bench distinct.
|
||||
|
||||
---
|
||||
|
||||
*Document destiné à être consommé par Dom et un agent d'exécution. Aucune action runtime déclenchée par cette spec. À mettre à jour quand A1 et A2 auront tranché leurs paramètres.*
|
||||
487
docs/recherche/AXE_A4_OCR_TEMPLATE_PHASH.md
Normal file
487
docs/recherche/AXE_A4_OCR_TEMPLATE_PHASH.md
Normal file
@@ -0,0 +1,487 @@
|
||||
# AXE A4 — OCR, Template matching, pHash : revue 2026 + correctif `_resolve_by_ocr_text`
|
||||
|
||||
**Date :** 2026-05-23
|
||||
**Auteur :** Claude (dispatch recherche)
|
||||
**Périmètre :** revue littérature/écosystème 2025-2026 pour la cascade UI `OCR → template → VLM` + alternatives à `pHash` pour LoopDetector et VERIFY. Patch ciblé du bug *center-of-line* de `_resolve_by_ocr_text` (`agent_v0/server_v1/resolve_engine.py:1447-1527`).
|
||||
**Lecture pré-requise :** `docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` §2, §4 ; `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` §1.2 et §5 ; `docs/BUG_PRECHECK_SPATIAL_BLINDNESS_2026-05-08.md` (DETTE-001).
|
||||
**Statut :** recherche + propositions. **Aucune modification de code.** Toute application validée par Dom.
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR
|
||||
|
||||
1. **Le bug primaire `center-of-line` est résolvable sans changer d'OCR.** docTR expose les `geometry` au niveau du `Word`, normalisées dans le **même repère que la ligne**. Le quick fix §5 du diagnostic 8 mai (cf. §5 ci-dessous, code copy-paste-ready) supprime la collision Imagerie/Notes/Synthèse en restant 100 % iso-stack.
|
||||
2. **OCR : garder docTR comme moteur OCR-DIRECT** (mode strict + cascade) car c'est le seul, avec Tesseract et PaddleOCR `return_word_box=True`, à exposer des bbox **token-level dans le même repère que la ligne**. EasyOCR retourne par défaut des bbox merges niveau line/segment et **n'est pas adapté** à la résolution multi-tokens d'un onglet sur barre. Surya OCR = line-level uniquement, à écarter pour ce besoin. RapidOCR (PaddleOCR ONNX repackagé) → candidat 2026 pour OCR-DIRECT *léger sans dépendance Paddle*, à valider sur français accentué.
|
||||
3. **Template matching : remplacer `cv2.matchTemplate` multi-scale par SuperPoint+LightGlue (ONNX, ~50 ms par paire sur RTX 5070).** C'est la sortie propre pour la drift exemption `≥ 0.95` actuelle, qui est un faux positif déguisé (score haut sur région différente). LightGlue est invariant à l'offset/scale/rotation et fournit un *score de cohérence géométrique* — donc plus de faux positifs « 0.95 sur mauvaise zone ». À encapsuler derrière `_resolve_by_template` sans casser la cascade.
|
||||
4. **pHash : sortir du global. Deux modes complémentaires :**
|
||||
- **LoopDetector (QW2)** → DINOv2 features sur l'écran entier, cos-sim < 0.99 = écran a bougé. Plus robuste qu'un pHash 64-bit à un curseur clignotant ou à un caret blinking.
|
||||
- **VERIFY post-action** → **SSIM par ROI** (skimage `structural_similarity`, ~5-10 ms sur crop 400×200), avec ROI = bbox de la cible cliquée + halo 50 px. C'est la version *spatialisée* qui résout aussi DETTE-001 (BUG_PRECHECK_SPATIAL_BLINDNESS).
|
||||
5. **Dépendances** : ce travail est **bloquant** pour AXE_A5 (tokenisation UI : OmniParser et UI-DETR-1 utilisent in fine un OCR + détection icônes — décider du moteur OCR avant tokenisation). Il **alimente** AXE_B2 (Validator) qui consommera SSIM-ROI comme signal sémantique de VERIFY.
|
||||
|
||||
---
|
||||
|
||||
## 1. Sous-axe 1 — OCR pour grounding
|
||||
|
||||
### 1.1. Question centrale : bbox token-level dans le même repère que la ligne
|
||||
|
||||
Le bug `center-of-line` apparaît parce que `_resolve_by_ocr_text` (resolve_engine.py:1486-1519) calcule `cx, cy` à partir de la `line_obj.geometry` (bbox de la ligne entière) alors que `target_text` n'est qu'un sous-fragment. Pour le résoudre **sans changer d'OCR**, il suffit que l'OCR expose, dans le même repère normalisé que la ligne, les bbox des **words** qui composent la ligne. C'est le critère discriminant.
|
||||
|
||||
### 1.2. Table comparative (mai 2026)
|
||||
|
||||
| OCR | Granularité bbox | Repère | Français/accents | Latence (CPU 2560×1600) | Stack | Licence | Date release majeure |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| **docTR** (`python-doctr`) | **word + line + block** | normalisé `[(xmin,ymin),(xmax,ymax)]` ∈ [0,1]², **commun line/word** | bon (modèle `crnn_vgg16_bn` français) | ~800 ms CPU, ~150 ms GPU | PyTorch + TF, ONNX optionnel | Apache 2.0 | v0.10 (2026-04, `python-doctr` PyPI) |
|
||||
| **EasyOCR** | line merged (par défaut) + char optionnel via `ycenter_ths`/`width_ths` | pixel absolu | bon | ~1.2 s CPU, ~200 ms GPU | PyTorch, CRNN | Apache 2.0 | v1.7.x (2024) |
|
||||
| **RapidOCR** (`rapidocr`) | line | pixel absolu | bon (modèle PP-OCRv4 fr) | ~200 ms ONNX-CPU, ~80 ms GPU | ONNXRuntime / OpenVINO / MNN / PaddlePaddle, **sans dépendance Paddle** | Apache 2.0 | v3.x (2026-04-11) |
|
||||
| **PaddleOCR / PP-StructureV3** | line par défaut ; **`return_word_box=True`** en option | pixel absolu | bon | ~250 ms GPU (PP-OCRv4) | PaddlePaddle (lourd) | Apache 2.0 | v3.0 (2025-07) |
|
||||
| **Surya OCR** (`surya-ocr`) | **line only** | pixel absolu | bon (90+ langues) | ~400 ms GPU (5070-class) | PyTorch | GPL-3.0 (commercial restrictif) | v0.17.x (2025) |
|
||||
| **Tesseract** (via `pytesseract`) | **word + line + char** via `image_to_data` / `hOCR` | pixel absolu | moyen-bon (modèle `fra`) | 100-500 ms CPU | C++ LSTM | Apache 2.0 | v5.4 (2024) |
|
||||
|
||||
**Sources principales :** [docTR Word/Line geometry — Discussion #570](https://github.com/mindee/doctr/discussions/570), [PaddleOCR return_word_box — Issue #15760](https://github.com/PaddlePaddle/PaddleOCR/issues/15760), [Surya line-level — repo datalab-to/surya](https://github.com/datalab-to/surya), [EasyOCR character bbox limitation — Issue #631](https://github.com/JaidedAI/EasyOCR/issues/631), [RapidOCR releases](https://github.com/RapidAI/RapidOCR/releases), [pytesseract image_to_data — PyPI](https://pypi.org/project/pytesseract/), [Codesota benchmark PaddleOCR vs EasyOCR 2025](https://www.codesota.com/ocr/paddleocr-vs-easyocr), [Tildalice benchmark PaddleOCR vs Doctr](https://buttondown.com/ckae930413/archive/paddleocr-vs-easyocr-vs-doctr-memory-latency-test/).
|
||||
|
||||
### 1.3. Analyse du bug center-of-line
|
||||
|
||||
**Le bug est résolvable nativement avec docTR.** L'API expose `line_obj.words` (List[Word]) avec chaque `Word.geometry` au même format `((xmin,ymin),(xmax,ymax))` normalisé que `line_obj.geometry`. Il n'y a aucun changement de repère à faire — c'est le même page-relative ∈ [0,1]². Cf. [docTR I/O modules doc](https://mindee.github.io/doctr/modules/io.html).
|
||||
|
||||
EasyOCR a la **mauvaise granularité par défaut** : il merge les détections en segments via `ycenter_ths=0.5` et `width_ths=0.5`, donc une rangée de tabs serrée tombera comme une boîte unique, **sans accès aux sous-words**. Demander explicitement `width_ths=0.0` casserait la fusion mais aussi pour les vrais textes longs (« Justification de la décision »). **EasyOCR seul ne résout pas le bug.**
|
||||
|
||||
Surya OCR est annoncé explicitement comme line-only : « Surya predicts line-level bboxes, while tesseract and others predict word-level or character-level » (cf. [datalab-to/surya README](https://github.com/datalab-to/surya)). **À écarter** pour ce besoin.
|
||||
|
||||
PaddleOCR `return_word_box=True` est disponible en v3.0 mais nécessite une dépendance PaddlePaddle ~700 Mo et un init ~8-12 s sur CPU.
|
||||
|
||||
RapidOCR repackage les modèles PaddleOCR en ONNX (80 Mo install, init <2 s) ; **il faut vérifier en mai 2026 si `return_word_box` est exposé dans la couche `rapidocr.RapidOCR(__call__)` ou seulement dans `paddleocr.PaddleOCR`**. À ce jour, la doc publique RapidOCR ne mentionne pas explicitement le mode word-bbox.
|
||||
|
||||
### 1.4. Snippets Python — récupérer bbox word-level
|
||||
|
||||
**docTR (déjà utilisé en production)**
|
||||
|
||||
```python
|
||||
from doctr.models import ocr_predictor
|
||||
from doctr.io import DocumentFile
|
||||
|
||||
predictor = ocr_predictor(pretrained=True)
|
||||
doc = DocumentFile.from_images("/path/screenshot.png")
|
||||
result = predictor(doc)
|
||||
|
||||
# Navigation hiérarchique : pages -> blocks -> lines -> words
|
||||
page = result.pages[0]
|
||||
H, W = page.dimensions # (height, width) pixels
|
||||
|
||||
for block in page.blocks:
|
||||
for line in block.lines:
|
||||
# line.geometry == ((xmin, ymin), (xmax, ymax)) normalisé [0,1]²
|
||||
# line.words == List[Word], chaque Word.geometry au même format
|
||||
for word in line.words:
|
||||
(xmin_n, ymin_n), (xmax_n, ymax_n) = word.geometry
|
||||
# Pixels absolus
|
||||
xmin_px = xmin_n * W
|
||||
ymax_px = ymax_n * H
|
||||
print(f"{word.value!r} bbox=({xmin_px:.0f},{ymin_px*H:.0f})-({xmax_px:.0f},{ymax_px:.0f})")
|
||||
```
|
||||
|
||||
**Tesseract (alternative légère, fallback CPU)**
|
||||
|
||||
```python
|
||||
import pytesseract
|
||||
from PIL import Image
|
||||
|
||||
img = Image.open("/path/screenshot.png")
|
||||
data = pytesseract.image_to_data(img, lang="fra", output_type=pytesseract.Output.DICT)
|
||||
|
||||
# data == dict with keys 'text','left','top','width','height','conf','line_num','word_num','block_num'
|
||||
n = len(data['text'])
|
||||
for i in range(n):
|
||||
if data['text'][i].strip() and int(data['conf'][i]) > 50:
|
||||
x, y, w, h = data['left'][i], data['top'][i], data['width'][i], data['height'][i]
|
||||
# Pixels absolus directement
|
||||
print(f"{data['text'][i]!r} bbox=({x},{y})-({x+w},{y+h}) line={data['line_num'][i]}")
|
||||
```
|
||||
|
||||
**RapidOCR (candidat migration, ONNX léger)**
|
||||
|
||||
```python
|
||||
from rapidocr import RapidOCR
|
||||
|
||||
engine = RapidOCR()
|
||||
result, elapsed = engine("/path/screenshot.png")
|
||||
# result == [[box, text, score], ...] avec box = [[x1,y1],[x2,y2],[x3,y3],[x4,y4]] pixel absolu
|
||||
# ⚠ Niveau line par défaut — à valider en mai 2026 si word-level disponible
|
||||
```
|
||||
|
||||
### 1.5. Recommandation
|
||||
|
||||
**Garder docTR pour OCR-DIRECT** (mode strict + cascade resolve_engine). C'est l'OCR qui colle déjà aux contraintes du bug. Le quick fix §5 (recalcul `cx, cy` depuis `line.words`) ne nécessite ni migration ni changement d'API.
|
||||
|
||||
**Ne PAS migrer en chaud vers EasyOCR ou Surya** : EasyOCR perd le sous-word, Surya est line-only par design.
|
||||
|
||||
**Évaluation parallèle** (post-démo, AXE_A5) :
|
||||
- RapidOCR sur 10 captures Easily fr — gain potentiel : init 2 s vs 5-8 s docTR, install 80 Mo vs 500 Mo + PyTorch.
|
||||
- Tesseract `image_to_data` lang `fra` — peut servir de **second moteur OCR de vérification** (vote OCR à 2 moteurs) pour DETTE-001.
|
||||
|
||||
---
|
||||
|
||||
## 2. Sous-axe 2 — Template matching (étage 2 cascade)
|
||||
|
||||
### 2.1. Question centrale : robustesse à l'offset/scale + élimination des faux positifs 0.95
|
||||
|
||||
`cv2.matchTemplate` multi-scale (range 0.25→2.0, `resolve_engine.py:130`) calcule un score de corrélation NCC pixel-à-pixel. Limites connues :
|
||||
- **Aucune invariance à la rotation.** Easily/Edge sont fixes en rotation, donc OK ici.
|
||||
- **Sensible à l'anti-aliasing** : un même bouton scaled 0.95× vs 1.0× peut perdre 0.10 sur le score.
|
||||
- **Le score haut ne garantit pas la bonne région** : le match peut être 0.95 sur un patch visuellement similaire (autre bouton de la même barre, même icône de close, etc.). C'est exactement le mécanisme qui force aujourd'hui le `drift exemption ≥ 0.95` (`resolve_engine.py:2367-2390`) à être une rustine — score haut, mauvais endroit.
|
||||
- Cf. [PyImageSearch multi-scale template matching](https://pyimagesearch.com/2015/01/26/multi-scale-template-matching-using-python-opencv/) et [Medium — Template Matching Beyond Basics](https://medium.com/@coders.stop/template-matching-beyond-basics-rotation-and-scale-invariant-detection-2ae78d8fa190).
|
||||
|
||||
### 2.2. Table comparative
|
||||
|
||||
| Méthode | Invariance | Score géométrique | Latence pair (RTX 5070, 800×500 vs 2560×1600) | Faux positif 0.95 ? | Licence | Maturité 2026 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **`cv2.matchTemplate` NCC multi-scale** (actuel) | scale ±20 % (force brute) | non — score pixel | ~50-200 ms CPU (multi-scale loop) | **oui** (rustine drift exemption) | BSD | mature |
|
||||
| **SIFT / AKAZE / ORB (cv2)** | scale + rotation + offset | non — inliers RANSAC | ~30 ms CPU | filtré par RANSAC mais sensible aux UI peu texturées | BSD | mature |
|
||||
| **SuperPoint + LightGlue (ONNX)** | scale + rotation + offset + photométrie | **oui** — score MNN + inliers | **~44 ms (22 FPS) pair complète RTX-class** | **non** si on prend `len(matches) > seuil` ET cohérence homographique | Apache 2.0 (modèle), [fabio-sim/LightGlue-ONNX](https://github.com/fabio-sim/LightGlue-ONNX) | très mature 2024-2026 |
|
||||
| **LoFTR / Efficient LoFTR** | id. | id. + dense | ~80 ms pair RTX-class | non | Apache 2.0 | mature, +1-2 pp AUC vs LightGlue mais 2× plus lent |
|
||||
| **DINOv2 patch features + kNN match** | id. + sémantique | cosine sim patch | ~150 ms (extract DINOv2 ViT-L) | rare (sémantique > pixel) | CC-BY-NC-4.0 ⚠ | très mature 2024-2026 |
|
||||
| **RoMa / RoMa v2** | id. + dense, sub-pixel | warp + certainties | ~200 ms RTX-class (v2 = 1.7× v1) | non | non-commercial | CVPR 2024, v2 fin 2025 |
|
||||
| **MASt3R-SfM** | id. + 3D | grid match | très lourd (~1 s+ par pair) | non | non-commercial | recherche 2024 |
|
||||
| **CLIP visual similarity** (global embedding) | id. + sémantique | cos-sim global | ~30 ms ViT-B/32 | échoue : trop global, ne localise pas | MIT | mature |
|
||||
|
||||
**Sources :** [LightGlue ICCV 2023 paper](https://openaccess.thecvf.com/content/ICCV2023/papers/Lindenberger_LightGlue_Local_Feature_Matching_at_Light_Speed_ICCV_2023_paper.pdf), [Efficient LoFTR arXiv 2403.04765](https://arxiv.org/pdf/2403.04765), [RoMa v2 emergent mind](https://www.emergentmind.com/papers/2511.15706), [DINOv2 features](https://www.emergentmind.com/topics/dinov2-features), [Image Matching Challenge 2025 — DINO-RotateMatch arXiv 2512.03715](https://arxiv.org/pdf/2512.03715), [LightGlue ONNX](https://github.com/fabio-sim/LightGlue-ONNX), [LightGlue HF Transformers](https://huggingface.co/docs/transformers/model_doc/lightglue).
|
||||
|
||||
### 2.3. Critère faux-positif 0.95
|
||||
|
||||
C'est le critère discriminant pour sortir de la drift exemption rustine. **SuperPoint + LightGlue** fournit deux signaux séparables :
|
||||
1. `n_matches` : nombre de keypoints appariés (typique 50-200 pour un widget visible).
|
||||
2. **Cohérence géométrique** : on calcule l'homographie via `cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)` sur les matches et on garde le ratio inliers / total. Un faux positif 0.95 sur région différente aura `n_matches < 10` ou un ratio inliers < 0.5.
|
||||
|
||||
Cela élimine la classe de bug « score 0.95, mauvais bouton » sans avoir besoin d'un seuil bas qui ferait passer le faux positif.
|
||||
|
||||
### 2.4. Recommandation
|
||||
|
||||
**Phase 1 (court terme, post-démo)** : conserver `cv2.matchTemplate` mais **ajouter une vérification géométrique LightGlue+SuperPoint en ratification** quand le score est ∈ [0.80, 0.95] (zone aujourd'hui ambiguë). Si LightGlue confirme la cohérence homographique → garder le match. Sinon → fallback VLM. Cela réduit l'exemption drift de 0.95 vers 0.80.
|
||||
|
||||
**Phase 2 (moyen terme)** : remplacer la boucle multi-scale `cv2.matchTemplate` par LightGlue+SuperPoint en méthode primaire d'étage 2. Garder un fallback NCC pour les widgets très uniformes/texturés faiblement (icônes monochromes plates où LightGlue manque de keypoints).
|
||||
|
||||
**Snippet — intégration LightGlue compatible cascade actuelle**
|
||||
|
||||
```python
|
||||
# Pseudo-code à brancher dans resolve_engine._resolve_by_template
|
||||
# Ne PAS appliquer en l'état — validation syntaxique seulement.
|
||||
|
||||
from lightglue import LightGlue, SuperPoint
|
||||
from lightglue.utils import load_image, rbd
|
||||
import cv2, torch
|
||||
|
||||
_LG_DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
_LG_EXTRACTOR = SuperPoint(max_num_keypoints=2048).eval().to(_LG_DEVICE)
|
||||
_LG_MATCHER = LightGlue(features="superpoint").eval().to(_LG_DEVICE)
|
||||
|
||||
def _verify_template_match_with_lightglue(
|
||||
screenshot_bgr,
|
||||
template_bgr,
|
||||
candidate_xy, # (cx, cy) pixel renvoyé par cv2.matchTemplate
|
||||
inlier_ratio_threshold=0.5,
|
||||
min_matches=10,
|
||||
):
|
||||
"""Confirme géométriquement un match cv2.matchTemplate.
|
||||
|
||||
Returns:
|
||||
dict(confirmed=bool, n_matches=int, inlier_ratio=float)
|
||||
"""
|
||||
# Crop autour du candidat (taille du template + halo)
|
||||
th, tw = template_bgr.shape[:2]
|
||||
cx, cy = candidate_xy
|
||||
x0 = max(0, cx - tw)
|
||||
y0 = max(0, cy - th)
|
||||
x1 = min(screenshot_bgr.shape[1], cx + tw)
|
||||
y1 = min(screenshot_bgr.shape[0], cy + th)
|
||||
crop = screenshot_bgr[y0:y1, x0:x1]
|
||||
|
||||
# Tensors LightGlue (1, 1, H, W) float [0,1]
|
||||
crop_t = torch.from_numpy(cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY)).float()[None, None] / 255.0
|
||||
tpl_t = torch.from_numpy(cv2.cvtColor(template_bgr, cv2.COLOR_BGR2GRAY)).float()[None, None] / 255.0
|
||||
|
||||
with torch.no_grad():
|
||||
feats0 = _LG_EXTRACTOR.extract(tpl_t.to(_LG_DEVICE))
|
||||
feats1 = _LG_EXTRACTOR.extract(crop_t.to(_LG_DEVICE))
|
||||
matches01 = _LG_MATCHER({"image0": feats0, "image1": feats1})
|
||||
|
||||
feats0, feats1, matches01 = (rbd(x) for x in [feats0, feats1, matches01])
|
||||
matches = matches01["matches"] # (M, 2)
|
||||
|
||||
n_matches = matches.shape[0]
|
||||
if n_matches < min_matches:
|
||||
return {"confirmed": False, "n_matches": n_matches, "inlier_ratio": 0.0}
|
||||
|
||||
pts0 = feats0["keypoints"][matches[..., 0]].cpu().numpy()
|
||||
pts1 = feats1["keypoints"][matches[..., 1]].cpu().numpy()
|
||||
H_, mask = cv2.findHomography(pts0, pts1, cv2.RANSAC, 5.0)
|
||||
if H_ is None:
|
||||
return {"confirmed": False, "n_matches": n_matches, "inlier_ratio": 0.0}
|
||||
inlier_ratio = float(mask.sum()) / n_matches
|
||||
|
||||
return {
|
||||
"confirmed": inlier_ratio >= inlier_ratio_threshold,
|
||||
"n_matches": n_matches,
|
||||
"inlier_ratio": inlier_ratio,
|
||||
}
|
||||
```
|
||||
|
||||
À brancher en **post-process** de `cv2.matchTemplate` : si score ∈ [0.80, 0.95], appel LightGlue. Si confirmé → garder. Cela transforme la rustine drift exemption en *vérification ratifiée*.
|
||||
|
||||
---
|
||||
|
||||
## 3. Sous-axe 3 — pHash → alternatives 2026
|
||||
|
||||
### 3.1. Usages actuels et limites
|
||||
|
||||
| Usage | Implémentation actuelle | Limite documentée |
|
||||
|---|---|---|
|
||||
| **LoopDetector QW2** | pHash global (`screen_static` ≥ threshold) + `action_repeat` + `retry_threshold` | un caret blinking ou un curseur sur barre de chargement fait varier le hash → faux négatif (« écran a bougé » alors qu'il n'a rien changé fonctionnellement) |
|
||||
| **VERIFY post-action** | pHash global avant/après click | un clic local sur un onglet change ~5 % de l'image (la zone des tabs + le contenu de l'onglet) — peut être absorbé par le hash global → faux négatif (le click n'a rien fait visible). Inversement, popup arrière-plan / curseur souris fait croire à un changement. |
|
||||
|
||||
Diagnostic principal : `feedback_phash_vs_dialog_in_vm.md` (memory) — pHash global est trop grossier pour la cascade VM. DETTE-001 (BUG_PRECHECK_SPATIAL_BLINDNESS) montre que c'est **spatialement aveugle** : `_text_match_fuzzy` valide le pré-check OCR au mauvais endroit parce que le radius 280 px englobe plusieurs tabs.
|
||||
|
||||
### 3.2. Table comparative — alternatives 2026
|
||||
|
||||
| Méthode | Mode | Latence (crop 400×200) | Mode ROI ? | Robustesse caret/curseur | Distingue mouvement local | Bibliothèque |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **pHash global 64-bit** | actuel | <5 ms | non | mauvaise | non | `imagehash` |
|
||||
| **pHash par ROI (rolling)** | extension simple | ~5 ms × N régions | oui (par tuiles) | OK | oui | `imagehash` |
|
||||
| **SSIM** (skimage) | classique | 5-10 ms CPU | **oui native** | bonne | oui | `skimage.metrics.structural_similarity` |
|
||||
| **MS-SSIM** | multi-échelle | 15-30 ms | oui | meilleure | oui | `pytorch-msssim` |
|
||||
| **LPIPS** (AlexNet/VGG) | deep | 30-80 ms | oui via crop | excellente (sémantique) | oui | `lpips` |
|
||||
| **DINOv2 patch features cos-sim** | deep semantic | 100-200 ms (ViT-S/14) | oui (patches) | excellente | oui | `transformers` + `dinov2_vits14` |
|
||||
| **CLIP image embedding cos-sim** | global semantic | ~30 ms | non (perd info spatiale) | bonne mais pas local | non | `open_clip` |
|
||||
|
||||
**Sources :** [Eureka — SSIM vs LPIPS](https://eureka.patsnap.com/article/ssim-vs-lpips-which-metric-should-you-trust-for-image-quality-evaluation), [SSIM scikit-image doc](https://scikit-image.org/docs/dev/auto_examples/transform/plot_ssim.html), [Wopee — Screenshot Comparison Algorithms](https://wopee.io/blog/screenshot-comparison-algorithms-visual-testing/), [Medium CLIP vs DINOv2 image similarity](https://medium.com/aimonks/clip-vs-dinov2-in-image-similarity-6fa5aa7ed8c6), [DinoHash arXiv 2503.11195](https://arxiv.org/pdf/2503.11195).
|
||||
|
||||
### 3.3. Recommandation par usage
|
||||
|
||||
**LoopDetector QW2 (écran statique → boucle)**
|
||||
- **Adopter** : DINOv2 features cos-sim sur frame entière (downscale 224×224 avant). Seuil cos < 0.99 = changement réel. Robuste au caret blinking, au scroll-bar position, à la souris.
|
||||
- **Coût** : ~100 ms par frame sur RTX 5070. Acceptable pour un trigger appelé 1×/sec.
|
||||
- **Alternative dégradée** : pHash par ROI (grille 4×4 tuiles), ré-utilise `imagehash` actuel, sans GPU.
|
||||
|
||||
**VERIFY post-action (a-t-on cliqué utilement ?)**
|
||||
- **Adopter SSIM par ROI** :
|
||||
- ROI = bbox du target résolu + halo 50 px (ou la zone qu'on s'attend à voir changer si elle est connue : par exemple, le contenu d'onglet pour un click sur onglet).
|
||||
- `structural_similarity(roi_before, roi_after, multichannel=True)`.
|
||||
- Seuil empirique à calibrer (0.85 = changement notable, 0.95 = rien n'a changé).
|
||||
- **Coût** : ~5 ms CPU sur crop 400×200, négligeable.
|
||||
- **Bénéfice transversal** : résout aussi DETTE-001 — au lieu de vérifier que `target_text` est présent dans un crop OCR autour du click, on vérifie que la **zone** elle-même a changé (= un click vraiment effectif déclenche un repaint local).
|
||||
|
||||
**Snippet — SSIM ROI VERIFY (drop-in dans `replay_verifier.py`)**
|
||||
|
||||
```python
|
||||
from skimage.metrics import structural_similarity as ssim
|
||||
import cv2, numpy as np
|
||||
|
||||
def verify_click_changed_roi(
|
||||
screenshot_before_path: str,
|
||||
screenshot_after_path: str,
|
||||
cx_px: int,
|
||||
cy_px: int,
|
||||
roi_w: int = 400,
|
||||
roi_h: int = 200,
|
||||
threshold: float = 0.95,
|
||||
) -> dict:
|
||||
"""Vérifie qu'un click a effectivement modifié la ROI cible.
|
||||
|
||||
Returns:
|
||||
dict(changed=bool, ssim=float, roi_bbox=(x0,y0,x1,y1))
|
||||
"""
|
||||
before = cv2.imread(screenshot_before_path)
|
||||
after = cv2.imread(screenshot_after_path)
|
||||
if before is None or after is None or before.shape != after.shape:
|
||||
return {"changed": False, "ssim": 0.0, "roi_bbox": (0, 0, 0, 0)}
|
||||
|
||||
H, W = before.shape[:2]
|
||||
x0 = max(0, cx_px - roi_w // 2)
|
||||
y0 = max(0, cy_px - roi_h // 2)
|
||||
x1 = min(W, cx_px + roi_w // 2)
|
||||
y1 = min(H, cy_px + roi_h // 2)
|
||||
|
||||
crop_b = cv2.cvtColor(before[y0:y1, x0:x1], cv2.COLOR_BGR2GRAY)
|
||||
crop_a = cv2.cvtColor(after[y0:y1, x0:x1], cv2.COLOR_BGR2GRAY)
|
||||
|
||||
score = float(ssim(crop_b, crop_a, data_range=255))
|
||||
return {
|
||||
"changed": score < threshold,
|
||||
"ssim": score,
|
||||
"roi_bbox": (x0, y0, x1, y1),
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Patch ciblé — bug center-of-line de `_resolve_by_ocr_text`
|
||||
|
||||
### 4.1. Cible exacte
|
||||
|
||||
Fichier : `agent_v0/server_v1/resolve_engine.py`, fonction `_resolve_by_ocr_text`, lignes **1486-1519** (référence dans `REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` §1.2).
|
||||
|
||||
Bloc actuel reconstitué d'après §1.2 du diagnostic 8 mai :
|
||||
|
||||
```python
|
||||
# resolve_engine.py:1486-1519 (état au 8 mai 2026)
|
||||
# Match exact > contient > mot par mot
|
||||
score = 0.0
|
||||
if target_lower == line_lower:
|
||||
score = 1.0
|
||||
elif target_lower in line_lower:
|
||||
score = 0.8
|
||||
elif any(target_lower == w.value.lower() for w in line_obj.words):
|
||||
score = 0.9
|
||||
|
||||
if score > best_score:
|
||||
box = line_obj.geometry # ⚠ bbox de la LIGNE ENTIÈRE
|
||||
cx = (box[0][0] + box[1][0]) / 2
|
||||
cy = (box[0][1] + box[1][1]) / 2
|
||||
best_score = score
|
||||
best_match = {"cx": cx, "cy": cy, "score": score, "line": line_obj.value}
|
||||
```
|
||||
|
||||
### 4.2. Patch proposé — center-of-span depuis `line.words`
|
||||
|
||||
**Principe** : pour les scores 0.8 (substring) et 0.9 (mot exact), recalculer `cx, cy` à partir des bbox des `words` qui couvrent le `target_text`, **pas** de la ligne entière.
|
||||
|
||||
Code copy-paste-ready (validation syntaxique seulement, **non exécuté**) :
|
||||
|
||||
```python
|
||||
# resolve_engine.py:1486-1519 (proposition)
|
||||
# Match exact > contient > mot par mot
|
||||
score = 0.0
|
||||
matched_words = [] # sous-ensemble de line_obj.words couvrant target_text
|
||||
|
||||
target_lower = target_text.lower().strip()
|
||||
line_lower = line_obj.value.lower().strip()
|
||||
|
||||
# 1) Match exact ligne entière
|
||||
if target_lower == line_lower:
|
||||
score = 1.0
|
||||
matched_words = list(line_obj.words)
|
||||
|
||||
# 2) Match substring (multi-mots possibles)
|
||||
elif target_lower in line_lower:
|
||||
score = 0.8
|
||||
# Reconstruire le span de words couvrant target_lower par concat séquentielle
|
||||
target_tokens = target_lower.split()
|
||||
line_words_lower = [w.value.lower() for w in line_obj.words]
|
||||
# Recherche d'une fenêtre contiguë qui matche tous les target_tokens dans l'ordre
|
||||
for start in range(len(line_words_lower) - len(target_tokens) + 1):
|
||||
window = line_words_lower[start:start + len(target_tokens)]
|
||||
# Comparaison tolérante : un token cible peut être préfixe/égal au token line
|
||||
if all(t == w or t in w or w in t for t, w in zip(target_tokens, window)):
|
||||
matched_words = line_obj.words[start:start + len(target_tokens)]
|
||||
break
|
||||
if not matched_words:
|
||||
# Fallback : tous les words contenant un token cible
|
||||
matched_words = [w for w in line_obj.words if any(t in w.value.lower() for t in target_tokens)]
|
||||
|
||||
# 3) Match mot-exact dans la ligne (single token)
|
||||
elif any(target_lower == w.value.lower() for w in line_obj.words):
|
||||
score = 0.9
|
||||
matched_words = [w for w in line_obj.words if w.value.lower() == target_lower]
|
||||
|
||||
if score > best_score:
|
||||
if matched_words:
|
||||
# ✅ Centre du SPAN matché, pas de la ligne entière
|
||||
xs = []
|
||||
ys = []
|
||||
for w in matched_words:
|
||||
(xmin, ymin), (xmax, ymax) = w.geometry
|
||||
xs.extend([xmin, xmax])
|
||||
ys.extend([ymin, ymax])
|
||||
cx = (min(xs) + max(xs)) / 2
|
||||
cy = (min(ys) + max(ys)) / 2
|
||||
else:
|
||||
# Fallback de sécurité : centre de la ligne (comportement actuel)
|
||||
box = line_obj.geometry
|
||||
cx = (box[0][0] + box[1][0]) / 2
|
||||
cy = (box[0][1] + box[1][1]) / 2
|
||||
|
||||
best_score = score
|
||||
best_match = {
|
||||
"cx": cx,
|
||||
"cy": cy,
|
||||
"score": score,
|
||||
"line": line_obj.value,
|
||||
"matched_span": " ".join(w.value for w in matched_words) if matched_words else None,
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3. Justification, risques, tests à faire avant merge
|
||||
|
||||
**Pourquoi ça résout le bug** : pour `target='Imagerie'` dans la ligne `"Motif d'admission Examens cliniques Imagerie Notes médicales Synthèse Urgences Codage >"`, `matched_words` capturera uniquement le `Word` `"Imagerie"` (geometry locale), pas tous les words de la ligne. `cx, cy` retomberont au centre exact de ce mot. Idem pour `'Notes médicales'` (2 words contigus) et `'Synthèse Urgences'` (2 words contigus). Plus de collision (0.23, 0.28).
|
||||
|
||||
**Repère identique** : `Word.geometry` est dans le **même repère normalisé** que `line_obj.geometry` (vérifié par doc docTR — cf. [Discussion #570](https://github.com/mindee/doctr/discussions/570) et [io modules](https://mindee.github.io/doctr/modules/io.html)). Aucune conversion d'échelle requise.
|
||||
|
||||
**Risques résiduels** :
|
||||
1. **Casse/accents** : `target_lower in line_lower` puis comparaison `t == w or t in w or w in t` — il faut **normaliser les accents** (NFD + strip diacritics) si `target='Notes médicales'` vs `Word='médicales'` matche, mais `target='Notes medicales'` (sans accent venant du JSON workflow) peut rater. Mitigation : `unicodedata.normalize('NFKD', s).encode('ascii','ignore').decode()` sur les deux côtés avant la comparaison.
|
||||
2. **Tokenisation docTR ≠ split blancs** : docTR sépare typiquement par espace mais peut séparer/grouper différemment des hyphens/apostrophes. Le fallback `matched_words = [w for w in line_obj.words if any(t in w.value.lower() for t in target_tokens)]` couvre ce cas mais peut sur-matcher.
|
||||
3. **Performance** : O(n_words × n_target_tokens) — négligeable (n_words < 50 typiquement).
|
||||
4. **Régressions cosmétiques** : `pre_check_text_match` (DETTE-001) actuellement OFF — à re-tester avec ce fix actif.
|
||||
|
||||
**Tests minimaux avant merge (10 min)** :
|
||||
```bash
|
||||
cd /home/dom/ai/rpa_vision_v3 && source .venv/bin/activate
|
||||
python -c "
|
||||
from agent_v0.server_v1.resolve_engine import _resolve_by_ocr_text
|
||||
img='/home/dom/ai/rpa_vision_v3/visual_workflow_builder/backend/data/anchors/anchor_0438bd2d9bdd_1778161174_full.png'
|
||||
for t in ['Imagerie','Notes médicales','Synthèse Urgences','Codage','Examens cliniques']:
|
||||
r = _resolve_by_ocr_text(img, t, 2560, 1600)
|
||||
print(f'{t:25s} -> cx={r[\"x_pct\"]:.4f} cy={r[\"y_pct\"]:.4f} score={r[\"score\"]:.2f}')
|
||||
"
|
||||
```
|
||||
|
||||
Critère succès : `Imagerie / Notes médicales / Synthèse Urgences` ont des `cx` séparés d'au moins 0.05 (≈ 130 px à 2560 px).
|
||||
|
||||
**À NE PAS faire en chaud démo** (cf. §5 du diagnostic 8 mai). Le quick fix démo reste le timeout client `5 → 30 s`. Ce patch s'applique sur runner 2 (post-démo).
|
||||
|
||||
---
|
||||
|
||||
## 5. Dépendances croisées avec les autres axes
|
||||
|
||||
- **AXE_A5 (tokenisation UI / OmniParser)** : OmniParser utilise PaddleOCR pour l'OCR d'icônes. Si on bascule vers tokenisation OmniParser-style en cascade `1.5` (entre OCR et VLM), il faudra décider **un seul moteur OCR pour tout le pipeline** ou accepter 2 moteurs (docTR pour resolve_engine, PaddleOCR/RapidOCR pour tokenisation). Voir AXE_A5 livrable.
|
||||
- **AXE_B2 (Validator)** : SSIM-ROI proposé §3 alimente directement le composant Validator du Planner-Actor-Validator (cf. SYNTHESE §5.2). C'est le signal sémantique « le click a fait quelque chose dans la zone attendue » qui élimine la classe de bugs « cliqué quelque part, REPORT success=True ».
|
||||
- **DETTE-001** : le patch §4 + SSIM-ROI §3 referment la dette (le pré-check OCR cesse d'être spatialement aveugle parce qu'il vise un span exact, et la vérification post-click se fait sur ROI ciblée).
|
||||
- **Drift exemption ≥ 0.95** : la ratification LightGlue (§2.4) permet de baisser le seuil vers 0.80 sans réintroduire de faux positifs.
|
||||
|
||||
---
|
||||
|
||||
## 6. Sources (chronologie)
|
||||
|
||||
- [docTR — Word/Line geometry — Discussion #570 (2022, valide en 2026)](https://github.com/mindee/doctr/discussions/570)
|
||||
- [docTR — I/O modules (doc officielle)](https://mindee.github.io/doctr/modules/io.html)
|
||||
- [docTR — repo principal (release v0.10, 2026-04)](https://github.com/mindee/doctr)
|
||||
- [docTR — PyPI python-doctr](https://pypi.org/project/python-doctr/)
|
||||
- [PaddleOCR — return_word_box Issue #15760 (2024)](https://github.com/PaddlePaddle/PaddleOCR/issues/15760)
|
||||
- [PaddleOCR 3.0 Technical Report (2025-07)](https://arxiv.org/pdf/2507.05595)
|
||||
- [Surya OCR — datalab-to/surya](https://github.com/datalab-to/surya)
|
||||
- [Surya OCR — PyPI v0.17.1](https://pypi.org/project/surya-ocr/)
|
||||
- [EasyOCR — Character bbox Issue #631](https://github.com/JaidedAI/EasyOCR/issues/631)
|
||||
- [RapidOCR — releases (v3.x, 2026-04-11)](https://github.com/RapidAI/RapidOCR/releases)
|
||||
- [RapidOCR — repo](https://github.com/RapidAI/RapidOCR)
|
||||
- [pytesseract — image_to_data + hOCR (PyPI)](https://pypi.org/project/pytesseract/)
|
||||
- [Codesota — PaddleOCR vs EasyOCR Speed 2025](https://www.codesota.com/ocr/paddleocr-vs-easyocr)
|
||||
- [Codesota — PaddleOCR vs Tesseract vs EasyOCR 2026](https://www.codesota.com/ocr/paddleocr-vs-tesseract)
|
||||
- [Buttondown — PaddleOCR vs EasyOCR vs Doctr Memory & Latency](https://buttondown.com/ckae930413/archive/paddleocr-vs-easyocr-vs-doctr-memory-latency-test/)
|
||||
- [LightGlue — ICCV 2023 paper](https://openaccess.thecvf.com/content/ICCV2023/papers/Lindenberger_LightGlue_Local_Feature_Matching_at_Light_Speed_ICCV_2023_paper.pdf)
|
||||
- [LightGlue — repo cvg/LightGlue](https://github.com/cvg/LightGlue)
|
||||
- [LightGlue ONNX — fabio-sim/LightGlue-ONNX](https://github.com/fabio-sim/LightGlue-ONNX)
|
||||
- [LightGlue — HuggingFace Transformers integration](https://huggingface.co/docs/transformers/model_doc/lightglue)
|
||||
- [Efficient LoFTR — arXiv 2403.04765 (CVPR 2024)](https://arxiv.org/pdf/2403.04765)
|
||||
- [RoMa — CVPR 2024](https://openaccess.thecvf.com/content/CVPR2024/html/Edstedt_RoMa_Robust_Dense_Feature_Matching_CVPR_2024_paper.html)
|
||||
- [RoMa v2 — emergent mind 2025-11](https://www.emergentmind.com/papers/2511.15706)
|
||||
- [DINOv2 — features tutorial](https://www.lightly.ai/blog/dinov2)
|
||||
- [DINO-RotateMatch — arXiv 2512.03715 (2025)](https://arxiv.org/pdf/2512.03715)
|
||||
- [PyImageSearch — Multi-scale Template Matching (2015, ref classique)](https://pyimagesearch.com/2015/01/26/multi-scale-template-matching-using-python-opencv/)
|
||||
- [Medium — Template Matching Beyond Basics: Rotation & Scale Invariant](https://medium.com/@coders.stop/template-matching-beyond-basics-rotation-and-scale-invariant-detection-2ae78d8fa190)
|
||||
- [Eureka — SSIM vs LPIPS](https://eureka.patsnap.com/article/ssim-vs-lpips-which-metric-should-you-trust-for-image-quality-evaluation)
|
||||
- [skimage — structural_similarity doc](https://scikit-image.org/docs/dev/auto_examples/transform/plot_ssim.html)
|
||||
- [Wopee — Screenshot Comparison Algorithms](https://wopee.io/blog/screenshot-comparison-algorithms-visual-testing/)
|
||||
- [Medium — CLIP vs DINOv2 image similarity](https://medium.com/aimonks/clip-vs-dinov2-in-image-similarity-6fa5aa7ed8c6)
|
||||
- [DinoHash — arXiv 2503.11195](https://arxiv.org/pdf/2503.11195)
|
||||
- [OmniParser — DeepWiki OCR module](https://deepwiki.com/microsoft/OmniParser/2.2-ocr-and-image-processing)
|
||||
|
||||
---
|
||||
|
||||
*Document de recherche. Aucun code modifié. Toute application validée par Dom au cas par cas.*
|
||||
318
docs/recherche/AXE_A5_SCREEN_TOKENIZATION.md
Normal file
318
docs/recherche/AXE_A5_SCREEN_TOKENIZATION.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# AXE A5 — Tokenisation d'écran (OmniParser, Set-of-Marks, modèles natifs)
|
||||
|
||||
**Date :** 2026-05-23
|
||||
**Auteur :** Claude (agent dispatché par session principale)
|
||||
**Statut :** lecture seule, recherche pour arbitrage post-démo. Pas d'action proposée.
|
||||
**Axes liés :** A1 (modèles VLM grounding), A4 (OCR/docTR/RapidOCR), CLAUDE.md §asymétrie record/replay.
|
||||
|
||||
---
|
||||
|
||||
## 1. TL;DR
|
||||
|
||||
**Recommandation : Scénario B (log implicite, no risk) maintenant, Scénario A (OmniParser V2 dans la cascade replay) à instruire post-démo, Scénario C (OS-Atlas / GUI-Actor) à benchmarker en R&D mais hors trajectoire courte.**
|
||||
|
||||
**Top finding** : `core/detection/som_engine.py` est **déjà câblé** sur les weights OmniParser (`/home/dom/ai/OmniParser/weights/icon_detect/model.pt`, YOLOv8) côté serveur. Utilisé en **recording** (`stream_processor.py:607-638`) ET déclaré dans une voie `_resolve_set_of_marks` du `resolve_engine.py:1083-1325`. L'asymétrie pointée par CLAUDE.md n'est donc pas "UI-DETR-1 vs rien", elle est "SomEngine activé selon les paths". À vérifier au runtime si `_resolve_set_of_marks` est effectivement appelé en replay — possible code orphelin (cf. champs de mines CLAUDE.md).
|
||||
|
||||
**Trois faits 2025 qui changent l'arbitrage :**
|
||||
1. **OmniParser V2** (publié 12 février 2025, MIT pour le code + Florence-2, **AGPL-3.0 pour les poids YOLOv8 icon_detect**) — 0.6 s/frame A100, 0.8 s/frame RTX 4090, état de l'art ScreenSpot-Pro 39.6 % combiné GPT-4o. Notre RTX 5070 ≈ 12 GB VRAM tient le pipeline complet (icon_detect ~150 MB + Florence-2 base ~750 MB).
|
||||
2. **Set-of-Mark** (Yang et al., arXiv 2310.11441, NeurIPS 2023) — pattern qui sous-tend OmniParser, Magma, ShowUI. Devenu vocabulaire standard du domaine en 2025.
|
||||
3. **Coordinate-free grounding émerge** : GUI-Actor (Microsoft, NeurIPS 2025) et Aguvis (ICML 2025) bypassent le besoin d'un étage de tokenisation séparé. GUI-Actor-7B sur Qwen2.5-VL = 44.6 % ScreenSpot-Pro, > UI-TARS-72B. **Tendance** : la tokenisation explicite OmniParser-style devient un raccourci d'aujourd'hui que les VLM "GUI-natifs" intégreront demain.
|
||||
|
||||
**Verdict honnête pour rpa_vision_v3** : la cause des bugs récents (cf. `REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md`) est **transport HTTP + OCR-DIRECT center-of-line**, pas la cascade vision. Ajouter OmniParser V2 en replay ne réparera **rien** des bugs P0 ouverts. Mais c'est la brique manquante pour le **Validator** (Skyvern-style) et pour un **log de candidats parsés** côté replay (suggestion §4.1 de `INSPIRATION_FRAMEWORKS`).
|
||||
|
||||
---
|
||||
|
||||
## 2. OmniParser V2 — fiche détaillée
|
||||
|
||||
### 2.1. Identité
|
||||
|
||||
- **Auteur** : Microsoft Research
|
||||
- **Repo** : https://github.com/microsoft/OmniParser
|
||||
- **Modèle HF** : https://huggingface.co/microsoft/OmniParser-v2.0
|
||||
- **Release V2** : 12 février 2025
|
||||
- **Stars GitHub mai 2026** : ~22k (cité dans `INSPIRATION_FRAMEWORKS_2026-05-10.md`)
|
||||
- **Article officiel** : https://www.microsoft.com/en-us/research/articles/omniparser-v2-turning-any-llm-into-a-computer-use-agent/
|
||||
|
||||
### 2.2. Architecture interne (2 modèles)
|
||||
|
||||
| Sous-modèle | Rôle | Taille | Backend | Licence |
|
||||
|---|---|---|---|---|
|
||||
| **icon_detect** | YOLOv8 fine-tuné sur dataset Microsoft d'éléments interactifs (boutons, icônes, champs) | ~150 MB | Ultralytics `YOLO` | **AGPL-3.0** ⚠ |
|
||||
| **icon_caption** | Florence-2 base fine-tuné sur 7K paires icon-description annotées GPT-4o | ~750 MB | Transformers | **MIT** |
|
||||
|
||||
**OCR** : OmniParser embarque aussi un OCR (PaddleOCR ou EasyOCR selon config). Notre `som_engine.py` actuel **a remplacé** par docTR (cf. lignes 117-127).
|
||||
|
||||
### 2.3. Format de sortie ("interactable elements")
|
||||
|
||||
Liste de dicts avec :
|
||||
- `bbox` (coordonnées normalisées 0-1)
|
||||
- `interactivity` (true / false)
|
||||
- `content` (caption Florence-2 : "close button", "search field", "OK")
|
||||
- `source` ("box_yolo_content_ocr" / "box_yolo_content_yolo" / "icon")
|
||||
|
||||
Cette sortie est conçue pour être insérée **dans le prompt du VLM principal** (GPT-4o, Claude) sous forme de tableau numéroté → c'est du Set-of-Marks appliqué automatiquement.
|
||||
|
||||
### 2.4. Performance benchmarks
|
||||
|
||||
| Métrique | Valeur | Source |
|
||||
|---|---|---|
|
||||
| ScreenSpot-Pro + GPT-4o | **39.6 %** | Microsoft Research article 2026-02-12 |
|
||||
| ScreenSpot-Pro GPT-4o seul (sans OmniParser) | 0.8 % | idem |
|
||||
| Latence A100 (1 frame) | **0.6 s** | idem |
|
||||
| Latence RTX 4090 (1 frame) | **0.8 s** | idem |
|
||||
| VRAM mini | 4 GB (inference) / 8 GB (recommandé loop agent) | GitHub Issue #31 |
|
||||
| Résolution supportée | 640 → **1920 px** côté long (icon_detect) | doc HF V2.0 |
|
||||
|
||||
**Implication directe rpa_vision_v3** : nos screenshots Léa sont **2560×1600** (RTX 5070). Il faudra **resize** à 1920 px côté long → ratio ~0.75. Toutes les coordonnées YOLO de retour doivent être re-scalées. **Risque** : c'est le même piège que `smart_resize` Qwen2.5-VL bbox_2d (DETTE-006/010/014, `MIGRATION_VLM_PLAN_2026-05-09.md` §2). À traiter explicitement.
|
||||
|
||||
### 2.5. Licence — point sensible commercial
|
||||
|
||||
Combinaison **AGPL-3.0** (icon_detect) + **MIT** (icon_caption + code) :
|
||||
- **AGPL-3.0** sur icon_detect = **interdit en produit commercial fermé** sans rachat de licence YOLOv8 commercial (Ultralytics, ~5 000 $/an estimé), ou remplacement par un détecteur alternatif (Florence-2 task `<OD>`, ou détecteur custom).
|
||||
- Pour rpa_vision_v3 en POC interne / GHT / Anoust : usage actuel défendable. **Pour vente externe : showstopper sur icon_detect tel quel**.
|
||||
|
||||
Le code de notre `som_engine.py` charge directement `/home/dom/ai/OmniParser/weights/icon_detect/model.pt` (AGPL-3.0) → audit licence à reprendre.
|
||||
|
||||
### 2.6. État interne du projet — code existant
|
||||
|
||||
- `core/detection/som_engine.py` (316 lignes, complet) — singleton thread-safe, YOLO + docTR + annotation. Device par défaut `cpu` (cf. `get_shared_engine`).
|
||||
- `core/detection/omniparser_adapter.py` — wrapper plus complet citant Florence-2 caption. **Sépare** du `som_engine.py` simplifié.
|
||||
- `agent_v0/server_v1/stream_processor.py:607-700` — appel SoM au **recording** pour enrichir chaque event click avec l'élément cliqué (cf. UI-DETR-1 dans `INSPIRATION_FRAMEWORKS_2026-05-10.md` §4 — c'est en fait SoM, pas un détecteur tiers).
|
||||
- `agent_v0/server_v1/resolve_engine.py:1083-1325` — voie `_resolve_set_of_marks` au **replay**. Lance SomEngine, fallback template matching anchor↔éléments YOLO. **À tracer au runtime** pour confirmer qu'elle est invoquée.
|
||||
|
||||
→ **L'asymétrie record/replay décrite dans CLAUDE.md n'est peut-être pas l'asymétrie réelle.** À valider explicitement avec Dom au prochain run.
|
||||
|
||||
---
|
||||
|
||||
## 3. Set-of-Marks original
|
||||
|
||||
### 3.1. Identité
|
||||
|
||||
- **Papier** : Yang et al., "Set-of-Mark Prompting Unleashes Extraordinary Visual Grounding in GPT-4V", arXiv 2310.11441 (oct 2023, dernière révision nov 2023)
|
||||
- **Code** : https://github.com/microsoft/SoM
|
||||
- **Lien** : https://arxiv.org/abs/2310.11441
|
||||
- 124 citations (mai 2026)
|
||||
|
||||
### 3.2. Approche
|
||||
|
||||
1. Segmenter l'image en régions sémantiques (SEEM ou SAM, off-the-shelf)
|
||||
2. Overlayer chaque région avec un **mark** (numéro, lettre, masque coloré, bbox)
|
||||
3. Demander au VLM : "click on mark 7" plutôt que "click at (x=420, y=380)"
|
||||
|
||||
Bénéfice : le VLM raisonne en **identifiants discrets** et non en coordonnées continues. Évite le bug des coords mal calibrées (cf. notre DETTE-006 bbox_2d Qwen2.5-VL).
|
||||
|
||||
### 3.3. Résultats clés (papier 2023)
|
||||
|
||||
GPT-4V + SoM en zero-shot **bat le SOTA fine-tuned** sur RefCOCOg (referring expression comprehension). Pour la première fois, un modèle généraliste prompted dépasse les modèles spécialisés sur ce benchmark.
|
||||
|
||||
### 3.4. Complémentarité avec OmniParser
|
||||
|
||||
- **SoM = méthode de prompting** (overlay sur l'image, le VLM répond un ID)
|
||||
- **OmniParser = pipeline complet** (détection + caption + format prompt)
|
||||
- OmniParser = "SoM industrialisé pour les UIs". OmniParser fournit les régions ET les marks ET les captions, contre SoM original qui fournit seulement les marks à partir d'une segmentation SEEM/SAM générique.
|
||||
- Magma (Microsoft, CVPR 2025) entraîne explicitement le modèle sur des images SoM-labellisées → SoM passe de "trick de prompting" à "supervision de pré-entraînement".
|
||||
|
||||
---
|
||||
|
||||
## 4. Modèles VLM qui tokenisent nativement
|
||||
|
||||
### 4.1. OS-Atlas-Base (ICLR 2025)
|
||||
|
||||
- **Repo** : https://github.com/OS-Copilot/OS-Atlas
|
||||
- **Modèles** : OS-Atlas-Base-4B (sur InternVL2-4B) et OS-Atlas-Base-7B (sur Qwen2-VL-7B-Instruct)
|
||||
- **Approche** : entraînement sur **13 M éléments GUI** cross-platform (Windows, Linux, macOS, Android, web). Tokens spéciaux `<|box_start|>(x1,y1),(x2,y2)<|box_end|>`. Coordonnées normalisées 0-1000.
|
||||
- **Tokenise nativement ?** Pas au sens "produit une liste d'éléments en sortie", mais **internalise la grammaire bbox** comme tokens spéciaux. Le modèle apprend que "(124, 380) → (228, 412)" est une bbox, pas une séquence d'entiers libres.
|
||||
- **Coût** : OS-Atlas-Base-7B = ~14 GB FP16, ~7 GB en 4-bit. Compatible RTX 5070 12 GB en 4-bit ou avec Flash-Attn.
|
||||
|
||||
### 4.2. ShowUI-2B (CVPR 2025, Outstanding Paper NeurIPS 2024 workshop)
|
||||
|
||||
- **Repo** : https://github.com/showlab/ShowUI
|
||||
- **HF** : https://huggingface.co/showlab/ShowUI-2B
|
||||
- **Innovation clé** : **UI-Guided Visual Token Selection** — construit un graphe UI au sein du modèle, élimine 33 % des tokens visuels redondants. **5× plus rapide, 2× plus précis** que les VLM généralistes en localisation.
|
||||
- **Perf** : **75.1 % accuracy zero-shot ScreenSpot grounding** avec seulement 2B params + 256K échantillons de training.
|
||||
- **Tokenise nativement ?** Oui, c'est sa nature : la sélection des tokens visuels EST une forme de tokenisation. Pas de pipeline externe.
|
||||
- **Coût** : 2B params → ~4 GB FP16, ~2 GB en 4-bit. **Léger pour notre RTX 5070**.
|
||||
|
||||
### 4.3. Aguvis (ICML 2025)
|
||||
|
||||
- **Repo** : https://aguvis-project.github.io/
|
||||
- **Approche** : framework "pure vision" multi-plateforme avec **inner monologue** structuré. Pipeline 2 étapes (grounding séparé de planification).
|
||||
- **Caractéristique** : premier agent GUI pure-vision entièrement **open-source** (sans dépendance closed-source).
|
||||
|
||||
### 4.4. GUI-Actor (Microsoft, NeurIPS 2025) — **le plus pertinent**
|
||||
|
||||
- **Repo** : https://github.com/microsoft/GUI-Actor
|
||||
- **HF** : https://huggingface.co/microsoft/GUI-Actor-7B-Qwen2.5-VL
|
||||
- **Innovation** : **coordinate-free**. Au lieu de produire "(420, 380)", produit un token `<ACTOR>` qui s'attache via attention aux patches visuels pertinents. Génère plusieurs régions candidates en 1 forward.
|
||||
- **Perf** : GUI-Actor-7B sur Qwen2.5-VL = **44.6 % ScreenSpot-Pro** (> UI-TARS-72B).
|
||||
- **Implication directe** : **tue le bug d'échelle bbox_2d** (DETTE-006/010/014) à la racine. Pas de smart_resize à débugger. Pas de mismatch de résolution.
|
||||
- **Coût** : 7B + Qwen2.5-VL → ~14 GB FP16, ~7 GB 4-bit. Compatible RTX 5070 en 4-bit.
|
||||
|
||||
### 4.5. Magma (Microsoft, CVPR 2025)
|
||||
|
||||
- **Repo** : https://github.com/microsoft/Magma
|
||||
- **Approche** : pré-entraînement multimodal avec **Set-of-Mark labellisation automatique** des éléments cliquables + **Trace-of-Mark** pour la planification d'actions (videos). Premier modèle multi-domaine (GUI + robotique).
|
||||
- **Tokenise nativement ?** Oui, SoM est dans la supervision de pré-entraînement, pas une étape inférence externe.
|
||||
|
||||
---
|
||||
|
||||
## 5. Comparaison latence / perf / robustesse
|
||||
|
||||
### 5.1. Tableau synthèse
|
||||
|
||||
| Approche | Latence / écran 2560×1600 | VRAM | Sortie | Licence | Tokenise nativement ? |
|
||||
|---|---|---|---|---|---|
|
||||
| **UI-DETR-1 record-only (actuel)** | n/a (recording offline) | n/a | événements VWB enrichis | propriétaire MS | non, étape séparée |
|
||||
| **SomEngine actuel** (YOLO icon_detect + docTR) | ~150-300 ms estimé (docTR seul ~100 ms, YOLO ~15 ms, +overhead annotation) | ~1 GB | liste `SomElement` | AGPL-3.0 (YOLO) | non |
|
||||
| **OmniParser V2 complet** (YOLO + Florence-2 caption) | **0.8 s** (RTX 4090) → ~1.0 s (RTX 5070 estimé) après resize 1920 | ~2 GB | liste `interactable_element` + caption sémantique | AGPL-3.0 + MIT | non |
|
||||
| **YOLOv8-UI fine-tuned custom** (sans caption) | ~50-100 ms | ~200 MB | bbox seules | AGPL-3.0 par défaut, MIT possible si from scratch | non |
|
||||
| **ShowUI-2B** | grounding direct, ~1-2 s estimé | ~2-4 GB | coord ou région | code MIT | **oui** (UI-guided token selection) |
|
||||
| **GUI-Actor-7B** | grounding direct, ~2-3 s estimé | ~7-14 GB | régions multiples via attention | code MIT | **oui** (attention head dédiée) |
|
||||
| **OS-Atlas-Base-7B** | grounding direct, ~2-3 s estimé | ~7-14 GB | bbox tokens spéciaux | code MIT (modèle Qwen2-VL Apache 2.0) | partiellement |
|
||||
| **InfiGUI-G1-3B (actuel)** | déjà mesuré ~1-2 s | 3.9 GB | bbox | Apache 2.0 (Qwen) | partiellement |
|
||||
|
||||
### 5.2. Robustesse — ce que dit la littérature 2025
|
||||
|
||||
- **GUI-Robust** (arXiv 2506.14477) : tous les MLLM (y compris GUI-spécialisés) se dégradent significativement sur 7 types d'anomalies (popups, modifications de layout, désactivation). **OmniParser ne corrige PAS ces anomalies à lui seul** — il aide à voir les éléments, pas à comprendre les bugs UI.
|
||||
- **Magma + SoM** (CVPR 2025) : SoM **en supervision** > SoM **en prompting inference-only**. Confirme que la prochaine génération est "SoM natif", pas "OmniParser externe".
|
||||
|
||||
### 5.3. Comparaison concrète avec notre cascade actuelle
|
||||
|
||||
Notre cascade `_resolve_target` (cf. `cartography_execution_flow.md`) :
|
||||
|
||||
```
|
||||
OCR docTR (~200 ms) → template cv2 (~50 ms) → YOLO/SoM optionnel → VLM (1-15 s)
|
||||
```
|
||||
|
||||
Avec SomEngine déjà existant et OmniParser V2 disponible localement (`/home/dom/ai/OmniParser/`), le coût marginal d'ajouter le caption Florence-2 = ~500-700 ms (Florence-2 base). Total cascade enrichie : **~2 s** vs actuel ~1-15 s suivant l'étage qui réussit. **Latence pas un blocker**.
|
||||
|
||||
---
|
||||
|
||||
## 6. Recommandation pour rpa_vision_v3 — 3 scénarios
|
||||
|
||||
### Scénario A — Intégrer OmniParser V2 complet dans la cascade replay
|
||||
|
||||
**Description** : activer `_resolve_set_of_marks` en replay, brancher icon_caption Florence-2 pour enrichir les éléments YOLO avec des descriptions sémantiques, exposer cette "vue parsée" à VWB UI et au VLM principal en prompt.
|
||||
|
||||
**Pour** :
|
||||
- Asymétrie record/replay corrigée explicitement.
|
||||
- Vue parsée disponible pour debug (UI VWB pourrait overlayer les éléments détectés sur captures Léa).
|
||||
- Caption Florence-2 = donnée d'entrée pour un Validator sémantique post-clic (compare "j'attendais Notes médicales" vs "j'ai cliqué sur élément captioned 'Imagerie tab'").
|
||||
- Coût latence ~1 s — compatible démo.
|
||||
|
||||
**Contre** :
|
||||
- **Licence AGPL-3.0** sur icon_detect → audit légal avant déploiement commercial Anoust / GHT vente.
|
||||
- N'efface AUCUN des 5 bugs P0 post-démo (transport HTTP, Stop VWB, mss monitors, échelle pixel, skip ord 13).
|
||||
- Ajoute une dépendance lourde (`/home/dom/ai/OmniParser/`) au runtime.
|
||||
- Le bug primaire diagnostiqué 8 mai est OCR-DIRECT center-of-line, pas un manque de candidats détectés.
|
||||
|
||||
**Effort** : 1-2 j (le code SomEngine existe, manque le câblage Florence-2 caption + UI debug).
|
||||
|
||||
**Risque** : moyen-faible. Tout est déjà en place côté code.
|
||||
|
||||
### Scénario B — Log implicite des candidats (no risk, à faire dès stabilisation post-démo)
|
||||
|
||||
**Description** : à chaque appel `_resolve_target`, logger systématiquement dans `logs/audit/` la liste des candidats produits par CHAQUE étage de la cascade (docTR lignes/spans, template matches > seuil, sorties YOLO si SomEngine actif, sorties VLM). JSON structuré, 1 ligne par résolution.
|
||||
|
||||
**Pour** :
|
||||
- **Zéro nouveau modèle, zéro nouvelle dépendance**. Pure instrumentation.
|
||||
- Vue parsée gratuite, exploitable hors-bande (analyse, dashboard).
|
||||
- Comble l'asymétrie record/replay au sens "trace équivalente".
|
||||
- Préalable indispensable à tout Validator sémantique futur.
|
||||
- Aligné avec la suggestion §4.1 de `INSPIRATION_FRAMEWORKS_2026-05-10.md` ("logger systématiquement la liste des candidats détectés par chaque étage de la cascade").
|
||||
|
||||
**Contre** :
|
||||
- N'apporte aucune nouvelle robustesse en propre — juste de l'observabilité.
|
||||
|
||||
**Effort** : 0.5 j.
|
||||
|
||||
**Risque** : très bas.
|
||||
|
||||
**→ À faire en priorité, indépendamment des autres scénarios.**
|
||||
|
||||
### Scénario C — Remplacer la cascade par un modèle VLM qui tokenise nativement
|
||||
|
||||
**Description** : remplacer `_resolve_target` par un appel direct à GUI-Actor-7B (ou ShowUI-2B en option légère) qui produit la zone-cible en coordinate-free. Peut coexister avec OCR docTR pour validation post-action.
|
||||
|
||||
**Pour** :
|
||||
- **Tue le bug d'échelle bbox_2d** (DETTE-006/010/014) — plus de smart_resize à débugger.
|
||||
- État de l'art ScreenSpot-Pro (44.6 % GUI-Actor > UI-TARS-72B).
|
||||
- Architecture plus simple long-terme : 1 modèle = 1 grounder.
|
||||
- ShowUI-2B = 2× moins gourmand que notre InfiGUI-G1-3B actuel.
|
||||
|
||||
**Contre** :
|
||||
- **Décision structurelle**, pas réversible facilement. Réécrit la moitié de `resolve_engine.py`.
|
||||
- Aucun bench interne pour valider sur nos écrans Easily / Citrix réels.
|
||||
- Pas de caption sémantique = pas de Validator sémantique post-action.
|
||||
- Effort production-grade : 1-2 semaines.
|
||||
|
||||
**Effort** : 5-10 j.
|
||||
|
||||
**Risque** : élevé tant que pas benché sur nos workflows.
|
||||
|
||||
---
|
||||
|
||||
## 7. Recommandation finale
|
||||
|
||||
**Maintenant (post-démo, semaine du 26 mai)** :
|
||||
1. **Scénario B obligatoirement** — log des candidats. Coût minimal, valeur immense pour debug futur.
|
||||
2. **Vérifier au runtime si `_resolve_set_of_marks` est appelé en replay**. Si non = code orphelin (CLAUDE.md §champs de mines). Si oui = on est déjà mi-Scénario A sans le savoir.
|
||||
3. **Audit licence** AGPL-3.0 sur icon_detect avant tout pitch commercial.
|
||||
|
||||
**Sprint suivant (juin si bande passante)** :
|
||||
4. **Scénario A partiel** — activer Florence-2 icon_caption sur l'événement de recording VWB ET sur les résolutions en échec replay (sentinelle, pas systématique). Caption disponible pour un Validator sémantique futur.
|
||||
5. **Bench GUI-Actor-7B et ShowUI-2B** sur 10-20 captures Easily Assure réelles. Décision Scénario C **uniquement** sur preuves.
|
||||
|
||||
**À ne PAS faire** :
|
||||
- Activer OmniParser V2 systématiquement en runtime tant que les 5 bugs P0 (`LESSONS_LEARNED_GHT_2026-05.md`) ne sont pas refermés. Latence supplémentaire sur démo fragile.
|
||||
- Remplacer InfiGUI-G1-3B sans bench comparatif documenté.
|
||||
|
||||
---
|
||||
|
||||
## 8. Sources
|
||||
|
||||
### OmniParser V2
|
||||
- [Microsoft Research — OmniParser V2: Turning Any LLM into a Computer Use Agent (2025-02-12)](https://www.microsoft.com/en-us/research/articles/omniparser-v2-turning-any-llm-into-a-computer-use-agent/)
|
||||
- [GitHub microsoft/OmniParser](https://github.com/microsoft/OmniParser)
|
||||
- [HF microsoft/OmniParser-v2.0](https://huggingface.co/microsoft/OmniParser-v2.0)
|
||||
- [MarkTechPost — Microsoft AI Releases OmniParser V2 (2025-02-18)](https://www.marktechpost.com/2025/02/18/microsoft-ai-releases-omniparser-v2-an-ai-tool-that-turns-any-llm-into-a-computer-use-agent/)
|
||||
- [LearnOpenCV — OmniParser Vision-Based GUI Agent](https://learnopencv.com/omniparser-vision-based-gui-agent/)
|
||||
- [DeepWiki — OmniParser Icon Detection and Captioning Models](https://deepwiki.com/microsoft/OmniParser/2.2-ocr-and-image-processing)
|
||||
|
||||
### Set-of-Marks
|
||||
- [Yang et al. arXiv 2310.11441 — Set-of-Mark Prompting (2023)](https://arxiv.org/abs/2310.11441)
|
||||
- [GitHub microsoft/SoM](https://github.com/microsoft/SoM)
|
||||
|
||||
### Modèles GUI natifs 2025
|
||||
- [arXiv 2410.23218 — OS-ATLAS Foundation Action Model (ICLR 2025)](https://arxiv.org/abs/2410.23218)
|
||||
- [OS-Atlas Homepage](https://osatlas.github.io/)
|
||||
- [GitHub showlab/ShowUI (CVPR 2025)](https://github.com/showlab/ShowUI)
|
||||
- [arXiv 2411.17465 — ShowUI Vision-Language-Action GUI Visual Agent](https://arxiv.org/abs/2411.17465)
|
||||
- [Aguvis Project — ICML 2025](https://aguvis-project.github.io/)
|
||||
- [arXiv 2412.04454 — Aguvis Unified Pure Vision Agents](https://arxiv.org/abs/2412.04454)
|
||||
- [GitHub microsoft/GUI-Actor (NeurIPS 2025)](https://github.com/microsoft/GUI-Actor)
|
||||
- [arXiv 2506.03143 — GUI-Actor Coordinate-Free Grounding](https://arxiv.org/abs/2506.03143)
|
||||
- [GitHub microsoft/Magma (CVPR 2025)](https://github.com/microsoft/Magma)
|
||||
- [arXiv 2502.13130 — Magma Foundation Model](https://arxiv.org/abs/2502.13130)
|
||||
|
||||
### Florence-2 (sous-jacent OmniParser icon_caption)
|
||||
- [HF microsoft/Florence-2-large](https://huggingface.co/microsoft/Florence-2-large)
|
||||
- [Microsoft Research — Florence-2 Unified Representation](https://www.microsoft.com/en-us/research/publication/florence-2-advancing-a-unified-representation-for-a-variety-of-vision-tasks/)
|
||||
|
||||
### Robustesse GUI agents
|
||||
- [arXiv 2506.14477 — GUI-Robust Dataset](https://arxiv.org/abs/2506.14477)
|
||||
|
||||
### Computer use / pure vision
|
||||
- [Skyvern — Browser Automation with LLM and Computer Vision](https://github.com/Skyvern-AI/skyvern)
|
||||
- [Claude Computer Use — Anthropic Docs](https://docs.claude.com/en/docs/agents-and-tools/tool-use/computer-use-tool)
|
||||
- [Tech Insider — Claude Computer Use Citrix Legacy Apps 2026](https://tech-insider.org/anthropic-claude-computer-use-agent-2026/)
|
||||
|
||||
### Référence interne
|
||||
- `core/detection/som_engine.py` (316 lignes) — SomEngine YOLO + docTR déjà câblé
|
||||
- `core/detection/omniparser_adapter.py` — wrapper Florence-2 caption
|
||||
- `agent_v0/server_v1/resolve_engine.py:1083-1325` — voie `_resolve_set_of_marks` au replay
|
||||
- `agent_v0/server_v1/stream_processor.py:607-700` — SoM au recording
|
||||
- `docs/INSPIRATION_FRAMEWORKS_2026-05-10.md` §4 — vague d'inspirations frameworks RPA visuels
|
||||
- `docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` §5.3 — note sur l'asymétrie OmniParser ↔ cascade
|
||||
- `docs/MIGRATION_VLM_PLAN_2026-05-09.md` §2 — bug d'échelle bbox_2d (lien Scénario C)
|
||||
- `docs/LESSONS_LEARNED_GHT_2026-05.md` §🔴 — 5 bugs P0 qui restent prioritaires sur tout ajout d'étage
|
||||
1117
docs/recherche/AXE_B1_DEEP_WATCHDOG.md
Normal file
1117
docs/recherche/AXE_B1_DEEP_WATCHDOG.md
Normal file
File diff suppressed because it is too large
Load Diff
507
docs/recherche/AXE_B1_REPLAY_TRANSPORT.md
Normal file
507
docs/recherche/AXE_B1_REPLAY_TRANSPORT.md
Normal file
@@ -0,0 +1,507 @@
|
||||
# AXE B1 — Refonte du transport replay + watchdog d'orphelins
|
||||
|
||||
**Date :** 2026-05-23
|
||||
**Auteur :** Claude (recherche dispatchée, lecture seule sur code)
|
||||
**Périmètre :** B1 (transport) + B3 (watchdog `_retry_pending`)
|
||||
**Statut :** Étude — aucun changement de code. Pseudo-code prêt à coller.
|
||||
|
||||
> **Lecture pré-requise :** `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` (diagnostic 9 actions perdues en 33 s) et `docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` §4.
|
||||
|
||||
---
|
||||
|
||||
## 1. TL;DR — recommandation
|
||||
|
||||
**Migration cible : SSE (`sse-starlette`).** En complément immédiat, **watchdog `_retry_pending` indépendant** activable sous flag, gardé même après bascule SSE (ceinture+bretelles).
|
||||
|
||||
WebSocket est techniquement supérieur mais coûte 2× plus cher à mettre en place pour un gain marginal vu notre besoin (push serveur → client, l'upload events/screenshots reste sur les endpoints HTTP POST existants, déjà robustes). **L'asymétrie « push descendant lourd / upload léger »** colle exactement à SSE.
|
||||
|
||||
HTTP/2 server push **éliminé** : déprécié au niveau protocole, non supporté par uvicorn, non implémenté côté `requests` Python. Hors course en 2026.
|
||||
|
||||
**Effort estimé :**
|
||||
- Watchdog seul : 2-4 h dev + 1 h test E2E. Déployable indépendamment.
|
||||
- Endpoint SSE serveur + client + bascule progressive : 2 j dev + 1 j test sur Léa Windows + démo de non-régression.
|
||||
- Total reco : **3-4 jours** pour aboutir à un transport robuste.
|
||||
|
||||
**Risque principal :** NoMachine 9.5.7 sur lien LAN peut intercaler un proxy implicite ou couper l'idle TCP. SSE expose `X-Accel-Buffering: no` et un `ping=15` qui couvrent ce cas — WebSocket exigerait un ping/pong applicatif explicite. Avantage SSE.
|
||||
|
||||
---
|
||||
|
||||
## 2. Table comparative
|
||||
|
||||
| Critère | Pull/Poll actuel | **SSE** | WebSocket | HTTP/2 push |
|
||||
|---|---|---|---|---|
|
||||
| **Reconnexion auto** | manuelle | **native (`EventSource`/`sseclient`)** + `Last-Event-ID` | code applicatif | non std |
|
||||
| **Latence push** | 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.*
|
||||
1350
docs/recherche/AXE_B2_DEEP_VALIDATOR.md
Normal file
1350
docs/recherche/AXE_B2_DEEP_VALIDATOR.md
Normal file
File diff suppressed because it is too large
Load Diff
817
docs/recherche/AXE_B2_VALIDATOR_PATTERN.md
Normal file
817
docs/recherche/AXE_B2_VALIDATOR_PATTERN.md
Normal file
@@ -0,0 +1,817 @@
|
||||
# AXE B2 — Pattern Planner-Actor-Validator & validation sémantique post-action
|
||||
|
||||
**Date :** 2026-05-23
|
||||
**Auteur :** agent recherche dispatché (Claude Opus 4.7 1M)
|
||||
**Statut :** livrable de recherche, lecture seule, AUCUNE modification de code
|
||||
**Lien dépendances :** AXE_A4 (OCR), AXE_A5 (tokenisation écran — déjà rédigé), AXE_B4 (ORA observe_reason_act)
|
||||
|
||||
---
|
||||
|
||||
## 1. TL;DR + recommandation
|
||||
|
||||
**Constat.** Skyvern (12k stars, SOTA 85.85 % WebVoyager) formalise le **Validator** comme un agent à part entière, séparé du Planner et de l'Actor. Son rôle : après chaque step, prendre une nouvelle capture, demander à un LLM (avec image + DOM élagué) si l'objectif courant est atteint, sinon renvoyer `continue` / `terminate`. C'est exactement ce qui manque à rpa_vision_v3 : VWB = Planner statique, Léa = Actor, et `replay_verifier.py` est un pixel-diff global qui n'a aucune notion de **sémantique** (« est-ce que l'onglet Imagerie de l'app Easily est maintenant actif ? »).
|
||||
|
||||
Le bug archétype step 10 démo GHT (« Imagerie » cliqué dans le bandeau Edge, REPORT success=True) tient **uniquement** à cette absence : pHash global voit du mouvement → conclut OK. Un Validator visuel par step le détecterait en 1-3 s.
|
||||
|
||||
**Recommandation design pour rpa_vision_v3** (justifiée §6, §9) :
|
||||
|
||||
1. **Garder** `replay_verifier.verify_action` (pixel) comme pré-filtre 10 ms.
|
||||
2. **Réactiver et étendre** `verify_with_critic` déjà câblé (§6) en lui passant un `expected_result` **typé** par action.
|
||||
3. **Ajouter un `Validator` pluggable** côté serveur, qui choisit la stratégie de check selon `action_type` (matrice §5). Implémentation Python = ~250 LOC.
|
||||
4. **Pour le bug step 10 précisément** : `click_anchor` doit déclencher une vérif OCR-ROI **autour du point cliqué** (rayon 60 px) ET une vérif title-bar (déjà fait par `core/grounding/title_verifier.py`). Si la ROI contient le mot Edge / le mot URL / un domaine `.com`, c'est un faux clic → retry, pas continue.
|
||||
5. **Latence cible** : pixel 10 ms, OCR-ROI 100 ms, LLM-judge 2-3 s. Ne lancer le LLM-judge que si pixel **OU** OCR-ROI suspect.
|
||||
|
||||
Le pattern Skyvern est directement adoptable. Le code Skyvern (Python, AGPL-3.0) montre que le Validator c'est **5 prompts Jinja2 + 1 méthode `complete_verify` + 1 dataclass `CompleteVerifyResult`**. Pas plus.
|
||||
|
||||
---
|
||||
|
||||
## 2. Skyvern Validator détaillé (code source 23 mai 2026)
|
||||
|
||||
### 2.1. Méthode `complete_verify` (extraite verbatim de `skyvern/forge/agent.py:2609-2730`)
|
||||
|
||||
Source : <https://github.com/Skyvern-AI/skyvern/blob/main/skyvern/forge/agent.py#L2609>
|
||||
|
||||
Le Validator chez Skyvern n'est pas un sous-processus exotique : c'est **une coroutine LLM** appelée après l'Actor, à chaque step où il n'y a pas déjà une `DecisiveAction` (= action terminale émise par l'Actor lui-même).
|
||||
|
||||
```python
|
||||
# skyvern/forge/agent.py (résumé condensé du flux)
|
||||
async def complete_verify(
|
||||
self, page: Page, scraped_page: ScrapedPage, task: Task, step: Step
|
||||
) -> CompleteVerifyResult:
|
||||
# 1. RE-SCRAPE la page (DOM élagué + screenshots), pas la version utilisée par l'Actor
|
||||
scraped_page_refreshed = await scraped_page.refresh(draw_boxes=False, scroll=scroll)
|
||||
|
||||
# 2. Construit le prompt avec : navigation_goal, payload, complete_criterion,
|
||||
# action_history, elements parsés, datetime
|
||||
template_name = "check-user-goal-with-termination" if use_termination_prompt else "check-user-goal"
|
||||
verification_prompt = load_prompt_with_elements(
|
||||
element_tree_builder=scraped_page_refreshed,
|
||||
template_name=template_name,
|
||||
navigation_goal=task.navigation_goal,
|
||||
navigation_payload=task.navigation_payload,
|
||||
complete_criterion=task.complete_criterion,
|
||||
terminate_criterion=task.terminate_criterion,
|
||||
action_history=actions_and_results_str,
|
||||
local_datetime=...,
|
||||
)
|
||||
|
||||
# 3. Appel LLM avec screenshots — un handler LLM dédié possible
|
||||
# via flag PostHog USE_CHECK_USER_GOAL_HANDLER_FOR_VERIFICATION
|
||||
verification_result = await llm_api_handler(
|
||||
prompt=verification_prompt,
|
||||
step=step,
|
||||
screenshots=scraped_page_refreshed.screenshots,
|
||||
prompt_name=prompt_name,
|
||||
)
|
||||
|
||||
# 4. Parse JSON strict → 3 verdicts possibles
|
||||
result = CompleteVerifyResult.model_validate(verification_result)
|
||||
if result.is_complete:
|
||||
verification_status = VerificationStatus.complete
|
||||
elif result.is_terminate:
|
||||
verification_status = VerificationStatus.terminate
|
||||
else:
|
||||
verification_status = VerificationStatus.continue_step
|
||||
|
||||
# 5. Trace OTEL : verification.status, verification.template, verification.reasoning_kind
|
||||
span.set_attribute("verification.status", verification_status.value)
|
||||
record_verification_span_attrs(span, result.thoughts)
|
||||
return result
|
||||
```
|
||||
|
||||
**Trois verdicts uniquement** : `complete` / `terminate` / `continue_step`. Pas de `success_partial` ni de `retry_silent`. C'est volontaire : la décision est forcée binaire.
|
||||
|
||||
Le `check_user_goal_complete` (lignes 2736+) wrap `complete_verify` et le convertit en `CompleteAction` ou `TerminateAction` pour l'orchestrateur.
|
||||
|
||||
### 2.2. Le prompt `check-user-goal.j2` (verbatim, fetch direct du repo)
|
||||
|
||||
Source : <https://raw.githubusercontent.com/Skyvern-AI/skyvern/main/skyvern/forge/prompts/skyvern/check-user-goal.j2>
|
||||
|
||||
```jinja
|
||||
Your are here to help the user determine if the user has completed their goal on the web{{ " according to the complete criterion" if complete_criterion else "" }}. Use the content of the elements parsed from the page,{{ "" if without_screenshots else " the screenshots of the page," }} the user goal and user details to determine whether the {{ "complete criterion has been met" if complete_criterion else "user goal has been completed" }} or not.
|
||||
|
||||
Make sure to ONLY return the JSON object in this format with no additional text before or after it:
|
||||
{
|
||||
"page_info": str, // Think step by step. Describe all the useful information in the page related to the user goal.
|
||||
"thoughts": str, // Think step by step. What information makes you believe whether user goal has completed or not. Use information you see on the site to explain.
|
||||
"user_goal_achieved": bool // True if the user goal has been completed, false otherwise.
|
||||
}
|
||||
|
||||
User Goal:
|
||||
{{ navigation_goal }}
|
||||
|
||||
User Details:
|
||||
{{ navigation_payload }}
|
||||
|
||||
Action History:
|
||||
{{ action_history }}
|
||||
|
||||
Elements on the page:
|
||||
{{ elements }}
|
||||
|
||||
Current datetime, ISO format:
|
||||
{{ local_datetime }}
|
||||
```
|
||||
|
||||
**Points clés** :
|
||||
- Sortie JSON stricte, parsée par Pydantic `CompleteVerifyResult.model_validate`.
|
||||
- Trois infos données au modèle : (a) screenshots, (b) elements parsés du DOM, (c) action_history textuelle. Multi-modal.
|
||||
- Le `page_info` → `thoughts` → `user_goal_achieved` impose une chain-of-thought structurée. C'est ce qui rend l'erreur diagnosticable.
|
||||
|
||||
### 2.3. Le prompt `check-user-goal-with-termination.j2` (expérimental, verbatim)
|
||||
|
||||
Source : <https://raw.githubusercontent.com/Skyvern-AI/skyvern/main/skyvern/forge/prompts/skyvern/check-user-goal-with-termination.j2>
|
||||
|
||||
Ajoute un 3e statut explicite `terminate` + une **classification des échecs** en 12 catégories :
|
||||
|
||||
```jinja
|
||||
"status": str, // Must be one of three values: "complete", "terminate", or "continue".
|
||||
"failure_categories": array // Only populate when status is "terminate". Classify the root cause.
|
||||
[{
|
||||
"category": str, // ANTI_BOT_DETECTION | BROWSER_ERROR | NAVIGATION_FAILURE |
|
||||
// PAGE_LOAD_TIMEOUT | AUTH_FAILURE | LLM_REASONING_ERROR |
|
||||
// CREDENTIAL_ERROR | ELEMENT_NOT_FOUND | WRONG_PAGE_STATE |
|
||||
// DATA_EXTRACTION_FAILURE | INFRASTRUCTURE_ERROR | UNKNOWN
|
||||
"confidence_float": float,
|
||||
"reasoning": str
|
||||
}]
|
||||
|
||||
Important: Think carefully about the difference between "terminate" and "continue":
|
||||
- "terminate" = impossible to achieve, stop trying
|
||||
- "continue" = not done yet, but achievable with more steps
|
||||
```
|
||||
|
||||
**À retenir** : Skyvern est très conservateur sur `terminate` (« only when CLEAR, EXPLICIT, UNAMBIGUOUS evidence »). C'est aligné avec le feedback `feedback_failure_is_learning.md` de Dom : échec ≠ stop avec erreur, c'est pause supervisée.
|
||||
|
||||
### 2.4. Quand le Validator se déclenche
|
||||
|
||||
Extrait `agent.py:1929-1971` :
|
||||
|
||||
```python
|
||||
enable_parallel_verification = False
|
||||
if (
|
||||
not has_decisive_action # l'Actor n'a pas déjà émis un COMPLETE
|
||||
and not task_completes_on_download
|
||||
and not isinstance(task_block, ActionBlock)
|
||||
and complete_verification # flag global activable par-task
|
||||
and (task.navigation_goal or task.complete_criterion)
|
||||
):
|
||||
# Géré par feature flag PostHog
|
||||
disable_user_goal_check = await app.EXPERIMENTATION_PROVIDER.is_feature_enabled_cached(
|
||||
"DISABLE_USER_GOAL_CHECK",
|
||||
task.task_id,
|
||||
...
|
||||
)
|
||||
enable_parallel_verification = not disable_user_goal_check
|
||||
```
|
||||
|
||||
→ **Le Validator tourne à CHAQUE step** par défaut (« deferred to handle_completed_step »). C'est désactivable par task ou globalement, mais l'état par défaut est ON. Skyvern accepte le coût LLM par step parce qu'un faux succès rend l'agent inutilisable.
|
||||
|
||||
### 2.5. Contrat de données
|
||||
|
||||
```python
|
||||
# skyvern/forge/sdk/schemas/tasks.py (déduit du code agent.py)
|
||||
class CompleteVerifyResult(BaseModel):
|
||||
page_info: str
|
||||
thoughts: str
|
||||
is_complete: bool
|
||||
is_terminate: bool = False
|
||||
status: str | None = None # "complete" | "terminate" | "continue"
|
||||
failure_categories: list[FailureCategory] = []
|
||||
```
|
||||
|
||||
### 2.6. Latence et coût
|
||||
|
||||
D'après le post Skyvern 2.0 (<https://www.skyvern.com/blog/skyvern-2-0-state-of-the-art-web-navigation-with-85-8-on-webvoyager-eval/>) :
|
||||
|
||||
- Un step moyen prend 2-10 s.
|
||||
- Validator = appel LLM séparé (souvent un GPT-4o-mini ou Claude Haiku), 1-3 s.
|
||||
- ROI = sans Validator, accuracy 68.7 % WebVoyager ; avec Validator, **85.85 %**. Le delta de +17 points en accuracy justifie largement la latence.
|
||||
|
||||
Source : <https://browser-use.com/posts/our-browser-agent-evaluation-system> (browser-use rapporte +17 pts également : 45 → 68.7 → 85.85 selon Planner/Validator).
|
||||
|
||||
---
|
||||
|
||||
## 3. Tour d'horizon Validator dans 5 autres frameworks
|
||||
|
||||
### 3.1. OpenAdapt — Evaluation-Driven Feedback
|
||||
|
||||
Source : <https://github.com/OpenAdaptAI/OpenAdapt/wiki/OpenAdapt-Architecture-(draft)>, <https://github.com/OpenAdaptAI/openadapt-evals>.
|
||||
|
||||
OpenAdapt formalise le concept au niveau **Process Graph** (graphe de steps avec arêtes = critères de complétion) :
|
||||
|
||||
- **Code-based validation** : LLM génère du Python qui vérifie une condition d'état (présence d'un message de confirmation, état d'un bouton, etc.). Code stocké, ré-exécuté à chaque replay.
|
||||
- **Model-based validation** : LMM (Large Multimodal Model) reçoit le screenshot courant + `completion_criteria` formulés en langage naturel → bool.
|
||||
|
||||
Particularité : si la validation échoue, OpenAdapt **bascule en mode recording** automatiquement → l'utilisateur démontre la suite → la trace devient training data. C'est l'« Evaluation-Driven Feedback ». Le sous-package `openadapt-evals` expose `evaluate_agent_on_benchmark`.
|
||||
|
||||
### 3.2. browser-use — agentic judge
|
||||
|
||||
Source : <https://browser-use.com/posts/our-browser-agent-evaluation-system>, <https://github.com/browser-use/browser-use>.
|
||||
|
||||
- LLM judge intégré dans le code agent, **tourne après `done`** ET « can also double as a real-time validation layer during regular use ».
|
||||
- Modèle : `gemini-2.5-flash`. Accuracy juge vs labels humains : 87 %.
|
||||
- Sortie JSON stricte :
|
||||
|
||||
```json
|
||||
{
|
||||
"reasoning": "Analysis covering what worked, failures, trajectory quality, tool usage, output quality",
|
||||
"verdict": "true|false",
|
||||
"failure_reason": "Max 5 sentences explanation if failed",
|
||||
"impossible_task": "true|false",
|
||||
"reached_captcha": "true|false"
|
||||
}
|
||||
```
|
||||
|
||||
- Philosophie : **simple prompts and absolute True/False verdicts work best**. Complex rubrics → indecisive judging.
|
||||
|
||||
### 3.3. Anthropic Computer Use
|
||||
|
||||
Source : <https://docs.anthropic.com/en/docs/build-with-claude/computer-use>.
|
||||
|
||||
Anthropic CU n'a pas de Validator nommé. Boucle minimaliste : `screenshot → action → screenshot → ...` jusqu'à ce que Claude lui-même décide qu'il a fini. **Validation = self-reflection implicite du modèle dans son raisonnement**.
|
||||
|
||||
→ Acceptable parce que Claude est puissant. **Pas applicable à rpa_vision_v3** où l'Actor n'est pas un LLM agentique mais un exécutant déterministe (Léa). Il faut un Validator externe.
|
||||
|
||||
### 3.4. OpenAI Operator / CUA
|
||||
|
||||
Source : <https://openai.com/index/operator-system-card/>.
|
||||
|
||||
Idem Anthropic CU : pas de Validator séparé. Le modèle CUA fait perception → reasoning → action en boucle. Selon le system card : « If it encounters challenges or makes mistakes, Operator can leverage its reasoning capabilities to self-correct ». Pas formalisé.
|
||||
|
||||
OpenCUA (open-source, <https://opencua.xlang.ai/>) entraîne avec « reflective Chain-of-Thought reasoning » mais pas de check externe.
|
||||
|
||||
### 3.5. Cradle (BAAI, Kunlun Tech) — Self-Reflection module
|
||||
|
||||
Source : <https://github.com/BAAI-Agents/Cradle>, <https://arxiv.org/pdf/2403.03186>.
|
||||
|
||||
Cradle décompose explicitement en 6 modules dont **Self-Reflection** :
|
||||
> « Through this module, the agent assesses previous actions to understand their outcomes, evaluate successes or failures, and adjust behavior accordingly. »
|
||||
|
||||
Mesure : +20.41 points sur tâches « professional domain » vs baselines. Mais c'est un agent jeu/applications, pas RPA déclaratif → moins directement transposable.
|
||||
|
||||
### 3.6. Tableau récap
|
||||
|
||||
| Framework | Validator nommé ? | Modalité | Modèle | Latence | Verdict format |
|
||||
|---|---|---|---|---|---|
|
||||
| Skyvern 2.0 | **Oui** (`complete_verify`) | VLM + DOM élagué | GPT-4o ou handler dédié | 1-3 s | JSON `is_complete/is_terminate/status` |
|
||||
| OpenAdapt | Oui (Process Graph) | LMM ou Python généré | Configurable | n/a | bool + falls back to recording |
|
||||
| browser-use | Oui (agentic judge) | VLM + DOM | gemini-2.5-flash | 1-2 s | JSON `verdict/failure_reason` |
|
||||
| Anthropic CU | Non (implicite) | Self-reflection | Claude lui-même | inclus | continuation libre |
|
||||
| OpenAI Operator | Non (implicite) | Self-reflection | CUA | inclus | continuation libre |
|
||||
| Cradle | Oui (Self-Reflection) | LMM | GPT-4V | 2-5 s | text reasoning |
|
||||
|
||||
**Convergence forte** : les 3 frameworks RPA matures (Skyvern, OpenAdapt, browser-use) ont un Validator **explicite, JSON-strict, multi-modal (VLM + structure DOM)**. Les agents généralistes (CU, Operator) délèguent au LLM agentique. Pour rpa_vision_v3 avec Actor déterministe = camp Skyvern.
|
||||
|
||||
---
|
||||
|
||||
## 4. Taxonomie des approches de validation post-action
|
||||
|
||||
| Approche | Coût | Précision | Faux-positifs | Quand l'utiliser |
|
||||
|---|---|---|---|---|
|
||||
| **A. LLM-as-judge (full VLM)** | 1-5 s | Très haute (sémantique) | Faibles | Validation finale de step / cas ambigus |
|
||||
| **B. OCR ROI** (texte attendu autour du clic) | 80-200 ms | Haute si texte connu | Sensible OCR errors | Tabs, boutons, libellés |
|
||||
| **C. OCR title-bar** (titre fenêtre) | ~120 ms (déjà câblé) | Moyenne | Bruit OCR sur petits crops | Navigation fenêtre / ouverture appli |
|
||||
| **D. Visual diff pHash global** | 10 ms | Très basse (juste « ça a bougé ») | Énormes | Pré-filtre `nothing-happened` |
|
||||
| **E. Visual diff pHash ROI** | 20 ms | Moyenne | Moyens | Détection focus tab (changement souligné) |
|
||||
| **F. CLIP features cos-sim** | 50-200 ms | Moyenne | Confond visuellement proches | Reconnaissance d'écran connu |
|
||||
| **G. DINOv2 features** | 100-300 ms | Haute (self-supervised, plus robuste que CLIP) | Faibles | Comparaison patches précis |
|
||||
| **H. LPIPS** | 100 ms | Haute (perceptual) | Moyens | Vérif après animations / transitions |
|
||||
| **I. Window-focus check** (win32 API ou OCR titlebar) | <50 ms | Très haute | Quasi nuls | Vérif que la bonne app est devant |
|
||||
| **J. Dialog presence detect** | OCR + template | Très haute | Faibles | Détection popups bloquantes |
|
||||
| **K. JSON schema validation** (extraction) | <10 ms | Déterministe | nuls | `extract_text`, `t2a_decision` |
|
||||
|
||||
**Source visual diff** : <https://wopee.io/blog/screenshot-comparison-algorithms-visual-testing/> — pHash est positionné comme « pre-filter, not a comparator ». Les VLM sont positionnés comme « triage layer on top of pixel diffs, not as the comparator itself ». Exactement le design pixel→sémantique déjà câblé dans `replay_verifier.verify_with_critic`.
|
||||
|
||||
**Pour DINOv2 / LPIPS / CLIP** : sources <https://github.com/facebookresearch/dinov2>, <https://medium.com/aimonks/clip-vs-dinov2-in-image-similarity-6fa5aa7ed8c6>. DINOv2 produit des features visuelles plus discriminantes que CLIP pour comparer deux crops d'UI (CLIP est entraîné texte↔image, pas pour le pixel-perfect).
|
||||
|
||||
---
|
||||
|
||||
## 5. Matrice type d'action → check recommandé pour rpa_vision_v3
|
||||
|
||||
Aligné avec `reference_vwb_action_types.md` (memory) et `_ALLOWED_ACTION_TYPES` de `replay_engine.py`.
|
||||
|
||||
| Action VWB (Léa) | Check primaire | Check secondaire (si primaire ambigu) | Budget latence |
|
||||
|---|---|---|---|
|
||||
| `click_anchor` → `click` | **B. OCR ROI** (rayon 60 px) + **I. Window focus** | A. LLM-as-judge si OCR ne trouve pas le label | 100 ms + 2 s si escalation |
|
||||
| `double_click_anchor` → `click button="double"` | **C. OCR title-bar** (déjà câblé) + **B. OCR ROI** | A. LLM-as-judge | 200 ms + 2 s |
|
||||
| `right_click_anchor` → `click button="right"` | **J. Dialog presence** (menu contextuel attendu) | B. OCR ROI sur menu | 150 ms |
|
||||
| `type_text` → `type` | **B. OCR ROI** : le texte tapé est-il visible dans la ROI ? | A. LLM-as-judge si texte tronqué | 100 ms |
|
||||
| `type_secret` | **D. pHash ROI** (vérifier qu'un input s'est rempli, pas le contenu) | — | 20 ms |
|
||||
| `keyboard_shortcut` → `key_combo` | **C. OCR title-bar** OU **J. Dialog presence** selon raccourci | A. LLM-as-judge en cas de doute | 200 ms |
|
||||
| `scroll_to_anchor` → `scroll` | **F. CLIP cos-sim** before/after ROI cible visible | D. pHash global change ≠ 0 | 100 ms |
|
||||
| `wait_for_anchor` → `wait` | **B. OCR ROI** : l'ancre est-elle visible ? | A. LLM-as-judge | 100 ms |
|
||||
| `extract_text` | **K. JSON schema** : type str, longueur > 0, langue fr ratio | A. LLM-as-judge sur le contenu plausibilité | 10 ms + 2 s si plausibilité requise |
|
||||
| `extract_text_scroll` | K + **A. LLM-as-judge** si plusieurs pages | — | 10 ms + 2 s |
|
||||
| `extract_table` | **K. JSON schema** : ≥ 1 row, headers attendus si fournis | A. LLM-as-judge | 10 ms |
|
||||
| `screenshot_evidence` | — (action passive) | I. Window focus | <50 ms |
|
||||
| `t2a_decision` | **K. JSON schema** strict (decision ∈ {UHCD, FORFAIT, NA}, JSON parseable) | — | 10 ms |
|
||||
| `pause_for_human` | **Checklist QW4** (déjà fait, `SafetyChecksProvider`) | — | n/a |
|
||||
| `db_save_data` | **K. Schema row sauvée** (SELECT verify) | — | <50 ms |
|
||||
| `import_excel`, `db_read_data` | **K. Schema rows** | — | <50 ms |
|
||||
| `visual_condition` | **A. LLM-as-judge** sur la condition formulée | — | 2 s |
|
||||
| `ai_ocr`, `ai_summarize`, etc. | **K. JSON schema** + **A. plausibilité** | — | 10 ms + 2 s |
|
||||
|
||||
**Principe directeur** : la plupart des actions ont un check pas-cher (OCR ROI, JSON) qui suffit dans 90 % des cas. Le LLM-as-judge (2 s) ne tire qu'en escalation, ou sur les actions à risque élevé (`click_anchor` sur cibles ambiguës, `t2a_decision`, `visual_condition`).
|
||||
|
||||
---
|
||||
|
||||
## 6. Design d'un Validator pluggable — code copy-paste-ready
|
||||
|
||||
### 6.1. Interface
|
||||
|
||||
À placer dans `agent_v0/server_v1/validator.py` (nouveau fichier, complète `replay_verifier.py` existant) :
|
||||
|
||||
```python
|
||||
# agent_v0/server_v1/validator.py
|
||||
"""
|
||||
Validator — vérification sémantique post-action pluggable.
|
||||
|
||||
Inspiré de Skyvern (Planner-Actor-Validator). Combine pixel-diff existant
|
||||
(replay_verifier.py) avec une couche sémantique typée par action_type.
|
||||
|
||||
Trois verdicts possibles, calque sur Skyvern :
|
||||
- COMPLETE → l'action a eu l'effet voulu, passer au step suivant
|
||||
- CONTINUE → l'effet n'est pas encore visible, re-vérifier après wait
|
||||
- TERMINATE → l'action a échoué de manière irrécupérable (pause supervisée)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, Optional, Protocol
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Verdict(str, Enum):
|
||||
COMPLETE = "complete"
|
||||
CONTINUE = "continue"
|
||||
TERMINATE = "terminate"
|
||||
|
||||
|
||||
class FailureCategory(str, Enum):
|
||||
WRONG_TARGET = "wrong_target" # cliqué ailleurs (ex. bug step 10)
|
||||
NO_VISUAL_CHANGE = "no_visual_change" # action sans effet
|
||||
UNEXPECTED_DIALOG = "unexpected_dialog" # popup bloque
|
||||
WRONG_APPLICATION = "wrong_application" # focus sur mauvaise app (Edge vs Easily)
|
||||
OCR_TEXT_MISSING = "ocr_text_missing" # texte attendu absent
|
||||
SCHEMA_INVALID = "schema_invalid" # JSON/extract invalide
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationResult:
|
||||
verdict: Verdict
|
||||
confidence: float # 0.0-1.0
|
||||
check_used: str # "ocr_roi" | "llm_judge" | "title_bar" | ...
|
||||
elapsed_ms: float
|
||||
reasoning: str = ""
|
||||
failure_category: Optional[FailureCategory] = None
|
||||
raw_evidence: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"verdict": self.verdict.value,
|
||||
"confidence": round(self.confidence, 3),
|
||||
"check_used": self.check_used,
|
||||
"elapsed_ms": round(self.elapsed_ms, 1),
|
||||
"reasoning": self.reasoning,
|
||||
"failure_category": self.failure_category.value if self.failure_category else None,
|
||||
"raw_evidence": self.raw_evidence,
|
||||
}
|
||||
|
||||
|
||||
class ActionChecker(Protocol):
|
||||
"""Contrat d'un checker spécifique par action_type."""
|
||||
name: str
|
||||
budget_ms: float
|
||||
|
||||
def check(
|
||||
self,
|
||||
action: Dict[str, Any],
|
||||
result: Dict[str, Any],
|
||||
screenshot_before: Optional[str],
|
||||
screenshot_after: Optional[str],
|
||||
context: Dict[str, Any],
|
||||
) -> ValidationResult: ...
|
||||
|
||||
|
||||
class Validator:
|
||||
"""Orchestrateur : route action_type → checker, gère l'escalation."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
checkers: Dict[str, list[ActionChecker]],
|
||||
default_checker: ActionChecker,
|
||||
escalation_checker: Optional[ActionChecker] = None,
|
||||
escalation_threshold: float = 0.5,
|
||||
):
|
||||
"""
|
||||
checkers: mapping action_type → liste de checkers à essayer en ordre.
|
||||
default_checker: fallback si action_type pas dans le mapping.
|
||||
escalation_checker: typiquement un LLM-as-judge, lancé si confidence < seuil.
|
||||
"""
|
||||
self._checkers = checkers
|
||||
self._default = default_checker
|
||||
self._escalation = escalation_checker
|
||||
self._escalation_threshold = escalation_threshold
|
||||
|
||||
def validate(
|
||||
self,
|
||||
action: Dict[str, Any],
|
||||
result: Dict[str, Any],
|
||||
screenshot_before: Optional[str] = None,
|
||||
screenshot_after: Optional[str] = None,
|
||||
context: Optional[Dict[str, Any]] = None,
|
||||
) -> ValidationResult:
|
||||
context = context or {}
|
||||
action_type = action.get("type", "")
|
||||
|
||||
candidates = self._checkers.get(action_type, [self._default])
|
||||
|
||||
last_result: Optional[ValidationResult] = None
|
||||
for checker in candidates:
|
||||
res = checker.check(action, result, screenshot_before, screenshot_after, context)
|
||||
last_result = res
|
||||
# Si verdict net + confiance haute → renvoyer
|
||||
if res.confidence >= self._escalation_threshold and res.verdict != Verdict.CONTINUE:
|
||||
return res
|
||||
|
||||
# Escalation LLM-as-judge si fourni
|
||||
if self._escalation and last_result and last_result.confidence < self._escalation_threshold:
|
||||
logger.info(
|
||||
"Validator escalation LLM-judge (last_conf=%.2f, check=%s)",
|
||||
last_result.confidence, last_result.check_used,
|
||||
)
|
||||
esc = self._escalation.check(action, result, screenshot_before, screenshot_after, context)
|
||||
# On combine : si LLM contredit, LLM prime (sa confiance est bornée à 0.9)
|
||||
return esc
|
||||
|
||||
return last_result or ValidationResult(
|
||||
verdict=Verdict.CONTINUE,
|
||||
confidence=0.3,
|
||||
check_used="no_checker",
|
||||
elapsed_ms=0.0,
|
||||
reasoning="Aucun checker n'a produit de verdict",
|
||||
)
|
||||
```
|
||||
|
||||
### 6.2. Exemple de checker : `OcrRoiChecker` (pour click_anchor)
|
||||
|
||||
```python
|
||||
# agent_v0/server_v1/checkers/ocr_roi.py
|
||||
import time
|
||||
from typing import Any, Dict, Optional
|
||||
from PIL import Image
|
||||
|
||||
from agent_v0.server_v1.validator import (
|
||||
ActionChecker, ValidationResult, Verdict, FailureCategory,
|
||||
)
|
||||
|
||||
|
||||
class OcrRoiChecker:
|
||||
"""Vérifie que le texte attendu apparaît dans la ROI autour du clic.
|
||||
|
||||
Spécifiquement conçu pour résoudre le bug step 10 :
|
||||
si on a cliqué sur 'Imagerie', la ROI 60px doit contenir 'Imagerie'.
|
||||
Si elle contient 'Edge' ou 'urgence.labs.laurinebazin.design',
|
||||
on a cliqué dans le bandeau navigateur → failure.
|
||||
"""
|
||||
name = "ocr_roi"
|
||||
budget_ms = 200.0
|
||||
|
||||
# Mots suspects = on a cliqué hors-app
|
||||
SUSPECT_TOKENS = {"edge", "chrome", "firefox", "http", "https", ".com", ".fr",
|
||||
"favoris", "favorite", "onglet", "tab "}
|
||||
|
||||
def __init__(self, ocr_fn, radius_px: int = 60):
|
||||
self._ocr = ocr_fn # callable(PIL.Image) -> str
|
||||
self._radius = radius_px
|
||||
|
||||
def check(self, action, result, screenshot_before, screenshot_after, context) -> ValidationResult:
|
||||
t0 = time.time()
|
||||
expected_text = action.get("by_text") or context.get("expected_text", "")
|
||||
x_pct = action.get("x_pct")
|
||||
y_pct = action.get("y_pct")
|
||||
|
||||
if not screenshot_after or x_pct is None or y_pct is None:
|
||||
return ValidationResult(
|
||||
verdict=Verdict.CONTINUE, confidence=0.2,
|
||||
check_used=self.name, elapsed_ms=(time.time() - t0) * 1000,
|
||||
reasoning="ROI indéfinie (pas de coords ou pas de screenshot)",
|
||||
)
|
||||
|
||||
img = self._load_image(screenshot_after)
|
||||
w, h = img.size
|
||||
cx, cy = int(x_pct * w), int(y_pct * h)
|
||||
r = self._radius
|
||||
roi = img.crop((max(0, cx - r), max(0, cy - r), min(w, cx + r), min(h, cy + r)))
|
||||
|
||||
text = (self._ocr(roi) or "").lower()
|
||||
expected_lower = expected_text.lower().strip()
|
||||
|
||||
elapsed_ms = (time.time() - t0) * 1000
|
||||
|
||||
# 1) Vérif : un token suspect (navigateur) dans la ROI → faux clic
|
||||
for suspect in self.SUSPECT_TOKENS:
|
||||
if suspect in text and suspect not in expected_lower:
|
||||
return ValidationResult(
|
||||
verdict=Verdict.TERMINATE, confidence=0.85,
|
||||
check_used=self.name, elapsed_ms=elapsed_ms,
|
||||
failure_category=FailureCategory.WRONG_APPLICATION,
|
||||
reasoning=f"Token navigateur '{suspect}' dans ROI clic — cible probablement hors-app",
|
||||
raw_evidence={"roi_text": text[:200], "expected": expected_lower},
|
||||
)
|
||||
|
||||
# 2) Vérif : le texte attendu est dans la ROI ?
|
||||
if expected_lower and expected_lower in text:
|
||||
return ValidationResult(
|
||||
verdict=Verdict.COMPLETE, confidence=0.9,
|
||||
check_used=self.name, elapsed_ms=elapsed_ms,
|
||||
reasoning=f"Texte '{expected_lower[:40]}' trouvé dans ROI",
|
||||
raw_evidence={"roi_text": text[:200]},
|
||||
)
|
||||
|
||||
# 3) Pas trouvé mais pas suspect non plus → confiance basse, escalation
|
||||
return ValidationResult(
|
||||
verdict=Verdict.CONTINUE, confidence=0.4,
|
||||
check_used=self.name, elapsed_ms=elapsed_ms,
|
||||
failure_category=FailureCategory.OCR_TEXT_MISSING,
|
||||
reasoning=f"Texte '{expected_lower[:40]}' non trouvé dans ROI",
|
||||
raw_evidence={"roi_text": text[:200]},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _load_image(source: str) -> Image.Image:
|
||||
# Délégué à replay_verifier._load_single_image, ou copy-paste équivalent
|
||||
from agent_v0.server_v1.replay_verifier import ReplayVerifier
|
||||
return ReplayVerifier()._load_single_image(source)
|
||||
```
|
||||
|
||||
### 6.3. Intégration avec `replay_verifier.py` existant
|
||||
|
||||
Le `replay_verifier.verify_with_critic` couvre déjà 80 % du besoin LLM-as-judge (étape sémantique VLM). Il suffit de :
|
||||
|
||||
1. Le wrapper dans un `LlmJudgeChecker` qui implémente `ActionChecker`.
|
||||
2. L'utiliser comme `escalation_checker` du `Validator`.
|
||||
|
||||
```python
|
||||
# agent_v0/server_v1/checkers/llm_judge.py
|
||||
import time
|
||||
from agent_v0.server_v1.replay_verifier import ReplayVerifier
|
||||
from agent_v0.server_v1.validator import (
|
||||
ActionChecker, ValidationResult, Verdict, FailureCategory,
|
||||
)
|
||||
|
||||
class LlmJudgeChecker:
|
||||
"""Wrapper autour de ReplayVerifier.verify_with_critic (VLM gemma4)."""
|
||||
name = "llm_judge"
|
||||
budget_ms = 3000.0
|
||||
|
||||
def __init__(self, verifier: ReplayVerifier):
|
||||
self._verifier = verifier
|
||||
|
||||
def check(self, action, result, screenshot_before, screenshot_after, context) -> ValidationResult:
|
||||
t0 = time.time()
|
||||
expected = context.get("expected_result", "")
|
||||
intention = context.get("action_intention", "")
|
||||
workflow_ctx = context.get("workflow_context", "")
|
||||
|
||||
critic = self._verifier.verify_with_critic(
|
||||
action=action, result=result,
|
||||
screenshot_before=screenshot_before,
|
||||
screenshot_after=screenshot_after,
|
||||
expected_result=expected,
|
||||
action_intention=intention,
|
||||
workflow_context=workflow_ctx,
|
||||
)
|
||||
elapsed_ms = (time.time() - t0) * 1000
|
||||
|
||||
if critic.semantic_verified is True:
|
||||
verdict = Verdict.COMPLETE
|
||||
conf = max(critic.confidence, 0.7)
|
||||
elif critic.semantic_verified is False:
|
||||
verdict = Verdict.TERMINATE
|
||||
conf = 0.8
|
||||
else:
|
||||
verdict = Verdict.CONTINUE
|
||||
conf = 0.4
|
||||
|
||||
return ValidationResult(
|
||||
verdict=verdict, confidence=conf,
|
||||
check_used=self.name, elapsed_ms=elapsed_ms,
|
||||
reasoning=critic.semantic_detail or critic.detail,
|
||||
raw_evidence={"pixel_change_pct": critic.change_area_pct,
|
||||
"semantic_verified": critic.semantic_verified},
|
||||
)
|
||||
```
|
||||
|
||||
### 6.4. Câblage côté `api_stream.py` (post-action)
|
||||
|
||||
Pseudo-diff (NE PAS appliquer, juste pour montrer le point d'insertion) :
|
||||
|
||||
```python
|
||||
# agent_v0/server_v1/api_stream.py — handler de REPORT
|
||||
from agent_v0.server_v1.validator import Validator, Verdict
|
||||
from agent_v0.server_v1.checkers.ocr_roi import OcrRoiChecker
|
||||
from agent_v0.server_v1.checkers.llm_judge import LlmJudgeChecker
|
||||
|
||||
# Init au boot
|
||||
_validator = Validator(
|
||||
checkers={
|
||||
"click": [OcrRoiChecker(ocr_fn=_easyocr_fn)],
|
||||
"type": [OcrRoiChecker(ocr_fn=_easyocr_fn)],
|
||||
"key_combo": [TitleBarChecker()], # voir core/grounding/title_verifier.py
|
||||
# ...
|
||||
},
|
||||
default_checker=PixelDiffChecker(), # wrapper ReplayVerifier.verify_action
|
||||
escalation_checker=LlmJudgeChecker(ReplayVerifier()),
|
||||
escalation_threshold=0.55,
|
||||
)
|
||||
|
||||
# Dans report_action_result, après le pixel-diff actuel
|
||||
async def report_action_result(payload):
|
||||
...
|
||||
if RPA_VALIDATOR_ENABLED: # kill-switch env var
|
||||
val = _validator.validate(
|
||||
action=action, result=result,
|
||||
screenshot_before=before, screenshot_after=after,
|
||||
context={"expected_text": action.get("by_text"),
|
||||
"expected_result": step.get("expected_result", ""),
|
||||
"action_intention": step.get("label", ""),
|
||||
"workflow_context": f"step {step_idx}/{total_steps}"},
|
||||
)
|
||||
if val.verdict == Verdict.TERMINATE:
|
||||
# Pause supervisée, pas stop avec error (cf. feedback_failure_is_learning)
|
||||
_enter_paused_state(reason=val.reasoning, evidence=val.to_dict())
|
||||
elif val.verdict == Verdict.CONTINUE:
|
||||
# Re-vérifier après wait, ou retry
|
||||
_schedule_recheck(action_id, after_ms=1500)
|
||||
# COMPLETE → continue normalement
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Application au bug step 10 démo GHT
|
||||
|
||||
**Rappel du bug** (cf. `REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md`) : step 10 « cliquer onglet Imagerie », OCR-DIRECT renvoie centre de la rangée de tabs → le clic tombe **dans la URL bar Edge** (au-dessus). pHash global voit du changement → REPORT success=True. Cascade.
|
||||
|
||||
**Avec le Validator proposé** :
|
||||
|
||||
1. Action `click_anchor` (`by_text="Imagerie"`, `x_pct=0.23`, `y_pct=0.28`).
|
||||
2. Léa rapporte success après mouseclick. Screenshot_after capturé.
|
||||
3. `Validator.validate(action_type="click", ...)` route vers `OcrRoiChecker`.
|
||||
4. ROI 60 px autour de (0.23, 0.28) → réellement la URL bar.
|
||||
5. EasyOCR du crop renvoie texte type : `« urgence.labs.laurinebazin.design/aiva-urgence/dossier.html#imagerie »`
|
||||
6. Token `.com` ou `https` détecté → **`Verdict.TERMINATE`** avec `FailureCategory.WRONG_APPLICATION`.
|
||||
7. Reasoning : « Token navigateur 'https' dans ROI clic — cible probablement hors-app ».
|
||||
8. `api_stream` entre en pause supervisée avec `evidence={roi_text, expected}`. Dom voit dans le dashboard ce qui s'est mal passé. Pas d'enchainement vers step 11.
|
||||
|
||||
**Latence ajoutée** : 100-200 ms (EasyOCR sur 120×120 px). **Négligeable** vs. les 6 s passés à enchaîner 5 steps faux et à entrer en pause supervisée 33 s plus tard.
|
||||
|
||||
**Effet secondaire bénéfique** : le même mécanisme attrape :
|
||||
- Clics sur popups Windows (Hello / UAC) → ROI contient « Sécurité Windows » → TERMINATE.
|
||||
- Clics sur le menu démarrer ou la barre des tâches.
|
||||
- Tout clic qui tombe dans une zone système non prévue.
|
||||
|
||||
---
|
||||
|
||||
## 8. Budget latence par check — qu'accepter en démo ?
|
||||
|
||||
Hypothèse démo GHT (40 steps, 2 min de pipeline cible) :
|
||||
|
||||
| Check | Latence | × 40 steps | Acceptable démo ? |
|
||||
|---|---|---|---|
|
||||
| Pixel diff global (existant) | 10 ms | 0.4 s | ✅ ON par défaut |
|
||||
| OCR ROI EasyOCR | 100-200 ms | 4-8 s | ✅ ON sur `click`, `type` |
|
||||
| OCR title-bar (existant) | 120 ms | 4.8 s | ✅ ON sur navigation |
|
||||
| Schema validation (JSON) | <10 ms | 0.4 s | ✅ ON sur `extract_*`, `t2a_decision` |
|
||||
| LLM-judge gemma4 critic | 2-3 s | 80-120 s | ⚠️ SEULEMENT en escalation |
|
||||
| LLM-judge cloud (Claude Haiku) | 1-2 s | 40-80 s | ⚠️ SEULEMENT en escalation |
|
||||
| DINOv2 features ROI | 150 ms | 6 s | ❓ pas nécessaire pour démo |
|
||||
|
||||
**Recommandation budget** :
|
||||
|
||||
- Démo : pixel + OCR ROI + title-bar + schema = ~10 s de latence cumulée sur 40 steps. Acceptable.
|
||||
- LLM-judge escalation déclenché ~5 fois max par démo = 10 s ajoutés. Tolérable si placé sur les steps à risque (clics ambigus sur tabs).
|
||||
- DINOv2 hors-périmètre démo. À benchmarker post-démo.
|
||||
|
||||
**Kill-switch** obligatoire (cf. QW Suite Mai, conventions Dom) :
|
||||
```bash
|
||||
RPA_VALIDATOR_ENABLED=true # active la couche entière
|
||||
RPA_VALIDATOR_LLM_JUDGE_ENABLED=true # active escalation LLM (coûteuse)
|
||||
RPA_VALIDATOR_OCR_ROI_RADIUS=60 # tunable
|
||||
RPA_VALIDATOR_ESCALATION_THRESHOLD=0.55
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Plan d'intégration gradué
|
||||
|
||||
### 9.1. Court terme — 1 jour, faisable avant prochaine démo (P0)
|
||||
|
||||
**But** : éliminer la classe « clic hors-app silencieusement success=True ».
|
||||
|
||||
1. Créer `agent_v0/server_v1/validator.py` (squelette §6.1) — 1 h.
|
||||
2. Créer `OcrRoiChecker` (§6.2) — 2 h.
|
||||
3. Wrapper `LlmJudgeChecker` autour de `verify_with_critic` existant (§6.3) — 30 min.
|
||||
4. Ajouter hook dans `api_stream.report_action_result` derrière `RPA_VALIDATOR_ENABLED=false` par défaut — 2 h.
|
||||
5. Tests :
|
||||
- Unit : ROI text matching, suspect tokens, escalation logic — 2 h.
|
||||
- Integration : rejouer step 10 sur fixture screenshot — 1 h.
|
||||
6. Démo interne avec `RPA_VALIDATOR_ENABLED=true` sur Demo_urgence_3_db, mesure latence + faux positifs — 1 h.
|
||||
|
||||
**Livrable** : pas de régression démo si flag off ; quand on, le bug step 10 est attrapé en TERMINATE → pause supervisée.
|
||||
|
||||
### 9.2. Moyen terme — 1-2 semaines (P1)
|
||||
|
||||
**But** : matrice complète action → check (§5).
|
||||
|
||||
1. `TitleBarChecker` adapté de `core/grounding/title_verifier.py` existant — 2 h.
|
||||
2. `JsonSchemaChecker` pour `extract_text`, `t2a_decision`, `extract_table` — 4 h.
|
||||
3. `DialogPresenceChecker` réutilisant la cascade de modaux VM (`feedback_phash_vs_dialog_in_vm.md`) — 4 h.
|
||||
4. `PixelDiffChecker` (wrapper de l'existant) avec verdict adapté au contrat Verdict — 2 h.
|
||||
5. Câblage de la matrice complète selon §5 — 4 h.
|
||||
6. Dashboard : panneau « Validator stats » par session — pourcentage COMPLETE / CONTINUE / TERMINATE, top failure_categories — 1 j.
|
||||
|
||||
### 9.3. Long terme — post-démo (P2)
|
||||
|
||||
1. Évaluer **DINOv2** vs OCR ROI sur fixtures GHT : meilleur signal pour distinguer « tab activé vs tab survolé » ? Bench 100 steps.
|
||||
2. Migration LLM-judge de gemma4:e4b (local) vers un handler dédié — séparer le « LLM décisionnel T2A » du « LLM judge ». Skyvern expose `USE_CHECK_USER_GOAL_HANDLER_FOR_VERIFICATION` qui sépare déjà.
|
||||
3. Apprentissage : enregistrer dans `TargetMemoryStore` chaque verdict TERMINATE pour produire du training data (pattern OpenAdapt « success traces become new training data »).
|
||||
4. Re-planification : si TERMINATE répété → renvoyer info au Planner pour ajuster le workflow (cf. Skyvern « reporting any errors / tweaks back to the Planner so it can make adjustments in real-time »). Pour rpa_vision_v3 : signaler à VWB que l'ancre est foireuse → suggestion recapture.
|
||||
|
||||
---
|
||||
|
||||
## 10. Sources avec liens
|
||||
|
||||
### Skyvern (Planner-Actor-Validator)
|
||||
|
||||
- Blog Skyvern 2.0 — <https://www.skyvern.com/blog/skyvern-2-0-state-of-the-art-web-navigation-with-85-8-on-webvoyager-eval/> (annonce de l'archi Planner-Actor-Validator, score WebVoyager 85.85 %)
|
||||
- GitHub repo — <https://github.com/Skyvern-AI/skyvern>
|
||||
- `agent.py` (méthode `complete_verify`) — <https://github.com/Skyvern-AI/skyvern/blob/main/skyvern/forge/agent.py> ligne 2609 (au 23 mai 2026)
|
||||
- Prompt `check-user-goal.j2` — <https://raw.githubusercontent.com/Skyvern-AI/skyvern/main/skyvern/forge/prompts/skyvern/check-user-goal.j2>
|
||||
- Prompt `check-user-goal-with-termination.j2` — <https://raw.githubusercontent.com/Skyvern-AI/skyvern/main/skyvern/forge/prompts/skyvern/check-user-goal-with-termination.j2>
|
||||
- Prompt `decisive-criterion-validate.j2` — <https://raw.githubusercontent.com/Skyvern-AI/skyvern/main/skyvern/forge/prompts/skyvern/decisive-criterion-validate.j2>
|
||||
- Hacker News show — <https://news.ycombinator.com/item?id=42724616>
|
||||
|
||||
### browser-use (agentic judge)
|
||||
|
||||
- Blog « Our browser agent evaluation system » — <https://browser-use.com/posts/our-browser-agent-evaluation-system>
|
||||
- AGENTS.md — <https://github.com/browser-use/browser-use/blob/main/AGENTS.md>
|
||||
|
||||
### OpenAdapt (Evaluation-Driven Feedback)
|
||||
|
||||
- GitHub OpenAdapt — <https://github.com/OpenAdaptAI/OpenAdapt>
|
||||
- GitHub openadapt-evals — <https://github.com/OpenAdaptAI/openadapt-evals>
|
||||
- Wiki architecture — <https://github.com/OpenAdaptAI/OpenAdapt/wiki/OpenAdapt-Architecture-(draft)>
|
||||
|
||||
### Anthropic Computer Use & OpenAI Operator
|
||||
|
||||
- Operator system card — <https://openai.com/index/operator-system-card/>
|
||||
- OpenCUA (open foundations CUA, xLANG / HKU) — <https://opencua.xlang.ai/>
|
||||
- Computer Use 2026 review — <https://tech-insider.org/anthropic-claude-computer-use-agent-2026/>
|
||||
|
||||
### Cradle (BAAI)
|
||||
|
||||
- Paper arXiv 2403.03186 — <https://arxiv.org/pdf/2403.03186>
|
||||
- GitHub — <https://github.com/BAAI-Agents/Cradle>
|
||||
- Project page — <https://baai-agents.github.io/Cradle/>
|
||||
|
||||
### Visual diff / VLM-as-judge / LLM-as-judge
|
||||
|
||||
- « Screenshot Comparison Algorithms » — <https://wopee.io/blog/screenshot-comparison-algorithms-visual-testing/> (pHash positionné comme pre-filter, VLM comme triage layer)
|
||||
- DINOv2 (Meta) — <https://github.com/facebookresearch/dinov2>
|
||||
- CLIP vs DINOv2 image similarity — <https://medium.com/aimonks/clip-vs-dinov2-in-image-similarity-6fa5aa7ed8c6>
|
||||
- « Aha Moment Revisited: Are VLMs Truly Capable of Self Verification » (arXiv 2506.17417) — <https://arxiv.org/pdf/2506.17417>
|
||||
- Vision-Language Model Verifier (review) — <https://www.emergentmind.com/topics/vision-language-model-vlm-verifier>
|
||||
- LLM-as-a-Judge guide 2026 — <https://labelyourdata.com/articles/llm-as-a-judge>
|
||||
- « Why Success is Lying to You: The 2026 Agent Eval Stack » — <https://micheallanham.substack.com/p/why-success-is-lying-to-you-the-2026>
|
||||
|
||||
### EDDOps (Evaluation-Driven Development & Operations)
|
||||
|
||||
- Paper arXiv 2411.13768 (v3, 2026) — <https://arxiv.org/html/2411.13768v3>
|
||||
|
||||
### Doc interne rpa_vision_v3 (référencée)
|
||||
|
||||
- `docs/INSPIRATION_FRAMEWORKS_2026-05-10.md` §3.1 — Planner-Actor-Validator
|
||||
- `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` — bug archétype step 10
|
||||
- `docs/BUG_PRECHECK_SPATIAL_BLINDNESS_2026-05-08.md` — DETTE-001
|
||||
- `agent_v0/server_v1/replay_verifier.py` — `verify_with_critic` déjà câblé
|
||||
- `core/grounding/title_verifier.py` — TitleVerifier déjà câblé
|
||||
- Memory `reference_vwb_action_types.md` — matrice action_types VWB
|
||||
|
||||
---
|
||||
|
||||
## 11. Dépendances avec autres axes
|
||||
|
||||
- **AXE_A4 (OCR)** : `OcrRoiChecker` repose sur EasyOCR/docTR rapides. Si AXE_A4 livre un OCR ROI < 100 ms calibré sur petits crops, le check primaire devient ultra-fiable. **Bloquant** : qualité OCR sur crop 120×120 px.
|
||||
- **AXE_A5 (tokenisation écran)** : si on a un parseur UI type OmniParser qui renvoie une liste d'éléments avec bbox + label, le check ROI devient déterministe (matche `target == element_at_point(cx, cy).label`). **Forte synergie** : un Validator + un tokenizer = on rentre dans le territoire Skyvern 2.0.
|
||||
- **AXE_B4 (ORA)** : ORA peut consommer les `ValidationResult` du Validator comme signal d'observation. Si TERMINATE → ORA ré-observe et propose une re-action. Le Validator devient l'œil de l'Actor.
|
||||
- **DETTE-008** (pre-check VLM par-clic désactivé par `if False:`) : ce Validator est sa version refaite-proprement. La désactivation actuelle est juste, mais le besoin reste — c'est ce livrable.
|
||||
- **DETTE-001** (pre-check OCR spatialement aveugle) : `OcrRoiChecker` avec `radius_px=60` est exactement l'Option B mentionnée dans la note de Dom. Réduire radius + bboxes individuelles = même direction.
|
||||
|
||||
---
|
||||
|
||||
*Document de recherche, lecture seule. Aucune décision d'implémentation prise par cet axe — décision relève de Dom et d'un planning de réintégration coordonné avec AXE_A4, A5, B4.*
|
||||
259
docs/recherche/AXE_B4_ORA_VS_REPLAY.md
Normal file
259
docs/recherche/AXE_B4_ORA_VS_REPLAY.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# AXE B4 — Agents GUI autonomes vs replay déclaratif : où placer le curseur pour rpa_vision_v3 ?
|
||||
|
||||
**Date** : 2026-05-23
|
||||
**Auteur** : Claude (agent dispatché, recherche prospective)
|
||||
**Statut** : Note de cadrage. Pas d'action de code. Décision Dom requise.
|
||||
**Périmètre** : état de l'art 2025-2026 des frameworks computer-use / GUI agents, en miroir de l'architecture actuelle (replay VWB déclaratif + Léa Windows + cascade OCR/template/VLM).
|
||||
|
||||
---
|
||||
|
||||
## 1. TL;DR et recommandation
|
||||
|
||||
**Insight central** : entre mars 2025 et mai 2026, l'autonomie GUI a fait un bond brutal. Les benchmarks de référence (OSWorld-Verified, WindowsAgentArena, ScreenSpot-Pro) sont passés de ~38 % (CUA d'OpenAI, fin 2024) à **>80 %** (Holo3, Claude Mythos Preview, Agent S3 Behavior Best-of-N) en moins d'un an, dépassant ou rivalisant avec le baseline humain expert (~72 %). Mais ces scores ne disent rien des deux contraintes qui dictent notre choix : **latence par step** (10-30 s pour les modèles autonomes contre <2 s pour le replay cache-hit) et **coût d'inférence cloud** (rédhibitoire pour un déploiement healthtech on-premise).
|
||||
|
||||
**Recommandation 3-6 mois** : **rester sur l'axe replay déclaratif amélioré**, mais ouvrir un **bac à sable autonomous "Copilot"** sur le pattern Skyvern "Planner-Actor-Validator" (cf. §3) câblé sur le module ORA existant. Concrètement :
|
||||
|
||||
1. **Fermer la dette transport** (HTTP → SSE/WebSocket, cf. SYNTHESE_TECHNOS §5.1) avant toute escalade vers l'autonome — sinon on bâtit un agent autonome sur un transport qui perd 9 actions sur 33 s.
|
||||
2. **Réactiver le pre-check ORA `if False:` ligne 1705** uniquement en mode "Copilot supervisé" (toggle par workflow), pas en autonome silencieux. C'est le pas le plus court vers l'échelle Skyvern niveau Validator-as-component, dont notre dette est explicite (`feedback_phash_vs_dialog_in_vm.md`).
|
||||
3. **Adopter explicitement le vocabulaire Shadow → Copilot → Autonomous** comme palier produit, avec des métriques de bascule mesurables (success rate ≥ 95 %, intervention rate < 1 step sur 20) issues de la littérature (Turian, SAFe-Copilot, cf. §5).
|
||||
4. **Ne PAS courir derrière Holo3 ou Claude Mythos** : ces modèles sont SOTA en autonomie mais cloud-only ou >35B params. Notre contrainte VRAM 12 GB et notre exigence on-premise les excluent.
|
||||
|
||||
**Dépendances directes** :
|
||||
- **AXE B2 Validator** : prérequis. Sans Validator sémantique solide, le mode Copilot ne peut pas détecter ses échecs → boucle d'erreur sans recovery. Le pattern Reflexion (§4) ne fonctionne que si l'évaluateur est fiable.
|
||||
- **AXE C apprentissage** : `TargetMemoryStore` (Phase 1 du PLAN_APPRENTISSAGE_LEA) devient le fondement d'une "memory tier" type Letta/MemGPT pour le mode Copilot. Brancher la mémoire AVANT toute escalade autonome.
|
||||
|
||||
---
|
||||
|
||||
## 2. Table comparative — frameworks GUI agents autonomes mai 2026
|
||||
|
||||
| Framework / Modèle | OSWorld-Verified | WindowsAgentArena | ScreenSpot-Pro | Latence/step (estim.) | On-prem ? | Licence | Notes |
|
||||
|---|---:|---:|---:|---:|:---:|---|---|
|
||||
| **Claude Sonnet 5** (Anthropic CU) | **88.3%** | n/a (CU générique) | n/a | 10-30 s (LLM agentic) | ❌ cloud only | propriétaire | Dépasse human baseline 72.4 %. API "computer use" tool. Coût ~$5/$25 par MTok |
|
||||
| **Claude Opus 4.7** | 78.0% | n/a | n/a | 10-30 s | ❌ | propriétaire | Successeur 4.6 (72.7 %). |
|
||||
| **Holo3-122B-A10B** (H Company) | 78.85% (mars) | n/a | n/a | n/a | ⚠ Apache 2.0 mais 10B actifs / 122B totaux | Apache 2.0 | MoE desktop-spécialisé, sort proprio |
|
||||
| **Holo3-35B-A3B** | **82.6%** (avril) | n/a | n/a | n/a | ⚠ 3B actifs / 35B totaux | Apache 2.0 | SOTA leaderboard fin avril 2026 |
|
||||
| **GPT-5.4 / OpenAI CUA** | 75.0% | n/a | **85.4%** (SS-Pro) | 10-20 s | ❌ cloud only | propriétaire | Computer Use tool API tiers 3-5, $3/$12 MTok |
|
||||
| **Agent S3** (Simular) | 66% (100 steps) / 72.6% (Best-of-N) | n/a | n/a | LLM-dépendant | ✅ orchestrateur open | Apache 2.0 | Compose any VLM (Claude/GPT/local) |
|
||||
| **Agent S2** (Simular) | 34.5% (50 steps) | +52.8% vs SOTA prec. | n/a | LLM-dépendant | ✅ | Apache 2.0 | Generalist-Specialist framework |
|
||||
| **UI-TARS-2** (ByteDance) | 47.5% | 50.6% | n/a | end-to-end, ~5 s GPU local | ✅ open weights | Apache 2.0 | 7B params, déployable local. Multi-turn RL |
|
||||
| **Magma** (Microsoft) | n/a (focus robotique + GUI) | n/a | n/a | n/a | ✅ open | MIT | Foundation model SoM/ToM, 39M samples. Pas de score OSWorld direct. |
|
||||
| **OS-Atlas-Pro-7B** | n/a | n/a | strong (focus grounding) | <2 s GPU local | ✅ open weights | Apache 2.0 | 3 modes : Grounding / Action / Agent |
|
||||
| **Skyvern v2** | n/a (browser-only) | n/a (browser) | n/a | Agent: ~5 s/step ; Script: 10-100× plus rapide | ✅ self-host | AGPL-3.0 | WebVoyager 85.85%. Dual mode agent/script |
|
||||
| **browser-use v2** | n/a (browser) | n/a | n/a | LLM-dépendant | ✅ self-host | MIT | 78k★ GitHub. Reasoning loop pure |
|
||||
| **Cradle** (BAAI) | OSWorld testé | n/a | n/a | élevé (6 modules) | ✅ open | Apache 2.0 | 6 modules : Info Gather, Self-Reflection, Task Inference, Skill Curation, Action Planning, Memory |
|
||||
| **AppAgent v2** (Tencent) | mobile-focused | n/a | n/a | n/a | ✅ open | MIT | Combine parser + visuel, flexible action space |
|
||||
| **OS-Genesis** (Shanghai AI Lab) | training pipeline | n/a | n/a | n/a | ✅ open (ACL 2025) | Apache 2.0 | **Reverse Task Synthesis** — pertinent pour Shadow→Copilot, cf. §5 |
|
||||
|
||||
**Lecture critique** :
|
||||
- Le **plafond verre des 85 %** sur OSWorld est dépassé par les cloud SOTA (Claude Sonnet 5, Holo3). Mais on parle de tâches **simples** type ouvrir LibreOffice, modifier un fichier. RIEN sur OSWorld ne ressemble à Easily Assure (UI métier propriétaire dans Edge/Citrix, 22+ steps, T2A médical).
|
||||
- Les modèles **vraiment on-premise <8B** (UI-TARS-2, OS-Atlas-Pro) plafonnent à **47-50 %** sur OSWorld — performance insuffisante pour de l'autonomie en production healthtech.
|
||||
- **WindowsAgentArena** reste le benchmark le plus proche de notre cible (154 tâches Windows multi-app). Score de référence UI-TARS-2 = **50.6 %**. À retenir : aucun modèle <100B ne dépasse 60 % sur WAA en mai 2026.
|
||||
- **OSWorld-Human** (arxiv 2506.16042) montre que **les meilleurs agents prennent 2.7 à 4.3× plus de steps que nécessaire**, et que chaque step successif peut prendre **3× plus longtemps** que le premier. Le coût latence n'est pas linéaire — il explose en fin de tâche.
|
||||
|
||||
---
|
||||
|
||||
## 3. L'échelle d'abstraction — 4 paliers, où on est, où aller
|
||||
|
||||
Reprise du §2.3 d'INSPIRATION_FRAMEWORKS_2026-05-10.md, instrumentée avec les benchmarks 2026.
|
||||
|
||||
| Palier | Description | Exemples framework | Robustesse cible | Latence/step | Coût LLM | Notre position |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **L1 — Replay déclaratif pur** | Workflow recorded → rejoué step par step. Aucun raisonnement runtime. | UiPath classique, TagUI, **Skyvern Script Mode** (cache) | Très haute si UI stable, fragile sur changement | <500 ms (resolve memory hit) à ~2 s (VLM grounding) | ~0 (un appel VLM si miss) | **C'est ici qu'on opère.** VWB = Planner statique, cascade = Grounding |
|
||||
| **L2 — Replay avec runtime fallback** | Replay déclaratif + fallback intelligent quand un step échoue : retry visuel, re-grounding, escalade VLM | **Skyvern dual mode** (script + agent fallback), Anthropic Computer Use en mode "tool" | Haute, dégradation gracieuse | 2-5 s en moyenne, pic 15 s au fallback | Faible (fallback rare) | **Cible 3-6 mois**. Le pre-check ORA `if False:` ligne 1705 est l'opportunité d'amorçage |
|
||||
| **L3 — Autonomous avec checkpoint** | Plan dynamique + Validator post-step + ability de re-planifier. Human-on-the-Loop. | **Skyvern Agent Mode v2** (Planner-Actor-Validator), **Cradle** (6 modules), **Agent S2/S3**, **MGA observation-centric** | Moyenne, dépend du Validator | 5-15 s/step | Significatif (validator + replan) | **Cible 12-18 mois**, après AXE B2 Validator solide |
|
||||
| **L4 — Autonomous full** | Goal → décomposition + exécution + recovery sans intervention humaine. Human-out-of-the-Loop. | **Claude CU**, **OpenAI CUA**, **Holo3** end-to-end | Variable — SOTA 88 % sur tâches simples, chute sur UI métier propriétaire | 10-30 s/step | Élevé (cloud) ou très VRAM-gourmand (local 35B+) | **Hors périmètre POC santé**. Risque juridique RGPD/AI Act, coût cloud, instabilité UI Easily |
|
||||
|
||||
**Position critique** : OpenAdapt, Skyvern, OmniParser et **toute la littérature 2026** convergent sur l'idée que **L1 → L2 est le saut le plus rentable**. L'écart L2 → L3 demande un Validator robuste qui n'existe pas encore chez nous (pHash global insuffisant, cf. bug step 10 du diagnostic 8 mai). L'écart L3 → L4 demande des modèles qu'on n'a pas (cloud only) ou qu'on ne peut pas servir (>35B params).
|
||||
|
||||
---
|
||||
|
||||
## 4. Recovery patterns 2026 — lequel adopter
|
||||
|
||||
Quatre familles de patterns dominent en 2026. Classés par robustesse vs effort d'implémentation chez nous.
|
||||
|
||||
| Pattern | Principe | Effort impl. | Robustesse | Recommandé pour rpa_vision_v3 ? |
|
||||
|---|---|---|---|---|
|
||||
| **Retry immédiat** | Refaire la même action 1-3 fois avec back-off | Trivial | Faible (n'aide pas si cause structurelle) | ✅ déjà partiellement en place, OK |
|
||||
| **Backtrack agent** (BacktrackAgent arxiv 2505.20660) | Verifier + Judger en pipeline. Si fail détecté → rollback step n, retry avec stratégie alternative | Moyen | Haute si Verifier solide | ⚠ utile, mais nécessite Verifier sémantique = AXE B2 |
|
||||
| **Reflexion** (NeurIPS 2023, Shinn et al.) | Verbal RL : LLM observe son échec, génère feedback texte stocké en mémoire épisodique, ré-essaie en lisant ce feedback | Élevé (Actor + Evaluator + Self-Reflection) | Très haute en long-horizon, surcoût LLM élevé | ❌ pas avant L3. Surcoût LLM rédhibitoire sur démo répétitive |
|
||||
| **Checkpoint + idempotency** (Agent DR 2026) | Checkpoint après chaque step validé, replay depuis le dernier checkpoint sain. Idempotency keys au scope task | Moyen | Très haute pour tâches state-mutating | ✅ **Pertinent pour T2A** : checkpoint après chaque ord validé, reprise depuis là si crash |
|
||||
| **Pause supervisée** (Human-on-the-Loop) | À la moindre détection d'anomalie : pause, demande validation humaine, reprend ou abandonne | Faible | Très haute (humain = oracle) | ✅ **Cohérent avec `feedback_failure_is_learning.md`** ("échec clic = pause supervisée, pas stop avec error"). DÉJÀ NOTRE PATTERN |
|
||||
| **Observation-centric (MGA)** | Closed loop observe-plan-act-verify ; "occlusion signals + failure clusters" déclenchent replan explicite | Moyen-élevé | Bonne en GUI dynamique | ⚠ pertinent pour Citrix/popups mais nécessite OmniParser-like |
|
||||
|
||||
**Recommandation** : combiner **(1) Pause supervisée** (déjà notre devise) + **(2) Checkpoint+idempotency au niveau workflow VWB** (chaque ord T2A = un checkpoint, reprise possible sans réexécution amont). Bonus : ces deux patterns sont **vendables** au pitch healthtech (sécurité, traçabilité). Reflexion et Backtrack agent restent en R&D pour AXE C.
|
||||
|
||||
---
|
||||
|
||||
## 5. Cycle Shadow → Copilot → Autonomous — état de la littérature
|
||||
|
||||
### 5.1 Qui le formalise ?
|
||||
|
||||
Le triptyque est **largement adopté en 2026** mais sous des noms variables :
|
||||
|
||||
- **Microsoft Copilot vs Agent vs Autonomous** (Microsoft 2026 Copilot Update, mai 2026) : trois layers explicites — "human-in-the-loop AI", "supervised agent AI", "autonomous agent AI". Microsoft Agent 365 = control plane de cette progression.
|
||||
- **5 levels of AI autonomy** (Turian.ai) : Manual → Assisted → Augmented → Autonomous → Fully Autonomous. Très repris en blogs entreprise.
|
||||
- **HITL / HOTL / Human-out-of-the-loop** (autonomous-systems-explained.com) : trois niveaux canoniques en robotique appliqués à l'IA.
|
||||
- **SAFe-Copilot** (arxiv 2511.04664) : unified shared autonomy framework — formalise les seuils de bascule.
|
||||
- **AI Autonomy Coefficient α** (arxiv 2512.11295) : tente une formalisation quantitative.
|
||||
|
||||
**Aucun papier** ne propose exactement notre triptyque "Shadow → Copilot → Autonomous" mais **tous les frameworks 2026 ont 3 paliers équivalents**. Notre vocabulaire produit (cf. `memory/project_vision.md`) est cohérent avec le mainstream.
|
||||
|
||||
### 5.2 Métriques de bascule entre paliers
|
||||
|
||||
Synthèse littérature + nos contraintes :
|
||||
|
||||
| Bascule | Métrique | Seuil indicatif littérature | Adaptation rpa_vision_v3 |
|
||||
|---|---|---|---|
|
||||
| **Shadow → Copilot** | Précision de la suggestion shadow validée par l'humain | 80-90 % d'acceptation des suggestions | Workflow VWB construit en Shadow accepté ≥ 80 % par le TIM sans modif majeure |
|
||||
| **Copilot → Autonomous** | Success rate replay sans intervention | ≥ 95 % sur N runs consécutifs (N≥50) | 50 runs MOREL Catherine successifs sans intervention humaine. Aucun aujourd'hui. |
|
||||
| **Recul Autonomous → Copilot** | Intervention rate > seuil | >5 % des steps requièrent humain | Tableau de bord temps réel intervention rate par workflow |
|
||||
|
||||
**Pratique concrète** : OS-Genesis (Shanghai AI Lab) propose un pipeline "Reverse Task Synthesis" qui est **conceptuellement Shadow → Copilot inverse** : l'agent explore d'abord, dérive ensuite les tâches. Pertinent pour notre vision **TargetMemoryStore → généralisation** (PLAN_APPRENTISSAGE_LEA Phase 2-3).
|
||||
|
||||
---
|
||||
|
||||
## 6. MCP (Model Context Protocol) — place dans une archi RPA on-premise
|
||||
|
||||
**Statut MCP** : standard ouvert Anthropic 2024, adopté largement en 2026. Architecture client-serveur. Anthropic, OpenAI, Microsoft Agent 365 le supportent.
|
||||
|
||||
**Pertinence pour rpa_vision_v3** :
|
||||
|
||||
1. **Notre serveur RPA pourrait s'exposer en MCP server** — déjà signalé dans INSPIRATION_FRAMEWORKS §5 et CLAUDE.md memory (`reference_mcp_servers.md`, on a 13 MCP actifs côté outillage). Cela permettrait à Claude Desktop / Cursor / VS Code d'invoquer nos workflows.
|
||||
2. **Le serveur on-prem peut exposer en MCP** : tables PostgreSQL T2A, dossiers DPI, modèles VLM locaux, dashboards. Pas de cloud requis pour la couche MCP elle-même.
|
||||
3. **Risque** : si on expose Léa en MCP, on rentre dans l'écosystème "shadow AI agents" pointé par les analyses Microsoft RSAC 2026 (gouvernance, traçabilité). Acceptable seulement avec audit log strict.
|
||||
4. **Pas de blocage RGPD spécifique** : MCP est juste un protocole, la souveraineté dépend de qui héberge le serveur.
|
||||
|
||||
**Recommandation MCP** : **horizon 12+ mois**. Pas de valeur immédiate démo. Mais positionnement commercial fort (« notre RPA est un MCP server consommable par n'importe quel agent IA, on-premise et conforme »).
|
||||
|
||||
---
|
||||
|
||||
## 7. Trois scénarios pour rpa_vision_v3
|
||||
|
||||
### Scénario A — Rester replay déclaratif amélioré (RECOMMANDÉ)
|
||||
|
||||
**Description** : on consolide L1, on ferme les 5 bugs P0, on adopte le vocabulaire Skyvern (Policy/Grounding/Validator) dans la doc et le code, on garde la cascade actuelle.
|
||||
|
||||
**Effort** : 4-6 semaines (clôture dette transport + Validator pHash → sémantique + smart_resize DETTE-014).
|
||||
|
||||
**Risque** : faible. On capitalise sur l'existant.
|
||||
|
||||
**Bénéfice** : démo robuste, vendable POC clinique. Pas de saut techno.
|
||||
|
||||
**Coût** : ne répond pas à l'objectif "Léa apprend / Léa comprend" du `memory/project_vision.md`.
|
||||
|
||||
---
|
||||
|
||||
### Scénario B — Hybride L2 + Copilot ORA (BAC À SABLE PARALLÈLE)
|
||||
|
||||
**Description** : Scénario A + on rebranche `_verify_pre_click` dans ORA (DETTE-008, ligne 1705), uniquement en mode toggle "Copilot supervisé" sur un workflow expérimental. Le pre-check VLM devient le Validator-as-component du pattern Skyvern.
|
||||
|
||||
**Effort** : 8-10 semaines (B2 Validator sémantique + un workflow expérimental en Copilot mode + métriques d'intervention rate).
|
||||
|
||||
**Risque** : moyen. Risque d'éparpillement entre L1 stable et L2 expérimental. Nécessite discipline forte (toggle ENV, pas de mélange runtime).
|
||||
|
||||
**Bénéfice** : on prépare AXE C apprentissage et AXE B2 Validator, on a un POC démontrable de "Léa qui vérifie avant de cliquer". Vendable au pitch healthtech.
|
||||
|
||||
**Coût** : double surface de maintenance.
|
||||
|
||||
---
|
||||
|
||||
### Scénario C — Sauter vers Autonomous L4 avec Holo3 ou Claude CU
|
||||
|
||||
**Description** : on abandonne progressivement VWB déclaratif, on bascule sur un modèle SOTA (Holo3-35B-A3B en open weights, ou Claude Sonnet 5 cloud) qui décompose le goal "T2A patient X" en steps autonomes.
|
||||
|
||||
**Effort** : 6-12 mois minimum. Recodage majeur. Infrastructure GPU >70 GB VRAM (Holo3) ou cloud bill significatif (Claude).
|
||||
|
||||
**Risque** : très élevé. Easily Assure n'est pas dans le set d'entraînement de ces modèles. Performance OSWorld 80 % ne se transfère pas à UI métier propriétaire. Risque RGPD si Claude (envoi screenshots à Anthropic). Risque hallucination en production médicale.
|
||||
|
||||
**Bénéfice** : narrative "vraiment agentique". Compétitif vs Skyvern/UiPath agentic.
|
||||
|
||||
**Coût** : casse la démo, désaligne avec contrat "100% vision" on-premise, casse l'asset commercial healthtech RGPD.
|
||||
|
||||
→ **Rejeté pour 2026**. Reconsidérer en 2027 si Holo3-7B (hypothétique) sort, ou si on a un client GPU H100 sur site.
|
||||
|
||||
---
|
||||
|
||||
## 8. Recommandation finale
|
||||
|
||||
**Adopter Scénario A en main track, Scénario B en bac à sable parallèle**, avec ces étapes ordonnées :
|
||||
|
||||
1. **S1-S2** : SSE/WebSocket transport (clôt §4 de SYNTHESE_TECHNOS, sans ça rien d'autre n'est crédible).
|
||||
2. **S3-S4** : Validator sémantique (AXE B2) — remplacer pHash global par vérification texte attendu présent dans zone visée. C'est aussi la condition d'AXE C.
|
||||
3. **S5-S6** : Sur un workflow expérimental, toggle `RPA_ORA_PRECHECK=true` → mode Copilot. Mesurer intervention rate.
|
||||
4. **S7-S8** : Brancher `TargetMemoryStore` Phase 1 (PLAN_APPRENTISSAGE_LEA) — bascule "Léa apprend" mesurable.
|
||||
5. **Post-S8** : décision Dom autonomous L3 oui/non, sur base métriques réelles.
|
||||
|
||||
**Dépendances explicites** :
|
||||
- AXE B2 Validator → débloque Copilot et toute progression L2 → L3.
|
||||
- AXE C apprentissage (TargetMemoryStore) → débloque la mémoire long-terme nécessaire à Copilot+.
|
||||
- Clôture dette transport → prérequis dur, indépendant des autres axes.
|
||||
|
||||
---
|
||||
|
||||
## 9. Sources (priorité < 6 mois)
|
||||
|
||||
### Benchmarks et leaderboards
|
||||
- [OSWorld-Verified leaderboard (llm-stats)](https://llm-stats.com/benchmarks/osworld-verified)
|
||||
- [OSWorld 2026 Benchmark Results (Coasty)](https://coasty.ai/blog/ai-agent-benchmark-results-2026-osworld-leaderboard-slashing)
|
||||
- [Windows Agent Arena (Microsoft GitHub)](https://microsoft.github.io/WindowsAgentArena/)
|
||||
- [ScreenSpot-Pro leaderboard](https://gui-agent.github.io/grounding-leaderboard/)
|
||||
- [Computer Use Leaderboard (Awesome Agents)](https://awesomeagents.ai/leaderboards/computer-use-leaderboard/)
|
||||
- [OSWorld-Human: Benchmarking Efficiency of CU Agents (arxiv 2506.16042)](https://arxiv.org/abs/2506.16042)
|
||||
|
||||
### Frameworks autonomes
|
||||
- [Anthropic Claude Computer Use 2026 (TokenMix)](https://tokenmix.ai/blog/claude-computer-use-api-2026)
|
||||
- [Claude Sonnet 5 benchmarks (DEV.to)](https://dev.to/best_codes/anthropic-just-dropped-claude-sonnet-5-and-the-benchmarks-are-kind-of-insane-3ppc)
|
||||
- [OpenAI CUA / Operator](https://openai.com/index/computer-using-agent/)
|
||||
- [Holo3 35B-A3B leaderboard top (ChatForest)](https://chatforest.com/guides/holo3-desktop-agent-osworld-record/)
|
||||
- [Holo Company launches Holo3 (TestingCatalog)](https://www.testingcatalog.com/holo-company-launches-holo3-sota-computer-use-model/)
|
||||
- [Magma foundation model (Microsoft Research)](https://www.microsoft.com/en-us/research/blog/magma-a-foundation-model-for-multimodal-ai-agents-across-digital-and-physical-worlds/)
|
||||
- [Magma arxiv 2502.13130](https://arxiv.org/abs/2502.13130)
|
||||
- [Agent S2 paper (arxiv 2504.00906)](https://arxiv.org/abs/2504.00906)
|
||||
- [Agent S Github (Simular)](https://github.com/simular-ai/agent-s)
|
||||
- [Skyvern dual mode (DEV.to)](https://dev.to/stevengonsalvez/browser-tools-for-ai-agents-part-2-the-framework-wars-browser-use-stagehand-skyvern-4gn)
|
||||
- [Skyvern Github](https://github.com/Skyvern-AI/skyvern)
|
||||
- [UI-TARS-2 technical report (arxiv 2509.02544)](https://arxiv.org/html/2509.02544v1)
|
||||
- [UI-TARS Github (ByteDance)](https://github.com/bytedance/UI-TARS)
|
||||
- [OS-Atlas-Pro-7B HuggingFace](https://huggingface.co/OS-Copilot/OS-Atlas-Pro-7B)
|
||||
- [OS-Atlas paper (arxiv 2410.23218)](https://arxiv.org/abs/2410.23218)
|
||||
- [Cradle BAAI (general computer control)](https://baai-agents.github.io/Cradle/)
|
||||
- [Cradle Github](https://github.com/BAAI-Agents/Cradle)
|
||||
- [OS-Genesis Reverse Task Synthesis (arxiv 2412.19723)](https://arxiv.org/abs/2412.19723)
|
||||
- [OS-Copilot Github](https://github.com/OS-Copilot)
|
||||
- [AppAgent v2 (arxiv 2408.11824)](https://arxiv.org/pdf/2408.11824)
|
||||
|
||||
### Patterns recovery / autonomie
|
||||
- [Reflexion paper (NeurIPS 2023, Shinn et al.)](https://arxiv.org/abs/2303.11366)
|
||||
- [BacktrackAgent (arxiv 2505.20660)](https://arxiv.org/pdf/2505.20660)
|
||||
- [MGA Memory-Driven GUI Agent (arxiv 2510.24168)](https://arxiv.org/html/2510.24168v1)
|
||||
- [Agentic Workflow Incident Response 2026 (DigitalApplied)](https://www.digitalapplied.com/blog/agentic-workflow-incident-response-playbook-2026)
|
||||
- [Agent Disaster Recovery (TianPan)](https://tianpan.co/blog/2026-04-28-agent-dr-working-memory-region-failover)
|
||||
- [Agentic Design Patterns 2026 (SitePoint)](https://www.sitepoint.com/the-definitive-guide-to-agentic-design-patterns-in-2026/)
|
||||
- [AI Agent Reflection patterns (Zylos)](https://zylos.ai/research/2026-03-06-ai-agent-reflection-self-evaluation-patterns)
|
||||
|
||||
### Mémoire long-terme
|
||||
- [State of AI Agent Memory 2026 (Mem0)](https://mem0.ai/blog/state-of-ai-agent-memory-2026)
|
||||
- [Best AI Agent Memory Frameworks 2026 (Atlan)](https://atlan.com/know/best-ai-agent-memory-frameworks-2026/)
|
||||
- [Memory in Agents — Short/Long-term with LangGraph (Medium)](https://medium.com/@anilnishad19799/memory-in-agents-complete-guide-to-short-term-long-term-memory-with-langgraph-c21d27455a77)
|
||||
|
||||
### MCP
|
||||
- [MCP introduction (Anthropic)](https://www.anthropic.com/news/model-context-protocol)
|
||||
- [MCP docs (Anthropic)](https://docs.anthropic.com/en/docs/agents-and-tools/mcp)
|
||||
- [Code execution with MCP (Anthropic engineering)](https://www.anthropic.com/engineering/code-execution-with-mcp)
|
||||
- [MCP overview (Phil Schmid)](https://www.philschmid.de/mcp-introduction)
|
||||
|
||||
### Autonomy frameworks (Shadow → Copilot → Autonomous)
|
||||
- [5 Levels of AI Autonomy (Turian)](https://www.turian.ai/blog/the-5-levels-of-ai-autonomy)
|
||||
- [Human-in-the-Loop vs Full Autonomy (Autonomous Systems Explained)](https://www.autonomous-systems-explained.com/articles/human-in-the-loop-autonomy.html)
|
||||
- [SAFe-Copilot Unified Shared Autonomy (arxiv 2511.04664)](https://arxiv.org/pdf/2511.04664)
|
||||
- [AI Autonomy Coefficient α (arxiv 2512.11295)](https://arxiv.org/pdf/2512.11295)
|
||||
- [Computer Use Agents 2026 Claude vs OpenAI vs Gemini (DigitalApplied)](https://www.digitalapplied.com/blog/computer-use-agents-2026-claude-openai-gemini-matrix)
|
||||
|
||||
### Healthcare RPA / agents
|
||||
- [Built 11 Autonomous Agents Healthcare RCM (Medium Apr 2026)](https://medium.com/@anilAmbharii/built-11-autonomous-agents-to-fix-healthcare-revenue-cycle-9d0c9f8d662a)
|
||||
- [Manus AI Enterprise Healthcare Evaluation Guide 2026 (Ventus)](https://www.ventus.ai/blog/manus-ai-agentic-ai-enterprise-healthcare-evaluation-guide/)
|
||||
- [Future of RPA Trends 2026 (Blue Prism)](https://www.blueprism.com/resources/blog/future-of-rpa-trends-predictions/)
|
||||
|
||||
---
|
||||
|
||||
*Document à débattre avec Dom. Pas d'action de code engagée. Le scénario retenu doit aussi être croisé avec les conclusions d'AXE B2 (Validator) et d'AXE C (apprentissage) avant arbitrage final.*
|
||||
453
docs/recherche/AXE_B5_D1_CAPTURE_REMOTE.md
Normal file
453
docs/recherche/AXE_B5_D1_CAPTURE_REMOTE.md
Normal file
@@ -0,0 +1,453 @@
|
||||
# AXE B5 + D1 — Capture multi-écran Windows & Desktop distant sans accessibility tree
|
||||
|
||||
**Date :** 2026-05-23
|
||||
**Auteur :** Claude (agent recherche sous-traité), brief Dom
|
||||
**Périmètre :** B5 (capture Windows 11 robuste, bug `mss.monitors[N]=2560×60`, DPI) + D1 (NoMachine/Citrix/RDP, capture sans accessibility tree)
|
||||
**Statut :** recommandations techniques, pas de modif code. À valider Dom avant action.
|
||||
|
||||
---
|
||||
|
||||
## 1. TL;DR
|
||||
|
||||
**Bug racine identifié** : `mss.monitors[1]=2560×60` n'est PAS un bug du multi-écran, c'est un **effet de bord DPI** documenté du couple `mss` + Windows. Quand un autre composant du process (PyQt5 GUI Léa, NoMachine, ou un appel `GetSystemMetrics` antérieur) modifie le `DPI_AWARENESS_CONTEXT` du process pendant l'exécution, `mss` (qui s'appuie sur `EnumDisplayMonitors` + `MONITORINFO`) renvoie des dims tronquées intermittemment. La 1re capture est saine, les suivantes peuvent dériver. Issue documentée : `BoboTiG/python-mss#197`, `#257`, `#108`, `#49`.
|
||||
|
||||
**Recommandation principale :** déclarer **`DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2`** AU LANCEMENT du process Léa Windows (avant tout `import mss`, avant PyQt5), via `ctypes.windll.user32.SetProcessDpiAwarenessContext(-4)`. Conserver le garde-fou `_acquire_safe_grab` actuel comme filet de sécurité (post-incident il a déjà sauvé la démo). Migrer à moyen terme vers **`dxcam`** (DXGI Desktop Duplication) qui est immunisé par construction (l'API DXGI travaille en pixels physiques sans dépendre du DPI awareness du process).
|
||||
|
||||
**Recommandation NoMachine :** garder NoMachine 9.5.7 pour la démo client (terrain connu), mais évaluer **RustDesk** ou **Parsec** comme alternative pour POC suivants. Le freeze NoMachine "clics avalés après quelques minutes" est confirmé par 9+ threads du forum officiel depuis v4.x, **jamais corrigé**. Implémenter un **heartbeat actif côté Léa** (capture pHash toutes les 5 s + détection écran figé > 30 s) avant tout déploiement client.
|
||||
|
||||
**Fix court terme bug coord Y cassé (P0)** : injecter `SetProcessDpiAwarenessContext` dans `executor.py` au démarrage + serrer le garde-fou existant (refuser TOUTE capture < 200 px de haut, pas seulement secondaire). Code copy-paste-ready en §4.
|
||||
|
||||
---
|
||||
|
||||
## 2. Section B5 — Capture Windows
|
||||
|
||||
### 2.1. Table comparative — bibliothèques de capture Windows (mai 2026)
|
||||
|
||||
| Lib | Backend | Cross-OS | DPI-safe | Multi-monitor | FPS 1080p | Statut maint. | Verdict RPA Vision |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| **mss** (BoboTiG) | GDI BitBlt | Linux/Mac/Win | ⚠ Buggy (Win) | OK via `monitors[]` | 30-60 | Actif mais bug DPI ouvert depuis 2018 | **Actuel** — conserver avec ceinture DPI |
|
||||
| **pyautogui** | Pillow + GDI | Cross-OS | ⚠ Idem mss | ❌ Composite seulement | 5-15 | Stable, fonctionnalités figées | À éviter pour capture (ok pour mouse) |
|
||||
| **Win32 GDI direct** (`BitBlt + GetDC`) | GDI | Win | ✅ Si DPI déclaré | OK manuel | 20-40 | Stable, bas niveau | Trop verbeux, ne pas réinventer |
|
||||
| **DXGI Desktop Duplication** (Win32 natif) | DXGI | Win 8+ | ✅ Pixels physiques | OK via `IDXGIOutput` | 240+ | Microsoft, stable | Cible idéale mais complexe en pur Win32 |
|
||||
| **dxcam** (ra1nty) | DXGI | Win | ✅ Natif | OK `output_idx=N` | **240+** | **Actif 2026** (release juin 2025 + maj mars 2026) | ⭐ **Migration cible** |
|
||||
| **D3DShot** (Serpent-AI) | DXGI | Win | ✅ | OK | 60-100 | **Quasi abandonné** (dernier commit 2022) | NON, deprecated |
|
||||
| **windows-capture** (NiiightmareXD) | DXGI + WGC | Win 10+ | ✅ | OK | 240+ | Actif, Rust+Python | Alternative à dxcam, plus jeune |
|
||||
| **BetterCam** (RootKit-Org) | DXGI fork DXcam | Win | ✅ | OK | 240+ | Actif | Fork sécurité/gaming, marketing FPS |
|
||||
|
||||
### 2.2. Diagnostic du bug `mss.monitors[N]=2560×60`
|
||||
|
||||
#### Root cause confirmée
|
||||
|
||||
Issue `BoboTiG/python-mss#197` documente précisément le pattern :
|
||||
|
||||
> *« After running `sct.grab(monitor)`, `GetSystemMetrics(0/1)` returns physical pixels (2560×1600) instead of scaled logical (1463×914). »*
|
||||
|
||||
Lecture causale (mainteneur + reproductions) :
|
||||
|
||||
1. Le process Léa Windows démarre **DPI-unaware** (défaut Python 3.x sur Windows sans `SetProcessDpiAwarenessContext` explicite).
|
||||
2. `mss.mss()` au premier appel passe par `ctypes.windll.user32` et **modifie implicitement** le DPI context du thread (effet de bord interne `mss` pour pouvoir capturer en pixels physiques sur écran HiDPI).
|
||||
3. À partir de là, `MONITORINFO.rcMonitor` peut renvoyer des coords incohérentes : la combinaison "process unaware → context modifié à la volée" laisse Windows dans un état intermédiaire où certains monitors logiques sont **réduits à la zone non-DPI-aware** (typiquement la barre de tâches = 60 px de haut sur écran 1600 px).
|
||||
4. Le bug est **intermittent** parce qu'il dépend de l'ordre des appels d'API par le process, de la présence d'une fenêtre PyQt5 active, et de l'événement NoMachine (resize remote → callback Windows qui re-évalue les monitors).
|
||||
|
||||
L'observation 2560×60 chez Léa correspond exactement à : "monitor reconnu, mais sa hauteur effective dans le contexte non-aware est la zone d'overlay NoMachine (≈ taskbar)".
|
||||
|
||||
#### Pourquoi `dxcam` est immunisé
|
||||
|
||||
DXGI Desktop Duplication s'appuie sur `IDXGIOutput::GetDesc` qui retourne `DXGI_OUTPUT_DESC.DesktopCoordinates` en **pixels physiques** indépendamment du DPI awareness du process. Le bug ne peut littéralement pas se produire.
|
||||
|
||||
#### Workaround documenté officiellement (mss issue #197)
|
||||
|
||||
```python
|
||||
# AVANT tout import mss / PyQt5 / win32api
|
||||
import ctypes
|
||||
# DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4
|
||||
try:
|
||||
ctypes.windll.user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(-4))
|
||||
except Exception:
|
||||
# Fallback Windows 8.1 (avant V2) : per-monitor v1
|
||||
try:
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(2)
|
||||
except Exception:
|
||||
ctypes.windll.user32.SetProcessDPIAware() # legacy
|
||||
```
|
||||
|
||||
### 2.3. Recommandation patterns multi-DPI
|
||||
|
||||
1. **Déclarer le DPI awareness TÔT** dans le process (avant tout `import mss`, avant `from PyQt5 import ...`). Idéalement dans `main.py` du client Léa, ligne 1-5.
|
||||
2. **Une fois en V2**, `mss.monitors[i]['width'/'height']` est cohérent avec les coords composite Windows (logique = physique pour le process).
|
||||
3. **Coordonnées agent → serveur** : toujours en pixels physiques globaux (origine = top-left du virtual desktop, qui peut être négative si moniteur secondaire à gauche). Pas de pourcentage tant que le DPI peut bouger.
|
||||
4. **Pour cliquer en pixels physiques** : utiliser `SendInput` (`ctypes.windll.user32.SendInput`) plutôt que `pyautogui.click` — `pyautogui` re-divise par le DPI scale.
|
||||
5. **Tester sur écran HiDPI** : la box dev Dom est 1×, le client cible peut être 1.5× ou 1.75× (cas réel issue #197). Bench obligatoire avant déploiement client.
|
||||
|
||||
### 2.4. Snippet Python — capture moderne dxcam multi-monitor prêt à coller
|
||||
|
||||
```python
|
||||
# capture_dxgi.py — capture Windows via DXGI Desktop Duplication
|
||||
# Drop-in pour remplacer mss.mss().grab(monitor) côté Léa Windows.
|
||||
import ctypes
|
||||
import dxcam
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from typing import Optional, Tuple
|
||||
|
||||
# Étape 1 : déclarer le DPI awareness avant toute capture
|
||||
def _ensure_dpi_aware_v2() -> bool:
|
||||
"""Déclare PER_MONITOR_AWARE_V2. À appeler en tout début de process."""
|
||||
try:
|
||||
# -4 = DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 (Win 10 1703+)
|
||||
ok = ctypes.windll.user32.SetProcessDpiAwarenessContext(
|
||||
ctypes.c_void_p(-4)
|
||||
)
|
||||
return bool(ok)
|
||||
except (AttributeError, OSError):
|
||||
try:
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(2) # PER_MONITOR
|
||||
return True
|
||||
except Exception:
|
||||
try:
|
||||
ctypes.windll.user32.SetProcessDPIAware()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
_ensure_dpi_aware_v2() # exécuté à l'import
|
||||
|
||||
# Étape 2 : cache d'instances dxcam par output_idx (création coûteuse)
|
||||
_camera_cache: dict[int, "dxcam.DXCamera"] = {}
|
||||
|
||||
def get_camera(output_idx: int = 0, device_idx: int = 0) -> "dxcam.DXCamera":
|
||||
"""Récupère (ou crée) une caméra DXGI pour un monitor donné."""
|
||||
key = (device_idx, output_idx)
|
||||
if key not in _camera_cache:
|
||||
_camera_cache[key] = dxcam.create(
|
||||
device_idx=device_idx,
|
||||
output_idx=output_idx,
|
||||
output_color="BGR", # cohérent avec cv2 / mss legacy
|
||||
)
|
||||
return _camera_cache[key]
|
||||
|
||||
|
||||
def list_monitors() -> list[dict]:
|
||||
"""Retourne la géométrie de chaque monitor en pixels physiques."""
|
||||
# dxcam.output_info() retourne une string formatée — on parse via API bas niveau
|
||||
monitors = []
|
||||
for output_idx, info in enumerate(dxcam.device_info().splitlines()):
|
||||
# Fallback robuste : interroger les outputs via dxcam directement
|
||||
try:
|
||||
cam = get_camera(output_idx=output_idx)
|
||||
# cam.width / cam.height exposés depuis dxcam 0.0.5+
|
||||
monitors.append({
|
||||
"idx": output_idx,
|
||||
"width": int(cam.width),
|
||||
"height": int(cam.height),
|
||||
"left": int(getattr(cam, "left", 0)),
|
||||
"top": int(getattr(cam, "top", 0)),
|
||||
"primary": output_idx == 0,
|
||||
})
|
||||
except Exception:
|
||||
continue
|
||||
return monitors
|
||||
|
||||
|
||||
def grab_monitor(output_idx: int = 0) -> Optional[Image.Image]:
|
||||
"""Capture un monitor en PIL.Image RGB (drop-in pour mss + Image.frombytes).
|
||||
|
||||
Retourne None si capture échoue (frame skipped par DXGI = no-change).
|
||||
"""
|
||||
cam = get_camera(output_idx=output_idx)
|
||||
frame = cam.grab() # ndarray (H, W, 3) BGR, ou None si frame inchangée
|
||||
if frame is None:
|
||||
# DXGI ne re-livre pas une frame identique → forcer
|
||||
frame = cam.grab(region=None)
|
||||
if frame is None:
|
||||
return None
|
||||
# BGR → RGB pour PIL
|
||||
return Image.fromarray(frame[:, :, ::-1])
|
||||
|
||||
|
||||
def safe_grab(
|
||||
output_idx: int = 0,
|
||||
min_width: int = 200,
|
||||
min_height: int = 200,
|
||||
max_attempts: int = 2,
|
||||
) -> Tuple[Optional[dict], Optional[Image.Image]]:
|
||||
"""Drop-in pour _acquire_safe_grab actuel mais sur DXGI.
|
||||
|
||||
Vérifie que la frame retournée a des dimensions plausibles.
|
||||
"""
|
||||
for attempt in range(max_attempts):
|
||||
img = grab_monitor(output_idx=output_idx)
|
||||
if img is None:
|
||||
continue
|
||||
w, h = img.size
|
||||
if w >= min_width and h >= min_height:
|
||||
return (
|
||||
{"width": w, "height": h, "idx": output_idx},
|
||||
img,
|
||||
)
|
||||
return None, None
|
||||
```
|
||||
|
||||
**Notes d'intégration :**
|
||||
- `pip install "dxcam[cv2]"` (ajoute opencv-headless si pas déjà installé).
|
||||
- Python 3.10-3.14 supporté.
|
||||
- À tester sur la box Léa avant migration globale : confirmer que `output_idx=0` correspond bien au monitor principal physique de NoMachine.
|
||||
- **Ne pas migrer en chaud avant la démo client** — l'archi actuelle marche grâce au garde-fou `_acquire_safe_grab`. Migration = post-démo Anouste.
|
||||
|
||||
### 2.5. Capture fenêtre vs capture écran (cas Easily Assure dans Edge)
|
||||
|
||||
Pour le cas spécifique de la démo (Easily Assure rendu dans Microsoft Edge plein écran sur Léa Windows) :
|
||||
|
||||
- **Ne pas utiliser `PrintWindow`** : sur Edge moderne (Chromium/WebView2), `PrintWindow` retourne souvent une image noire ou figée (composition GPU contourne GDI). Issue connue : `Microsoft/microsoft-ui-xaml#7170`.
|
||||
- **Privilégier capture écran complet + crop** : c'est ce que `capture_active_window` fait déjà dans `capturer.py:381`. Conserver.
|
||||
- **Pour Edge fullscreen** : la frame DXGI inclut toujours le contenu Chromium même en mode exclusive (contrairement à GDI). DXcam est donc encore mieux ici.
|
||||
|
||||
---
|
||||
|
||||
## 3. Section D1 — Desktop distant sans accessibility tree
|
||||
|
||||
### 3.1. Table comparative — remote desktop pour RPA visuel (mai 2026)
|
||||
|
||||
| Solution | Latence LAN | Couleur | Color depth | RPA-friendly | Freeze pattern | Verdict |
|
||||
|---|---:|---|---|---|---|---|
|
||||
| **NoMachine 9.5.7** | 15-30 ms | NX H.264 | 24 bpp | Moyen (clipboard cassé, input passive grab) | ⚠ **Confirmé** clics avalés après N min, forum officiel | Actuel, à remplacer dès Anouste |
|
||||
| **RustDesk** (open source) | 18-30 ms | VP9 | 24 bpp | Moyen | Pas de freeze connu, mais latence WAN x2 vs Parsec | Alternative crédible, on-prem possible |
|
||||
| **Parsec** | 7-10 ms | H.264 NVENC | 24/32 bpp | Bon (input fiable) | Aucun rapporté | Excellent mais cloud + fermé |
|
||||
| **AnyDesk** | 20-40 ms | DeskRT | 24 bpp | Bon, support commercial | Rare, restart fix | Standard entreprise, on-prem cher |
|
||||
| **Citrix Workspace** | variable | HDX (YUV420/YUV444) | 8-24 bpp configurable | **Difficile** (color depth réduit, lag) | Spécifique app | Terrain réel hôpital, accepter contraintes |
|
||||
| **RDP vanille** | 20-50 ms | RemoteFX/AVC | 16-32 bpp | Bon | "RDP freezing Win11 24H2" connu, fix par TCP-only | Acceptable mais Win-Win only |
|
||||
| **VNC** | 50-100 ms | divers | 8-24 bpp | Pauvre (latence input) | Variable | Éviter |
|
||||
|
||||
### 3.2. NoMachine 9.5.7 — analyse freeze
|
||||
|
||||
**Pattern documenté** (forum officiel NoMachine, multiples threads depuis v4) :
|
||||
|
||||
- *"NoMachine stops accepting mouse and keyboard input"* — persiste v4 → v8, **pas de fix officiel**.
|
||||
- *"Mouse click not working"*, *"Inputs suddenly stopped working"*.
|
||||
- Workaround unique remonté : toucher physiquement la souris sur l'hôte pour débloquer.
|
||||
|
||||
**Cause probable** (lecture forum + connaissance archi) : NoMachine utilise du *passive grab* X11 pour propager les events Windows→Linux. Quand le buffer X11 est saturé (compositor lent, refresh display, sleep system court), le grab est libéré silencieusement mais NoMachine ne réinjecte plus les events.
|
||||
|
||||
**Conséquences pour RPA :**
|
||||
1. **Les clics côté Léa Windows partent**, mais ne sont pas vus par l'host Linux.
|
||||
2. Pas de feedback d'erreur — Léa croit avoir cliqué.
|
||||
3. La capture côté Léa Windows continue à montrer l'écran AVANT le clic (puisque le compositor host n'a rien repeint).
|
||||
4. → **L'agent boucle sur "je vois l'écran avant clic, je reclique"** — c'est exactement le pattern du LoopDetector QW2.
|
||||
|
||||
**Recommandations P1 (cf. §5) :** instrumenter un heartbeat actif côté client Léa qui détecte l'écran figé > 30 s et **pause supervisée explicite** ("la connexion NoMachine semble figée, restart NoMachine puis reprendre ?").
|
||||
|
||||
### 3.3. Cradle (BAAI-Agents) — comment ils capturent en jeu vidéo Windows
|
||||
|
||||
**Cradle** n'est pas de Microsoft (commune confusion) mais de BAAI (Beijing Academy of AI). Code GitHub : `BAAI-Agents/Cradle`.
|
||||
|
||||
Leur stack capture (regard rapide du repo, à confirmer si pertinent) :
|
||||
- Capture via **`mss`** également (!), avec `monitor_index` configurable
|
||||
- Pour RDR2 / fullscreen exclusive DirectX : capture via **window-handle ciblé** par `pygetwindow` + `mss` sur la zone
|
||||
- Pas de wrapper DXGI custom dans la branche main → ils sont confrontés aux mêmes bugs que nous, leur env est juste plus contrôlé (un seul écran, pas de remote desktop intermédiaire)
|
||||
|
||||
**Apprentissage :** Cradle n'a PAS résolu le problème, ils l'évitent en contrôlant l'environnement (PC dédié, un écran, pas de scaling). Notre setup remote desktop multi-écran est intrinsèquement plus difficile.
|
||||
|
||||
### 3.4. Patterns côté hôte vs côté viewer
|
||||
|
||||
| Approche | Description | Avantages | Inconvénients |
|
||||
|---|---|---|---|
|
||||
| **Capture côté hôte distant** (Windows Léa) | Léa capture localement Windows, envoie au serveur Linux | Pixels physiques natifs, pas de re-encoding NoMachine | Bug DPI, nécessite agent sur l'hôte |
|
||||
| **Capture côté viewer Linux** (notre poste Dom) | Linux capture la fenêtre NoMachine via mss/dxcam Linux | Pas besoin d'agent Windows | "screenshots through screenshots", artefacts H.264 NoMachine, perte qualité OCR |
|
||||
| **Hybride : agent host + screenshot fallback viewer** | Pondération selon dispo réseau | Robuste | Complexité, désync entre les 2 sources |
|
||||
|
||||
**Recommandation projet :** rester sur capture côté hôte (Léa Windows). C'est ce qui est implémenté et c'est la bonne décision : la qualité OCR sur capture re-encodée par NoMachine (H.264 lossy) est mauvaise (cf. bug OCR-DIRECT 8 mai sur tabs Easily). Si NoMachine devient bloquant, migrer vers **RustDesk auto-hébergé** plutôt que vers une capture côté viewer.
|
||||
|
||||
### 3.5. Heartbeat actif — détecter le freeze AVANT d'envoyer les clics
|
||||
|
||||
Pattern à implémenter côté Léa Windows (P1, cf. §5) :
|
||||
|
||||
1. Toutes les 5 s, capture pHash de l'écran complet.
|
||||
2. Comparer au pHash N-1. Si identique pendant **> 30 s** ET un input a été émis dans cette fenêtre → considérer connexion gelée.
|
||||
3. Notifier serveur via heartbeat enrichi : `remote_session_status: "frozen_suspected"`.
|
||||
4. Côté serveur, basculer en `replay_paused` automatique, dialogue VWB *"Restart NoMachine ?"*.
|
||||
|
||||
Code de référence existant : `windows.forum.nomachine.com` confirme que toucher physiquement la souris débloque. Donc le restart NoMachine est la bonne action de récupération — mais il faut un humain.
|
||||
|
||||
---
|
||||
|
||||
## 4. Recommandations P0 — Fix bug coord Y intermittent (`executor.py:606-617`)
|
||||
|
||||
Le code actuel `_resolve_via_uia_local` (lignes 606-617 d'`executor.py`) n'est PAS l'origine du bug coord Y — c'est UIA, pas la capture. Le bug racine est dans `capturer.py` (déjà partiellement traité par `_acquire_safe_grab`) ET dans le manque de déclaration DPI au démarrage.
|
||||
|
||||
**Fix recommandé en 2 temps :**
|
||||
|
||||
### Fix #1 — Déclarer DPI awareness au démarrage du client Léa
|
||||
|
||||
Ajouter en **première ligne** de `agent_v0/agent_v1/main.py` (avant tout autre import) :
|
||||
|
||||
```python
|
||||
# agent_v0/agent_v1/main.py — TOUT EN HAUT, avant tout import
|
||||
import platform
|
||||
|
||||
if platform.system() == "Windows":
|
||||
import ctypes
|
||||
try:
|
||||
# DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4
|
||||
# Cf. mss issue #197 : sans ça, mss.monitors retourne intermittemment
|
||||
# des dims tronquées (cas observé 2560×60 démo GHT 19 mai 2026).
|
||||
ctypes.windll.user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(-4))
|
||||
except (AttributeError, OSError):
|
||||
try:
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(2) # PER_MONITOR (Win 8.1)
|
||||
except Exception:
|
||||
try:
|
||||
ctypes.windll.user32.SetProcessDPIAware() # legacy
|
||||
except Exception:
|
||||
pass # fallback silencieux si vraiment ancien
|
||||
```
|
||||
|
||||
**Validation attendue :** après ce fix + restart Léa, `mss.monitors[1]` doit toujours retourner `2560×1600` (jamais `2560×60`). Si le bug persiste → c'est un autre composant qui modifie le DPI context après start (suspect : PyQt5 GUI). Investiguer via instrumentation.
|
||||
|
||||
### Fix #2 — Renforcer `_acquire_safe_grab` en filet de sécurité
|
||||
|
||||
Le code actuel de `capturer.py:115-203` est **déjà solide**. Une seule amélioration : refuser le fallback secondaire dans **toutes** les méthodes coord-bearing (déjà fait pour `capture_dual`, vérifier que `capture_active_window` standalone fait pareil — c'est le cas L:371).
|
||||
|
||||
**Aucune modification recommandée sur le fichier** dans le scope court terme. Le fix #1 suffit en théorie ; `_acquire_safe_grab` est la ceinture, pas la cause.
|
||||
|
||||
### Fix #3 — Coordonnées agent serveur
|
||||
|
||||
Vérifier dans `executor.py` autour de L:606-617 (et plus largement) que **toute coord renvoyée au serveur** est en pixels physiques absolus du virtual desktop. Si ailleurs dans le code des pourcentages sont calculés AVEC `mss.monitors[1]['height']` qui peut être 60 → division par 60 → `y_pct × 1600 = grand nombre`. **Le bug Y ÷27 vient probablement de là, pas du clic en sortie**.
|
||||
|
||||
Action : `grep -rn "monitors\[" agent_v0/agent_v1/` et auditer chaque site. Hors scope de ce doc.
|
||||
|
||||
---
|
||||
|
||||
## 5. Recommandations P1 — Détection + recovery freeze NoMachine
|
||||
|
||||
### Détection (côté client Léa)
|
||||
|
||||
Ajouter à `capturer.py` un thread heartbeat de monitoring :
|
||||
|
||||
```python
|
||||
# pseudocode pour ajout heartbeat dans VisionCapturer
|
||||
import threading, time
|
||||
|
||||
class FreezeDetector(threading.Thread):
|
||||
def __init__(self, capturer, on_freeze_callback,
|
||||
interval_s=5.0, freeze_threshold_s=30.0):
|
||||
super().__init__(daemon=True)
|
||||
self.capturer = capturer
|
||||
self.callback = on_freeze_callback
|
||||
self.interval = interval_s
|
||||
self.threshold = freeze_threshold_s
|
||||
self._last_hash = None
|
||||
self._last_change_ts = time.time()
|
||||
self._stop = threading.Event()
|
||||
|
||||
def run(self):
|
||||
while not self._stop.is_set():
|
||||
try:
|
||||
_mon, img = _acquire_safe_grab()
|
||||
if img is not None:
|
||||
h = self.capturer._compute_quick_hash(img)
|
||||
now = time.time()
|
||||
if h != self._last_hash:
|
||||
self._last_hash = h
|
||||
self._last_change_ts = now
|
||||
elif (now - self._last_change_ts) > self.threshold:
|
||||
self.callback(stale_for_s=(now - self._last_change_ts))
|
||||
self._last_change_ts = now # éviter re-trigger en rafale
|
||||
except Exception:
|
||||
pass
|
||||
self._stop.wait(self.interval)
|
||||
|
||||
def stop(self):
|
||||
self._stop.set()
|
||||
```
|
||||
|
||||
### Recovery (côté serveur / VWB)
|
||||
|
||||
Sur réception d'un heartbeat enrichi `remote_session_status="frozen_suspected"` :
|
||||
|
||||
1. Pause replay (`replay_paused=True`) + bulle Léa *"Connexion NoMachine figée détectée"*.
|
||||
2. Dialog VWB côté Dom : `[Restart NoMachine] [J'ai débloqué, reprendre] [Stop]`.
|
||||
3. Tracer le freeze dans `data/runner_captures/freeze_events.jsonl` pour stats post-démo.
|
||||
|
||||
---
|
||||
|
||||
## 6. Dépendances / liens avec AXE B1 (transport)
|
||||
|
||||
Le bug coord Y et le freeze NoMachine ont une **interaction critique** avec le bug transport diagnostiqué le 8 mai (`REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md`) :
|
||||
|
||||
- Si client Léa freeze (NoMachine) ET timeout HTTP client = 5 s → l'action est doublement perdue.
|
||||
- Le watchdog `_retry_pending` côté serveur (fix moyen-terme du doc 8 mai) doit s'articuler avec le heartbeat freeze : ne PAS re-dispatcher une action si la session client est suspectée gelée (sinon empilement).
|
||||
|
||||
Recommandation transverse : **ne pas implémenter le watchdog `_retry_pending` sans intégrer le heartbeat freeze**. Sinon on multiplie les clics fantômes pendant un freeze.
|
||||
|
||||
---
|
||||
|
||||
## 7. Sources
|
||||
|
||||
### Bug `mss` DPI / monitors
|
||||
|
||||
- [BoboTiG/python-mss#197 — GetSystemMetrics wrong after sct.grab](https://github.com/BoboTiG/python-mss/issues/197) — root cause DPI shift
|
||||
- [BoboTiG/python-mss#30 — monitors does not correspond with screen resolution](https://github.com/BoboTiG/python-mss/issues/30)
|
||||
- [BoboTiG/python-mss#108 — combined monitor image and monitors incorrect](https://github.com/BoboTiG/python-mss/issues/108)
|
||||
- [BoboTiG/python-mss#49 — secondary screen Windows 8.1](https://github.com/BoboTiG/python-mss/issues/49)
|
||||
- [BoboTiG/python-mss#257 — scaling factor 2 applied](https://github.com/BoboTiG/python-mss/issues/257)
|
||||
- [Lightrun answer — GetSystemMetrics wrong screen resolution](https://lightrun.com/answers/bobotig-python-mss-getsystemmetrics-module-returns-wrong-screen-resolution-after-running-sctgrabmonitor)
|
||||
|
||||
### DXGI / dxcam / windows-capture
|
||||
|
||||
- [ra1nty/DXcam — high-performance Python DXGI Desktop Duplication, updated 2026](https://github.com/ra1nty/DXcam)
|
||||
- [DXcam Releases](https://github.com/ra1nty/DXcam/releases)
|
||||
- [dxcam PyPI](https://pypi.org/project/dxcam/0.1.0.dev2/)
|
||||
- [NiiightmareXD/windows-capture — Rust+Python DXGI+WGC](https://github.com/NiiightmareXD/windows-capture)
|
||||
- [windows-capture PyPI](https://pypi.org/project/windows-capture/)
|
||||
- [RootKit-Org/BetterCam — DXcam fork 240+Hz](https://github.com/RootKit-Org/BetterCam)
|
||||
- [Microsoft Learn — Desktop Duplication API](https://learn.microsoft.com/en-us/windows/win32/direct3ddxgi/desktop-dup-api)
|
||||
- [microsoft/Windows-classic-samples — DXGIDesktopDuplication](https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/DXGIDesktopDuplication/README.md)
|
||||
- [Kyle Fu — Python Fast Screen Capture benchmark](https://kylefu.me/2023/02/18/python-fast-screen-capture.html)
|
||||
- [ScreenshotOne — How to Capture Desktop Screen with DXcam in Python](https://screenshotone.com/blog/dxcam-python-screenshots/)
|
||||
- [SerpentAI/D3DShot — historique, quasi abandonné](https://github.com/SerpentAI/D3DShot)
|
||||
|
||||
### DPI awareness Windows
|
||||
|
||||
- [Microsoft Learn — SetProcessDpiAwarenessContext](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setprocessdpiawarenesscontext)
|
||||
- [Microsoft Learn — Setting default DPI awareness for a process](https://learn.microsoft.com/en-us/windows/win32/hidpi/setting-the-default-dpi-awareness-for-a-process)
|
||||
- [Microsoft Learn — DPI_AWARENESS_CONTEXT handle](https://learn.microsoft.com/en-us/windows/win32/hidpi/dpi-awareness-context)
|
||||
- [Win32 DPI And Monitor Scaling — gist marler8997](https://gist.github.com/marler8997/9f39458d26e2d8521d48e36530fbb459)
|
||||
- [Python issue 33656 — IDLE DPI awareness on Windows](https://bugs.python.org/issue33656)
|
||||
|
||||
### NoMachine freeze (forum officiel)
|
||||
|
||||
- [NoMachine stops accepting mouse and keyboard input](https://forum.nomachine.com/topic/nomachine-stops-accepting-mouse-and-keyboard-input)
|
||||
- [Mouse click not working](https://forum.nomachine.com/topic/mouse-click-not-working)
|
||||
- [NoMachine connects but doesn't accept keyboard or mouse input](https://forum.nomachine.com/topic/nomachine-connects-but-doesnt-accept-keyboard-or-mouse-input)
|
||||
- [Inputs suddenly stopped working](https://forum.nomachine.com/topic/inputs-suddenly-stopped-working)
|
||||
- [Mouse click unresponsive until Alt-key pressed](https://forum.nomachine.com/topic/mouse-click-unresponsive-until-alt-key-pressed)
|
||||
- [Mouse freeze after forwarding](https://forum.nomachine.com/topic/mouse-freeze-after-forwarding)
|
||||
|
||||
### Alternatives remote desktop
|
||||
|
||||
- [Bundl — RustDesk vs Parsec 2026](https://bundl.run/compare/rustdesk-vs-parsec)
|
||||
- [QuantVPS — Parsec vs RDP vs Rustdesk](https://www.quantvps.com/blog/parsec-vs-rdp-vs-rustdesk)
|
||||
- [Ultimate Systems Blog — AnyDesk vs Parsec vs RustDesk Showdown](https://blog.usro.net/2025/06/anydesk-vs-parsec-vs-rustdesk-showdown/)
|
||||
- [Fileion — AnyDesk vs RustDesk 2026](https://fileion.com/blog/anydesk-vs-rustdesk-best-free-remote-desktop-2026)
|
||||
- [AlternativeTo — NoMachine Alternatives](https://alternativeto.net/software/nomachine/)
|
||||
- [Cendio ThinLinc — NoMachine Alternative](https://www.cendio.com/blog/nomachine-alternative/)
|
||||
- [TechTarget — Windows 11 Remote Desktop freezing](https://www.techtarget.com/searchvirtualdesktop/tip/What-to-do-when-a-Windows-11-remote-desktop-keeps-freezing)
|
||||
- [Windows Forum — RDP Freezing 24H2](https://windowsforum.com/threads/how-to-fix-rdp-freezing-issues-in-windows-11-24h2-complete-guide.362476/)
|
||||
|
||||
### Citrix / RDP / accessibility tree
|
||||
|
||||
- [Citrix Docs — Graphics policy settings](https://docs.citrix.com/en-us/xenapp-and-xendesktop/7-15-ltsr/policies/reference/ica-policy-settings/graphics-policy-settings.html)
|
||||
- [Citrix Docs — Visual display policy](https://docs.citrix.com/en-us/xenapp-and-xendesktop/7-15-ltsr/policies/reference/ica-policy-settings/visual-display-policy-settings.html)
|
||||
- [Citrix CTX202687 — HDX Graphics Modes DCR/Thinwire/H.264](https://support.citrix.com/article/CTX202687)
|
||||
- [UiPath — Citrix Automation](https://www.uipath.com/platform/agentic-automation/ai-ecosystem/citrix-automation)
|
||||
|
||||
### Cradle / agent computer use
|
||||
|
||||
- [BAAI-Agents/Cradle](https://github.com/BAAI-Agents/Cradle)
|
||||
- [arXiv 2403.03186 — Cradle: Empowering Foundation Agents Towards General Computer Control](https://arxiv.org/abs/2403.03186)
|
||||
- [Cradle project page](https://baai-agents.github.io/Cradle/)
|
||||
- [CursorTouch/Windows-MCP — MCP Server for Computer Use in Windows](https://github.com/CursorTouch/Windows-MCP)
|
||||
|
||||
---
|
||||
|
||||
*Document de recherche, lecture seule. Toute implémentation nécessite arbitrage explicite de Dom — la migration `mss → dxcam` est NON urgente tant que la ceinture `_acquire_safe_grab` tient. Le fix DPI awareness §4.1 est par contre **chirurgical, 8 lignes, à valider rapidement** (post-démo client Anouste).*
|
||||
352
docs/recherche/AXE_C_LEARNING_SHADOW.md
Normal file
352
docs/recherche/AXE_C_LEARNING_SHADOW.md
Normal file
@@ -0,0 +1,352 @@
|
||||
# AXE C — Shadow learning, Fine-tuning VLM grounding, Memory store visuel
|
||||
|
||||
**Date :** 2026-05-23
|
||||
**Auteur :** Claude (agent recherche prospective)
|
||||
**Statut :** lecture seule, recherche externe + croisement avec dette interne (DETTE-005, DETTE-009). Aucune modif de code. Sources web < 6 mois priorisées.
|
||||
**Périmètre :** 3 sous-axes prospectifs sur l'apprentissage et la mémoire visuelle pour rpa_vision_v3.
|
||||
|
||||
---
|
||||
|
||||
## 1. TL;DR + recommandation priorisée
|
||||
|
||||
**Insight central :** les 3 axes (C1 Shadow, C2 Fine-tuning, C3 Memory) ne sont **pas substituables** mais **séquentiels** :
|
||||
|
||||
- **C3 (memory)** est l'**investissement le plus immédiatement rentable** : `VisualEmbeddingManager` est déjà écrit, le pattern Skyvern (cache + fallback IA) est éprouvé à 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/<session>/events.jsonl` | Aucun risque runtime, repassable | Pas de boucle d'amélioration continue |
|
||||
|
||||
**Recommandation :** commencer par le **job batch offline** (sécurisé), puis migrer vers post-success hook quand AXE_B2 Validator sémantique sera en place. Ne PAS activer sur `ShadowObserver` direct tant que Validator pHash global est en vigueur — c'est précisément ce que `feedback_phash_vs_dialog_in_vm.md` reproche.
|
||||
|
||||
### 2.3 Autres frameworks — patterns Shadow 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/<session>/events.jsonl` post-démo réussie, appeler `ShadowLearningHook.on_click_observed` rétrospectivement → enrichit `SignatureStore`.
|
||||
2. Mesurer le hit-rate de `SignatureStore` au replay suivant.
|
||||
|
||||
**Moyen terme (1 mois)** :
|
||||
3. Définir notre **format de trace canonique** (inspirer du schéma OpenAdapt SQLite + privacy scrub avant tout).
|
||||
4. Brancher le hook sur `ShadowObserver._on_mouse_click` UNIQUEMENT après merge du Validator sémantique (B2).
|
||||
|
||||
**Anti-pattern à éviter** : activer le hook sur des replays VWB sans Validator. Cf. bug step 10 mai : on apprendrait à cliquer dans le bandeau Edge au lieu du tab Imagerie.
|
||||
|
||||
---
|
||||
|
||||
## 3. C2 — Fine-tuning VLM grounding 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/<session>/events.jsonl` post-réussite, appelle `ShadowLearningHook.on_click_observed` rétroactivement. Mesure hit-rate `SignatureStore` au replay suivant.
|
||||
3. **Définir le format de trace canonique** (inspirer OpenAdapt SQLite + anonymisation t2a-style).
|
||||
|
||||
**Coût :** ~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.*
|
||||
1545
docs/recherche/AXE_D2_DEEP_POPUP_CHAIN.md
Normal file
1545
docs/recherche/AXE_D2_DEEP_POPUP_CHAIN.md
Normal file
File diff suppressed because it is too large
Load Diff
552
docs/recherche/AXE_D2_DIALOG_POPUP.md
Normal file
552
docs/recherche/AXE_D2_DIALOG_POPUP.md
Normal file
@@ -0,0 +1,552 @@
|
||||
# AXE D2 — Gestion des modaux & popups imprévus
|
||||
|
||||
**Date :** 2026-05-23
|
||||
**Auteur :** agent recherche (dispatch session principale)
|
||||
**Périmètre :** stratégie pour gérer en 100% vision les modaux Windows / navigateur / métier qui interrompent un replay Léa.
|
||||
**Statut :** brief de recherche — aucune modification de code proposée. Décisions = Dom.
|
||||
|
||||
---
|
||||
|
||||
## 1. TL;DR + recommandation architecture
|
||||
|
||||
### 1.1. Diagnostic
|
||||
|
||||
- Le projet a déjà 80% de l'outillage : `core/grounding/dialog_handler.py` (EasyOCR + InfiGUI + fallback OCR direct) couvre la **résolution** d'un dialog connu.
|
||||
- Trois lacunes documentées :
|
||||
1. Côté **client Léa**, `_handle_possible_popup` est défini avec **0 site d'appel** (`LESSONS_LEARNED_GHT_2026-05.md`, F5.5.1) — il existe également un `_handle_popup_vlm` qui prend le relais mais sans déclencheur générique.
|
||||
2. Pas de **détecteur d'apparition** (la cascade ne sait pas qu'un modal vient d'apparaître — elle continue à viser l'élément initial, qui est désormais masqué).
|
||||
3. Pas de **politique de fail-safe** différenciée par type de modal (UAC ≠ "fichier déjà existe" ≠ Windows Hello).
|
||||
- Anti-pattern interdit (`feedback_100pct_visual.md`) : inventer un raccourci Win+R / Ctrl+X / Échap-systématique. La cascade reste **OCR → template → VLM**, jamais "réflexe magique".
|
||||
|
||||
### 1.2. Recommandation
|
||||
|
||||
Une stack en trois couches, branchée APRÈS chaque action (et au démarrage du tick d'observation), réutilisant le code existant :
|
||||
|
||||
```
|
||||
ChangeDetector ──► DialogClassifier ──► DialogResolver
|
||||
(léger, < 50ms) (OCR titre + LLM) (catalogue déclaratif
|
||||
+ InfiGUI/OCR + escalation)
|
||||
```
|
||||
|
||||
1. **ChangeDetector** — détecte qu'un modal *vient d'apparaître*. Combinaison foreground-window-change (Windows API côté client) + screenshot diff région centrale + heuristiques visuelles (ombre, centrage). Latence cible < 50 ms.
|
||||
2. **DialogClassifier** — décide *quoi faire* (catalog match → action déterministe ; sinon → VLM pour catégoriser). Réutilise `_read_title` + `KNOWN_DIALOGS` du `dialog_handler.py` actuel, étend avec une catégorisation par type (`UAC` / `HELLO` / `SMARTSCREEN` / `BROWSER_PERM` / `METIER_SAVE` / `INCONNU`).
|
||||
3. **DialogResolver** — applique la **matrice modal → action** (§5). En santé : aucun auto-accept système (UAC/Hello/SmartScreen) — uniquement pause supervisée. Auto-dismiss déterministe **uniquement** pour les modaux métier déclarés dans le workflow (ex. "fichier déjà existe → Oui").
|
||||
|
||||
Cette stack résout **mécaniquement** la dépendance avec AXE_B2 (Validator) : un modal non détecté = un Validator post-action raté ; un Validator sémantique strict force le détecteur à être appelé avant le verdict.
|
||||
|
||||
---
|
||||
|
||||
## 2. Taxonomie des modaux Windows 11 / navigateur (mai 2026)
|
||||
|
||||
Sept catégories qui couvrent ~95% des cas terrain healthtech.
|
||||
|
||||
### 2.1. UAC (User Account Control)
|
||||
|
||||
- **Quand** : installation, élévation `runas`, modification système.
|
||||
- **Aspect visuel** : fond bleu ou assombri global (secure desktop), titre `Contrôle de compte d'utilisateur`, deux boutons `Oui` / `Non` ou champ mot de passe administrateur.
|
||||
- **Particularité** : sur **secure desktop**, screenshot Léa peut être noir/inaccessible (`pyautogui` ne voit rien). Microsoft a livré des fixes UAC en oct. 2025 → janv. 2026 (KB5063878 / KB5074109) — quelques replays plantent encore sur 24H2 idle/locked.
|
||||
- **Politique healthtech recommandée** : **JAMAIS auto-accept**. Pause supervisée immédiate.
|
||||
|
||||
### 2.2. Windows Hello / WebAuthn / FIDO2
|
||||
|
||||
- **Quand** : déverrouillage app sensible (gestionnaire de mots de passe, Outlook), site avec WebAuthn, élévation UAC configurée pour exiger biométrie.
|
||||
- **Aspect visuel** : popup système centré, icône empreinte ou caméra, message "Touchez le capteur" ou "Saisissez votre code PIN".
|
||||
- **Particularité** : nécessite **interaction physique humaine** par construction. Aucune solution 100% vision ne peut résoudre. Anti-pattern absolu : tenter de cliquer "Annuler" pour passer outre.
|
||||
- **Politique** : pause supervisée + **tip** suggérant "désactiver Windows Hello pour la session de démo" en config préalable.
|
||||
|
||||
### 2.3. Microsoft Defender SmartScreen
|
||||
|
||||
- **Quand** : exe non signé, téléchargement suspect, premier lancement Léa.
|
||||
- **Aspect visuel** : bandeau bleu ciel "Windows a protégé votre PC", lien "Informations complémentaires" qui révèle le bouton "Exécuter quand même".
|
||||
- **Politique** : pause supervisée + log security. Notre `feedback_auth_dialogs_runtime.md` rappelle d'anticiper AVANT démo client (signature code, allowlist hash SHA256 — voir `project_code_signing.md`).
|
||||
|
||||
### 2.4. Permissions navigateur (caméra, micro, notifications, géoloc, clipboard)
|
||||
|
||||
- **Quand** : première visite site `https://`, démo Easily Assure si elle demande accès clipboard / notifications.
|
||||
- **Aspect visuel** : popup ancrée à l'URL bar (Chrome/Edge), boutons `Autoriser` / `Bloquer`. Variant inline overlay (Edge 2026).
|
||||
- **Particularité critique sécurité (CVE-2026-0628 janv. 2026)** : malveillances via extensions Chrome qui détournent des permissions. Auto-accept = risque RGPD/HDS.
|
||||
- **Politique** : **déclaratif dans le workflow** ("À cette étape, autoriser le micro") ou pause supervisée. Aucun auto-accept générique.
|
||||
|
||||
### 2.5. Dialog métier (sauvegarde, écrasement, perte de modifications)
|
||||
|
||||
- **Quand** : fermeture document non sauvé, "fichier existe déjà", "Voulez-vous quitter ?".
|
||||
- **Aspect visuel** : popup applicative au-dessus de la fenêtre parente, OCR titre fiable, boutons texte clair (`Oui` / `Non` / `Annuler` / `Enregistrer`).
|
||||
- **Politique** : **catalogue déclaratif**. C'est exactement ce que `KNOWN_DIALOGS` du `dialog_handler.py` actuel gère. Résolution InfiGUI + fallback OCR direct.
|
||||
|
||||
### 2.6. Avertissements / erreurs applicatives (popup OK)
|
||||
|
||||
- **Quand** : timeout réseau, validation backend KO, message info-utilisateur.
|
||||
- **Aspect visuel** : popup centré, icône triangle/croix, un seul bouton `OK` / `Fermer`.
|
||||
- **Politique** : auto-dismiss déterministe SAUF si message critique (regex blocklist "données perdues", "supprimé"). Log obligatoire.
|
||||
|
||||
### 2.7. Modaux inattendus / inconnus (notification push Teams, Outlook reminder, popup mise à jour, cookie banner, newsletter)
|
||||
|
||||
- **Quand** : tout le temps, surtout en démo live (Slack ping, Outlook reminder à xx:00, popup Windows Update).
|
||||
- **Aspect visuel** : très variable. Souvent en coin (toast) plutôt que centré, mais peut voler le focus.
|
||||
- **Politique** : VLM classifier (catégoriser : "publicité" / "système" / "métier inconnu") → pause supervisée avec proposition humaine ou auto-dismiss conservateur (Échap visuel via clic croix détectée).
|
||||
|
||||
---
|
||||
|
||||
## 3. Comparaison frameworks 2026
|
||||
|
||||
| Framework | Détection modal | Politique | Pause humaine | Catalogue déclaratif |
|
||||
|---|---|---|---|---|
|
||||
| **Anthropic Computer Use** (avril 2026) | classifiers anti-prompt-injection détectent screenshots suspects → demande confirmation user | permission-first par défaut, refuse "high-risk", monitor model peut pause | oui (Auto Mode mai 2026 = classifier décide auto-approve vs prompt) | non public |
|
||||
| **OpenAI Operator / ChatGPT Agent** (juillet 2025 → 2026) | "monitor model" surveille comportement suspect ; CAPTCHA / login → cède le contrôle à l'humain (screenshots OFF pendant ce temps) | confirme avant action critique, prend la main sur prompts sécurité | oui, prend-le-relais explicite | non |
|
||||
| **Skyvern 2.0** (oct. 2025) | **Validator** regarde écran post-action, détecte "popup blocked the click", redonne main au Planner pour retry | retry visuel ; built-in TOTP / 2FA / file downloads | non documenté publiquement | implicite via Validator |
|
||||
| **browser-use** (issue #1996 ouvert mai 2026) | **pas de solution intégrée** — la communauté demande explicitement une infra générique | au cas par cas | non | non |
|
||||
| **Cradle** (R&D fin 2025) | screenshot-only, demande au VLM principal d'identifier popup à chaque tick | dépend prompt | non | non |
|
||||
| **PopSweeper** (recherche acad. 2024, applicable mobile) | classifier deux étages **ResNet50 + MobileNetV2** + **YOLO-World** pour bouton croix ; image-diff 100 ms tick ; **60 ms par frame** | auto-dismiss centré sur close-button | non | non |
|
||||
| **rpa_vision_v3 (actuel)** | aucun détecteur ; `dialog_handler.py` ne s'exécute que si appelé explicitement | catalogue déclaratif + InfiGUI/OCR fallback | feedback_failure_is_learning oui | oui (`KNOWN_DIALOGS`) |
|
||||
|
||||
**Lecture** :
|
||||
- Skyvern et OpenAI Operator **valident l'idée centrale** : un Validator strict détecte le modal indirectement (action attendue échoue → l'écran a changé sans cause connue → modal).
|
||||
- Anthropic et OpenAI **convergent** sur le "monitor model" parallèle qui pause sans toucher au workflow principal.
|
||||
- PopSweeper démontre qu'un détecteur **léger spécialisé** (CNN custom < 60 ms) suffit pour ne PAS ajouter de coût VLM à chaque tick.
|
||||
- L'écosystème open source (browser-use) n'a **toujours pas** de solution générique — le sujet est ouvert.
|
||||
|
||||
---
|
||||
|
||||
## 4. Architecture proposée pour rpa_vision_v3
|
||||
|
||||
### 4.1. Vue d'ensemble
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────┐
|
||||
│ Léa (Windows client) │
|
||||
│ │
|
||||
action click ───►│ executor │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ perform_click │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ChangeDetector ◄─── screenshot t+1 ──┐ │
|
||||
│ │ │ │
|
||||
│ ▼ │ │
|
||||
│ is_modal_appeared? │ │
|
||||
│ │ │ │
|
||||
│ no ┴ yes │ │
|
||||
│ │ │ │
|
||||
│ ▼ │ │
|
||||
│ DialogClassifier │ │
|
||||
│ │ │ │
|
||||
│ ▼ │ │
|
||||
│ type ∈ {UAC, HELLO, SMART, PERM, │ │
|
||||
│ SAVE, OK, INCONNU} │ │
|
||||
│ │ │ │
|
||||
│ ▼ │ │
|
||||
│ DialogResolver │ │
|
||||
│ │ │ │
|
||||
│ ▼ │ │
|
||||
│ policy(type, workflow_ctx) │ │
|
||||
│ │ │ │
|
||||
│ ▼ │ │
|
||||
│ ┌─ AUTO_DISMISS (OK trivial) │ │
|
||||
│ ├─ DECLARATIVE (catalog match) │ │
|
||||
│ ├─ ASK_HUMAN (pause supervisée) │ │
|
||||
│ └─ ESCALATE_SECURITY (log + pause) │ │
|
||||
│ │ │
|
||||
│ ▼ │ │
|
||||
│ resume_or_wait │ │
|
||||
│ │ │
|
||||
└───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
serveur (api_stream)
|
||||
│
|
||||
▼
|
||||
report dialog event + replay state
|
||||
```
|
||||
|
||||
### 4.2. Étendre `core/grounding/dialog_handler.py` existant
|
||||
|
||||
Le fichier actuel (lecture seule pour ce doc, voir Read) fait déjà :
|
||||
- `_read_title` (EasyOCR full-screen, `fr+en`, GPU)
|
||||
- catalogue `KNOWN_DIALOGS` ordonné (popups modaux prioritaires avant fenêtres parents)
|
||||
- `_click_via_infigui` (UI-TARS / InfiGUI grounder déjà branché)
|
||||
- `_click_via_ocr` (fallback OCR direct)
|
||||
- retour `dict` avec `handled / title / dialog_type / action / position / time_ms`
|
||||
|
||||
**À ajouter (esquisses, pas de code à committer) :**
|
||||
|
||||
```python
|
||||
# core/grounding/change_detector.py — nouveau fichier proposé
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Tuple
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChangeSignal:
|
||||
"""Signal qu'un changement écran significatif vient d'apparaître."""
|
||||
is_modal: bool # heuristique modal vs scroll normal
|
||||
foreground_hwnd_changed: bool # côté Windows uniquement
|
||||
diff_ratio: float # 0.0 = identique, 1.0 = tout différent
|
||||
central_diff_ratio: float # diff zone centrale (modaux centrés)
|
||||
timestamp: float
|
||||
|
||||
|
||||
class ChangeDetector:
|
||||
"""Détecte qu'un modal *vient d'apparaître* sans appeler le VLM principal.
|
||||
|
||||
Stratégie :
|
||||
1) foreground window change (Windows API, ~1 ms)
|
||||
2) screenshot diff centre vs périphérie (numpy diff sur sous-region, ~10 ms)
|
||||
3) heuristique "centré + bordure assombrie" (modal pattern, ~5 ms)
|
||||
Budget total cible : < 50 ms pour ne PAS ralentir la boucle replay.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._last_screenshot = None
|
||||
self._last_hwnd = None
|
||||
|
||||
def detect(self, screenshot_pil) -> ChangeSignal:
|
||||
t0 = time.time()
|
||||
arr = np.asarray(screenshot_pil.convert("L"))
|
||||
|
||||
fg_changed = self._check_foreground_changed()
|
||||
|
||||
diff_ratio = 0.0
|
||||
central_diff = 0.0
|
||||
if self._last_screenshot is not None:
|
||||
prev = np.asarray(self._last_screenshot.convert("L"))
|
||||
if prev.shape == arr.shape:
|
||||
diff = np.abs(prev.astype(int) - arr.astype(int))
|
||||
diff_ratio = float((diff > 25).mean())
|
||||
# zone centrale = modaux Windows typiques
|
||||
h, w = arr.shape
|
||||
cy0, cy1 = h // 4, 3 * h // 4
|
||||
cx0, cx1 = w // 4, 3 * w // 4
|
||||
central_diff = float((diff[cy0:cy1, cx0:cx1] > 25).mean())
|
||||
|
||||
is_modal = (
|
||||
fg_changed
|
||||
or (central_diff > 0.10 and diff_ratio < 0.40) # changement centré
|
||||
)
|
||||
|
||||
self._last_screenshot = screenshot_pil
|
||||
return ChangeSignal(
|
||||
is_modal=is_modal,
|
||||
foreground_hwnd_changed=fg_changed,
|
||||
diff_ratio=diff_ratio,
|
||||
central_diff_ratio=central_diff,
|
||||
timestamp=t0,
|
||||
)
|
||||
|
||||
def _check_foreground_changed(self) -> bool:
|
||||
"""Côté Windows uniquement — sinon retourne False."""
|
||||
try:
|
||||
import ctypes # noqa
|
||||
hwnd = ctypes.windll.user32.GetForegroundWindow()
|
||||
changed = (self._last_hwnd is not None) and (hwnd != self._last_hwnd)
|
||||
self._last_hwnd = hwnd
|
||||
return changed
|
||||
except Exception:
|
||||
return False
|
||||
```
|
||||
|
||||
**Note importante** : `feedback_popup_vlm.md` documente que `GetForegroundWindow` est **non fiable seul** (retourne 0 en SSH, popups Windows modernes partagent hwnd du parent). On l'utilise comme **signal complémentaire**, jamais comme source unique. La détection finale repose sur le diff écran (vision).
|
||||
|
||||
### 4.3. DialogClassifier — extension du `dialog_handler.py`
|
||||
|
||||
```python
|
||||
# core/grounding/dialog_classifier.py — proposition
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class DialogType(str, Enum):
|
||||
UAC = "uac"
|
||||
HELLO = "windows_hello"
|
||||
SMARTSCREEN = "defender_smartscreen"
|
||||
BROWSER_PERMISSION = "browser_permission"
|
||||
METIER_SAVE = "metier_save" # match catalog KNOWN_DIALOGS
|
||||
METIER_CONFIRM = "metier_confirm"
|
||||
OK_TRIVIAL = "ok_trivial" # popup avec 1 seul bouton OK
|
||||
INCONNU = "inconnu"
|
||||
|
||||
|
||||
# Signatures texte par type (extension du catalogue actuel)
|
||||
TYPE_SIGNATURES = {
|
||||
DialogType.UAC: [
|
||||
"contrôle de compte d'utilisateur",
|
||||
"user account control",
|
||||
"voulez-vous autoriser cette application",
|
||||
],
|
||||
DialogType.HELLO: [
|
||||
"windows hello", "saisissez votre code pin",
|
||||
"touchez le capteur d'empreintes",
|
||||
],
|
||||
DialogType.SMARTSCREEN: [
|
||||
"windows a protégé votre pc",
|
||||
"smartscreen", "informations complémentaires",
|
||||
],
|
||||
DialogType.BROWSER_PERMISSION: [
|
||||
"autoriser", "bloquer",
|
||||
"souhaite utiliser votre caméra",
|
||||
"souhaite utiliser votre microphone",
|
||||
"souhaite afficher des notifications",
|
||||
],
|
||||
# METIER_SAVE et METIER_CONFIRM = KNOWN_DIALOGS existant
|
||||
}
|
||||
|
||||
|
||||
class DialogClassifier:
|
||||
"""Classifie un dialogue détecté en type connu.
|
||||
|
||||
Stratégie cascade :
|
||||
1) match signatures texte (OCR titre) — ~150 ms (EasyOCR cache)
|
||||
2) si pas de match → VLM compact (qwen3-vl:8b) avec prompt
|
||||
"Classify this dialog: uac / hello / smartscreen / browser_perm /
|
||||
metier / unknown" — ~1.7 s
|
||||
3) fallback : INCONNU
|
||||
"""
|
||||
|
||||
def classify(self, screenshot_pil, ocr_text: str) -> DialogType:
|
||||
text = ocr_text.lower()
|
||||
for dtype, signatures in TYPE_SIGNATURES.items():
|
||||
for sig in signatures:
|
||||
if sig in text:
|
||||
return dtype
|
||||
|
||||
# Catalogue métier existant (KNOWN_DIALOGS du dialog_handler.py)
|
||||
from core.grounding.dialog_handler import KNOWN_DIALOGS
|
||||
for key in KNOWN_DIALOGS:
|
||||
if key in text:
|
||||
return DialogType.METIER_SAVE # ou METIER_CONFIRM selon key
|
||||
|
||||
# Fallback VLM si rien ne matche et qu'on a un signal modal fort
|
||||
return self._classify_via_vlm(screenshot_pil) or DialogType.INCONNU
|
||||
|
||||
def _classify_via_vlm(self, screenshot_pil) -> Optional[DialogType]:
|
||||
# Appel qwen3-vl:8b via Ollama LAN (port 11434)
|
||||
# Prompt court, format=json strict
|
||||
# ⚠ qwen3-vl:8b ignore parfois format=json — fallback regex sur stdout
|
||||
...
|
||||
```
|
||||
|
||||
### 4.4. DialogResolver — politique par type
|
||||
|
||||
Voir matrice §5 ci-dessous. Le resolver applique la politique et émet un événement structuré au serveur :
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class DialogEvent:
|
||||
type: DialogType
|
||||
title_ocr: str
|
||||
policy_applied: str # "auto_dismiss" / "declarative" / "ask_human" / "escalate_security"
|
||||
action_taken: Optional[str] # "click 'Oui' (123,456)" / "paused"
|
||||
duration_ms: float
|
||||
screenshot_path: str # toujours archivé pour audit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Matrice modal → action
|
||||
|
||||
| Type | Détection signature | Politique healthtech | Action concrète | Audit |
|
||||
|---|---|---|---|---|
|
||||
| **UAC** | "contrôle de compte", "user account control" | **escalate_security + ask_human** — JAMAIS auto-accept | `pause_for_human` ; toast "élévation requise — opérateur valide" | log full + screenshot |
|
||||
| **Windows Hello** | "windows hello", "code pin", "touchez le capteur" | **ask_human** — interaction physique requise par construction | pause + tip pré-démo : "désactiver Hello pour la session" (paramètres Windows) | log |
|
||||
| **Defender SmartScreen** | "windows a protégé votre pc", "smartscreen" | **escalate_security + ask_human** | pause + log security ; rappel `project_code_signing.md` (signature SHA256) | log full |
|
||||
| **Permission navigateur** (cam/mic/notif/geoloc) | "souhaite utiliser votre", "autoriser / bloquer" | **declarative** si déclaré dans workflow ; sinon **ask_human** | catalog match → click `Autoriser` ; sinon pause | log + screenshot |
|
||||
| **Métier sauvegarde** ("Voulez-vous enregistrer ?", "Enregistrer sous") | `KNOWN_DIALOGS` existant | **declarative** | InfiGUI click `Enregistrer` (catalog priority basse — fenêtre parent) | log standard |
|
||||
| **Métier confirmation** ("Voulez-vous remplacer ?", "Existe déjà", "Écraser") | `KNOWN_DIALOGS` priorité HAUTE | **declarative** | InfiGUI click `Oui` ; fallback OCR direct (code actuel) | log standard |
|
||||
| **OK trivial** (erreur app, info) | 1 seul bouton détecté, mots-clés "erreur/error/warning" ; pas de mot blocklist | **auto_dismiss** | click `OK` | log standard |
|
||||
| **OK trivial SUSPECT** (mots-clés "supprimé", "perdu", "irréversible") | blocklist regex | **ask_human** | pause | log full |
|
||||
| **INCONNU** | aucun match | **ask_human** par défaut (pas d'auto-dismiss aveugle) | pause + capture VLM pour catégorisation a posteriori → enrichit catalogue | log full |
|
||||
|
||||
**Garde-fou healthtech** : tout dialog non métier listé dans `KNOWN_DIALOGS` ou dans le workflow déclaratif **escalade en pause supervisée** par défaut. C'est cohérent avec `feedback_failure_is_learning.md` (échec = pause, pas stop) et le constat OpenAI/Anthropic 2026 (login/sécurité → cède la main à l'humain).
|
||||
|
||||
---
|
||||
|
||||
## 6. Détection rapide d'apparition de modal
|
||||
|
||||
Trois pistes, à composer plutôt qu'à choisir :
|
||||
|
||||
### 6.1. Foreground window change (Windows API)
|
||||
|
||||
- **Coût** : ~1 ms.
|
||||
- **Fiabilité** : faible seul (`feedback_popup_vlm.md` 27 mars 2026 : popups Windows modernes partagent hwnd du parent, retourne 0 en SSH).
|
||||
- **Usage** : **signal complémentaire** dans `ChangeSignal`, jamais source unique.
|
||||
|
||||
```python
|
||||
import ctypes
|
||||
hwnd = ctypes.windll.user32.GetForegroundWindow()
|
||||
title_buf = ctypes.create_unicode_buffer(256)
|
||||
ctypes.windll.user32.GetWindowTextW(hwnd, title_buf, 256)
|
||||
# Comparer hwnd_n vs hwnd_n-1 ; titre dans title_buf.value
|
||||
```
|
||||
|
||||
### 6.2. Screenshot diff zone centrale vs périphérie
|
||||
|
||||
- **Coût** : ~10 ms (numpy `abs(prev - curr) > seuil`, downscale 1/4).
|
||||
- **Fiabilité** : bonne pour modaux centrés (UAC, dialog métier, Hello). Faible pour toasts en coin.
|
||||
- **Heuristique** : `central_diff_ratio > 0.10` **ET** `diff_ratio < 0.40` → modal centré probable (le centre change beaucoup, le reste peu).
|
||||
- **Pattern visuel auxiliaire** : zone "assombrie" en bordure (`secure desktop` UAC = pixels < 50 en luminance sur > 60% de l'écran).
|
||||
|
||||
### 6.3. PopSweeper-style classifier (option future, si latence VLM trop forte)
|
||||
|
||||
- ResNet50 + MobileNetV2 deux étages → 60 ms/frame, 91.7% précision sur RICO (mobile).
|
||||
- À envisager **uniquement** si la détection diff+heuristique laisse passer trop de cas. Pour l'instant, surcoût d'un modèle dédié non justifié — l'OCR titre + signatures couvre 80%.
|
||||
|
||||
### 6.4. Pas de pHash global pour détection modal
|
||||
|
||||
`feedback_phash_vs_dialog_in_vm.md` est explicite : **pHash global est inadapté** à la cascade de modaux en VM. Le pHash compare des images entières, masquant les changements locaux qui sont précisément l'indice d'un modal. Utiliser screenshot diff zoné OU OCR titre — pas pHash global.
|
||||
|
||||
---
|
||||
|
||||
## 7. Activation de `_handle_possible_popup` orphelin
|
||||
|
||||
### 7.1. État actuel
|
||||
|
||||
- **Côté client (Léa Windows)** : `_handle_possible_popup` défini, **0 site d'appel** (`LESSONS_LEARNED_GHT_2026-05.md`, F5.5.1). Un `_handle_popup_vlm` existe en parallèle (le "remplacement" mentionné dans l'audit) mais sans déclencheur générique.
|
||||
- **Côté serveur** : `core/grounding/dialog_handler.py` (étudié pour ce doc) — handler complet, ne se déclenche que si on l'appelle explicitement.
|
||||
- **Constat memoire 27 mars 2026** : `qwen3-vl:8b` détecte popup en 3.6 s avec coordonnées précises depuis le client (appel direct LAN port 11434), pas via le serveur. Donc le VLM-post-clic est **techniquement validé**.
|
||||
|
||||
### 7.2. Câblage proposé (sans modifier le code dans ce doc)
|
||||
|
||||
Le pattern à brancher dans `agent_v1/core/executor.py` ressemble à :
|
||||
|
||||
```
|
||||
def perform_click(self, x, y, ...):
|
||||
screenshot_before = grab()
|
||||
do_click(x, y)
|
||||
time.sleep(short_delay)
|
||||
screenshot_after = grab()
|
||||
|
||||
signal = ChangeDetector().detect(screenshot_after)
|
||||
if signal.is_modal:
|
||||
ocr_text = read_ocr(screenshot_after)
|
||||
dtype = DialogClassifier().classify(screenshot_after, ocr_text)
|
||||
event = DialogResolver(policy).resolve(dtype, screenshot_after, workflow_ctx)
|
||||
report_to_server(event)
|
||||
if event.policy_applied == "ask_human":
|
||||
return WAIT_HUMAN
|
||||
return OK
|
||||
```
|
||||
|
||||
**Sites d'appel à brancher** (à valider avec Dom avant tout commit) :
|
||||
1. Après chaque `_replay_action` côté client (suit la mémoire 27 mars).
|
||||
2. Avant chaque vérification de Validator post-action (cohérence AXE_B2).
|
||||
3. Au démarrage du tick d'observation `observe_reason_act` côté serveur (capture un modal apparu pendant un wait long).
|
||||
|
||||
### 7.3. Décision sur `_handle_popup_vlm` vs `_handle_possible_popup`
|
||||
|
||||
À trancher avec Dom : garder UN seul handler (probablement `_handle_popup_vlm` qui appelle l'extension proposée ici), supprimer l'orphelin. Sinon dette technique persistante (DETTE-XXX à créer si décision prise).
|
||||
|
||||
---
|
||||
|
||||
## 8. Anti-patterns à proscrire
|
||||
|
||||
### 8.1. Hardcoder un raccourci système "fix"
|
||||
|
||||
`feedback_100pct_visual.md` est sans appel : **JAMAIS** :
|
||||
- `keyboard.press_and_release('escape')` pour "fermer un popup".
|
||||
- `keyboard.press_and_release('win+r')` pour "ouvrir un truc rapidement".
|
||||
- `keyboard.press_and_release('ctrl+x')` pour "annuler".
|
||||
|
||||
Raisons :
|
||||
- Casse le récit "Léa comprend visuellement". Démontage immédiat face à un DSI healthtech.
|
||||
- Échappe à la cascade de validation (OCR → template → VLM).
|
||||
- Effets de bord imprévisibles : Échap dans un formulaire peut purger des données saisies ; Win+R sur un secure desktop ne fait rien et perd l'état.
|
||||
|
||||
**Exception unique** : `gesture_catalog.py` autorise les réflexes système **explicitement référencés** (voir `feedback_lea_reflexes_catalog.md`). Mais c'est une **composition** orchestrée, pas un "fix popup ad hoc".
|
||||
|
||||
### 8.2. Auto-accept système (UAC, Hello, SmartScreen)
|
||||
|
||||
Interdit en healthtech. Toujours pause supervisée. Cohérent avec FDA / RGPD / HDS — un agent qui élève des privilèges seul est un risque inacceptable.
|
||||
|
||||
### 8.3. pHash global pour détecter un modal
|
||||
|
||||
Voir §6.4. Utiliser screenshot diff zoné + OCR titre.
|
||||
|
||||
### 8.4. Polling VLM principal à chaque tick
|
||||
|
||||
Latence Qwen2.5-VL = 8-11 s par appel (synthèse `MIGRATION_VLM_PLAN_2026-05-09.md`). Détection modal doit rester < 100 ms — sinon on bloque la boucle replay. Le VLM est appelé **uniquement** quand le ChangeDetector signale `is_modal=True` ET que le catalogue texte n'a pas matché.
|
||||
|
||||
### 8.5. Tenter de "désactiver" Windows Hello programmatiquement
|
||||
|
||||
Solution = config humaine pré-démo, pas action runtime de l'agent. L'agent **détecte et escalade**, il ne modifie pas la config sécurité Windows.
|
||||
|
||||
---
|
||||
|
||||
## 9. Liens forts avec les autres AXES et la dette projet
|
||||
|
||||
- **AXE_A4 (OCR/Template/pHash)** : la **détection** modal réutilise screenshot diff zoné — surface à coordonner avec la décision pHash de A4 (pas pHash global ici).
|
||||
- **AXE_A5 (Screen Tokenization)** : un parser d'écran qui produit la liste de regions interactives détecte aussi les "boutons de modaux" — la classification peut bénéficier de cette liste sans coût VLM supplémentaire.
|
||||
- **AXE_B2 (Validator)** : dépendance forte. Un Validator strict (texte attendu présent dans la zone visée) **force** l'appel au DialogResolver quand le check post-action échoue. Sans ce couplage, un modal non vu reste un échec silencieux (cf. bug step 10 démo 8 mai : Imagerie cliqué dans bandeau Edge, REPORT success=True).
|
||||
- **AXE_A3 (Bench Protocol)** : ajouter un harness "modal injection" — pendant un replay test, injecter UAC simulé / popup métier / SmartScreen factice, mesurer detect→classify→resolve latency et taux pause vs auto-dismiss.
|
||||
- **DETTE existante** : DETTE-008 (`if False:` pré-check VLM par-clic, `observe_reason_act.py:1704-1713`) sera **résolue** par le ChangeDetector léger — on n'a plus besoin d'un VLM par clic, le détecteur fait le filtrage en amont.
|
||||
- **`feedback_capture_purge_policy.md`** : screenshots de DialogEvent à conserver pour audit (RGPD / HDS) — politique de rétention à valider avec Dom.
|
||||
|
||||
---
|
||||
|
||||
## 10. Sources (liens cliquables)
|
||||
|
||||
### Frameworks comparés (publications, blogs, papers)
|
||||
|
||||
- [Anthropic Computer Use tool — docs API](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/computer-use-tool)
|
||||
- [Anthropic Claude Desktop browser permissions controversy (avril 2026)](https://www.sovereignmagazine.com/article/anthropic-claude-desktop-browser-permissions)
|
||||
- [Anthropic — Auto Mode permission classifier (mars 2026)](https://medium.com/@joe.njenga/anthropic-adds-new-claude-code-auto-mode-no-more-permission-modes-52c8094ab742)
|
||||
- [OpenAI Operator System Card](https://openai.com/index/operator-system-card/)
|
||||
- [OpenAI ChatGPT Agent System Card — juillet 2025](https://cdn.openai.com/pdf/839e66fc-602c-48bf-81d3-b21eacc3459d/chatgpt_agent_system_card.pdf)
|
||||
- [ChatGPT Agent — login takeover + screenshot OFF](https://help.openai.com/en/articles/11752874-chatgpt-agent)
|
||||
- [Skyvern — AI RPA Guide (oct. 2025)](https://www.skyvern.com/blog/ai-rpa-guide-intelligent-browser-automation/)
|
||||
- [Skyvern — API-less Legacy System Automation (mai 2026)](https://www.skyvern.com/blog/api-less-system-automation-tools-legacy-enterprise/)
|
||||
- [Skyvern GitHub — Planner-Actor-Validator](https://github.com/Skyvern-AI/skyvern)
|
||||
- [browser-use — Issue #1996 : Need Robust Strategy for Handling Dynamic Popups (ouvert mai 2026)](https://github.com/browser-use/browser-use/issues/1996)
|
||||
- [OmniParser V2 — Microsoft Research](https://microsoft.github.io/OmniParser/)
|
||||
- [OmniParser arXiv 2408.00203](https://arxiv.org/abs/2408.00203)
|
||||
- [UI-TARS arXiv 2501.12326](https://arxiv.org/abs/2501.12326)
|
||||
|
||||
### Détection popup, classifiers légers
|
||||
|
||||
- [PopSweeper — arXiv 2412.02933 (déc. 2024)](https://arxiv.org/abs/2412.02933)
|
||||
- [PopSweeper — HTML lecture directe](https://arxiv.org/html/2412.02933v1)
|
||||
- [ShowUI — arXiv 2411.17465 (2B GUI grounding)](https://arxiv.org/pdf/2411.17465)
|
||||
- [ZonUI-3B cross-resolution GUI grounding](https://arxiv.org/pdf/2506.23491)
|
||||
|
||||
### Sécurité Windows 11 / browser
|
||||
|
||||
- [Microsoft KB UAC fixes oct. 2025 → janv. 2026 — Microsoft Q&A](https://learn.microsoft.com/en-nz/answers/questions/5733506/windows-uac-prompt-becomes-unresponsive-after-cred)
|
||||
- [NinjaOne — Change UAC Behavior Windows 11](https://www.ninjaone.com/blog/change-uac-behavior-for-administrators-in-windows-11/)
|
||||
- [CVE-2026-0628 Chrome Gemini Live panel takeover](https://news.corksafetyalerts.com/chrome-flaw-allowed-extensions-to-hijack-googles-ai-assistant-camera-and-microphone/)
|
||||
- [AI-powered phishing leveraging camera/mic permissions (2026)](https://www.scworld.com/brief/ai-powered-phishing-campaign-leverages-hardware-access-for-data-theft)
|
||||
|
||||
### Safety gates / human-in-the-loop healthcare
|
||||
|
||||
- [Agentic Workflow Approval Gate Framework (4 gate types)](https://www.digitalapplied.com/blog/agentic-workflow-approval-gate-framework-governance)
|
||||
- [AI Agents for Healthcare — Architecture and Safety Guide (Momentum)](https://www.themomentum.ai/blog/ai-agents-healthcare-architecture-safety-implementation)
|
||||
- [Human-in-the-Loop Agentic AI (Elementum)](https://www.elementum.ai/blog/human-in-the-loop-agentic-ai)
|
||||
|
||||
### Références internes (à charger en parallèle de ce doc)
|
||||
|
||||
- `docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` (synthèse maîtresse)
|
||||
- `docs/LESSONS_LEARNED_GHT_2026-05.md` (zones popup F5.5.1, F6.1.1, DETTE-008)
|
||||
- `core/grounding/dialog_handler.py` (commit `487bcb861`)
|
||||
- `memory/feedback_popup_vlm.md` (VLM post-clic, pas ctypes seul)
|
||||
- `memory/feedback_lea_reflexes_catalog.md` (gesture_catalog autorisé, pas hardcode ad hoc)
|
||||
- `memory/feedback_phash_vs_dialog_in_vm.md` (pas pHash global)
|
||||
- `memory/feedback_100pct_visual.md` (jamais raccourci inventé)
|
||||
- `memory/feedback_auth_dialogs_runtime.md` (anticiper Hello/UAC/Basic Auth AVANT démo)
|
||||
|
||||
---
|
||||
|
||||
## 11. Hors-périmètre de ce doc
|
||||
|
||||
À demander à Dom si besoin avant action :
|
||||
|
||||
- Décision finale sur unification `_handle_possible_popup` orphelin vs `_handle_popup_vlm` (les deux côté client).
|
||||
- Politique de rétention RGPD/HDS des screenshots `DialogEvent` (par défaut `data/runner_captures/dialogs/` purge ACK serveur).
|
||||
- Choix exact du modèle VLM compact pour la classification fallback (qwen3-vl:8b acceptable mais ignore parfois `format=json` Ollama — voir §2.4 synthèse).
|
||||
- Bench de la latence `ChangeDetector` sur capture réelle 2560×1600 (cible < 50 ms à vérifier empiriquement).
|
||||
- Politique sur la suppression auto de toasts (Teams, Outlook) pendant démo : déclaratif "do not disturb" en amont vs détection runtime.
|
||||
|
||||
---
|
||||
|
||||
*Document de recherche. Lecture seule. Toute mise en code = décision explicite Dom puis chirurgie itérative supervisée (CLAUDE.md projet).*
|
||||
549
docs/recherche/AXE_D4_MULTI_TENANT_DEPLOY.md
Normal file
549
docs/recherche/AXE_D4_MULTI_TENANT_DEPLOY.md
Normal file
@@ -0,0 +1,549 @@
|
||||
# AXE D4 — Patterns de déploiement multi-tenant / multi-user d'un agent RPA Windows on-premise (2026)
|
||||
|
||||
**Date :** 2026-05-23
|
||||
**Auteur :** Claude (agent de recherche, dispatché)
|
||||
**Périmètre :** packaging, code signing, multi-tenant, silent install, auto-update, observabilité, comparaison concurrents, plan en 3 paliers (POC 2 → 10 → 100+ postes).
|
||||
**Statut :** recherche en lecture seule, aucune modification de code. Sources cliquables en §10.
|
||||
|
||||
---
|
||||
|
||||
## 1. TL;DR — Recommandations immédiates
|
||||
|
||||
**Packaging exe Windows** : **Nuitka commercial standalone** pour Léa client (Agent V1). PyInstaller `--onefile` est à proscrire en milieu hospitalier : 100% des sources convergent sur le taux énorme de faux positifs antivirus, car le pattern self-extracting est indistinguable de malwares connus. Nuitka compile vers du C natif et obfusque le code Python — double bénéfice : moins de faux positifs ET protection IP modérée. Coût : temps de build 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 <token>`. Le token contient en clair (avant hash) un préfixe identifiant : `lea_<tenant_id>_<machine_id_short>_<random_32>` pour faciliter le debug logs sans révéler le secret.
|
||||
|
||||
Snippet de référence (Python serveur) :
|
||||
|
||||
```python
|
||||
import secrets
|
||||
import hashlib
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
def generate_machine_token(tenant_id: str, machine_id: str) -> tuple[str, str]:
|
||||
"""Retourne (token_clair, token_hash). Stocker uniquement le hash."""
|
||||
random_part = secrets.token_urlsafe(32)
|
||||
token = f"lea_{tenant_id}_{machine_id[:8]}_{random_part}"
|
||||
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
||||
return token, token_hash
|
||||
|
||||
def verify_token(token_clair: str, stored_hash: str) -> bool:
|
||||
return hashlib.sha256(token_clair.encode()).hexdigest() == stored_hash
|
||||
```
|
||||
|
||||
**Rotation** : tokens long-lived (90 jours) côté machine, refresh manuel depuis dashboard admin. Pour Phase 3 : pattern OAuth2 client credentials + refresh tokens avec rotation à chaque usage (TOTP-like).
|
||||
|
||||
**Révocation** : flag `status='revoked'` en base + cache court (60s) côté serveur. Avec le transport HTTP pull/long-poll actuel, la révocation prend effet au prochain poll (5-30s). **Avec SSE/WebSocket (AXE_B1)**, le serveur peut fermer la connexion immédiatement à la révocation.
|
||||
|
||||
### 4.3. machine_id unique Windows
|
||||
|
||||
Une seule source ne suffit pas (UUID virtualisé, MAC qui change, hostname dupliqué chez les vraies clinique). Recommandation : **identifiant composite stable**.
|
||||
|
||||
```python
|
||||
import uuid
|
||||
import subprocess
|
||||
import socket
|
||||
import hashlib
|
||||
|
||||
def get_machine_id_composite() -> str:
|
||||
"""ID composite stable sur Windows, fallback Linux/macOS."""
|
||||
components = []
|
||||
|
||||
# 1. WMI UUID (Win32_ComputerSystemProduct) — stable même après reset partiel
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
["wmic", "csproduct", "get", "uuid"],
|
||||
timeout=5, stderr=subprocess.DEVNULL
|
||||
).decode().strip().split("\n")
|
||||
if len(out) > 1:
|
||||
wmi_uuid = out[1].strip()
|
||||
if wmi_uuid and "FFFFFFFF" not in wmi_uuid:
|
||||
components.append(wmi_uuid)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. MAC address de la première interface active (uuid.getnode)
|
||||
mac = uuid.getnode()
|
||||
if (mac >> 40) % 2 == 0: # éviter les MAC aléatoires (bit local)
|
||||
components.append(str(mac))
|
||||
|
||||
# 3. Hostname (NetBIOS / DNS)
|
||||
components.append(socket.gethostname())
|
||||
|
||||
if not components:
|
||||
# Fallback : MachineGuid registre Windows
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
["reg", "query", r"HKLM\SOFTWARE\Microsoft\Cryptography", "/v", "MachineGuid"],
|
||||
timeout=5
|
||||
).decode()
|
||||
for line in out.split("\n"):
|
||||
if "MachineGuid" in line:
|
||||
components.append(line.split()[-1])
|
||||
except Exception:
|
||||
components.append(str(uuid.uuid1())) # ultime fallback
|
||||
|
||||
raw = "|".join(components)
|
||||
return hashlib.sha256(raw.encode()).hexdigest()[:32]
|
||||
```
|
||||
|
||||
**Important** : ne JAMAIS cloner une VM avec Léa installée sans regenerer le `machine_id` (cf. doc Power Automate Desktop : "if you reset your PC, your machine registration will be lost"). Prévoir un script `python -m lea_agent.reset_machine_id` documenté.
|
||||
|
||||
### 4.4. Pattern auth bouchon → cible
|
||||
|
||||
**État actuel (bouchon, Phase 1)** : `DASHBOARD_USER=lea`, `DASHBOARD_PASSWORD=Medecin2026!` en HTTP Basic. Un token global Bearer dans `.env.local` côté NPM reverse proxy pour `lea.labs.laurinebazin.design`.
|
||||
|
||||
**Cible Phase 2** (3 jours d'effort) :
|
||||
1. Migration SQLite → table `users` + `client_machines`.
|
||||
2. `flask-login` ou `fastapi-users` pour sessions dashboard (cookies signés).
|
||||
3. Endpoint admin `POST /api/v1/admin/machines` qui génère un token, retourne le clair UNE seule fois, stocke le hash.
|
||||
4. Endpoint client `GET /api/v1/replay/next` qui vérifie `Authorization: Bearer ...` et update `last_seen`.
|
||||
5. Page dashboard `/admin/machines` : liste, révoquer, rotation, télécharger ZIP installer pré-configuré avec le token.
|
||||
|
||||
**Cible Phase 3** (1 semaine d'effort) :
|
||||
1. Postgres + RLS, JWT avec refresh tokens (durée courte access 15 min, refresh 7 jours).
|
||||
2. SCIM pour synchroniser AD/Entra (utilisateurs hospitaliers).
|
||||
3. TOTP RFC 6238 pour les admins (déjà câblé dans `core/auth/`, à exposer).
|
||||
4. Audit trail signé (chaîne de hash) pour conformité HDS V2.
|
||||
|
||||
---
|
||||
|
||||
## 5. Déploiement silent install + auto-update
|
||||
|
||||
### 5.1. Silent install Windows entreprise
|
||||
|
||||
**MSI > exe self-extracting** en hôpital. Les DSI hospitalières utilisent Intune, SCCM ou GPO, qui sont câblées sur MSI.
|
||||
|
||||
**Commande de référence** (à documenter dans le dossier DSI Anoust) :
|
||||
```
|
||||
msiexec /i Lea-Agent-v1.0.0.msi /qn /norestart ALLUSERS=1 \
|
||||
LEA_SERVER_URL=https://lea.anoust.fr \
|
||||
LEA_MACHINE_TOKEN=lea_anoust_a3b9c2_xxxxxxxxx \
|
||||
INSTALLDIR="C:\Program Files\Lea Agent" \
|
||||
/l*v "C:\Windows\Temp\lea-install.log"
|
||||
```
|
||||
|
||||
**Patterns DSI hospitalier** :
|
||||
- Déploiement par OU (Organisational Unit) AD pour cibler "Postes TIM" ou "Postes Médecins urgences".
|
||||
- Per-machine install (`ALLUSERS=1`) car les RPA tournent sous SYSTEM ou compte de service.
|
||||
- Pré-provisionning du token à l'installation : éviter le pattern "lance Léa, copie-colle un token". À la place, le ZIP téléchargé depuis le dashboard contient un MSI avec le token déjà intégré (custom MSI per machine).
|
||||
|
||||
**Si MSI trop complexe en Phase 2** : `Inno Setup` (gratuit, plus simple que WiX) avec `/VERYSILENT /SUPPRESSMSGBOXES` accepte les déploiements GPO/Intune en mode "Win32 app" Intune. Briefcase BeeWare expose WiX 5.0.2 sans complexité, c'est probablement la voie la plus simple si on reste Python-first.
|
||||
|
||||
### 5.2. Auto-update — recommandation
|
||||
|
||||
**Pour Phase 2 (10 postes Anouste)** : pas d'auto-update automatique. Procédure manuelle : pousser un nouveau MSI via SCCM/Intune ou via un script PowerShell que la DSI exécute. Notification dans le tray Léa "Une nouvelle version est disponible, contactez votre DSI".
|
||||
|
||||
**Pour Phase 3 (100+ postes)** : framework d'auto-update.
|
||||
|
||||
| Framework | Pertinence Léa | Verdict |
|
||||
|---|---|---|
|
||||
| **Velopack** | Réécriture moderne de Squirrel.Windows en Rust, delta updates, multi-langage, multi-plateforme. Actif 2025-2026. | **Recommandé** |
|
||||
| **Squirrel.Windows** | Historique .NET, maintenu mais Velopack est le successeur. | À éviter (legacy) |
|
||||
| **tufup** | Successeur Python de PyUpdater, basé sur python-tuf (sécurisé par design), indépendant du packaging. | **Très pertinent** si on reste Python natif et qu'on ne passe pas par Velopack. |
|
||||
| **PyUpdater** | **Archivé**, ne pas utiliser. | À proscrire |
|
||||
|
||||
**Recommandation finale** : **tufup**. Avantages pour Léa :
|
||||
- Indépendant du packaging (compatible Nuitka et PyInstaller),
|
||||
- Sécurité basée sur TUF (The Update Framework) — signature des manifests, protection contre rollback attacks,
|
||||
- Maintenance active 2025-2026,
|
||||
- Manifest hosting peut être le serveur Linux on-premise existant.
|
||||
|
||||
**Pattern d'update** : side-by-side (installer dans `C:\Program Files\Lea Agent\v1.0.1\` à côté de `v1.0.0\`), bascule du raccourci système au prochain redémarrage. Rollback = pointer le raccourci vers la version précédente, qui reste sur disque pendant N versions (paramétrable). Avantage : zéro corruption sur update interrompue, rollback en < 5 minutes.
|
||||
|
||||
**Audit trail update** : chaque agent log dans `audit_log` côté serveur le succès/échec de l'update, version source, version cible, durée. Permet d'alerter si > 5% de la flotte est sur une ancienne version.
|
||||
|
||||
---
|
||||
|
||||
## 6. Stack serveur multi-tenant on-premise
|
||||
|
||||
### 6.1. docker-compose multi-tenant (squelette de référence)
|
||||
|
||||
Pour Phase 3 (le stack actuel `svc.sh` reste valable Phase 1-2) :
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: lea_main
|
||||
POSTGRES_USER: lea_admin
|
||||
POSTGRES_PASSWORD_FILE: /run/secrets/pg_password
|
||||
volumes:
|
||||
- pg_data:/var/lib/postgresql/data
|
||||
networks: [lea_internal]
|
||||
|
||||
api_stream:
|
||||
image: rpa-vision/api-stream:1.0.0
|
||||
environment:
|
||||
DATABASE_URL: postgresql://lea_admin@postgres/lea_main
|
||||
TENANT_RESOLUTION_HEADER: X-Lea-Tenant-Id
|
||||
networks: [lea_internal, lea_public]
|
||||
depends_on: [postgres]
|
||||
|
||||
ollama:
|
||||
image: ollama/ollama:latest
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- capabilities: [gpu]
|
||||
volumes:
|
||||
- ollama_models:/root/.ollama
|
||||
networks: [lea_internal]
|
||||
|
||||
loki:
|
||||
image: grafana/loki:3.0.0
|
||||
command: -config.file=/etc/loki/loki-config.yaml
|
||||
volumes:
|
||||
- ./loki-config.yaml:/etc/loki/loki-config.yaml
|
||||
- loki_data:/loki
|
||||
networks: [lea_internal]
|
||||
# auth_enabled: true dans loki-config.yaml
|
||||
|
||||
promtail:
|
||||
image: grafana/promtail:3.0.0
|
||||
volumes:
|
||||
- /var/log:/var/log
|
||||
- ./promtail.yaml:/etc/promtail/config.yaml
|
||||
networks: [lea_internal]
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:11.0.0
|
||||
networks: [lea_internal, lea_public]
|
||||
environment:
|
||||
GF_AUTH_GENERIC_OAUTH_ENABLED: "true"
|
||||
# SSO via Keycloak ou Entra
|
||||
```
|
||||
|
||||
### 6.2. Observabilité par tenant
|
||||
|
||||
**Logs (Loki)** : auth multi-tenant activé (`auth_enabled: true`), chaque pod/agent envoie `X-Scope-OrgID: <tenant_id>`. Promtail config :
|
||||
```yaml
|
||||
clients:
|
||||
- url: http://loki:3100/loki/api/v1/push
|
||||
tenant_id: ${LEA_TENANT_ID}
|
||||
```
|
||||
Côté Grafana : un datasource Loki par tenant, ou un seul datasource avec le tenant_id injecté par variable dashboard.
|
||||
|
||||
**Métriques (Prometheus)** : labels `tenant_id="anoust"`, `machine_id="abc123..."`. Quotas par tenant via Cortex/Mimir si on monte en charge.
|
||||
|
||||
**Audit trail RGPD/HDS** : tableau `audit_log` partitionné par tenant + signature SHA-256 chaînée (chaque ligne contient le hash de la précédente). Conservation : 3 ans en hot, 7 ans en cold (S3 ou disque externe), conforme HDS V2.
|
||||
|
||||
### 6.3. Secrets management
|
||||
|
||||
Phase 1-2 : `.env.local` + chmod 600. C'est OK tant qu'on est < 5 tenants.
|
||||
|
||||
Phase 3 : **HashiCorp Vault** self-hosted (image officielle dispo) ou **age** + secrets chiffrés en git. Pour les tokens API machines, déjà couvert par le hash en base. Pour les mots de passe Citrix/Easily Assure, utiliser `core/auth/credential_vault.py` déjà câblé (Fernet+PBKDF2).
|
||||
|
||||
### 6.4. HDS V2 — implications concrètes
|
||||
|
||||
Si Léa stocke des screenshots patients côté serveur ET que ce serveur est on-premise hôpital, l'hôpital est l'hébergeur (HDS interne) et n'a pas besoin de certifier l'éditeur. Si le serveur est chez Dom / chez le client / dans un cloud, **certification HDS V2 obligatoire** (article L1111-8 CSP). Implication produit : prioriser le déploiement on-premise hôpital pour Phase 2-3, le SaaS multi-tenant centralisé est un sujet HDS-lourd (~30-60 k€ d'audit + 6 mois) à n'envisager qu'après stabilisation produit.
|
||||
|
||||
---
|
||||
|
||||
## 7. Comparaison concurrents — multi-tenant et déploiement
|
||||
|
||||
### 7.1. UiPath Robot
|
||||
|
||||
Modèle : licence par runtime (Production Unattended), allocation par tenant (Orchestrator → Admin → Tenants → Edit license allocation). Le robot consomme des runtimes du pool tenant à la connexion, les libère à la déconnexion. **Limite** : robot licences cantonnées à UN tenant — pas de "robot multi-tenant". Pour un hôpital multi-établissements GHT, on créerait 1 tenant par établissement.
|
||||
|
||||
Pattern à reprendre : pool de runtimes au niveau tenant, allocation dynamique. Pertinent pour Phase 3 GHT.
|
||||
|
||||
### 7.2. Power Automate Desktop (Microsoft)
|
||||
|
||||
Modèle : **silent registration** des machines via `Power-Automate-machine-runtime.exe /SILENT REGISTRATIONKEY=xxxx`. Service principal pour l'enrôlement bulk. Multi-session sur Windows Server (RDS) pour scaler à 10-20 bots par machine physique. **Limite critique** : si on clone une VM avec PAD installé, casse l'enrôlement.
|
||||
|
||||
Pattern à reprendre : enrôlement silencieux avec clé pré-générée, **interdiction de cloner les VM** documentée.
|
||||
|
||||
### 7.3. Automation Anywhere Bot Runner
|
||||
|
||||
Modèle : Bot Runners = comptes utilisateurs taggés "runner license" dans Control Room. Déploiement via RDP-based : le Control Room ouvre une session RDP sur la machine cible et lance le bot. Device Pools = groupes logiques pour distribuer la charge.
|
||||
|
||||
Pattern à reprendre : Device Pools pour parallélisation (intéressant pour un déploiement multi-postes TIM dans le même service).
|
||||
|
||||
### 7.4. Skyvern (open source, le plus proche de nous)
|
||||
|
||||
Modèle : docker-compose `postgres + skyvern (API+browser) + skyvern-ui`. Self-hosting complet possible. **Limite documentée** : pas de VNC multi-session en self-hosted, problème d'observabilité multi-bot. Pas de tenancy native dans la version open-source (à coder soi-même).
|
||||
|
||||
Pattern à reprendre : isolation par container Docker pour chaque session active, pratique pour scaler horizontalement. Différentiel pour nous : Léa tourne sur Windows utilisateur, pas dans un container — donc le pattern Skyvern ne s'applique qu'au serveur.
|
||||
|
||||
### 7.5. browser-use
|
||||
|
||||
Modèle : Cloud Skyvern-like, self-hosting Docker en cours de maturation (issue #658 GitHub février 2025). Multi-tenant : non documenté officiellement, communauté demande.
|
||||
|
||||
À surveiller mais pas d'inspiration directe en 2026.
|
||||
|
||||
### 7.6. Stack hospitalière française (Dedalus, Maincare, Easily Assure)
|
||||
|
||||
**Dedalus** et **Maincare** déploient leurs DPI via MSI signés + GPO. Auth utilisateur via SSO LDAP/Active Directory (Kerberos). Pas de modèle "agent par poste" car le DPI est web ou client-serveur — pas applicable directement.
|
||||
|
||||
**Easily Assure** : client lourd Windows (.NET) + serveur central. Authentification par compte utilisateur (couplé AD).
|
||||
|
||||
Implication pour Léa : nos clients DSI savent gérer GPO + MSI signé + AD SSO. **C'est le standard attendu**. Notre dispositif actuel (token Bearer envoyé manuellement) est en dessous des standards de la branche.
|
||||
|
||||
---
|
||||
|
||||
## 8. Plan en 3 paliers
|
||||
|
||||
### Palier 1 — POC 2 postes (actuel, mai 2026)
|
||||
|
||||
**État** : 1 démo + 1 dev. `lea / Medecin2026!` HTTP Basic, MSI absent, `.exe` PyInstaller `--onefile` non signé, install manuel via AnyDesk.
|
||||
|
||||
**Action** : ne rien casser. Documenter clairement le delta produit/cible.
|
||||
|
||||
**Effort** : 0.
|
||||
|
||||
### Palier 2 — 10 postes Anouste + 5 postes GHT pilote (T3 2026)
|
||||
|
||||
**À livrer** :
|
||||
1. **Packaging** : migration Nuitka standalone (commercial, ~250 €/an) ou PyInstaller `--onedir` packagé MSI Briefcase. Cible : binaire signé OV, démarrage < 2s, taille < 200 Mo.
|
||||
2. **Code signing** : achat Sectigo OV 1 an (~150 €) + négociation whitelist SHA256 avec DSI Anoust en parallèle (filet de sécurité 0 €).
|
||||
3. **Multi-tenant** : migration SQLite → tables `tenants`, `users`, `client_machines`, `api_tokens_audit`. `flask-login` pour dashboard. 3 rôles (admin / superviseur / viewer).
|
||||
4. **machine_id composite** (snippet §4.3) intégré au démarrage agent.
|
||||
5. **Token API par machine** généré depuis dashboard admin, MSI custom par machine.
|
||||
6. **Logs par tenant** : préfixe fichier `logs/<tenant_id>/<machine_id>/agent.log`. Rotation logrotate.
|
||||
7. **Silent install MSI** documenté pour DSI Anoust.
|
||||
8. **Audit trail** basique : table `audit_log` (qui a lancé quel replay sur quelle machine).
|
||||
9. **Procédure update** manuelle documentée (DSI pousse le MSI).
|
||||
|
||||
**Effort estimé** : 10-15 jours dev, 3-5 jours validation site Anoust.
|
||||
|
||||
**Dépendance critique** : AXE_B1 transport. Tant qu'on est en HTTP long-poll, la révocation de token a une latence 5-30s. Acceptable Phase 2 mais à corriger Phase 3.
|
||||
|
||||
### Palier 3 — 100+ postes GHT/ARS (T4 2026 - T2 2027)
|
||||
|
||||
**À livrer** :
|
||||
1. **Postgres + RLS** ou schéma par tenant (selon profil clients).
|
||||
2. **JWT access + refresh tokens** avec rotation (15 min / 7 jours).
|
||||
3. **SCIM** pour synchroniser AD/Entra hospitalier.
|
||||
4. **TOTP** pour admins (déjà câblé dans `core/auth/`, à exposer).
|
||||
5. **SignPath.io** pour orchestrer signature CI/CD + audit (~600 €/an).
|
||||
6. **Sectigo EV** ou DigiCert EV avec HSM YubiKey (~400 €/an).
|
||||
7. **tufup** pour auto-update side-by-side + rollback.
|
||||
8. **Loki + Grafana** observabilité avec `X-Scope-OrgID`.
|
||||
9. **Audit trail signé** chaîné SHA-256 pour HDS V2.
|
||||
10. **Quotas par tenant** (nombre de replays/h, taille captures, etc.).
|
||||
11. **Onboarding self-service** tenant via dashboard admin global (création tenant, premier admin, premier token).
|
||||
|
||||
**Effort estimé** : 30-60 jours dev, 10-15 jours sécurité/audit.
|
||||
|
||||
**Décision HDS** à trancher : on-premise hôpital systématique (pas de certification HDS éditeur requise) vs SaaS centralisé (HDS V2 obligatoire, 30-60 k€).
|
||||
|
||||
---
|
||||
|
||||
## 9. Restitution finale (< 250 mots)
|
||||
|
||||
**Packaging** : passer Léa de PyInstaller `--onefile` (faux positifs AV massifs en milieu hospitalier) à **Nuitka standalone** (compilation C native, faux positifs faibles) packagé dans un **MSI WiX via Briefcase** pour déploiement GPO/Intune/SCCM. Étape transitoire acceptable : PyInstaller `--onedir` dans MSI WiX.
|
||||
|
||||
**Code signing — Phase 1** : rester non signé (statu quo). **Phase 2 Anouste** : achat **Sectigo OV 1 an ~150 € HT** + négociation whitelist SHA256 DSI en parallèle. **Phase 3** : Sectigo EV avec HSM YubiKey + orchestration SignPath.io (~600 €/an). Azure Trusted Signing **bloqué** car réservé US/Canada + 3 ans d'ancienneté — ne pas planifier dessus, contrairement à ce qui était espéré dans `project_code_signing.md`.
|
||||
|
||||
**Multi-tenant 3 paliers** :
|
||||
1. **POC 2 postes** : statu quo, machine_id = UUID WMI.
|
||||
2. **10 postes** : tables `users`/`client_machines`, tokens API par machine, 3 rôles RBAC, `flask-login`, silent MSI, logs par tenant.
|
||||
3. **100+ postes** : Postgres + RLS, JWT refresh, SCIM AD, TOTP admins, tufup auto-update, Loki/Grafana multi-tenant, audit trail signé HDS V2.
|
||||
|
||||
**Dépendances** :
|
||||
- **AXE_B1 (transport HTTP → SSE/WebSocket)** : prérequis pour révocation de token immédiate et fermeture de session propre. Avec HTTP long-poll, latence 5-30s sur révocation.
|
||||
- **AXE_B5_D1 (capture distante)** : ne pas brouiller le nom du binaire (Léa ≠ outil de prise en main DSI).
|
||||
|
||||
**HDS V2** : on-premise hôpital systématique en Phase 2-3 pour éviter le coût de certification HDS éditeur (30-60 k€, 6 mois). SaaS centralisé hors périmètre tant que produit non stabilisé.
|
||||
|
||||
---
|
||||
|
||||
## 10. Sources
|
||||
|
||||
### Packaging Python → exe
|
||||
|
||||
- [From PyInstaller to Nuitka: Convert Python to EXE Without False Positives (DEV.to)](https://dev.to/weisshufer/from-pyinstaller-to-nuitka-convert-python-to-exe-without-false-positives-19jf)
|
||||
- [How to Fix Antivirus False Positives with PyInstaller Executables (Python GUIs)](https://www.pythonguis.com/faq/problems-with-antivirus-software-and-pyinstaller/)
|
||||
- [Compilation vs Bundling: The Real Differences Between Nuitka and PyInstaller (KRRT7)](https://krrt7.dev/en/blog/nuitka-vs-pyinstaller)
|
||||
- [Best PyInstaller Alternatives 2026 (No AV Flags)](https://beatsyncpro.ai/alternative/pyinstaller.html)
|
||||
- [2026 Showdown: PyInstaller vs cx_Freeze vs Nuitka For Python EXE Builds](https://ahmedsyntax.com/2026-comparison-pyinstaller-vs-cx-freeze-vs-nui/)
|
||||
- [Nuitka Performance documentation](https://nuitka.net/user-documentation/performance.html)
|
||||
- [Nuitka Compilation: C-Level Python Performance Boost 2026](https://www.johal.in/nuitka-compilation-c-level-python-performance-boost-2026/)
|
||||
- [PyOxidizer has been abandoned (Anki Issue #3081)](https://github.com/ankitects/anki/issues/3081)
|
||||
- [Briefcase Windows MSI documentation](https://briefcase.beeware.org/en/stable/reference/platforms/windows/)
|
||||
- [GitHub - beeware/briefcase-windows-msi-template](https://github.com/beeware/briefcase-windows-msi-template)
|
||||
- [PyInstaller AV false positive Issue #6754](https://github.com/pyinstaller/pyinstaller/issues/6754)
|
||||
|
||||
### Code signing
|
||||
|
||||
- [Azure Artifact Signing FAQ (Microsoft Learn)](https://learn.microsoft.com/en-us/azure/artifact-signing/faq)
|
||||
- [Azure Artifact Signing Pricing](https://azure.microsoft.com/en-us/pricing/details/artifact-signing/)
|
||||
- [Trusted Signing open for individual developers - Public Preview](https://techcommunity.microsoft.com/blog/microsoft-security-blog/trusted-signing-is-now-open-for-individual-developers-to-sign-up-in-public-previ/4273554)
|
||||
- [Trusted Signing 3-year requirement Q&A](https://learn.microsoft.com/en-us/answers/questions/2261318/is-there-any-exception-process-for-the-azure-trust)
|
||||
- [Fighting through Setting up Microsoft Trusted Signing (Rick Strahl)](https://weblog.west-wind.com/posts/2025/Jul/20/Fighting-through-Setting-up-Microsoft-Trusted-Signing)
|
||||
- [Code signing on Windows with Azure Trusted Signing (Melatonin)](https://melatonin.dev/blog/code-signing-on-windows-with-azure-trusted-signing/)
|
||||
- [Top 10 Best Code Signing Certificate Providers in 2026](https://sslinsights.com/best-code-signing-certificate-providers/)
|
||||
- [Sectigo EV Code Signing Certificate ($279.99/yr SignMyCode)](https://signmycode.com/sectigo-ev-code-signing)
|
||||
- [Sectigo EV Code Signing Certificates (TheSSLStore)](https://www.thesslstore.com/sectigo/sectigo-ev-code-signing-certificate.aspx)
|
||||
- [SignPath Pricing](https://about.signpath.io/product/pricing)
|
||||
- [SmartScreen reputation for Windows app developers (Microsoft Learn)](https://learn.microsoft.com/en-us/windows/apps/package-and-deploy/smartscreen-reputation)
|
||||
- [Code signing options for Windows app developers (Microsoft Learn)](https://learn.microsoft.com/en-us/windows/apps/package-and-deploy/code-signing-options)
|
||||
- [Automate PyInstaller Builds and Code Signing (johanneskinzig)](https://johanneskinzig.com/automating-pyinstaller-builds-and-code-signing-with-powershell.html)
|
||||
|
||||
### Multi-tenant, auth, machine_id
|
||||
|
||||
- [Multi-Tenant Architecture with FastAPI: Design Patterns and Pitfalls (Medium)](https://medium.com/@koushiksathish3/multi-tenant-architecture-with-fastapi-design-patterns-and-pitfalls-aa3f9e75bf8c)
|
||||
- [Multitenancy with FastAPI - A practical guide](https://app-generator.dev/docs/technologies/fastapi/multitenancy.html)
|
||||
- [Building Multi-Tenant APIs with FastAPI and Subdomain Routing](https://medium.com/@diwasb54/building-multi-tenant-apis-with-fastapi-and-subdomain-routing-a-complete-guide-cc076cb02513)
|
||||
- [FastAPI RBAC Permissions: Role-Based Access for ML Resources 2026](https://www.johal.in/fastapi-rbac-permissions-role-based-access-for-ml-resources-2026/)
|
||||
- [Get a unique computer ID in Python on Windows and Linux](https://www.iditect.com/faq/python/get-a-unique-computer-id-in-python-on-windows-and-linux.html)
|
||||
- [Get Windows Unique ID by Python](https://nashorn892087495.wordpress.com/2019/09/12/get-windows-unique-id-by-python/)
|
||||
- [JWT in FastAPI - Refresh Tokens Explained (Medium)](https://medium.com/@jagan_reddy/jwt-in-fastapi-the-secure-way-refresh-tokens-explained-f7d2d17b1d17)
|
||||
|
||||
### Silent install et auto-update
|
||||
|
||||
- [Silent install cheatsheet (GitHub)](https://github.com/offlineinstallersetup/silent-install-cheatsheet)
|
||||
- [A Guide to Install MSI Silently (Server Scheduler)](https://serverscheduler.com/blog/install-msi-silently)
|
||||
- [Velopack - Cross-platform installer and auto-update framework](https://velopack.io/)
|
||||
- [Velopack documentation - Migrating from Squirrel](https://docs.velopack.io/migrating/squirrel)
|
||||
- [tufup - Automated updates for Python apps (GitHub)](https://github.com/dennisvang/tufup)
|
||||
- [PyUpdater archived](https://github.com/Digital-Sapphire/PyUpdater)
|
||||
|
||||
### Concurrents RPA
|
||||
|
||||
- [UiPath Orchestrator About Licensing](https://docs.uipath.com/orchestrator/docs/about-licensing)
|
||||
- [UiPath Robot Licensing Standalone 2025.10](https://docs.uipath.com/robot/standalone/2025.10/admin-guide/licensing-troubleshooting)
|
||||
- [UiPath Automation Suite - Allocating Robot Licenses to Tenants](https://docs.uipath.com/automation-suite/docs/allocating-robot-and-service-licenses-to-tenants)
|
||||
- [Power Automate Desktop - Silent registration for machines](https://learn.microsoft.com/en-us/power-automate/desktop-flows/machines-silent-registration)
|
||||
- [Power Automate Desktop - Manage machine groups](https://learn.microsoft.com/en-us/power-automate/desktop-flows/manage-machine-groups)
|
||||
- [Power Automate Desktop - Hosted machines](https://learn.microsoft.com/en-us/power-automate/desktop-flows/hosted-machines)
|
||||
- [Automation Anywhere - RDP-based bot deployment](https://docs.automationanywhere.com/bundle/enterprise-v11.3/page/enterprise/topics/control-room/bots/my-bots/rdp-based-approach-to-bot-deployment.html)
|
||||
- [Skyvern Docker Setup](https://docs-new.skyvern.com/self-hosted/docker)
|
||||
- [Skyvern Issue #4392 - Multi-session VNC support](https://github.com/Skyvern-AI/skyvern/issues/4392)
|
||||
- [browser-use Issue #658 - Docker Image for Self Hosting](https://github.com/browser-use/browser-use/issues/658)
|
||||
|
||||
### Observabilité multi-tenant
|
||||
|
||||
- [Grafana Loki - Manage tenant isolation](https://grafana.com/docs/loki/latest/operations/multi-tenancy/)
|
||||
- [Creating Multi-Tenant Observability Dashboards with Grafana & Loki (2025)](https://sollybombe.medium.com/creating-multi-tenant-observability-dashboards-with-grafana-loki-2025-edition-85a673eff596)
|
||||
- [Managing Grafana and Loki in a regulated multitenant environment (AWS)](https://aws.amazon.com/blogs/opensource/how-to-manage-grafana-and-loki-in-a-regulated-multitenant-environment/)
|
||||
|
||||
### HDS / RGPD / DSI hospitalière France
|
||||
|
||||
- [Certification HDS en 2026 - ce que chaque hôpital doit vérifier (Galeon)](https://www.galeon.care/fr/blog/certification-hds-en-2026-ce-que-chaque-hopital-doit-verifier-avant-de-signer)
|
||||
- [HDS V2 certification framework (LSTI)](https://www.lsti-certification.fr/en/News/certification-HDS)
|
||||
- [HDS - Agence du Numérique en Santé](https://esante.gouv.fr/ens/offre/hds)
|
||||
- [Health Data Hosting (HDS) France - Microsoft Compliance](https://learn.microsoft.com/en-us/compliance/regulatory/offering-hds-france)
|
||||
- [Doctrine Numérique Santé 2025 - Règles de sécurité](https://esante.gouv.fr/doctrine/securite)
|
||||
|
||||
---
|
||||
|
||||
*Document destiné à être consommé en lecture seule par Dom comme support de décision sur les axes packaging, code signing et architecture multi-tenant. Pas d'action engagée. Validation explicite requise avant chaque étape de Palier 2 ou 3.*
|
||||
408
docs/recherche/AXE_E_FRAMEWORKS_BENCHMARKS.md
Normal file
408
docs/recherche/AXE_E_FRAMEWORKS_BENCHMARKS.md
Normal file
@@ -0,0 +1,408 @@
|
||||
# Axe E — Référentiel benchmarks GUI 2026 & delta frameworks RPA visuels
|
||||
|
||||
**Date :** 2026-05-23
|
||||
**Auteur :** Claude (subagent veille) via Dom
|
||||
**Périmètre :** veille externe — pas de modif code.
|
||||
**Source de référence à mettre à jour :** `docs/INSPIRATION_FRAMEWORKS_2026-05-10.md` (10 mai 2026) + `docs/superpowers/specs/2026-05-05-qw-suite-mai-design.md` (5 mai 2026).
|
||||
**Statut :** veille brute, à valider avec Dom avant toute action.
|
||||
|
||||
---
|
||||
|
||||
## 1. TL;DR
|
||||
|
||||
**En 2 semaines (10 → 23 mai 2026), 3 mouvements à retenir :**
|
||||
|
||||
1. **OSWorld n'est plus humain-level, il est passé super-humain.** Coasty (open source, github `coasty-ai/open-computer-use`) annonce 82 % sur OSWorld vs ~72 % humain, devant Claude Sonnet 4.6 à 73 % et Agent-S3 (Simular) à 69,9 %–72,6 % (bBoN). OpenAI Operator stagne à 38 %. La marche entre "agent qui copie l'humain" et "agent qui fait mieux" est franchie côté frontière open source.
|
||||
2. **WebVoyager est saturé.** Om Labs 98,9 %, Alumnium 98,5 %, Magnitude 94 %. Skyvern 2.0 (85,85 %) n'est plus SOTA. Le benchmark ne discrimine plus — Skyvern a anticipé en lançant **Web Bench** (5 750 tâches × 452 sites, partenariat Halluminate).
|
||||
3. **MCP est devenu standard d'agent.** 97 M downloads SDK mensuel en mars 2026 (+970× en 18 mois), 78 % des équipes IA enterprise déclarent au moins un agent MCP en prod (avril 2026). Microsoft Agent 365 (GA 1er mai 2026) intègre gouvernance MCP au niveau tenant. Anthropic, OpenAI, Google Gemini et Vercel SDK supportent tous MCP nativement.
|
||||
|
||||
**Tendances 2026 :**
|
||||
- Mixture-of-Grounding et Best-of-N rollouts (Agent S2/S3 « bBoN ») remplacent le single-pass.
|
||||
- Continual learning sur GUI (GUI-AiF, AAAI 2026) émerge — replay engine devient training ground.
|
||||
- Le rythme des sorties papier sur arXiv (AAAI 2026, ICLR 2026) double vs 2025 sur la verticale "GUI agent".
|
||||
|
||||
**Recommandation immédiate pour rpa_vision_v3 :**
|
||||
- Adopter **ScreenSpot-Pro** (1 581 instructions, 23 apps, 3 OS, leaderboard maintenu jusqu'à mai 2026) comme bench de grounding interne — c'est le seul qui a des screenshots haute résolution réalistes (notre cas Easily Assure).
|
||||
- Surveiller **Coasty open-computer-use** (apparu post-doc inspiration) et **Agent-S3 bBoN** — les deux poussent un pattern Best-of-N qui résoudrait notre Validator laxiste (cf. §7).
|
||||
|
||||
---
|
||||
|
||||
## 2. Carte référentiel benchmarks GUI 2026
|
||||
|
||||
| Benchmark | Mesure | Type tâches | Utilité rpa_vision_v3 |
|
||||
|---|---|---|---|
|
||||
| **ScreenSpot** (V1) | Grounding pur (clic) sur captures recadrées | 1 272 instructions web/desktop/mobile | Faible (résolutions trop basses, "consumer apps") |
|
||||
| **ScreenSpot-V2** | Idem V1, 11,32 % de samples re-annotés | Idem V1 corrigé | Référence académique, pas notre cas réel |
|
||||
| **ScreenSpot-Pro** | **Grounding haute résolution pro** | 1 581 instructions, 23 apps pro, 3 OS, écrans HD | **★★★★★ — notre cas** |
|
||||
| **WindowsAgentArena** | Agent autonome end-to-end Windows | 154 tâches Windows (Notepad, Paint, navigateurs, etc.) | **★★★★ — OS cible** |
|
||||
| **OSWorld** | Agent autonome end-to-end multi-OS | 369 tâches (LibreOffice, Chrome, VS Code, file mgmt) | **★★★★ — gold standard "agent"** |
|
||||
| **OSWorld-Verified** | OSWorld durci anti-gaming (juillet 2025) | Sous-ensemble vérifié humain | ★★★ |
|
||||
| **WebVoyager** | Agent web SOTA | 610 tâches sites live, jugement GPT | ★★ — saturé, pas notre cible (browser only) |
|
||||
| **Online-Mind2Web** | Agent web réaliste | 300 tâches × 136 sites | ★★ |
|
||||
| **Web Bench** (Skyvern + Halluminate) | Agent web large couverture | 5 750 tâches × 452 sites | ★★ |
|
||||
| **AgentBench** (THUDM) | LLM-as-agent multi-environnement | 8 envs (OS, SQL, KG, jeux, web, etc.) | ★ — trop générique |
|
||||
| **VisualWebBench** | Compréhension/grounding web MLLM | 1,5 k instances × 139 sites | ★ |
|
||||
| **GUI-World** (ICLR 2025) | Compréhension **vidéo** GUI | 6 scénarios × 8 types Q dynamiques | ★ — pas notre angle replay |
|
||||
| **AndroidWorld** | Mobile Android agent | 116 tâches × 20 apps Android | ✗ — hors scope healthtech desktop |
|
||||
| **AndroidArena / A3** | Mobile dynamique | Tâches réalistes en ligne | ✗ |
|
||||
| **MobileWorld** (ACL 2026) | Mobile + MCP-augmented | Tâches user-interactive | ✗ |
|
||||
|
||||
**Carte de couverture qui mesure quoi :**
|
||||
- **Grounding seul (point/bbox)** → ScreenSpot-Pro (★ pour nous), ScreenSpot-V2, VisualWebBench.
|
||||
- **Agent autonome Windows** → WindowsAgentArena (★ pour nous).
|
||||
- **Agent autonome multi-OS** → OSWorld, OSWorld-Verified (★ pour nous, partiellement).
|
||||
- **Agent web** → WebVoyager (saturé), Online-Mind2Web, Web Bench.
|
||||
- **Compréhension vidéo GUI** → GUI-World.
|
||||
- **Mobile** → AndroidWorld, AndroidArena, MobileWorld (hors scope).
|
||||
|
||||
---
|
||||
|
||||
## 3. Fiches des 5 benchmarks les plus pertinents pour nous
|
||||
|
||||
### 3.1 ScreenSpot-Pro — `arxiv:2504.07981`
|
||||
|
||||
- **Composition** : 1 581 instructions, 1 instruction par screenshot unique, 23 applications professionnelles, 5 secteurs (CAD, dev, ingénierie, science, design), 3 OS (Windows, macOS, Linux). Annotations expert humain.
|
||||
- **Métriques** : taux de clic correct (point dans bbox vérité-terrain), bbox IoU.
|
||||
- **Dataset accessible** : github `likaixin2000/ScreenSpot-Pro-GUI-Grounding`, leaderboard public `gui-agent.github.io/grounding-leaderboard/` (MAJ 14 avril 2026).
|
||||
- **SOTA mai 2026** :
|
||||
- GPT-5.2 (OpenAI) : 86,3 %
|
||||
- GPT-5.4 (OpenAI) : 85,4 % (référence `benchlm.ai`)
|
||||
- Muse Spark : 84,1 %
|
||||
- Gemini 3 Pro (Google) : 72,7 %
|
||||
- Qwen3.5 (féb 2026) : 70,3 % overall
|
||||
- Qwen3.5-35B-A3B : 68,6 %
|
||||
- Qwen2.5-VL-72B + RegionFocus : 61,6 %
|
||||
- Baseline historique (papier original) : 18,9 % (modèles non spécialisés).
|
||||
- **Lien** : https://arxiv.org/abs/2504.07981
|
||||
- **Pertinence rpa_vision_v3** : c'est **le seul bench grounding qui ressemble vraiment à Easily Assure** — résolutions ≥ 1920×1080, mix de menus denses, panneaux à droite, tableaux. Notre `MIGRATION_VLM_PLAN_2026-05-09.md` cite ScreenSpot-Pro mais nous n'avons pas de score interne récent à comparer.
|
||||
|
||||
### 3.2 WindowsAgentArena (WAA) — `arxiv:2409.08264`
|
||||
|
||||
- **Composition** : 154 tâches Windows réelles (Notepad, Paint, File Explorer, Clock, Settings, browsers, documents, vidéo, code).
|
||||
- **Métriques** : success rate task-level, parallélisable en Azure (~20 min run complet).
|
||||
- **Dataset accessible** : github `microsoft/WindowsAgentArena`, paper page `huggingface.co/papers/2409.08264`.
|
||||
- **SOTA mai 2026** :
|
||||
- UI-TARS-2 (ByteDance, sept 2025) : 50,6 %
|
||||
- Multi-modal Navi (Microsoft, baseline) : 19,5 %
|
||||
- Humain : 74,5 %
|
||||
- **Lien** : https://microsoft.github.io/WindowsAgentArena/
|
||||
- **Pertinence rpa_vision_v3** : **★★★★★ pour positionnement client GHT** — Windows = terrain réel des TIM. Le gap 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é.*
|
||||
@@ -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.*
|
||||
279
docs/recherche/INDEX_REPLAY_SPECS_2026-05-24.md
Normal file
279
docs/recherche/INDEX_REPLAY_SPECS_2026-05-24.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# Index — Specs opérationnelles replay (transport / validator / popups)
|
||||
|
||||
**Date :** 2026-05-24
|
||||
**Auteur :** Claude (session principale) à partir des 3 specs ciblées + 6 docs de recherche préalables.
|
||||
**Public :** humain ou agent qui doit prendre en charge l'un des 3 chantiers de fiabilisation replay post-démo GHT.
|
||||
**Statut :** lecture seule. Aucune décision figée. Plan d'action proposé à valider par Dom.
|
||||
|
||||
---
|
||||
|
||||
## 0. Comment lire ce document
|
||||
|
||||
Tu prends en charge un chantier replay → tu lis §1 (TL;DR) et tu sautes directement à la spec correspondante (§3.1, §3.2 ou §3.3). Tu reviens à §2 (croisements) si ton chantier touche les deux autres. Les décisions ouvertes Dom sont consolidées §4.
|
||||
|
||||
Les 3 specs s'appuient sur des **recherches préalables** déjà livrées (`AXE_B1`, `AXE_B1_DEEP`, `AXE_B2`, `AXE_B2_DEEP`, `AXE_D2`, `AXE_D2_DEEP`). Les specs ne refont pas l'étude — elles produisent le contrat opérationnel.
|
||||
|
||||
---
|
||||
|
||||
## 1. TL;DR
|
||||
|
||||
**3 chantiers replay à mener en parallèle pour fermer les bugs racines post-démo GHT (~3 j homme MVP cumulés)** :
|
||||
|
||||
| # | Chantier | Bug racine fermé | Spec | Code prêt | Effort MVP |
|
||||
|---|---|---|---|---|---|
|
||||
| **B1** | Transport + watchdog | Désync 8 mai (9 actions perdues en 33s) | `SPEC_TRANSPORT_CONTRAT.md` | `replay_watchdog.py` ~270 LOC + patches `api_stream.py` | **3h30** |
|
||||
| **B2** | Validator post-action | Step 10 (clic Imagerie dans Edge, REPORT success=True) | `SPEC_VALIDATOR_MATRICE.md` | package `core/validation/` ~590 LOC (MVP P0 ~190 LOC) | **8h** |
|
||||
| **D2** | Chaîne popup propre | `_handle_possible_popup` orphelin + auto-dismiss risqué | `SPEC_POPUPS_CATALOGUE.md` | package `core/dialog/` ~700 LOC + 59 entrées catalogue | **1j** |
|
||||
|
||||
**Résultat attendu cumulé** : démo prochaine sans contournement `static_result/static_text`, sans `cancel-replays.sh` manuel, sans pause humaine sur dialogs métier connus.
|
||||
|
||||
**Contraintes invariantes respectées** :
|
||||
- 100% vision (aucun raccourci système inventé)
|
||||
- Healthtech (jamais d'auto-accept UAC / Hello / SmartScreen / suppression non déclarée)
|
||||
- Backward compatible (kill-switches env var par défaut OFF sur chaque chantier)
|
||||
|
||||
---
|
||||
|
||||
## 2. Croisements entre les 3 specs
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ ┌───────────────┐ ┌───────────────┐ │
|
||||
│ │ B1 Transport │◄────────│ B2 Validator │ │
|
||||
│ │ + Watchdog │ ack │ + 6 Checkers │ │
|
||||
│ └───────────────┘ └───────────────┘ │
|
||||
│ ▲ │ │
|
||||
│ │ purge si paused │ failure_category= │
|
||||
│ │ ▼ UNEXPECTED_DIALOG │
|
||||
│ │ ┌───────────────┐ │
|
||||
│ └──────────────────│ D2 Popup │ │
|
||||
│ │ Resolver │ │
|
||||
│ └───────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Dépendances et contrats inter-modules
|
||||
|
||||
| Source | Cible | Contrat | Quand |
|
||||
|---|---|---|---|
|
||||
| B1 | B2 | Action arrive bien côté serveur (REPORT non perdu) | Prérequis. Sans B1, B2 vote sur du vide. |
|
||||
| B2 | D2 | `failure_category=UNEXPECTED_DIALOG/NO_VISUAL_CHANGE/WRONG_APPLICATION` → `DialogResolver.resolve()` | Au verdict d'échec d'un Checker |
|
||||
| D2 | B1 | Si modal détecté pendant qu'une action est en `_retry_pending`, watchdog doit purger | v1.1 du watchdog (déjà partiel via `cancel_replay` ligne 4489) |
|
||||
| D2 | B2 | `DialogPresenceChecker` (Checker B2) fournit la bbox utilisée par `DialogClosedChecker` | Coordination intra-Validator |
|
||||
| B2 | B1 | Verdict `WRONG_APPLICATION` (bug step 10) → override `success=True → False` → relance watchdog | Au report client |
|
||||
|
||||
### Orthogonalité
|
||||
|
||||
- **B1 ↔ B2** : techniquement orthogonaux (peuvent être codés en parallèle), mais **les deux ensemble** = fermeture totale du bug du 8 mai (transport + validation).
|
||||
- **D2** : indépendant de B1 et B2 pour l'implémentation, mais consomme leurs interfaces au runtime.
|
||||
|
||||
---
|
||||
|
||||
## 3. Résumé par spec
|
||||
|
||||
### 3.1. SPEC_TRANSPORT_CONTRAT.md
|
||||
|
||||
**Chemin** : `/home/dom/ai/rpa_vision_v3/docs/recherche/SPEC_TRANSPORT_CONTRAT.md`
|
||||
**Volume** : 766 lignes, 169 lignes de tables.
|
||||
**Recherches sources** : `AXE_B1_REPLAY_TRANSPORT.md`, `AXE_B1_DEEP_WATCHDOG.md`.
|
||||
|
||||
**Contenu** :
|
||||
- 2 state machines ASCII (serveur PENDING → DISPATCHED → ORPHAN → ACKED → ABANDONED → PAUSE | client POLLING → RECEIVED → DEDUP → EXECUTING → REPORTING → ACKED) avec invariants I1-I6 et C1-C5.
|
||||
- Contrats JSON DISPATCH / REPORT / re-dispatch / pause / resume / cancel, avec lignes de code source précises (`api_stream.py:626-651` schema, `3354-3359` retry_pending, `4361-4474` resume, `4489` cancel).
|
||||
- **Matrice 21 cas limites** (a→u) couvrant timeout pré-réponse, déconnexion post-réception, report perdu, watchdog race, client mort, server restart, polls concurrents, idempotence double-clic, pause pendant action en vol, cancel UI, abandon MAX_RESENDS, etc.
|
||||
- Sémantique d'idempotence à 7 couches + spec `dedup_set` client copy-paste + idempotence par type d'action.
|
||||
- 18 timeouts/seuils tabulés avec env vars (`RPA_WATCHDOG_ORPHAN_TIMEOUT_S=30`, `RPA_WATCHDOG_SCAN_INTERVAL_S=10`, `RPA_WATCHDOG_MAX_RESENDS=2`, etc.).
|
||||
- Transitions pause supervisée & resume (5 déclencheurs `paused_need_help`).
|
||||
- Compatibilité transport polling ↔ SSE (table d'invariance).
|
||||
- 7 fiches précédents externes (SQS, NATS, Skyvern, browser-use, Anthropic CU, Playwright MCP) + table comparative.
|
||||
|
||||
**Code production-ready dans le doc** : module `agent_v0/server_v1/replay_watchdog.py` (~270 lignes), 4 patches diff unified `api_stream.py`, 8 tests pytest `tests/integration/test_replay_watchdog.py`.
|
||||
|
||||
**Effort intégration** : **3h30** (45min schéma+watchdog, 30min câblage, 1h tests pytest, 1h chasse races sur E2E réel, 15min DETTE).
|
||||
|
||||
---
|
||||
|
||||
### 3.2. SPEC_VALIDATOR_MATRICE.md
|
||||
|
||||
**Chemin** : `/home/dom/ai/rpa_vision_v3/docs/recherche/SPEC_VALIDATOR_MATRICE.md`
|
||||
**Volume** : ~1300 lignes denses.
|
||||
**Recherches sources** : `AXE_B2_VALIDATOR_PATTERN.md`, `AXE_B2_DEEP_VALIDATOR.md`, `AXE_A4_OCR_TEMPLATE_PHASH.md`.
|
||||
|
||||
**Contenu** :
|
||||
- Matrice principale **27 types d'action × 9 colonnes** (signal primaire / secondaire / fallback / verdicts possibles / latence cible / coût / Checker à utiliser).
|
||||
- **4 fiches détaillées** pour les cas listés explicitement par Dom :
|
||||
- `switch_tab` (le bug step 10) : signal primaire = `OcrRoiChecker` ROI 120×40 autour du tab attendu cherchant label exact dans words OCR ; secondaire = pHash de la zone contenu sous les tabs a changé ≥ 5%.
|
||||
- `close_tab` : `TabAbsenceChecker` + visibilité tab voisin actif.
|
||||
- `save` : disparition indicateur "modifié" + apparition toast "Enregistré".
|
||||
- `dialog_button` : disparition du dialog (handoff D2 via `DialogClosedChecker`).
|
||||
- 5 fiches secondaires (`click_anchor`, `type_text`, `extract_text`, `t2a_decision`, `keyboard_shortcut`).
|
||||
- **6 Checkers production-ready** : `PixelDiffChecker` (15ms), `OcrRoiChecker` (80ms, **résout step 10**), `TitleBarChecker` (130ms wrapper existant), `JsonSchemaChecker` (10ms), `LlmJudgeChecker` (3s wrapper `verify_with_critic` existant `replay_verifier.py:367`), `TabActiveChecker`/`TabAbsenceChecker`/`SaveSuccessChecker`/`DialogClosedChecker` (nouveaux).
|
||||
- Confidence scoring + règles d'agrégation multi-checker (`switch_tab` → SUCCESS si primaire ≥ 0.85 OU primaire ≥ 0.65 AND secondaire ≥ 0.70).
|
||||
- **Anti-patterns table 12 entrées** : pHash global pour switch_tab, title-bar seule pour SPA, `success=True` parce que coords envoyées, SSIM global dialog, etc.
|
||||
- 6 précédents externes (Skyvern `complete_verify`, browser-use `evaluation` agent, Playwright assertions, Selenium `expected_conditions`, SikuliX `waitVanish`, PyImageSearch).
|
||||
- Plan d'intégration en 3 étapes graduées (1j MVP / 2 sem couverture complète / 1 mois bench).
|
||||
|
||||
**Réutilisation existant** : `OcrRoiChecker` réutilise le singleton EasyOCR de `TitleVerifier._get_ocr()` (zéro coût d'init).
|
||||
|
||||
**Reproduction offline du bug step 10** : script `repro_bug_step10_validator.py` fourni + test pytest `test_validator_step10.py`, sur capture `visual_workflow_builder/backend/data/anchors/anchor_0438bd2d9bdd_1778161174_full.png`.
|
||||
|
||||
**Wiring** : diff unified format insertion à `api_stream.py:3447-3582` derrière flag `RPA_VALIDATOR_V2_ENABLED=false` (default OFF, aucune régression flag off).
|
||||
|
||||
**Budget latence démo 46 steps** : **+11s cumulés** (vs 33s perdus en pause/reprise step 10).
|
||||
|
||||
**Effort intégration P0** : **8h** (création package 4h + tests 1.5h + patch api_stream 2h + smoke 30min).
|
||||
|
||||
---
|
||||
|
||||
### 3.3. SPEC_POPUPS_CATALOGUE.md
|
||||
|
||||
**Chemin** : `/home/dom/ai/rpa_vision_v3/docs/recherche/SPEC_POPUPS_CATALOGUE.md`
|
||||
**Volume** : 1758 lignes après enrichissement.
|
||||
**Recherches sources** : `AXE_D2_DIALOG_POPUP.md`, `AXE_D2_DEEP_POPUP_CHAIN.md`.
|
||||
|
||||
**Contenu** :
|
||||
- **Section 2bis — Catalogue compact (format Dom)** avec exactement 5 colonnes : `ID | Titre exact (FR/EN) | Appli source | Boutons attendus | Politique`.
|
||||
- **Politique trichotomie stricte** :
|
||||
- **`auto`** = Léa clique un bouton précis (action explicite définie)
|
||||
- **`pause`** = Léa s'arrête et attend décision humaine
|
||||
- **`skip`** = Léa ignore le modal (ne clique rien, ne s'arrête pas)
|
||||
- **59 entrées catalogue** réparties en 5 catégories (A SYSTÈME, B NAVIGATEUR, C MÉTIER, D APP TIERS, E INCONNU) :
|
||||
- **44 `pause`** : toute la catégorie A SYSTÈME + identification + suppression + warnings cliniques
|
||||
- **10 `auto`** : save/confirm Easily, dialogs métier connus, dialogs disposables avec clic explicite
|
||||
- **5 `skip`** : `browser-translate-prompt`, `easily-toast-saved`, `outlook-reminder`, `chrome-update`, `edge-update`
|
||||
- 10 fiches détaillées des modaux critiques (UAC, Hello, SmartScreen, save unconfirmed, browser perms, etc.) avec capture-type ASCII.
|
||||
- **Workflow VWB déclaratif** `expected_modal` (YAML + Pydantic schema) avec validateur `system_modals_cannot_be_overridden` qui **rejette toute politique ≠ `pause` sur préfixes `windows-` / `defender-`** — un workflow VWB ne peut pas forcer un UAC en auto.
|
||||
- Snippet Python `KNOWN_DIALOGS` étendu (781 lignes, syntaxe valide `ast.parse` OK) avec champs `window_title`, `app_source`, `policy` (`auto`/`pause`/`skip`), `declarative_override: bool`. Helpers `get_metadata()` et `can_be_overridden()`.
|
||||
- Tests offline pytest + protocole de capture.
|
||||
- 7 précédents externes (Skyvern issue #69, browser-use issue #1996, Anthropic CU human takeover, OpenAI Operator watch mode, AutoIt/Sikuli, pywinauto, Selenium JS alerts).
|
||||
|
||||
**Décisions tranchées par les agents** :
|
||||
- `_handle_possible_popup` orphelin **supprimé** (0 site d'appel + antipattern Tab+Enter aveugle).
|
||||
- `_handle_popup_vlm` actif **conservé mais simplifié** → devient client léger d'un nouvel endpoint `POST /api/v1/dialog/resolve`.
|
||||
- DialogResolver côté **serveur** (pas client) pour mutualiser avec `dialog_handler.py` existant.
|
||||
|
||||
**Couverture estimée** : **~85%** des modaux courants sans intervention humaine. Les 15% restants = pause supervisée par design healthtech.
|
||||
|
||||
**Effort intégration** : **1j MVP** (P0 démo) → 1 sem couverture complète + tests fixtures (P1) → 1 mois bench injection + apprentissage catalogue (P2).
|
||||
|
||||
---
|
||||
|
||||
## 4. Décisions ouvertes consolidées pour Dom
|
||||
|
||||
Les 3 specs remontent des points qu'un agent ne peut pas trancher seul. Regroupés ici par thème.
|
||||
|
||||
### 4.1. Transport / watchdog (8 décisions — `SPEC_TRANSPORT_CONTRAT.md` §12)
|
||||
|
||||
| ID | Question | Recommandation auteur |
|
||||
|---|---|---|
|
||||
| D1 | Persistance `_retry_pending` au restart serveur ? | Non par défaut (cas extrême ; restart démo = redémarrage replay manuel) |
|
||||
| D2 | Politique d'abandon par type d'action (click vs wait) ? | Différenciée : click MAX_RESENDS=2, extract_text MAX_RESENDS=1 |
|
||||
| D3 | Retry des actions server-side (`extract_text`, `t2a_decision`) ? | Non (idempotence non garantie : LLM peut diverger) |
|
||||
| D4 | Purge `_retry_pending` à la complétion workflow ? | **Oui** (recommandé) |
|
||||
| D5 | `dedup_set` client : taille LRU + clé ? | LRU 50 entrées, clé = `action_id` + `attempt_id` |
|
||||
| D6 | Génération `attempt_id` côté serveur ? | UUID4 court 8 chars, incrémenté à chaque resend |
|
||||
| D7 | Backward-compat client v1 ↔ serveur v2 ? | Header `X-Replay-Protocol-Version` côté client |
|
||||
| D8 | `cancel_in_flight` (annuler action en cours sur le client) ? | **Non** (recommandé) — trop de risques |
|
||||
|
||||
### 4.2. Validator (7 actions où le signal "qui fait foi" reste flou — `SPEC_VALIDATOR_MATRICE.md` §10)
|
||||
|
||||
| Action | Question |
|
||||
|---|---|
|
||||
| `paste_and_execute` | Vérif côté Léa Windows ou côté SSH VM (cas NoMachine pixel intermédiaire) ? |
|
||||
| `screenshot_evidence` | `TitleBarChecker` suffit ou exiger netteté image ? |
|
||||
| `pause_for_human` mode autonome | Aujourd'hui silencieusement ignorée (`api_stream.py:3011-3017`) — laisser ou changer ? |
|
||||
| `t2a_decision = NA` | Verdict métier vs erreur LLM (hors-scope médical Claude) — qui décide ? |
|
||||
| Tab déjà actif au moment du clic | Idempotence (SUCCESS) ou NO_VISUAL_CHANGE ? |
|
||||
| `drag_drop_anchor` | Whitelist serveur mais pas de handler Léa — implémenter ou retirer du whitelist ? |
|
||||
| Animations longues > 1s | `wait_after_action_ms` par type d'action vs généralisé ? |
|
||||
|
||||
### 4.3. Popups (5 modaux ambigus — `SPEC_POPUPS_CATALOGUE.md` §10)
|
||||
|
||||
| Modal | Politique proposée | Risque à valider |
|
||||
|---|---|---|
|
||||
| `easily-required-field` | `auto` | Peut masquer un bug de grounding réel |
|
||||
| `outlook-reminder` | `skip` | Risque clinique de masquer un rappel pro |
|
||||
| `chrome-update` / `edge-update` | `skip` | À confirmer si ne prend pas le focus après inactivité |
|
||||
| `easily-clinical-warning` | `pause` non surchargeable | Volontairement strict, à valider |
|
||||
| `browser-perm-microphone` (Easily dictée vocale) | `pause` | Déclaration globale `declared_dialogs` ou par-workflow ? |
|
||||
|
||||
---
|
||||
|
||||
## 5. Plan d'attaque concret proposé (~3 jours homme cumulés)
|
||||
|
||||
### Vague 1 (jour 1) — Transport + start Validator
|
||||
- **Matin (3h30)** — Watchdog `replay_watchdog.py` + patches `api_stream.py`. Kill-switch `RPA_WATCHDOG_ENABLED=false` par défaut → activable progressivement. Tests pytest sans Windows. Smoke E2E réel.
|
||||
- **Après-midi (4h)** — Validator MVP P0 : `PixelDiffChecker` + `OcrRoiChecker` + orchestrateur, sans `LlmJudgeChecker`. Flag `RPA_VALIDATOR_V2_ENABLED=false`. Test offline du bug step 10 sur fixture du 8 mai.
|
||||
|
||||
### Vague 2 (jour 2) — Finir Validator + Popup chain MVP
|
||||
- **Matin (4h)** — Compléter Validator : `TabActiveChecker`, `SaveSuccessChecker`, `DialogClosedChecker`, `JsonSchemaChecker`. Tests par type d'action.
|
||||
- **Après-midi (4h)** — Chaîne popup MVP : signatures FR+EN, `ChangeDetector` + `DialogClassifier` + `DialogResolver` côté serveur. Endpoint `/api/v1/dialog/resolve`. Simplification `_handle_popup_vlm` client.
|
||||
|
||||
### Vague 3 (jour 3) — Intégration et démo
|
||||
- **Matin (3h)** — Coordination Validator → DialogResolver (handoff `UNEXPECTED_DIALOG`). Test bout-en-bout démo MOREL Catherine avec les 3 chantiers actifs.
|
||||
- **Après-midi (3h)** — Bench latence sur démo réelle (cible : +20s overhead max sur 46 steps). Activation progressive des flags. Mise à jour `DETTE_TECHNIQUE.md` (DETTE-001, 008 closables).
|
||||
|
||||
**Sortie attendue** : démo prochaine `Demo_urgence_3_db` qui tourne **sans** `static_result/static_text`, **sans** `cancel-replays.sh` manuel, **sans** pause humaine sur dialogs métier connus.
|
||||
|
||||
---
|
||||
|
||||
## 6. Hors-périmètre de ces 3 specs (rappel)
|
||||
|
||||
Ces 3 specs **ne traitent pas** :
|
||||
- Migration grounding VLM (`AXE_A1`, `AXE_A2`, `AXE_A3` — Qwen3-VL, smart_resize, bench bbox).
|
||||
- Bug capture client Y `mss.monitors[N]=2560×60` (`AXE_B5_D1_CAPTURE_REMOTE.md` — fix DPI 8 lignes en tête de `main.py`).
|
||||
- Shadow learning / fine-tuning / memory store (`AXE_C_LEARNING_SHADOW.md`).
|
||||
- Packaging / code signing / multi-tenant (`AXE_D4_MULTI_TENANT_DEPLOY.md`).
|
||||
- Veille frameworks externes (`AXE_E_FRAMEWORKS_BENCHMARKS.md`).
|
||||
- Bug recapture anchor VWB silencieuse (P0 — non couvert par cette vague).
|
||||
- Bug skip ord 13 orchestration (P0 — non identifié, NOT REPRO 100%).
|
||||
|
||||
Ces sujets sont couverts par les autres docs `docs/recherche/AXE_*.md`. Voir `SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` pour la carte d'ensemble.
|
||||
|
||||
---
|
||||
|
||||
## 7. Cartographie complète des livrables de recherche
|
||||
|
||||
### Vague 1 — Panorama 13 axes (23 mai 2026)
|
||||
```
|
||||
docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md (synthèse croisée initiale)
|
||||
|
||||
docs/recherche/AXE_A1_VLM_GROUNDING_SOTA.md (état art VLM grounding)
|
||||
docs/recherche/AXE_A2_SMART_RESIZE_BBOX.md (DETTE-014/010/007/006)
|
||||
docs/recherche/AXE_A3_BENCH_PROTOCOL.md (script bench reproductible)
|
||||
docs/recherche/AXE_A4_OCR_TEMPLATE_PHASH.md (cascade déterministe)
|
||||
docs/recherche/AXE_A5_SCREEN_TOKENIZATION.md (OmniParser / SoM)
|
||||
docs/recherche/AXE_B1_REPLAY_TRANSPORT.md (SSE / WS / pull-poll)
|
||||
docs/recherche/AXE_B2_VALIDATOR_PATTERN.md (Planner-Actor-Validator)
|
||||
docs/recherche/AXE_B4_ORA_VS_REPLAY.md (autonomous vs déclaratif)
|
||||
docs/recherche/AXE_B5_D1_CAPTURE_REMOTE.md (mss / DXGI / NoMachine)
|
||||
docs/recherche/AXE_C_LEARNING_SHADOW.md (Shadow + FT + memory)
|
||||
docs/recherche/AXE_D2_DIALOG_POPUP.md (chaîne dialog handling)
|
||||
docs/recherche/AXE_D4_MULTI_TENANT_DEPLOY.md (packaging + code signing)
|
||||
docs/recherche/AXE_E_FRAMEWORKS_BENCHMARKS.md (delta veille + benchmarks)
|
||||
```
|
||||
|
||||
### Vague 2 — Approfondissement 3 axes replay (24 mai 2026)
|
||||
```
|
||||
docs/recherche/AXE_B1_DEEP_WATCHDOG.md (watchdog production-ready)
|
||||
docs/recherche/AXE_B2_DEEP_VALIDATOR.md (Validator package)
|
||||
docs/recherche/AXE_D2_DEEP_POPUP_CHAIN.md (chaîne popup complète)
|
||||
```
|
||||
|
||||
### Vague 3 — Specs opérationnelles (24 mai 2026)
|
||||
```
|
||||
docs/recherche/SPEC_TRANSPORT_CONTRAT.md (contrat dispatch→ack→retry→orphan→resume)
|
||||
docs/recherche/SPEC_VALIDATOR_MATRICE.md (matrice action → signal qui fait foi)
|
||||
docs/recherche/SPEC_POPUPS_CATALOGUE.md (catalogue 59 entrées + politique auto/pause/skip)
|
||||
```
|
||||
|
||||
### Vague 4 — Index (ce document)
|
||||
```
|
||||
docs/recherche/INDEX_REPLAY_SPECS_2026-05-24.md (point d'entrée brief agent / humain)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Index maintenu par Dom. À mettre à jour quand de nouvelles specs replay sont livrées ou quand une décision §4 est tranchée.*
|
||||
@@ -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.
|
||||
79
docs/recherche/RAPPORT_PILOTAGE_CORE_JUDGE_VLM_2026-05-24.md
Normal file
79
docs/recherche/RAPPORT_PILOTAGE_CORE_JUDGE_VLM_2026-05-24.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Rapport d'Analyse : Pilotage de l'Agent Léa & Architecture "Juge VLM"
|
||||
|
||||
**Date :** 2026-05-24
|
||||
**Objet :** Optimisation du Core Central et fiabilisation des interactions OS Windows.
|
||||
**Statut :** Recherche & Suggestions (Zéro modification de code source).
|
||||
|
||||
---
|
||||
|
||||
## 1. Diagnostic de l'Architecture de Pilotage Actuelle
|
||||
|
||||
L'analyse du core central (`core/execution/observe_reason_act.py` et `agent_v0/agent_v1/core/executor.py`) révèle une architecture de type **"Open-Loop Execution"** (Exécution en boucle ouverte) :
|
||||
|
||||
1. **Le Serveur (Cerveau)** : Envoie une commande atomique (ex: `click` à `x,y`).
|
||||
2. **L'Agent (Muscle)** : Exécute la commande et renvoie un rapport immédiat.
|
||||
3. **Le Critic (Post-Vérification)** : Compare les screenshots avant/après via pHash ou vision.
|
||||
|
||||
### Points de friction identifiés :
|
||||
* **Asynchronisme de l'OS** : Windows 11 est asynchrone. L'agent renvoie son rapport de succès *avant* que l'effet visuel ne soit stabilisé (ex: animation du menu Démarrer ou ouverture du dialogue "Enregistrer sous").
|
||||
* **Frustation du Critic** : Le serveur reçoit une image "en transition" (noire, floue ou incomplète), échoue à valider l'action, et ordonne un `RETRY`. Cela crée la boucle infinie observée sur le `start_button`.
|
||||
* **Cécité Contextuelle** : L'agent local n'a pas d'autonomie pour dire "Attends, une popup bloque le chemin" ; il obéit aveuglément aux coordonnées reçues.
|
||||
|
||||
---
|
||||
|
||||
## 2. Deep Dive : Le Concept de "Juge VLM"
|
||||
|
||||
Le **Juge VLM** est l'évolution majeure des frameworks SOTA de 2026 (Agent-S3, Coasty). Il consiste à découpler la **réussite technique** (le clic a été fait) de la **réussite sémantique** (l'objectif est atteint).
|
||||
|
||||
### 2.1. Fonctionnement du Juge VLM
|
||||
Au lieu de renvoyer des coordonnées, le Juge VLM répond à des questions binaires ou sémantiques sur l'état du système. Il intervient à trois niveaux :
|
||||
|
||||
#### A. Le Juge de Pré-Condition (Visual Guard)
|
||||
Avant d'exécuter l'action, l'agent interroge le VLM local (Ollama `qwen3-vl:8b`) :
|
||||
* **Prompt** : *"Est-ce que le bouton 'Enregistrer' est visible et cliquable sans obstacle ?"*
|
||||
* **Utilité** : Évite de cliquer dans le vide si un dialogue n'est pas encore apparu ou si une fenêtre UAC bloque l'écran.
|
||||
|
||||
#### B. Le Juge de Stabilisation (Visual Anchor)
|
||||
Après l'action, l'agent ne rend pas la main au serveur tant que le Juge n'a pas validé la transition.
|
||||
* **Prompt** : *"Le menu Démarrer est-il maintenant ouvert ? Répondre par OUI ou NON."*
|
||||
* **Utilité** : Absorbe la latence de l'OS. L'agent fait du polling interne jusqu'au "OUI" ou au timeout. Le serveur ne voit que le résultat final stabilisé.
|
||||
|
||||
#### C. Le Juge de Conformité (Semantic Critic)
|
||||
C'est le niveau le plus élevé, utilisé pour les dialogues complexes.
|
||||
* **Prompt** : *"L'utilisateur a demandé d'enregistrer le fichier sous le nom 'test.txt'. Est-ce que le dialogue actuel confirme que le fichier sera enregistré dans le dossier 'Documents' ?"*
|
||||
* **Utilité** : Détecte les erreurs métier que le pixel-matching (pHash) ignore totalement.
|
||||
|
||||
### 2.2. Intégration avec le pattern "Best-of-N"
|
||||
Le Juge VLM permet d'implémenter le **Best-of-N rollouts**. Si le Juge dit "NON" après une action, l'agent peut tenter localement une variante (ex: presser `Enter` au lieu de cliquer sur `Enregistrer`) sans que le serveur n'ait à gérer la complexité du retry.
|
||||
|
||||
---
|
||||
|
||||
## 3. Suggestions de Pilotage pour "Léa V3"
|
||||
|
||||
### Suggestion 1 : L'Autonomie de la "Dernière Milliseconde"
|
||||
L'agent local (`executor.py`) doit cesser d'être un simple terminal. Il doit intégrer une **boucle de rétroaction locale**.
|
||||
* **Idée** : Si le serveur demande un clic sur "Enregistrer", l'agent local vérifie par OCR/Vision rapide que le texte "Enregistrer" est bien sous le curseur avant de cliquer. S'il ne l'est pas, il attend 500ms et ré-essaie localement.
|
||||
|
||||
### Suggestion 2 : Normalisation sémantique des Dialogues
|
||||
Utiliser des signatures sémantiques pour les fenêtres Windows.
|
||||
* **Idée** : Créer un dictionnaire de "DialogStates" (ex: `STATE_SAVE_AS`, `STATE_START_MENU`). Chaque état est défini par un ensemble de mots-clés et d'icônes. Le pilotage devient une suite de transitions d'états : `IDLE -> STATE_START_MENU -> STATE_APP_OPENED`.
|
||||
|
||||
### Suggestion 3 : La Cascade de Modèles (Mixture-of-Grounding)
|
||||
* **Rapide/Local** : Pour le mouvement de souris et la détection de texte simple (EasyOCR).
|
||||
* **Lourd/Serveur** : Pour la résolution de cibles complexes (UI-TARS-2 / InfiGUI).
|
||||
* **Cognitif/Ollama** : Pour le "Juge VLM" et la validation de l'intention.
|
||||
|
||||
---
|
||||
|
||||
## 4. Conclusion : Pourquoi Léa boucle sur le `start_button` ?
|
||||
|
||||
D'après cette analyse, le blocage est une **crise de confiance entre l'Agent et le Serveur** :
|
||||
1. L'Agent fait le fallback `Win`.
|
||||
2. L'Agent envoie le screenshot trop tôt (pendant l'animation Windows).
|
||||
3. Le Serveur (Critic) voit un écran flou, ne trouve pas le menu, et dit "Échec, recommence".
|
||||
4. L'Agent reçoit l'ordre de recommencer et boucle.
|
||||
|
||||
**La solution recommandée (sans code)** : Introduire une pause de stabilisation visuelle pilotée par un Juge VLM local qui ne libère l'agent que lorsque le menu est "vrai" à l'écran.
|
||||
|
||||
---
|
||||
*Ce rapport est une étude technique destinée à orienter les futures itérations de l'architecture Léa. Aucune modification n'a été apportée aux fichiers sources du projet.*
|
||||
1758
docs/recherche/SPEC_POPUPS_CATALOGUE.md
Normal file
1758
docs/recherche/SPEC_POPUPS_CATALOGUE.md
Normal file
File diff suppressed because it is too large
Load Diff
766
docs/recherche/SPEC_TRANSPORT_CONTRAT.md
Normal file
766
docs/recherche/SPEC_TRANSPORT_CONTRAT.md
Normal file
@@ -0,0 +1,766 @@
|
||||
# SPEC TRANSPORT — Contrat dispatch / ack / retry / orphan / resume
|
||||
|
||||
**Date :** 2026-05-24
|
||||
**Auteur :** Claude (recherche dispatchée, lecture seule sur code)
|
||||
**Statut :** spécification contractuelle. Aucune modif code. Toutes les sections sont en tables ou state machines, prose minimale.
|
||||
**Pré-requis :**
|
||||
- `docs/recherche/AXE_B1_REPLAY_TRANSPORT.md` (transport SSE/WebSocket — choix techno)
|
||||
- `docs/recherche/AXE_B1_DEEP_WATCHDOG.md` (implémentation watchdog)
|
||||
- `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` (bug 9 actions perdues)
|
||||
- `docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` §4
|
||||
|
||||
Ce doc fige **le contrat** entre `api_stream.py` (FastAPI Linux) et `agent_v1/core/executor.py` + `agent_v1/network/streamer.py` (Léa Windows). Il est **isomorphe poll ↔ SSE** (cf. §9).
|
||||
|
||||
---
|
||||
|
||||
## 1. TL;DR + diagramme d'ensemble
|
||||
|
||||
Une action visuelle est un **message borné par 2 IDs** : `action_id` (unique par step de replay, déterministe) + `attempt_id` (UUID, incrémenté à chaque re-dispatch transport). Le serveur tient une **mini-visibility-timeout in-memory** (`_retry_pending`). Le client tient un **LRU `dedup_set`** des `attempt_id` récemment exécutés. La pause supervisée (`paused_need_help`) est l'état absorbant en cas d'épuisement des essais ou de signal explicite (`system_dialog`, `wrong_window`, `target_not_found`). Le contrat est identique en polling (transport actuel) et en SSE (cible AXE_B1).
|
||||
|
||||
```
|
||||
┌───────────── server (Linux, FastAPI :5005) ─────────────┐
|
||||
│ │
|
||||
_replay_queues │ ┌──────────┐ DISPATCH ┌─────────────┐ │
|
||||
[session_id] │ │ PENDING │ ──────────────►│ DISPATCHED │ │
|
||||
│ └──────────┘ │ (_retry_ │ │
|
||||
│ ▲ │ pending) │ │
|
||||
│ │ repush head └─────┬───────┘ │
|
||||
│ │ (watchdog) │ REPORT(success) │
|
||||
│ ┌────┴─────┐ age>30s, resent │ verify OK │
|
||||
│ │ ORPHAN │ ◄─── timeout ◄────────┤ │
|
||||
│ │ (resent_ │ │ │
|
||||
│ │ count++)│ ▼ │
|
||||
│ └──────────┘ ┌──────────┐ │
|
||||
│ │ resent ≥ MAX │ ACKED │ │
|
||||
│ ▼ └──────────┘ │
|
||||
│ ┌──────────┐ REPORT(fail+system_dialog/wrong_window) │
|
||||
│ │ ABANDONED│◄──┬────────────────┐ │
|
||||
│ └──────────┘ │ │ │
|
||||
│ │ ▼ ▼ │
|
||||
│ │ ┌──────────────┐ ┌──────────┐ │
|
||||
│ └──►│ PAUSE_NEED_ │ │ FAILED │ │
|
||||
│ │ HELP │ │ (retry │ │
|
||||
│ └─────┬────────┘ │ budget │ │
|
||||
│ │ /resume │ out) │ │
|
||||
│ ▼ └──────────┘ │
|
||||
│ (resume_action en tête queue) │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
▲ │
|
||||
│ POST /replay/result │ GET /replay/next (poll)
|
||||
│ │ ou SSE event
|
||||
│ ▼
|
||||
┌───────────┴──────────── client Léa (Windows) ────────────┐
|
||||
│ ┌──────────┐ parse ┌──────────┐ execute_replay_ │
|
||||
│ │ RECEIVED │──────────►│ DEDUP │ action() │
|
||||
│ └──────────┘ │ CHECK │ │
|
||||
│ └────┬─────┘ │
|
||||
│ hit LRU │ miss │
|
||||
│ │ ▼ │
|
||||
│ │ ┌──────────┐ │
|
||||
│ │ │EXECUTING │ │
|
||||
│ │ └────┬─────┘ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ REPORTING │ → POST /replay/result │
|
||||
│ └──────────────┘ │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. State machine serveur — une action dans `_retry_pending`
|
||||
|
||||
```
|
||||
start
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ PENDING │
|
||||
│ présent dans _replay_queues[session_id], pas encore │
|
||||
│ extrait par get_next_action │
|
||||
└───────────────────────┬───────────────────────────────────────┘
|
||||
│ get_next_action → pop queue + write
|
||||
│ _retry_pending[action_id]={dispatched_at=now}
|
||||
│ + log [REPLAY] DISPATCH
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ DISPATCHED │
|
||||
│ dans _retry_pending, attente du REPORT │
|
||||
│ invariant : action absente de _replay_queues │
|
||||
└───┬───────────────────────┬───────────────────────────────────┘
|
||||
│ REPORT(success=True) │ REPORT(success=False, error=*)
|
||||
│ verify OK ou skip │ watchdog scan age>ORPHAN_TIMEOUT
|
||||
│ │
|
||||
│ pop _retry_pending │ resent_count < MAX_RESENDS
|
||||
│ completed_actions++ │ → repush head + resent++
|
||||
│ current_idx++ │ → dispatched_at=0
|
||||
▼ │ → log [BUS] lea:dispatch_
|
||||
┌────────┐ │ orphan_resent
|
||||
│ ACKED │ │
|
||||
│ (term) │ │ resent_count ≥ MAX_RESENDS
|
||||
└────────┘ │ → pop _retry_pending
|
||||
│ → log [BUS] lea:dispatch_
|
||||
│ orphan_giveup
|
||||
│ → ABANDONED
|
||||
▼
|
||||
┌─────────────────┐ watchdog repush
|
||||
│ ORPHAN_RESENT │ ───────────► PENDING
|
||||
│ (transitoire) │ (avec resent++)
|
||||
└─────────────────┘
|
||||
|
||||
REPORT(verify failed AND retry_count<MAX_RETRIES_PER_ACTION):
|
||||
_schedule_retry crée action_id_retry{N+1}, repush head
|
||||
le nouveau action_id entre en PENDING (nouvelle entrée)
|
||||
l'ancien action_id sort de DISPATCHED via pop
|
||||
|
||||
REPORT(system_dialog | wrong_window | target_not_found):
|
||||
──► PAUSE_NEED_HELP (replay_state.status)
|
||||
_retry_pending non purgé sur-le-champ (peut être réutilisé par /resume)
|
||||
|
||||
ABANDONED:
|
||||
entrée _retry_pending supprimée
|
||||
──► si politique = pause sur Nème giveup → PAUSE_NEED_HELP
|
||||
sinon : action perdue, replay continue sur l'action suivante
|
||||
|
||||
CANCELLED (POST /replay/<id>/cancel):
|
||||
purge _retry_pending par replay_id (api_stream.py:4489)
|
||||
_replay_queues[session_id] = []
|
||||
state.status = "cancelled"
|
||||
```
|
||||
|
||||
**Invariants serveurs :**
|
||||
|
||||
| # | Invariant | Garanti par |
|
||||
|---|---|---|
|
||||
| I1 | Une action en `DISPATCHED` est absente de `_replay_queues` | pop atomique sous `_replay_lock` (api_stream.py:3346-3348) |
|
||||
| I2 | `action_id` unique dans `_retry_pending` à un instant t | clé dict + check `if action_id_sent not in _retry_pending` (api_stream.py:3354) |
|
||||
| I3 | `report_action_result.pop(action_id)` est idempotent | `pop(key, None)` retourne None si déjà acquitté (api_stream.py:3491) |
|
||||
| I4 | Cancel purge bien `_retry_pending` pour ce replay | iter `_retry_pending.items() if v["replay_id"]==replay_id` (api_stream.py:4489-4491) |
|
||||
| I5 | Watchdog re-check sous lock avant repush | pattern `if aid not in self._retry_pending: skip` (AXE_B1_DEEP §3) |
|
||||
| I6 | Pause `paused_need_help` ne distribue aucune action | `get_next_action` retourne `replay_paused=True` (api_stream.py:2951) |
|
||||
|
||||
---
|
||||
|
||||
## 3. State machine client Léa — une action côté `executor.py`
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ POLLING │ thread `_poll_loop`, every 1s (+backoff)
|
||||
│ (idle) │ GET /replay/next?session_id&machine_id
|
||||
└──────┬───────┘
|
||||
│ HTTP 200 + action ≠ null
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ RECEIVED │ data["action"] parsé
|
||||
└──────┬───────┘
|
||||
│
|
||||
┌───────────────┴────────────────┐
|
||||
│ attempt_id ∈ dedup_set ? │
|
||||
▼ ▼
|
||||
OUI : SKIP NON
|
||||
(log warning, ack synthetique) │
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ EXECUTING │ execute_replay_action()
|
||||
│ │ ├─ pre-check window
|
||||
│ │ ├─ resolve target visuel
|
||||
│ │ ├─ click / type / key
|
||||
│ │ └─ screenshot_after
|
||||
└──────┬───────┘
|
||||
│
|
||||
┌─────────────────┼──────────────────┐
|
||||
│ │ │
|
||||
success=True success=False system_dialog
|
||||
warning=None error=* détecté
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────────────────────┐
|
||||
│ REPORTING │
|
||||
│ POST /replay/result + retry │
|
||||
│ (timeout=10s, allow_redirects=False)
|
||||
└─────────────────┬────────────────┘
|
||||
│
|
||||
┌──────────┴──────────┐
|
||||
HTTP 200 fail/timeout
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────┐ ┌──────────────┐
|
||||
│ ACKED │ │ REPORT_RETRY │ (PAS implémenté
|
||||
│ + LRU │ │ (in-memory) │ v1 ; à v2)
|
||||
│ store │ └──────┬───────┘
|
||||
│ attempt │ │
|
||||
│ _id │ ▼
|
||||
└──────────┘ (perte report : serveur watchdog
|
||||
rattrapera via orphan)
|
||||
|
||||
Pendant POLLING : si data.replay_paused → afficher PauseDialog
|
||||
Pendant EXECUTING : timeout par étape gérée par execute_replay_action
|
||||
(resolve serveur 30s, _wait_for_screen_change 1000ms+, capture 0.5s)
|
||||
```
|
||||
|
||||
**Invariants clients :**
|
||||
|
||||
| # | Invariant | Garanti par |
|
||||
|---|---|---|
|
||||
| C1 | Une action reçue est TOUJOURS reportée (succès ou échec) | try/except global executor.py:2429-2503, fallback `result={success:False, error=…}` |
|
||||
| C2 | Un seul `poll_and_execute` à la fois | `self._replay_lock.acquire(blocking=False)` executor.py:2291 |
|
||||
| C3 | Pas de blocage event tray UI pendant exécution | thread dédié `_poll_loop` |
|
||||
| C4 | Idempotence côté action (v2) | dedup_set LRU bornée 256 entrées sur `attempt_id` (§6.2) — **À AJOUTER** |
|
||||
| C5 | Pause UI déclenchée uniquement sur signal serveur explicite | `data.get("replay_paused")` executor.py:2346 |
|
||||
|
||||
---
|
||||
|
||||
## 4. Contrats JSON
|
||||
|
||||
### 4.1. Payload DISPATCH serveur → client (GET /replay/next OU SSE event)
|
||||
|
||||
**Cas nominal : action visuelle**
|
||||
|
||||
| Champ | Type | Obligatoire | Description | Source code |
|
||||
|---|---|---|---|---|
|
||||
| `action` | object \| null | oui | l'action ou `null` si rien | api_stream.py:3436 |
|
||||
| `session_id` | str | oui | session active | api_stream.py:3438 |
|
||||
| `machine_id` | str | oui | machine cible | api_stream.py:3439 |
|
||||
| `action.action_id` | str | oui | identifiant unique step, ex. `step_4c0663941f22` | DB workflow + suffixes `_retry1/_resume` |
|
||||
| `action.attempt_id` | str | **À AJOUTER** | UUID hex 16, nouveau à chaque dispatch (initial OU resend) | n/a, v2 |
|
||||
| `action.type` | enum | oui | `click`/`type`/`key_combo`/`wait`/`scroll`/`pause_for_human`/`extract_text`/... | core/executor.py:2422 |
|
||||
| `action.target_spec` | object | si visuelle | `{by_text,vlm_description,anchor_image_base64,resolve_order,window_title,uia_target}` | api_stream.py:3364 |
|
||||
| `action.parameters` | object | dep. type | `{text,keys,duration_ms,condition,…}` | dépend du type |
|
||||
| `action.expected_window_before` | str | non | titre fenêtre attendue avant clic | api_stream.py:3366 |
|
||||
| `action.expected_window_title` | str | non | titre fenêtre attendue après clic | api_stream.py:3369 |
|
||||
| `action.success_strict` | bool | non | mode strict (skip OCR fuzzy) | api_stream.py:3387 |
|
||||
| `action.intention` | str | non | description humaine | api_stream.py:3379 |
|
||||
| `action.monitor_resolution` | object | oui (QW1) | `{idx,offset_x,offset_y,w,h,source}` | api_stream.py:3403 |
|
||||
| `action.from_node` | str | non | id node WorkflowGraph (active pre-check) | api_stream.py:3229 |
|
||||
| `action.dispatch_meta` | object | **À AJOUTER** | `{first_dispatched_at,resent_count,last_resent_at}` pour visibilité client | v2 |
|
||||
| `precheck` | object | non | résultat pre-check serveur `{match,similarity,popup_detected}` | api_stream.py:3441 |
|
||||
| `server_busy` | bool | non | lock occupé, retry plus tard | api_stream.py:2944 |
|
||||
| `replay_paused` | bool | non | replay en pause supervisée | api_stream.py:2960 |
|
||||
| `pause_message` | str | si paused | message à afficher dans bulle | api_stream.py:2961 |
|
||||
| `replay_id` | str | si paused | pour ack ciblé via /resume | api_stream.py:2962 |
|
||||
| `auth_detected` | bool | non | injection automatique d'actions d'auth | api_stream.py:3304 |
|
||||
|
||||
**Exemple complet (v2 cible) :**
|
||||
```json
|
||||
{
|
||||
"action": {
|
||||
"action_id": "step_4c0663941f22",
|
||||
"attempt_id": "a8f3c2d1e9b4f720",
|
||||
"type": "click",
|
||||
"target_spec": {
|
||||
"by_text": "Imagerie",
|
||||
"resolve_order": ["ocr","template","vlm"],
|
||||
"anchor_image_base64": "iVBORw0KGgo…",
|
||||
"window_title": "MOREL Catherine — Easily Assure",
|
||||
"uia_target": null
|
||||
},
|
||||
"parameters": {},
|
||||
"expected_window_before": "MOREL Catherine — Easily Assure",
|
||||
"expected_window_title": "MOREL Catherine — Easily Assure",
|
||||
"success_strict": true,
|
||||
"intention": "Cliquer onglet Imagerie",
|
||||
"monitor_resolution": {"idx":1,"offset_x":0,"offset_y":0,"w":2560,"h":1600,"source":"action_hint"},
|
||||
"from_node": "node_tab_imagerie",
|
||||
"dispatch_meta": {
|
||||
"first_dispatched_at": 1779015600.123,
|
||||
"resent_count": 0,
|
||||
"last_resent_at": 0.0
|
||||
}
|
||||
},
|
||||
"session_id": "sess_demo_42",
|
||||
"machine_id": "DESKTOP-58D5CAC"
|
||||
}
|
||||
```
|
||||
|
||||
**Cas pause supervisée :**
|
||||
```json
|
||||
{
|
||||
"action": null,
|
||||
"session_id": "sess_demo_42",
|
||||
"machine_id": "DESKTOP-58D5CAC",
|
||||
"replay_paused": true,
|
||||
"pause_message": "Je n'y arrive pas (« Coller ou saisir... »)",
|
||||
"replay_id": "replay_free_68ca51ab"
|
||||
}
|
||||
```
|
||||
|
||||
**Cas idle / server_busy :**
|
||||
```json
|
||||
{"action": null, "session_id":"sess_demo_42", "machine_id":"…", "server_busy": true}
|
||||
```
|
||||
|
||||
### 4.2. Payload REPORT client → serveur (POST /replay/result)
|
||||
|
||||
| Champ | Type | Obligatoire | Description | Source code |
|
||||
|---|---|---|---|---|
|
||||
| `session_id` | str | oui | | api_stream.py:628 |
|
||||
| `action_id` | str | oui | identifiant de l'action acquittée | api_stream.py:629 |
|
||||
| `attempt_id` | str | **À AJOUTER** | echo du `attempt_id` reçu (corrélation watchdog ↔ client) | v2 |
|
||||
| `success` | bool | oui | résultat global | api_stream.py:630 |
|
||||
| `error` | str \| null | dep. success | message court (`target_not_found`, `system_dialog:uac_consent`, …) | api_stream.py:631 |
|
||||
| `warning` | str \| null | non | `no_screen_change`/`popup_handled`/`visual_resolve_failed`/`wrong_window` | api_stream.py:632 |
|
||||
| `screenshot_after` | str \| null | recommandé | base64 PNG ou path | api_stream.py:634 |
|
||||
| `screenshot_before` | str \| null | recommandé (clic) | base64 PNG du frame pre-action (Critic) | api_stream.py:635 |
|
||||
| `actual_position` | object | si visuel | `{x_pct: float, y_pct: float}` coords cliquées | api_stream.py:636 |
|
||||
| `resolution_method` | str | si visuel | `server_resolve_hybrid`/`template_match`/... | api_stream.py:638 |
|
||||
| `resolution_score` | float | si visuel | 0.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_<hex>` puis suffixes `_retry{N}` ou `_resume`) | clé du `pop` côté serveur, clé du dedup côté client | ✅ DB workflow + replay_engine.py:2609 |
|
||||
| **Action — `attempt_id` rotatif** | UUID nouveau à chaque DISPATCH (initial + chaque resend) | distingue un re-dispatch légitime d'un doublon réseau, permet stats orphan | ❌ **À AJOUTER v2** |
|
||||
| **Client — dedup_set LRU 256** | `set` bornée de `(action_id)` ou `(action_id, attempt_id)` récemment exécutés | bloque ré-exécution en cas g/k | ❌ **À AJOUTER v2 obligatoire** |
|
||||
| **Action — idempotence intrinsèque** | clear field avant `type`, idempotence native du `click` sur tab actif | minimise dégât en cas de double exécution résiduelle | ⚠ **À documenter dans VWB**, pas dans code |
|
||||
|
||||
### 6.2. Spec dedup_set client (v2)
|
||||
|
||||
```python
|
||||
# agent_v1/core/executor.py — à ajouter
|
||||
from collections import OrderedDict
|
||||
|
||||
class ActionDedupSet:
|
||||
"""LRU bornée d'action_id récemment exécutées.
|
||||
Bloque ré-exécution si action arrive 2 fois (orphan resent + double réseau).
|
||||
"""
|
||||
def __init__(self, max_size: int = 256):
|
||||
self._store: OrderedDict[str, float] = OrderedDict() # action_id → ts
|
||||
self._max = max_size
|
||||
|
||||
def seen(self, action_id: str) -> bool:
|
||||
if action_id in self._store:
|
||||
# Touch (LRU)
|
||||
self._store.move_to_end(action_id)
|
||||
return True
|
||||
return False
|
||||
|
||||
def mark(self, action_id: str) -> None:
|
||||
self._store[action_id] = time.time()
|
||||
self._store.move_to_end(action_id)
|
||||
if len(self._store) > self._max:
|
||||
self._store.popitem(last=False)
|
||||
```
|
||||
|
||||
**Usage dans `poll_and_execute_inner` AVANT `execute_replay_action` :**
|
||||
```python
|
||||
if self._dedup.seen(action.get("action_id","")):
|
||||
logger.warning(f"[DEDUP] action {action.get('action_id')} déjà exécutée — ack synthétique")
|
||||
self._post_synthetic_ack(action, server_url, replay_result_url,
|
||||
success=True, warning="already_executed")
|
||||
return True
|
||||
self._dedup.mark(action.get("action_id",""))
|
||||
# … execute_replay_action(action) …
|
||||
```
|
||||
|
||||
### 6.3. Idempotence intrinsèque par type d'action
|
||||
|
||||
| Type | Idempotent nativement ? | Mitigation si exécuté 2× |
|
||||
|---|---|---|
|
||||
| `click` sur tab/bouton actif | OUI (le tab reste actif) | aucune |
|
||||
| `click` sur bouton "Submit"/"Valider" | NON (double formulaire) | **dedup_set CRITIQUE** + dialog confirm côté app |
|
||||
| `type` texte | NON (double saisie) | préfixer `Ctrl+A` (clear) + dedup_set |
|
||||
| `keyboard_shortcut` Ctrl+S | dep. (1 sauvegarde = 1 dialog) | dedup_set |
|
||||
| `keyboard_shortcut` Ctrl+V | NON (double collage) | dedup_set + clear avant |
|
||||
| `scroll` | OUI mais déplace 2× | tolérable, dedup_set conseillé |
|
||||
| `wait` | OUI | aucun risque |
|
||||
| `extract_text` (server-side) | OUI (lecture pure) | n/a |
|
||||
| `t2a_decision` (server-side, LLM) | OUI mais re-coût LLM ($/temps) | retry serveur, pas client |
|
||||
|
||||
---
|
||||
|
||||
## 7. Timeouts et seuils
|
||||
|
||||
| Nom | Défaut | Env var | Effet | Source |
|
||||
|---|---:|---|---|---|
|
||||
| `client_poll_timeout` | 30 s | non, en dur | `requests.get(/replay/next, timeout=30)` côté Léa | executor.py:2320 |
|
||||
| `client_report_timeout` | 10 s | non, en dur | `requests.post(/replay/result, timeout=10)` | executor.py:2480 |
|
||||
| `client_resolve_timeout` | 30 s | non, en dur | appel serveur `/resolve_target` | executor.py:1898 |
|
||||
| `server_replay_lock_timeout` | 4.5 s | non, en dur | `_async_replay_lock(timeout=4.5)` → 503 ou server_busy | api_stream.py:539, 2938 |
|
||||
| `server_action_server_side_timeout` | 180 s | non, en dur | `asyncio.wait_for(extract_text/t2a, 180)` | api_stream.py:3141 |
|
||||
| `server_paste_and_execute_timeout` | 30 s | non, en dur | paste+execute ydotool | api_stream.py:3192 |
|
||||
| `server_precheck_timeout` | 0.5 s | non, en dur | CLIP embed pre-check | api_stream.py:3250 |
|
||||
| `heartbeat_max_age` | varie | `RPA_HEARTBEAT_MAX_AGE_SECONDS` | utilité pre-check | api_stream.py:3235 |
|
||||
| `WATCHDOG_SCAN_INTERVAL_S` | 10 s | `RPA_WATCHDOG_SCAN_INTERVAL_S` | période scan orphan | AXE_B1_DEEP §11 |
|
||||
| `WATCHDOG_ORPHAN_TIMEOUT_S` | 30 s | `RPA_WATCHDOG_ORPHAN_TIMEOUT_S` | age sans report → orphan | AXE_B1_DEEP §11 |
|
||||
| `WATCHDOG_MAX_RESENDS` | 2 | `RPA_WATCHDOG_MAX_RESENDS` | give-up après N resends | AXE_B1_DEEP §11 |
|
||||
| `WATCHDOG_REPUSH_POSITION` | `head` | `RPA_WATCHDOG_REPUSH_POSITION` | head/tail | AXE_B1_DEEP §11 |
|
||||
| `WATCHDOG_ENABLED` | `1` | `RPA_WATCHDOG_ENABLED` | kill-switch | AXE_B1_DEEP §11 |
|
||||
| `REPLAY_STATE_TTL_SECONDS` | varie | `RPA_REPLAY_STATE_TTL` | purge states finis | api_stream.py:726 |
|
||||
| `MAX_REPLAY_STATES` | n | en dur | borne `_replay_states` | api_stream.py:735 |
|
||||
| `MAX_RETRIES_PER_ACTION` | 3 | en dur | budget retry métier `_schedule_retry` | replay_engine.py:2591 |
|
||||
| `_poll_backoff_min` | varie | n/a | reset après HTTP 200 | executor.py:2334 |
|
||||
| `_poll_backoff_max` | varie | n/a | plafond backoff exponentiel | executor.py:2326 |
|
||||
| `_poll_backoff_factor` | 2.0 | n/a | facteur multiplicatif | executor.py:2326 |
|
||||
| `SSE_PING_INTERVAL_S` (cible) | 15 | env futur | heartbeat SSE | AXE_B1 §4 |
|
||||
|
||||
**Cohérence des seuils :**
|
||||
- `client_poll_timeout (30s) > server_replay_lock_timeout (4.5s)` → OK, le client attend bien la réponse server_busy.
|
||||
- `server_action_server_side_timeout (180s) > client_poll_timeout (30s)` → SI extract_text dure 35s sans dispatcher d'action visuelle entre temps, le client coupe MAIS le serveur continue ; au prochain poll le serveur a fini, dispatche l'action visuelle suivante. **Pas de perte tant que l'action visuelle dispatch est rapide après extract_text.** Bug 8 mai = extract_text + dispatch click dans la MÊME réponse → 5s timeout dépassé → fix `timeout=30` adopté.
|
||||
- `WATCHDOG_ORPHAN_TIMEOUT_S (30s) > client_poll_timeout (30s)` → frontière dangereuse. **Recommandation : remonter à 45s** pour laisser le temps au client de retenter au moins 1 poll naturellement avant que le watchdog résende.
|
||||
|
||||
---
|
||||
|
||||
## 8. Transitions vers pause supervisée et resume
|
||||
|
||||
### 8.1. Déclencheurs `status = "paused_need_help"`
|
||||
|
||||
| Déclencheur | Source | État avant | État après | Champ enrichi |
|
||||
|---|---|---|---|---|
|
||||
| `pause_for_human` en mode supervised ou safety_checks présents | api_stream.py:3066-3111 | running | paused_need_help | `safety_checks`, `pause_payload`, `failed_action.reason="user_request"` |
|
||||
| Report `system_dialog:*` (UAC/CredUI/SmartScreen) | api_stream.py:3785-3870 | running | paused_need_help | `failed_action.reason="system_dialog"`, message contextualisé |
|
||||
| Report `warning="wrong_window"` | api_stream.py:3872-3920 | running | paused_need_help | `failed_action.reason="wrong_window"` |
|
||||
| Report `success=false` + `error="target_not_found"` après MAX_RETRIES_PER_ACTION (3) | api_stream.py:3949-4030 | running | paused_need_help | `failed_action.target_description` |
|
||||
| Hook v1.1 dead client signal (2+ giveups en 60s) | À AJOUTER (AXE_B1_DEEP §6 R4) | running | paused_need_help | `failed_action.reason="dead_client"` |
|
||||
| Hook v2 (cas n) : N orphan giveup sur action type critique | À DÉCIDER avec Dom | running | paused_need_help | `failed_action.reason="orphan_max_resends"` |
|
||||
|
||||
### 8.2. État de `_retry_pending` au moment de la pause
|
||||
|
||||
- **Pause via `pause_for_human`** : aucune action en vol (pause arrive *avant* dispatch).
|
||||
- **Pause via report failed** : l'action qui a déclenché la pause vient d'être `pop`pée. `_retry_pending` est **vide pour cet action_id** (déjà acquittée). Aucune purge supplémentaire nécessaire.
|
||||
- **Pause via watchdog hook (v1.1)** : `_retry_pending` peut contenir des entrées orphelines avec age > MAX. **Politique :** purger en transition (à ajouter dans le hook).
|
||||
|
||||
### 8.3. /resume — reconstruction de l'action
|
||||
|
||||
`resume_replay` (api_stream.py:4361-4474) :
|
||||
1. Vérifie state.status == `paused_need_help` (sinon 409).
|
||||
2. Vérifie acquittement safety_checks required (sinon 400).
|
||||
3. Reset state : `status="running"`, `failed_action=None`, `pause_message=None`, `safety_checks=[]`.
|
||||
4. Reconstruit l'action :
|
||||
- Priorité 1 : `failed_action.original_action` si présent.
|
||||
- Priorité 2 : `_retry_pending.pop(failed_action.action_id, {}).get("action")`.
|
||||
- Priorité 3 : minimum `{action_id, type, target_spec, visual_mode}`.
|
||||
5. Nouveau `action_id = "{original}_resume"`.
|
||||
6. Enregistre dans `_retry_pending[resume_id] = {action,retry_count:0,replay_id,reason:"resume_after_pause"}`.
|
||||
7. Insère en tête `_replay_queues[session_id]`.
|
||||
|
||||
**Lacune v1 :** le nouveau `action_id` (`_resume`) **n'a pas de `attempt_id`** explicite. Au prochain dispatch, le watchdog démarre le compteur à 0. Cohérent.
|
||||
|
||||
### 8.4. Event bus `[BUS]`
|
||||
|
||||
| Event | Quand | Payload (log structuré) | Source |
|
||||
|---|---|---|---|
|
||||
| `[BUS] lea:safety_checks_generated` | Pause `pause_for_human` avec checks | `replay=<id> count=N sources=[…]` | api_stream.py:3081 |
|
||||
| `[BUS] lea:monitor_routed` | Dispatch action visuelle (résolution monitor) | `replay=<id> action=<id> idx=N source=<…>` | api_stream.py:3419 |
|
||||
| `[BUS] lea:dispatch_orphan_resent` (v1.1) | Watchdog repush | `action_id=X resent=N/MAX age=Ts session machine replay` | AXE_B1_DEEP §3 |
|
||||
| `[BUS] lea:dispatch_orphan_giveup` (v1.1) | Watchdog abandon | `action_id=X resent=N age_total=Ts session machine replay` | AXE_B1_DEEP §3 |
|
||||
| `[BUS] lea:dead_client_signal` (v2) | Hook ≥2 giveups/60s | `session=<S> dead_count=N period=60s` | À AJOUTER |
|
||||
|
||||
Tous les events sont consommables via `journalctl --user -u rpa-streaming -f | grep '\[BUS\]'`. Pas de bus pub/sub réel (pattern QW1/QW4 = log structuré).
|
||||
|
||||
---
|
||||
|
||||
## 9. Compatibilité polling actuel ↔ futur SSE
|
||||
|
||||
Le contrat des §4 / §5 / §6 / §8 est **invariant** par rapport au transport. Voici ce qui change vs ce qui ne change pas :
|
||||
|
||||
| Aspect | Polling (v1 actuel) | SSE (cible AXE_B1) | Change ? |
|
||||
|---|---|---|---|
|
||||
| Endpoint dispatch | `GET /api/v1/traces/stream/replay/next` (1 réponse JSON) | `GET /api/v1/traces/stream/replay/events` (stream `text/event-stream`) | OUI |
|
||||
| Format payload action | JSON dans body | JSON dans `data:` field d'un `ServerSentEvent` (event=`action`) | NON (même schéma) |
|
||||
| Endpoint report | `POST /api/v1/traces/stream/replay/result` | identique | NON |
|
||||
| ID corrélation | `action_id` + (v2) `attempt_id` | identique + `id:` SSE = `action_id` | NON |
|
||||
| Détection déco client | Indirecte (pas de poll suivant) | `await request.is_disconnected()` immédiat | OUI (gain) |
|
||||
| Détection déco serveur | Timeout client 30s | `EventSource.onerror` + reconnect natif | OUI (gain) |
|
||||
| Reprise après reconnect | Pas de Last-Event-ID, watchdog seul | `Last-Event-ID` header automatique côté sseclient-py | OUI (gain) |
|
||||
| Watchdog `_retry_pending` | **ACTIF** | **ACTIF** (ceinture+bretelles, cf. AXE_B1_DEEP §12) | NON |
|
||||
| dedup_set client | **ACTIF v2** | **ACTIF v2** | NON |
|
||||
| `_replay_lock` serveur | Tient pendant exécution serveur (extract_text…) | Idem (les actions server-side restent dans la même boucle) | NON |
|
||||
| Bulle pause client | Reçue via `replay_paused:true` au prochain poll | Reçue via event `event=paused` ou via `replay_paused:true` dans event `action` | NON (même UX) |
|
||||
|
||||
**Flag de bascule :** `RPA_REPLAY_TRANSPORT=poll|sse` côté client (executor.py choisit poll vs `replay_subscriber.py`) et serveur (les 2 endpoints coexistent — pas de mutual exclusion). Permet rollback 1-ligne.
|
||||
|
||||
**Garantie de migration :** un client v2 polling et un client v2 SSE consomment **strictement le même contrat de message**. Le watchdog serveur scanne `_retry_pending` indépendamment du transport. Tous les invariants 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_<hex_workflow>` + suffixes. dedup_set client = équivalent action cache court-terme.
|
||||
- **Différence :** browser-use est intra-process (loop Python contrôle Chromium via CDP local). Notre cas inter-process inter-machine.
|
||||
- **Source :** [browser-use AGENTS.md](https://github.com/browser-use/browser-use/blob/main/AGENTS.md), [Idempotent AI agents — buildmvpfast 2026](https://www.buildmvpfast.com/blog/idempotent-ai-agent-retry-safe-patterns-production-workflow-2026)
|
||||
|
||||
### 10.5. Anthropic Computer Use SDK — tool_use_id binding
|
||||
- **Contrat :** chaque `tool_use` retourné par Claude a un `id` ; le code applicatif doit retourner un `tool_result` avec `tool_use_id` identique (loop.py lignes 234-254).
|
||||
- **Retry :** uniquement au niveau API (`max_retries=4` côté client Anthropic, ligne 182). **Pas de retry au niveau tool execution** — c'est le modèle qui re-décide au prochain tour.
|
||||
- **Idempotence :** non garantie par le SDK. Délégué à l'application (« deduplication or idempotency key handling visible in this loop : none »).
|
||||
- **Notre mapping :** `tool_use_id` ↔ `action_id`. Mais notre boucle est *server-driven* (queue d'actions pré-compilée par VWB), pas LLM-driven. Plus déterministe, donc plus simple à idempotenter.
|
||||
- **Source :** [computer-use-demo/loop.py main](https://github.com/anthropics/claude-quickstarts/blob/main/computer-use-demo/computer_use_demo/loop.py), [Tool use Claude API docs](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/overview)
|
||||
|
||||
### 10.6. Playwright MCP — SSE remote transport
|
||||
- **Contrat :** transport stdio (local) OU HTTP/SSE (remote). Tools/list au handshake, tool/call par event SSE descendant, tool/result POST remontant.
|
||||
- **Issue connue 2026 :** « SSE stream disconnected » après idle (cline/cline #8367). Mitigation = ping applicatif.
|
||||
- **Timeout :** 30s par défaut sur CDP endpoint connect.
|
||||
- **Notre mapping :** très proche du pattern cible AXE_B1 §4 (SSE descendant + POST ack). Confirme robustesse du choix techno. Précaution : prévoir reconnect natif (sseclient-py).
|
||||
- **Source :** [microsoft/playwright-mcp](https://github.com/microsoft/playwright-mcp), [cline issue #8367 SSE disconnect](https://github.com/cline/cline/issues/8367)
|
||||
|
||||
### 10.7. Synthèse comparative
|
||||
|
||||
| Système | ID corrélation | Visibility/Ack timeout | Max retry transport | Dedup client | Modèle delivery |
|
||||
|---|---|---|---|---|---|
|
||||
| **AWS SQS std** | MessageId + ReceiptHandle | VisibilityTimeout 30s | maxReceiveCount (DLQ) | obligatoire app | at-least-once |
|
||||
| **NATS JetStream** | StreamSeq + ConsumerSeq | AckWait 30s | MaxDeliver | obligatoire app | at-least-once |
|
||||
| **Skyvern** | step.step_id | n/a (monolithe) | max_steps_per_run | n/a | exactly-once local |
|
||||
| **browser-use** | (cmd, selector, value) | n/a | max_failures=3 | action cache | exactly-once local |
|
||||
| **Anthropic CU** | tool_use_id | n/a | max_retries client (API) | non garanti | exactly-once par tour |
|
||||
| **Playwright MCP** | request_id | 30s CDP | n/a (LLM décide) | non garanti | best-effort |
|
||||
| **Nous (v2 cible)** | `action_id` + `attempt_id` | ORPHAN_TIMEOUT 30s | MAX_RESENDS=2 | dedup_set 256 LRU | at-least-once + dedup → effectif exactly-once |
|
||||
|
||||
---
|
||||
|
||||
## 11. Sources
|
||||
|
||||
### Code interne (lecture seule, lignes vérifiées 2026-05-24)
|
||||
- `agent_v0/server_v1/api_stream.py:520-559` — `_replay_lock`, `_async_replay_lock`, `_replay_queues`, `_replay_states`, `_machine_replay_target`
|
||||
- `agent_v0/server_v1/api_stream.py:626-651` — `ReplayResultReport` Pydantic schema
|
||||
- `agent_v0/server_v1/api_stream.py:2906-3443` — `get_next_action` (DISPATCH path)
|
||||
- `agent_v0/server_v1/api_stream.py:3132-3197` — actions server-side `extract_text/t2a_decision/...`
|
||||
- `agent_v0/server_v1/api_stream.py:3354-3359` — création `_retry_pending` (à enrichir AXE_B1_DEEP §4.1)
|
||||
- `agent_v0/server_v1/api_stream.py:3446-3491` — `report_action_result`, pop idempotent
|
||||
- `agent_v0/server_v1/api_stream.py:3785-3870` — bascule `paused_need_help` sur system_dialog
|
||||
- `agent_v0/server_v1/api_stream.py:4361-4474` — `resume_replay` + safety_checks
|
||||
- `agent_v0/server_v1/api_stream.py:4477-4494` — `cancel_replay` + purge
|
||||
- `agent_v0/server_v1/replay_engine.py:2583-2642` — `_schedule_retry` (retry métier, distinct du retry transport)
|
||||
- `agent_v0/agent_v1/core/executor.py:2275-2503` — `poll_and_execute` + `_poll_and_execute_inner`
|
||||
- `agent_v0/agent_v1/core/executor.py:2308-2321` — `requests.get(/replay/next, timeout=30)` (fix 8 mai)
|
||||
- `agent_v0/agent_v1/core/executor.py:2476-2501` — `requests.post(/replay/result, timeout=10)`
|
||||
- `agent_v0/agent_v1/network/streamer.py:1-120` — streaming events/screenshots (canal séparé du replay)
|
||||
|
||||
### Docs internes
|
||||
- `docs/recherche/AXE_B1_REPLAY_TRANSPORT.md` (2026-05-23) — choix SSE vs WebSocket, pseudo-code endpoint
|
||||
- `docs/recherche/AXE_B1_DEEP_WATCHDOG.md` (2026-05-24) — implémentation watchdog complète
|
||||
- `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` — diagnostic 9 actions perdues, racine du contrat
|
||||
- `docs/SYNTHESE_TECHNOS_REPLAY_2026-05-23.md` §4 — synthèse replay
|
||||
- `docs/LESSONS_LEARNED_GHT_2026-05.md` — bugs P0 post-démo
|
||||
- `docs/SMOKE_TEST_FINALIZE_REPLAY_2026-05-20.md` — contrat finalize → replay
|
||||
|
||||
### Sources externes (consultées 2026-05-24)
|
||||
|
||||
**Patterns queue / visibility timeout / idempotence**
|
||||
- [Amazon SQS visibility timeout](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html)
|
||||
- [Amazon SQS exactly-once processing FIFO](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues-exactly-once-processing.html)
|
||||
- [Amazon SQS message deduplication ID](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/using-messagededuplicationid-property.html)
|
||||
- [NATS JetStream Consumers](https://docs.nats.io/nats-concepts/jetstream/consumers)
|
||||
- [NATS JetStream Model Deep Dive](https://docs.nats.io/using-nats/developer/develop_jetstream/model_deep_dive)
|
||||
- [How to Handle SQS Visibility Timeout (oneuptime 2026-01-27)](https://oneuptime.com/blog/post/2026-01-27-sqs-message-visibility-timeout/view)
|
||||
- [Achieving idempotency in AWS serverless (Albaqali)](https://qasimalbaqali.medium.com/achieving-idempotency-in-the-aws-serverless-space-d0671a521479)
|
||||
|
||||
**Frameworks RPA / Computer Use**
|
||||
- [Skyvern forge/agent.py main](https://github.com/Skyvern-AI/skyvern/blob/main/skyvern/forge/agent.py) — `execute_step` 1094-1577
|
||||
- [Skyvern webeye/actions/handler.py](https://github.com/Skyvern-AI/skyvern/blob/main/skyvern/webeye/actions/handler.py)
|
||||
- [Skyvern PR #434 better catch exceptions](https://github.com/Skyvern-AI/skyvern/pull/434)
|
||||
- [Skyvern retry run webhook docs](https://www.skyvern.com/docs/api-reference/api-reference/agent/retry-run-webhook)
|
||||
- [Anthropic computer-use-demo loop.py](https://github.com/anthropics/claude-quickstarts/blob/main/computer-use-demo/computer_use_demo/loop.py) — dispatch 234-254
|
||||
- [Anthropic Tool use overview docs](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/overview)
|
||||
- [Microsoft Playwright MCP](https://github.com/microsoft/playwright-mcp)
|
||||
- [Cline SSE disconnect issue #8367](https://github.com/cline/cline/issues/8367)
|
||||
- [browser-use AGENTS.md main](https://github.com/browser-use/browser-use/blob/main/AGENTS.md)
|
||||
- [browser-use issue #3615 agent not stopping](https://github.com/browser-use/browser-use/issues/3615)
|
||||
|
||||
**Patterns idempotence agents AI**
|
||||
- [Idempotent AI Agents — buildmvpfast 2026](https://www.buildmvpfast.com/blog/idempotent-ai-agent-retry-safe-patterns-production-workflow-2026)
|
||||
- [Fault-Tolerant AI Agent Pipelines — MightyBot](https://mightybot.ai/blog/fault-tolerant-ai-agent-pipelines/)
|
||||
- [Action Verification and Retries in LLM Agent Loops — ingramhaus](https://ingramhaus.com/action-verification-and-retries-in-llm-agent-execution-loops)
|
||||
- [Idempotency in Distributed Systems — aloknecessary](https://aloknecessary.github.io/blogs/idempotency-distributed-systems/)
|
||||
- [Stripe API idempotent requests](https://docs.stripe.com/api/idempotent_requests)
|
||||
|
||||
**Transport SSE / FastAPI**
|
||||
- [sse-starlette GitHub](https://github.com/sysid/sse-starlette)
|
||||
- [FastAPI lifespan events](https://fastapi.tiangolo.com/advanced/events/)
|
||||
- [Stop streaming response when client disconnects (FastAPI #7572)](https://github.com/fastapi/fastapi/discussions/7572)
|
||||
|
||||
---
|
||||
|
||||
## 12. Décisions non tranchables sans Dom (sortir explicitement du contrat v2)
|
||||
|
||||
Ces points sont identifiés mais demandent un arbitrage produit :
|
||||
|
||||
| # | Cas limite | Question | Recommandation Claude |
|
||||
|---|---|---|---|
|
||||
| D1 | Cas (i) — restart serveur pendant actions en `_retry_pending` | Faut-il persister `_retry_pending` (SQLite) pour rebuild ? Ou accepter perte transport au restart ? | **Accepter perte v2** : le restart serveur est volontaire (`systemctl restart`), Pauline relance le replay depuis VWB. Surcoût persistance > bénéfice. |
|
||||
| D2 | Cas (n) — politique abandon | Bascule en `paused_need_help` après MAX_RESENDS atteint ? Pour quels types d'action ? | **OUI pour `click`/`type`/`t2a_decision`** (critiques). **Log seul pour `wait`/`scroll`** (continuer). À ajouter dans hook watchdog v1.1. |
|
||||
| D3 | Cas (o) — actions server-side perdues | Retry serveur sur `extract_text` qui timeout ? Watchdog dédié actions server-side ? | **Différer** : v1 = un seul try + log warning, comportement actuel acceptable. v2 envisageable si bench Ollama montre instabilités fréquentes. |
|
||||
| D4 | Cas (p) — purge `_retry_pending` à la complétion workflow | Ajouter purge automatique en transition vers `completed/error/failed` ? | **OUI**, simple à ajouter analogue à cancel (api_stream.py:4489). |
|
||||
| D5 | dedup_set côté client | Implémenter v2 obligatoire ? Quelle taille LRU ? Inclure `attempt_id` ou juste `action_id` ? | **OUI obligatoire v2.** Taille 256 (couvre largement les workflows GHT 50 steps). Key = `action_id` seul (le `attempt_id` n'apporte rien côté dedup — l'objectif est de bloquer la double exécution même action). |
|
||||
| D6 | `attempt_id` côté serveur | Générer UUID à chaque dispatch (initial + resend) ? Stocker l'historique ? | **OUI v2.** Génération à chaque DISPATCH dans `get_next_action`. Pas d'historique nécessaire (logs structurés `[BUS] lea:dispatch_orphan_resent` suffisent). |
|
||||
| D7 | Migration backward-compat | Si client v1 (sans dedup_set, sans `attempt_id` echo) parle à serveur v2, casse-t-il ? | **NON** : `attempt_id` est optionnel côté serveur (toléré absent). dedup_set est purement défensif côté client. Migration progressive sans rupture. |
|
||||
| D8 | Cas (l) — protocole d'interruption serveur→client d'une action en vol | Ajouter mécanisme `cancel_in_flight` ? | **NON v1, v2** : pas nécessaire pour la démo. Pause supervisée sur step suivant suffit. |
|
||||
|
||||
---
|
||||
|
||||
## 13. Liens vers autres specs en cours
|
||||
|
||||
- **`spec_validator`** (à venir) — un Validator strict (sémantique post-action) ne peut être fiable que si toutes les actions arrivent. **SPEC_TRANSPORT est prérequis logique de spec_validator.** Le contrat REPORT.warning peut s'enrichir de codes de Validator (`semantic_fail`, `expected_text_not_found`) sans casser ce contrat.
|
||||
- **`spec_popups`** (à venir) — la détection popup côté serveur (pre-check) ET côté client (DialogHandler) émet des actions synthétiques `wait` ou des reports `warning="popup_handled"`. **Cas (q) du §5 documente la non-interférence.** Le contrat dialog/popup s'imbrique sur les mêmes endpoints sans extension.
|
||||
- **AXE_B2 (Validator)** : couvre le côté `verify_action` + Critic sémantique (déjà partiellement codé dans `replay_verifier.py`). À spécifier en parallèle.
|
||||
- **AXE_B4 (ORA Observe-Reason-Act)** : pousse aussi dans `_replay_queues` → bénéficie gratuitement du watchdog et du contrat.
|
||||
- **AXE_D2 (Dialog/Popup)** : `system_dialog` + `wrong_window` + DialogHandler — branches bascule pause supervisée déjà tracées §8.1.
|
||||
|
||||
---
|
||||
|
||||
*Document de spécification contractuelle. Lecture seule sur code, aucune modification. À valider par Dom avant implémentation v2 (dedup_set, attempt_id, hooks watchdog).*
|
||||
1319
docs/recherche/SPEC_VALIDATOR_MATRICE.md
Normal file
1319
docs/recherche/SPEC_VALIDATOR_MATRICE.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user