9.8 KiB
9.8 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-actionlimité à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.pyen 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/tagset outilslist_vaults/list_directory/read_file/search_fulltext/list_tagsdélèguent à la même couche. LesServiceErrorsont mappées versHTTPException(routes) etToolError(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, parsingtool_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-inPOST /api/ai/bookslm/agent(events SSEtool/message/confirmation) - B3. Fallback : retry sans
toolssi le provider rejette les tools (400/404/422) → chat simple ; protocole texteobsigate-actionconservé côté frontend pour le chat classique uniquement (BUG-053 : en mode agent, le prompt impose les outils natifs et interdit les blocsobsigate-action) - B4. SSE réellement streaming —
ai_chat.stream_completion(_openai_stream+_gemini_stream) alimente/api/ai/bookslm/chattoken par token ; le middleware GZip laisse passer les endpoints SSE BooksLM. - B5. Confirmations UI : toggle « mode agent » (front →
/agent), événementstool/confirmation, carte Apply + aperçu diff (LCS) pour les mutations, repriseconfirm/confirm_messagescôté backend. S'active dès que la phase D enregistre des outilswrite. - B6. Outils de navigation in-app :
open_file,reveal_in_tree(événementobsigate: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_filesajouté viabackend/services/vaults.py - C2. Lecture :
read_file,read_file_raw,get_backlinks,list_backups,diff_backup,get_graph— servicesbackend/services/backups.py(backups/diff) etgraph.py;read_file_rawredacte les secrets - C3. Recherche :
search_fulltext,search_advanced,search_paths,list_tags,suggest_tags,list_recent— extensionsbackend/services/search.py(advanced_search_vaults,search_paths) etrecent.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/advancedrefactorées en wrappers des services ; teststests/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 viabackend/services/backups.create_backup) ; les routes REST correspondantes délèguent à cette couche. Risques :WRITE(create/edit/append/restore) etDANGEROUS(rename/move/replace/delete) → confirmation two-step automatique par le registry. Toggle par vaultaiDestructiveTools(défaut activé) pour désactiver les outilsDANGEROUS. 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 Pythonmcp==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), authAuthorization: Bearer <JWT>→get_current_user.stdiooptionnel plus tard - E4. Confirmations two-step
propose_*/apply_*avec token JWT signé (usage unique, TTLOBSIGATE_MCP_CONFIRMATION_TTL, défaut 300 s) + blacklist de JTI persistée (data/mcp_used_tokens.json, anti-rejeu) - E5. Toggle par vault
aiDestructiveToolsappliqué à la proposition et à l'application - E6. Tests :
tests/test_mcp.py(13 tests) — handshakeinitialize,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(Serverlowlevel +StreamableHTTPSessionManagerenjson_response=True,McpMountpour matcher/mcpexact, 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é parbackend/auth/middleware.py) sinon id/username). Env :OBSIGATE_TOOL_RATE_LIMIT(60),OBSIGATE_TOOL_RATE_LIMIT_PER_TOOL,OBSIGATE_TOOL_RATE_WINDOW(60 s) ; erreurToolRateLimitError(rate_limited). QuotasBOOKSLM_MAX_*:BOOKSLM_MAX_TOOL_CALLS(25) plafonne les appels d'outils par run d'agent (backend/agent/loop.py,stopped="quota_exceeded") etBOOKSLM_MAX_TOOL_READ_BYTES(200000) plafonneread_file. - F2. Redaction des secrets avant retour au LLM —
backend/tools/redaction.py(redact_payload, récursif) appliqué à tout résultat d'outil danscall_tool(couvre les diffs, extraits de recherche et lectures non pré-redactées). - F3. Documentation OpenAPI + guide MCP —
backend/openapi_docs.py: tagMCP, règle/mcp, injection du path/mcp(Streamable HTTP, JSON-RPC) dans le schéma ; nouveaudocs/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 completread→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_modelsdansdata/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
deferredpour 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.