"""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...).""" from __future__ import annotations from PySide6.QtWidgets import ( QCheckBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QFormLayout, QLabel, QLineEdit, QMessageBox, QPushButton, QVBoxLayout, ) from app.ai.provider import OpenAICompatibleProvider 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 class AISettingsDialog(QDialog): def __init__(self, credential_store: CredentialStore | None = None, parent=None): super().__init__(parent) self.credential_store = credential_store or NullCredentialStore() self._test_worker: AIRequestWorker | None = None self.setWindowTitle("Assistant IA") self.setMinimumWidth(460) settings = load_ai_settings() api_key = load_api_key(self.credential_store) layout = QVBoxLayout(self) form = QFormLayout() self.enabled_check = QCheckBox("Activer l'assistant IA") self.enabled_check.setChecked(settings.enabled) self.enabled_check.setToolTip( "Desactive par defaut. Aucune donnee n'est envoyee a un service externe " "tant que cette case n'est pas cochee et qu'un modele n'est pas configure." ) 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.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") form.addRow("Modele :", self.model_edit) 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(): 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) self.timeout_spin = QDoubleSpinBox() self.timeout_spin.setRange(1.0, 600.0) self.timeout_spin.setSuffix(" s") self.timeout_spin.setValue(settings.timeout) form.addRow("Delai d'attente :", self.timeout_spin) self.temperature_spin = QDoubleSpinBox() self.temperature_spin.setRange(0.0, 2.0) self.temperature_spin.setSingleStep(0.1) self.temperature_spin.setValue(settings.temperature) form.addRow("Temperature :", self.temperature_spin) layout.addLayout(form) note = QLabel( "La cle API est memorisee dans le trousseau securise du systeme, jamais " "dans un fichier de configuration. L'assistant est entierement optionnel : " "aucune fonctionnalite existante n'en depend." ) note.setWordWrap(True) note.setObjectName("statusLabel") layout.addWidget(note) test_row = QVBoxLayout() self.test_btn = QPushButton("Tester la connexion") self.test_btn.clicked.connect(self._test_connection) self.test_status = QLabel("") self.test_status.setWordWrap(True) test_row.addWidget(self.test_btn) test_row.addWidget(self.test_status) layout.addLayout(test_row) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(self._on_accept) buttons.rejected.connect(self.reject) layout.addWidget(buttons) # ---------- 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", base_url=self.base_url_edit.text().strip(), model=self.model_edit.text().strip(), timeout=self.timeout_spin.value(), temperature=self.temperature_spin.value(), ) def _build_provider(self) -> OpenAICompatibleProvider | None: settings = self._form_settings() if not settings.base_url or not settings.model: QMessageBox.warning( self, "Configuration incomplete", "Renseignez au moins l'URL de base et le modele avant de tester.", ) return None return OpenAICompatibleProvider( base_url=settings.base_url, api_key=self.api_key_edit.text().strip(), model=settings.model, timeout=settings.timeout, temperature=settings.temperature, ) # ---------- test de connexion ---------- def _test_connection(self) -> None: if self._test_worker is not None and self._test_worker.isRunning(): return provider = self._build_provider() if provider is None: return self.test_btn.setEnabled(False) self.test_status.setText("Test en cours...") self._test_worker = AIRequestWorker(provider, KIND_TEST, "") self._test_worker.finished_ok.connect(self._on_test_ok) self._test_worker.finished_error.connect(self._on_test_error) self._test_worker.start() def _on_test_ok(self, _text: str) -> None: self.test_btn.setEnabled(True) self.test_status.setText("✓ Connexion reussie.") def _on_test_error(self, message: str) -> None: self.test_btn.setEnabled(True) self.test_status.setText(f"✗ Echec : {message}") # ---------- validation ---------- def _on_accept(self) -> None: settings = self._form_settings() if settings.enabled and (not settings.base_url or not settings.model): QMessageBox.warning( self, "Configuration incomplete", "Renseignez au moins l'URL de base et le modele pour activer l'assistant IA.", ) return save_ai_settings(settings) save_api_key(self.credential_store, self.api_key_edit.text().strip()) self.accept() def closeEvent(self, event) -> None: if self._test_worker is not None and self._test_worker.isRunning(): self._test_worker.wait(2000) super().closeEvent(event)