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

16 KiB
Raw Blame History

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é

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

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