docs: add POC specs, handoffs, and research notes
This commit is contained in:
274
docs/specs/Q-P1-agentchat-shadow-spec.md
Normal file
274
docs/specs/Q-P1-agentchat-shadow-spec.md
Normal file
@@ -0,0 +1,274 @@
|
||||
# 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 <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+(.+)"` | 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)
|
||||
Reference in New Issue
Block a user