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

@@ -617,11 +617,8 @@
</div>
<div class="header-right">
<div class="mode-toggle">
<button class="mode-btn active" onclick="setMode('workflow')" id="modeWorkflow">
📋 Workflows
</button>
<button class="mode-btn" onclick="setMode('agent')" id="modeAgent">
🚀 Agent Libre
<button class="mode-btn active" id="modeWorkflow">
💬 Assistant
</button>
</div>
<div class="status-pill" id="statusPill">
@@ -715,6 +712,23 @@
updateAgentProgress(data);
});
// Copilot events
socket.on('copilot_step', (data) => {
showCopilotStep(data);
});
socket.on('copilot_step_result', (data) => {
updateCopilotStepResult(data);
});
socket.on('copilot_complete', (data) => {
completeCopilot(data);
});
socket.on('copilot_error', (data) => {
addMessage(`Copilot: ${data.message}`);
});
// =====================================================
// UI Functions
// =====================================================
@@ -853,40 +867,6 @@
return card;
}
function createAgentPlanCard(plan) {
const card = document.createElement('div');
card.className = 'action-card';
const stepsHtml = plan.steps.map((step, i) => `
<div class="progress-step pending" id="step-${i}">
<div class="progress-step-icon">${i + 1}</div>
<span>${step.description}</span>
</div>
`).join('');
card.innerHTML = `
<div class="action-card-header">
<div class="action-card-title">
🚀 Plan d'exécution
<span class="confidence-badge">${plan.steps.length} étapes</span>
</div>
</div>
<div class="progress-steps" style="margin-bottom: 12px;">
${stepsHtml}
</div>
<div class="action-buttons">
<button class="btn btn-primary" onclick="executeAgentPlan()">
<i class="bi bi-play-fill"></i> Exécuter
</button>
<button class="btn btn-danger" onclick="cancelAction()">
<i class="bi bi-x"></i> Annuler
</button>
</div>
`;
return card;
}
function createExecutionProgress() {
const progress = document.createElement('div');
progress.className = 'execution-progress';
@@ -1033,11 +1013,7 @@
addTypingIndicator();
try {
if (currentMode === 'agent') {
await sendAgentRequest(message);
} else {
await sendChatRequest(message);
}
await sendChatRequest(message);
} catch (error) {
removeTypingIndicator();
addMessage(`❌ Erreur: ${error.message}`);
@@ -1073,9 +1049,35 @@
data.intent?.confidence || 0.9
);
addMessage(data.response.message, 'bot', card);
} else if (data.result?.gesture) {
// Geste primitif exécuté
addMessage(data.response.message);
} else if (data.result?.mode === 'copilot') {
// Mode copilot — les étapes arrivent via WebSocket
addMessage(data.response.message);
} else if (data.result?.success) {
const progress = createExecutionProgress();
addMessage(data.response.message, 'bot', progress);
} else if (data.result?.teach_me) {
// Workflow non trouvé — proposer l'apprentissage
const teachCard = document.createElement('div');
teachCard.className = 'action-card';
teachCard.innerHTML = `
<div class="action-card-header">
<div class="action-card-title">
Apprentissage disponible
</div>
</div>
<p style="margin: 8px 0; opacity: 0.8; font-size: 0.9em;">
Lancez l'enregistrement sur votre PC et montrez-moi comment faire.
</p>
<div class="action-buttons">
<button class="btn btn-primary" onclick="window.open('/api/help', '_blank')">
<i class="bi bi-mortarboard"></i> Comment m'apprendre ?
</button>
</div>
`;
addMessage(data.response.message, 'bot', teachCard);
} else if (data.result?.workflows) {
let msg = data.response.message + '\n\n';
data.result.workflows.slice(0, 5).forEach(w => {
@@ -1087,30 +1089,6 @@
}
}
async function sendAgentRequest(message) {
const response = await fetch('/api/agent/plan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ request: message })
});
const data = await response.json();
removeTypingIndicator();
if (data.error) {
addMessage(`${data.error}`);
return;
}
if (data.plan) {
pendingConfirmation = data.plan;
const card = createAgentPlanCard(data.plan);
addMessage(`J'ai préparé un plan pour "${message}":`, 'bot', card);
} else {
addMessage(data.message || "Je n'ai pas pu créer de plan pour cette demande.");
}
}
async function confirmAction() {
if (!pendingConfirmation) return;
@@ -1127,40 +1105,11 @@
// Show execution progress
const progress = createExecutionProgress();
addMessage("Exécution en cours...", 'bot', progress);
addMessage("Execution en cours...", 'bot', progress);
pendingConfirmation = null;
}
async function executeAgentPlan() {
if (!pendingConfirmation) return;
isProcessing = true;
updateInputState();
addMessage("⏳ Exécution du plan en cours...", 'bot');
const response = await fetch('/api/agent/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ plan: pendingConfirmation })
});
const data = await response.json();
if (data.success) {
const results = data.results || [];
const successCount = results.filter(r => r.success).length;
addMessage(`✅ Plan exécuté: ${successCount}/${results.length} étapes réussies`);
} else {
addMessage(`❌ Erreur: ${data.error}`);
}
pendingConfirmation = null;
isProcessing = false;
updateInputState();
}
function modifyAction() {
if (!pendingConfirmation) return;
addMessage("✏️ Modification non implémentée. Décrivez les changements souhaités.");
@@ -1173,7 +1122,79 @@
function cancelExecution() {
socket.emit('cancel_execution');
addMessage("⏹️ Demande d'annulation envoyée...");
addMessage("Demande d'annulation envoyée...");
}
// =====================================================
// Copilot Mode
// =====================================================
function showCopilotStep(data) {
const card = document.createElement('div');
card.className = 'action-card';
card.id = `copilot-step-${data.step_index}`;
card.innerHTML = `
<div class="action-card-header">
<div class="action-card-title">
Copilot - Étape ${data.step_index + 1}/${data.total}
</div>
<span style="font-size: 0.8em; opacity: 0.6;">${data.workflow}</span>
</div>
<p style="margin: 8px 0; font-size: 0.95em;">
<strong>${data.action.type}</strong>: ${data.action.description}
</p>
<div class="action-buttons" id="copilot-btns-${data.step_index}">
<button class="btn btn-primary" onclick="copilotApprove(${data.step_index})">
<i class="bi bi-check-lg"></i> Exécuter
</button>
<button class="btn btn-secondary" onclick="copilotSkip(${data.step_index})">
<i class="bi bi-skip-forward"></i> Passer
</button>
<button class="btn btn-danger" onclick="copilotAbort()">
<i class="bi bi-x-circle"></i> Annuler tout
</button>
</div>
`;
addMessage(`Copilot étape ${data.step_index + 1}/${data.total}`, 'bot', card);
}
function copilotApprove(stepIndex) {
socket.emit('copilot_approve');
const btns = document.getElementById(`copilot-btns-${stepIndex}`);
if (btns) btns.innerHTML = '<span style="color: var(--success);">Approuvé - en cours...</span>';
}
function copilotSkip(stepIndex) {
socket.emit('copilot_skip');
const btns = document.getElementById(`copilot-btns-${stepIndex}`);
if (btns) btns.innerHTML = '<span style="color: var(--warning);">Passé</span>';
}
function copilotAbort() {
socket.emit('copilot_abort');
}
function updateCopilotStepResult(data) {
const card = document.getElementById(`copilot-step-${data.step_index}`);
if (!card) return;
const btns = card.querySelector('.action-buttons') ||
document.getElementById(`copilot-btns-${data.step_index}`);
if (!btns) return;
if (data.status === 'completed') {
btns.innerHTML = '<span style="color: var(--success);">Réussi</span>';
} else if (data.status === 'failed') {
btns.innerHTML = `<span style="color: var(--error);">Échoué: ${data.message}</span>`;
} else if (data.status === 'skipped') {
btns.innerHTML = '<span style="color: var(--warning);">Passé</span>';
}
}
function completeCopilot(data) {
const statusColor = data.status === 'completed' ? 'var(--success)' :
data.status === 'aborted' ? 'var(--error)' : 'var(--warning)';
addMessage(`<span style="color: ${statusColor};">Copilot terminé: ${data.message}</span>`);
}
// =====================================================