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

@@ -0,0 +1,214 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RPA Vision V3 - Extractions</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; }
.header { background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); color: white; padding: 20px 30px; display: flex; justify-content: space-between; align-items: center; }
.header h1 { font-size: 24px; display: flex; align-items: center; gap: 10px; }
.header .nav-links { display: flex; gap: 15px; align-items: center; }
.header .nav-links a { color: rgba(255,255,255,0.8); text-decoration: none; font-size: 14px; padding: 8px 16px; border-radius: 8px; transition: all 0.2s; }
.header .nav-links a:hover { background: rgba(255,255,255,0.15); color: white; }
.header .nav-links a.active { background: rgba(255,255,255,0.2); color: white; font-weight: 600; }
.container { max-width: 1600px; margin: 0 auto; padding: 20px; }
.page-intro { background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); border-radius: 12px; padding: 25px; border: 1px solid #334155; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px; }
.page-intro div:first-child h2 { color: #e2e8f0; margin-bottom: 8px; font-size: 20px; }
.page-intro div:first-child p { color: #64748b; font-size: 14px; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 20px; }
.grid-2 { grid-template-columns: 1fr 1fr; }
.grid-4 { grid-template-columns: repeat(4, 1fr); }
.card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; }
.card h2 { font-size: 16px; margin-bottom: 15px; color: #94a3b8; display: flex; align-items: center; gap: 8px; }
.card h2 .icon { font-size: 20px; }
.stat-card { text-align: center; }
.stat-value { font-size: 36px; font-weight: bold; color: #3b82f6; }
.stat-label { font-size: 12px; color: #64748b; margin-top: 5px; text-transform: uppercase; }
.btn { padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; }
.btn-primary { background: #3b82f6; color: white; }
.btn-primary:hover { background: #2563eb; }
.btn-success { background: #22c55e; color: white; }
.btn-success:hover { background: #16a34a; }
.btn-warning { background: #f59e0b; color: white; }
.btn-warning:hover { background: #d97706; }
.btn-small { padding: 8px 16px; font-size: 12px; }
.extraction-list { display: flex; flex-direction: column; gap: 12px; }
.extraction-item { display: flex; justify-content: space-between; align-items: center; padding: 20px; background: #0f172a; border-radius: 10px; border: 1px solid #334155; transition: all 0.2s; }
.extraction-item:hover { border-color: #3b82f6; }
.extraction-main { display: flex; align-items: center; gap: 15px; flex: 1; }
.extraction-icon { font-size: 32px; }
.extraction-details h4 { color: #e2e8f0; margin-bottom: 5px; }
.extraction-details p { color: #64748b; font-size: 12px; }
.extraction-stats { display: flex; gap: 20px; }
.extraction-stat { text-align: center; }
.extraction-stat .value { font-size: 18px; font-weight: bold; color: #3b82f6; }
.extraction-stat .label { font-size: 10px; color: #64748b; text-transform: uppercase; }
.extraction-actions { display: flex; gap: 8px; margin-left: 20px; }
.unavailable-msg { text-align: center; padding: 60px 20px; color: #64748b; }
.unavailable-msg .msg-icon { font-size: 48px; margin-bottom: 15px; }
.unavailable-msg h3 { color: #94a3b8; margin-bottom: 10px; }
.unavailable-msg p { font-size: 14px; max-width: 500px; margin: 0 auto; }
.empty-state { text-align: center; padding: 60px 20px; }
.empty-state .empty-icon { font-size: 64px; margin-bottom: 15px; opacity: 0.5; }
.empty-state h3 { color: #94a3b8; margin-bottom: 10px; font-size: 18px; }
.empty-state p { color: #64748b; font-size: 14px; max-width: 400px; margin: 0 auto; }
.status-badge { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; }
.status-badge.completed { background: #052e16; color: #22c55e; }
.status-badge.running { background: #172554; color: #3b82f6; }
.status-badge.failed { background: #450a0a; color: #ef4444; }
@media (max-width: 768px) {
.grid-4 { grid-template-columns: 1fr 1fr; }
.grid-2 { grid-template-columns: 1fr; }
.header { flex-direction: column; gap: 15px; }
.extraction-item { flex-direction: column; gap: 15px; align-items: flex-start; }
}
</style>
</head>
<body>
<div class="header">
<h1>RPA Vision V3 - Extractions</h1>
<div class="nav-links">
<a href="/">Dashboard</a>
<a href="/gestures">Gestes</a>
<a href="/streaming">Streaming</a>
<a href="/extractions" class="active">Extractions</a>
</div>
</div>
<div class="container">
<div class="page-intro">
<div>
<h2>Extractions de donnees</h2>
<p>Visualisation des donnees extraites par le moteur RPA depuis les ecrans des applications (scraping visuel).</p>
</div>
<div>
<button class="btn btn-primary" onclick="refreshExtractions()">Actualiser</button>
</div>
</div>
{% if not available %}
<div class="card">
<div class="unavailable-msg">
<div class="msg-icon">&#9888;&#65039;</div>
<h3>Module non disponible</h3>
<p>Le module <code>core.extraction</code> n'est pas encore installe. Cette fonctionnalite sera disponible dans une prochaine version.</p>
</div>
</div>
{% else %}
<!-- Stats -->
<div class="grid grid-4">
<div class="card stat-card">
<div class="stat-value" id="statTotalExtractions">{{ stats.total }}</div>
<div class="stat-label">Extractions</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="statTotalRecords">{{ stats.total_records }}</div>
<div class="stat-label">Enregistrements</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="statSchemas">{{ stats.schemas }}</div>
<div class="stat-label">Schemas</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="statLastExtraction">{{ stats.last_extraction or '-' }}</div>
<div class="stat-label">Derniere extraction</div>
</div>
</div>
<!-- Liste des extractions -->
<div class="card">
<h2><span class="icon">&#128203;</span> Extractions passees</h2>
<div class="extraction-list" id="extractionList">
{% if extractions %}
{% for ext in extractions %}
<div class="extraction-item">
<div class="extraction-main">
<div class="extraction-icon">&#128196;</div>
<div class="extraction-details">
<h4>{{ ext.schema_name or ext.name or 'Extraction' }}</h4>
<p>{{ ext.description or '' }} | {{ ext.date or '' }}</p>
</div>
</div>
<div class="extraction-stats">
<div class="extraction-stat">
<div class="value">{{ ext.records_count or 0 }}</div>
<div class="label">Enregistrements</div>
</div>
<div class="extraction-stat">
<div class="value">{{ ext.fields_count or 0 }}</div>
<div class="label">Champs</div>
</div>
</div>
<div class="extraction-actions">
<span class="status-badge {{ ext.status or 'completed' }}">{{ ext.status or 'completed' }}</span>
{% if ext.id %}
<button class="btn btn-success btn-small" onclick="exportCSV('{{ ext.id }}')">Export CSV</button>
{% endif %}
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<div class="empty-icon">&#128269;</div>
<h3>Aucune extraction</h3>
<p>Les extractions apparaitront ici lorsque le moteur RPA aura extrait des donnees depuis les ecrans des applications.</p>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
<script>
async function fetchJSON(url) {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
async function refreshExtractions() {
try {
const data = await fetchJSON('/api/extractions');
// Recharger la page pour afficher les nouvelles donnees
// (plus simple que de mettre a jour le DOM Jinja)
window.location.reload();
} catch (e) {
console.error('Erreur refresh:', e);
}
}
async function exportCSV(extractionId) {
try {
const response = await fetch(`/api/extractions/${extractionId}/export?format=csv`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `extraction_${extractionId}.csv`;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
} catch (e) {
alert('Erreur export: ' + e.message);
}
}
</script>
</body>
</html>