feat(agent): add learn action flow and grounding guards
This commit is contained in:
147
agent_v0/agent_v1/network/lea_orchestrator_client.py
Normal file
147
agent_v0/agent_v1/network/lea_orchestrator_client.py
Normal 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}"
|
||||
)
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user