docs: add POC specs, handoffs, and research notes

This commit is contained in:
Dom
2026-06-02 16:28:34 +02:00
parent 18ed6cb751
commit f2e9aac6b7
86 changed files with 27615 additions and 25 deletions

View 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)