15 KiB
Spec technique — Raccord agent-chat (5004) → Shadow (5005)
Ticket: Q-P1-agentchat Date: 2026-06-01
1. État des lieux
| Composant | Fichier | État |
|---|---|---|
| Endpoints Shadow | agent_v0/server_v1/api_stream.py L2415-2650 |
Existent, fonctionnels |
| ShadowObserver | core/workflow/shadow_observer.py |
Existe, get_shared_observer() |
| ShadowValidator | core/workflow/shadow_validator.py |
Existe, apply_feedback + build_workflow_ir |
| Agent-chat intent | agent_chat/intent_parser.py |
"apprends-moi" → IntentType.EXECUTE (L120) |
| Agent-chat caller | agent_chat/app.py |
AUCUN appel Shadow — endpoints orphelins |
| Persist competence | api_stream.py |
N'existe pas — tâche séparée |
2. Contrats d'endpoints Shadow (port 5005)
2.1 POST /api/v1/shadow/start
// Request body
{"session_id": "sess_xxx"}
// Response 200
{"status": "shadow_started", "session_id": "sess_xxx", "message": "..."}
Appelle observer.start(session_id). Aucune autre dépendance.
2.2 POST /api/v1/shadow/stop
// Request body
{"session_id": "sess_xxx"}
// Response 200
{
"status": "shadow_stopped",
"session_id": "sess_xxx",
"steps_count": N,
"understanding": [{"step": 1, "intent": "...", "confidence": 0.x, ...}]
}
Appelle observer.stop(session_id) puis observer.get_understanding().
2.3 GET /api/v1/shadow/{session_id}/understanding
Retourne steps, current_step, notifications (optionnel ?since_ts=...).
2.4 POST /api/v1/shadow/feedback
{
"session_id": "sess_xxx",
"action": "validate|correct|undo|cancel|merge_next|split",
"step_index": 1, // optionnel sauf pour correct/undo/merge/split
"new_intent": "...", // requis si action=correct
"at_event_index": 5 // requis si action=split
}
Appelle validator.apply_feedback(). Retourne {"status": "feedback_applied|feedback_rejected", "result": {...}}.
2.5 POST /api/v1/shadow/build
{"session_id": "sess_xxx", "name": "Nom workflow", "domain": "generic", "require_all_validated": false}
Retourne {"status": "workflow_built", "workflow_ir": {...}} ou {"status": "cancelled"}.
Auth
Tous les endpoints exigent Authorization: Bearer <RPA_API_TOKEN> (sauf si RPA_AUTH_DISABLED=true). Agent-chat utilise déjà _streaming_headers() pour ses appels replay — réutiliser le même mécanisme.
3. Résolution session_id
Problème : le ShadowObserver est indexé par session_id, mais les événements sont streamés par l'agent V1 (sur la machine de Dom) via POST /api/v1/traces/stream/event. Le session_id du Shadow start/stop doit correspondre au session_id utilisé par l'agent V1 pour streamer ses événements.
Mécanisme existant : le StreamProcessor auto-enregistre les sessions à la réception du premier event (_ensure_session_registered). Le session_id est donc déterminé par l'agent V1 côté client.
Solution retenue : agent-chat ne génère pas de session_id. Il utilise la session active de la machine cible.
3.1 Résolution machine_id
Agent-chat appelle GET /api/v1/traces/stream/machines (déjà implémenté dans _fetch_connected_machines(), app.py L193). En mode mono-machine (cas Dom seul), une seule machine est connectée → machine_id = celle de la liste.
3.2 Résolution session_id (flux)
1. Dom: "Léa, observe ce que je fais"
2. Agent-chat → GET /api/v1/traces/stream/machines
→ machine_id = "DESKTOP-XXX" (seule machine connectée)
3. Agent-chat → GET /api/v1/traces/stream/sessions?machine_id=DESKTOP-XXX
→ session_active = sessions non-finalisées [0].session_id
→ Si aucune session active, agent-chat dit "Je ne vois pas de session active,
commence par faire une action d'abord"
4. Agent-chat → POST /api/v1/shadow/start {"session_id": session_active}
Alternative (plus simple) : si l'agent V1 stream déjà des événements (session existe), le ShadowObserver peut auto-start via observe_event (L355 de shadow_observer.py : "Auto-start si pas encore démarré"). Dans ce cas, agent-chat peut appeler /shadow/start avec le session_id résolu pour être explicite, mais ce n'est pas strictement nécessaire — les événements alimenteront l'observer même sans start explicite.
Décision : start explicite requis pour la sémantique ("Léa, observe" = début intentionnel) et pour que observer.stop() puisse finaliser le segment en cours proprement.
4. Flux complet spécifié
┌─────────────────────────────────────────────────────┐
│ Phase 1 : DÉMARRAGE OBSERVATION │
│ │
│ Dom → Agent-chat (5004): "Léa, observe ce que je fais"│
│ ↓ │
│ Agent-chat: │
│ 1. IntentParser.parse() → IntentType.EXECUTE │
│ workflow_hint = "observe ce que je fais" │
│ (pattern "apprends-moi" ou "observe" détecté) │
│ 2. Résoudre session_id: │
│ GET /api/v1/traces/stream/sessions │
│ → session_active = première session non-finalisée│
│ → Si aucune: "Aucune session active détectée" │
│ 3. POST /api/v1/shadow/start │
│ {"session_id": session_active} │
│ headers = _streaming_headers() │
│ 4. Si erreur 404 → session inactive │
│ Si erreur 401 → token manquant │
│ Si connexion error → "Streaming server injoignable"│
│ 5. Réponse Dom: "J'observe. Fais ta tâche." │
│ │
├─────────────────────────────────────────────────────┤
│ Phase 2 : DOM EFFECTUE L'ACTION │
│ │
│ Agent V1 (machine Dom) → Streaming server (5005): │
│ POST /api/v1/traces/stream/event (en continu) │
│ → worker.process_event_direct() │
│ → shadow_observe_event(session_id, event) │
│ → ShadowObserver.observe_event() (incrémental) │
│ │
│ (Aucune action requise d'agent-chat pendant cette │
│ phase — les événements sont streamés directement) │
│ │
├─────────────────────────────────────────────────────┤
│ Phase 3 : ARRÊT OBSERVATION │
│ │
│ Dom → Agent-chat: "Léa, c'est fini" │
│ ↓ │
│ Agent-chat: │
│ 1. IntentParser.parse() → IntentType.EXECUTE │
│ (détecté par verbe "fini" + contexte shadow) │
│ OU par contexte conversation (pending shadow) │
│ 2. POST /api/v1/shadow/stop │
│ {"session_id": session_active} │
│ 3. Retour: {understanding: [...], steps_count: N} │
│ 4. Affiche à Dom les étapes comprises: │
│ "J'ai observé N étapes:" │
│ 1. Ouvrir le Bloc-notes (confiance 0.8) │
│ 2. Saisir du texte (confiance 0.5) │
│ ... │
│ 5. Demande validation: "C'est correct ?" │
│ │
├─────────────────────────────────────────────────────┤
│ Phase 4 : FEEDBACK (N tours) │
│ │
│ Dom → Agent-chat: "L'étape 2, c'est 'Rechercher X'" │
│ ↓ │
│ Agent-chat: │
│ 1. Parse la correction → action=correct │
│ step_index=2, new_intent="Rechercher X" │
│ 2. POST /api/v1/shadow/feedback │
│ {session_id, action:"correct", │
│ step_index:2, new_intent:"Rechercher X"} │
│ 3. Réaffiche les étapes mises à jour │
│ 4. Redemande: "Et là, c'est bon ?" │
│ │
│ Si Dom dit "oui" → action=validate pour chaque étape │
│ Si Dom dit "supprime l'étape 3" → action=undo │
│ Si Dom dit "annule tout" → action=cancel │
│ │
├─────────────────────────────────────────────────────┤
│ Phase 5 : BUILD + PERSIST │
│ │
│ Après validation complète: │
│ 1. POST /api/v1/shadow/build │
│ {session_id, name:"Nom auto ou donné par Dom", │
│ domain:"generic", require_all_validated:false} │
│ 2. Retour: {workflow_ir: {...}} │
│ 3. POST /api/v1/lea/competences/candidate/persist │
│ ← N'EXISTE PAS ENCORE (tâche séparée) │
│ À implémenter : reçoit workflow_ir + machine_id │
│ → crée YAML dans data/competences/candidate/ │
│ 4. Réponse Dom: "Tâche apprise et sauvegardée" │
│ 5. observer.reset(session_id) via call Shadow │
│ (nettoyage état mémoire) │
└─────────────────────────────────────────────────────┘
5. Gestion des intentions agent-chat
Détection existante (intent_parser.py)
| Phrase type | Pattern | IntentType actuel |
|---|---|---|
| "apprends-moi à ..." | `r"(?:apprends | apprenez)[- ]moi\s+(.+)"` |
| "observe ce que je fais" | NON DÉTECTÉ — pas de pattern | UNKNOWN → CLARIFY |
Modifications requises dans agent_chat/intent_parser.py
Ajouter une nouvelle intention IntentType.LEARN (ou SHADOW) dans l'enum, avec les patterns :
IntentType.LEARN = "learn" # Démarrer/arrêter une session d'observation Shadow
Patterns à ajouter dans INTENT_PATTERNS[IntentType.LEARN] :
r"(?:observe|regarde)[ -]?(?:moi|ce que je fais)"r"(?:apprend|apprenez)[- ]moi"r"(?:montre[- ]moi\s+comment|fais\s+comme\s+moi)"r"(?:c'est\s+fini|j'ai\s+fini|stop\s+observation)"(pour le stop, détectable aussi par contexte)
Ou approche contextuelle (recommandée)
Ne pas créer de nouveau IntentType. Utiliser un flag de contexte dans le ConversationManager :
# Quand Dom dit "observe ce que je fais" (EXECUTE, non-match workflow)
if result.get("teach_me") and not matcher:
conversation_manager.set_session_flag(session, "shadow_pending")
# Au prochain message, si flag shadow_pending:
if conversation_manager.has_session_flag(session, "shadow_pending"):
# Dom dit "oui", "c'est parti" → start shadow
# Dom dit autre chose → interpréter comme contexte shadow
Décision recommandée : approche contextuelle. Elle évite de polluer l'enum et permet de gérer le cycle start/stop comme un sous-état de la conversation, pas comme une intention globale.
6. Gestion des erreurs
| Erreur | Endpoint | Réponse agent-chat à Dom |
|---|---|---|
| Streaming server injoignable (ConnectionError) | start/stop/feedback/build | "Je n'arrive pas à joindre le serveur de streaming. Vérifie qu'il tourne (port 5005)." |
| 401 Unauthorized | start/stop/feedback/build | "Problème d'authentification — le token API est invalide." |
| 404 Not Found (session inactive) | start/stop | "Je ne trouve pas de session active. Commence par faire une action pour que je puisse observer." |
| 400 Bad Request (build sans étapes validées) | build | "Impossible de construire le workflow — aucune étape n'a été validée." |
| Timeout (>15s) | tout | "Le serveur met trop de temps à répondre. Réessaie." |
| Session déjà active (double start) | start | No-op côté observer (reset implicite) — informer Dom: "J'observe déjà cette session." |
| Stop sans session active | stop | "Je n'étais pas en mode observation." |
7. Paramètres par appel
| Appel | Méthode | Body | Headers | Timeout |
|---|---|---|---|---|
| Résoudre session | GET | (query params) | _streaming_headers() |
3s |
| Shadow start | POST | {"session_id": "..."} |
_streaming_headers() |
5s |
| Shadow stop | POST | {"session_id": "..."} |
_streaming_headers() |
5s |
| Shadow feedback | POST | {"session_id": "...", "action": "...", ...} |
_streaming_headers() |
5s |
| Shadow build | POST | {"session_id": "...", "name": "...", ...} |
_streaming_headers() |
10s |
| Persist | POST | {"workflow_ir": {...}, "machine_id": "..."} |
_streaming_headers() |
10s |
Tous les appels utilisent STREAMING_SERVER_URL (déjà configuré via RPA_STREAMING_URL, défaut http://localhost:5005).
8. Ce qui n'est PAS dans le scope
- Endpoint
/persist: n'existe pas. Tâche séparée (~80-120 lignes, cf. handoff). - Bouton dashboard : interdit par contrainte.
- Canvas/nodes VWB : interdit par contrainte.
- Génération de session_id par agent-chat : le session_id est résolu côté serveur.
- Multi-machine : en mono-machine Dom, la résolution est triviale (seule machine connectée). Multi-machine = évolution future.
9. Fichiers à modifier
| Fichier | Modification |
|---|---|
agent_chat/app.py |
Ajouter fonctions _shadow_start(), _shadow_stop(), _shadow_feedback(), _shadow_build() + logique de détection dans api_chat() |
agent_chat/intent_parser.py |
Ajouter patterns de détection "observe"/"c'est fini" (shadow) OU approche contextuelle via ConversationManager |
agent_chat/conversation_manager.py |
Ajouter set_session_flag / has_session_flag pour état shadow_session |
10. Implémentation suggérée (ordre)
- Fonctions callers Shadow dans
app.py— fonctions pures qui appellent les endpoints (analogues à_try_streaming_server_replay) - Détection intention shadow — patterns + contexte dans
api_chat() - Cycle start → stop → understanding — afficher les étapes à Dom
- Cycle feedback — parser les corrections de Dom et appeler
/shadow/feedback - Build — appeler
/shadow/buildaprès validation complète - Persist — brancher quand l'existe (tâche séparée)