mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 15:14:43 +02:00
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:
parent
ae4d5bd20e
commit
e49b4fc3ba
11 changed files with 121 additions and 113 deletions
|
|
@ -34,11 +34,19 @@ async def _prom_query(client: httpx.AsyncClient, expr: str) -> list[tuple[dict,
|
|||
return out
|
||||
|
||||
|
||||
async def ghostfolio_block(client: httpx.AsyncClient) -> str:
|
||||
def _money(v) -> str:
|
||||
return f"{round(float(v)):,}".replace(",", " ")
|
||||
|
||||
|
||||
async def _ghostfolio_fetch(client: httpx.AsyncClient, token: str | None):
|
||||
"""Récupère le portefeuille → (value, currency, holdings) ou une str d'erreur.
|
||||
|
||||
`token` : jeton fourni par le client (prioritaire) ; à défaut, jeton serveur (env).
|
||||
"""
|
||||
base = settings.ghostfolio_url.rstrip("/")
|
||||
token = settings.ghostfolio_token
|
||||
token = token or settings.ghostfolio_token
|
||||
if not token:
|
||||
return "Données indisponibles : jeton Ghostfolio non configuré côté serveur."
|
||||
return "Données indisponibles : aucun jeton Ghostfolio (ni client, ni serveur)."
|
||||
try:
|
||||
r = await client.post(
|
||||
f"{base}/api/v1/auth/anonymous",
|
||||
|
|
@ -58,27 +66,47 @@ async def ghostfolio_block(client: httpx.AsyncClient) -> str:
|
|||
summary = data.get("summary", {}) or {}
|
||||
val = (summary.get("currentValueInBaseCurrency")
|
||||
or summary.get("currentValue") or summary.get("netWorth"))
|
||||
# quelques positions principales (par valeur), si présentes
|
||||
cur = summary.get("baseCurrency") or "EUR"
|
||||
holdings = data.get("holdings", {}) or {}
|
||||
tops = sorted(
|
||||
(h for h in holdings.values() if isinstance(h, dict)),
|
||||
key=lambda h: h.get("valueInBaseCurrency") or 0, reverse=True,
|
||||
)[:5]
|
||||
lines = []
|
||||
if val is not None:
|
||||
lines.append(f"Valeur actuelle du portefeuille : {round(float(val)):,} (devise de base).".replace(",", " "))
|
||||
for h in tops:
|
||||
name = h.get("name") or h.get("symbol") or "?"
|
||||
hv = h.get("valueInBaseCurrency")
|
||||
if hv:
|
||||
lines.append(f"- {name} : {round(float(hv)):,}".replace(",", " "))
|
||||
return "\n".join(lines) if lines else "Aucune donnée de portefeuille renvoyée."
|
||||
return (val, cur, holdings)
|
||||
except httpx.HTTPError:
|
||||
return "Données indisponibles : Ghostfolio injoignable."
|
||||
except (ValueError, KeyError):
|
||||
return "Données indisponibles : réponse Ghostfolio inattendue."
|
||||
|
||||
|
||||
async def ghostfolio_block(client: httpx.AsyncClient, token: str | None = None) -> str:
|
||||
"""Bloc de contexte (valeur + positions principales)."""
|
||||
res = await _ghostfolio_fetch(client, token)
|
||||
if isinstance(res, str):
|
||||
return res
|
||||
val, cur, holdings = res
|
||||
tops = sorted(
|
||||
(h for h in holdings.values() if isinstance(h, dict)),
|
||||
key=lambda h: h.get("valueInBaseCurrency") or 0, reverse=True,
|
||||
)[:5]
|
||||
lines = []
|
||||
if val is not None:
|
||||
lines.append(f"Valeur actuelle du portefeuille : {_money(val)} {cur}.")
|
||||
for h in tops:
|
||||
name = h.get("name") or h.get("symbol") or "?"
|
||||
hv = h.get("valueInBaseCurrency")
|
||||
if hv:
|
||||
lines.append(f"- {name} : {_money(hv)}")
|
||||
return "\n".join(lines) if lines else "Aucune donnée de portefeuille renvoyée."
|
||||
|
||||
|
||||
async def ghostfolio_phrase(client: httpx.AsyncClient, token: str | None = None) -> str:
|
||||
"""Phrase parlable (intent vocal « combien sur mon ghostfolio »)."""
|
||||
res = await _ghostfolio_fetch(client, token)
|
||||
if isinstance(res, str):
|
||||
return res
|
||||
val, cur, _ = res
|
||||
if val is None:
|
||||
return "Je n'ai pas trouvé la valeur du portefeuille dans la réponse de Ghostfolio."
|
||||
return f"Ton portefeuille Ghostfolio vaut environ {_money(val)} {cur}."
|
||||
|
||||
|
||||
async def alerts_block(client: httpx.AsyncClient) -> str:
|
||||
base = settings.alertmanager_url.rstrip("/")
|
||||
try:
|
||||
|
|
@ -153,15 +181,30 @@ _FETCHERS = {
|
|||
}
|
||||
|
||||
|
||||
async def fetch_blocks(client: httpx.AsyncClient, sources: tuple[str, ...]) -> list[tuple[str, str]]:
|
||||
"""Récupère les blocs live demandés (hors « docs », géré par le RAG). Best-effort."""
|
||||
async def fetch_blocks(
|
||||
client: httpx.AsyncClient,
|
||||
sources: tuple[str, ...],
|
||||
secrets: dict | None = None,
|
||||
) -> list[tuple[str, str]]:
|
||||
"""Récupère les blocs live demandés (hors « docs », géré par le RAG). Best-effort.
|
||||
|
||||
`secrets` : jetons fournis par le client (ex. {"ghostfolio_token": …}) → priorité
|
||||
sur les jetons serveur (env). Permet d'unifier la source du jeton côté client.
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
secrets = secrets or {}
|
||||
wanted = [s for s in sources if s in _FETCHERS]
|
||||
if not wanted:
|
||||
return []
|
||||
|
||||
def _call(s):
|
||||
if s == "ghostfolio":
|
||||
return ghostfolio_block(client, secrets.get("ghostfolio_token"))
|
||||
return _FETCHERS[s](client)
|
||||
|
||||
results = await asyncio.gather(
|
||||
*(_FETCHERS[s](client) for s in wanted), return_exceptions=True
|
||||
*(_call(s) for s in wanted), return_exceptions=True
|
||||
)
|
||||
out: list[tuple[str, str]] = []
|
||||
for src, res in zip(wanted, results):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue