feat(stt-client): sélecteur de contexte + visualiseur dans le HUD (#39)

Moitié client de la fonctionnalité contextes (le serveur #38 est déployé). Tu choisis
un contexte pour Asa et tu vois exactement ce qu'on lui envoie.

- Sélecteur (Réglages → « Contexte d'Asa ») : liste peuplée depuis GET /v1/contexts ;
  le choix est envoyé en settings{context}, persisté en localStorage, et réappliqué au
  backend à la connexion. Masqué si le serveur ne supporte pas (dégradation).
- Visualiseur (icône ▤ de la barre) : drawer affichant le contexte assemblé renvoyé
  après chaque tour — consigne (system prompt), données live injectées, doc RAG, mémoire.
- api.py : ServerClient envoie `context`, expose last_context + contexts().
- engine.py : context_provider → émet {type:context} après chaque réponse.
- app.py : récupère /v1/contexts au démarrage, diffuse à la connexion, applique le
  changement de contexte (client.context).
- bump 0.10.0 → 0.11.0.

Validé via Playwright : sélecteur (5 contextes), changement → settings{context},
visualiseur (consigne + bloc live Ghostfolio + mémoire), drawer partagé. Rendu conforme.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ALI YESILKAYA 2026-06-21 02:57:03 +02:00 committed by GitHub
parent 89aeb418ff
commit de4b66ed56
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 239 additions and 14 deletions

View file

@ -16,25 +16,47 @@ class ServerClient:
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
# 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
r = requests.post(
f"{self.url}/v1/ask",
json={
"text": text,
"model": model or self.model,
"session_id": self.session_id,
},
timeout=self.timeout,
)
payload = {
"text": text,
"model": model or self.model,
"session_id": self.session_id,
}
if self.context:
payload["context"] = self.context
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()
return r.json()["reply"].strip()
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 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