feat: chat unifié, GestureCatalog, Copilot, Léa UI, extraction données, vérification replay

Refonte majeure du système Agent Chat et ajout de nombreux modules :

- Chat unifié : suppression du dual Workflows/Agent Libre, tout passe par /api/chat
  avec résolution en 3 niveaux (workflow → geste → "montre-moi")
- GestureCatalog : 38 raccourcis clavier universels Windows avec matching sémantique,
  substitution automatique dans les replays, et endpoint /api/gestures
- Mode Copilot : exécution pas-à-pas des workflows avec validation humaine via WebSocket
  (approve/skip/abort) avant chaque action
- Léa UI (agent_v0/lea_ui/) : interface PyQt5 pour Windows avec overlay transparent
  pour feedback visuel pendant le replay
- Data Extraction (core/extraction/) : moteur d'extraction visuelle de données
  (OCR + VLM → SQLite), avec schémas YAML et export CSV/Excel
- ReplayVerifier (agent_v0/server_v1/) : vérification post-action par comparaison
  de screenshots, avec logique de retry (max 3)
- IntentParser durci : meilleur fallback regex, type GREETING, patterns améliorés
- Dashboard : nouvelles pages gestures, streaming, extractions
- Tests : 63 tests GestureCatalog, 47 tests extraction, corrections tests existants
- Dépréciation : /api/agent/plan et /api/agent/execute retournent HTTP 410,
  suppression du code hardcodé _plan_to_replay_actions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dom
2026-03-15 10:02:09 +01:00
parent 74a1cb4e03
commit cf495dd82f
93 changed files with 12463 additions and 1080 deletions

View File

@@ -2085,6 +2085,269 @@ def import_config():
return jsonify({"success": False, "error": str(e)}), 500
# =============================================================================
# Catalogue de gestes primitifs (intégré)
# =============================================================================
# Catalogue intégré des gestes primitifs connus par le moteur RPA
_BUILTIN_GESTURE_CATALOG = [
# Fenêtres
{"name": "Fermer la fenêtre", "shortcut": "Alt+F4", "category": "windows", "icon": "",
"description": "Ferme la fenêtre active ou l'application en cours."},
{"name": "Agrandir la fenêtre", "shortcut": "Win+Up", "category": "windows", "icon": "⬆️",
"description": "Agrandit la fenêtre active en plein écran."},
{"name": "Réduire la fenêtre", "shortcut": "Win+Down", "category": "windows", "icon": "⬇️",
"description": "Réduit ou restaure la fenêtre active."},
{"name": "Fenêtre à gauche", "shortcut": "Win+Left", "category": "windows", "icon": "⬅️",
"description": "Ancre la fenêtre active sur la moitié gauche de l'écran."},
{"name": "Fenêtre à droite", "shortcut": "Win+Right", "category": "windows", "icon": "➡️",
"description": "Ancre la fenêtre active sur la moitié droite de l'écran."},
{"name": "Changer de fenêtre", "shortcut": "Alt+Tab", "category": "windows", "icon": "🔄",
"description": "Bascule entre les fenêtres ouvertes."},
{"name": "Bureau", "shortcut": "Win+D", "category": "windows", "icon": "🖥️",
"description": "Affiche le bureau (réduit toutes les fenêtres)."},
# Navigation Chrome
{"name": "Nouvel onglet", "shortcut": "Ctrl+T", "category": "chrome", "icon": "",
"description": "Ouvre un nouvel onglet dans le navigateur."},
{"name": "Fermer l'onglet", "shortcut": "Ctrl+W", "category": "chrome", "icon": "✖️",
"description": "Ferme l'onglet actif du navigateur."},
{"name": "Onglet précédent", "shortcut": "Ctrl+Shift+Tab", "category": "chrome", "icon": "◀️",
"description": "Passe à l'onglet précédent."},
{"name": "Onglet suivant", "shortcut": "Ctrl+Tab", "category": "chrome", "icon": "▶️",
"description": "Passe à l'onglet suivant."},
{"name": "Rouvrir onglet fermé", "shortcut": "Ctrl+Shift+T", "category": "chrome", "icon": "♻️",
"description": "Rouvre le dernier onglet fermé."},
{"name": "Barre d'adresse", "shortcut": "Ctrl+L", "category": "chrome", "icon": "🔗",
"description": "Sélectionne la barre d'adresse."},
{"name": "Rechercher dans la page", "shortcut": "Ctrl+F", "category": "chrome", "icon": "🔍",
"description": "Ouvre la barre de recherche dans la page."},
{"name": "Actualiser la page", "shortcut": "F5", "category": "chrome", "icon": "🔄",
"description": "Recharge la page courante."},
{"name": "Page précédente", "shortcut": "Alt+Left", "category": "chrome", "icon": "",
"description": "Retourne à la page précédente dans l'historique."},
{"name": "Page suivante", "shortcut": "Alt+Right", "category": "chrome", "icon": "",
"description": "Avance à la page suivante dans l'historique."},
# Édition
{"name": "Copier", "shortcut": "Ctrl+C", "category": "edition", "icon": "📋",
"description": "Copie la sélection dans le presse-papiers."},
{"name": "Coller", "shortcut": "Ctrl+V", "category": "edition", "icon": "📌",
"description": "Colle le contenu du presse-papiers."},
{"name": "Couper", "shortcut": "Ctrl+X", "category": "edition", "icon": "✂️",
"description": "Coupe la sélection et la place dans le presse-papiers."},
{"name": "Annuler", "shortcut": "Ctrl+Z", "category": "edition", "icon": "↩️",
"description": "Annule la dernière action."},
{"name": "Rétablir", "shortcut": "Ctrl+Y", "category": "edition", "icon": "↪️",
"description": "Rétablit la dernière action annulée."},
{"name": "Tout sélectionner", "shortcut": "Ctrl+A", "category": "edition", "icon": "📝",
"description": "Sélectionne tout le contenu."},
{"name": "Enregistrer", "shortcut": "Ctrl+S", "category": "edition", "icon": "💾",
"description": "Enregistre le document en cours."},
{"name": "Imprimer", "shortcut": "Ctrl+P", "category": "edition", "icon": "🖨️",
"description": "Ouvre la boîte de dialogue d'impression."},
# Système
{"name": "Menu Démarrer", "shortcut": "Win", "category": "system", "icon": "🪟",
"description": "Ouvre le menu Démarrer de Windows."},
{"name": "Explorateur de fichiers", "shortcut": "Win+E", "category": "system", "icon": "📁",
"description": "Ouvre l'explorateur de fichiers."},
{"name": "Gestionnaire de tâches", "shortcut": "Ctrl+Shift+Esc", "category": "system", "icon": "📊",
"description": "Ouvre le gestionnaire de tâches."},
{"name": "Paramètres", "shortcut": "Win+I", "category": "system", "icon": "⚙️",
"description": "Ouvre les paramètres Windows."},
{"name": "Verrouiller l'écran", "shortcut": "Win+L", "category": "system", "icon": "🔒",
"description": "Verrouille la session utilisateur."},
{"name": "Capture d'écran", "shortcut": "Win+Shift+S", "category": "system", "icon": "📷",
"description": "Lance l'outil de capture d'écran."},
{"name": "Exécuter", "shortcut": "Win+R", "category": "system", "icon": "▶️",
"description": "Ouvre la boîte de dialogue Exécuter."},
# Actions souris
{"name": "Clic gauche", "shortcut": "", "category": "mouse", "icon": "🖱️",
"description": "Clic gauche sur un élément de l'interface."},
{"name": "Double clic", "shortcut": "", "category": "mouse", "icon": "🖱️",
"description": "Double clic gauche pour ouvrir ou sélectionner."},
{"name": "Clic droit", "shortcut": "", "category": "mouse", "icon": "🖱️",
"description": "Clic droit pour ouvrir le menu contextuel."},
{"name": "Glisser-déposer", "shortcut": "", "category": "mouse", "icon": "",
"description": "Maintenir le clic et déplacer un élément."},
{"name": "Scroll (molette)", "shortcut": "", "category": "mouse", "icon": "🔃",
"description": "Défilement vertical avec la molette de la souris."},
]
_GESTURE_CATEGORIES = {
"windows": {"id": "windows", "name": "Gestion des fenêtres", "icon": "🪟"},
"chrome": {"id": "chrome", "name": "Navigation Chrome / Web", "icon": "🌐"},
"edition": {"id": "edition", "name": "Édition de texte", "icon": "✏️"},
"system": {"id": "system", "name": "Système", "icon": "⚙️"},
"mouse": {"id": "mouse", "name": "Actions souris", "icon": "🖱️"},
}
def _get_gestures():
"""Récupère les gestes depuis le module agent_chat ou le catalogue intégré."""
catalog_available = False
gestures = []
try:
from agent_chat.gesture_catalog import get_all_gestures
gestures = get_all_gestures()
catalog_available = True
except (ImportError, AttributeError):
gestures = _BUILTIN_GESTURE_CATALOG
catalog_available = False
return gestures, catalog_available
def _build_gesture_categories(gestures):
"""Organise les gestes par catégorie pour le template."""
categories_map = {}
for g in gestures:
cat_id = g.get('category', 'other')
if cat_id not in categories_map:
cat_info = _GESTURE_CATEGORIES.get(cat_id, {
"id": cat_id, "name": cat_id.capitalize(), "icon": "📦"
})
categories_map[cat_id] = {**cat_info, "gestures": []}
categories_map[cat_id]["gestures"].append(g)
# Ordre déterministe
order = ["windows", "chrome", "edition", "system", "mouse"]
result = []
for cat_id in order:
if cat_id in categories_map:
result.append(categories_map.pop(cat_id))
# Catégories restantes
for cat_id in sorted(categories_map.keys()):
result.append(categories_map[cat_id])
return result
@app.route('/gestures')
def gestures_page():
"""Page listant tous les gestes primitifs connus."""
gestures, available = _get_gestures()
categories = _build_gesture_categories(gestures)
with_shortcut = sum(1 for g in gestures if g.get('shortcut'))
stats = {
'total': len(gestures),
'categories': len(categories),
'with_shortcut': with_shortcut,
'source': 'Module' if available else 'Intégré',
}
return render_template('gestures.html',
categories=categories,
stats=stats,
available=available)
@app.route('/api/gestures')
def api_gestures():
"""API JSON des gestes primitifs."""
gestures, available = _get_gestures()
categories = _build_gesture_categories(gestures)
return jsonify({
'gestures': gestures,
'categories': [{'id': c['id'], 'name': c['name'], 'count': len(c['gestures'])}
for c in categories],
'total': len(gestures),
'source': 'agent_chat.gesture_catalog' if available else 'builtin',
'available': available,
})
# =============================================================================
# Pages Streaming et Extractions
# =============================================================================
@app.route('/streaming')
def streaming_page():
"""Page montrant l'état des sessions de streaming."""
return render_template('streaming.html')
@app.route('/api/streaming/status')
def api_streaming_status():
"""Statut en temps réel du streaming server."""
import urllib.request
import urllib.error
streaming_url = 'http://localhost:5005/api/v1/traces/stream/stats'
try:
req = urllib.request.Request(streaming_url, headers={'Accept': 'application/json'})
with urllib.request.urlopen(req, timeout=5) as response:
data = json.loads(response.read().decode())
return jsonify(data)
except urllib.error.URLError:
return jsonify({
'error': 'Serveur streaming inaccessible',
'active_sessions': 0,
'total_events': 0,
'workflows_built': 0,
}), 502
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/extractions')
def extractions_page():
"""Page listant les extractions de données."""
extraction_available = False
extractions = []
stats = {'total': 0, 'total_records': 0, 'schemas': 0, 'last_extraction': None}
try:
from core.extraction import get_extractions
raw = get_extractions()
extractions = raw.get('extractions', [])
stats = raw.get('stats', stats)
extraction_available = True
except (ImportError, AttributeError):
extraction_available = False
return render_template('extractions.html',
extractions=extractions,
stats=stats,
available=extraction_available)
@app.route('/api/extractions')
def api_extractions():
"""API JSON des extractions de données."""
try:
from core.extraction import get_extractions
data = get_extractions()
return jsonify({**data, 'available': True})
except (ImportError, AttributeError):
return jsonify({
'available': False,
'extractions': [],
'message': 'Module core.extraction non disponible',
})
@app.route('/api/extractions/<extraction_id>/export')
def api_extraction_export(extraction_id):
"""Export d'une extraction au format CSV."""
try:
from core.extraction import export_extraction
fmt = request.args.get('format', 'csv')
result = export_extraction(extraction_id, fmt)
if result is None:
return jsonify({'error': 'Extraction non trouvée'}), 404
return send_file(
result['path'],
as_attachment=True,
download_name=result.get('filename', f'extraction_{extraction_id}.csv'),
)
except (ImportError, AttributeError):
return jsonify({
'error': 'Module core.extraction non disponible',
}), 501
except Exception as e:
return jsonify({'error': str(e)}), 500
# =============================================================================
# API Streaming - Proxy vers le serveur de streaming (port 5005)
# =============================================================================