Funk-lab/stt/client/stt/api.py
ALI YESILKAYA f47bc36f29
feat(stt-client): pages détail enrichies (aperçu portefeuille + alertes) (#59)
Les pages détail du portail HUD affichent désormais un aperçu live des données
du service, pas seulement santé + lien :

- Ghostfolio → portefeuille complet : valeur, performance globale, positions
  (allocation % + P/L par ligne, coloré). Nouvel endpoint serveur
  POST /v1/portfolio/details (jeton client, source unique réutilisée).
- Alertmanager → TOUTES les alertes actives du homelab (total + critiques +
  liste avec sévérité), rendu côté HUD depuis `all_alerts` poussé par le
  StatusPoller (auto-rafraîchi, aucun appel supplémentaire).

Mécanisme extensible (`DETAIL_PROVIDERS` : `server` = requête backend |
`alerts` = liste locale). Cartes de stats + lignes de données génériques.

Client 0.18.0. Vérifié via Playwright (rendu, tons, normalisation des %).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 23:48:20 +02:00

133 lines
4.8 KiB
Python

"""Client HTTP vers le STT-server (in-cluster).
Le client n'a plus de cerveau : il envoie le texte transcrit au serveur, qui
orchestre l'inférence (LiteLLM → Qwen3/Claude) et renvoie la réponse. Le client
choisit le modèle (alias LiteLLM) et le passe au serveur par requête.
"""
from __future__ import annotations
import uuid
from typing import Any
class ServerClient:
def __init__(self, cfg: dict[str, Any]):
self.url = cfg["url"].rstrip("/")
self.timeout = cfg.get("timeout_sec", 90)
self.model = cfg.get("model", "hermes-default")
# contexte présélectionné (None = défaut serveur). Piloté par le HUD.
self.context = cfg.get("context")
# dernier contexte assemblé renvoyé par le serveur (pour le visualiseur)
self.last_context: dict | None = None
self.last_context_id: str | None = None
# jetons à transmettre au serveur (ex. {"ghostfolio_token": …}) — source UNIQUE
# du jeton, côté client ; le serveur les consomme (contexte + /v1/portfolio).
self.secrets: dict = {}
# une session par run → mémoire court-terme côté serveur
self.session_id = uuid.uuid4().hex
def ask(self, text: str, model: str | None = None) -> str:
import requests
payload = {
"text": text,
"model": model or self.model,
"session_id": self.session_id,
}
if self.context:
payload["context"] = self.context
if self.secrets:
payload["secrets"] = self.secrets
r = requests.post(f"{self.url}/v1/ask", json=payload, timeout=self.timeout)
if r.status_code == 400:
raise RuntimeError(r.json().get("detail", "requête invalide"))
r.raise_for_status()
data = r.json()
# contexte assemblé (système + sources live + RAG + mémoire) → visualiseur HUD
self.last_context = data.get("context")
self.last_context_id = data.get("context_id")
return data["reply"].strip()
def portfolio(self) -> str:
"""Valeur du portefeuille Ghostfolio via le serveur (jeton client transmis).
Unifie le jeton : c'est le serveur qui parle à Ghostfolio. Dégrade en message
clair si le serveur est antérieur (pas d'endpoint) ou injoignable.
"""
import requests
try:
r = requests.post(
f"{self.url}/v1/portfolio",
json={"token": self.secrets.get("ghostfolio_token")},
timeout=self.timeout,
)
r.raise_for_status()
return (r.json().get("summary") or "").strip() or "Pas de réponse de Ghostfolio."
except requests.RequestException:
return "Je n'arrive pas à joindre le serveur pour Ghostfolio."
def portfolio_details(self) -> dict:
"""Aperçu structuré du portefeuille (page détail du portail HUD).
{value, currency, performance_pct, holdings:[…]} ou {error: …}. Dégrade en
clair si le serveur est antérieur (pas d'endpoint) ou injoignable.
"""
import requests
try:
r = requests.post(
f"{self.url}/v1/portfolio/details",
json={"token": self.secrets.get("ghostfolio_token")},
timeout=self.timeout,
)
if r.status_code == 404:
return {"error": "Aperçu indisponible (STT-server antérieur)."}
r.raise_for_status()
return r.json()
except requests.RequestException:
return {"error": "Je n'arrive pas à joindre le serveur pour Ghostfolio."}
def contexts(self) -> dict | None:
"""{'default':…, 'contexts':[{id,label,icon,description}]} ; None si indisponible.
Dégrade en None sur un serveur antérieur (endpoint absent) → pas de sélecteur.
"""
import requests
try:
r = requests.get(f"{self.url}/v1/contexts", timeout=10)
r.raise_for_status()
return r.json()
except requests.RequestException:
return None
def reset(self) -> None:
import requests
try:
requests.post(
f"{self.url}/v1/reset",
json={"text": "", "session_id": self.session_id},
timeout=10,
)
except requests.RequestException:
pass
def models(self) -> dict:
"""{'default': ..., 'available': [...]} depuis le serveur."""
import requests
r = requests.get(f"{self.url}/v1/models", timeout=10)
r.raise_for_status()
return r.json()
def healthy(self) -> bool:
import requests
try:
r = requests.get(f"{self.url}/healthz", timeout=5)
return r.ok
except requests.RequestException:
return False