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

@@ -67,7 +67,8 @@ def test_action_executor_click_position():
action = Mock()
action.type = ActionType.MOUSE_CLICK
action.target = Mock()
action.params = None
action.parameters = {}
action.params = {}
# Mock screen state
screen_state = Mock()
@@ -122,18 +123,18 @@ def test_target_resolver_position_matching():
# Position de recherche proche de elem3
search_position = (170, 170)
# Mock screen state avec nos éléments
screen_state = Mock()
screen_state.ui_elements = elements
# Mock context avec spatial_index=None pour forcer le fallback linéaire
mock_context = Mock()
mock_context.workflow_context = {"spatial_index": None}
# Mock _get_ui_elements pour retourner nos éléments
resolver = TargetResolver(position_tolerance=50)
with patch.object(resolver, '_get_ui_elements', return_value=elements):
# Résoudre par position
result = resolver._resolve_by_position(search_position, elements, Mock())
result = resolver._resolve_by_position(search_position, elements, mock_context)
# Devrait trouver elem3 (distance ≈ 14)
assert result is not None
assert result.element.element_id == "elem3"
@@ -142,21 +143,24 @@ def test_target_resolver_position_matching():
def test_target_resolver_proximity_filter():
"""Test que le filtre de proximité utilise les bons calculs de centre"""
# Élément ancre au centre (100, 120) -> centre (100, 120)
anchor = MockUIElement("anchor", (100, 120, 0, 0))
# Éléments à tester
# Élément ancre: bbox (90, 110, 20, 20) -> centre (100, 120)
anchor = MockUIElement("anchor", (90, 110, 20, 20))
# Éléments à tester (distances au centre de l'ancre (100, 120)):
# near: centre (125, 125), distance = sqrt(25² + 5²) ≈ 25.5
# medium: centre (130, 130), distance = sqrt(30² + 10²) ≈ 31.6
# far: centre (205, 205), distance = sqrt(105² + 85²) ≈ 135.1
elements = [
MockUIElement("near", (120, 120, 10, 10)), # centre: (125, 125), distance ≈ 25
MockUIElement("medium", (140, 140, 10, 10)), # centre: (145, 145), distance ≈ 35
MockUIElement("far", (200, 200, 10, 10)), # centre: (205, 205), distance ≈ 120
MockUIElement("near", (120, 120, 10, 10)),
MockUIElement("medium", (125, 125, 10, 10)),
MockUIElement("far", (200, 200, 10, 10)),
]
resolver = TargetResolver()
# Filtrer avec distance max = 50
filtered = resolver._filter_by_proximity(elements, anchor, max_distance=50)
# Seuls "near" et "medium" devraient être dans le résultat
filtered_ids = [elem.element_id for elem in filtered]
assert "near" in filtered_ids