feat(vwb-v3): Architecture Thin Client fonctionnelle

API = Source de vérité unique (SQLite + Flask)
- Backend: API v3 avec session, workflow, capture, execute
- Frontend: Vanilla TypeScript, pas de state local
- Contrats stricts pour les actions RPA
- Drag & drop pour réorganiser les étapes
- Insertion d'étapes entre deux existantes
- Bibliothèque de captures (sessionStorage)
- Exécution avec coordonnées statiques (pyautogui)

Fonctionne mais fragile (coordonnées fixes, pas de détection visuelle)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Dom
2026-01-23 12:07:13 +01:00
parent a9a53991bc
commit 858e6007f9
23 changed files with 6813 additions and 6 deletions

View File

@@ -0,0 +1,54 @@
"""
Contrats de Données VWB - Module d'initialisation
Auteur : Dom, Alice, Kiro - 09 janvier 2026
Ce module contient les contrats de données spécifiques au Visual Workflow Builder
pour les actions VisionOnly RPA.
Contrats disponibles :
- VWBActionError : Gestion des erreurs d'actions
- VWBEvidence : Preuves d'exécution avec screenshots
- VWBVisualAnchor : Ancres visuelles pour sélection d'éléments UI
"""
from .error import VWBActionError, VWBErrorType, VWBErrorSeverity
from .evidence import VWBEvidence, VWBEvidenceType
from .visual_anchor import VWBVisualAnchor, VWBVisualAnchorType
from .action_contracts import (
ActionContract,
ContractViolation,
ContractViolationType,
ContractValidationError,
VWB_ACTION_CONTRACTS,
validate_action_contract,
enforce_action_contract,
get_action_contract,
get_required_params,
list_all_action_types
)
__all__ = [
'VWBActionError',
'VWBErrorType',
'VWBErrorSeverity',
'VWBEvidence',
'VWBEvidenceType',
'VWBVisualAnchor',
'VWBVisualAnchorType',
# Contrats d'actions
'ActionContract',
'ContractViolation',
'ContractViolationType',
'ContractValidationError',
'VWB_ACTION_CONTRACTS',
'validate_action_contract',
'enforce_action_contract',
'get_action_contract',
'get_required_params',
'list_all_action_types'
]
__version__ = '1.0.0'
__author__ = 'Dom, Alice, Kiro'
__date__ = '09 janvier 2026'

View File

@@ -0,0 +1,403 @@
"""
Contrats Stricts des Actions VWB - Définition et Validation
Auteur : Dom, Alice, Kiro - 23 janvier 2026
Ce module définit les contrats stricts pour chaque action VWB.
Chaque action a des paramètres OBLIGATOIRES qui doivent être présents
pour que l'exécution soit autorisée.
PRINCIPE CLÉ: Si le contrat n'est pas respecté → BLOQUER l'exécution
avec un message d'erreur clair.
"""
from dataclasses import dataclass, field
from typing import Dict, List, Any, Optional, Set, Callable
from enum import Enum
class ContractViolationType(Enum):
"""Types de violation de contrat."""
MISSING_REQUIRED = "missing_required" # Paramètre obligatoire manquant
INVALID_TYPE = "invalid_type" # Mauvais type de valeur
INVALID_VALUE = "invalid_value" # Valeur invalide
FORBIDDEN_PARAM = "forbidden_param" # Paramètre interdit présent
INCOMPATIBLE_ACTION = "incompatible_action" # Type d'action incompatible avec params
@dataclass
class ContractViolation:
"""Représente une violation de contrat."""
violation_type: ContractViolationType
parameter: str
message: str
expected: Optional[str] = None
received: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
return {
"type": self.violation_type.value,
"parameter": self.parameter,
"message": self.message,
"expected": self.expected,
"received": self.received
}
@dataclass
class ActionContract:
"""Définition du contrat d'une action VWB."""
action_type: str
description: str
required_params: List[str]
optional_params: List[str] = field(default_factory=list)
param_validators: Dict[str, Callable[[Any], bool]] = field(default_factory=dict)
# Actions qui ne peuvent PAS avoir certains paramètres
forbidden_if_missing: Dict[str, str] = field(default_factory=dict)
def validate(self, parameters: Dict[str, Any]) -> List[ContractViolation]:
"""
Valide les paramètres contre le contrat.
Returns:
Liste de violations (vide si tout est OK)
"""
violations = []
# 1. Vérifier les paramètres obligatoires
for param in self.required_params:
if param not in parameters or parameters[param] is None:
violations.append(ContractViolation(
violation_type=ContractViolationType.MISSING_REQUIRED,
parameter=param,
message=f"Paramètre obligatoire '{param}' manquant pour l'action '{self.action_type}'",
expected=f"'{param}' doit être fourni",
received="absent ou None"
))
elif param in self.param_validators:
# Valider le contenu du paramètre
if not self.param_validators[param](parameters[param]):
violations.append(ContractViolation(
violation_type=ContractViolationType.INVALID_VALUE,
parameter=param,
message=f"Valeur invalide pour '{param}' dans l'action '{self.action_type}'",
expected="valeur valide selon les règles du contrat",
received=str(type(parameters[param]).__name__)
))
return violations
def has_visual_anchor(params: Dict[str, Any]) -> bool:
"""Vérifie si visual_anchor est présent et valide."""
anchor = params.get('visual_anchor') or params.get('target')
if not anchor:
return False
if not isinstance(anchor, dict):
return False
# Doit avoir soit une image, soit des coordonnées
has_image = bool(
anchor.get('screenshot') or
anchor.get('image') or
anchor.get('reference_image_base64') or
anchor.get('id') # ID d'ancre stockée sur le serveur
)
has_coords = bool(
anchor.get('bounding_box') or
anchor.get('boundingBox')
)
return has_image or has_coords
def has_text(params: Dict[str, Any]) -> bool:
"""Vérifie si text est présent et non vide."""
text = params.get('text') or params.get('text_to_type') or params.get('texte')
return bool(text and isinstance(text, str) and len(text.strip()) > 0)
def has_timeout(params: Dict[str, Any]) -> bool:
"""Vérifie si timeout est présent et valide."""
timeout = params.get('timeout') or params.get('timeout_ms') or params.get('max_wait_time_ms')
if timeout is None:
return True # Optionnel, une valeur par défaut sera utilisée
try:
return int(timeout) > 0
except (ValueError, TypeError):
return False
# =============================================================================
# DÉFINITION DES CONTRATS POUR CHAQUE ACTION VWB
# =============================================================================
VWB_ACTION_CONTRACTS: Dict[str, ActionContract] = {
# --- ACTIONS DE CLIC ---
"click_anchor": ActionContract(
action_type="click_anchor",
description="Clic sur un élément identifié par ancre visuelle",
required_params=["visual_anchor"],
optional_params=["click_type", "click_offset_x", "click_offset_y", "confidence_threshold"],
param_validators={"visual_anchor": lambda p: has_visual_anchor({"visual_anchor": p})}
),
"double_click_anchor": ActionContract(
action_type="double_click_anchor",
description="Double-clic sur un élément identifié par ancre visuelle",
required_params=["visual_anchor"],
optional_params=["click_offset_x", "click_offset_y", "confidence_threshold"],
param_validators={"visual_anchor": lambda p: has_visual_anchor({"visual_anchor": p})}
),
"right_click_anchor": ActionContract(
action_type="right_click_anchor",
description="Clic droit sur un élément identifié par ancre visuelle",
required_params=["visual_anchor"],
optional_params=["click_offset_x", "click_offset_y", "confidence_threshold"],
param_validators={"visual_anchor": lambda p: has_visual_anchor({"visual_anchor": p})}
),
"hover_anchor": ActionContract(
action_type="hover_anchor",
description="Survol d'un élément identifié par ancre visuelle",
required_params=["visual_anchor"],
optional_params=["hover_duration_ms", "confidence_threshold"],
param_validators={"visual_anchor": lambda p: has_visual_anchor({"visual_anchor": p})}
),
# --- ACTIONS DE SAISIE ---
"type_text": ActionContract(
action_type="type_text",
description="Saisie de texte (PAS de clic automatique, le focus doit être déjà fait)",
required_params=["text"],
optional_params=["typing_speed_ms", "clear_field_first", "press_enter_after"],
param_validators={"text": lambda p: bool(p and isinstance(p, str))}
),
"type_secret": ActionContract(
action_type="type_secret",
description="Saisie sécurisée de texte sensible (mot de passe)",
required_params=["secret_text"],
optional_params=["typing_speed_ms", "clear_field_first", "mask_in_evidence"],
param_validators={"secret_text": lambda p: bool(p and isinstance(p, str))}
),
# --- ACTIONS DE FOCUS ---
"focus_anchor": ActionContract(
action_type="focus_anchor",
description="Donne le focus à un élément (clic pour activer)",
required_params=["visual_anchor"],
optional_params=["focus_method", "verify_focus", "confidence_threshold"],
param_validators={"visual_anchor": lambda p: has_visual_anchor({"visual_anchor": p})}
),
# --- ACTIONS D'ATTENTE ---
"wait_for_anchor": ActionContract(
action_type="wait_for_anchor",
description="Attendre qu'un élément apparaisse ou disparaisse",
required_params=["visual_anchor"],
optional_params=["wait_mode", "max_wait_time_ms", "check_interval_ms"],
param_validators={"visual_anchor": lambda p: has_visual_anchor({"visual_anchor": p})}
),
# --- ACTIONS DE SCROLL ---
"scroll_to_anchor": ActionContract(
action_type="scroll_to_anchor",
description="Défiler jusqu'à ce qu'un élément soit visible",
required_params=["visual_anchor"],
optional_params=["scroll_direction", "scroll_speed", "max_scroll_attempts", "target_position"],
param_validators={"visual_anchor": lambda p: has_visual_anchor({"visual_anchor": p})}
),
"drag_drop_anchor": ActionContract(
action_type="drag_drop_anchor",
description="Glisser-déposer d'un élément vers un autre",
required_params=["source_anchor", "target_anchor"],
optional_params=["drag_speed", "hold_duration_ms"],
param_validators={
"source_anchor": lambda p: has_visual_anchor({"visual_anchor": p}),
"target_anchor": lambda p: has_visual_anchor({"visual_anchor": p})
}
),
# --- ACTIONS CLAVIER ---
"keyboard_shortcut": ActionContract(
action_type="keyboard_shortcut",
description="Exécuter un raccourci clavier",
required_params=["keys"],
optional_params=["hold_duration_ms"],
param_validators={"keys": lambda p: isinstance(p, list) and len(p) > 0}
),
# --- ACTIONS D'EXTRACTION ---
"extract_text": ActionContract(
action_type="extract_text",
description="Extraire du texte d'une zone identifiée par ancre",
required_params=["visual_anchor"],
optional_params=["extraction_mode", "text_filters", "output_format", "output_variable"],
param_validators={"visual_anchor": lambda p: has_visual_anchor({"visual_anchor": p})}
),
"extract_table": ActionContract(
action_type="extract_table",
description="Extraire un tableau d'une zone identifiée par ancre",
required_params=["visual_anchor"],
optional_params=["table_format", "output_variable"],
param_validators={"visual_anchor": lambda p: has_visual_anchor({"visual_anchor": p})}
),
"screenshot_evidence": ActionContract(
action_type="screenshot_evidence",
description="Capturer une preuve visuelle (screenshot)",
required_params=[], # Aucun paramètre obligatoire
optional_params=["region", "label", "include_timestamp"]
),
# --- ACTIONS CONDITIONNELLES ---
"visual_condition": ActionContract(
action_type="visual_condition",
description="Condition basée sur présence/absence d'élément visuel",
required_params=["visual_anchor"],
optional_params=["condition_type", "timeout_ms"],
param_validators={"visual_anchor": lambda p: has_visual_anchor({"visual_anchor": p})}
),
"loop_visual": ActionContract(
action_type="loop_visual",
description="Boucle tant qu'un élément est visible",
required_params=["visual_anchor"],
optional_params=["max_iterations", "timeout_ms"],
param_validators={"visual_anchor": lambda p: has_visual_anchor({"visual_anchor": p})}
),
# --- ACTIONS DONNÉES ---
"download_to_folder": ActionContract(
action_type="download_to_folder",
description="Télécharger un fichier vers un dossier",
required_params=["target_folder"],
optional_params=["filename_pattern", "timeout_ms"]
),
"ai_analyze_text": ActionContract(
action_type="ai_analyze_text",
description="Analyser du texte avec IA",
required_params=["visual_anchor", "analysis_prompt"],
optional_params=["model", "output_variable"],
param_validators={"visual_anchor": lambda p: has_visual_anchor({"visual_anchor": p})}
),
"db_save_data": ActionContract(
action_type="db_save_data",
description="Sauvegarder des données en base",
required_params=["data", "table_name"],
optional_params=["connection_id"]
),
"db_read_data": ActionContract(
action_type="db_read_data",
description="Lire des données depuis la base",
required_params=["query"],
optional_params=["connection_id", "output_variable"]
),
# --- ACTIONS DE VÉRIFICATION ---
"verify_element_exists": ActionContract(
action_type="verify_element_exists",
description="Vérifier qu'un élément existe à l'écran",
required_params=["visual_anchor"],
optional_params=["timeout_ms", "should_exist"],
param_validators={"visual_anchor": lambda p: has_visual_anchor({"visual_anchor": p})}
),
"verify_text_content": ActionContract(
action_type="verify_text_content",
description="Vérifier le contenu textuel d'un élément",
required_params=["visual_anchor", "expected_text"],
optional_params=["match_mode", "case_sensitive"],
param_validators={"visual_anchor": lambda p: has_visual_anchor({"visual_anchor": p})}
),
}
class ContractValidationError(Exception):
"""Exception levée quand un contrat n'est pas respecté."""
def __init__(self, violations: List[ContractViolation], action_type: str):
self.violations = violations
self.action_type = action_type
messages = [v.message for v in violations]
super().__init__(f"Contrat violé pour '{action_type}': {'; '.join(messages)}")
def to_dict(self) -> Dict[str, Any]:
return {
"error": "contract_violation",
"action_type": self.action_type,
"violations": [v.to_dict() for v in self.violations],
"message": str(self)
}
def validate_action_contract(action_type: str, parameters: Dict[str, Any]) -> List[ContractViolation]:
"""
Valide les paramètres d'une action contre son contrat.
Args:
action_type: Type de l'action (ex: "click_anchor", "type_text")
parameters: Paramètres fournis pour l'action
Returns:
Liste de violations (vide si tout est OK)
Raises:
ContractValidationError si le contrat n'est pas respecté
"""
# Normaliser le type d'action
action_type_normalized = action_type.lower().strip()
# Vérifier si l'action existe
if action_type_normalized not in VWB_ACTION_CONTRACTS:
# Action non reconnue - on laisse passer avec un warning
print(f"⚠️ [Contract] Action '{action_type}' non reconnue dans les contrats")
return []
contract = VWB_ACTION_CONTRACTS[action_type_normalized]
violations = contract.validate(parameters)
return violations
def enforce_action_contract(action_type: str, parameters: Dict[str, Any]) -> None:
"""
Valide et BLOQUE si le contrat n'est pas respecté.
Args:
action_type: Type de l'action
parameters: Paramètres fournis
Raises:
ContractValidationError si le contrat n'est pas respecté
"""
violations = validate_action_contract(action_type, parameters)
if violations:
print(f"🚫 [Contract] VIOLATION DÉTECTÉE pour '{action_type}':")
for v in violations:
print(f" - {v.parameter}: {v.message}")
raise ContractValidationError(violations, action_type)
print(f"✅ [Contract] Contrat respecté pour '{action_type}'")
def get_action_contract(action_type: str) -> Optional[ActionContract]:
"""Retourne le contrat d'une action."""
return VWB_ACTION_CONTRACTS.get(action_type.lower().strip())
def get_required_params(action_type: str) -> List[str]:
"""Retourne la liste des paramètres obligatoires pour une action."""
contract = get_action_contract(action_type)
return contract.required_params if contract else []
def list_all_action_types() -> List[str]:
"""Retourne la liste de tous les types d'actions avec contrat."""
return list(VWB_ACTION_CONTRACTS.keys())

View File

@@ -0,0 +1,291 @@
"""
Contrat de Données VWB - Gestion des Erreurs d'Actions
Auteur : Dom, Alice, Kiro - 09 janvier 2026
Ce module définit les contrats pour la gestion des erreurs dans les actions
VisionOnly du Visual Workflow Builder.
Classes :
- VWBErrorType : Types d'erreurs possibles
- VWBErrorSeverity : Niveaux de gravité
- VWBActionError : Contrat principal pour les erreurs d'actions
"""
from enum import Enum
from dataclasses import dataclass, asdict
from typing import Dict, Any, Optional, List
from datetime import datetime
import json
class VWBErrorType(Enum):
"""Types d'erreurs possibles dans les actions VWB."""
# Erreurs de capture d'écran
SCREEN_CAPTURE_FAILED = "screen_capture_failed"
SCREEN_CAPTURE_TIMEOUT = "screen_capture_timeout"
# Erreurs de détection d'éléments
ELEMENT_NOT_FOUND = "element_not_found"
ELEMENT_NOT_VISIBLE = "element_not_visible"
ELEMENT_NOT_CLICKABLE = "element_not_clickable"
MULTIPLE_ELEMENTS_FOUND = "multiple_elements_found"
# Erreurs d'interaction
CLICK_FAILED = "click_failed"
TYPE_TEXT_FAILED = "type_text_failed"
WAIT_TIMEOUT = "wait_timeout"
# Erreurs de validation
VALIDATION_FAILED = "validation_failed"
PARAMETER_INVALID = "parameter_invalid"
ANCHOR_INVALID = "anchor_invalid"
# Erreurs système
SYSTEM_ERROR = "system_error"
NETWORK_ERROR = "network_error"
PERMISSION_DENIED = "permission_denied"
# Erreurs de configuration
CONFIG_ERROR = "config_error"
DEPENDENCY_MISSING = "dependency_missing"
class VWBErrorSeverity(Enum):
"""Niveaux de gravité des erreurs VWB."""
INFO = "info" # Information, pas d'impact
WARNING = "warning" # Avertissement, impact mineur
ERROR = "error" # Erreur, impact modéré
CRITICAL = "critical" # Erreur critique, arrêt nécessaire
FATAL = "fatal" # Erreur fatale, système compromis
@dataclass
class VWBActionError:
"""
Contrat de données pour les erreurs d'actions VWB.
Cette classe encapsule toutes les informations nécessaires pour
diagnostiquer et résoudre les erreurs survenant lors de l'exécution
des actions VisionOnly dans le Visual Workflow Builder.
"""
# Identification de l'erreur
error_id: str
error_type: VWBErrorType
severity: VWBErrorSeverity
# Message et description
message: str
description: str
# Contexte d'exécution
action_id: str
step_id: str
# Informations temporelles
timestamp: datetime
# Détails techniques
technical_details: Dict[str, Any]
# Suggestions de résolution
suggestions: List[str]
# Paramètres optionnels avec valeurs par défaut
workflow_id: Optional[str] = None
execution_time_ms: Optional[float] = None
stack_trace: Optional[str] = None
retry_possible: bool = True
user_id: Optional[str] = None
session_id: Optional[str] = None
environment: str = "development"
def __post_init__(self):
"""Validation et initialisation post-création."""
if not self.error_id:
self.error_id = f"err_{self.action_id}_{int(self.timestamp.timestamp())}"
if not self.technical_details:
self.technical_details = {}
if not self.suggestions:
self.suggestions = self._generate_default_suggestions()
def _generate_default_suggestions(self) -> List[str]:
"""Génère des suggestions par défaut selon le type d'erreur."""
suggestions_map = {
VWBErrorType.SCREEN_CAPTURE_FAILED: [
"Vérifiez que l'écran est accessible",
"Redémarrez le service de capture d'écran",
"Vérifiez les permissions d'accès à l'écran"
],
VWBErrorType.ELEMENT_NOT_FOUND: [
"Vérifiez que l'élément est visible à l'écran",
"Ajustez les paramètres de détection",
"Attendez que la page soit complètement chargée"
],
VWBErrorType.CLICK_FAILED: [
"Vérifiez que l'élément est cliquable",
"Attendez que l'interface soit stable",
"Réessayez avec un délai plus long"
],
VWBErrorType.VALIDATION_FAILED: [
"Vérifiez les paramètres de l'action",
"Consultez la documentation de l'action",
"Contactez l'administrateur si le problème persiste"
]
}
return suggestions_map.get(self.error_type, [
"Consultez les logs pour plus de détails",
"Réessayez l'opération",
"Contactez le support technique si nécessaire"
])
def to_dict(self) -> Dict[str, Any]:
"""Convertit l'erreur en dictionnaire pour sérialisation JSON."""
data = asdict(self)
# Convertir les enums en strings
data['error_type'] = self.error_type.value
data['severity'] = self.severity.value
# Convertir le timestamp en ISO string
data['timestamp'] = self.timestamp.isoformat()
return data
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'VWBActionError':
"""Crée une instance depuis un dictionnaire."""
# Convertir les strings en enums
data['error_type'] = VWBErrorType(data['error_type'])
data['severity'] = VWBErrorSeverity(data['severity'])
# Convertir le timestamp
if isinstance(data['timestamp'], str):
data['timestamp'] = datetime.fromisoformat(data['timestamp'])
return cls(**data)
def to_json(self) -> str:
"""Sérialise l'erreur en JSON."""
return json.dumps(self.to_dict(), ensure_ascii=False, indent=2)
@classmethod
def from_json(cls, json_str: str) -> 'VWBActionError':
"""Désérialise depuis JSON."""
data = json.loads(json_str)
return cls.from_dict(data)
def is_retryable(self) -> bool:
"""Détermine si l'erreur permet un retry."""
non_retryable_types = {
VWBErrorType.PARAMETER_INVALID,
VWBErrorType.ANCHOR_INVALID,
VWBErrorType.PERMISSION_DENIED,
VWBErrorType.CONFIG_ERROR,
VWBErrorType.DEPENDENCY_MISSING
}
return (
self.retry_possible and
self.error_type not in non_retryable_types and
self.severity not in {VWBErrorSeverity.FATAL}
)
def get_user_friendly_message(self) -> str:
"""Retourne un message convivial pour l'utilisateur."""
friendly_messages = {
VWBErrorType.SCREEN_CAPTURE_FAILED: "Impossible de capturer l'écran",
VWBErrorType.ELEMENT_NOT_FOUND: "Élément non trouvé sur l'écran",
VWBErrorType.CLICK_FAILED: "Impossible de cliquer sur l'élément",
VWBErrorType.TYPE_TEXT_FAILED: "Impossible de saisir le texte",
VWBErrorType.WAIT_TIMEOUT: "Délai d'attente dépassé",
VWBErrorType.VALIDATION_FAILED: "Validation échouée",
VWBErrorType.SYSTEM_ERROR: "Erreur système",
VWBErrorType.NETWORK_ERROR: "Erreur réseau",
VWBErrorType.PERMISSION_DENIED: "Permissions insuffisantes"
}
base_message = friendly_messages.get(self.error_type, self.message)
if self.severity == VWBErrorSeverity.CRITICAL:
return f"🚨 {base_message}"
elif self.severity == VWBErrorSeverity.ERROR:
return f"{base_message}"
elif self.severity == VWBErrorSeverity.WARNING:
return f"⚠️ {base_message}"
else:
return f" {base_message}"
def add_technical_detail(self, key: str, value: Any) -> None:
"""Ajoute un détail technique."""
self.technical_details[key] = value
def add_suggestion(self, suggestion: str) -> None:
"""Ajoute une suggestion de résolution."""
if suggestion not in self.suggestions:
self.suggestions.append(suggestion)
def __str__(self) -> str:
"""Représentation string de l'erreur."""
return f"VWBActionError({self.error_type.value}, {self.severity.value}): {self.message}"
def __repr__(self) -> str:
"""Représentation détaillée de l'erreur."""
return (
f"VWBActionError("
f"error_id='{self.error_id}', "
f"error_type={self.error_type.value}, "
f"severity={self.severity.value}, "
f"action_id='{self.action_id}', "
f"message='{self.message}'"
f")"
)
def create_vwb_error(
error_type: VWBErrorType,
message: str,
action_id: str,
step_id: str,
severity: VWBErrorSeverity = VWBErrorSeverity.ERROR,
**kwargs
) -> VWBActionError:
"""
Fonction utilitaire pour créer rapidement une erreur VWB.
Args:
error_type: Type d'erreur
message: Message d'erreur
action_id: ID de l'action
step_id: ID de l'étape
severity: Gravité (ERROR par défaut)
**kwargs: Paramètres additionnels
Returns:
Instance de VWBActionError
"""
return VWBActionError(
error_id=kwargs.get('error_id', ''),
error_type=error_type,
severity=severity,
message=message,
description=kwargs.get('description', message),
action_id=action_id,
step_id=step_id,
workflow_id=kwargs.get('workflow_id'),
timestamp=kwargs.get('timestamp', datetime.now()),
execution_time_ms=kwargs.get('execution_time_ms'),
technical_details=kwargs.get('technical_details', {}),
stack_trace=kwargs.get('stack_trace'),
suggestions=kwargs.get('suggestions', []),
retry_possible=kwargs.get('retry_possible', True),
user_id=kwargs.get('user_id'),
session_id=kwargs.get('session_id'),
environment=kwargs.get('environment', 'development')
)

View File

@@ -0,0 +1,375 @@
"""
Contrat de Données VWB - Evidence d'Exécution
Auteur : Dom, Alice, Kiro - 09 janvier 2026
Ce module définit les contrats pour les preuves d'exécution (Evidence) des actions
VisionOnly dans le Visual Workflow Builder.
Classes :
- VWBEvidenceType : Types d'evidence possibles
- VWBEvidence : Contrat principal pour les preuves d'exécution
"""
from enum import Enum
from dataclasses import dataclass, asdict
from typing import Dict, Any, Optional, List
from datetime import datetime
import json
import base64
from pathlib import Path
class VWBEvidenceType(Enum):
"""Types d'evidence possibles dans le VWB."""
# Evidence visuelles
SCREENSHOT_BEFORE = "screenshot_before" # Capture avant action
SCREENSHOT_AFTER = "screenshot_after" # Capture après action
SCREENSHOT_ERROR = "screenshot_error" # Capture lors d'erreur
ELEMENT_HIGHLIGHT = "element_highlight" # Élément surligné
# Evidence d'interaction
CLICK_EVIDENCE = "click_evidence" # Preuve de clic
TYPE_EVIDENCE = "type_evidence" # Preuve de saisie
WAIT_EVIDENCE = "wait_evidence" # Preuve d'attente
# Evidence de validation
VALIDATION_SUCCESS = "validation_success" # Validation réussie
VALIDATION_FAILURE = "validation_failure" # Validation échouée
# Evidence système
SYSTEM_STATE = "system_state" # État système
PERFORMANCE_METRICS = "performance_metrics" # Métriques de performance
# Evidence de débogage
DEBUG_INFO = "debug_info" # Informations de débogage
LOG_ENTRY = "log_entry" # Entrée de log
@dataclass
class VWBEvidence:
"""
Contrat de données pour les preuves d'exécution VWB.
Cette classe encapsule toutes les informations nécessaires pour
documenter et tracer l'exécution des actions VisionOnly dans le
Visual Workflow Builder.
"""
# Identification de l'evidence
evidence_id: str
evidence_type: VWBEvidenceType
# Contexte d'exécution
action_id: str
step_id: str
# Informations temporelles
timestamp: datetime
# Contenu de l'evidence
title: str
description: str
# Données structurées
data: Dict[str, Any]
# Liens vers autres evidence
related_evidence_ids: List[str]
# Paramètres optionnels avec valeurs par défaut
workflow_id: Optional[str] = None
execution_time_ms: Optional[float] = None
screenshot_base64: Optional[str] = None
screenshot_width: Optional[int] = None
screenshot_height: Optional[int] = None
highlight_box: Optional[Dict[str, int]] = None # {x, y, width, height}
success: bool = True
confidence_score: Optional[float] = None
user_id: Optional[str] = None
session_id: Optional[str] = None
parent_evidence_id: Optional[str] = None
def __post_init__(self):
"""Validation et initialisation post-création."""
if not self.evidence_id:
self.evidence_id = f"ev_{self.action_id}_{int(self.timestamp.timestamp())}"
if not self.data:
self.data = {}
if not self.related_evidence_ids:
self.related_evidence_ids = []
# Validation des dimensions screenshot
if self.screenshot_base64:
if not self.screenshot_width or not self.screenshot_height:
self._extract_screenshot_dimensions()
def _extract_screenshot_dimensions(self):
"""Extrait les dimensions du screenshot depuis les données base64."""
try:
if self.screenshot_base64:
from PIL import Image
import io
# Décoder le base64
image_data = base64.b64decode(self.screenshot_base64)
image = Image.open(io.BytesIO(image_data))
self.screenshot_width = image.width
self.screenshot_height = image.height
except Exception:
# En cas d'erreur, utiliser des valeurs par défaut
self.screenshot_width = 1920
self.screenshot_height = 1080
def to_dict(self) -> Dict[str, Any]:
"""Convertit l'evidence en dictionnaire pour sérialisation JSON."""
data = asdict(self)
# Convertir l'enum en string
data['evidence_type'] = self.evidence_type.value
# Convertir le timestamp en ISO string
data['timestamp'] = self.timestamp.isoformat()
return data
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'VWBEvidence':
"""Crée une instance depuis un dictionnaire."""
# Convertir le string en enum
data['evidence_type'] = VWBEvidenceType(data['evidence_type'])
# Convertir le timestamp
if isinstance(data['timestamp'], str):
data['timestamp'] = datetime.fromisoformat(data['timestamp'])
return cls(**data)
def to_json(self) -> str:
"""Sérialise l'evidence en JSON."""
return json.dumps(self.to_dict(), ensure_ascii=False, indent=2)
@classmethod
def from_json(cls, json_str: str) -> 'VWBEvidence':
"""Désérialise depuis JSON."""
data = json.loads(json_str)
return cls.from_dict(data)
def has_screenshot(self) -> bool:
"""Vérifie si l'evidence contient un screenshot."""
return self.screenshot_base64 is not None and len(self.screenshot_base64) > 0
def has_highlight(self) -> bool:
"""Vérifie si l'evidence contient une zone surlignée."""
return (
self.highlight_box is not None and
all(key in self.highlight_box for key in ['x', 'y', 'width', 'height'])
)
def get_screenshot_data_url(self) -> Optional[str]:
"""Retourne l'URL data du screenshot pour affichage web."""
if not self.has_screenshot():
return None
# Déterminer le format (PNG par défaut)
format_prefix = "data:image/png;base64,"
if self.screenshot_base64.startswith('/9j/'): # JPEG magic bytes en base64
format_prefix = "data:image/jpeg;base64,"
return f"{format_prefix}{self.screenshot_base64}"
def save_screenshot(self, file_path: str) -> bool:
"""Sauvegarde le screenshot sur disque."""
if not self.has_screenshot():
return False
try:
image_data = base64.b64decode(self.screenshot_base64)
with open(file_path, 'wb') as f:
f.write(image_data)
return True
except Exception:
return False
def add_data(self, key: str, value: Any) -> None:
"""Ajoute une donnée à l'evidence."""
self.data[key] = value
def get_data(self, key: str, default: Any = None) -> Any:
"""Récupère une donnée de l'evidence."""
return self.data.get(key, default)
def set_highlight_box(self, x: int, y: int, width: int, height: int) -> None:
"""Définit la zone de surbrillance."""
self.highlight_box = {
'x': max(0, x),
'y': max(0, y),
'width': max(1, width),
'height': max(1, height)
}
def add_related_evidence(self, evidence_id: str) -> None:
"""Ajoute une evidence liée."""
if evidence_id not in self.related_evidence_ids:
self.related_evidence_ids.append(evidence_id)
def get_file_size_mb(self) -> float:
"""Calcule la taille approximative en MB."""
size_bytes = 0
# Taille du screenshot
if self.screenshot_base64:
size_bytes += len(self.screenshot_base64.encode('utf-8'))
# Taille des données JSON
size_bytes += len(json.dumps(self.data).encode('utf-8'))
return size_bytes / (1024 * 1024)
def is_visual_evidence(self) -> bool:
"""Vérifie si c'est une evidence visuelle."""
visual_types = {
VWBEvidenceType.SCREENSHOT_BEFORE,
VWBEvidenceType.SCREENSHOT_AFTER,
VWBEvidenceType.SCREENSHOT_ERROR,
VWBEvidenceType.ELEMENT_HIGHLIGHT
}
return self.evidence_type in visual_types
def is_interaction_evidence(self) -> bool:
"""Vérifie si c'est une evidence d'interaction."""
interaction_types = {
VWBEvidenceType.CLICK_EVIDENCE,
VWBEvidenceType.TYPE_EVIDENCE,
VWBEvidenceType.WAIT_EVIDENCE
}
return self.evidence_type in interaction_types
def get_summary(self) -> Dict[str, Any]:
"""Retourne un résumé de l'evidence pour affichage."""
return {
'evidence_id': self.evidence_id,
'type': self.evidence_type.value,
'title': self.title,
'timestamp': self.timestamp.isoformat(),
'success': self.success,
'has_screenshot': self.has_screenshot(),
'has_highlight': self.has_highlight(),
'file_size_mb': round(self.get_file_size_mb(), 2),
'confidence_score': self.confidence_score
}
def __str__(self) -> str:
"""Représentation string de l'evidence."""
status = "" if self.success else ""
return f"{status} VWBEvidence({self.evidence_type.value}): {self.title}"
def __repr__(self) -> str:
"""Représentation détaillée de l'evidence."""
return (
f"VWBEvidence("
f"evidence_id='{self.evidence_id}', "
f"evidence_type={self.evidence_type.value}, "
f"action_id='{self.action_id}', "
f"success={self.success}, "
f"has_screenshot={self.has_screenshot()}"
f")"
)
def create_screenshot_evidence(
action_id: str,
step_id: str,
screenshot_base64: str,
evidence_type: VWBEvidenceType = VWBEvidenceType.SCREENSHOT_BEFORE,
title: str = "Capture d'écran",
**kwargs
) -> VWBEvidence:
"""
Fonction utilitaire pour créer rapidement une evidence de screenshot.
Args:
action_id: ID de l'action
step_id: ID de l'étape
screenshot_base64: Screenshot en base64
evidence_type: Type d'evidence
title: Titre de l'evidence
**kwargs: Paramètres additionnels
Returns:
Instance de VWBEvidence
"""
return VWBEvidence(
evidence_id=kwargs.get('evidence_id', ''),
evidence_type=evidence_type,
action_id=action_id,
step_id=step_id,
workflow_id=kwargs.get('workflow_id'),
timestamp=kwargs.get('timestamp', datetime.now()),
execution_time_ms=kwargs.get('execution_time_ms'),
title=title,
description=kwargs.get('description', title),
screenshot_base64=screenshot_base64,
screenshot_width=kwargs.get('screenshot_width'),
screenshot_height=kwargs.get('screenshot_height'),
highlight_box=kwargs.get('highlight_box'),
data=kwargs.get('data', {}),
success=kwargs.get('success', True),
confidence_score=kwargs.get('confidence_score'),
user_id=kwargs.get('user_id'),
session_id=kwargs.get('session_id'),
related_evidence_ids=kwargs.get('related_evidence_ids', []),
parent_evidence_id=kwargs.get('parent_evidence_id')
)
def create_interaction_evidence(
action_id: str,
step_id: str,
evidence_type: VWBEvidenceType,
title: str,
interaction_data: Dict[str, Any],
**kwargs
) -> VWBEvidence:
"""
Fonction utilitaire pour créer une evidence d'interaction.
Args:
action_id: ID de l'action
step_id: ID de l'étape
evidence_type: Type d'evidence d'interaction
title: Titre de l'evidence
interaction_data: Données de l'interaction
**kwargs: Paramètres additionnels
Returns:
Instance de VWBEvidence
"""
return VWBEvidence(
evidence_id=kwargs.get('evidence_id', ''),
evidence_type=evidence_type,
action_id=action_id,
step_id=step_id,
workflow_id=kwargs.get('workflow_id'),
timestamp=kwargs.get('timestamp', datetime.now()),
execution_time_ms=kwargs.get('execution_time_ms'),
title=title,
description=kwargs.get('description', title),
screenshot_base64=kwargs.get('screenshot_base64'),
screenshot_width=kwargs.get('screenshot_width'),
screenshot_height=kwargs.get('screenshot_height'),
highlight_box=kwargs.get('highlight_box'),
data=interaction_data,
success=kwargs.get('success', True),
confidence_score=kwargs.get('confidence_score'),
user_id=kwargs.get('user_id'),
session_id=kwargs.get('session_id'),
related_evidence_ids=kwargs.get('related_evidence_ids', []),
parent_evidence_id=kwargs.get('parent_evidence_id')
)

View File

@@ -0,0 +1,474 @@
"""
Contrat de Données VWB - Ancres Visuelles
Auteur : Dom, Alice, Kiro - 09 janvier 2026
Ce module définit les contrats pour les ancres visuelles utilisées dans les actions
VisionOnly du Visual Workflow Builder pour la sélection et l'identification
d'éléments UI.
Classes :
- VWBVisualAnchorType : Types d'ancres visuelles
- VWBVisualAnchor : Contrat principal pour les ancres visuelles
"""
from enum import Enum
from dataclasses import dataclass, asdict
from typing import Dict, Any, Optional, List, Tuple
from datetime import datetime
import json
import base64
import hashlib
class VWBVisualAnchorType(Enum):
"""Types d'ancres visuelles possibles dans le VWB."""
# Type générique (par défaut pour les captures VWB)
GENERIC = "generic" # Type par défaut, utilise template matching
# Ancres basées sur l'image
IMAGE_TEMPLATE = "image_template" # Template d'image exact
IMAGE_FUZZY = "image_fuzzy" # Template d'image avec tolérance
# Ancres basées sur le texte
TEXT_EXACT = "text_exact" # Texte exact
TEXT_PARTIAL = "text_partial" # Texte partiel
TEXT_REGEX = "text_regex" # Expression régulière
# Ancres basées sur la position
COORDINATES = "coordinates" # Coordonnées absolues
RELATIVE_POSITION = "relative_position" # Position relative à un autre élément
# Ancres basées sur les propriétés UI
UI_ELEMENT = "ui_element" # Élément UI spécifique
BUTTON = "button" # Bouton
INPUT_FIELD = "input_field" # Champ de saisie
DROPDOWN = "dropdown" # Liste déroulante
CHECKBOX = "checkbox" # Case à cocher
RADIO_BUTTON = "radio_button" # Bouton radio
# Ancres composites
MULTI_CRITERIA = "multi_criteria" # Plusieurs critères combinés
CONTEXTUAL = "contextual" # Basée sur le contexte
@dataclass
class VWBVisualAnchor:
"""
Contrat de données pour les ancres visuelles VWB.
Cette classe encapsule toutes les informations nécessaires pour
identifier et localiser des éléments UI dans les actions VisionOnly
du Visual Workflow Builder.
"""
# Identification de l'ancre
anchor_id: str
anchor_type: VWBVisualAnchorType
# Métadonnées de base
name: str
description: str
# Critères de recherche
search_criteria: Dict[str, Any]
# Contexte d'utilisation
created_by: str
created_at: datetime
# Paramètres optionnels avec valeurs par défaut
reference_image_base64: Optional[str] = None
reference_width: Optional[int] = None
reference_height: Optional[int] = None
bounding_box: Optional[Dict[str, int]] = None # {x, y, width, height}
confidence_threshold: float = 0.8
max_search_time_ms: int = 5000
retry_count: int = 3
visual_embedding: Optional[List[float]] = None
embedding_model: Optional[str] = None
last_used_at: Optional[datetime] = None
usage_count: int = 0
success_rate: float = 0.0
average_match_time_ms: float = 0.0
screen_resolution: Optional[Tuple[int, int]] = None
application_context: Optional[str] = None
is_active: bool = True
validation_hash: Optional[str] = None
def __post_init__(self):
"""Validation et initialisation post-création."""
if not self.anchor_id:
self.anchor_id = f"anchor_{self.name.lower().replace(' ', '_')}_{int(self.created_at.timestamp())}"
if not self.search_criteria:
self.search_criteria = {}
# Générer le hash de validation
self._update_validation_hash()
# Valider les dimensions de l'image de référence
if self.reference_image_base64:
if not self.reference_width or not self.reference_height:
self._extract_reference_dimensions()
def _extract_reference_dimensions(self):
"""Extrait les dimensions de l'image de référence."""
try:
if self.reference_image_base64:
from PIL import Image
import io
# Décoder le base64
image_data = base64.b64decode(self.reference_image_base64)
image = Image.open(io.BytesIO(image_data))
self.reference_width = image.width
self.reference_height = image.height
except Exception:
# En cas d'erreur, utiliser des valeurs par défaut
self.reference_width = 100
self.reference_height = 50
def _update_validation_hash(self):
"""Met à jour le hash de validation basé sur les critères principaux."""
hash_data = {
'anchor_type': self.anchor_type.value,
'search_criteria': self.search_criteria,
'bounding_box': self.bounding_box,
'confidence_threshold': self.confidence_threshold
}
hash_string = json.dumps(hash_data, sort_keys=True)
self.validation_hash = hashlib.md5(hash_string.encode()).hexdigest()
def to_dict(self) -> Dict[str, Any]:
"""Convertit l'ancre en dictionnaire pour sérialisation JSON."""
data = asdict(self)
# Convertir l'enum en string
data['anchor_type'] = self.anchor_type.value
# Convertir les timestamps en ISO strings
data['created_at'] = self.created_at.isoformat()
if self.last_used_at:
data['last_used_at'] = self.last_used_at.isoformat()
return data
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'VWBVisualAnchor':
"""Crée une instance depuis un dictionnaire.
Gère les données partielles du frontend en fournissant des valeurs par défaut
pour les champs obligatoires manquants.
"""
# Copier pour ne pas modifier l'original
data = data.copy()
# Convertir le string en enum (avec fallback sur GENERIC)
anchor_type_val = data.get('anchor_type', 'generic')
if isinstance(anchor_type_val, str):
try:
data['anchor_type'] = VWBVisualAnchorType(anchor_type_val)
except ValueError:
data['anchor_type'] = VWBVisualAnchorType.GENERIC
elif not isinstance(anchor_type_val, VWBVisualAnchorType):
data['anchor_type'] = VWBVisualAnchorType.GENERIC
# Fournir des valeurs par défaut pour les champs obligatoires manquants
if 'anchor_id' not in data:
data['anchor_id'] = f"anchor_{datetime.now().timestamp()}"
if 'name' not in data:
data['name'] = data.get('description', 'Ancre visuelle')[:50]
if 'description' not in data:
data['description'] = 'Ancre visuelle VWB'
if 'search_criteria' not in data:
data['search_criteria'] = {}
if 'created_by' not in data:
data['created_by'] = 'vwb_frontend'
if 'created_at' not in data:
data['created_at'] = datetime.now()
# Convertir les timestamps string en datetime
if isinstance(data.get('created_at'), str):
try:
data['created_at'] = datetime.fromisoformat(data['created_at'])
except ValueError:
data['created_at'] = datetime.now()
if data.get('last_used_at') and isinstance(data['last_used_at'], str):
try:
data['last_used_at'] = datetime.fromisoformat(data['last_used_at'])
except ValueError:
data['last_used_at'] = None
# Filtrer les clés non reconnues par le dataclass
valid_fields = {
'anchor_id', 'anchor_type', 'name', 'description', 'search_criteria',
'created_by', 'created_at', 'reference_image_base64', 'reference_width',
'reference_height', 'bounding_box', 'confidence_threshold', 'max_search_time_ms',
'retry_count', 'visual_embedding', 'embedding_model', 'last_used_at',
'usage_count', 'success_rate', 'average_match_time_ms', 'screen_resolution',
'application_context', 'is_active', 'validation_hash'
}
filtered_data = {k: v for k, v in data.items() if k in valid_fields}
return cls(**filtered_data)
def to_json(self) -> str:
"""Sérialise l'ancre en JSON."""
return json.dumps(self.to_dict(), ensure_ascii=False, indent=2)
@classmethod
def from_json(cls, json_str: str) -> 'VWBVisualAnchor':
"""Désérialise depuis JSON."""
data = json.loads(json_str)
return cls.from_dict(data)
def has_reference_image(self) -> bool:
"""Vérifie si l'ancre a une image de référence."""
return self.reference_image_base64 is not None and len(self.reference_image_base64) > 0
def has_bounding_box(self) -> bool:
"""Vérifie si l'ancre a une bounding box définie."""
return (
self.bounding_box is not None and
all(key in self.bounding_box for key in ['x', 'y', 'width', 'height'])
)
def has_visual_embedding(self) -> bool:
"""Vérifie si l'ancre a un embedding visuel."""
return self.visual_embedding is not None and len(self.visual_embedding) > 0
def get_reference_data_url(self) -> Optional[str]:
"""Retourne l'URL data de l'image de référence pour affichage web."""
if not self.has_reference_image():
return None
# Déterminer le format (PNG par défaut)
format_prefix = "data:image/png;base64,"
if self.reference_image_base64.startswith('/9j/'): # JPEG magic bytes en base64
format_prefix = "data:image/jpeg;base64,"
return f"{format_prefix}{self.reference_image_base64}"
def update_usage_stats(self, match_time_ms: float, success: bool):
"""Met à jour les statistiques d'utilisation."""
self.usage_count += 1
self.last_used_at = datetime.now()
# Mettre à jour le temps moyen de matching
if self.average_match_time_ms == 0:
self.average_match_time_ms = match_time_ms
else:
# Moyenne mobile
self.average_match_time_ms = (
(self.average_match_time_ms * (self.usage_count - 1) + match_time_ms) /
self.usage_count
)
# Mettre à jour le taux de succès
if success:
success_count = int(self.success_rate * (self.usage_count - 1)) + 1
else:
success_count = int(self.success_rate * (self.usage_count - 1))
self.success_rate = success_count / self.usage_count
def is_reliable(self) -> bool:
"""Détermine si l'ancre est fiable basé sur les statistiques."""
return (
self.usage_count >= 3 and
self.success_rate >= 0.8 and
self.average_match_time_ms <= self.max_search_time_ms
)
def needs_optimization(self) -> bool:
"""Détermine si l'ancre a besoin d'optimisation."""
return (
self.usage_count >= 5 and
(self.success_rate < 0.7 or self.average_match_time_ms > self.max_search_time_ms * 0.8)
)
def add_search_criterion(self, key: str, value: Any):
"""Ajoute un critère de recherche."""
self.search_criteria[key] = value
self._update_validation_hash()
def remove_search_criterion(self, key: str):
"""Supprime un critère de recherche."""
if key in self.search_criteria:
del self.search_criteria[key]
self._update_validation_hash()
def set_bounding_box(self, x: int, y: int, width: int, height: int):
"""Définit la bounding box."""
self.bounding_box = {
'x': max(0, x),
'y': max(0, y),
'width': max(1, width),
'height': max(1, height)
}
self._update_validation_hash()
def set_visual_embedding(self, embedding: List[float], model: str):
"""Définit l'embedding visuel."""
self.visual_embedding = embedding
self.embedding_model = model
def is_compatible_with_resolution(self, width: int, height: int) -> bool:
"""Vérifie si l'ancre est compatible avec une résolution donnée."""
if not self.screen_resolution:
return True # Pas de contrainte de résolution
# Tolérance de 10% sur les dimensions
tolerance = 0.1
ref_width, ref_height = self.screen_resolution
width_ok = abs(width - ref_width) / ref_width <= tolerance
height_ok = abs(height - ref_height) / ref_height <= tolerance
return width_ok and height_ok
def get_search_area(self, screen_width: int, screen_height: int) -> Optional[Dict[str, int]]:
"""Calcule la zone de recherche sur l'écran actuel."""
if not self.has_bounding_box():
return None
# Si pas de résolution de référence, utiliser les coordonnées telles quelles
if not self.screen_resolution:
return self.bounding_box.copy()
# Adapter les coordonnées à la résolution actuelle
ref_width, ref_height = self.screen_resolution
scale_x = screen_width / ref_width
scale_y = screen_height / ref_height
return {
'x': int(self.bounding_box['x'] * scale_x),
'y': int(self.bounding_box['y'] * scale_y),
'width': int(self.bounding_box['width'] * scale_x),
'height': int(self.bounding_box['height'] * scale_y)
}
def validate_integrity(self) -> bool:
"""Valide l'intégrité de l'ancre."""
current_hash = self.validation_hash
self._update_validation_hash()
return current_hash == self.validation_hash
def get_summary(self) -> Dict[str, Any]:
"""Retourne un résumé de l'ancre pour affichage."""
return {
'anchor_id': self.anchor_id,
'name': self.name,
'type': self.anchor_type.value,
'confidence_threshold': self.confidence_threshold,
'usage_count': self.usage_count,
'success_rate': round(self.success_rate, 2),
'average_match_time_ms': round(self.average_match_time_ms, 1),
'is_reliable': self.is_reliable(),
'needs_optimization': self.needs_optimization(),
'has_reference_image': self.has_reference_image(),
'has_embedding': self.has_visual_embedding(),
'is_active': self.is_active
}
def __str__(self) -> str:
"""Représentation string de l'ancre."""
status = "🟢" if self.is_reliable() else "🟡" if self.is_active else "🔴"
return f"{status} VWBVisualAnchor({self.anchor_type.value}): {self.name}"
def __repr__(self) -> str:
"""Représentation détaillée de l'ancre."""
return (
f"VWBVisualAnchor("
f"anchor_id='{self.anchor_id}', "
f"anchor_type={self.anchor_type.value}, "
f"name='{self.name}', "
f"success_rate={self.success_rate:.2f}, "
f"usage_count={self.usage_count}"
f")"
)
def create_image_anchor(
name: str,
reference_image_base64: str,
created_by: str,
bounding_box: Optional[Dict[str, int]] = None,
confidence_threshold: float = 0.8,
**kwargs
) -> VWBVisualAnchor:
"""
Fonction utilitaire pour créer une ancre basée sur une image.
Args:
name: Nom de l'ancre
reference_image_base64: Image de référence en base64
created_by: Créateur de l'ancre
bounding_box: Zone de capture
confidence_threshold: Seuil de confiance
**kwargs: Paramètres additionnels
Returns:
Instance de VWBVisualAnchor
"""
return VWBVisualAnchor(
anchor_id=kwargs.get('anchor_id', ''),
anchor_type=VWBVisualAnchorType.IMAGE_TEMPLATE,
name=name,
description=kwargs.get('description', f"Ancre image: {name}"),
reference_image_base64=reference_image_base64,
bounding_box=bounding_box,
search_criteria=kwargs.get('search_criteria', {}),
confidence_threshold=confidence_threshold,
max_search_time_ms=kwargs.get('max_search_time_ms', 5000),
retry_count=kwargs.get('retry_count', 3),
visual_embedding=kwargs.get('visual_embedding'),
embedding_model=kwargs.get('embedding_model'),
created_by=created_by,
created_at=kwargs.get('created_at', datetime.now()),
screen_resolution=kwargs.get('screen_resolution'),
application_context=kwargs.get('application_context')
)
def create_text_anchor(
name: str,
text_pattern: str,
created_by: str,
anchor_type: VWBVisualAnchorType = VWBVisualAnchorType.TEXT_EXACT,
**kwargs
) -> VWBVisualAnchor:
"""
Fonction utilitaire pour créer une ancre basée sur du texte.
Args:
name: Nom de l'ancre
text_pattern: Motif de texte à rechercher
created_by: Créateur de l'ancre
anchor_type: Type d'ancre texte
**kwargs: Paramètres additionnels
Returns:
Instance de VWBVisualAnchor
"""
search_criteria = {'text_pattern': text_pattern}
search_criteria.update(kwargs.get('search_criteria', {}))
return VWBVisualAnchor(
anchor_id=kwargs.get('anchor_id', ''),
anchor_type=anchor_type,
name=name,
description=kwargs.get('description', f"Ancre texte: {name}"),
search_criteria=search_criteria,
confidence_threshold=kwargs.get('confidence_threshold', 0.9),
max_search_time_ms=kwargs.get('max_search_time_ms', 3000),
retry_count=kwargs.get('retry_count', 2),
created_by=created_by,
created_at=kwargs.get('created_at', datetime.now()),
application_context=kwargs.get('application_context')
)