feat(agent): add learn action flow and grounding guards

This commit is contained in:
Dom
2026-06-02 16:24:10 +02:00
parent 86b3c8f7e7
commit d38f0b0f2f
39 changed files with 5901 additions and 212 deletions

View File

@@ -0,0 +1,147 @@
"""
Client HTTP minimal pour l'orchestrateur Léa-first (agent-chat Linux).
Rebranchement P1-LEA-SHADOW : le bouton "Apprenez-moi" côté Windows déclenche
la création d'une session d'apprentissage côté agent-chat (REST) AVANT de
lancer la capture locale. Le pipeline streaming (capture frames/événements
via start_recording) n'est PAS modifié — seule la prise de contact initiale
avec Léa change.
Contrat :
POST {AGENT_CHAT_URL}/api/learn/start
Headers : Authorization: Bearer <RPA_API_TOKEN>, Content-Type: application/json
Body : { machine_id, session_name, user_id?, trigger_source }
Réponse : { session_id, state, message }
Politique :
- Timeout 10s (connect + read)
- Retry x2 avec backoff 0.5s puis 1.0s
- En cas d'échec définitif : lève LeaOrchestratorError (le caller doit
basculer en mode dégradé : start_recording local sans assistance).
"""
from __future__ import annotations
import logging
import time
from dataclasses import dataclass
from typing import Optional
logger = logging.getLogger(__name__)
# Timeout HTTP (connect + read) — 10s comme spec
_HTTP_TIMEOUT_S = 10.0
# Nombre de tentatives totales (1 + 2 retry)
_MAX_ATTEMPTS = 3
# Backoff progressif entre les tentatives
_BACKOFF_S = (0.5, 1.0)
@dataclass(frozen=True)
class LearnStartResponse:
"""Réponse normalisée de POST /api/learn/start."""
session_id: str
state: str
message: str
class LeaOrchestratorError(RuntimeError):
"""Erreur définitive de communication avec l'orchestrateur Léa."""
def start_learning_session(
base_url: str,
*,
machine_id: str,
session_name: str,
api_token: str = "",
user_id: Optional[str] = None,
trigger_source: str = "windows_button",
timeout_s: float = _HTTP_TIMEOUT_S,
max_attempts: int = _MAX_ATTEMPTS,
backoff_s: tuple = _BACKOFF_S,
) -> LearnStartResponse:
"""Démarre une session d'apprentissage via l'orchestrateur agent-chat.
Args:
base_url: URL racine de l'agent-chat (ex. http://localhost:5004).
machine_id: Identifiant unique du poste Windows.
session_name: Nom humain de la tâche (saisi par l'utilisateur).
api_token: Bearer token (RPA_API_TOKEN). Vide => header omis.
user_id: Identifiant utilisateur optionnel.
trigger_source: Source du déclenchement (windows_button, tray, ...).
timeout_s: Timeout total connect+read par tentative.
max_attempts: Nombre total de tentatives (1 + retry).
backoff_s: Tuple des délais en secondes entre tentatives (len = max_attempts-1).
Returns:
LearnStartResponse normalisée.
Raises:
LeaOrchestratorError: si toutes les tentatives échouent.
"""
# Import local : httpx peut ne pas être installé sur tous les postes
# Windows historiques. On veut un message d'erreur clair plutôt qu'un
# ImportError en chaîne au moment du clic bouton.
try:
import httpx
except ImportError as exc: # pragma: no cover (dépend du venv)
raise LeaOrchestratorError(
"httpx non disponible — installer httpx>=0.27 sur le poste Windows."
) from exc
url = base_url.rstrip("/") + "/api/learn/start"
payload = {
"machine_id": machine_id,
"session_name": session_name,
"trigger_source": trigger_source,
}
if user_id:
payload["user_id"] = user_id
headers = {"Content-Type": "application/json"}
if api_token:
headers["Authorization"] = f"Bearer {api_token}"
last_exc: Optional[Exception] = None
for attempt in range(max_attempts):
try:
logger.info(
"POST %s (tentative %d/%d) machine_id=%s session=%s",
url, attempt + 1, max_attempts, machine_id, session_name,
)
with httpx.Client(timeout=timeout_s) as client:
resp = client.post(url, json=payload, headers=headers)
resp.raise_for_status()
data = resp.json()
session_id = data.get("session_id", "")
state = data.get("state", "")
message = data.get("message", "")
if not session_id:
raise LeaOrchestratorError(
f"Réponse invalide (pas de session_id) : {data!r}"
)
logger.info(
"Session Léa démarrée : session_id=%s state=%s",
session_id, state,
)
return LearnStartResponse(
session_id=str(session_id),
state=str(state),
message=str(message),
)
except Exception as exc: # noqa: BLE001 — on retry sur toute erreur réseau/HTTP
last_exc = exc
logger.warning(
"Echec tentative %d/%d POST %s : %s",
attempt + 1, max_attempts, url, exc,
)
if attempt < max_attempts - 1:
delay = backoff_s[attempt] if attempt < len(backoff_s) else backoff_s[-1]
time.sleep(delay)
raise LeaOrchestratorError(
f"Echec définitif POST {url} après {max_attempts} tentatives : {last_exc}"
)

View File

@@ -63,8 +63,14 @@ JPEG_QUALITY = 85
# Taille max de la queue (backpressure)
QUEUE_MAX_SIZE = 100
# Types d'événements à ne jamais dropper
PRIORITY_EVENT_TYPES = {"click", "key", "scroll", "action", "screenshot"}
# Types d'événements à ne jamais dropper.
# Les noms historiques sont conservés, mais les événements réels du captor
# Agent V1 sont mouse_click/key_combo/text_input/mouse_scroll.
PRIORITY_EVENT_TYPES = {
"click", "key", "scroll", "action", "screenshot",
"mouse_click", "double_click", "key_combo", "key_press",
"text_input", "mouse_scroll",
}
# Purge locale après ACK serveur (Partie A de l'audit)
# Activé par défaut : le serveur conserve déjà les screenshots 180 jours