89 lines
11 KiB
Markdown
89 lines
11 KiB
Markdown
# #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](../ROADMAP.md) · [Guide d'architecture IA](../AI_ARCHITECTURE_GUIDE.md)
|
|
|
|
- **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](../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)
|
|
- [x] **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`
|
|
- [x] **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).
|
|
- [x] **A3.** Contexte de permissions : chaque outil applique `check_vault_access` + `resolve_safe_path`
|
|
- [x] **A4.** Audit : journalisation JSONL de chaque appel d'outil (qui, quoi, vault, résultat) — action `ai_tool_call`, arguments sensibles résumés
|
|
- [x] **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)
|
|
- [x] **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`)
|
|
- [x] **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`)
|
|
- [x] **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`)
|
|
- [x] **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.
|
|
- [x] **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. »).
|
|
- [x] **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](./ai-assistant-ux.md))
|
|
- [x] **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)
|
|
- [x] **C1.** Vaults/navigation : `list_vaults`, `list_directory`, `list_all_files` — `list_all_files` ajouté via `backend/services/vaults.py`
|
|
- [x] **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
|
|
- [x] **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)
|
|
- [x] **D1.** `create_file`, `create_directory` (migration du protocole texte existant)
|
|
- [x] **D2.** `edit_file`, `append_to_file`, `rename_file`, `rename_directory`, `move_path`
|
|
- [x] **D3.** `replace_in_files` (⚠️ confirmation two-step + backup auto)
|
|
- [x] **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)
|
|
- [x] **E1.** `backend/mcp/server.py` (SDK MCP Python `mcp==1.9.4`) enregistrant les outils depuis le registry
|
|
- [x] **E2.** Mapping des primitives : Tools (lecture + `propose_*`/`apply_*`), Resources (`vault://<name>`, `vault://<name>/<path>`), Prompts (`summarize-directory`, `generate-note`, `find-related`)
|
|
- [x] **E3.** Transport **Streamable HTTP** : route `/mcp` (matching exact + sous-chemins), auth `Authorization: Bearer <JWT>` → `get_current_user`. `stdio` optionnel plus tard
|
|
- [x] **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)
|
|
- [x] **E5.** Toggle par vault `aiDestructiveTools` appliqué à la proposition et à l'application
|
|
- [x] **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)
|
|
- [x] **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`.
|
|
- [x] **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).
|
|
- [x] **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`](../GUIDES/MCP.md) (endpoint, auth, config Claude Desktop /
|
|
Cursor, tools/resources/prompts, sécurité, variables, dépannage).
|
|
- [x] **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é
|
|
- [x] **G1.** Persistance `ai_default_provider` + `ai_default_models` dans `data/config.json` (`_DEFAULT_CONFIG`)
|
|
- [x] **G2.** Lecture + rechargement à chaud dans `backend/ai.py` (`get_default_provider`, `reload_ai_config`)
|
|
- [x] **G3.** UI : sélecteurs « Fournisseur par défaut » + « Modèle par défaut » dans `#cfg-ai` (`frontend/index.html`, `frontend/js/config.js`)
|
|
- [x] **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](../AI_ARCHITECTURE_GUIDE.md).
|