pyproject.toml devient le point d'entree unique de configuration des outils
(pytest.ini supprime, sa config y est deplacee). Les dependances restent dans
requirements.txt/requirements-dev.txt (deja utilises par setup.bat) plutot que
dupliquees ici : ce projet se distribue par PyInstaller, pas par "pip install".
ruff : configure puis passe sur tout app/ + tests/ (154 corrections
automatiques, purement mecaniques - tri des imports, imports multiples sur une
ligne separes, alias depreciees) suivies de 7 corrections manuelles ciblees :
- app/ssh_connection.py : `raise ... from None` pour le refus explicite de
passphrase (ce n'est pas un echec a diagnostiquer).
- app/controllers/workspace_manager.py : zip() sans strict= documente en
commentaire (deliberement tolerant a un workspace legerement perime).
- tests/test_ai_redaction.py : nom de variable ambigu renomme.
- E702 (construction compacte "widget = X(); widget.setY(...)" dans
sftp_panel.py/tunnel_dialog.py) et E402 (quelques tests qui importent un
symbole juste avant la section qui l'utilise) : ignores explicitement, ce
sont des styles deliberes preexistants, pas des oublis.
`ruff check app main.py tests` est desormais propre.
pyright : mode "basic" (le code n'a jamais ete type). Plus de 200 signalements
sur ~300 etaient le meme faux positif connu de l'ecosysteme PySide6 (acces
plat aux enums Qt, ex. Qt.UserRole, valide a l'execution mais que les stubs ne
reconnaissent que sous leur forme imbriquee) : desactive avec justification en
commentaire. Reste un socle de 77 signalements reels (surtout des acces
Optional non filtres dans app/ui/tabs.py et app/ssh_connection.py) : non
corrige dans cette phase - resserrer le typage la ou pyright a raison est un
travail a part, pas une consequence automatique de l'ajout de l'outil. La CI
le lance en mode informatif (continue-on-error) tant que ce socle n'est pas
resorbe.
.gitea/workflows/ci.yml : lint (ruff, bloquant), tests (pytest, bloquant),
typecheck (pyright, informatif), build (PyInstaller, sur main uniquement,
artefact du dossier portable). Necessite un runner Gitea Actions Windows
enregistre - PySide6/pywinpty/Credential Manager/ACL NTFS sont Windows
uniquement, comme l'application elle-meme ; non verifiable depuis ce poste.
Suite complete revalidee apres coup (33/33) pour confirmer qu'aucune des 154
corrections automatiques n'a modifie de comportement.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
170 lines
6.8 KiB
Python
170 lines
6.8 KiB
Python
"""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)
|