Files
ObsiGate/docs/features/ai-tools-mcp.md
T
bruno e1842043d8
CI / lint (push) Successful in 2m1s
CI / security (push) Successful in 1m25s
CI / test (push) Successful in 4m19s
CI / build (push) Successful in 1m26s
CI / e2e (push) Successful in 13m38s
feat: assistant IA — approbation groupee des actions, bloc d'etapes, refresh UI et bouton Stop (BUG-074, BUG-075, BUG-076, BUG-077)
2026-09-24 10:05:32 -04:00

11 KiB

#79 — Assistant IA — Outils (function calling) & serveur MCP

Statut : ✅ Livré — Phase 0 + A2 + B1/B2/B3/B4/B5/B6/B7 + C + D + E + F + G livrés (2026-09-11) Effort : 10-15 jours | Impact : 🟡 Références : Roadmap · Guide d'architecture IA

  • Description : Transformer l'assistant BooksLM (protocole texte obsigate-action limité à create_file/create_directory) en un agent capable de lire, chercher, lister, ouvrir et modifier, via function calling natif, puis exposer ObsiGate à des clients MCP externes (Claude Desktop, Cursor…). Les deux fronts consomment une couche d'outils partagée — source unique de vérité.
  • Documentation : AI_ARCHITECTURE_GUIDE.md (architecture, catalogue d'outils, sécurité, phases).

A. Fondations — couche d'outils partagée (2-3 jours) — ✅ livré (2026-09-11)

  • A1. Créer backend/tools/ : context.py (ToolContext : user, allowed_vaults, mode, confirmed), registry.py (décorateur @tool + schéma JSON), schemas.py, service.py, audit.py
  • A2. Extraire la logique métier des routes de backend/main.py en fonctions de service réutilisables (les routes deviennent des wrappers) — backend/services/ (errors.py, paths.py, vaults.py, files.py, search.py) ; routes /api/vaults, /api/browse, /api/file/{vault}/raw, /api/search, /api/tags et outils list_vaults/list_directory/read_file/search_fulltext/list_tags délèguent à la même couche. Les ServiceError sont mappées vers HTTPException (routes) et ToolError (outils).
  • A3. Contexte de permissions : chaque outil applique check_vault_access + resolve_safe_path
  • A4. Audit : journalisation JSONL de chaque appel d'outil (qui, quoi, vault, résultat) — action ai_tool_call, arguments sensibles résumés
  • A5. Tests unitaires du registry + services (sans IA) — tests/test_tools.py (30 tests)

B. Function calling in-app (3-4 jours) — ✅ livré (2026-09-11)

  • B1. Abstraction tool-calling provider-agnostique : backend/ai_chat.py (chat_completion, ToolCall, LLMResponse) — OpenAI-compat (tools/tool_choice, parsing tool_calls) + Gemini (functionDeclarations/functionCall)
  • B2. Agent loop backend/agent/loop.py : boucle tool→résultat→tool, limite d'itérations (10), truncation des résultats ; endpoint opt-in POST /api/ai/bookslm/agent (events SSE tool/message/confirmation)
  • B3. Fallback : retry sans tools si le provider rejette les tools (400/404/422) → chat simple ; protocole texte obsigate-action conservé côté frontend pour le chat classique uniquement (BUG-053 : en mode agent, le prompt impose les outils natifs et interdit les blocs obsigate-action)
  • B4. SSE réellement streaming — ai_chat.stream_completion (_openai_stream + _gemini_stream) alimente /api/ai/bookslm/chat token par token ; le middleware GZip laisse passer les endpoints SSE BooksLM.
  • B5. Confirmations UI : toggle « mode agent » (front → /agent), événements tool/confirmation, carte Apply + aperçu diff (LCS) pour les mutations, reprise confirm/confirm_messages côté backend. S'active dès que la phase D enregistre des outils write.
    • Complément (BUG-074 → BUG-077) : la pause de confirmation regroupe toutes les mutations d'un même tour LLM (pending.actions, chacune avec son libellé step et son diff) et la carte n'offre plus qu'un seul bouton « Tout approuver (N) » ; la reprise envoie confirm_all et le backend arme ToolContext.confirmed pour le reste du run (plus d'approbation action par action). Le bloc d'étapes affiche un titre (1re action) et ne compte que les actions (hors réflexions) ; la reprise diffuse dans le même message (« N étapes » cumulées). L'arborescence et le document affiché sont rafraîchis dès une action mutatrice (refresh débouncé + obsigate:file-written, documents xlsx/docx/csv/pdf inclus). Le bouton d'envoi devient « Stop » pendant le stream (abort SSE, tâche serveur annulée à la déconnexion, marqueur « Exécution arrêtée. »).
  • B6. Outils de navigation in-app : open_file, reveal_in_tree (événement obsigate:open-file) — livré via les liens cliquables de l'assistant (#80, ai-assistant-ux.md)
  • B7. Tests : agent loop LLM mocké (tests/test_agent_loop.py), providers (tests/test_ai_chat.py), endpoint (tests/test_bookslm.py)

C. Catalogue d'outils — lecture & recherche (1-2 jours) — ✅ livré (2026-09-11)

  • C1. Vaults/navigation : list_vaults, list_directory, list_all_files — list_all_files ajouté via backend/services/vaults.py
  • C2. Lecture : read_file, read_file_raw, get_backlinks, list_backups, diff_backup, get_graph — services backend/services/backups.py (backups/diff) et graph.py ; read_file_raw redacte les secrets
  • C3. Recherche : search_fulltext, search_advanced, search_paths, list_tags, suggest_tags, list_recent — extensions backend/services/search.py (advanced_search_vaults, search_paths) et recent.py ; filtrage systématique par permissions vault
  • Routes /api/recent, /api/vault/{vault}/files, /api/file/{vault}/backups, /api/file/{vault}/diff, /api/graph/{vault}, /api/tree-search, /api/search/advanced refactorées en wrappers des services ; tests tests/test_tools.py (+17)

D. Catalogue d'outils — mutations (2 jours) — ✅ livré (2026-09-11)

  • D1. create_file, create_directory (migration du protocole texte existant)
  • D2. edit_file, append_to_file, rename_file, rename_directory, move_path
  • D3. replace_in_files (⚠️ confirmation two-step + backup auto)
  • D4. delete_file, delete_directory, restore_backup (⚠️ confirmation two-step + backup auto, toggle par vault)
  • Détail : service backend/services/mutations.py (source unique : anti path-traversal, garde lecture seule, backup auto via backend/services/backups.create_backup) ; les routes REST correspondantes délèguent à cette couche. Risques : WRITE (create/edit/append/restore) et DANGEROUS (rename/move/replace/delete) → confirmation two-step automatique par le registry. Toggle par vault aiDestructiveTools (défaut activé) pour désactiver les outils DANGEROUS. Tests : tests/test_tools_mutations.py (40 tests).

E. Serveur MCP (2-3 jours) — ✅ livré (2026-09-11)

  • E1. backend/mcp/server.py (SDK MCP Python mcp==1.9.4) enregistrant les outils depuis le registry
  • E2. Mapping des primitives : Tools (lecture + propose_*/apply_*), Resources (vault://<name>, vault://<name>/<path>), Prompts (summarize-directory, generate-note, find-related)
  • E3. Transport Streamable HTTP : route /mcp (matching exact + sous-chemins), auth Authorization: Bearer <JWT> → get_current_user. stdio optionnel plus tard
  • E4. Confirmations two-step propose_*/apply_* avec token JWT signé (usage unique, TTL OBSIGATE_MCP_CONFIRMATION_TTL, défaut 300 s) + blacklist de JTI persistée (data/mcp_used_tokens.json, anti-rejeu)
  • E5. Toggle par vault aiDestructiveTools appliqué à la proposition et à l'application
  • E6. Tests : tests/test_mcp.py (13 tests) — handshake initialize, tools/list/tools/call, resources, prompts, permissions par vault, anti-rejeu du token
  • Détail : backend/mcp/confirmations.py (jetons signés + anti-rejeu) ; backend/mcp/server.py (Server lowlevel + StreamableHTTPSessionManager en json_response=True, McpMount pour matcher /mcp exact, démarrage paresseux du manager pour les tests). Dépendances ajoutées : mcp==1.9.4, sse-starlette==2.1.3 (compatibles avec FastAPI 0.110 / starlette 0.37).

F. Durcissement & documentation (1 jour) — ✅ livré (2026-09-11)

  • F1. Rate limiting par token/outil + quotas — backend/tools/ratelimit.py (fenêtre glissante par identité et par outil ; identité = JTI du jeton (_token_jti, ajouté par backend/auth/middleware.py) sinon id/username). Env : OBSIGATE_TOOL_RATE_LIMIT (60), OBSIGATE_TOOL_RATE_LIMIT_PER_TOOL, OBSIGATE_TOOL_RATE_WINDOW (60 s) ; erreur ToolRateLimitError (rate_limited). Quotas BOOKSLM_MAX_* : BOOKSLM_MAX_TOOL_CALLS (25) plafonne les appels d'outils par run d'agent (backend/agent/loop.py, stopped="quota_exceeded") et BOOKSLM_MAX_TOOL_READ_BYTES (200000) plafonne read_file.
  • F2. Redaction des secrets avant retour au LLM — backend/tools/redaction.py (redact_payload, récursif) appliqué à tout résultat d'outil dans call_tool (couvre les diffs, extraits de recherche et lectures non pré-redactées).
  • F3. Documentation OpenAPI + guide MCP — backend/openapi_docs.py : tag MCP, règle /mcp, injection du path /mcp (Streamable HTTP, JSON-RPC) dans le schéma ; nouveau docs/GUIDES/MCP.md (endpoint, auth, config Claude Desktop / Cursor, tools/resources/prompts, sécurité, variables, dépannage).
  • F4. Tests E2E de bout en bout — tests/test_ai_e2e.py : agent in-app read→confirmation→write, quota d'outils, rate limiting, redaction, et flux MCP complet read → propose_edit_file → apply_edit_file → resources/read.

G. Sélection fournisseur/modèle par défaut — ✅ livré

  • G1. Persistance ai_default_provider + ai_default_models dans data/config.json (_DEFAULT_CONFIG)
  • G2. Lecture + rechargement à chaud dans backend/ai.py (get_default_provider, reload_ai_config)
  • G3. UI : sélecteurs « Fournisseur par défaut » + « Modèle par défaut » dans #cfg-ai (frontend/index.html, frontend/js/config.js)
  • G4. i18n FR/EN

Décisions

  • ✅ Transport MCP : Streamable HTTP (2026-09-11)
  • ✅ Confirmation MCP : two-step propose/apply (2026-09-11)
  • ✅ Périmètre des mutations externes : toutes autorisées (create/edit/rename/move/delete) — encadrées par confirmation + backup auto + audit + toggle par vault (2026-09-11)
  • ✅ Confirmation d'un lot d'appels (BUG-050) : quand un tour contient plusieurs appels d'outils et qu'un seul est mutateur, les appels non atteints reçoivent un résultat deferred pour préserver la validité du protocole tool-calling ; ils sont réémis après confirmation (2026-09-16)
  • Détail et justification dans le guide §8.