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

@@ -92,6 +92,41 @@ def get_execution_result():
from .execution_result import WorkflowExecutionResult
return WorkflowExecutionResult
# Lazy import via __getattr__ pour éviter les imports circulaires
_LAZY_IMPORTS = {
"StateEmbedding": "core.models.state_embedding",
"EmbeddingComponent": "core.models.state_embedding",
"Workflow": "core.models.workflow_graph",
"WorkflowNode": "core.models.workflow_graph",
"WorkflowEdge": "core.models.workflow_graph",
"ScreenTemplate": "core.models.workflow_graph",
"Action": "core.models.workflow_graph",
"TargetSpec": "core.models.workflow_graph",
"ActionType": "core.models.workflow_graph",
"EdgeConstraints": "core.models.workflow_graph",
"PostConditions": "core.models.workflow_graph",
"LearningState": "core.models.workflow_graph",
"SelectionPolicy": "core.models.workflow_graph",
"WindowConstraint": "core.models.workflow_graph",
"TextConstraint": "core.models.workflow_graph",
"UIConstraint": "core.models.workflow_graph",
"EmbeddingPrototype": "core.models.workflow_graph",
"EdgeStats": "core.models.workflow_graph",
"SafetyRules": "core.models.workflow_graph",
"WorkflowStats": "core.models.workflow_graph",
"LearningConfig": "core.models.workflow_graph",
"WorkflowExecutionResult": "core.models.execution_result",
"PerformanceMetrics": "core.models.execution_result",
}
def __getattr__(name):
if name in _LAZY_IMPORTS:
import importlib
module = importlib.import_module(_LAZY_IMPORTS[name])
return getattr(module, name)
raise AttributeError(f"module 'core.models' has no attribute {name!r}")
__all__ = [
# Modèles de base standardisés (Tâche 4)
"BBox",

View File

@@ -45,6 +45,25 @@ class BBox(BaseModel):
return int(v)
raise ValueError("Dimensions must be numeric")
def __iter__(self):
"""Permet le unpacking: x, y, w, h = bbox"""
return iter((self.x, self.y, self.width, self.height))
def __getitem__(self, index):
"""Permet l'accès par index: bbox[0], bbox[1], etc."""
return (self.x, self.y, self.width, self.height)[index]
def __len__(self):
return 4
def __eq__(self, other):
if isinstance(other, BBox):
return (self.x == other.x and self.y == other.y and
self.width == other.width and self.height == other.height)
if isinstance(other, (tuple, list)) and len(other) == 4:
return (self.x, self.y, self.width, self.height) == tuple(other)
return NotImplemented
def to_tuple(self) -> Tuple[int, int, int, int]:
"""Conversion vers tuple (x, y, w, h)"""
return (self.x, self.y, self.width, self.height)

View File

@@ -311,8 +311,8 @@ class ScreenTemplate:
# Vérifier contraintes de texte
if hasattr(screen_state, 'perception'):
detected_texts = getattr(screen_state.perception, 'detected_texts', [])
if not self.text.matches(detected_texts):
detected_text = getattr(screen_state.perception, 'detected_text', [])
if not self.text.matches(detected_text):
return False, 0.0
# Vérifier contraintes UI