Files
hope-voice-api/tests/test_compaction.py
T
snowspeeder 360e3276b0
Rebuild & Deploy / rebuild (push) Successful in 3m18s
feat: switch LLM to OpenWebUI with live model selection
- 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
2026-08-29 12:09:03 -04:00

95 lines
3.2 KiB
Python

"""Tests for HopeLLM context compaction (agent.HopeLLM._compact_context).
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 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, os, sys
from livekit.agents import llm as lk_llm
sys.path.insert(0, "/opt/voice-agent")
from agent import HopeLLM
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
llm = HopeLLM(
model="{model}",
base_url="{base_url}",
api_key=os.environ.get("LLM_API_KEY", "not-needed"),
max_completion_tokens=100,
extra_body={{"chat_template_kwargs": {{"enable_thinking": False}}}},
)
items = [lk_llm.ChatMessage(role="system", content=["You are Hope, a voice assistant."])]
for i in range({num_messages}):
items.append(lk_llm.ChatMessage(role="user", content=["user message number " + str(i)]))
items.append(lk_llm.ChatMessage(role="assistant", content=["agent reply number " + str(i)]))
ctx = lk_llm.ChatContext(items)
print("BUILT ctx with " + str(len(ctx.items)) + " items")
import asyncio
async def main():
try:
compacted = llm._compact_context(ctx, None)
except Exception as e:
print("COMPACT_ERROR " + repr(e))
return
print("AFTER items=" + str(len(compacted.items)))
for it in compacted.items:
role = getattr(it, "role", None)
content = HopeLLM._msg_content_text(it)
print("ITEM " + str(role) + ": " + content[:120])
{do_chat}
asyncio.run(main())
'''
CHAT_SNIPPET = ' stream = llm.chat(chat_ctx=compacted)\n collected = await stream.collect()\n print("CHAT_OK text_len=" + str(len(collected.text)))'
def _run(num_messages: int, *, do_chat: bool = False) -> str:
code = TEMPLATE.format(
model=LLM_MODEL,
base_url=LLM_BASE_URL,
num_messages=num_messages,
do_chat=CHAT_SNIPPET if do_chat else "pass",
)
proc = docker_exec_python(code, timeout=120, check=False)
return proc.stdout + (("\n" + proc.stderr) if proc.stderr else "")
@pytest.mark.slow
def test_compaction_triggers_on_size():
"""A context with > 24 items must compact without crashing and chat() must still work."""
# 13 pairs = 26 non-system + 1 system = 27 items (> threshold of 24)
out = _run(13, do_chat=True)
assert "BUILT ctx with 27 items" in out
assert "Context compacted:" in out
assert "CHAT_OK text_len=" in out
@pytest.mark.slow
def test_compaction_preserves_system_prompt():
out = _run(28)
lines = [l for l in out.splitlines() if l.startswith("ITEM system:")]
assert lines, f"no system message survived compaction:\n{out}"
assert "You are Hope" in lines[0]
def test_short_context_no_compaction():
"""Context with non_sys <= KEEP_RECENT must NOT be compacted."""
# system + 4 user/assistant pairs = 9 items, non_sys=8 <= KEEP_RECENT(10)
out = _run(4)
assert "BUILT ctx with 9 items" in out
# _compact_context should return the context unchanged (early return)
assert "AFTER items=9" in out, f"expected no compaction:\n{out}"