Files
pyssh-client/app/ai/settings.py
Florian 0b2ff38bfd Assistant IA optionnel (analyse de sortie, diagnostic, generation de commande)
Nouvelle couche app/ai/ (provider OpenAI-compatible via urllib, redaction de
secrets, detection de commandes dangereuses, parametres, worker QThread) et son
integration dans chaque onglet terminal via un bouton "Assistant IA" (memes
conventions que le bouton Snippets existant) :

- Expliquer cette sortie / Diagnostiquer cette erreur / Proposer une solution /
  Poser une question... : utilisent la selection courante du terminal, affichees
  dans un panneau lateral (memoire uniquement, jamais journalise) reutilisant le
  splitter comme le panneau SFTP.
- Generer une commande : ne s'execute jamais automatiquement (Copier / Inserer
  dans le terminal sans Entree / Annuler), avec avertissement si la commande
  proposee correspond a un motif destructeur connu.
- Avant tout envoi : avis unique (persist) puis detection de secrets evidents
  (mot de passe, cle, jeton) avec choix de masquer, envoyer tel quel ou annuler.

Desactive par defaut, sans liste de fournisseur figee (URL + modele libres,
compatible OpenAI/OpenRouter/LLM local). La cle API passe exclusivement par
CredentialStore (jamais dans settings.json ni journalisee). Parametrable via
le nouveau menu "Assistant IA > Parametres..." (bouton "Tester la connexion").

Bug trouve par les tests avant commit : un panneau nouvellement insere dans un
QSplitter deja visible est affiche automatiquement par Qt, ce qui inversait le
premier clic sur "Afficher le panneau IA" ; corrige en le masquant explicitement
a sa creation.

9 nouveaux fichiers de test (provider, redaction, dangers, parametres, worker,
confirmations de securite, dialogue de commande, panneau, integration onglet) :
39 verifications, toutes au vert. Suite complete (31 fichiers) executee sans
regression.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 05:48:13 +02:00

86 lines
3.0 KiB
Python

"""Configuration de l'assistant IA (optionnel, desactive par defaut).
La cle API n'est jamais stockee ici : seuls les parametres non sensibles
(activation, fournisseur, URL, modele, timeout, temperature) le sont, via
app.app_settings. La cle passe exclusivement par CredentialStore
(app.credentials), jamais dans settings.json/sessions.json/logs."""
from __future__ import annotations
from dataclasses import dataclass
from app.app_settings import get_setting, set_setting
from app.credentials import CredentialStore
from app.ai.provider import AIProvider, OpenAICompatibleProvider
AI_API_KEY_CREDENTIAL = "ai:api_key"
DEFAULT_BASE_URL = "https://api.openai.com/v1"
DEFAULT_PROVIDER_NAME = "OpenAI"
DEFAULT_MODEL = ""
DEFAULT_TIMEOUT = 30.0
DEFAULT_TEMPERATURE = 0.3
@dataclass
class AISettings:
enabled: bool = False
provider_name: str = DEFAULT_PROVIDER_NAME
base_url: str = DEFAULT_BASE_URL
model: str = DEFAULT_MODEL
timeout: float = DEFAULT_TIMEOUT
temperature: float = DEFAULT_TEMPERATURE
def load_ai_settings() -> AISettings:
try:
timeout = float(get_setting("ai_timeout", DEFAULT_TIMEOUT))
except (TypeError, ValueError):
timeout = DEFAULT_TIMEOUT
try:
temperature = float(get_setting("ai_temperature", DEFAULT_TEMPERATURE))
except (TypeError, ValueError):
temperature = DEFAULT_TEMPERATURE
return AISettings(
enabled=bool(get_setting("ai_enabled", False)),
provider_name=get_setting("ai_provider_name", DEFAULT_PROVIDER_NAME) or DEFAULT_PROVIDER_NAME,
base_url=get_setting("ai_base_url", DEFAULT_BASE_URL) or DEFAULT_BASE_URL,
model=get_setting("ai_model", DEFAULT_MODEL) or DEFAULT_MODEL,
timeout=timeout,
temperature=temperature,
)
def save_ai_settings(settings: AISettings) -> None:
set_setting("ai_enabled", settings.enabled)
set_setting("ai_provider_name", settings.provider_name)
set_setting("ai_base_url", settings.base_url)
set_setting("ai_model", settings.model)
set_setting("ai_timeout", settings.timeout)
set_setting("ai_temperature", settings.temperature)
def load_api_key(credential_store: CredentialStore) -> str:
return credential_store.get_secret(AI_API_KEY_CREDENTIAL) or ""
def save_api_key(credential_store: CredentialStore, api_key: str) -> bool:
if api_key:
return credential_store.set_secret(AI_API_KEY_CREDENTIAL, api_key)
credential_store.delete_secret(AI_API_KEY_CREDENTIAL)
return True
def build_provider_from_settings(credential_store: CredentialStore) -> AIProvider | None:
"""None si l'assistant est desactive : point d'appel unique pour tous les
endroits de l'UI qui ont besoin d'un fournisseur pret a l'emploi."""
settings = load_ai_settings()
if not settings.enabled:
return None
return OpenAICompatibleProvider(
base_url=settings.base_url,
api_key=load_api_key(credential_store),
model=settings.model,
timeout=settings.timeout,
temperature=settings.temperature,
)