Files
ObsiGate/docs/AI_ARCHITECTURE_GUIDE.md
T
bruno 705f755b6b
CI / lint (push) Successful in 2m2s
CI / security (push) Successful in 1m25s
CI / test (push) Successful in 4m13s
CI / build (push) Successful in 1m16s
CI / e2e (push) Successful in 11m56s
docs: guides d'utilisation, capture reelle et README ameliores
2026-09-22 22:40:51 -04:00

389 lines
23 KiB
Markdown

# ObsiGate — Guide d'architecture IA
> **Statut :** document de conception (référence pour l'implémentation)
> **Dernière mise à jour :** 2026-09-11
> **Portée :** sous-systèmes IA d'ObsiGate, couche d'outils (function calling), serveur MCP, sélection du fournisseur/modèle par défaut.
---
## 1. Vue d'ensemble
ObsiGate possède aujourd'hui **deux sous-systèmes IA** qui partagent la même couche de fournisseurs (`backend/ai.py`) :
| Sous-système | Rôle | Backend | Frontend |
|---|---|---|---|
| **Barre d'outils IA de l'éditeur** | 16 transformations de texte statiques (améliorer, traduire, résumer…) | `backend/ai.py`, `backend/ai_routes.py` | `frontend/js/ai.js` |
| **Assistant BooksLM** | Chat contextuel (dossier / documents / général) | `backend/bookslm.py`, `backend/bookslm_routes.py` | `frontend/js/bookslm.js` |
**Constat clé :** il n'existe **aucun tool calling / function calling natif ni MCP**. La seule « action » de l'assistant repose sur un protocole texte maison (`obsigate-action`) limité à `create_file` et `create_directory`.
### Objectif cible
1. Un **assistant in-app** capable de lire, chercher, lister, ouvrir et modifier, via **function calling natif**.
2. Un **serveur MCP** exposant ObsiGate aux clients externes (Claude Desktop, Cursor…).
3. Les deux fronts consomment **la même couche d'outils** — source unique de vérité.
```
┌──────────────────────────────┐
│ Couche d'outils partagée │
│ registry + services + perms │
└───────┬───────────────┬───────┘
│ │
┌─────────────▼─────┐ ┌─────▼──────────────────┐
│ Assistant in-app │ │ Serveur MCP (externe) │
│ function calling │ │ Claude Desktop, Cursor │
│ + confirmations UI│ │ tools + resources │
└───────────────────┘ └────────────────────────┘
```
---
## 2. État actuel (références de code)
### 2.1 Barre d'outils IA de l'éditeur
- `ai_complete()` — `backend/ai.py:175`
- 16 endpoints REST `POST /api/ai/*` — `backend/ai_routes.py:135+`
- Requête `AIRequest` (`text`, `instruction`, `target_lang`, `tone`, `provider`, `model`) — `backend/ai_routes.py:100`
- Statut `GET /api/ai/status` — `backend/ai_routes.py:34`
### 2.2 Assistant BooksLM
- Collecte du contexte : `collect_directory_context()` `backend/bookslm.py:69`, `collect_files_context()` `backend/bookslm.py:209`
- Arborescence : `_build_directory_tree()` `backend/bookslm.py:276`
- Limites : `BOOKSLM_MAX_FILES=200`, `BOOKSLM_MAX_TOTAL_CHARS=200000`, `BOOKSLM_MAX_FILE_CHARS=30000` — `backend/bookslm.py:21`
- Cache 5 min indexé par mtime — `backend/bookslm.py:26`, `:111`
- Prompts système : `build_system_prompt()` `backend/bookslm.py:295`, `build_general_system_prompt()` `backend/bookslm.py:380`
- Protocole d'action : `GENERAL_SYSTEM_PROMPT` `backend/bookslm.py:353-377`
- Routes : `POST /api/ai/bookslm/context`, `POST /api/ai/bookslm/chat` — `backend/bookslm_routes.py:96,118`
- Extraction/application d'action côté client : `_extractActions()` `frontend/js/bookslm.js:502`, `_applyAction()` `frontend/js/bookslm.js:550`
### 2.3 Couche fournisseurs
- `PROVIDERS` (dict module, chargé une fois à l'import) — `backend/ai.py:37-94`
- `DEFAULT_PROVIDER` — `backend/ai.py:96`
- Appel OpenAI-compatible : `_call_deepseek_openrouter()` — `backend/ai.py:115`
- Appel Gemini : `_call_gemini()` — `backend/ai.py:156`
- Clés stockées dans `data/api_keys.json`, fallback `.env` — `get_ai_key()` `backend/ai.py:30`
- Surcharge de modèle par requête : `_handle()` `backend/ai_routes.py:114`, `bookslm_routes.py:194`
### 2.4 Sécurité existante à réutiliser
| Mécanisme | Référence |
|---|---|
| Auth JWT | `require_auth` `backend/auth/middleware.py:69` |
| Admin | `require_admin` `backend/auth/middleware.py:80` |
| Accès vault | `check_vault_access` `backend/auth/middleware.py:87`, `require_vault_access` `:101` |
| Anti path-traversal | `_resolve_safe_path` `backend/main.py:867` |
| Redaction secrets | `backend/secret_redactor.py` (`redact_file_content`) |
| Audit | `data/audit.log` |
### 2.5 Limites de l'existant
1. **Pas de tool calling** : parsing regex fragile, pas de résultats structurés, pas de multi-étapes.
→ **résolu** (phases B/C/D) : tool calling natif, catalogue lecture/recherche + mutations.
2. **2 actions seulement** (`create_file`, `create_directory`) ; ni lecture active, ni recherche, ni édition, ni suppression exposées au modèle.
→ **résolu** (phases C/D) : catalogue complet (lecture, recherche, mutations avec confirmation).
3. **SSE** : ~~non réellement streaming~~ → **corrigé** (B4) : `ai_chat.stream_completion` alimente `/api/ai/bookslm/chat` token par token ; le tool-calling (`/agent`) reste non-streaming (les appels d'outils exigent la réponse complète).
4. **`PROVIDERS` chargé une seule fois** : les modèles par défaut ne sont modifiables que par variables d'environnement (pas de persistance UI).
---
## 3. Architecture cible
### 3.1 Couche 1 — Capacités (services backend)
Extraire la logique métier des routes de `backend/main.py` vers des fonctions réutilisables (services). Les routes REST, l'agent in-app et le serveur MCP appellent ces mêmes services.
**Nouveau module `backend/services/` (A2 lecture/recherche + C + D mutations) :**
```
backend/services/
├── errors.py # ServiceError (code + status HTTP)
├── paths.py # resolve_safe_path (anti path-traversal, source unique)
├── vaults.py # list_accessible_vaults, browse_directory, get_vault_root, list_all_files
├── files.py # read_raw_file, read_file_text (redaction + quota)
├── mutations.py # create/edit/append/rename/move/delete/restore/replace (D)
├── search.py # search_vaults (pagination), list_tags, advanced_search_vaults, search_paths
├── backups.py # get_backup_dir, create_backup, list_backup_files, diff_backup
├── graph.py # get_graph (nodes/edges, wikilinks)
└── recent.py # list_recent, humanize_mtime
```
Les routes `/api/vaults`, `/api/browse/{vault}`, `/api/file/{vault}/raw`, `/api/search`, `/api/tags`,
`/api/recent`, `/api/vault/{vault}/files`, `/api/file/{vault}/backups`, `/api/file/{vault}/diff`,
`/api/graph/{vault}`, `/api/tree-search`, `/api/search/advanced` (phase C) ainsi que
`/api/file/{vault}` (POST/PATCH/DELETE), `/api/file/{vault}/save`, `/api/file/{vault}/restore`,
`/api/directory/{vault}` (POST/PATCH/DELETE), `/api/move/{vault}` et `/api/search/replace` (phase D)
en sont de simples wrappers, tout comme les outils correspondants du catalogue. Un `ServiceError`
est traduit en `HTTPException` (handler global de `backend/main.py`) ou en `ToolError`
(`backend/tools/registry.py`).
**Nouveau module `backend/tools/` :**
```
backend/tools/
├── api.py # façade publique (enregistre les outils, réexporte l'API)
├── context.py # ToolContext (user, allowed_vaults, mode, confirmed)
├── registry.py # décorateur @tool(...) + schémas JSON
├── schemas.py # modèles Pydantic entrée/sortie
├── service.py # implémentations (wrappers des services métier)
└── audit.py # journalisation des appels d'outils
```
> ObsiGate utilise des **namespace packages** implicites (aucun `__init__.py` suivi — `.gitignore` exclut `_*.py`). La façade `api.py` joue le rôle de point d'entrée et déclenche l'enregistrement des outils.
### 3.2 Couche 2 — Registry d'outils
Chaque outil déclare : `name`, `description`, `parameters` (JSON Schema), `risk` (`read` | `write` | `dangerous`), `requires_confirmation` (bool), `scopes` (`in_app`, `mcp`).
### 3.3 Couche 3 — Agent loop (in-app)
Nouveau `backend/agent/loop.py` :
```
boucle (max N itérations):
réponse = LLM(messages, tools)
si tool_calls:
pour chaque appel:
vérifier permissions + confirmation
exécuter via la couche d'outils (ToolContext)
réinjecter le résultat comme message "tool"
continuer
sinon:
renvoyer le texte final
```
- Fallback protocole texte (`obsigate-action`) si le modèle ne supporte pas les tools.
- SSE réellement streaming sur `/chat` (`ai_chat.stream_completion`) ; `/agent` reste buffered
(les appels d'outils exigent la réponse complète avant exécution). Confirmations UI en deux temps
(`confirmation` → `confirm`/`confirm_messages`).
### 3.4 Couche 4 — Serveur MCP (livré, phase E)
`backend/mcp/server.py` s'appuie sur le SDK MCP Python (`mcp==1.9.4`) :
- **Tools** : enregistrés depuis le registry (scope `mcp`). Les outils `read` sont exposés
directement ; les outils `write`/`dangerous` le sont via la paire `propose_<tool>` /
`apply_<tool>` (jeton signé, usage unique, TTL).
- **Resources** : `vault://<name>` (vaults accessibles) et `vault://<name>/<path>` (fichiers,
lecture seule, secrets redactés).
- **Prompts** : `summarize-directory`, `generate-note`, `find-related`.
- Transport : **Streamable HTTP** (`/mcp`, SDK `StreamableHTTPSessionManager` en
`json_response=True`), auth `Authorization: Bearer <JWT>` → `get_current_user`. `stdio`
optionnel plus tard.
- Confirmations (E4) : `backend/mcp/confirmations.py` — jeton JWT (`type=mcp_confirmation`)
contenant outil + arguments + utilisateur ; blacklist de JTI persistée pour l'anti-rejeu.
- Le manager de session est démarré **paresseusement** à la première requête, pour fonctionner
aussi bien sous uvicorn que sous le client de test (sans lifespan ASGI).
---
## 4. Catalogue des outils
> **Inventaire au registre** — 28 outils enregistrés (`backend/tools/service.py`,
> `backend/tools/web.py`), vérifiable par
> `docker exec obsigate-test python -c "from backend.tools import api; from backend.tools.registry import _REGISTRY; print(len(_REGISTRY))"`.
> Les tables ci-dessous reflètent le code, pas l'intention : ce qui n'est pas
> listé n'est pas exposé au modèle.
Légende : **R** = lecture (appel automatique), **M** = mutation (confirmation
two-step obligatoire), **D** = dangereux (confirmation **+** réglage par vault
`aiDestructiveTools`). « Étape (UI) » = libellé affiché dans la section
« N étapes » de l'assistant (`backend/tools/labels.py` → clés `ai.step.*` FR/EN).
### Vaults & navigation
| Outil | Type | Paramètres | Étape (UI) |
|---|---|---|---|
| `list_vaults` | R | — | Liste des vaults consultée |
| `list_directory` | R | `vault`, `path` | Répertoire exploré : {chemin} |
| `list_all_files` | R | `vault`, `dir`, `limit`, `recursive` | Fichiers listés |
### Lecture de contenu
| Outil | Type | Paramètres | Étape (UI) |
|---|---|---|---|
| `read_file` | R | `vault`, `path` | Fichier lu : {chemin} |
| `read_file_raw` | R | `vault`, `path` | Fichier lu : {chemin} |
| `get_backlinks` | R | `vault`, `path` | Backlinks analysés |
| `list_backups` | R | `vault`, `path` | Sauvegardes consultées |
| `diff_backup` | R | `vault`, `path`, `version`, `compare_with` | Comparaison de sauvegarde : {chemin} |
| `get_graph` | R | `vault`, `path`, `depth`, `scope`, `tag` | Graphe du vault consulté |
### Recherche
| Outil | Type | Paramètres | Étape (UI) |
|---|---|---|---|
| `search_fulltext` | R | `q`, `vault`, `tag`, `limit` | Recherche dans le vault : {q} |
| `search_advanced` | R | `q`, `vault`, `tag`, `limit`, `offset`, `sort`, `case_sensitive`, `whole_word`, `regex`, `include_paths`, `exclude_paths`, `created`, `modified`, `size` | Recherche dans le vault : {q} |
| `search_paths` | R | `q`, `vault` | Chemins recherchés : {q} |
| `list_tags` | R | `vault` | Tags consultés |
| `suggest_tags` | R | `q`, `vault`, `limit` | Tags suggérés pour {q} |
| `list_recent` | R | `vault`, `limit`, `mode` | Fichiers récents consultés |
### Web (in-app uniquement, jamais exposé en MCP)
| Outil | Type | Paramètres | Étape (UI) |
|---|---|---|---|
| `web_search` | R | `query`, `max_results`, `category`, `language`, `page` | Recherche sur le web : {query} |
| `fetch_url` | R | `url` | Page web consultée : {url} |
`web_search` interroge l'instance SearXNG auto-hébergée
(`OBSIGATE_SEARXNG_URL`, `search.dracodev.net` par défaut — aucune clé API) ;
`fetch_url` extrait le texte d'une page publique après garde SSRF (URL et
redirections reverrouillées hop par hop, plafond de taille).
### Création / modification (confirmation)
| Outil | Type | Paramètres | Étape (UI) |
|---|---|---|---|
| `create_file` | M | `vault`, `path`, `content` | Fichier proposé : {chemin} |
| `create_directory` | M | `vault`, `path` | Dossier proposé : {chemin} |
| `edit_file` | M | `vault`, `path`, `content` | Fichier modifié : {chemin} |
| `append_to_file` | M | `vault`, `path`, `content` | Fichier complété : {chemin} |
| `restore_backup` | M | `vault`, `path`, `version` | Sauvegarde restaurée : {chemin} |
### Suppression & opérations destructives (confirmation + toggle par vault)
| Outil | Type | Paramètres | Étape (UI) |
|---|---|---|---|
| `rename_file` | D | `vault`, `path`, `new_name` | Fichier renommé : {chemin} |
| `rename_directory` | D | `vault`, `path`, `new_name` | Dossier renommé : {chemin} |
| `move_path` | D | `vault`, `source_path`, `destination_dir` | Élément déplacé : {chemin} |
| `replace_in_files` | D | `find`, `replace`, `vault`, `case_sensitive`, `whole_word`, `regex`, `include_paths`, `exclude_paths`, `replace_all`, `dry_run` | Remplacements : {motif} |
| `delete_file` | D | `vault`, `path` | Fichier supprimé : {chemin} |
| `delete_directory` | D | `vault`, `path`, `recursive` | Dossier supprimé : {chemin} |
### Prévu, non implémenté
Navigation front (`open_file`, `reveal_in_tree` — réalisés côté UI par les liens
cliquables de l'assistant, pas comme outils du registry), partage
(`create_share`, `list_shares`) et sources connectées (Gitea, Drive, courriel… :
voir [#92](./features/ai-tools-roadmap.md)).
---
## 5. Sécurité & permissions
Tout outil reçoit un `ToolContext` et applique **systématiquement** :
1. `check_vault_access(vault, user)` — `backend/auth/middleware.py:87`
2. `_resolve_safe_path(vault_root, path)` — `backend/main.py:867`
3. Confirmation utilisateur pour `risk in (write, dangerous)` (UI in-app : carte Apply ; MCP : two-step `propose`/`apply`).
4. Redaction des secrets avant tout envoi au LLM — `redact_file_content()` puis
`backend/tools/redaction.py` sur **tout** résultat d'outil (diffs, extraits de recherche).
5. Journalisation dans l'audit (`data/audit.log`).
6. **Rate limiting par jeton/outil** (`backend/tools/ratelimit.py` :
`OBSIGATE_TOOL_RATE_LIMIT`, `OBSIGATE_TOOL_RATE_LIMIT_PER_TOOL`,
`OBSIGATE_TOOL_RATE_WINDOW`) et **quotas** réutilisant `BOOKSLM_MAX_*`
(`BOOKSLM_MAX_TOOL_CALLS` par run d'agent, `BOOKSLM_MAX_TOOL_READ_BYTES`
pour `read_file`).
### Confirmations MCP — décision : two-step `propose`/`apply`
MCP n'a pas de bouton « Apply ». Mécanisme retenu :
- **Two-step (canonique)** : `propose_*` (non destructif) renvoie un **aperçu/diff** + un **`confirmation_token`** signé (usage unique, TTL) ; `apply_*` exige ce token pour exécuter. Universel (tout client), anti-rejeu/anti-TOCTOU, et **unifie in-app et MCP** (la carte Apply de l'UI est un `propose`→`apply`).
- **Élicitation (optionnelle, plus tard)** : quand le client l'annonce dans ses `capabilities`, afficher la confirmation inline. Fallback two-step sinon.
Les outils destructifs (`delete_*`, `rename_*`, `move_path`, `replace_in_files`) sont **autorisés** (ObsiGate + MCP = outil de gestion des vaults pour des agents), mais encadrés :
- **Confirmation two-step obligatoire** (`propose_*` → `apply_*`, token signé).
- **Backup automatique** avant toute opération destructive (mécanisme existant, `POST /api/file/{vault}/restore`).
- **Audit** systématique (`data/audit.log`).
- **Toggle par vault** pour désactiver les outils destructifs (défaut : activés).
---
## 6. Sélection du fournisseur et des modèles par défaut
Pour répondre au besoin de cibler un fournisseur/modèle sans dépendre uniquement des variables d'environnement, la configuration expose deux paramètres persistés dans `data/config.json` :
| Clé | Type | Défaut | Rôle |
|---|---|---|---|
| `ai_default_provider` | str | `deepseek` | Fournisseur utilisé quand aucun override n'est fourni |
| `ai_default_models` | dict | `{}` | Modèle par défaut par fournisseur (ex. `{"deepseek": "deepseek-chat"}`) |
- Lecture : `backend/ai.py` (`_read_app_config`, `get_default_provider`, `_load_provider_keys`).
- Écriture : `POST /api/config` (admin) — clés ajoutées à `_DEFAULT_CONFIG` (`backend/main.py:4270`).
- Rechargement à chaud : `reload_ai_config()` met à jour `PROVIDERS` **en place** (les imports existants restent valides).
- UI : section « Clés API Intelligence Artificielle » (`frontend/index.html` `#cfg-ai`, cartes dépliables par fournisseur — #104), sélecteurs « Fournisseur par défaut » + « Modèle par défaut », sauvegardés par `saveAIKeys()` (`frontend/js/config.js`).
**Précédence de résolution du modèle** : override par requête > `ai_default_models[provider]` > variable d'environnement `*_MODEL` > défaut codé en dur.
**Précédence du fournisseur** : override par requête > `ai_default_provider` > env `AI_DEFAULT_PROVIDER` > `deepseek`.
---
## 6bis. Skills, commandes, vision & capacités (#81)
- **Skills & commandes `/`** — `backend/skills.py` définit les skills intégrés (id, icône, description,
prompt) et les métadonnées des commandes admin. Les skills utilisateur sont persistés par
utilisateur dans `data/skills.json`. Endpoints : `GET/POST /api/ai/skills`,
`DELETE /api/ai/skills/{id}`. Le champ `skill` d'une requête chat/agent injecte le prompt du skill
dans le system prompt (`_resolve_system_prompt`). Les commandes admin (`/help`, `/providers`,
`/provider`, `/model`, `/keys`) sont exécutées côté client.
- **Contexte ad-hoc `@`** — champs `extra_files` / `extra_directories` sur les requêtes
context/chat/agent ; collecte et fusion par `collect_adhoc_context()` + `merge_contexts()`
(`backend/bookslm.py`).
- **Vision** — les messages peuvent porter un contenu multimodal (tableau OpenAI
`text` + `image_url`). `backend/ai_chat.py` convertit les data URLs en `inlineData` Gemini
(`_content_to_gemini_parts`) ; l'OpenAI-compatible passe le tableau tel quel. Les images viennent
d'un copier-coller (base64) ou d'un fichier de vault (data URL chargée par
`load_vault_image_data_url`). Garde-fou : `_validate_vision_support` rejette (400) une requête
d'image si le modèle n'est pas vision.
- **Capacités** — table statique curée `backend/model_capabilities.py` (8 flags : chat, embeddings,
rerank, images, video, audio_speech, audio_transcription, vision). Exposée par
`GET /api/ai/model-capabilities` et par le champ `capabilities` de `GET /api/config/ai-models` ;
affichée dans le picker de l'assistant et le panneau de configuration.
---
## 7. Plan par phases
| Phase | Contenu | Livrable |
|---|---|---|
| **0 — Fondations** | `backend/tools/` (registry, context, service, audit) + extraction des services métier + tests unitaires | Couche d'outils testable sans IA |
| **1 — Function calling in-app** | Abstraction tool-calling multi-provider, agent loop, confirmations UI, SSE réel, outils de navigation | Assistant qui lit/cherche/lit/ouvre/modifie avec confirmation |
| **2 — Serveur MCP** | `backend/mcp/server.py` (tools + resources + prompts), **Streamable HTTP** (`/mcp`, auth JWT), confirmation two-step | ObsiGate accessible comme serveur MCP (local + distant, multi-utilisateur) |
| **3 — Durcissement** ✅ | Rate limiting (`backend/tools/ratelimit.py`), quotas `BOOKSLM_MAX_*`, redaction systématique des résultats (`backend/tools/redaction.py`), doc OpenAPI (tag/path MCP) + [guide MCP](./GUIDES/MCP.md), tests E2E | Observabilité et sécurité complètes |
Voir `docs/ROADMAP.md` (item dédié) pour le détail des activités.
---
## 8. Décisions
| # | Décision | Statut |
|---|---|---|
| 1 | **Transport MCP** : **Streamable HTTP** (`/mcp` dans FastAPI existant). `stdio` optionnel plus tard pour clients locaux sans MCP distant. | ✅ Décidé (2026-09-11) |
| 2 | **Confirmation MCP** : **two-step `propose`/`apply`** (token signé). Élicitation en bonus quand le client l'annonce. | ✅ Décidé (2026-09-11) |
| 3 | **Périmètre des mutations externes** : **toutes autorisées** via MCP — `create`, `edit`, `rename`, `move`, `delete` (ObsiGate + MCP = outil de gestion vault↔agents). Encadrées par confirmation two-step + backup auto + audit + toggle par vault. | ✅ Décidé (2026-09-11) |
| 4 | **Provider tool-calling** : DeepSeek par défaut → adressé par la sélection du fournisseur/modèle par défaut (§6). | ✅ Résolu |
### Justification
- **HTTP** : ObsiGate est déjà un serveur web avec JWT et permissions par vault ; exposer `/mcp` réutilise l'auth, le multi-utilisateur et l'accès distant. `stdio` imposerait un process séparé dupliquant l'infra pour un usage local mono-utilisateur.
- **Two-step** : universel (aucune dépendance aux capacités du client), permet un diff avant application, token signé à usage unique (anti-rejeu), et cohérent avec le flux in-app existant.
- **Mutations complètes** : l'objectif est de faire d'ObsiGate + MCP la couche de gestion entre les vaults et des agents ; restreindre delete/rename/move limiterait fortement les cas d'usage. La sécurité repose sur la confirmation, le backup automatique, l'audit et le toggle par vault plutôt que sur une interdiction par défaut.
---
## 9. Références
- `backend/ai.py`, `backend/ai_routes.py` — couche fournisseurs + actions éditeur
- `backend/services/` — logique métier partagée (vaults, files, search, backups, graph, recent) consommée par les routes et les outils
- `backend/ai_chat.py` — chat completion provider-agnostique avec tool calling et streaming (OpenAI-compat + Gemini)
- `backend/agent/loop.py` — agent loop in-app (multi-étapes, LLM injectable)
- `backend/mcp/server.py` — serveur MCP (Streamable HTTP `/mcp`, tools/resources/prompts)
- `backend/mcp/confirmations.py` — jetons de confirmation signés (two-step, anti-rejeu)
- `backend/tools/ratelimit.py` — rate limiting par jeton/outil (phase F)
- `backend/tools/redaction.py` — redaction récursive des résultats d'outils (phase F)
- `docs/GUIDES/MCP.md` — guide d'installation et d'utilisation des clients MCP
- `backend/bookslm.py`, `backend/bookslm_routes.py` — assistant contextuel (+ endpoint `/agent`)
- `frontend/js/ai.js`, `frontend/js/bookslm.js` — UI IA
- `backend/auth/middleware.py` — permissions
- `backend/secret_redactor.py` — redaction
- `docs/ROADMAP.md` — suivi des phases