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
+3
-3
@@ -9,7 +9,7 @@ tool servers, and agent process.
|
||||
- The container is up: `docker compose up -d` (and healthy)
|
||||
- Host deps installed: `pip install -r tests/requirements.txt`
|
||||
(or `uv pip install -r tests/requirements.txt`)
|
||||
- The LLM at `GEMMA_BASE_URL` (default `http://192.168.86.2:8023/v1`) is reachable
|
||||
- The LLM at `LLM_BASE_URL` (default `http://192.168.86.2:3005/api`, OpenWebUI) is reachable
|
||||
- Internet access for the weather tool tests (wttr.in)
|
||||
|
||||
## Run
|
||||
@@ -25,10 +25,10 @@ python -m pytest tests/ -v -m "not slow" # skip LLM/network-heavy tests
|
||||
| File | Target | Notes |
|
||||
|------|--------|-------|
|
||||
| `test_web_ui.py` | nginx HTTPS UI (:9178) + token endpoint (:8091) | static assets, `/token` (direct + via nginx), `/livekit/` proxy |
|
||||
| `test_llm_api.py` | Gemma LLM API directly | completion, tool calling, `enable_thinking=false` latency, streaming |
|
||||
| `test_llm_api.py` | LLM API directly | completion, tool calling, `enable_thinking=false` latency, streaming |
|
||||
| `test_mcp_tools.py` | MCP servers via `docker exec` | weather/time, authenticated remote memory save/recall/delete, and local skills — probes are cleaned up |
|
||||
| `test_agent_integration.py` | running container | agent process alive, worker registered with LiveKit, all 4 supervised processes up, container health, LiveKit HTTP API |
|
||||
| `test_compaction.py` | `GemmaLLM._compact_context` in-container | compaction triggers >24 items, system prompt preserved, short context untouched |
|
||||
| `test_compaction.py` | `HopeLLM._compact_context` in-container | compaction triggers >24 items, system prompt preserved, short context untouched |
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
+21
-5
@@ -3,7 +3,7 @@
|
||||
Tests run on the HOST machine against the live "voice" container:
|
||||
- HTTPS web UI -> https://localhost:9178 (self-signed cert, verify=False)
|
||||
- Token server -> http://127.0.0.1:8091 (host network mode)
|
||||
- LLM API -> GEMMA_BASE_URL (default http://192.168.86.2:8023/v1)
|
||||
- LLM API -> LLM_BASE_URL (default http://192.168.86.2:3005/api)
|
||||
- In-container -> docker exec voice ...
|
||||
"""
|
||||
|
||||
@@ -18,8 +18,21 @@ import pytest
|
||||
CONTAINER = "voice"
|
||||
WEB_BASE_URL = os.environ.get("VOICE_WEB_URL", "https://localhost:9178")
|
||||
TOKEN_URL = os.environ.get("VOICE_TOKEN_URL", "http://127.0.0.1:8091/token")
|
||||
GEMMA_BASE_URL = os.environ.get("GEMMA_BASE_URL", "http://192.168.86.2:8023/v1").rstrip("/")
|
||||
GEMMA_MODEL = os.environ.get("GEMMA_MODEL", "gemma-4-e4b")
|
||||
LLM_BASE_URL = (
|
||||
os.environ.get("LLM_BASE_URL")
|
||||
or os.environ.get("GEMMA_BASE_URL")
|
||||
or "http://192.168.86.2:3005/api"
|
||||
).rstrip("/")
|
||||
LLM_MODEL = (
|
||||
os.environ.get("LLM_MODEL")
|
||||
or os.environ.get("GEMMA_MODEL")
|
||||
or "gemma-4-e4b"
|
||||
)
|
||||
LLM_API_KEY = (
|
||||
os.environ.get("LLM_API_KEY")
|
||||
or os.environ.get("GEMMA_API_KEY")
|
||||
or "not-needed"
|
||||
)
|
||||
_users = json.loads((Path(__file__).parents[1] / "config" / "users.json").read_text())
|
||||
_shane_memory = _users["users"]["shane"]["memory"]
|
||||
MEMORY_MCP_URL = os.environ.get("MEMORY_MCP_URL", _shane_memory["url"])
|
||||
@@ -88,8 +101,11 @@ def token() -> dict:
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def llm_client():
|
||||
"""httpx client pointed at the Gemma LLM API (same one Hope uses)."""
|
||||
with httpx.Client(base_url=GEMMA_BASE_URL, timeout=60) as c:
|
||||
"""httpx client pointed at the LLM API (same one Hope uses)."""
|
||||
headers = {}
|
||||
if LLM_API_KEY and LLM_API_KEY != "not-needed":
|
||||
headers["Authorization"] = f"Bearer {LLM_API_KEY}"
|
||||
with httpx.Client(base_url=LLM_BASE_URL, headers=headers, timeout=60) as c:
|
||||
yield c
|
||||
|
||||
|
||||
|
||||
+12
-11
@@ -1,26 +1,27 @@
|
||||
"""Tests for GemmaLLM context compaction (agent.GemmaLLM._compact_context).
|
||||
"""Tests for HopeLLM context compaction (agent.HopeLLM._compact_context).
|
||||
|
||||
Compaction runs a real side LLM call to the xNAS, so these are marked slow.
|
||||
The agent module is imported inside the container via docker exec.
|
||||
Compaction runs a real side LLM call to the configured endpoint, so these
|
||||
are marked slow. The agent module is imported inside the container via
|
||||
docker exec.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from conftest import GEMMA_BASE_URL, GEMMA_MODEL, docker_exec_python
|
||||
from conftest import LLM_BASE_URL, LLM_MODEL, docker_exec_python
|
||||
|
||||
# {num_messages} and {do_chat} are filled per test; all other braces are literal.
|
||||
TEMPLATE = '''
|
||||
import logging, sys
|
||||
import logging, os, sys
|
||||
from livekit.agents import llm as lk_llm
|
||||
sys.path.insert(0, "/opt/voice-agent")
|
||||
from agent import GemmaLLM
|
||||
from agent import HopeLLM
|
||||
|
||||
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
|
||||
|
||||
llm = GemmaLLM(
|
||||
llm = HopeLLM(
|
||||
model="{model}",
|
||||
base_url="{base_url}",
|
||||
api_key="not-needed",
|
||||
api_key=os.environ.get("LLM_API_KEY", "not-needed"),
|
||||
max_completion_tokens=100,
|
||||
extra_body={{"chat_template_kwargs": {{"enable_thinking": False}}}},
|
||||
)
|
||||
@@ -44,7 +45,7 @@ async def main():
|
||||
print("AFTER items=" + str(len(compacted.items)))
|
||||
for it in compacted.items:
|
||||
role = getattr(it, "role", None)
|
||||
content = GemmaLLM._msg_content_text(it)
|
||||
content = HopeLLM._msg_content_text(it)
|
||||
print("ITEM " + str(role) + ": " + content[:120])
|
||||
|
||||
{do_chat}
|
||||
@@ -57,8 +58,8 @@ CHAT_SNIPPET = ' stream = llm.chat(chat_ctx=compacted)\n collected = await
|
||||
|
||||
def _run(num_messages: int, *, do_chat: bool = False) -> str:
|
||||
code = TEMPLATE.format(
|
||||
model=GEMMA_MODEL,
|
||||
base_url=GEMMA_BASE_URL,
|
||||
model=LLM_MODEL,
|
||||
base_url=LLM_BASE_URL,
|
||||
num_messages=num_messages,
|
||||
do_chat=CHAT_SNIPPET if do_chat else "pass",
|
||||
)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Tests for the Gemma LLM API — the exact endpoint Hope's agent uses."""
|
||||
"""Tests for the LLM API — the exact endpoint Hope's agent uses."""
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from conftest import GEMMA_BASE_URL, GEMMA_MODEL
|
||||
from conftest import LLM_BASE_URL, LLM_MODEL
|
||||
|
||||
WEATHER_TOOL = {
|
||||
"type": "function",
|
||||
@@ -22,7 +22,7 @@ WEATHER_TOOL = {
|
||||
|
||||
def _chat(llm_client, messages, *, tools=None, stream=False, max_tokens=200):
|
||||
payload = {
|
||||
"model": GEMMA_MODEL,
|
||||
"model": LLM_MODEL,
|
||||
"messages": messages,
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": 0.3,
|
||||
|
||||
Reference in new issue
Block a user