Files
flowdeck/docs/V74_Agent_Plus_Menu.md
T
bruno 1d7750bda0
FlowDeck CI / lint (push) Successful in 2m14s
FlowDeck CI / test (push) Failing after 16m4s
FlowDeck CI / docker (push) Skipped
feat: connecteurs Google + Microsoft 365 (OAuth2 PKCE) — phase 6/8 (v7.56.0)
- app/services/oauth_connectors.py : flow OAuth2 complet PKCE (S256) pour
  2 fournisseurs décrits par 1 dict — Google (Drive/Gmail/Calendar en lecture
  seule) et Microsoft 365 (Graph Files.Read / Mail.Read / Calendars.Read) ;
  begin() = URL d'autorisation + state + code_verifier, complete() = échange du
  code, access_token() = refresh automatique (60 s de marge, refresh_token
  conservé si absent de la réponse), api_get() = path absolu refusé + validation
  SSRF + borne 20 000 car.
- Tokens chiffrés Fernet en réutilisant calendar_sync._encrypt_tokens (zéro
  dépendance) dans la table connector_tokens (migration 33, PK (kind, user_id)).
- 4 routes /api/agent/connectors/oauth/{kind}/… : status, authorize (cookies
  d'état HttpOnly 10 min, retour same-origin validé), callback (GET safe, state
  comparé en temps constant, tokens stockés puis cookies purgés, redirection
  ?oauth=connected / ?oauth_error=), disconnect. OpenAPI 523 chemins.
- Config + .env.example : GOOGLE_CLIENT_ID/SECRET, MS_CLIENT_ID/SECRET (vidés =
  « non configuré »), redirect URI dérivé d'APP_BASE_URL.
- Catalogue : google/ms365 en natifs avec badge connecté/non connecté ;
  connector_fetch et Tester passent par l'API du fournisseur avec le token de
  l'utilisateur (user_id transmis par l'outil LLM).
- Menu + : « Se connecter » / « Déconnecter » sur la fiche, toast au retour du
  flux (URL nettoyée par history.replaceState). État dans la fiche du menu
  plutôt qu'une page dédiée.
- Tests : tests/test_v756_oauth_connectors.py (13), 0 appel réseau réel
  (_post_form / _api_get monkeypatchés) — state forgé refusé sans échange,
  tokens chiffrés en base, refresh, URL absolue refusée, 401/404, câblage menu ;
  test_v755 adapté (5 natifs). Suite complète 1319 verts (-n auto), ruff 0,
  eslint 0 erreur (19 warnings préexistants hors fichiers touchés).
2026-10-07 08:29:22 -04:00

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

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