refactor(stt): unifie le jeton Ghostfolio — source client unique, appel côté serveur (#41)

Avant : jeton Ghostfolio en double (config client pour l'intent vocal + secret k8s pour
le contexte serveur). Désormais une SEULE source — la config client — transmise au
serveur, qui fait tous les appels Ghostfolio (un seul code, lecture seule).

Serveur :
- sources.py : cœur _ghostfolio_fetch(token) partagé ; ghostfolio_block (contexte) et
  ghostfolio_phrase (intent) acceptent un jeton par requête (priorité client > env).
- app.py : /v1/ask accepte `secrets` (jetons client) passés aux sources ; nouvel
  endpoint POST /v1/portfolio {token} → valeur du portefeuille.
- bump 0.4.0 → 0.5.0.

Client :
- api.py : ServerClient.secrets transmis dans /v1/ask ; méthode portfolio() → /v1/portfolio.
- cli.py : _make_client renseigne secrets["ghostfolio_token"] depuis [ghostfolio].
- ui/app.py : l'intent « combien ghostfolio » appelle client.portfolio (serveur).
- supprime portal/ghostfolio.py (client) — l'appel API vit côté serveur.
- config : [ghostfolio] = juste access_token (transmis au serveur).
- bump 0.12.0 → 0.13.0.

Le secret k8s STT_GHOSTFOLIO_TOKEN devient un simple fallback. Validé bout-en-bout
contre une instance serveur locale + Ghostfolio réel : « …vaut environ 16 520 EUR. »

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ALI YESILKAYA 2026-06-21 03:28:52 +02:00 committed by GitHub
parent ae4d5bd20e
commit e49b4fc3ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 121 additions and 113 deletions

View file

@ -21,6 +21,9 @@ class ServerClient:
# 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
@ -34,6 +37,8 @@ class ServerClient:
}
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"))
@ -44,6 +49,25 @@ class ServerClient:
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 contexts(self) -> dict | None:
"""{'default':…, 'contexts':[{id,label,icon,description}]} ; None si indisponible.