Assistant IA : liste deroulante de fournisseurs (dont Ollama, LM Studio)
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:
@@ -1,10 +1,14 @@
|
||||
"""Configuration de l'assistant IA (optionnel, desactive par defaut) : aucune
|
||||
liste figee de fournisseurs/modeles, juste une URL de base et un nom de modele
|
||||
compatibles avec l'API OpenAI (OpenAI, OpenRouter, proxy interne, LLM local...)."""
|
||||
liste figee de MODELES, juste un nom de modele libre compatible avec l'API
|
||||
OpenAI. Le fournisseur, lui, propose une liste deroulante de raccourcis usuels
|
||||
(cloud et LLM local via Ollama/LM Studio) qui pre-remplissent l'URL de base -
|
||||
mais reste modifiable librement ("Personnalise...") pour tout autre service
|
||||
compatible OpenAI."""
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QDoubleSpinBox,
|
||||
@@ -21,6 +25,22 @@ from app.ai.service import KIND_TEST, AIRequestWorker
|
||||
from app.ai.settings import AISettings, load_ai_settings, load_api_key, save_ai_settings, save_api_key
|
||||
from app.credentials import CredentialStore, NullCredentialStore
|
||||
|
||||
# (nom affiche, URL de base) - un point de depart, pas une liste fermee : le
|
||||
# champ reste editable, pour tout fournisseur/proxy/LLM local compatible avec
|
||||
# l'API OpenAI mais absent d'ici.
|
||||
PROVIDER_PRESETS: list[tuple[str, str]] = [
|
||||
("OpenAI", "https://api.openai.com/v1"),
|
||||
("OpenRouter", "https://openrouter.ai/api/v1"),
|
||||
("Mistral AI", "https://api.mistral.ai/v1"),
|
||||
("Ollama (local)", "http://localhost:11434/v1"),
|
||||
("LM Studio (local)", "http://localhost:1234/v1"),
|
||||
("Personnalise...", ""),
|
||||
]
|
||||
|
||||
|
||||
def _looks_local(base_url: str) -> bool:
|
||||
return "localhost" in base_url or "127.0.0.1" in base_url
|
||||
|
||||
|
||||
class AISettingsDialog(QDialog):
|
||||
def __init__(self, credential_store: CredentialStore | None = None, parent=None):
|
||||
@@ -44,22 +64,34 @@ class AISettingsDialog(QDialog):
|
||||
)
|
||||
form.addRow("", self.enabled_check)
|
||||
|
||||
self.provider_name_edit = QLineEdit(settings.provider_name)
|
||||
self.provider_name_edit.setPlaceholderText("ex : OpenAI, OpenRouter, LLM local...")
|
||||
form.addRow("Fournisseur :", self.provider_name_edit)
|
||||
self.provider_combo = QComboBox()
|
||||
self.provider_combo.setEditable(True)
|
||||
self.provider_combo.setToolTip(
|
||||
"Choisissez un raccourci (pre-remplit l'URL de base) ou saisissez "
|
||||
"librement le nom de tout autre fournisseur/proxy compatible OpenAI."
|
||||
)
|
||||
for label, url in PROVIDER_PRESETS:
|
||||
self.provider_combo.addItem(label, url)
|
||||
self.provider_combo.setCurrentText(settings.provider_name)
|
||||
# activated (et non currentIndexChanged) : ne se declenche que sur un
|
||||
# choix explicite dans la liste, jamais pendant la saisie libre d'un
|
||||
# nom de fournisseur personnalise.
|
||||
self.provider_combo.activated.connect(self._on_provider_preset_chosen)
|
||||
form.addRow("Fournisseur :", self.provider_combo)
|
||||
|
||||
self.base_url_edit = QLineEdit(settings.base_url)
|
||||
self.base_url_edit.setPlaceholderText("https://api.openai.com/v1")
|
||||
form.addRow("URL de base :", self.base_url_edit)
|
||||
|
||||
self.model_edit = QLineEdit(settings.model)
|
||||
self.model_edit.setPlaceholderText("ex : gpt-4o-mini")
|
||||
self.model_edit.setPlaceholderText("ex : gpt-4o-mini, llama3.2, qwen2.5-coder...")
|
||||
form.addRow("Modele :", self.model_edit)
|
||||
|
||||
self._credential_store_available = self.credential_store.is_available()
|
||||
self.api_key_edit = QLineEdit(api_key)
|
||||
self.api_key_edit.setEchoMode(QLineEdit.Password)
|
||||
self.api_key_edit.setPlaceholderText("Laisser vide si le fournisseur n'en requiert pas")
|
||||
if not self.credential_store.is_available():
|
||||
if not self._credential_store_available:
|
||||
self.api_key_edit.setEnabled(False)
|
||||
self.api_key_edit.setPlaceholderText("Aucun trousseau securise disponible sur ce systeme")
|
||||
form.addRow("Cle API :", self.api_key_edit)
|
||||
@@ -101,11 +133,23 @@ class AISettingsDialog(QDialog):
|
||||
buttons.rejected.connect(self.reject)
|
||||
layout.addWidget(buttons)
|
||||
|
||||
# ---------- fournisseur ----------
|
||||
def _on_provider_preset_chosen(self, index: int) -> None:
|
||||
_label, url = PROVIDER_PRESETS[index]
|
||||
if url:
|
||||
self.base_url_edit.setText(url)
|
||||
if not self._credential_store_available:
|
||||
return
|
||||
if _looks_local(url):
|
||||
self.api_key_edit.setPlaceholderText("Generalement inutile pour un LLM local")
|
||||
else:
|
||||
self.api_key_edit.setPlaceholderText("Laisser vide si le fournisseur n'en requiert pas")
|
||||
|
||||
# ---------- lecture du formulaire ----------
|
||||
def _form_settings(self) -> AISettings:
|
||||
return AISettings(
|
||||
enabled=self.enabled_check.isChecked(),
|
||||
provider_name=self.provider_name_edit.text().strip() or "OpenAI",
|
||||
provider_name=self.provider_combo.currentText().strip() or "OpenAI",
|
||||
base_url=self.base_url_edit.text().strip(),
|
||||
model=self.model_edit.text().strip(),
|
||||
timeout=self.timeout_spin.value(),
|
||||
|
||||
Reference in New Issue
Block a user