mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 13:34:43 +02:00
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:
parent
89aeb418ff
commit
de4b66ed56
5 changed files with 239 additions and 14 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "stt"
|
name = "stt"
|
||||||
version = "0.10.0"
|
version = "0.11.0"
|
||||||
description = "STT — client vocal Jarvis du homelab Funk (voix + HUD, parle au STT-server)"
|
description = "STT — client vocal Jarvis du homelab Funk (voix + HUD, parle au STT-server)"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
|
||||||
|
|
@ -16,25 +16,47 @@ class ServerClient:
|
||||||
self.url = cfg["url"].rstrip("/")
|
self.url = cfg["url"].rstrip("/")
|
||||||
self.timeout = cfg.get("timeout_sec", 90)
|
self.timeout = cfg.get("timeout_sec", 90)
|
||||||
self.model = cfg.get("model", "hermes-default")
|
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
|
# une session par run → mémoire court-terme côté serveur
|
||||||
self.session_id = uuid.uuid4().hex
|
self.session_id = uuid.uuid4().hex
|
||||||
|
|
||||||
def ask(self, text: str, model: str | None = None) -> str:
|
def ask(self, text: str, model: str | None = None) -> str:
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
r = requests.post(
|
payload = {
|
||||||
f"{self.url}/v1/ask",
|
"text": text,
|
||||||
json={
|
"model": model or self.model,
|
||||||
"text": text,
|
"session_id": self.session_id,
|
||||||
"model": model or self.model,
|
}
|
||||||
"session_id": self.session_id,
|
if self.context:
|
||||||
},
|
payload["context"] = self.context
|
||||||
timeout=self.timeout,
|
r = requests.post(f"{self.url}/v1/ask", json=payload, timeout=self.timeout)
|
||||||
)
|
|
||||||
if r.status_code == 400:
|
if r.status_code == 400:
|
||||||
raise RuntimeError(r.json().get("detail", "requête invalide"))
|
raise RuntimeError(r.json().get("detail", "requête invalide"))
|
||||||
r.raise_for_status()
|
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:
|
def reset(self) -> None:
|
||||||
import requests
|
import requests
|
||||||
|
|
|
||||||
|
|
@ -753,6 +753,35 @@
|
||||||
.svc-actions .btn { width: auto; font-size: 14px; padding: 11px 10px; }
|
.svc-actions .btn { width: auto; font-size: 14px; padding: 11px 10px; }
|
||||||
.svc-actions .btn#svcStop { grid-column: 1 / -1; } /* Arrêter sur toute la largeur */
|
.svc-actions .btn#svcStop { grid-column: 1 / -1; } /* Arrêter sur toute la largeur */
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
VISUALISEUR DE CONTEXTE
|
||||||
|
============================================================ */
|
||||||
|
.ctx-current {
|
||||||
|
display: flex; align-items: center; gap: 10px;
|
||||||
|
padding: 10px 14px; margin-bottom: 16px;
|
||||||
|
border-radius: 12px; background: var(--bg-soft); border: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.ctx-current .ctx-ico { font-size: 20px; }
|
||||||
|
.ctx-current .ctx-label { font-size: 14px; font-weight: 600; }
|
||||||
|
.ctx-section { padding: 14px 0; border-bottom: 1px solid var(--line); }
|
||||||
|
.ctx-section .flabel { display: block; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-soft); margin-bottom: 8px; }
|
||||||
|
.ctx-pre {
|
||||||
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
||||||
|
font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
|
||||||
|
font-size: 12px; line-height: 1.5; color: var(--ink);
|
||||||
|
background: var(--bg-soft); border: 1px solid var(--line);
|
||||||
|
border-radius: 10px; padding: 12px; max-height: 220px; overflow-y: auto;
|
||||||
|
}
|
||||||
|
.ctx-block { margin-bottom: 10px; }
|
||||||
|
.ctx-block .ctx-block-title { font-size: 12px; font-weight: 600; color: var(--accent-active); margin-bottom: 4px; }
|
||||||
|
.ctx-block .ctx-pre { max-height: 160px; }
|
||||||
|
.ctx-item {
|
||||||
|
font-size: 12.5px; line-height: 1.5; color: var(--ink-soft);
|
||||||
|
padding: 8px 10px; margin-bottom: 6px;
|
||||||
|
background: var(--bg-soft); border: 1px solid var(--line);
|
||||||
|
border-radius: 8px; white-space: pre-wrap; word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
/* Toast — confirmation éphémère (clic / voix) */
|
/* Toast — confirmation éphémère (clic / voix) */
|
||||||
.toast {
|
.toast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
@ -833,6 +862,13 @@
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Visualiseur : voir le contexte envoyé à Asa -->
|
||||||
|
<button class="icon-btn context" id="openContext" aria-label="Voir le contexte d'Asa" title="Contexte envoyé à Asa">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M4 5h16M4 10h16M4 15h10M4 20h7"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button class="icon-btn gear" id="openSettings" aria-label="Ouvrir les réglages" title="Réglages">
|
<button class="icon-btn gear" id="openSettings" aria-label="Ouvrir les réglages" title="Réglages">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
<circle cx="12" cy="12" r="3.2"></circle>
|
<circle cx="12" cy="12" r="3.2"></circle>
|
||||||
|
|
@ -961,6 +997,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Contexte d'Asa -->
|
||||||
|
<div class="field" id="contextField" hidden>
|
||||||
|
<label for="contextSelect" class="flabel">Contexte d'Asa</label>
|
||||||
|
<p class="hint">Oriente Asa sur un domaine (injecte des données live). Voir l'icône ▤ pour le détail.</p>
|
||||||
|
<select id="contextSelect" aria-label="Contexte d'Asa"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- No-think -->
|
<!-- No-think -->
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
@ -1046,6 +1089,43 @@
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
<!-- ===================== VISUALISEUR DE CONTEXTE (voile partagé) ===================== -->
|
||||||
|
<aside class="drawer" id="contextDrawer" role="dialog" aria-modal="true" aria-labelledby="contextTitle" aria-hidden="true">
|
||||||
|
<header>
|
||||||
|
<h2 id="contextTitle">Contexte d'Asa</h2>
|
||||||
|
<button class="icon-btn" id="closeContext" aria-label="Fermer le visualiseur" title="Fermer">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true">
|
||||||
|
<path d="M6 6l12 12M18 6L6 18"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<div class="body">
|
||||||
|
<p class="hint" style="margin:6px 0 14px">Exactement ce qui est envoyé à Asa au dernier tour : consigne, données live, doc et mémoire.</p>
|
||||||
|
<div class="ctx-current" id="ctxCurrent" hidden>
|
||||||
|
<span class="ctx-ico" id="ctxIco">🛠️</span>
|
||||||
|
<span class="ctx-label" id="ctxLabel">—</span>
|
||||||
|
</div>
|
||||||
|
<p class="ctx-empty hint" id="ctxEmpty">Aucun contexte encore. Pose une question à Asa pour voir ce qu'il reçoit.</p>
|
||||||
|
|
||||||
|
<div class="ctx-section" id="ctxSysWrap" hidden>
|
||||||
|
<span class="flabel">Consigne (system prompt)</span>
|
||||||
|
<pre class="ctx-pre" id="ctxSystem"></pre>
|
||||||
|
</div>
|
||||||
|
<div class="ctx-section" id="ctxBlocksWrap" hidden>
|
||||||
|
<span class="flabel">Données live injectées</span>
|
||||||
|
<div id="ctxBlocks"></div>
|
||||||
|
</div>
|
||||||
|
<div class="ctx-section" id="ctxDocsWrap" hidden>
|
||||||
|
<span class="flabel">Documentation (RAG)</span>
|
||||||
|
<div id="ctxDocs"></div>
|
||||||
|
</div>
|
||||||
|
<div class="ctx-section" id="ctxMemWrap" hidden>
|
||||||
|
<span class="flabel">Mémoire long-terme</span>
|
||||||
|
<div id="ctxMem"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
<!-- Toast (confirmation d'ouverture, clic ou voix) -->
|
<!-- Toast (confirmation d'ouverture, clic ou voix) -->
|
||||||
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||||||
|
|
||||||
|
|
@ -1168,6 +1248,8 @@ function handleEvent(ev) {
|
||||||
case 'mic': setMicUI(!!ev.muted); break; // état micro renvoyé par le backend
|
case 'mic': setMicUI(!!ev.muted); break; // état micro renvoyé par le backend
|
||||||
case 'services': renderServices(ev.services || []); break; // tuiles du portail
|
case 'services': renderServices(ev.services || []); break; // tuiles du portail
|
||||||
case 'portal-status': applyPortalStatus(ev); break; // santé live des services
|
case 'portal-status': applyPortalStatus(ev); break; // santé live des services
|
||||||
|
case 'contexts': applyContexts(ev); break; // contextes présélectionnables
|
||||||
|
case 'context': renderContextViewer(ev.context); break; // contexte assemblé (visualiseur)
|
||||||
case 'toast': showToast(ev.text || ''); break; // confirmation (clic/voix)
|
case 'toast': showToast(ev.text || ''); break; // confirmation (clic/voix)
|
||||||
default: console.warn('Événement inconnu :', ev);
|
default: console.warn('Événement inconnu :', ev);
|
||||||
}
|
}
|
||||||
|
|
@ -1553,10 +1635,12 @@ function flashSaved() {
|
||||||
const scrim = el('scrim');
|
const scrim = el('scrim');
|
||||||
const drawer = el('drawer');
|
const drawer = el('drawer');
|
||||||
const svcDrawer = el('servicesDrawer');
|
const svcDrawer = el('servicesDrawer');
|
||||||
|
const ctxDrawer = el('contextDrawer');
|
||||||
|
const ALL_DRAWERS = [drawer, svcDrawer, ctxDrawer];
|
||||||
|
|
||||||
function closeDrawers() {
|
function closeDrawers() {
|
||||||
scrim.classList.remove('open');
|
scrim.classList.remove('open');
|
||||||
for (const d of [drawer, svcDrawer]) {
|
for (const d of ALL_DRAWERS) {
|
||||||
d.classList.remove('open');
|
d.classList.remove('open');
|
||||||
d.setAttribute('aria-hidden', 'true');
|
d.setAttribute('aria-hidden', 'true');
|
||||||
}
|
}
|
||||||
|
|
@ -1570,15 +1654,18 @@ function openDrawer(which, onOpen) {
|
||||||
}
|
}
|
||||||
const openSettings = () => openDrawer(drawer, () => intensity.focus());
|
const openSettings = () => openDrawer(drawer, () => intensity.focus());
|
||||||
const openServices = () => { showHome(); openDrawer(svcDrawer); };
|
const openServices = () => { showHome(); openDrawer(svcDrawer); };
|
||||||
|
const openContext = () => openDrawer(ctxDrawer);
|
||||||
|
|
||||||
/* ----- Écouteurs Réglages + Services ----- */
|
/* ----- Écouteurs Réglages + Services + Contexte ----- */
|
||||||
el('openSettings').addEventListener('click', openSettings);
|
el('openSettings').addEventListener('click', openSettings);
|
||||||
el('closeSettings').addEventListener('click', () => { closeDrawers(); el('openSettings').focus(); });
|
el('closeSettings').addEventListener('click', () => { closeDrawers(); el('openSettings').focus(); });
|
||||||
el('openServices').addEventListener('click', openServices);
|
el('openServices').addEventListener('click', openServices);
|
||||||
el('closeServices').addEventListener('click', () => { closeDrawers(); el('openServices').focus(); });
|
el('closeServices').addEventListener('click', () => { closeDrawers(); el('openServices').focus(); });
|
||||||
|
el('openContext').addEventListener('click', openContext);
|
||||||
|
el('closeContext').addEventListener('click', () => { closeDrawers(); el('openContext').focus(); });
|
||||||
scrim.addEventListener('click', closeDrawers);
|
scrim.addEventListener('click', closeDrawers);
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape' && (drawer.classList.contains('open') || svcDrawer.classList.contains('open'))) closeDrawers();
|
if (e.key === 'Escape' && ALL_DRAWERS.some(d => d.classList.contains('open'))) closeDrawers();
|
||||||
});
|
});
|
||||||
|
|
||||||
intensity.addEventListener('input', () => { applyIntensity(Number(intensity.value)); });
|
intensity.addEventListener('input', () => { applyIntensity(Number(intensity.value)); });
|
||||||
|
|
@ -1591,6 +1678,84 @@ nothink.addEventListener('change', () => { persistLocal(); sendSettings(); });
|
||||||
brainLocal.addEventListener('click', () => { setBrain('local'); persistLocal(); sendSettings(); });
|
brainLocal.addEventListener('click', () => { setBrain('local'); persistLocal(); sendSettings(); });
|
||||||
brainAdv.addEventListener('click', () => { setBrain('advanced'); persistLocal(); sendSettings(); });
|
brainAdv.addEventListener('click', () => { setBrain('advanced'); persistLocal(); sendSettings(); });
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
CONTEXTE D'ASA — sélecteur (Réglages) + visualiseur (drawer dédié)
|
||||||
|
Le backend pousse {"type":"contexts",...} à la connexion (liste + courant) et
|
||||||
|
{"type":"context","context":{...}} après chaque tour (contexte assemblé).
|
||||||
|
============================================================================ */
|
||||||
|
const CTX_LS = 'stt-context';
|
||||||
|
|
||||||
|
function sendContext(id) {
|
||||||
|
if (DEMO || !ws || ws.readyState !== WebSocket.OPEN) { console.log('[DÉMO] context →', id); return; }
|
||||||
|
ws.send(JSON.stringify({ type: 'settings', data: { context: id } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyContexts(msg) {
|
||||||
|
const list = (msg && msg.contexts) || [];
|
||||||
|
const field = document.getElementById('contextField');
|
||||||
|
const sel = document.getElementById('contextSelect');
|
||||||
|
if (!list.length) { field.hidden = true; return; } // serveur antérieur → pas de sélecteur
|
||||||
|
sel.innerHTML = '';
|
||||||
|
for (const c of list) {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = c.id;
|
||||||
|
o.textContent = `${c.icon || ''} ${c.label}`.trim();
|
||||||
|
o.title = c.description || '';
|
||||||
|
sel.appendChild(o);
|
||||||
|
}
|
||||||
|
field.hidden = false;
|
||||||
|
let saved = null;
|
||||||
|
try { saved = localStorage.getItem(CTX_LS); } catch {}
|
||||||
|
const want = (saved && list.some(c => c.id === saved)) ? saved : (msg.current || msg.default);
|
||||||
|
if (want) sel.value = want;
|
||||||
|
// si la sauvegarde diffère du backend, on l'aligne tout de suite
|
||||||
|
if (saved && saved !== msg.current) sendContext(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('contextSelect').addEventListener('change', (e) => {
|
||||||
|
const id = e.target.value;
|
||||||
|
try { localStorage.setItem(CTX_LS, id); } catch {}
|
||||||
|
sendContext(id);
|
||||||
|
flashSaved();
|
||||||
|
});
|
||||||
|
|
||||||
|
function _ctxSection(id, show) { document.getElementById(id).hidden = !show; }
|
||||||
|
function _ctxItems(wrapId, listId, items) {
|
||||||
|
_ctxSection(wrapId, items.length > 0);
|
||||||
|
const c = document.getElementById(listId); c.innerHTML = '';
|
||||||
|
for (const it of items) {
|
||||||
|
const d = document.createElement('div');
|
||||||
|
d.className = 'ctx-item';
|
||||||
|
d.textContent = (typeof it === 'string') ? it : JSON.stringify(it);
|
||||||
|
c.appendChild(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderContextViewer(ctx) {
|
||||||
|
if (!ctx) return;
|
||||||
|
document.getElementById('ctxEmpty').hidden = true;
|
||||||
|
document.getElementById('ctxCurrent').hidden = false;
|
||||||
|
document.getElementById('ctxIco').textContent = ctx.icon || '🧠';
|
||||||
|
document.getElementById('ctxLabel').textContent = ctx.label || ctx.id || 'Contexte';
|
||||||
|
|
||||||
|
_ctxSection('ctxSysWrap', !!ctx.system_prompt);
|
||||||
|
document.getElementById('ctxSystem').textContent = ctx.system_prompt || '';
|
||||||
|
|
||||||
|
const blocks = ctx.blocks || [];
|
||||||
|
_ctxSection('ctxBlocksWrap', blocks.length > 0);
|
||||||
|
const bc = document.getElementById('ctxBlocks'); bc.innerHTML = '';
|
||||||
|
for (const b of blocks) {
|
||||||
|
const wrap = document.createElement('div'); wrap.className = 'ctx-block';
|
||||||
|
const t = document.createElement('div'); t.className = 'ctx-block-title';
|
||||||
|
t.textContent = b.title || b.source || 'source';
|
||||||
|
const pre = document.createElement('pre'); pre.className = 'ctx-pre';
|
||||||
|
pre.textContent = b.text || '';
|
||||||
|
wrap.appendChild(t); wrap.appendChild(pre); bc.appendChild(wrap);
|
||||||
|
}
|
||||||
|
_ctxItems('ctxDocsWrap', 'ctxDocs', ctx.docs || []);
|
||||||
|
_ctxItems('ctxMemWrap', 'ctxMem', ctx.memories || []);
|
||||||
|
}
|
||||||
|
|
||||||
el('resetConv').addEventListener('click', () => {
|
el('resetConv').addEventListener('click', () => {
|
||||||
clearTranscript();
|
clearTranscript();
|
||||||
setState('idle');
|
setState('idle');
|
||||||
|
|
@ -1665,6 +1830,17 @@ if (DEMO) {
|
||||||
argocd: { state:'down', health:'down', latency_ms:null, components:[], alerts:[] },
|
argocd: { state:'down', health:'down', latency_ms:null, components:[], alerts:[] },
|
||||||
n8n: { state:'up', health:'ok', latency_ms:16, components:[{name:'Application',state:'up'}], alerts:[] },
|
n8n: { state:'up', health:'ok', latency_ms:16, components:[{name:'Application',state:'up'}], alerts:[] },
|
||||||
}});
|
}});
|
||||||
|
applyContexts({ default:'funk', current:'ghostfolio', contexts:[
|
||||||
|
{ id:'funk', label:'Funk · cluster', icon:'🛠️', description:'Doc homelab' },
|
||||||
|
{ id:'ghostfolio', label:'Ghostfolio', icon:'💰', description:'Portefeuille' },
|
||||||
|
{ id:'grafana', label:'Grafana · métriques', icon:'📊', description:'Observabilité' },
|
||||||
|
{ id:'alerting', label:'Alerting', icon:'🚨', description:'Alertes actives' },
|
||||||
|
{ id:'cluster', label:'Gestion cluster', icon:'🚢', description:'État cluster' },
|
||||||
|
]});
|
||||||
|
renderContextViewer({ id:'ghostfolio', label:'Ghostfolio', icon:'💰',
|
||||||
|
system_prompt:'Tu es Hermes… Tu aides sur le portefeuille d\'investissement. Appuie-toi sur les données Ghostfolio fournies ci-dessous.',
|
||||||
|
blocks:[{ source:'ghostfolio', title:'Portefeuille Ghostfolio (données live)', text:'Valeur actuelle : 12 345 (devise de base).\n- ETF Monde : 8 100\n- Bitcoin : 2 400' }],
|
||||||
|
docs:[], memories:['aime les ETF capitalisants'] });
|
||||||
runDemo();
|
runDemo();
|
||||||
}
|
}
|
||||||
else connectWS();
|
else connectWS();
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,16 @@ async def serve(cfg: dict[str, Any], make_engine, client) -> None:
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Contextes présélectionnables (récupérés du serveur ; None = serveur antérieur).
|
||||||
|
# Appel HTTP bloquant → hors boucle. Le HUD reçoit la liste à la connexion.
|
||||||
|
contexts_info = await loop.run_in_executor(None, client.contexts)
|
||||||
|
contexts_msg = {
|
||||||
|
"type": "contexts",
|
||||||
|
"contexts": (contexts_info or {}).get("contexts", []),
|
||||||
|
"default": (contexts_info or {}).get("default"),
|
||||||
|
"current": client.context or (contexts_info or {}).get("default"),
|
||||||
|
}
|
||||||
|
|
||||||
def broadcast(event: dict) -> None:
|
def broadcast(event: dict) -> None:
|
||||||
nonlocal last_state, last_mic, last_status
|
nonlocal last_state, last_mic, last_status
|
||||||
if event.get("type") == "state":
|
if event.get("type") == "state":
|
||||||
|
|
@ -142,6 +152,7 @@ async def serve(cfg: dict[str, Any], make_engine, client) -> None:
|
||||||
await ws.send(json.dumps(last_state, ensure_ascii=False))
|
await ws.send(json.dumps(last_state, ensure_ascii=False))
|
||||||
await ws.send(json.dumps(last_mic, ensure_ascii=False)) # état micro courant
|
await ws.send(json.dumps(last_mic, ensure_ascii=False)) # état micro courant
|
||||||
await ws.send(json.dumps(services_msg, ensure_ascii=False)) # tuiles du portail
|
await ws.send(json.dumps(services_msg, ensure_ascii=False)) # tuiles du portail
|
||||||
|
await ws.send(json.dumps(contexts_msg, ensure_ascii=False)) # contextes Asa
|
||||||
if last_status is not None: # santé connue → l'envoyer tout de suite
|
if last_status is not None: # santé connue → l'envoyer tout de suite
|
||||||
await ws.send(json.dumps(last_status, ensure_ascii=False))
|
await ws.send(json.dumps(last_status, ensure_ascii=False))
|
||||||
# Le HUD envoie {"type":"settings","data":{...}} (réglage),
|
# Le HUD envoie {"type":"settings","data":{...}} (réglage),
|
||||||
|
|
@ -186,6 +197,8 @@ async def serve(cfg: dict[str, Any], make_engine, client) -> None:
|
||||||
_start_http(ui["ws_host"], ui["http_port"])
|
_start_http(ui["ws_host"], ui["http_port"])
|
||||||
|
|
||||||
engine = make_engine(emit)
|
engine = make_engine(emit)
|
||||||
|
# le moteur expose au HUD le dernier contexte assemblé renvoyé par le serveur
|
||||||
|
engine.context_provider = lambda: client.last_context
|
||||||
# chargement Whisper + boucle audio dans un thread dédié
|
# chargement Whisper + boucle audio dans un thread dédié
|
||||||
threading.Thread(target=_run_engine, args=(engine, emit), daemon=True).start()
|
threading.Thread(target=_run_engine, args=(engine, emit), daemon=True).start()
|
||||||
|
|
||||||
|
|
@ -340,6 +353,9 @@ def _apply_settings(client, engine, data: dict, loop) -> None:
|
||||||
from stt.config import resolve_model
|
from stt.config import resolve_model
|
||||||
|
|
||||||
client.model = resolve_model("hermes" if brain == "local" else "claude")
|
client.model = resolve_model("hermes" if brain == "local" else "claude")
|
||||||
|
ctx = (data.get("context") or "").strip()
|
||||||
|
if ctx:
|
||||||
|
client.context = ctx # contexte présélectionné, envoyé au prochain /v1/ask
|
||||||
wake = (data.get("wakeword") or "").strip()
|
wake = (data.get("wakeword") or "").strip()
|
||||||
if wake and engine is not None:
|
if wake and engine is not None:
|
||||||
from stt.voice.engine import _deaccent
|
from stt.voice.engine import _deaccent
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,9 @@ class VoiceEngine:
|
||||||
self.no_tts = no_tts
|
self.no_tts = no_tts
|
||||||
# callable renvoyant l'alias modèle courant → affiché en tag dans le HUD
|
# callable renvoyant l'alias modèle courant → affiché en tag dans le HUD
|
||||||
self.model_label = model_label
|
self.model_label = model_label
|
||||||
|
# callable renvoyant le dernier contexte assemblé (serveur) → visualiseur HUD.
|
||||||
|
# Renseigné après coup par le serveur (app.py) ; None tant qu'absent.
|
||||||
|
self.context_provider: Callable[[], dict | None] | None = None
|
||||||
# backend ASR enfichable (whisper par défaut, onnx/parakeet en option)
|
# backend ASR enfichable (whisper par défaut, onnx/parakeet en option)
|
||||||
self._asr = make_backend(cfg)
|
self._asr = make_backend(cfg)
|
||||||
self._stop = False
|
self._stop = False
|
||||||
|
|
@ -165,6 +168,14 @@ class VoiceEngine:
|
||||||
return
|
return
|
||||||
mode = self.model_label() if self.model_label else None
|
mode = self.model_label() if self.model_label else None
|
||||||
self.emit({"type": "assistant", "text": resp, "mode": mode})
|
self.emit({"type": "assistant", "text": resp, "mode": mode})
|
||||||
|
# contexte assemblé renvoyé par le serveur → visualiseur HUD (best-effort)
|
||||||
|
if self.context_provider is not None:
|
||||||
|
try:
|
||||||
|
ctx = self.context_provider()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
ctx = None
|
||||||
|
if ctx:
|
||||||
|
self.emit({"type": "context", "context": ctx})
|
||||||
if self.memory:
|
if self.memory:
|
||||||
self.memory.log("assistant", resp)
|
self.memory.log("assistant", resp)
|
||||||
self.emit({"type": "state", "state": "speaking"})
|
self.emit({"type": "state", "state": "speaking"})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue