Nouvelle action native VWB qui force le replay à basculer en paused_need_help
avec un message custom. Quand Léa atteint cette étape, elle ne tente pas
d'exécuter — elle pose immédiatement le state, ce qui déclenche la bulle
interactive ChatWindow (J3.5) avec boutons Continuer / Annuler.
Asset démo majeur GHT Sud 95 : permet de scénariser le moment "Léa doute"
au bon endroit dans le workflow, sans dépendre d'un échec aléatoire.
Chaîne complète :
- VWB UI (types.ts) : nouvelle entrée ACTIONS catégorie 'logic', icône ⏸,
paramètre 'message' éditable (textarea).
- Bridge VWB → core (learned_workflow_bridge.py) : passthrough du type +
préservation du message dans parameters.
- Pipeline replay (replay_engine.py) : type ajouté à _ALLOWED_ACTION_TYPES,
conversion edge → action normalisée préserve le message.
- Streaming server (api_stream.py /replay/next) : interception avant envoi
à l'Agent V1 → bascule state en paused_need_help avec pause_message,
retourne {action: None, replay_paused: True}.
- L'action n'est jamais transmise à l'Agent V1 — pure logique serveur.
10 nouveaux tests pytest. Total branche : 57/57 verts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Le pHash global 8x8 sur écran 1920x1080 ne détecte pas l'ouverture d'un
dialog modal dans une VM QEMU (un dialog 800x500 couvre ~3 pixels pHash,
distance Hamming typique = 1-2, sous le seuil de 3). Découvert sur Win11/
Notepad : Ctrl+Shift+S ouvrait bien le dialog mais Léa abortait à tort.
_handle_post_shortcut() poll désormais DialogHandler.handle_if_dialog()
toutes les 500ms (EasyOCR + KNOWN_DIALOGS). 8s pour le premier dialog,
3s de stabilité entre dialogs successifs, 60s budget total.
KNOWN_DIALOGS réordonné : popups modaux (confirmer/remplacer/écraser)
prioritaires sur fenêtres parents (enregistrer sous/save as) car l'OCR
full-screen capte les deux simultanément.
DialogHandler bascule sur UITarsGrounder subprocess one-shot (au lieu
du serveur HTTP localhost:8200 qui n'existait plus). InfiGUI worker,
think_arbiter et ui_tars_grounder alignés sur le même contrat.
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Architecture grounding complète :
- core/grounding/server.py : serveur FastAPI (port 8200) avec UI-TARS-1.5-7B en 4-bit NF4
Process séparé avec son propre contexte CUDA (résout le crash Flask/CUDA)
- core/grounding/pipeline.py : orchestrateur cascade template→OCR→UI-TARS→static
- core/grounding/template_matcher.py : TemplateMatcher centralisé (remplace 5 copies)
- core/grounding/ui_tars_grounder.py : client HTTP vers le serveur de grounding
- core/grounding/target.py : GroundingTarget + GroundingResult
ORA modifié :
- _act_click() : capture unique de l'écran envoyée au serveur de grounding
- Pre-check VLM skippé pour ui_tars (redondant, et Ollama n'a plus de VRAM)
- verify_level='none' par défaut (vérification titre OCR prévue en Phase 2)
- Détection réponses négatives UI-TARS ("I don't see it" → fallback OCR)
Nettoyage :
- 9 fichiers morts archivés dans _archive/ (~6300 lignes supprimées)
- 21 tests ajoutés pour TemplateMatcher
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
L'OCR seul donnait du bruit (\"- C\", \"emo\"). Le VLM (qwen2.5vl:3b)
est maintenant appelé systématiquement pour décrire l'ancre en 5 mots
(\"folder icon named Demo\", \"search bar with magnifier icon\").
Le target_text utilise l'OCR si lisible, sinon la description VLM.
La description VLM est toujours stockée dans ocr_description.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CRITIQUE : ajout should_continue callback dans ORALoop pour supporter
les boutons Stop/Pause du frontend en mode verified et instruction.
HAUTE : suppression sys.stdout.write de debug, logger.warning→debug
dans _grounding_ocr.
BASSE : suppression import mort 'field' dans observe_reason_act.py.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
reason_instruction() : le VLM regarde l'écran, décide la prochaine
action atomique (click/type/hotkey/scroll/done), retourne un Decision
avec expected_after pour la vérification.
run_instruction() : boucle ORA complète pour instructions texte.
CognitiveContext mis à jour à chaque étape (objectif, historique,
faits appris, confiance).
POST /api/v3/execute/instruction : endpoint API pour lancer une
instruction en langage naturel. Thread daemon, polling du résultat
via GET /api/v3/execute/instruction/result.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
23/24 tests du workflow Demo PMSI réussis (1 échec = main sur souris).
Template matching en premier (~80ms), CLIP batch en fallback (~4.5s).
Total workflow : ~20s (était 131s il y a 24h).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Boucle fermée : time.sleep(2.0) remplacé par _wait_for_screen_change()
qui poll le pHash toutes les 150ms. Sort dès que l'écran change.
4 occurrences remplacées.
Batch CLIP : filtre par distance AVANT le CLIP (90→~20 éléments),
puis embed_image_batch() en un seul appel GPU + np.dot vectorisé.
Estimé : 42s→~20s total workflow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
cv2.matchTemplate cherche l'ancre directement dans le screenshot.
Pas de RF-DETR, pas de CLIP, pas de 90 comparaisons.
Seuil 0.75 pour éviter les faux positifs.
Ordre : template (1ms) → CLIP (fallback) → OCR/UI-TARS (dernier recours)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
RF-DETR détecte 90+ éléments UI par screenshot. Sur CPU = 28s.
Sur GPU RTX 5070 = devrait être 1-3s.
CLIP auto-GPU déjà en place (vérifie 1.5 Go VRAM libre).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Après minimisation du navigateur, xdotool active la fenêtre suivante
(VM QEMU, app cible). Avant, le terminal restait au premier plan →
mss capturait le terminal au lieu de la VM.
Cause racine de tous les échecs de matching.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
docTR se chargeait au premier appel OCR (~30s). Maintenant pré-chargé
au démarrage du backend → premier clic rapide.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Avant : OCR retournait le premier match → cliquait sur la barre de titre
("CR_patient_demo" dans le path) au lieu du fichier dans la liste.
Après : collecte tous les matchs, choisit le plus proche de la position
originale de l'ancre (anchor_bbox). Si pas de bbox, prend le plus central.
Élimine les clics sur les barres de titre, breadcrumbs, menus.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Le grounding par texte (OCR → UI-TARS) est maintenant la méthode
PRINCIPALE. CLIP n'est appelé que si le grounding échoue.
Avant : CLIP (faux positifs confiants) → cascade grounding (rarement atteinte)
Après : OCR 1s → UI-TARS 3s → CLIP (fallback visuel pur)
C'est comme ça que font UI-TARS, Agent-S3 et AppAgent.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Quand l'utilisateur sélectionne une ancre dans le VWB :
1. OCR docTR extrait le texte du crop → target_text
2. Si texte < 3 chars → VLM qwen2.5vl:3b décrit en 5 mots
3. Stocké en BDD (VisualAnchor.target_text + ocr_description)
4. Injecté automatiquement dans les params à l'exécution
L'exécution sait maintenant QUOI chercher dès le départ :
- CLIP vérifie par OCR que le texte correspond
- Le grounding cascade a un vrai target_text
- Plus besoin de deviner à chaque run
Migration SQLite gracieuse (ALTER TABLE si colonnes absentes).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Quand CLIP dit "trouvé", on vérifie par OCR que le texte à cette
position correspond au target. Si CLIP clique sur "Ce PC" au lieu
de "CR_patient_demo", l'OCR le rejette → fallback sur la cascade.
Description VLM de l'ancre AVANT le CLIP quand le label est un
type d'action (double_click_anchor → "text file icon CR_patient").
Le target_text enrichi sert à la vérification croisée ET au grounding.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
La capture VWB utilisait monitors[0] (composite) mais l'exécution
utilisait monitors[1] (premier écran). Images incompatibles → CLIP
retournait 0.00 sur un écran identique.
Tous les fichiers alignés sur monitors[0].
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Le seuil 0.20 faisait que CLIP cliquait sur Chrome au lieu du dossier
Demo (score 0.25 accepté = faux positif). Le seuil 0.45 rejette les
matchs faibles et la cascade OCR/UI-TARS prend le relais proprement.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Seuil CLIP abaissé pour les icônes génériques (dossier, fichier)
qui obtenaient 0.25 au lieu de 0.45.
IntelligentExecutor en singleton — CLIP et RF-DETR chargés une
seule fois et réutilisés entre les étapes. Élimine le rechargement
de ~40s par étape.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SeeClick supprimé : modèle HF incompatible (QWenConfig non reconnu),
crashait à chaque exécution et polluait les logs.
Remplacé par UI-TARS via la chaîne de grounding.
Log warning visible quand la description VLM de l'ancre échoue
(pour diagnostiquer les problèmes de VRAM).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Grounding en cascade quand CLIP/template échouent :
1. OCR (docTR) → cherche le texte exact sur l'écran (~1s)
2. UI-TARS grounding → "click on X" → coordonnées (~3s, 94% ScreenSpot)
3. VLM reasoning → raisonnement complet + confirmation OCR (~10s)
find_element_on_screen() dans input_handler.py (partagé VWB + Léa).
Câblé dans find_and_click() et execute_action() comme fallback.
Refonte capture écran :
- mss.monitors[0] (composite) pour capturer la VM en plein écran
- FullscreenSelector réécrit : overlay via getBoundingClientRect()
- Bboxes et sélection alignées avec l'image (calcul JS, pas CSS)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FullscreenSelector réécrit :
- Overlay unique positionné via getBoundingClientRect()
- Recalcul auto au resize
- Coordonnées souris relatives à l'image
- Plus de décalage bboxes/sélection
Capture backend :
- mss.monitors[0] (écran composite) au lieu de pyautogui.screenshot()
- Capture la VM en plein écran correctement
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Point de sauvegarde incluant les fichiers non committés des sessions
précédentes (systemd, docs, agents, GPU manager).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Après la dernière étape, Léa vérifie l'écran et gère les dialogues
restants (jusqu'à 3 vérifications en cascade). Le workflow laisse
l'écran propre à la fin.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Le filtre len<3 bloquait les boutons "OK" (2 chars) et "No" (2 chars).
Seuil abaissé à 2 — filtre les lettres isolées mais laisse passer
les boutons courts courants des dialogues Windows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
UIPatternLibrary câblée dans l'executor et le stream processor.
Pendant un wait_for_anchor, Léa surveille l'écran toutes les secondes :
1. OCR plein écran (docTR)
2. Pattern matching (dialogues Save, OK, Cancel, cookies...)
3. OCR ciblé pour trouver le bouton par son texte réel
4. Clic sur le match le plus bas (bouton, pas titre)
Fix : seuil ratio supprimé (trigger trouvé = match, quelle que soit
la longueur du texte OCR). Matching strict mot exact ≥3 chars
(évite les faux positifs sur lettres isolées). Fallback recherche
partielle pour les lettres soulignées (E_nregistrer).
Plus aucune coordonnée hardcodée — 100% vision.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
VWB Executor :
- _check_screen_for_patterns() : capture écran + OCR + pattern matching
- _handle_detected_pattern() : clic automatique sur dialogues connus
- Vérifie entre chaque étape en mode intelligent/debug
- Si un dialogue bloque (OK, Save, Cancel), Léa le gère seule
Stream Processor :
- Enrichit les ScreenState avec ui_pattern/ui_pattern_action/ui_pattern_target
- Les patterns détectés sont loggés et stockés dans les résultats
- Permet au GraphBuilder de savoir quels écrans sont des dialogues
Phase 2 du plan "connaissance native de l'environnement".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CSS : le dropdown héritait color:white du header → forcé #212121
sur .workflow-dropdown et .dropdown-item .item-name
Bibliothèque : migration localStorage → backend (capture_library.json)
- GET/POST /api/v3/capture/library (max 50 captures)
- loadLibraryAsync() charge depuis backend, fallback localStorage
- saveLibrary() écrit dans les deux (localStorage + backend)
- capture_library.json gitignored
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Le refresh du layout X11 juste avant xdotool type force le bon keymap.
Sans ça, xdotool envoie des keycodes décalés (: → M, / → !, etc.)
dans les VM spice/QEMU.
Solution trouvée via askubuntu.com/questions/914718.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remet xclip+Ctrl+V comme méthode prioritaire. Les spice tools sont
installés dans la VM → le presse-papier est partagé → pas de problème
de mapping clavier. xdotool envoie des événements synthétiques X11
que spice/QEMU traite différemment des vraies frappes (: → M).
Citrix partage aussi le clipboard nativement → même méthode en prod.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
xdotool type pour les segments alphanumériques (un seul appel, rapide),
xdotool key avec keysym uniquement pour les caractères spéciaux
(:, /, @, etc.) qui cassent en AZERTY dans les VM.
Évite le subprocess par caractère (trop lent, effet visuel désagréable).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
xdotool type envoie des scancodes QWERTY — dans une VM AZERTY,
':' devient 'M', '/' devient '!', etc.
Nouvelle approche : xdotool key avec les noms de keysym X11
(colon, slash, period, etc.) qui sont indépendants du layout.
Chaque caractère est envoyé individuellement — plus lent mais
100% fiable en AZERTY/QWERTY, local ou VM.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
safe_type_text() : xdotool type en priorité au lieu du presse-papier.
Le clipboard xclip ne traverse pas les VM (QEMU) ni Citrix/RDP.
xdotool envoie des frappes X11 réelles que les VM capturent.
Délai 20ms entre caractères pour fiabilité.
Cosmétique : couleur texte forcée sur les items workflow du sidebar
(color: var(--text-primary)) — était blanc sur blanc.
Logs diagnostic ajoutés dans execute_workflow_thread et execute_action.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MAX_TEMPLATE_DISTANCE dans zoned_template_match était encore à 150px.
Tous les seuils de distance sont maintenant alignés à 500px :
- MAX_DISTANCE_PX (CLIP) : 500
- MAX_GLOBAL_DISTANCE (template global) : 500
- MAX_SEECLICK_DISTANCE : 500
- MAX_TEMPLATE_DISTANCE (template zonée) : 500
Commentaires périmés corrigés (plus de références aux anciennes valeurs).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Le template matching global trouvait l'icône Chrome à 0.99 de confiance
mais la rejetait car elle avait bougé de >150px. Même problème pour
SeeClick (>200px). Aligné tous les seuils de distance à 500px
pour supporter les workflows VWB cross-résolution.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MAX_DISTANCE_PX 120→500 (ancre peut être loin si résolution différente)
MIN_CLIP_SCORE 0.55→0.50 (tolérance basique suffisante)
MIN_COMBINED_SCORE 0.5→0.45 (accepter les matchs raisonnables)
L'icône Chrome à 81% de confiance était rejetée à cause de la distance.
Les workflows VWB manuels capturent sur un écran et s'exécutent
potentiellement sur un autre — la tolérance de distance doit être large.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Avant : tous les workflows importés s'appelaient « Unnamed Workflow »
→ la liste devenait illisible dès qu'il y en avait plusieurs.
Après : génération d'un nom explicite par _derive_default_name :
1. Premier `template.window.title_pattern` utile dans les nodes
(filtrage de "Unknown" / "unknown_window"), avec extraction de
l'app derrière le séparateur Windows « – » / « - »
(ex: « Sans titre – Bloc-notes » → « Bloc-notes »).
2. Premier `template.window.process_name` non-null
(ex: « explorer.exe »).
3. Fallback : 8 premiers caractères du workflow_id, après
nettoyage des préfixes techniques ("workflow_sess_", ...).
Le nom final inclut toujours la date de l'import :
« Léa Bloc-notes — 2026-04-16 08:41 »
« Léa explorer.exe — 2026-04-16 08:41 »
« Léa 20260404 — 2026-04-16 08:41 » (fallback)
Ne se déclenche que si le nom entrant est vide,
« Unnamed Workflow » ou « Workflow importé » (insensible à la
casse). Le paramètre `name` explicite de la requête reste
prioritaire. L'utilisateur peut renommer via le bouton éditer.
Pas de modification du schema workflow (champ `name` existant).
Tests manuels sur données réelles :
- notepad_enriched.json (tous nodes "Unknown") → fallback id OK
- Bloc-notes, Explorateur et Recherche (2) → « Léa Rechercher »
- workflow construit avec title 'Sans titre – Bloc-notes'
→ « Léa Bloc-notes » OK
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fichier SQLite vide (toutes tables à 0 lignes), tracé en git mais
jamais peuplé. La vraie source de vérité est `workflows.db`
(DATABASE_URL dans backend/.env → 3 workflows, 115 exécutions,
920 steps).
Risque éliminé : si `.env` n'était pas chargé (ex : systemd mal
configuré), SQLAlchemy retombait sur le fallback
`sqlite:///vwb_v3.db` et l'app créait/utilisait une BDD
complètement vide à côté de la vraie. Foot-gun classique.
Correctif :
- Fallback de app.py aligné sur workflows.db.
- Fichier vwb_v3.db supprimé du repo.
workflows.db reste seule source de vérité.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- B4 : supprime le double logging dans backend/app.py.
app.py est importé 2 fois (une fois comme __main__ via `python app.py`,
une fois comme module `app` via `from app import socketio` dans
api/websocket_handlers.py). Le RotatingFileHandler était donc ajouté
2× au root logger → chaque ligne loguée dupliquée. Fix : garde
idempotente qui vérifie si un handler vers vwb.log existe déjà.
- B6 : supprime les fichiers .pid résiduels (.backend.pid,
.frontend.pid, .frontend_v4.pid) et les ajoute au .gitignore
(avec *.lock, *.orig, *.bak).
- B7 : ajoute launch.sh (wrapper → run_v4.sh par défaut, legacy
→ run.sh), clarifie en tête de run.sh et run_v4.sh la distinction
frontend/ (legacy v3) vs frontend_v4/ (actif), et rectifie le
README.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>