# 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. 2. **2 actions seulement** (`create_file`, `create_directory`) ; ni lecture active, ni recherche, ni édition, ni suppression exposées au modèle. 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) :** ``` 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) ├── search.py # search_vaults (pagination), list_tags, advanced_search_vaults, search_paths ├── backups.py # get_backup_dir, 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` et `/api/search/advanced` en sont de simples wrappers, tout comme les outils correspondants du catalogue (phase C). 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 Nouveau `backend/mcp/server.py` basé sur le SDK MCP Python : - **Tools** : enregistrés depuis le registry (mutations + recherche). - **Resources** : vaults/fichiers en lecture (`vault://name/path.md`). - **Prompts** : templates (résumer un dossier, générer une note…). - Transports : **stdio** (local) et **HTTP/SSE** (distant, auth JWT). --- ## 4. Catalogue des outils Légende : **R** = lecture (auto), **M** = mutation (confirmation), **N** = navigation (in-app uniquement). ### Vaults & navigation | Outil | Type | Paramètres | Endpoint source | |---|---|---|---| | `list_vaults` | R | — | `GET /api/vaults` (`main.py:1267`) | | `list_directory` | R | `vault`, `path` | `GET /api/browse/{vault}` (`main.py:1503`) | | `list_all_files` | R | `vault` | `GET /api/vault/{vault}/files` (`main.py:3883`) | | `open_file` | N | `vault`, `path` | événement front `obsigate:open-file` | | `reveal_in_tree` | N | `vault`, `path` | UI sidebar | ### Lecture de contenu | Outil | Type | Paramètres | Endpoint source | |---|---|---|---| | `read_file` | R | `vault`, `path` | `GET /api/file/{vault}` (`main.py:2722`) | | `read_file_raw` | R | `vault`, `path` | `GET /api/file/{vault}/raw` (`main.py:1589`) | | `get_backlinks` | R | `vault`, `path` | `GET /api/file/{vault}/backlinks` (`main.py:2683`) | | `list_backups` | R | `vault`, `path` | `GET /api/file/{vault}/backups` (`main.py:2476`) | | `diff_backup` | R | `vault`, `path` | `GET /api/file/{vault}/diff` (`main.py:2513`) | | `get_graph` | R | `vault` | `GET /api/graph/{vault}` (`main.py:3390`) | ### Recherche | Outil | Type | Paramètres | Endpoint source | |---|---|---|---| | `search_fulltext` | R | `q`, `vault`, `tag`, `limit` | `GET /api/search` (`main.py:3097`) | | `search_advanced` | R | `q`, `vault`, filtres, `regex`… | `GET /api/search/advanced` (`main.py:3195`) | | `search_paths` | R | `q`, `vault` | `GET /api/tree-search` (`main.py:3151`) | | `list_tags` | R | `vault` | `GET /api/tags` (`main.py:3137`) | | `suggest_tags` | R | `q` | `GET /api/tags/suggest` (`main.py:3352`) | | `list_recent` | R | `vault` | `GET /api/recent` (`main.py:1301`) | ### Création / modification | Outil | Type | Paramètres | Endpoint source | |---|---|---|---| | `create_file` | M | `vault`, `path`, `content` | `POST /api/file/{vault}` (`main.py:2134`) | | `create_directory` | M | `vault`, `path` | `POST /api/directory/{vault}` (`main.py:1921`) | | `edit_file` | M | `vault`, `path`, `content` | `PUT /api/file/{vault}/save` (`main.py:1796`) | | `append_to_file` | M | `vault`, `path`, `content` | dérivé de save | | `rename_file` | M | `vault`, `path`, `new_name` | `PATCH /api/file/{vault}` (`main.py:2207`) | | `rename_directory` | M | `vault`, `path`, `new_name` | `PATCH /api/directory/{vault}` (`main.py:1995`) | | `move_path` | M | `vault`, `src`, `dst` | `POST /api/move/{vault}` (`main.py:2289`) | | `replace_in_files` | M ⚠️ | `vault`, `find`, `replace`, filtres | `POST /api/search/replace` (`main.py:3241`) | ### Suppression (confirmation renforcée) | Outil | Type | Paramètres | Endpoint source | |---|---|---|---| | `delete_file` | M ⚠️ | `vault`, `path` | `DELETE /api/file/{vault}` (`main.py:1851`) | | `delete_directory` | M ⚠️ | `vault`, `path`, `recursive` | `DELETE /api/directory/{vault}` (`main.py:2068`) | | `restore_backup` | M | `vault`, `path` | `POST /api/file/{vault}/restore` (`main.py:2601`) | ### Partage (optionnel, admin) | Outil | Type | Paramètres | Endpoint source | |---|---|---|---| | `create_share` | M | `vault`, `path`, options | `POST /api/share/{vault}` (`main.py:4719`) | | `list_shares` | R | — | — | --- ## 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()`. 5. Journalisation dans l'audit (`data/audit.log`). 6. Rate limiting par token/outil ; quotas réutilisant `BOOKSLM_MAX_*`. ### 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`), 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`. --- ## 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, quotas, redaction, tests, doc OpenAPI/MCP | 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/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