Files
ObsiGate/docs/features/ai-provider-picker.md
T
bruno b69cb9b0f8
CI / lint (push) Successful in 1m20s
CI / security (push) Successful in 53s
CI / test (push) Successful in 2m27s
CI / build (push) Successful in 50s
CI / e2e (push) Successful in 10m48s
fix(ai): BUG-044 capacités des modèles lues chez le fournisseur (Mistral vision)
Le panneau de modèle par défaut et la bulle ⓘ n'affichaient aucun modèle Mistral
« Vision capable » alors que GET api.mistral.ai/v1/models en déclare 28 : la table
de capacités était entièrement statique et aucun de ses motifs ne correspondait aux
familles Mistral actuelles (seul `pixtral`, retiré de l'API, les matchait).

- backend/provider_capabilities.py (nouveau) : capacités déclarées par le
  fournisseur (Mistral `capabilities`, OpenRouter `architecture`), détectées par
  la forme du payload, snapshot en cache process-wide (TTL 30 min, surchargeable
  par AI_CAPABILITIES_TTL_SECONDS) rempli par GET /api/config/ai-models.
- backend/model_capabilities.py : une déclaration prime sur la table statique
  pour chaque drapeau mentionné ; la table ne comble que le reste (Mistral ne
  déclare jamais `embedding`). Table corrigée pour le repli hors ligne : familles
  vision Mistral (ministral, magistral, mistral-small, mistral-medium,
  mistral-vibe-cli, labs-leanstral), mistral-ocr = vision sans chat, et défaut du
  fournisseur Mistral sans `embeddings` (mistral-large / codestral n'étaient plus
  des « embedders »).
- backend/ai_routes.py : GET /api/ai/model-capabilities reste sans appel réseau
  (cache froid → table statique).
- Tests : tests/test_provider_capabilities.py (nouveau), TestMistralFamilies et
  TestDeclaredCapabilities (bout en bout via l'API).
- Docs : CHANGELOG [Unreleased], registre + journal ISSUES_TODOLIST,
  fiche docs/features/ai-provider-picker.md (§L).

Vérifié : 28/28 modèles vision déclarés par Mistral détectés (0 avant), 0 écart
dans les deux sens ; pytest 1007 passed / 6 skipped ; ruff 0 (backend) ; mypy 0 ;
tests frontend unit 9/9 + IA 66/66 + validate-imports 37 modules ; instance de test
reconstruite et vérifiée sur http://localhost:2020.
2026-09-15 09:26:31 -04:00

147 lines
10 KiB
Markdown

# #82 — Assistant IA — Section « Fournisseur & modèle » compacte
> **Statut :** ✅ Livré (en attente de validation utilisateur)
> **Effort :** 1 jour | **Impact :** 🟢
> **Références :** [Roadmap](../ROADMAP.md) · [Assistant IA #81](./ai-assistant-commands.md) · [Changelog](../../CHANGELOG.md)
- **Description :** rendre la barre de sélection du modèle de l'assistant IA **plus discrète** tout
en conservant les informations utiles :
1. **Capacités via bulle** — la liste complète des types d'endpoints (Chat, Embeddings, Rerank,
Images, Video, Audio Speech, Audio Transcriptions, Vision) n'est plus affichée en permanence.
Un bouton **ⓘ** révèle une bulle d'information au **survol**, au **clic** ou par **appui long**
(mobile).
2. **Recherche de modèle** — le `<select>` natif est remplacé par une liste déroulante compacte
dotée d'un **champ de recherche** qui filtre les modèles en tapant (insensible à la casse et
aux accents).
3. **Compatibilité** — un `<select class="ai-picker-model">` masqué reste présent (source de
vérité) pour les commandes admin `/model` et l'état persisté `obsigate_ai_picker`.
## A. Picker partagé — ✅ livré
- [x] `frontend/js/ai.js` : `buildAIPickerUI()` réécrit (provider compact + combobox modèle + bulle
capacités). Le picker reste partagé par l'assistant (`bookslm.js`) et la barre d'édition IA.
- [x] `renderCapabilityList()` conservé et affiché dans la bulle (le panneau de configuration
`#cfg-ai-default-model-caps` continue de l'utiliser directement).
- [x] Fermeture des popovers au clic extérieur via un unique listener délégué (pas de fuite).
- [x] i18n FR/EN (`ai.model_search`, `ai.model_info`).
## B. Correctifs associés — ✅ livré
- [x] **BUG-007** — détection `/` et `@` Unicode + filtrage insensible aux accents + navigation
clavier ↑/↓ fiabilisée (jeton de séquence, `scrollIntoView`).
- [x] **BUG-008** — contexte ad-hoc `@` pris en compte en mode Général côté backend
(`backend/bookslm_routes.py`) et recherche de mention sur `vault=all` sans vault courant.
## C. Tests — ✅ livré
- [x] `tests/frontend/ai.test.mjs` : filtrage accentué, mentions accentuées, navigation ↑/↓,
recherche de modèle + bulle de capacités, plafonnement des longues listes (OpenRouter).
- [x] `tests/test_bookslm.py` : contexte ad-hoc en mode Général (`/chat`).
## D. Durcissement anti-cache (BUG-009) — ✅ livré
- [x] Locales chargées avec `cache: 'no-store'` (`frontend/js/i18n.js`) : plus de clés brutes
affichées à cause d'un `fr.json` obsolète en cache HTTP.
- [x] `SW_VERSION` incrémenté (`frontend/sw.js`) pour purger les caches du service worker.
- [x] Styles critiques du picker appliqués **en ligne** (popover absolu, liste en colonne, options
`display:block`) pour rester corrects même si `style.css` est servi depuis un cache.
- [x] Rendu plafonné à 200 modèles (`MODEL_RENDER_LIMIT`) + indicateur `ai.model_more`.
## E. Lisibilité & contexte `@` (BUG-010, BUG-011) — ✅ livré
- [x] Popover modèle **aligné à droite** du déclencheur (`right: 0`), largeur 340 px bornée à
`100vw - 24px` : ne dépasse plus le bord de la sidebar ancrée à droite (BUG-011).
- [x] Noms de modèles sur **plusieurs lignes** (`overflow-wrap: anywhere`, police 0,78 rem) avec
attribut `title` complet.
- [x] La sélection `@` **capture le vault** renvoyé par `/api/tree-search` ; `_contextVault()`
propage ce vault aux requêtes `/context` et `/chat` et les recherches suivantes restent dans
ce vault (BUG-010, mode Général).
## F. Barre latérale épurée & suivi visuel (BUG-012, #82) — ✅ livré
- [x] Section « Fournisseur & modèle » sans intitulés (ni titre de section, ni « Fournisseur : »).
- [x] Description du contexte Général retirée de l'affichage (bandeau de statut masqué) et
exposée via `title` au survol de l'en-tête.
- [x] Placeholder « Posez une question sur ces documents… » retiré ; bouton « Envoyer » remplacé
par l'emoji compact ✈️.
- [x] **Indicateur d'activité** (`.bookslm-activity`) affichant le workflow : envoi, réception,
outil en cours, attente de confirmation, terminé, échec (`ai.activity_*`).
- [x] `@` : le menu est toujours rendu et `_mentionVault()` retombe sur le vault de contexte puis
le premier vault disponible (BUG-012).
## G. Robustesse menus & liens (BUG-013, BUG-014) — ✅ livré
- [x] Navigation clavier ↑/↓ des menus `/` et `@` gérée au niveau du **panneau en phase de
capture** : fonctionne quel que soit l'élément focalisé (BUG-014).
- [x] Les liens fichiers/répertoires des réponses utilisent `_activeVault()` (vault de contexte →
vault sélectionné → premier vault), ce qui supprime l'erreur « Aucun vault actif » en mode
Général (BUG-013).
- [x] Purge des caches élargie : `SW_VERSION` v6 + migration qui supprime tous les caches
`obsigate-*` (clé `obsigate-sw-migration` → `v3`).
- [x] Indice clavier sous la zone de saisie retiré pour un composeur plus épuré (#82).
- [x] Sélection clavier des menus `/` et `@` rendue visible : état actif en `--bg-hover` +
barre d'accent à gauche (`--surface2` étant identique à `--bg-primary` en thème sombre,
la sélection était invisible) — BUG-015.
## H. Performance `@`, sélecteurs agrandis & BooksLM racine (#82) — ✅ livré
- [x] Nouvel endpoint `GET /api/vault/{vault}/paths` (`backend/services/search.py: list_paths`,
`backend/main.py`) : index des chemins plat et plafonné (5000 par défaut).
- [x] Frontend : `_ensurePaths()` précharge cette liste **une fois par vault** (au `openContext`),
puis `_showMentionMenu` filtre **côté client** → affichage instantané des fichiers/
répertoires, sans requête par frappe (repli serveur si le cache est vide).
- [x] Sélecteurs Fournisseur/Modèle agrandis pour s'aligner sur les autres contrôles
(police 0,8 rem, hauteur 34 px, rayon 6 px).
- [x] Menu contextuel de la **racine d'une vault** : ajout de « 🧠 BooksLM » (`frontend/js/ui.js`).
## J. Alignement à droite & ordre des sélecteurs (#82) — ✅ livré
- [x] Le groupe de sélection Fournisseur/Modèle est **aligné sur le bord droit** de la barre
(`frontend/style.css` : `.bookslm-toolbar { justify-content: flex-end; }`).
- [x] Ordre visuel : **capacité du modèle (ⓘ) → menu fournisseurs → menu des modèles**
(`frontend/js/ai.js`, ordre d'ajout dans `_buildPickerUI()`).
- [x] La bulle de capacités s'ouvre vers la droite (`left: 0`, inline + CSS) pour rester dans la
barre latérale désormais que le bouton ⓘ est en tête de groupe.
- [x] Test JSDOM : l'ordre des trois contrôles visibles est vérifié (`tests/frontend/ai.test.mjs`).
## K. Synchronisation de la liste des fournisseurs (BUG-043) — ✅ livré
- [x] `frontend/js/ai.js` : nouveau `refreshAIPickers()` — reconstruit chaque picker
monté dans son emplacement `.ai-picker-slot` (constante `PICKER_SLOT_CLASS`).
Le slot est conservé même quand aucun picker ne peut être construit (aucun
fournisseur configuré), donc le **premier** fournisseur ajouté s'y monte aussi.
- [x] `frontend/js/bookslm.js` : l'emplacement du picker (`span.bookslm-picker-host`)
porte la classe `ai-picker-slot` et **reste dans la barre** — le picker est monté
à l'intérieur au lieu de remplacer le host (`replaceChildren`).
- [x] `frontend/js/config.js` : `refreshAIPickers()` appelé après l'enregistrement
(`saveAIKeys()`) et la suppression (`deleteAIKey()`) d'une clé API.
- [x] Une sélection persistée dont le fournisseur n'est plus configuré est purgée de
`obsigate_ai_picker` (fournisseur **et** modèle effacés → retour au défaut, plus
de nom de modèle fantôme dans le déclencheur).
- [x] Tests JSDOM : ajout/retrait d'un fournisseur dans la barre de l'assistant,
montage du premier fournisseur dans un slot vide, purge de la sélection orpheline,
et garde-fou sur le câblage `saveAIKeys`/`deleteAIKey` (`tests/frontend/ai.test.mjs`).
## I. Points d'attention
- La table statique (`backend/model_capabilities.py`) n'est plus la source principale : elle sert
de **repli** (fournisseur muet, pas de clé API, cache froid) et comble les drapeaux non déclarés.
Un modèle inconnu retombe toujours sur le défaut du fournisseur.
- Le cache de déclarations est **en mémoire, par process** : un redémarrage ou un cache froid ne
casse rien (repli statique), et `GET /api/config/ai-models` le repeuple au premier affichage de
la liste des modèles.
- Le select masqué est conservé uniquement pour la rétro-compatibilité ; ne pas le supprimer sans
migrer `_cmdSwitchModel` / `_applyPickerSelection` dans `frontend/js/bookslm.js`.
## L. Capacités fournies par le provider (BUG-044) — ✅ livré
- [x] `backend/provider_capabilities.py` (nouveau) : lecture des capacités **déclarées** par
l'API du fournisseur (`capabilities` Mistral, `architecture` OpenRouter), détectées par la
**forme** du payload — un fournisseur qui se met à les publier est pris en charge sans
modification de code.
- [x] Snapshot en cache process-wide (TTL 30 min par défaut, `AI_CAPABILITIES_TTL_SECONDS`),
rempli par `GET /api/config/ai-models` (appel déjà effectué pour lister les modèles :
aucune requête supplémentaire) ; `clear_declared_capabilities()` + `cache_info()` pour les
tests et le diagnostic.
- [x] `backend/model_capabilities.py` : une déclaration **prime** sur la table statique pour
chaque drapeau qu'elle mentionne ; la table ne comble que le reste (Mistral ne déclare
jamais `embedding`, seulement l'absence de `completion_chat`).
- [x] Table statique corrigée pour le repli hors ligne : familles vision Mistral (`ministral`,
`magistral`, `mistral-small`, `mistral-medium`, `mistral-vibe-cli`, `labs-leanstral`),
`mistral-ocr` = vision sans chat, et défaut du fournisseur Mistral sans `embeddings`
(un modèle inconnu n'est plus présenté comme un modèle d'embeddings).
- [x] `GET /api/ai/model-capabilities` reste **sans appel réseau** : il répond depuis le cache
(froid → table statique), donc la bulle ⓘ ne bloque jamais.
- [x] Tests : `tests/test_provider_capabilities.py` (parsing, cache, TTL, fusion),
`tests/test_model_capabilities.py::TestMistralFamilies` (repli hors ligne) et
`tests/test_ai_models.py::TestDeclaredCapabilities` (bout en bout : payload Mistral simulé →
`capabilities` de `/api/config/ai-models` puis `/api/ai/model-capabilities`).