feat(vwb): câblage 19 blocs, OCR réel, screenshots ancres, configs déploiement
Some checks failed
security-audit / Bandit (scan statique) (push) Successful in 13s
security-audit / pip-audit (CVE dépendances) (push) Successful in 11s
security-audit / Scan secrets (grep) (push) Successful in 8s
tests / Lint (ruff + black) (push) Successful in 13s
tests / Tests unitaires (sans GPU) (push) Failing after 14s
tests / Tests sécurité (critique) (push) Has been skipped

Dispatch execute_action élargi de 12 à 19 blocs opérationnels :
- 4 blocs souris (hover, drag_drop, scroll, focus) avec pyautogui
- extract_text via Ollama VLM (remplace stub hardcodé)
- 5 blocs ai_* redirigés vers execute_ai_analyze avec prompts adaptés
- screenshot_evidence (capture + sauvegarde PNG)
- verify_element_exists (détection visuelle CLIP)

Import workflows Léa enrichi :
- Bridge extrait anchor_image_base64 des edges
- Import crée VisualAnchor en DB + fichiers thumbnail sur disque
- PropertiesPanel affiche automatiquement les screenshots

Frontend :
- visual_condition et loop_visual masqués (hidden: true)
- Filtre dans ToolPalette pour exclure les blocs cachés

Déploiement :
- 2 configs agent (TIM Pauline + Dev Windows) avec machine_id unique
- 2 workflows démo dans la BDD (batch factures + extraction IA)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-04-18 09:40:28 +02:00
parent 4f61741420
commit 1acea85fa6
11 changed files with 717 additions and 103 deletions

View File

@@ -218,6 +218,20 @@ def convert_learned_to_vwb_steps(
if target.get("by_text"):
vwb_params["target_text"] = target["by_text"]
# Extraire le screenshot de l'ancre pour la preview dans le VWB
anchor_b64 = (
target.get("anchor_image_base64")
or target.get("screenshot")
or action_params.get("anchor_image_base64")
)
if anchor_b64:
vwb_params["_anchor_image_base64"] = anchor_b64
bbox = target.get("by_position")
if bbox and isinstance(bbox, (list, tuple)) and len(bbox) >= 2:
vwb_params["_anchor_bbox"] = {
"x_pct": bbox[0], "y_pct": bbox[1]
}
label = _build_step_label(vwb_action_type, vwb_params, from_name, to_name)
steps.append({
"action_type": vwb_action_type,
@@ -229,6 +243,10 @@ def convert_learned_to_vwb_steps(
"metadata": edge_meta,
})
# Fusionner les type_text consécutifs et les key_press en combos
steps = _merge_consecutive_text_inputs(steps)
steps = _merge_consecutive_key_presses(steps)
# Appliquer le layout serpentin à tous les steps
_compute_layout(steps)
@@ -298,6 +316,79 @@ def _convert_compound_substep(
return vwb_type, vwb_params
def _merge_consecutive_text_inputs(
steps: List[Dict[str, Any]],
) -> List[Dict[str, Any]]:
"""
Fusionne les steps type_text consécutifs en un seul.
Quand un compound est décomposé lettre par lettre (ex: "bonjour" → 7 steps),
cette fonction les recombine en un seul step "Saisir : bonjour".
"""
if not steps:
return steps
merged = [steps[0]]
for step in steps[1:]:
prev = merged[-1]
if (prev["action_type"] == "type_text"
and step["action_type"] == "type_text"):
# Concaténer le texte
prev_text = prev.get("parameters", {}).get("text", "")
curr_text = step.get("parameters", {}).get("text", "")
prev["parameters"]["text"] = prev_text + curr_text
# Mettre à jour le label
combined = prev["parameters"]["text"]
prev["label"] = f'Saisir : "{combined}"'
else:
merged.append(step)
# Réindexer les ordres
for idx, step in enumerate(merged):
step["order"] = idx
return merged
def _merge_consecutive_key_presses(
steps: List[Dict[str, Any]],
) -> List[Dict[str, Any]]:
"""
Fusionne les key_press / keyboard_shortcut consécutifs portant une seule touche
en un seul keyboard_shortcut combo (ex: ctrl puis s → ctrl+s).
Ne fusionne que les steps keyboard_shortcut consécutifs dont chacun ne porte
qu'une seule touche (signe d'un combo décomposé). Les raccourcis déjà composés
(keys avec 2+ éléments) ne sont pas touchés.
"""
if not steps:
return steps
merged = [steps[0]]
for step in steps[1:]:
prev = merged[-1]
if (prev["action_type"] == "keyboard_shortcut"
and step["action_type"] == "keyboard_shortcut"):
prev_keys = prev.get("parameters", {}).get("keys", [])
curr_keys = step.get("parameters", {}).get("keys", [])
# Ne fusionner que si chaque step porte exactement 1 touche
# (un combo déjà composé comme ["ctrl", "s"] ne doit pas absorber le suivant)
if len(curr_keys) == 1 and len(prev_keys) >= 1:
# Vérifier que le prev est lui-même issu d'une fusion ou d'une seule touche
# On fusionne tant que c'est un enchaînement de touches simples
prev["parameters"]["keys"] = prev_keys + curr_keys
combo_str = "+".join(prev["parameters"]["keys"])
prev["label"] = f"Raccourci : {combo_str}"
continue
merged.append(step)
# Réindexer les ordres
for idx, step in enumerate(merged):
step["order"] = idx
return merged
def _compute_layout(
steps: List[Dict[str, Any]],
cols: int = 3,