Files
rpa_vision_v3/docs/specs/Q-P1-agentchat-shadow-spec.md

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)

  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)