# 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` ```json // 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` ```json // 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` ```json { "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` ```json {"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 ` (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+(.+)"` | EXECUTE | | "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 : ```python 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 : ```python # 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) 1. **Fonctions callers Shadow** dans `app.py` — fonctions pures qui appellent les endpoints (analogues à `_try_streaming_server_replay`) 2. **Détection intention shadow** — patterns + contexte dans `api_chat()` 3. **Cycle start → stop → understanding** — afficher les étapes à Dom 4. **Cycle feedback** — parser les corrections de Dom et appeler `/shadow/feedback` 5. **Build** — appeler `/shadow/build` après validation complète 6. **Persist** — brancher quand l'existe (tâche séparée)