diff --git a/CHANGELOG.md b/CHANGELOG.md index 36577ec..72753d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/), et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). > **En cours de développement** : les changements à venir sont listés dans la section -> [Unreleased](#unreleased). La dernière version livrée est **2.7.3**. +> [Unreleased](#unreleased). La dernière version livrée est **2.7.4**. --- @@ -14,6 +14,22 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [2.7.4] — 2026-09-17 + +### Corrigé + +- **BUG-053 - Assistant IA (mode agent) : le fichier demandé n'est pas créé** : le prompt + système du mode Général (et du dossier vide) enseignait encore le **protocole texte** + `obsigate-action`, si bien que le modèle décrivait l'action dans un bloc texte au lieu + d'appeler l'outil natif `create_file` — rien n'était donc créé (et le bloc, volumineux, + était tronqué avant sa fermeture). En mode agent, le prompt demande désormais d'appeler + directement les outils natifs (`create_file`, `create_directory`, …) et interdit les blocs + `obsigate-action` ; le chat classique conserve le protocole texte. La limite de sortie de + l'agent passe à 8 192 jetons pour laisser place au contenu complet d'un fichier. + Fichiers : `backend/bookslm.py`, `backend/bookslm_routes.py`. Tests : `tests/test_bookslm.py` (+3). + +--- + ## [2.7.3] — 2026-09-16 ### Corrigé diff --git a/README.fr.md b/README.fr.md index 9137722..8225693 100644 --- a/README.fr.md +++ b/README.fr.md @@ -4,7 +4,7 @@ **Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive. -[![Version](https://img.shields.io/badge/Version-2.7.3-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.7.4-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -916,8 +916,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l ## 📝 Changelog -Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.7.3). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.7.4). --- -*Projet : ObsiGate | Version : 2.7.3 | Dernière mise à jour : Juin 2026* +*Projet : ObsiGate | Version : 2.7.4 | Dernière mise à jour : Juin 2026* diff --git a/README.md b/README.md index 3365f22..ddf63bc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface. -[![Version](https://img.shields.io/badge/Version-2.7.3-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.7.4-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -1085,8 +1085,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) ## 📝 Changelog -See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.7.3). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.7.4). --- -*Project: ObsiGate | Version: 2.7.3 | Last updated: May 2026* +*Project: ObsiGate | Version: 2.7.4 | Last updated: May 2026* diff --git a/VERSION b/VERSION index 2c9b4ef..a4dd9db 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.7.3 +2.7.4 diff --git a/backend/bookslm.py b/backend/bookslm.py index 6045138..0b57126 100644 --- a/backend/bookslm.py +++ b/backend/bookslm.py @@ -486,12 +486,17 @@ def build_system_prompt(context: dict[str, Any], scope: str = "directory", vault return prompt -GENERAL_SYSTEM_PROMPT = """Tu es l'assistant intégré d'ObsiGate, une application web auto-hébergée pour consulter, rechercher et éditer des vaults Obsidian (Markdown). +GENERAL_SYSTEM_HEADER = """Tu es l'assistant intégré d'ObsiGate, une application web auto-hébergée pour consulter, rechercher et éditer des vaults Obsidian (Markdown). Tes deux rôles : 1. **Aider sur l'application** : expliquer la navigation, la recherche (full-text, filtres `tag:`, `created:`, `path:`), l'éditeur (CodeMirror, autosave, raccourcis), les onglets et le split view, les sauvegardes et la restauration, le partage public, l'export (HTML/Markdown/ePub/PDF), Mermaid, Excalidraw, les plugins, les thèmes, le mode hors-ligne, le MFA, etc. 2. **Proposer des actions concrètes** : créer un fichier ou un dossier dans un vault. +""" +# Text action protocol — used by the classic (non-agent) chat endpoint, where +# the model has no native tool calling; the frontend turns each block into a +# clickable “Apply” card. +GENERAL_ACTION_TEXT_PROTOCOL = """ Quand l'utilisateur demande explicitement de créer un fichier, inclus un bloc de ce type dans ta réponse (un bloc par fichier, et rien d'autre à l'intérieur du bloc) : ```obsigate-action @@ -513,6 +518,25 @@ Règles : - Réponds dans la langue de l'utilisateur, de façon concise et structurée (Markdown). """ +# Agent mode: the model has native tools, so it must call them (function +# calling) instead of emitting the text `obsigate-action` blocks — otherwise +# the requested file is never created (BUG-053). +GENERAL_ACTION_TOOL_PROTOCOL = """ +Tu disposes d'outils natifs (function calling) pour lire, chercher et modifier les vaults : `create_file`, `create_directory`, `append_to_file`, `edit_file`, `read_file`, `search_fulltext`, etc. + +Quand l'utilisateur demande explicitement de créer un fichier, **appelle directement l'outil `create_file`** avec `{"vault": "", "path": "", "content": ""}`. Pour créer un dossier, appelle `create_directory`. + +Règles : +- N'écris **jamais** de bloc ```obsigate-action``` : en mode agent, toutes les actions passent par les outils natifs. +- Écris le contenu **complet** demandé dans l'argument `content` (ne le tronque pas, pas de « … » ni de ligne omise). +- Pour créer un fichier dans un nouveau dossier, un seul appel `create_file` avec le chemin complet suffit (les dossiers parents sont créés automatiquement). +- N'invente jamais un nom de vault : utilise l'un des vaults disponibles listés ci-dessous. +- Réponds dans la langue de l'utilisateur, de façon concise et structurée (Markdown). +""" + +# Backwards-compatible alias (classic chat prompt). +GENERAL_SYSTEM_PROMPT = GENERAL_SYSTEM_HEADER + GENERAL_ACTION_TEXT_PROTOCOL + def _format_app_context(app_context: dict[str, Any] | None, recent_files: list[dict[str, Any]] | None) -> str: """Render the live application state for the General assistant prompt. @@ -588,14 +612,20 @@ def build_general_system_prompt( vaults: list[str] | None = None, app_context: dict[str, Any] | None = None, recent_files: list[dict[str, Any]] | None = None, + agent: bool = False, ) -> str: """System prompt for the General assistant (app help + actions). ``app_context`` carries the live UI state (open documents, current directory, active search) and ``recent_files`` the last modified files, so the assistant knows what the user is doing rather than answering blind. + + ``agent`` selects the action protocol: the classic chat endpoint (no native + tools) uses the text ``obsigate-action`` blocks, while the tool-calling + agent endpoint must invoke the native tools instead (BUG-053). """ - prompt = GENERAL_SYSTEM_PROMPT + protocol = GENERAL_ACTION_TOOL_PROTOCOL if agent else GENERAL_ACTION_TEXT_PROTOCOL + prompt = GENERAL_SYSTEM_HEADER + protocol if vaults: prompt += "\nVaults disponibles : " + ", ".join(sorted(vaults)) + "\n" else: diff --git a/backend/bookslm_routes.py b/backend/bookslm_routes.py index f2b60c4..7b780fc 100644 --- a/backend/bookslm_routes.py +++ b/backend/bookslm_routes.py @@ -193,10 +193,12 @@ def _recent_files_for_prompt(current_user, limit: int = 10) -> list[dict[str, An return [] -def _resolve_system_prompt(req, current_user) -> str: +def _resolve_system_prompt(req, current_user, agent: bool = False) -> str: """Resolve the vault access and build the assistant system prompt. Shared by the classic chat endpoint and the tool-calling agent endpoint. + ``agent=True`` selects the native-tool action protocol (no text + ``obsigate-action`` blocks) for the General/empty-directory prompts. """ mode = _normalize_mode(req.mode) vault_path: Path | None = None @@ -222,6 +224,7 @@ def _resolve_system_prompt(req, current_user) -> str: list(index.keys()), app_context=_submitted_app_context(req), recent_files=_recent_files_for_prompt(current_user), + agent=agent, ) elif effective_mode == "documents": prompt = build_system_prompt(context, scope="documents", vault_name=req.vault) @@ -233,6 +236,7 @@ def _resolve_system_prompt(req, current_user) -> str: list(index.keys()), app_context=_submitted_app_context(req), recent_files=_recent_files_for_prompt(current_user), + agent=agent, ) prompt += ( f"\n## Dossier vide\nLe dossier « {req.directory or '/'} » " @@ -243,6 +247,14 @@ def _resolve_system_prompt(req, current_user) -> str: else: prompt = build_system_prompt(context, scope="directory", vault_name=req.vault) + if agent and effective_mode != "general" and context["file_count"] > 0: + prompt += ( + "\n## Mode agent\n" + "Utilise les outils natifs (function calling) pour agir sur les fichiers " + "(`create_file`, `create_directory`, `append_to_file`, `edit_file`, …). " + "N'écris jamais de bloc ```obsigate-action```." + ) + skill_id = getattr(req, "skill", None) if skill_id: skill_prompt = get_skill_prompt(skill_id, current_user) @@ -499,7 +511,7 @@ async def api_bookslm_agent( ``confirm`` / ``confirm_messages``. """ _validate_vision_support(req) - system_prompt = _resolve_system_prompt(req, current_user) + system_prompt = _resolve_system_prompt(req, current_user, agent=True) vault_path = _resolve_optional_vault_path(req, current_user) messages: list[dict] = [{"role": "system", "content": system_prompt}] @@ -519,7 +531,9 @@ async def api_bookslm_agent( provider=req.provider, model=req.model, temperature=0.3, - max_tokens=4096, + # Tool-call arguments can carry a whole file body (e.g. a generated + # table): leave more room than the plain-chat default. + max_tokens=8192, ) async def generate_sse(): diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index 9537364..f7c2152 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.7.3" +version = "2.7.4" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 85e4ab5..e4f5090 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.7.3" +version = "2.7.4" description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian" authors = ["Bruno Charest"] edition = "2021" diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index c2df39a..d3987c5 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json", "productName": "ObsiGate", - "version": "2.7.3", + "version": "2.7.4", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/ISSUES_TODOLIST.md b/docs/ISSUES_TODOLIST.md index 348933f..636d923 100644 --- a/docs/ISSUES_TODOLIST.md +++ b/docs/ISSUES_TODOLIST.md @@ -162,6 +162,7 @@ Avant de corriger quoi que ce soit, un agent IA doit : | *BUG-050* | [🟡 IMPORTANT] Assistant IA : échec de la création d'un sous-dossier contenant un fichier (appels d'outils parallèles + confirmation) | 🟢 corrigé | P1 | ⚙️ backend + 🤖 ia | IA | `backend/agent/loop.py`, `backend/services/mutations.py`, `backend/tools/service.py`, `backend/bookslm.py` | Mode Agent : « crée le dossier X et un fichier Y dedans » puis Appliquer | `backend/agent/loop.py` : résultats « deferred » (`_deferred_tool_message`) pour les `tool_calls` non atteints lors d'une pause de confirmation ; `backend/services/mutations.py` : `create_directory(..., exist_ok=True)` ; `backend/tools/service.py` + `backend/bookslm.py` : consignes `create_file` (parents auto-créés, chemin imbriqué unique) | Cause : le message assistant listait plusieurs `tool_calls` mais la pause n'ajoutait le résultat que du seul appel confirmé → conversation invalide (tool_call_id sans réponse) au resume. Tests : `tests/test_agent_loop.py` (+1), `tests/test_tools_mutations.py` (+1), `tests/test_api_main.py` (+1) | | *BUG-051* | [🟡 IMPORTANT] Assistant IA : la recherche web répond toujours « je ne peux pas accéder à internet » (mode agent ou non) | 🟢 corrigé | P1 | ⚙️ backend + 🤖 ia | IA | `backend/tools/web.py`, `tests/test_web_tools.py` | Assistant : « fais une recherche sur l'horaire du Canadien de Montréal 2026-2027 » | `backend/tools/web.py` : chaîne de repli sans clé — SearXNG puis DuckDuckGo (HTML sans JS) puis Bing (HTML), premier fournisseur non vide retenu (`provider`), replis désactivables via `OBSIGATE_WEB_FALLBACK=0` | Cause : l'instance SearXNG par défaut (`search.dracodev.net`) remonte 0 résultat (moteurs amont suspendus/CAPTCHA) → le modèle en déduisait une absence d'accès réseau. Tests : `tests/test_web_tools.py` (+4) | | *BUG-052* | [🟡 IMPORTANT] Assistant IA : recherche web sans réponse finale (10 étapes + sources affichées, aucun texte dans la conversation) | 🟢 corrigé | P1 | ⚙️ backend + 🤖 ia | IA | `backend/agent/loop.py`, `tests/test_agent_loop.py` | Assistant (mode agent) : recherche web qui enchaîne 10 étapes puis n'affiche aucune réponse | `backend/agent/loop.py` : `_finalize_answer` — dernier appel LLM sans outil (instruction de synthèse) quand le budget d'itérations/quota est épuisé, repli déterministe `_fallback_summary` (liste des sources), résultats `deferred` pour les appels non atteints du lot en quota | Cause : `content=""` renvoyé sur `STOP_MAX_ITERATIONS`/`STOP_QUOTA_EXCEEDED` alors que le modèle appelait encore des outils. Tests : `tests/test_agent_loop.py` (+2) | +| *BUG-053* | [🟡 IMPORTANT] Assistant IA (mode agent) : le fichier demandé n'est pas créé — le modèle émet un bloc texte `obsigate-action` au lieu d'appeler l'outil `create_file` | 🟢 corrigé | P1 | ⚙️ backend + 🤖 ia | IA | `backend/bookslm.py`, `backend/bookslm_routes.py`, `tests/test_bookslm.py` | Mode agent, contexte Général (ou dossier vide) : « créer le fichier TestVault/sport/… avec le tableau des 84 matchs » → réponse avec un bloc ```obsigate-action``` tronqué, aucun fichier | `backend/bookslm.py` : protocole d'action scindé — `GENERAL_ACTION_TOOL_PROTOCOL` (outils natifs, interdiction des blocs `obsigate-action`) utilisé quand `agent=True`, protocole texte conservé pour le chat classique ; `backend/bookslm_routes.py` : `_resolve_system_prompt(..., agent=True)` depuis l'endpoint agent + règle « Mode agent » pour les prompts dossier/documents, `max_tokens` agent 4096 → 8192 (contenu de fichier complet) | Cause : le prompt Général enseignait encore le protocole texte alors que l'agent dispose du function calling. Tests : `tests/test_bookslm.py` (+3) | | | | | | | | | | | | | ### TODOs techniques (améliorations / nouvelles tâches) @@ -215,6 +216,7 @@ Avant de corriger quoi que ce soit, un agent IA doit : | 2026-09-16 | BUG-051 | Correction | `backend/tools/web.py`, `tests/test_web_tools.py`, `docs/features/ai-tools-roadmap.md`, `docs/features/ai-assistant-conversation-ux.md`, `docs/ROADMAP.md`, `CHANGELOG.md` | **BUG-051** : `web_search` ne dépend plus d'une seule instance SearXNG. Nouvelle chaîne de fournisseurs (`_provider_chain`) : SearXNG (auto-hébergé, JSON) → DuckDuckGo (`html.duckduckgo.com/html/`, extraction `result__a`/`result__snippet`, décodage du lien `uddg=`) → Bing (`www.bing.com/search`, extraction `h2 > a` + `p.b_lineclamp*`, décodage de la redirection `u=a1`), UA navigateur, premier fournisseur non vide retenu et exposé (`provider`). Le champ `warning` final liste les fournisseurs essayés ; replis désactivables via `OBSIGATE_WEB_FALLBACK=0` ; erreur `web_search_unavailable` uniquement si tous les fournisseurs sont injoignables. Vérifié : pytest 1082 passed / 6 skipped (14 erreurs MCP préexistantes, sans lien), ruff 0 (backend), mypy 0 (`backend/tools/web.py`), `tests/test_web_tools.py` 13/13, recherche live Bing (horaire Canadiens) sur l'hôte. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-16 | BUG-051 (complément) | Correction | `backend/tools/web.py`, `CHANGELOG.md` | **BUG-051** suite : un `User-Agent` navigateur seul ne suffit pas — Bing renvoie une SERP factice (résultats sans rapport, ex. « highest paying jobs » / « Sam Reid ») aux requêtes sans en-têtes de navigation. Ajout de `BROWSER_HEADERS` (`Accept-Language`, `Sec-Fetch-*`, `Upgrade-Insecure-Requests`) pour DuckDuckGo et Bing. Vérifié **en conteneur** (`obsigate-test`, v2.7.1) : `web_search('Canadien de Montreal horaire matchs 2026 2027')` → `provider: bing`, 5 résultats pertinents (nhl.com/fr/canadiens, rds.ca, fr.wikipedia.org). | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-16 | BUG-052 | Correction | `backend/agent/loop.py`, `tests/test_agent_loop.py`, `CHANGELOG.md` | **BUG-052** : la boucle d'agent ne rendait plus jamais de réponse vide. `_finalize_answer` : à l'épuisement du budget d'itérations (`STOP_MAX_ITERATIONS`) ou du quota d'appels (`STOP_QUOTA_EXCEEDED`), un dernier appel LLM **sans outil** reçoit une instruction de synthèse (« N'appelle plus aucun outil. Réponds maintenant… ») et son texte devient la réponse ; si l'appel échoue ou reste vide, `_fallback_summary` compose une liste déterministe des sources (`web_search`/`fetch_url`) pour ne jamais renvoyer un tour vide. Les `tool_calls` non atteints lors d'un arrêt sur quota reçoivent un résultat `deferred` (conversation valide pour la synthèse). Vérifié : `tests/test_agent_loop.py` 16/16 (+2 : synthèse finale, repli sources), suite complète 1084 passed / 6 skipped (14 erreurs MCP préexistantes), ruff 0 (backend), mypy 0 (`backend/agent/loop.py`). | 🟢 corrigé (en attente vérif utilisateur) | +| 2026-09-16 | BUG-053 | Correction | `backend/bookslm.py`, `backend/bookslm_routes.py`, `tests/test_bookslm.py`, `CHANGELOG.md` | **BUG-053** : en mode agent, le prompt Général (et dossier vide) enseignait le protocole texte `obsigate-action` ; le modèle décrivait donc l'action au lieu d'appeler l'outil natif `create_file` (bloc volumineux de surcroît tronqué avant fermeture → aucun fichier créé). Le prompt est scindé : `GENERAL_ACTION_TOOL_PROTOCOL` (appel direct des outils natifs, interdiction explicite des blocs `obsigate-action`) pour `build_general_system_prompt(agent=True)`, le protocole texte restant utilisé par le chat classique ; `_resolve_system_prompt` propage `agent` et ajoute une règle « Mode agent » aux prompts dossier/documents ; `max_tokens` de l'agent porté à 8192 pour un contenu de fichier complet. Vérifié : `tests/test_bookslm.py` 68/68 (+3 : prompt agent sans protocole texte, prompt classique inchangé, prompt système de l'endpoint agent), suite complète 1087 passed / 6 skipped (14 erreurs MCP préexistantes), ruff 0 (backend), mypy 0 (`backend/bookslm.py`, `backend/bookslm_routes.py`). | 🟢 corrigé (en attente vérif utilisateur) | --- diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 987736d..e4ee5bf 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.7.3 | **Dernière mise à jour :** 2026-09-16 +> **Version :** 2.7.4 | **Dernière mise à jour :** 2026-09-17 > **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact > vers les fonctionnalités livrées. > - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)** diff --git a/docs/features/ai-tools-mcp.md b/docs/features/ai-tools-mcp.md index 80df9a2..f21a67f 100644 --- a/docs/features/ai-tools-mcp.md +++ b/docs/features/ai-tools-mcp.md @@ -17,7 +17,7 @@ ## B. Function calling in-app (3-4 jours) — ✅ livré (2026-09-11) - [x] **B1.** Abstraction tool-calling provider-agnostique : `backend/ai_chat.py` (`chat_completion`, `ToolCall`, `LLMResponse`) — OpenAI-compat (`tools`/`tool_choice`, parsing `tool_calls`) + Gemini (`functionDeclarations`/`functionCall`) - [x] **B2.** Agent loop `backend/agent/loop.py` : boucle tool→résultat→tool, limite d'itérations (10), truncation des résultats ; endpoint opt-in `POST /api/ai/bookslm/agent` (events SSE `tool`/`message`/`confirmation`) - - [x] **B3.** Fallback : retry sans `tools` si le provider rejette les tools (400/404/422) → chat simple ; protocole texte `obsigate-action` conservé côté frontend + - [x] **B3.** Fallback : retry sans `tools` si le provider rejette les tools (400/404/422) → chat simple ; protocole texte `obsigate-action` conservé côté frontend **pour le chat classique uniquement** (BUG-053 : en mode agent, le prompt impose les outils natifs et interdit les blocs `obsigate-action`) - [x] **B4.** SSE réellement streaming — `ai_chat.stream_completion` (`_openai_stream` + `_gemini_stream`) alimente `/api/ai/bookslm/chat` token par token ; le middleware GZip laisse passer les endpoints SSE BooksLM. - [x] **B5.** Confirmations UI : toggle « mode agent » (front → `/agent`), événements `tool`/`confirmation`, carte Apply + aperçu diff (LCS) pour les mutations, reprise `confirm`/`confirm_messages` côté backend. *S'active dès que la phase D enregistre des outils `write`.* - [x] **B6.** Outils de navigation in-app : `open_file`, `reveal_in_tree` (événement `obsigate:open-file`) — livré via les liens cliquables de l'assistant (#80, [ai-assistant-ux.md](./ai-assistant-ux.md)) diff --git a/package.json b/package.json index 7db1344..29d6d0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsigate", - "version": "2.7.3", + "version": "2.7.4", "description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.", "main": "patch.js", "directories": { diff --git a/tests/test_bookslm.py b/tests/test_bookslm.py index 8c0a803..b4de409 100644 --- a/tests/test_bookslm.py +++ b/tests/test_bookslm.py @@ -729,6 +729,23 @@ class TestGeneralPrompt: prompt = build_general_system_prompt(["Alpha"]) assert "Contexte applicatif actuel" not in prompt + def test_general_prompt_agent_uses_native_tools(self): + """BUG-053: the agent must call tools, not emit `obsigate-action` blocks.""" + from backend.bookslm import build_general_system_prompt + + prompt = build_general_system_prompt(["Alpha"], agent=True) + assert "create_file" in prompt + # The text-protocol example block must not be taught in agent mode. + assert '"action": "create_file"' not in prompt + assert "inclus un bloc de ce type" not in prompt + + def test_general_prompt_classic_keeps_text_protocol(self): + """The classic chat endpoint still uses the text action protocol.""" + from backend.bookslm import build_general_system_prompt + + prompt = build_general_system_prompt(["Alpha"]) + assert '"action": "create_file"' in prompt + def test_documents_prompt_scope(self): from backend.bookslm import build_system_prompt @@ -899,6 +916,30 @@ class TestBooksLMAgentEndpoint: ) assert resp.status_code == 401 + def test_agent_prompt_uses_native_tools_not_text_protocol(self, bookslm_client, monkeypatch): + """BUG-053: the agent system prompt must not teach the text protocol.""" + import backend.bookslm_routes as routes + from backend.ai_chat import LLMResponse + + captured = {} + + async def fake_chat_completion(messages, **kwargs): + captured["system"] = messages[0]["content"] + return LLMResponse(content="ok") + + monkeypatch.setattr(routes, "chat_completion", fake_chat_completion) + monkeypatch.setattr(routes, "_resolve_provider_name", lambda requested: "deepseek") + + token, _ = _login_bookslm(bookslm_client) + resp = bookslm_client.post( + "/api/ai/bookslm/agent", + json={"directory": "", "message": "cree un fichier", "mode": "general"}, + headers={"Authorization": f"Bearer {token}"}, + ) + assert resp.status_code == 200 + assert "create_file" in captured["system"] + assert '"action": "create_file"' not in captured["system"] + def test_agent_tool_call_flow(self, bookslm_client, monkeypatch): import backend.bookslm_routes as routes from backend.ai_chat import LLMResponse, ToolCall