# #92 — Assistant IA — Écosystème d'outils : feuille de route technique > **Statut :** ✅ livré (phase 2, version 2.10.0) — phase 1 livrée dans #91 > **Effort estimé :** 3-5 jours pour la phase 2 | **Impact :** 🟠 > **Références :** [Roadmap](../ROADMAP.md) · [Outils & MCP #79](./ai-tools-mcp.md) · > [Fenêtre de discussion #91](./ai-assistant-conversation-ux.md) · [Changelog](../../CHANGELOG.md) ## 1. Ce qui existe déjà (phase 1, #91) | Élément | État | |---|---| | Registre d'outils typé (`@tool`, Pydantic, risque, scope, rate-limit, audit) | ✅ `backend/tools/registry.py` | | Boucle agent multi-tours + confirmation en deux étapes | ✅ `backend/agent/loop.py` | | Section « N étapes » Notion-style avec libellés humains + événements SSE en direct | ✅ `backend/tools/labels.py` | | Recherche web auto-hébergée (`web_search` via SearXNG) | ✅ `backend/tools/web.py` | | Lecture d'une page publique (`fetch_url`, garde SSRF, HTML→texte) | ✅ `backend/tools/web.py` | | Outils vault (lecture, recherche, navigation, mutations confirmées) | ✅ `backend/tools/service.py` (26 outils) | ## 2. Frameworks d'agents — décision ObsiGate **ne migre pas** vers un framework externe : la boucle maison (`run_agent`) est déjà testée, typée, auditable et intégrée au flux SSE (critique pour la section « steps » en direct). Les candidats restent suivis : | Framework | Point fort | Verdict pour ObsiGate | |---|---|---| | **Pydantic AI** | validation type-safe, cœur léger | inspirant : la validation existe déjà via Pydantic dans `registry.call_tool` | | **LangChain / LangGraph** | écosystème, workflows complexes | dépendance lourde ; à reconsidérer seulement si multi-agents | | **LlamaIndex** | RAG clé en main | la recherche locale (TF-IDF + embeddings, #70) couvre déjà le besoin | | **SmolAgents** | agents qui écrivent du code | risque d'exécution : hors périmètre | | **LightAgent** | ultra-léger, mémoire | redondant avec la mémoire de session déjà fournie | **Règle :** tout nouvel outil = `@tool` dans `backend/tools/` + un libellé dans `labels.py` + deux clés i18n (`ai.step.` FR/EN) + un test. Aucune réécriture de la boucle n'est nécessaire. ## 3. Phase 2 — catégories à implémenter > **Livré (2.10.0, #92).** Récapitulatif des décisions finales : | Catégorie | Décision livrée | |---|---| | Recherche web étendue | Tavily, Brave, SerpAPI, Exa à clé (`OBSIGATE_*_API_KEY`), essayés avant SearXNG ; ordre via `OBSIGATE_WEB_PROVIDERS` | | Lecture de pages | `fetch_url(render=True)` → worker Playwright isolé (`backend/tools/webrender.py`), dépendance optionnelle + erreur explicite | | Crawl multi-pages | `crawl_site` (WRITE + confirmation) : BFS httpx borné (≤ 20 pages, même hôte, SSRF sur chaque URL) → condensé Markdown dans le vault. Scrapy écarté (dépendance lourde inutile à cette échelle) | | Sources connectées | Gitea + GitHub (`git_list_repos`, `git_search_issues`, `git_get_file`) via env/Infisical ; drives cloud (Drive/OneDrive) orientés serveur MCP externe (#79). **#103 (2.11.0)** : les clés (URL Gitea, tokens Gitea/GitHub, clés Tavily/Brave/SerpAPI/Exa) se saisissent aussi dans la page Configurations — `backend/tools/secrets.py`, valeur stockée prioritaire sur l'env | | Production de documents | `create_xlsx`, `create_docx`, `create_csv`, `create_pdf` — WRITE + confirmation, écrit via `save_raw_file(allow_docs=True)` (path safety + backup) | | Transverse | Cache SQLite (`webcache.py`, TTL `OBSIGATE_WEB_CACHE_TTL`), retry backoff maison (`OBSIGATE_WEB_RETRY`), secrets par env (Infisical-compatible) | ### 3.1 Recherche web étendue (`web_search`) - **Fallback sans clé — ✅ livré (BUG-051)** : chaîne de fournisseurs dans `backend/tools/web.py` — SearXNG auto-hébergé (`OBSIGATE_SEARXNG_URL`) puis, si aucun résultat, DuckDuckGo (`html.duckduckgo.com/html/`) puis Bing (`www.bing.com/search`). Le premier fournisseur non vide est retenu et exposé (`provider`) ; replis désactivables via `OBSIGATE_WEB_FALLBACK=0`. - **Fournisseurs optionnels** (clé dans Infisical, jamais en dur) : Tavily (résultats orientés agents), Brave Search API, SerpAPI (Google), Exa. Interface unifiée type `anysearch` pour un sélecteur de fournisseur unique. - **Paramètres déjà exposés** : `category` (general/news/it/science), `language`, `page`, `max_results`. ### 3.2 Lecture de pages (`fetch_url`) - Pages **statiques** : couvert (httpx + extraction texte maison). - Pages **dynamiques (SPA/React)** : `playwright` (async) en option, exécuté dans un worker isolé (jamais dans le process web) — images Docker lourdes à prévoir. - **Crawl multi-pages** : `scrapy` uniquement en tâche de fond, jamais déclenché par le modèle sans confirmation (WRITE/`confirm`). - Alternative légère de parsing : `lxml` ou `beautifulsoup4` si l'extraction maison devient insuffisante (aujourd'hui volontairement sans dépendance). ### 3.3 Sources connectées (« Searched connected sources ») Chaque intégration reste **derrière le registre** (risque, scope, rate-limit, audit) et **jamais** avec un token en dur : - **Gitea / GitHub** : `httpx` direct (déjà utilisé) ou `gitea-sdk` / `PyGithub`. Priorité haute : ObsiGate est hébergé sur Gitea (issues, PR, commits). - **Google Drive / Gmail / Calendar** : `google-api-python-client` (+ `PyDrive4` pour les tâches simples). OAuth2 via `authlib`. - **OneDrive / SharePoint** : `onedrive-personal-sdk` (async, Microsoft Graph). - **Notion / Slack / Jira / Confluence / Salesforce** : SDK officiels ou `httpx` + OAuth2 générique. - **Voie recommandée** : privilégier le **serveur MCP externe** (#79) pour les services tiers — l'attaque surface reste hors du cœur d'ObsiGate. ### 3.4 Fichiers et production de documents - Conversion de formats, tableurs (`openpyxl`), documents (`python-docx`, `pypdf`), graphiques (`matplotlib`) : outils **WRITE** (confirmation obligatoire), exécutés hors requête web si lourd (tâche de fond + SSE). - Pièces jointes : déjà couvert par l'upload vault + `python-multipart`. ### 3.5 Raisonnement interne (« thought », « planned the task ») - Livré : la note intermédiaire du modèle devient une étape visible. - Extension possible : exposer les itérations de la boucle (`iterations`) comme étapes de planification quand un outil de plan est ajouté. - **Garantie de réponse finale — ✅ livré (BUG-052)** : à l'épuisement du budget d'itérations ou du quota d'appels d'outils, `_finalize_answer` déclenche un dernier appel LLM **sans outil** (instruction de synthèse) ; un repli déterministe liste les sources si cet appel échoue. Une recherche web ne peut plus se terminer sur une conversation sans texte. ## 4. Transverse — à faire avec la phase 2 | Sujet | Bibliothèque / approche | Où | |---|---|---| | Réessais avec backoff | `tenacity` (ou boucle maison) | appels réseau des outils | | Cache des résultats web | table SQLite dédiée + TTL (`OBSIGATE_WEB_CACHE_TTL`) | `backend/tools/web.py` | | Rate limiting | déjà en place (`backend/tools/ratelimit.py`) | registre | | Secrets | Infisical / variables d'environnement | jamais en dur | | Async | `httpx` (async) pour ne pas bloquer la boucle | nouveaux outils réseau | | Fallback | chaque outil externe doit **échouer proprement** (`ToolError` + message) | registre | | Observabilité | audit déjà en place (`backend/tools/audit.py`) | registre | ## 5. Points d'attention - **Sécurité d'abord** : tout outil réseau passe par la garde SSRF (`_assert_public_http_url`), des limites de taille, et n'est jamais exposé sans rate-limit. - **Coût** : chaque outil supplémentaire augmente le prompt système (schémas). Mesurer (`/api/ai/status`) et exposer les outils par lots si besoin. - **Testabilité** : les outils réseau se testent avec `httpx` mocké (voir `tests/test_web_tools.py`), jamais contre Internet en CI. - **Une seule source d'affichage** : ne pas dupliquer les étapes côté frontend — le libellé vient du backend (`labels.py`), la traduction du locale.