feat: switch LLM to OpenWebUI with live model selection
Rebuild & Deploy / rebuild (push) Successful in 3m18s
Rebuild & Deploy / rebuild (push) Successful in 3m18s
- Point the agent at OpenWebUI's OpenAI-compatible API (LLM_BASE_URL,
LLM_API_KEY, LLM_MODEL) with GEMMA_* env fallbacks
- Add list_available_models() fetching {base_url}/models, filtering
non-chat ids (embedding/reranker)
- Rename GemmaLLM -> HopeLLM with update_options(model=...) for runtime
model switching (mutates _opts.model) and instance-based compaction
- Data channel: agent publishes {type: models} after connect and on
get_models; set_model switches live and acks model_changed
- Web UI: Brain section in the settings sheet with a model picker that
persists the choice in localStorage
- Update tests, CI workflow env, and docs for the new endpoint
This commit is contained in:
1 parent
024c42ffb1
commit
360e3276b0
14 files changed
+334
-76
No files matched your search
+86
-2
@@ -27,6 +27,7 @@ const VOICES = [
|
||||
const HISTORY_KEY = "voice_history";
|
||||
const HISTORY_MAX = 50;
|
||||
const MODE_KEY = "hope_conversation_mode";
|
||||
const MODEL_KEY = "preferred_model";
|
||||
|
||||
// ── State ───────────────────────────────────────────────────────────────────
|
||||
let room = null;
|
||||
@@ -40,6 +41,10 @@ let wakeLock = null;
|
||||
let conversationMode = localStorage.getItem(MODE_KEY) === "text" ? "text" : "voice";
|
||||
let connectionPromise = null;
|
||||
|
||||
// ── Model (brain) state — populated from the agent's data channel ──────────
|
||||
let availableModels = []; // ids published by the agent
|
||||
let selectedModel = localStorage.getItem(MODEL_KEY) || null;
|
||||
|
||||
function escapeHtml(s) {
|
||||
const d = document.createElement("div");
|
||||
d.textContent = s || "";
|
||||
@@ -63,6 +68,8 @@ const settingsSheet = document.getElementById("settingsSheet");
|
||||
const sheetBackdrop = document.getElementById("sheetBackdrop");
|
||||
const closeSheetBtn = document.getElementById("closeSheetBtn");
|
||||
const voiceListEl = document.getElementById("voiceList");
|
||||
const modelListEl = document.getElementById("modelList");
|
||||
const modelNoteEl = document.getElementById("modelNote");
|
||||
const clearBtn = document.getElementById("clearBtn");
|
||||
const tasksBtn = document.getElementById("tasksBtn");
|
||||
const tasksPanel = document.getElementById("tasksPanel");
|
||||
@@ -547,8 +554,20 @@ function handleDataPacket(payload, participant, topic) {
|
||||
}
|
||||
} else if (msg.type === "task_event") {
|
||||
handleTaskEvent(msg);
|
||||
} else if (msg.type === "models") {
|
||||
availableModels = Array.isArray(msg.models) ? msg.models : [];
|
||||
// Don't clobber a saved preference — the agent acks the applied
|
||||
// model separately via model_changed.
|
||||
if (!selectedModel && msg.selected) selectedModel = msg.selected;
|
||||
renderModelList();
|
||||
} else if (msg.type === "model_changed") {
|
||||
if (msg.model) {
|
||||
selectedModel = msg.model;
|
||||
localStorage.setItem(MODEL_KEY, selectedModel);
|
||||
renderModelList();
|
||||
}
|
||||
}
|
||||
// set_voice messages flow the other way; nothing to do client-side.
|
||||
// set_voice/set_model messages flow the other way; nothing else to do client-side.
|
||||
} catch (e) {
|
||||
console.warn("bad data packet", e);
|
||||
}
|
||||
@@ -598,8 +617,61 @@ function selectVoice(voiceId) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Model (brain) settings ──────────────────────────────────────────────────
|
||||
function currentModelId() {
|
||||
if (availableModels.length === 0) return selectedModel;
|
||||
if (selectedModel && availableModels.includes(selectedModel)) return selectedModel;
|
||||
return availableModels[0];
|
||||
}
|
||||
|
||||
function renderModelList() {
|
||||
modelListEl.innerHTML = "";
|
||||
if (!availableModels.length) {
|
||||
modelNoteEl.hidden = false;
|
||||
modelNoteEl.textContent = room
|
||||
? "Loading available models…"
|
||||
: "Start a conversation to load the model list.";
|
||||
return;
|
||||
}
|
||||
modelNoteEl.hidden = true;
|
||||
const active = currentModelId();
|
||||
for (const id of availableModels) {
|
||||
const card = document.createElement("div");
|
||||
card.className = "voice-card" + (id === active ? " active" : "");
|
||||
card.dataset.model = id;
|
||||
card.setAttribute("role", "radio");
|
||||
card.setAttribute("aria-checked", String(id === active));
|
||||
card.tabIndex = 0;
|
||||
|
||||
const name = document.createElement("span");
|
||||
name.className = "voice-name";
|
||||
name.textContent = id;
|
||||
|
||||
card.appendChild(name);
|
||||
card.addEventListener("click", () => selectModel(id));
|
||||
card.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
selectModel(id);
|
||||
}
|
||||
});
|
||||
modelListEl.appendChild(card);
|
||||
}
|
||||
}
|
||||
|
||||
function selectModel(modelId) {
|
||||
selectedModel = modelId;
|
||||
localStorage.setItem(MODEL_KEY, modelId);
|
||||
renderModelList();
|
||||
if (room && agentParticipant) {
|
||||
sendModelMessage(modelId);
|
||||
addMessage("system", `Brain changed to ${modelId}`);
|
||||
}
|
||||
}
|
||||
|
||||
function openSheet() {
|
||||
renderVoiceList();
|
||||
renderModelList();
|
||||
sheetBackdrop.hidden = false;
|
||||
settingsSheet.classList.add("open");
|
||||
settingsBtn.setAttribute("aria-expanded", "true");
|
||||
@@ -665,8 +737,9 @@ function markAgentFound(rm, participant) {
|
||||
statusEl.textContent = conversationMode === "text"
|
||||
? "Hope joined · Text chat ready"
|
||||
: "Hope joined · Preparing microphone…";
|
||||
// Synchronize voice and interaction mode when the agent joins.
|
||||
// Synchronize voice, brain and interaction mode when the agent joins.
|
||||
sendVoiceMessage(currentVoiceId(), rm);
|
||||
if (selectedModel) sendModelMessage(selectedModel, rm);
|
||||
sendModeMessage(conversationMode, rm);
|
||||
}
|
||||
|
||||
@@ -980,3 +1053,14 @@ function sendModeMessage(mode, targetRoom) {
|
||||
reliable: true,
|
||||
});
|
||||
}
|
||||
|
||||
// ── Helper: send model selection to the agent via data channel ─────────────
|
||||
function sendModelMessage(model, targetRoom) {
|
||||
const r = targetRoom || room;
|
||||
if (!r || !r.localParticipant || !model) return;
|
||||
const payload = new TextEncoder().encode(JSON.stringify({ type: "set_model", model }));
|
||||
r.localParticipant.publishData(payload, {
|
||||
topic: "voice-control",
|
||||
reliable: true,
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user