feat(vwb): Ajouter SeeClick, Self-Healing interactif et Dashboard confiance

## Nouvelles fonctionnalités

### 1. SeeClick Adapter (visual grounding fallback)
- Nouvel adapter pour le modèle SeeClick (HuggingFace)
- Intégré dans la chaîne de fallback: CLIP → Template → SeeClick → Static
- Localise les éléments GUI à partir de descriptions textuelles

### 2. Self-Healing Interactif
- Dialogue qui propose des alternatives quand l'ancre n'est pas trouvée
- L'utilisateur peut choisir: candidat alternatif, coords statiques, ou sauter
- Nouveaux endpoints: /healing/status, /healing/choose, /healing/candidates
- État "waiting_for_choice" pour mettre l'exécution en pause

### 3. Dashboard Confiance (temps réel)
- Affiche les scores de confiance pendant l'exécution
- Montre: méthode utilisée, distance, taux de succès
- Interface pliable en bas à droite
- Visible uniquement en mode intelligent/debug

## Fichiers ajoutés
- core/detection/seeclick_adapter.py
- frontend_v4/src/components/SelfHealingDialog.tsx
- frontend_v4/src/components/ConfidenceDashboard.tsx

## Fichiers modifiés
- core/detection/__init__.py
- backend/services/intelligent_executor.py
- backend/api_v3/execute.py
- frontend_v4/src/App.tsx
- frontend_v4/src/services/api.ts
- docs/VISION_RPA_INTELLIGENT.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Dom
2026-01-24 02:34:01 +01:00
parent f04f156144
commit 21bfa3b337
9 changed files with 1656 additions and 13 deletions

View File

@@ -52,7 +52,13 @@ _execution_state = {
'should_stop': False,
'current_execution_id': None,
'thread': None,
'execution_mode': 'basic' # 'basic', 'intelligent', 'debug'
'execution_mode': 'basic', # 'basic', 'intelligent', 'debug'
# Self-healing interactif
'waiting_for_choice': False,
'pending_action': None, # Action en attente de choix utilisateur
'candidates': [], # Candidats proposés
'user_choice': None, # Choix de l'utilisateur (coordonnées ou 'skip' ou 'static')
'current_step_info': None # Info sur l'étape en cours pour affichage
}
@@ -164,6 +170,85 @@ def execute_workflow_thread(execution_id: str, workflow_id: str, app):
# === EXÉCUTION DE L'ACTION ===
result = execute_action(step.action_type, params)
# === SELF-HEALING INTERACTIF ===
# Si l'action nécessite un choix utilisateur, attendre
if result.get('needs_user_choice'):
print(f"🔄 [Self-Healing] Attente choix utilisateur pour étape {index + 1}")
# Stocker les informations pour le frontend
_execution_state['waiting_for_choice'] = True
_execution_state['pending_action'] = {
'step_id': step.id,
'step_index': index,
'action_type': step.action_type,
'params': params
}
_execution_state['candidates'] = result.get('candidates', [])
_execution_state['current_step_info'] = {
'index': index,
'total': len(steps),
'original_bbox': result.get('original_bbox'),
'error': result.get('error')
}
_execution_state['user_choice'] = None
# Mettre à jour le status de l'exécution
execution.status = 'waiting_user_choice'
db.session.commit()
# Attendre le choix de l'utilisateur
timeout_seconds = 120 # 2 minutes max
waited = 0
while _execution_state['waiting_for_choice'] and waited < timeout_seconds:
if _execution_state['should_stop']:
break
time.sleep(0.5)
waited += 0.5
# Vérifier si on doit arrêter
if _execution_state['should_stop']:
execution.status = 'cancelled'
break
# Traiter le choix de l'utilisateur
user_choice = _execution_state['user_choice']
_execution_state['waiting_for_choice'] = False
_execution_state['pending_action'] = None
_execution_state['candidates'] = []
if user_choice is None:
# Timeout - aucun choix
step_result.status = 'error'
step_result.error_message = "Timeout: aucun choix utilisateur"
execution.failed_steps += 1
execution.status = 'error'
execution.error_message = f"Timeout à l'étape {index + 1}: aucun choix utilisateur"
db.session.commit()
break
elif user_choice == 'skip':
# Utilisateur a choisi de sauter l'étape
step_result.status = 'skipped'
step_result.output = {'skipped_by_user': True}
execution.status = 'running'
db.session.commit()
print(f"⏭️ [Self-Healing] Étape {index + 1} sautée par l'utilisateur")
continue
elif user_choice == 'static':
# Utiliser les coordonnées statiques
result = execute_action_with_static_coords(step.action_type, params)
execution.status = 'running'
elif isinstance(user_choice, dict) and 'x' in user_choice:
# Coordonnées choisies par l'utilisateur
result = execute_action_with_coords(step.action_type, params, user_choice)
execution.status = 'running'
else:
step_result.status = 'error'
step_result.error_message = f"Choix invalide: {user_choice}"
execution.failed_steps += 1
execution.status = 'error'
db.session.commit()
break
step_result.ended_at = datetime.utcnow()
step_result.duration_ms = int((step_result.ended_at - step_result.started_at).total_seconds() * 1000)
@@ -323,6 +408,75 @@ def execute_ai_analyze(params: dict) -> dict:
return {'success': False, 'error': str(e)}
def execute_action_with_coords(action_type: str, params: dict, coords: dict) -> dict:
"""
Exécute une action avec des coordonnées spécifiées par l'utilisateur (self-healing).
"""
import pyautogui
import time
try:
x, y = coords['x'], coords['y']
print(f"🖱️ [Self-Healing] Clic aux coordonnées choisies: ({x}, {y})")
if action_type in ['double_click_anchor']:
pyautogui.doubleClick(x, y)
elif action_type in ['right_click_anchor']:
pyautogui.rightClick(x, y)
else:
pyautogui.click(x, y)
time.sleep(2.0) # Délai après le clic
return {
'success': True,
'output': {
'clicked_at': {'x': x, 'y': y},
'method': 'user_choice',
'self_healed': True
}
}
except Exception as e:
return {'success': False, 'error': str(e)}
def execute_action_with_static_coords(action_type: str, params: dict) -> dict:
"""
Exécute une action avec les coordonnées statiques originales (fallback self-healing).
"""
import pyautogui
import time
try:
anchor = params.get('visual_anchor', {})
bbox = anchor.get('bounding_box', {})
x = bbox.get('x', 0) + bbox.get('width', 0) / 2
y = bbox.get('y', 0) + bbox.get('height', 0) / 2
print(f"🖱️ [Self-Healing] Clic aux coordonnées statiques: ({x}, {y})")
if action_type in ['double_click_anchor']:
pyautogui.doubleClick(x, y)
elif action_type in ['right_click_anchor']:
pyautogui.rightClick(x, y)
else:
pyautogui.click(x, y)
time.sleep(2.0)
return {
'success': True,
'output': {
'clicked_at': {'x': x, 'y': y},
'method': 'static_fallback',
'self_healed': True
}
}
except Exception as e:
return {'success': False, 'error': str(e)}
def execute_action(action_type: str, params: dict) -> dict:
"""
Exécute une action RPA.
@@ -403,11 +557,25 @@ def execute_action(action_type: str, params: dict) -> dict:
}
else:
# En mode intelligent/debug, on refuse d'utiliser les coordonnées statiques
# si l'ancre n'est pas trouvée - cela évite les clics au mauvais endroit
# si l'ancre n'est pas trouvée - MAIS on peut proposer des alternatives
reason = result.get('reason', 'Ancre non trouvée à l\'écran')
confidence = result.get('confidence', 0)
candidates = result.get('candidates', [])
print(f"❌ [Vision] Ancre NON trouvée (confiance: {confidence:.2f})")
print(f" Raison: {reason}")
# Si self-healing interactif activé, proposer des alternatives
if _execution_state.get('execution_mode') == 'intelligent' and candidates:
print(f"🔄 [Self-Healing] {len(candidates)} candidats disponibles - attente choix utilisateur")
return {
'success': False,
'needs_user_choice': True, # Flag pour self-healing
'error': f"Ancre non trouvée - veuillez choisir parmi les alternatives",
'candidates': candidates,
'original_bbox': bbox,
'confidence': confidence
}
return {
'success': False,
'error': f"Ancre non trouvée à l'écran (confiance: {confidence:.2f}). {reason}"
@@ -707,7 +875,11 @@ def get_execution_status():
'is_paused': _execution_state['is_paused'],
'execution_mode': _execution_state.get('execution_mode', 'basic'),
'execution': execution.to_dict() if execution else None,
'session': session.to_dict()
'session': session.to_dict(),
# Self-healing interactif
'waiting_for_choice': _execution_state.get('waiting_for_choice', False),
'candidates': _execution_state.get('candidates', []) if _execution_state.get('waiting_for_choice') else [],
'current_step_info': _execution_state.get('current_step_info') if _execution_state.get('waiting_for_choice') else None
})
@@ -734,3 +906,121 @@ def get_execution_history():
'success': False,
'error': str(e)
}), 500
# ==============================================================================
# SELF-HEALING INTERACTIF - Endpoints
# ==============================================================================
@api_v3_bp.route('/execute/healing/status', methods=['GET'])
def get_healing_status():
"""
Retourne l'état du self-healing interactif.
Si waiting_for_choice est True, retourne les candidats à afficher.
"""
global _execution_state
return jsonify({
'success': True,
'waiting_for_choice': _execution_state.get('waiting_for_choice', False),
'candidates': _execution_state.get('candidates', []),
'current_step_info': _execution_state.get('current_step_info'),
'pending_action': _execution_state.get('pending_action')
})
@api_v3_bp.route('/execute/healing/choose', methods=['POST'])
def submit_healing_choice():
"""
Soumet le choix de l'utilisateur pour le self-healing.
Request:
{
"choice": "skip" | "static" | {"x": 123, "y": 456}
}
- "skip": Sauter cette étape et continuer
- "static": Utiliser les coordonnées statiques originales
- {"x": N, "y": N}: Utiliser ces coordonnées précises
"""
global _execution_state
if not _execution_state.get('waiting_for_choice'):
return jsonify({
'success': False,
'error': "Aucune décision en attente"
}), 400
data = request.get_json() or {}
choice = data.get('choice')
if choice is None:
return jsonify({
'success': False,
'error': "Choix non spécifié"
}), 400
# Valider le choix
valid_choices = ['skip', 'static']
if isinstance(choice, str) and choice not in valid_choices:
return jsonify({
'success': False,
'error': f"Choix invalide. Valeurs acceptées: {valid_choices} ou {{x, y}}"
}), 400
if isinstance(choice, dict):
if 'x' not in choice or 'y' not in choice:
return jsonify({
'success': False,
'error': "Coordonnées invalides. Format attendu: {x: number, y: number}"
}), 400
# Stocker le choix
_execution_state['user_choice'] = choice
_execution_state['waiting_for_choice'] = False
print(f"✅ [Self-Healing] Choix reçu: {choice}")
return jsonify({
'success': True,
'choice': choice
})
@api_v3_bp.route('/execute/healing/candidates', methods=['GET'])
def get_healing_candidates():
"""
Retourne les candidats avec leurs screenshots pour affichage visuel.
Permet à l'utilisateur de voir les alternatives possibles.
"""
global _execution_state
candidates = _execution_state.get('candidates', [])
step_info = _execution_state.get('current_step_info', {})
# Enrichir les candidats avec plus d'informations si disponible
enriched_candidates = []
for i, candidate in enumerate(candidates):
enriched = {
'id': i,
'element_id': candidate.get('element_id'),
'score': candidate.get('score', candidate.get('combined_score', 0)),
'bbox': candidate.get('bbox', {}),
'distance': candidate.get('distance'),
'method': candidate.get('method', 'unknown')
}
# Calculer le centre si bbox disponible
bbox = candidate.get('bbox', {})
if bbox:
enriched['center'] = {
'x': (bbox.get('x1', 0) + bbox.get('x2', 0)) // 2,
'y': (bbox.get('y1', 0) + bbox.get('y2', 0)) // 2
}
enriched_candidates.append(enriched)
return jsonify({
'success': True,
'candidates': enriched_candidates,
'step_info': step_info,
'original_bbox': step_info.get('original_bbox')
})