23 KiB
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
- Un assistant in-app capable de lire, chercher, lister, ouvrir et modifier, via function calling natif.
- Un serveur MCP exposant ObsiGate aux clients externes (Claude Desktop, Cursor…).
- 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_PROMPTbackend/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-94DEFAULT_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
- 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 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). - SSE :
non réellement streaming→ corrigé (B4) :ai_chat.stream_completionalimente/api/ai/bookslm/chattoken par token ; le tool-calling (/agent) reste non-streaming (les appels d'outils exigent la réponse complète). PROVIDERSchargé 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__.pysuivi —.gitignoreexclut_*.py). La façadeapi.pyjoue 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) ;/agentreste 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 outilsreadsont exposés directement ; les outilswrite/dangerousle sont via la pairepropose_<tool>/apply_<tool>(jeton signé, usage unique, TTL). - Resources :
vault://<name>(vaults accessibles) etvault://<name>/<path>(fichiers, lecture seule, secrets redactés). - Prompts :
summarize-directory,generate-note,find-related. - Transport : Streamable HTTP (
/mcp, SDKStreamableHTTPSessionManagerenjson_response=True), authAuthorization: Bearer <JWT>→get_current_user.stdiooptionnel 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 pardocker 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).
5. Sécurité & permissions
Tout outil reçoit un ToolContext et applique systématiquement :
check_vault_access(vault, user)—backend/auth/middleware.py:87_resolve_safe_path(vault_root, path)—backend/main.py:867- Confirmation utilisateur pour
risk in (write, dangerous)(UI in-app : carte Apply ; MCP : two-steppropose/apply). - Redaction des secrets avant tout envoi au LLM —
redact_file_content()puisbackend/tools/redaction.pysur tout résultat d'outil (diffs, extraits de recherche). - Journalisation dans l'audit (
data/audit.log). - 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éutilisantBOOKSLM_MAX_*(BOOKSLM_MAX_TOOL_CALLSpar run d'agent,BOOKSLM_MAX_TOOL_READ_BYTESpourread_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 + unconfirmation_tokensigné (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 unpropose→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 à jourPROVIDERSen 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 parsaveAIKeys()(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.pydé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 dansdata/skills.json. Endpoints :GET/POST /api/ai/skills,DELETE /api/ai/skills/{id}. Le champskilld'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
@— champsextra_files/extra_directoriessur les requêtes context/chat/agent ; collecte et fusion parcollect_adhoc_context()+merge_contexts()(backend/bookslm.py). - Vision — les messages peuvent porter un contenu multimodal (tableau OpenAI
text+image_url).backend/ai_chat.pyconvertit les data URLs eninlineDataGemini (_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 parload_vault_image_data_url). Garde-fou :_validate_vision_supportrejette (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 parGET /api/ai/model-capabilitieset par le champcapabilitiesdeGET /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, 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
/mcpréutilise l'auth, le multi-utilisateur et l'accès distant.stdioimposerait 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 éditeurbackend/services/— logique métier partagée (vaults, files, search, backups, graph, recent) consommée par les routes et les outilsbackend/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/MCP_GUIDE.md— guide d'installation et d'utilisation des clients MCPbackend/bookslm.py,backend/bookslm_routes.py— assistant contextuel (+ endpoint/agent)frontend/js/ai.js,frontend/js/bookslm.js— UI IAbackend/auth/middleware.py— permissionsbackend/secret_redactor.py— redactiondocs/ROADMAP.md— suivi des phases