Files
flowdeck/docs/V74_Agent_Plus_Menu.md
T
bruno 16f73fe39e
FlowDeck CI / lint (push) Successful in 2m14s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 2h7m52s
feat: Add plugins — catalogue on/off à effet réel (v7.58.0, phase 8/8)
- app/services/plugins.py + migration 35 : table plugins (slug, name,
  description, enabled) pré-remplie avec 3 modules câblés — web-tools,
  web-clipper, automations ; ligne absente = activé (défaut sûr)
- automations OFF → dépendance FastAPI posée à l'include_router dans main.py
  (aucun router touché) → toutes les routes /workspace/automations* refusées +
  garde de tick du scheduler en arrière-plan
- web-clipper OFF → GET /extensions et tout /api/v2/web-clipper/* refusés
- web-tools OFF → web_search et fetch_url retirés du schéma ET de execute()
  via ToolRegistry._all() : le LLM ne les voit plus
- UI rendue côté serveur : global Jinja plugin_enabled(slug) — nav
  « Extensions » / « Automations » en {% if %} (absentes du DOM), sections
  conditionnées en x-show dans settings.html
- menu + : l'entrée « Add plugins » devient vivante (fini disabled:true) —
  liste des 3 plugins avec bascule, GET/PATCH /api/agent/plugins[/slug]
  (slug inconnu → 404, 401 sans session)
- tests : tests/test_v758_plugins.py (10 tests) — routes refusées (302 hors
  /api, 404 JSON pour /api*), outils retirés, nav disparue, persistance,
  câblage ; assertions disabled:true == 0 dans les tests des phases 1/3/4/5/7
- livraison : VERSION + app/main = 7.58.0, OpenAPI 525 chemins, CHANGELOG,
  ROADMAP phase 8 cochée (menu + complet), avenant phase 8 (docs)
2026-10-07 10:19:25 -04:00

254 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# V74 — Menu + de l'assistant : hub de contexte (design)
> **Statut** : design — **phases 1 à 6 livrées (v7.51.0 → v7.56.0, 2026-10-06)** ;
> phases 7-8 restent à livrer. La **mémoire** a été simplifiée par rapport au
> modèle ici : une seule ligne résumé **par conversation** (pas de lignes
> `workspace_id NULL` — rien ne les écrivait), extraction déterministe sans LLM. Adaptations faites en cours de route : le parcours
> utilise `GET /api/nav/menu?parent_id=` (un niveau par appel, contrat
> dossier = `icon === 'folder'`), la gestion des skills est une **section du menu
> avec formulaire intégré** (pas une modale — `PATCH /api/agent/skills/{id}`
> ajouté), et les canevas vivent sous **`/board/api/page-templates`** (prefix
> `/board`) avec la nouvelle route `GET …/{id}/blocks` pour l'insertion.
> **Plan phasé** : `ROADMAP.md` → section « v7.51.0 → v7.58.0 — Menu + de l'assistant ».
> **Date** : 2026-10-06 · Version cible de départ : **v7.51.0** (phase 1).
---
## 1. Objectif
Le bouton **+** du panneau agent (`app/templates/agent_panel.html:409`) ouvre
aujourd'hui une liste plate de fichiers/pages/bases à épingler en contexte. On le
transforme en **menu à sections à deux niveaux** :
```
+ ┌──────────────────────────────────────────────────────┐
│ 📎 Ajouter des fichiers ou répertoires › │ → picker actuel + « Parcourir… »
│ ✨ Compétences-skills › │ → Deep research, Skill-creator, …
│ │ ────────────────
│ │ Gérer les compétences
│ │ Parcourir les compétences
│ 🔌 Connecteurs › │ → Parcourir les connecteurs
│ │ Ajouter un connecteur personnalisé
│ 🎨 Design System – Canevas › │ → liste des canevas
│ 🧩 Add plugins │ → catalogue on/off (modale)
│ 🧠 Mémoire [◉/○] │ → toggle persisté
└──────────────────────────────────────────────────────┘
```
Règle d'or : **chaque entrée vit dans la même phase qui la rend réelle**. Tant qu'une
section n'est pas livrée, elle s'affiche grisée « Bientôt (phase N) » — le menu ne
promet rien que le code ne fait.
## 2. État des lieux (lu dans le code, 2026-10-06)
| Section | Existe | Manque |
|---|---|---|
| Fichiers / répertoires | `GET /api/agent/mentions` (`app/routers/agent.py:596`) → documents, collections, pages ; jetons `document:id`, `collection:id`, `page:id` résolus par `ContextBuilder._mentions_context` (`app/services/context_builder.py:120`) | Sous-menu, mode parcours d'arborescence, jeton `folder:<id>` |
| Compétences | `agent_skills` CRUD (`/api/agent/skills` : GET/POST/DELETE, apply, import/export), galerie de 17 presets (`app/services/skill_gallery.py`), 11 skills builtin (`FD_SKILLS`, `static/js/agent_panel_2.js:11`) | UI « Gérer » (CRUD complet : **`PATCH /skills/{id}` à créer**), UI « Parcourir » (la galerie n'est accessible que via la palette `/`), libellés affichés type « Deep research » |
| Canevas | `GET /api/page-templates` (built-ins `app/services/block_templates.py` + personnels), `POST /api/page-templates/{id}/use` (`app/routers/board/page_api.py:75,135`) | Menu dans le panneau agent, action « insérer dans le document ouvert », canevas « design system » |
| Connecteurs | Rien (0 hit `connector` dans `app/`). Natifs déjà branchés ailleurs : Gitea (`gitea_client.py`), GitHub (`github_adapter.py`), Web (`web_search.py` + tool `fetch_url` v7.46) | Table, service d'adapters, catalogue UI, tools agent, OAuth Google/M365, Discord/Telegram, MCP |
| Plugins | Rien | Registre, catalogue UI, **câblage réel** (désactivation effective) |
| Mémoire | Rien (l'historique de la conversation est déjà envoyé, ce n'est pas de la mémoire) | Table, service d'extraction, injection conditionnelle, toggle persisté |
## 3. Découpage front (Alpine)
Tout vit dans `static/js/agent_panel_2.js` (composant `agentPanel()`) + un bloc de
markup dans `app/templates/agent_panel.html`, sans nouveau fichier JS.
**État ajouté**
```js
plusOpen: false, // menu ouvert
plusSection: 'root', // 'root' | 'files' | 'skills' | 'connectors' | 'canvases'
plusFocus: -1, // index de l'item focalisé
plusItems: [], // items de la section courante (fetch si besoin)
memoryOn: true, // état du toggle (phase 4)
```
**Items = données, pas du markup en dur** : un tableau `FD_PLUS_MENU`
`{key, icon, label, sub, action, phase}` → le rendu et la navigation clavier
(↑/↓/Entrée/Échap) sont une seule boucle. Les sections `phase > livrée` sont
`disabled: true`.
**Réemploi** : le conteneur et les styles `.fd-ap-mention-menu` /
`.fd-mention-item` (`agent_panel.html:171-181`) servent déjà exactement ce
comportement — on duplique le CSS minimal (`.fd-plus-*`) et on garde la logique de
focus/scroll (`moveMentionFocus`, `_scrollMentionItem`) comme modèle.
**Entrées existantes préservées** : la touche `@` dans le composer et le mode
recherche du `+` continuent de fonctionner à l'identique (régression zéro) ; le `+`
devient seulement *l'entrée en menu* au lieu d'ouvrir directement la liste.
## 4. Backend par phase
| Phase | Existant réutilisé | À créer |
|---|---|---|
| 1 — menu + fichiers/répertoires | `/api/agent/mentions`, `/api/local-workspace/tree?folder=`, enfants de page (`parent_id`, `local_workspace.py:243`) | Route(s) de parcours adaptée(s) au panneau (shape items compatible mention), jeton `folder:<id>` dans `ContextBuilder` |
| 2 — compétences | `/api/agent/skills` (GET/POST/DELETE/apply/import/export), `/api/agent/skills/gallery` + install | `PATCH /api/agent/skills/{id}` (édition), 2 modales |
| 3 — canevas | `/api/page-templates` (GET/POST/use) | Insersion dans l'éditeur ouvert (API d'insertion de blocs de l'éditeur), presets « design system » dans `block_templates.py` |
| 4 — mémoire | `ContextBuilder`, `PATCH /conversations/{id}` (pattern du toggle) | Table `agent_memory`, `app/services/agent_memory.py`, colonne `conversations.memory_enabled`, config `AGENT_MEMORY_DEFAULT` |
| 5 — connecteurs (socle) | `_is_public_host` (SSRF), `encrypt_secret` (`sso_provisioning.py:51`), `http_client.shared_client`, `tool_registry` | Table `agent_connectors`, `app/services/connectors/`, 3 adapters natifs (gitea/github/web), catalogue UI |
| 6 — Google + M365 | `auth/oauth.py` (client OAuth2), `encrypt_secret`, `shared_client` | Flows PKCE Google + Microsoft Graph, table de tokens (motif `calendar_sync.py:38`), écran connecteur |
| 7 — Discord / Telegram / Teams / MCP | Graph (phase 6), pattern de tools dynamiques du registre | 3 adapters + client MCP (`initialize`, `tools/list`, `tools/call`) |
| 8 — plugins | — | Table `plugins`, catalogue UI, désactivation réelle (routes + UI) |
## 5. Modèle de données
```sql
-- Phase 4 — mémoire
ALTER TABLE conversations ADD COLUMN memory_enabled INTEGER NOT NULL DEFAULT 1;
CREATE TABLE agent_memory (
id INTEGER PRIMARY KEY,
workspace_id INTEGER NOT NULL,
conversation_id INTEGER, -- NULL = mémoire d'espace (partagée)
kind TEXT NOT NULL DEFAULT 'summary', -- summary | fact
content TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX idx_agent_memory_ws ON agent_memory(workspace_id, conversation_id);
-- Phase 5 — connecteurs
CREATE TABLE agent_connectors (
id INTEGER PRIMARY KEY,
workspace_id INTEGER,
kind TEXT NOT NULL, -- gitea | github | web | google | ms365 | discord | telegram | mcp | custom
name TEXT NOT NULL,
config_json TEXT NOT NULL DEFAULT '{}', -- URL, scopes, channels…
secret_encrypted TEXT, -- Fernet (encrypt_secret, sso_provisioning.py:51)
enabled INTEGER NOT NULL DEFAULT 1,
status TEXT NOT NULL DEFAULT 'unknown', -- ok | error | unknown
last_probe_at TEXT,
created_by INTEGER
);
-- Phase 8 — plugins
CREATE TABLE plugins (
slug TEXT PRIMARY KEY, -- web-tools | web-clipper | automations | …
enabled INTEGER NOT NULL DEFAULT 1,
updated_at TEXT NOT NULL
);
```
Migrations ajoutées dans `app/migrations.py` (motif `_migration_*` existant,
transaction par migration — A31).
## 6. Sécurité (non négociable)
- **SSRF** : toute URL de connecteur passée par `_is_public_host`
(`app/services/importers/url_fetch.py`), re-vérifiée à chaque saut de redirection.
- **Secrets** : jamais en clair, jamais renvoyés par l'API (champ retiré des
sérialiseurs) — `encrypt_secret()` existant, dérivé de `app_secret_key`.
- **Auth + CSRF** : session requise sur tout CUD ; `/api/agent` est sur la liste des
préfixes CSRF cookie → header `X-CSRF-Token` sur les nouveaux `fetch` (helper
`getCsrf()`).
- **Borne de tokens** : la mémoire et les sources de connecteurs injectées dans le
contexte sont tronquées (budget explicite) — sinon la phase 4 fait exploser le
coût de chaque run.
- **Plugins OFF = effet réel** : un simple drapeau affiché serait un menu qui ment
(règle du projet).
## 7. Décisions techniques
- **D1 — Un seul menu, deux niveaux** (pas de modale racine) : ouverture instantanée,
clavier, et le `+` reste un geste unique. Les modales ne servent qu'aux écrans
lourds (Gérer / Parcourir / Connecteurs / Plugins).
- **D2 — Pas de nouveau fichier JS** : tout dans `agent_panel_2.js` + markup du
template, conforme à l'extraction A27 (un fichier par bloc).
- **D3 — États « Bientôt » visibles** plutôt que sections masquées : le menu sert de
spec vivante et évite d'implémenter6 features d'un coup.
- **D4 — La mémoire est un résumé structuré, pas l'historique brut** : réinvoquer
tout l'historique à chaque run coûterait plus cher que la réponse ; un résumé borné
(~1–2 k tokens) + les faits saillants suffisent, avec repli déterministe hors-ligne
(motif `ai_writing.py`).
- **D5 — Connecteurs = registry + adapters** (interface `probe/list_sources/fetch`),
pas un if par provider : la phase 7 (MCP) ajoute des adapters sans toucher au socle.
- **D6 — Canevas : « créer » par défaut, « insérer » seulement si un document est
ouvert** — l'insertion dans l'éditeur est le point le plus fragile (blocs +
sélection), donc livrée en second dans la phase 3.
- **D7 — Pas de dépendance ajoutée** : OAuth via `httpx` (`shared_client`), chiffrement
via `cryptography` déjà présent, MCP en HTTP direct.
## 8. Pièges du repo à respecter
- `ROADMAP.md` / `CHANGELOG.md` sont en **CRLF** : `patch` exige des lignes complètes.
- **Routes statiques avant les wildcards** (`/skills/gallery` avant `/skills/{id}`),
sinon 422 sur un id non numérique.
- **Events `AgentEngine.run()` plats** (`{"type": …, "content": …}`), pas enveloppés
dans `data`.
- Bumper **`VERSION` + `app/main.py`** à chaque phase, régénérer
`docs/openapi-v2.json` (CRLF, sans newline final) si de nouvelles routes sortent.
- Le panneau agent est rendu sur **toutes les pages** (`base.html:2555`) : le JS doit
rester défensif (`window.appState` absent, page non authentifiée).
## 9. Critères d'acceptation par phase
1. **Phase 1** : le `+` ouvre le menu, « Parcourir… » montre l'arborescence, un
dossier épinglé apparaît en chip et son contenu est bien dans le contexte du run.
2. **Phase 2** : créer/éditer/supprimer un skill depuis la modale, l'installer depuis
la galerie, il apparaît dans la palette `/` et le sous-menu.
3. **Phase 3** : choisir un canevas crée la page (et l'insertion dans le document
ouvert fonctionne quand un doc est édité).
4. **Phase 4** : ON → la mémoire est injectée (test qui le prouve) ; OFF → absente ;
l'état survit au rechargement et au changement de conversation.
5. **Phase 5** : connecteur créé/testé/supprimé, URL privée refusée (SSRF), tool
exposé au LLM avec le bon schéma.
6. **Phase 6** : connexion Google et Microsoft, refresh, déconnexion, aucun appel
réseau réel dans les tests.
7. **Phase 7** : Discord/Telegram branchés, MCP `tools/list` → outils visibles dans le
registre.
8. **Phase 8** : plugin OFF = route refusée + UI disparue (test de non-réintroduction).
Chaque phase se clôt par : `ruff` + suite verte, bump de version, CHANGELOG, ROADMAP
à jour, tag.
## 9bis. Avenant phase 7 (v7.57.0) — décisions livrées
- **D5 (`probe/list_sources/fetch` par adapter) écartée** : 1 switch sur
`agent_connectors.kind` dans `probe()` / `connector_fetch()` — une interface
à 3 méthodes pour 4 kinds tordrait plus qu'elle ne simplifie ; ajouter un
kind = 1 branche de plus dans le même switch.
- **Discord / Telegram = presets du socle**, pas 3 adapters dédiés : le champ
**Type** du formulaire pré-remplit l'URL + le schéma d'auth
(`bearer`/`bot`/`none`). La Bot API de Telegram impose le jeton **dans l'URL**
→ substitut `{secret}` remplacé à l'appel (`connectors._with_secret`),
stockage = motif seul, test « le jeton n'est jamais en base ».
- **MCP : outils dynamiques sans état** — `ToolRegistry._all()` relit le cache
`tools_json` à chaque run (rien à invalider, « Tester » suffit) ; nom LLM
`mcp_<serveur>_<outil>` (slug sans double soulignement) ; dispatch
`tools/call` dans `McpTool.execute()`. Serveur désactivé → outil **absent du
schéma**, échec réseau → `ToolResult(error)` : le run continue toujours.
- **Pas de client SSE à l'état** : `parse_body()` lit le JSON direct ou le
premier `data:` d'un `text/event-stream`, ce qui couvre
`initialize` / `tools/list` / `tools/call` des serveurs streamables courants.
Upgrade : client SSE persistant si un serveur ne répond qu'en flux.
## 9ter. Avenant phase 8 (v7.58.0) — décisions livrées
- **Garde de route = dépendance posée à l'`include_router`** (`main.py`), pas
de décorateur par route ni de middleware : 3 lignes, aucun router modifié.
⚠️ l'annotation `request: Request` doit être importée **au module** : avec
`from __future__ import annotations` les annotations sont des chaînes et
FastAPI les résout dans les globales du module — sinon il lit un query param
→ 422 (piège réel, corrigé en cours de phase).
- **Effet réel aux 4 niveaux** : routes (dépendance), arrière-plan (garde de
tick du scheduler), outils (`ToolRegistry._all()`), UI (rendu serveur).
- **UI : `{% if %}` pour la nav, `x-show` pour les sections** — la nav est
**absente du DOM** (test « UI absente » au sens propre) ; les grosses
sections ne sont pas restructurées, leur booléen est rendu au serveur.
- **Handler unifié des 404** (comportement historique conservé) : hors `/api`
une route refusée **redirige 302 → /workspaces**, `/api*` répond 404 JSON —
les tests assertent les deux formes au lieu de réécrire le handler.
- **SQLite** : `INSERT … ON CONFLICT` évalue NOT NULL **avant** l'upsert →
fournir toutes les colonnes NOT NULL, même pour un simple changement d'état.
- `ponytail:` plafond assumé — pas de classe-adapter par plugin ni de
« marketplace » : ajouter un plugin = 1 tuple dans `CATALOG` (+ 1 garde si
son effet n'est pas déjà couvert).
## 10. Hors périmètre
- Écriture dans les sources distantes (Google Docs, Slack…) — lecture seule au départ.
- Synchronisation d'arborescence locale ↔ connecteur.
- Marketplace de plugins tiers (le registre est interne à l'instance).
- Refonte du composer / de la palette `/` (compatibilité maintenue à l'identique).