Assistant IA : liste deroulante de fournisseurs (dont Ollama, LM Studio)
Some checks failed
CI / lint (push) Has been cancelled
CI / tests (push) Has been cancelled
CI / typecheck (push) Has been cancelled
CI / build (push) Has been cancelled

Retour utilisateur : le champ "Fournisseur" en simple texte libre ne donnait
aucune indication de ce qui est possible, notamment pour l'IA locale
(Ollama, LM Studio), alors que d'autres preferent un service cloud.

Remplace par une QComboBox editable : une liste de raccourcis usuels (OpenAI,
OpenRouter, Mistral AI, Ollama local, LM Studio local, Personnalise...) qui
pre-remplit l'URL de base au choix - sans jamais empecher de saisir librement
un autre fournisseur/proxy compatible OpenAI, conformement a l'exigence
d'origine de ne rien figer en dur. Le modele reste toujours en saisie libre,
chaque installation Ollama/LM Studio ayant ses propres modeles telecharges.

Bonus cible : choisir un prereglage local (Ollama/LM Studio, detecte via
localhost/127.0.0.1 dans l'URL) met a jour l'indication du champ cle API pour
signaler qu'elle n'est generalement pas necessaire.

tests/test_ai_settings_dialog.py (8 verifications, nouveau fichier - aucun
test ne couvrait encore AISettingsDialog elle-meme) : presence d'Ollama/LM
Studio dans la liste, pre-remplissage de l'URL par prereglage, "Personnalise"
qui ne touche pas une URL deja saisie, persistance d'un nom de fournisseur
personnalise a travers un enregistrement puis une reouverture. Suite complete
revalidee (34/34) et ruff toujours propre.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Florian
2026-09-05 08:00:25 +02:00
parent 6cceb3f3b2
commit f5d99584fd
3 changed files with 186 additions and 11 deletions

View File

@@ -0,0 +1,127 @@
"""AISettingsDialog : liste deroulante de fournisseurs (raccourcis usuels,
dont des LLM locaux comme Ollama/LM Studio) qui pre-remplit l'URL de base sans
jamais empecher de saisir un fournisseur personnalise."""
import pathlib
import sys
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
import tempfile
from pathlib import Path
from unittest.mock import patch
from PySide6.QtWidgets import QApplication
import app.app_settings as app_settings
from app.ai.settings import load_ai_settings
from app.credentials import CredentialStore
from app.dialogs.ai_settings_dialog import PROVIDER_PRESETS, AISettingsDialog
app = QApplication.instance() or QApplication(sys.argv)
class FakeCredentialStore(CredentialStore):
def __init__(self):
self._data = {}
def is_available(self):
return True
def set_secret(self, key, secret):
self._data[key] = secret
return True
def get_secret(self, key):
return self._data.get(key)
def delete_secret(self, key):
self._data.pop(key, None)
def select_preset(dialog, label: str) -> int:
"""Simule un choix reel dans la liste (pas une saisie libre) : Qt met a
jour l'index courant ET emet `activated` lors d'un clic - setCurrentIndex()
seul n'emet pas `activated`, il faut donc reproduire les deux."""
index = dialog.provider_combo.findText(label)
assert index >= 0, f"prereglage introuvable : {label}"
dialog.provider_combo.setCurrentIndex(index)
dialog.provider_combo.activated.emit(index)
return index
tmp = Path(tempfile.mkdtemp())
settings_redirect = patch.object(app_settings, "SETTINGS_FILE", tmp / "settings.json")
settings_redirect.start()
print("=== 1. La liste propose des fournisseurs cloud ET des LLM locaux ===")
store = FakeCredentialStore()
dialog = AISettingsDialog(credential_store=store)
labels = [dialog.provider_combo.itemText(i) for i in range(dialog.provider_combo.count())]
print(" fournisseurs proposes:", labels)
assert "OpenAI" in labels
assert "Ollama (local)" in labels
assert "LM Studio (local)" in labels
assert "Personnalise..." in labels
print(" OK")
print()
print("=== 2. Choisir 'Ollama (local)' pre-remplit l'URL de base ===")
dialog.base_url_edit.setText("")
select_preset(dialog, "Ollama (local)")
print(" URL de base:", dialog.base_url_edit.text())
assert dialog.base_url_edit.text() == "http://localhost:11434/v1"
print(" OK")
print()
print("=== 3. ... et signale que la cle API n'est generalement pas necessaire ===")
print(" placeholder cle API:", dialog.api_key_edit.placeholderText())
assert "inutile" in dialog.api_key_edit.placeholderText().lower()
print(" OK")
print()
print("=== 4. Choisir un fournisseur cloud restaure le message par defaut pour la cle API ===")
select_preset(dialog, "OpenAI")
assert dialog.base_url_edit.text() == "https://api.openai.com/v1"
print(" placeholder cle API:", dialog.api_key_edit.placeholderText())
assert "inutile" not in dialog.api_key_edit.placeholderText().lower()
print(" OK")
print()
print("=== 5. 'Personnalise...' ne touche pas a une URL de base deja saisie ===")
dialog.base_url_edit.setText("https://mon-proxy-interne.example.com/v1")
select_preset(dialog, "Personnalise...")
print(" URL de base:", dialog.base_url_edit.text())
assert dialog.base_url_edit.text() == "https://mon-proxy-interne.example.com/v1"
print(" OK")
print()
print("=== 6. Le champ reste editable : un nom personnalise est bien conserve a l'enregistrement ===")
dialog.provider_combo.setCurrentText("Mon LLM interne")
dialog.model_edit.setText("mon-modele")
dialog._on_accept()
reloaded = load_ai_settings()
print(" fournisseur enregistre:", reloaded.provider_name)
assert reloaded.provider_name == "Mon LLM interne"
assert reloaded.base_url == "https://mon-proxy-interne.example.com/v1"
print(" OK")
print()
print("=== 7. Rouvrir le dialogue recharge le fournisseur personnalise precedemment saisi ===")
dialog2 = AISettingsDialog(credential_store=store)
print(" fournisseur affiche:", dialog2.provider_combo.currentText())
assert dialog2.provider_combo.currentText() == "Mon LLM interne"
print(" OK")
print()
print("=== 8. Les prereglages restent coherents avec la liste affichee (pas de doublon, tous non vides) ===")
seen = set()
for label, _url in PROVIDER_PRESETS:
assert label not in seen, f"doublon dans PROVIDER_PRESETS : {label}"
seen.add(label)
assert label.strip(), "un prereglage ne doit jamais avoir un nom vide"
print(" OK")
settings_redirect.stop()
print()
print("AI SETTINGS DIALOG : TOUS LES TESTS OK")