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:
@@ -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)
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user