- 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)
254 lines
16 KiB
Markdown
254 lines
16 KiB
Markdown
# 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).
|