Commit Graph
56 Commits
Author SHA1 Message Date
snowspeeder 8a7cca456f fix: force tool calls for drifted intents and harden tool-use prompt
Small local models (qwen2.5-vl-3b, gemma-4-e4b) narrate actions ('Sure,
I'll pause the music') without emitting the tool call once the full
system prompt and 26-tool set are in context. Measured: qwen emitted
tools in 2/8 realistic intents, gemma 5/8; single-tool tests pass 100%,
so this is context-induced drift, not plumbing.

- agent/intent_router.py: deterministic forced-call router for
  high-frequency intents — media controls, now_playing/list_library,
  'remember that ...' memory saves, explicit YouTube Music searches,
  and personal-fact recall (memory_recall/search_notes/skill_recall,
  excluded for weather/time questions). Fires only when the tool is
  available; guards against how-do-I questions.
- agent.py: generalize _ForcedToolCallStream to any tool; route through
  intent_router after the dispatch check. System prompt adds a hard
  'call, don't narrate' rule and reconditions 'answer directly'.
- tests/test_intent_router.py: 30 unit tests pinning router behavior.

Verified against the live LLM with the full fidelity harness: combined
forced-or-emitted coverage 8/8 on qwen2.5-vl-3b (was 2/8) and 8/8 on
gemma-4-e4b (was 5/8). Full suite: 79 passed.
2026-08-29 15:38:35 -04:00
snowspeeder 48b92585c6 fix: harden media server — temp-file yt-dlp stderr, parse guards, UI failure surfacing
Rebuild & Deploy / rebuild (push) Failing after 4m27s
- yt-dlp stderr now goes to a temp file so a chatty stderr can never
  deadlock the stdout progress loop.
- report/search/seek numeric parsing returns 400 instead of 500.
- now-playing card reflects failed/preparing download states.
- media runtime data (library.json, jobs/) is gitignored; only
  .gitkeep is tracked.
2026-08-29 13:27:58 -04:00
snowspeeder f534b8ccb5 chore: ignore media library runtime data, keep directory via .gitkeep 2026-08-29 13:23:21 -04:00
snowspeeder a53c090882 docs: media player subsystem architecture and conventions
Document the media server process, /media/ port map, library layout on
the ./media bind mount, Hope's media MCP toolset, the music/mic
mutual-exclusion rule, and yt-dlp download/proxy conventions.
2026-08-29 13:22:01 -04:00
snowspeeder 04c4a53594 feat: media player API, web player panel, and Hope music MCP
- web/media_server.py: FastAPI service owning the permanent library
  (library.json + ./media volume), yt-dlp YouTube Music search, queued
  download jobs with mp3 transcode (ffmpeg), per-room player desired
  state, and an HTTPS proxy for external live-stream URLs.
- agent/media_mcp.py: Hope tools — search_music, play_song,
  offer_song_choices, pause/resume/stop/skip/seek, now_playing,
  list_library, add_stream.
- agent/agent.py: media toolset wiring + conversation rule (music
  playing disables the agent mic; re-engaging pauses music).
- web UI: slide-out Music panel (search, suggestions, library, live
  stream add, play/pause, ±10s, seek, volume) with a reconciling poller
  and media_state data-channel reports.
- token server returns the minted room name; nginx serves /media/files
  as static (native Range) and proxies /media/api to the media server.
- supervisord runs media-server; compose mounts ./media and adds
  ffmpeg to the image.
2026-08-29 13:03:12 -04:00
snowspeeder a4ae96f01b feat: default to qwen2.5-vl-3b and cap tool schema length constraints
Rebuild & Deploy / rebuild (push) Successful in 54s
- Default LLM_MODEL to qwen2.5-vl-3b across agent, task worker, tests,
  compose, workflow, and docs
- Cap min/max length-style constraints in tool schemas to 1000 before
  LLM requests; llama.cpp rejects grammar repetitions >= 2000 and the
  memory MCP's update_note body maxLength (200000) broke qwen2.5-vl-3b
  with 'Failed to initialize samplers: failed to parse grammar'
2026-08-29 12:46:20 -04:00
snowspeeder a66f09eb28 fix: resolve agent crash and expand settings sheet
Rebuild & Deploy / rebuild (push) Successful in 55s
- Move task_dir assignment before VoiceAssistant construction in
  handle_job; UnboundLocalError was crashing every agent job before it
  joined the room, leaving the UI stuck on 'Opening a secure room'
- Make settings sheet full-window height (100dvh) to match tasks panel
2026-08-29 12:19:00 -04:00
snowspeeder 360e3276b0 feat: switch LLM to OpenWebUI with live model selection
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
2026-08-29 12:09:03 -04:00
snowspeeder 024c42ffb1 fix: require usable recipe task results
Rebuild & Deploy / rebuild (push) Successful in 49s
2026-08-29 08:16:48 -04:00
snowspeeder cb035751d2 fix: route explicit online requests to tasks
Rebuild & Deploy / rebuild (push) Successful in 1m13s
2026-08-29 08:13:03 -04:00
snowspeeder 1fffed8ab3 fix: recover voice UI after text tasks
Rebuild & Deploy / rebuild (push) Successful in 47s
2026-08-29 08:03:59 -04:00
snowspeeder b1643b635d fix: wait for agent before sending text
Rebuild & Deploy / rebuild (push) Successful in 1m2s
2026-08-29 08:00:42 -04:00
snowspeeder 63e15b6023 fix: restore controls after task completion
Rebuild & Deploy / rebuild (push) Successful in 1m6s
2026-08-29 07:57:23 -04:00
snowspeeder 120ab192f1 feat: add seamless text and voice chat
Rebuild & Deploy / rebuild (push) Successful in 49s
2026-08-29 07:49:29 -04:00
snowspeeder 321421e37f fix: reliably dispatch and return background tasks
Rebuild & Deploy / rebuild (push) Successful in 47s
2026-08-29 07:29:38 -04:00
snowspeeder 746ca8dca9 fix: define deployment health check
Rebuild & Deploy / rebuild (push) Successful in 28s
2026-08-29 07:09:56 -04:00
snowspeeder 8b88b7646c fix: harden voice rebuild workflow
Rebuild & Deploy / rebuild (push) Failing after 5m13s
2026-08-29 07:04:17 -04:00
snowspeeder f5b84b90e6 chore: trigger gitea runner 2026-08-29 07:00:46 -04:00
snowspeeder 360fc63c20 ci: add rebuild workflow triggered on push to master
Rebuild & Deploy / rebuild (push) Failing after 14s
2026-08-29 06:58:16 -04:00
snowspeeder dac9d195ec fix: clarify voice connection readiness 2026-08-29 06:52:59 -04:00
snowspeeder 16aadcae96 feat: redesign Hope voice experience 2026-08-29 06:51:09 -04:00
snowspeeder 3c4c43f983 fix: use valid LiveKit development secret 2026-08-28 16:30:01 -04:00
snowspeeder 7e15573c10 fix: avoid Unraid agent port collision 2026-08-28 16:27:01 -04:00
snowspeeder c940923539 feat: connect Shane voice sessions to memory 2026-08-28 16:22:18 -04:00
snowspeeder 2a2a2ff768 feat: plan per-user memory integration 2026-08-28 15:59:26 -04:00
Shane 3700b52883 fix: use session_id for instant memory operations
- Updated Memory section to always use session_id='hope' for remember/recall
- This makes memory operations instant (0.3s) instead of 20+ seconds
- Fixed tool_call logging bug (FunctionToolCall has .name, not .function.name)
2026-08-23 08:57:25 -04:00
Shane 65deb0e4ce refactor: remove web tools from agent (dispatch handles all web work)
Reduces tool count from 10 to 8 for the LLM, removing the conflict
between 'use dispatch for everything' and having web_search available.
The task_worker has its own web tools for background tasks.
2026-08-23 08:13:37 -04:00
Shane c815bcb485 feat: replace custom memory MCP with Cognee remote server
- Remove memory_mcp.py (stdio markdown-file memory)
- Add Cognee MCP as remote Streamable HTTP toolset at 192.168.86.2:8003/mcp
- Filter to only remember/recall/forget tools via allowed_tools
- Update system prompt: Memory section moved to top priority with
  explicit 'call recall FIRST' instructions and examples
- Add COGNEE_MCP_URL env var to docker-compose
- Remove /memory volume mount (no longer needed)
- Rewrite memory tests to use Cognee HTTP client fixture
- 28/28 tests passing
2026-08-23 08:10:42 -04:00
Shane f80b9ebe3d fix: unique room names per session, JT_ROOM dispatch, dispatch-first prompt
- token_server: generate voice-{uuid8} room name per request so each
  browser session creates a fresh room and triggers agent dispatch
- token claim: add jobType JT_ROOM to roomConfig.agents
- livekit.yaml: revert empty_timeout to default (300s)
- agent.py: system prompt now mandates dispatch_task for all non-trivial
  tasks (not just research); removed inline web_search instructions;
  added tool_call logging in _ReasoningFallbackWrapper
2026-08-23 07:05:03 -04:00
Shane 98168f2876 fix: dispatch_mcp.py missing mcp.run() entry point
The FastMCP server never started listening on stdio because the file was
missing 'if __name__ == "__main__": mcp.run(transport="stdio")'. The
LiveKit agent's MCP client got 'Connection closed' during initialize,
which killed the entire toolset setup — ALL tools (weather, memory,
skills, dispatch) were unavailable. This is why Hope could hear you but
never called any tools.
2026-08-22 19:53:27 -04:00
Shane d08a4e1fcb fix: keep LiveKit rooms alive so agent survives reconnects
Set room.empty_timeout to 86400 (24h) in livekit.yaml. Previously the
default 300s timeout destroyed the room when all participants left,
which killed the agent job and left no worker registered for the next
join — causing 'assistant not ready retry' on tab reopen.
2026-08-22 18:54:22 -04:00
Shane dd33837055 fix: import task_registry without 'agent.' package prefix in container
In the container, agent.py is at /opt/voice-agent/agent.py (flat, not a
package). The 'from agent.task_registry import ...' raised ModuleNotFoundError
which killed the job before session.start(), causing 'assistant not ready
retry' in the browser. Added try/except ImportError fallback to bare import.
2026-08-22 18:41:11 -04:00
Shane 44e8d05e2c feat: background task dispatch system with live UI panel
- agent/task_registry.py: file-based JSONL event registry (cross-process)
- agent/task_worker.py: autonomous LLM loop with weather/time/memory/web tools
- agent/dispatch_mcp.py: MCP tool exposing dispatch_task to the main agent
- agent/agent.py: registers dispatch toolset, polls task events → room data
- web: slide-out task panel (FAB button + badge), live step streaming via
  data channel topic 'tasks', status dots (running/completed/failed)
- Dockerfile: copies new task_*.py and dispatch_mcp.py files

The dispatch MCP runs in its own process; events flow through
/tmp/tasks/events.jsonl which the main agent tails every second and
forwards to the browser. Tasks run up to 10 LLM iterations with tool calls.
2026-08-22 18:06:23 -04:00
Shane d372adca7d fix: strengthen memory tool prompt — Gemma 4B needs explicit examples
The 4B model was responding conversationally instead of calling memory_save/
memory_recall. Added imperative language (MUST call), concrete examples of
trigger phrases, and explicit instructions to never skip the tool call.
Verified: model now reliably generates tool_calls for save/recall/list.
2026-08-22 17:42:26 -04:00
Shane a1d59580f7 fix: ChatContext is not a sequence — use .items for len/list; add test suite
Bug fix:
- GemmaLLM.chat(): len(chat_ctx) → len(chat_ctx.items)
- GemmaLLM._compact_context(): list(chat_ctx) → list(chat_ctx.items)
  ChatContext in livekit-agents 1.7 is not iterable or sized directly;
  it exposes an .items list. The TypeError was silently killing all LLM
  responses (agent heard user but never spoke back).

Test suite (tests/):
- test_web_ui.py: static assets, token endpoint, LiveKit WS proxy
- test_llm_api.py: completion, tool calling, thinking-disabled latency, streaming
- test_mcp_tools.py: weather (Fahrenheit), time, memory, skills — all via
  docker exec + in-container MCP client
- test_agent_integration.py: process alive, worker registered, supervisord,
  container health, LiveKit API
- test_compaction.py: size-triggered compaction, system prompt preservation,
  short-context no-op
- conftest.py: shared fixtures (HTTPS client, token, LLM, docker exec helpers)
- Run with: .venv-tests/bin/python -m pytest tests/ -v
2026-08-22 17:09:29 -04:00
Shane 938629df83 feat: LLM-based context compaction with idle trigger
When conversation exceeds 24 items (~12 turns), Gemma summarizes the older
messages into a 2-4 sentence recap that replaces them, keeping the last 10
items verbatim. Also triggers after 5 minutes of idle time so returning
users get a compacted context rather than a bloated one.

Falls back to hard truncation if the summary call fails.
2026-08-22 16:51:21 -04:00
Shane ab6b5254ef feat: context compaction + skills system
Compaction:
- GemmaLLM.chat() truncates ChatContext to last 30 items (~15 turns)
  before sending to LLM, preventing context window overflow on long
  conversations. Preserves system prompt and removes orphaned tool calls.

Skills:
- agent/skills_mcp.py: MCP server with skill_save, skill_recall,
  skill_list, skill_update tools backed by .md files in /skills
- skills/ dir bind-mounted into container, git-trackable
- System prompt instructs Hope to save repeatable procedures as skills
  and recall them before performing tasks she's done before
- Distinct from memory (facts) — skills are learned *procedures*
2026-08-22 16:42:16 -04:00
Shane de1a5d8400 feat: markdown memory system — Hope learns from conversations
- agent/memory_mcp.py: MCP server with memory_recall, memory_save, memory_list
  tools backed by .md files in /memory (simple keyword matching for v1)
- memory/ dir bind-mounted into container, persists across rebuilds,
  easily backed up via git
- System prompt instructs Hope to recall on past references and save
  personal info/preferences naturally without announcing it
- Dockerfile: copy memory_mcp.py; compose: ./memory:/memory volume
2026-08-22 16:33:00 -04:00
Shane 49d8577a4c feat: name the agent Hope 2026-08-22 16:26:01 -04:00
Shane f882b694b9 fix: report weather in Fahrenheit by default 2026-08-22 16:23:09 -04:00
Shane c620867854 fix: disable preemptive_generation for tool call compatibility; add get_time tool
- preemptive_generation was incompatible with MCP tool calls: it starts
  generating before the turn finalizes, breaking the tool execution loop
  (agent said 'let me check' then hung forever)
- Add get_time(location?) to weather_mcp.py for date/time queries
- Update system prompt with Weather & Time section
2026-08-22 16:13:06 -04:00
Shane 24026e47de fix: live transcription partials update in place instead of stacking rows
- lk.transcription handler now uses addPartial() (in-place update)
  instead of addMessage() (new row per word)
- addMessage() clears stale partials before rendering the final
2026-08-22 15:57:27 -04:00
Shane c2f4c62aad feat: add mic mute button with visual state 2026-08-22 15:51:59 -04:00
Shane 510a577761 feat: add get_weather MCP tool using wttr.in API
- New agent/weather_mcp.py: stdio MCP server with get_weather(location)
  that returns a short conversational summary (temp, conditions, high/low)
- Register weather toolset in build_mcp_toolsets() alongside web-access
- Update system prompt with Weather section
2026-08-22 15:26:25 -04:00
Shane d3f9f2c4ed feat: implement full UPDATE.md review — critical fixes, UI upgrade, infra hardening
Critical frontend bugs:
- Add TrackSubscribed/attach() for agent audio playback
- Fix decodeToString TypeError with TextDecoder
- XSS fix: innerHTML -> textContent in addMessage
- Fresh token on reconnect retry

Agent fixes:
- GemmaLLM subclass with reasoning_content fallback wrapper
- Disable Gemma 4 thinking mode via chat_template_kwargs (6.8s -> 0.5s)
- Remove duplicate session-level LLM
- Replace global _active_session with closure-based handler
- asyncio.create_task instead of deprecated get_event_loop
- Explicit silero VAD, topic filter on voice-control

Infra:
- supervisord: all programs log to /dev/stdout
- Dockerfile: uv sync --frozen with committed uv.lock
- nginx config moved to real file, token_server.py no longer served
- entrypoint.sh: cert persisted, only regenerated on IP change
- compose: healthcheck + cert volume
- token_server: CORS removed, room pinned to voice-room

UI upgrade:
- Orb UI with state machine (idle/connecting/listening/thinking/speaking)
- Streaming transcripts via lk.transcription text streams
- Barge-in hint, thinking chip, audio visualizer
- Glassmorphism, chat bubbles, settings sheet, light mode
- PWA manifest, favicon, wake-lock, safe-area insets
- localStorage conversation history

Docs: AGENTS.md drift fixed
2026-08-22 15:21:59 -04:00
Shane d1eeb01f3d fix: set SSL_CERT_FILE to correct path for slim base image 2026-08-22 12:51:01 -04:00
Shane f7e799c9e9 fix: add WEB_MCP_ENABLED + FIRECRAWL_BASE to docker-compose env 2026-08-22 12:41:16 -04:00
Shane b55e6ed39a refactor: use MCPToolset instead of deprecated mcp_servers param
- Switch from Agent(mcp_servers=[...]) to Agent(tools=[MCPToolset(...)])
- Add WEB_MCP_ENABLED + FIRECRAWL_BASE to supervisord agent env
- Fixes deprecation warning in livekit-agents 1.7
2026-08-22 12:40:07 -04:00
Shane 045ddabac7 feat: web access via MCP (Firecrawl search/scrape) + mcp server attach support
- agent/web_mcp.py: stdio MCP server exposing web_search and web_scrape,
  backed by the self-hosted Firecrawl stack on xNAS (no API key needed)
- agent.py: Agent now attaches mcp_servers built from config; EXTRA_MCP_SERVERS
  env var allows adding arbitrary HTTP/SSE MCP servers as JSON
- Dockerfile: installs livekit-agents[mcp], copies web_mcp.py
- .env.example: WEB_MCP_ENABLED, FIRECRAWL_BASE, EXTRA_MCP_SERVERS documented
2026-08-22 12:29:04 -04:00
Shane 6f2b231938 feat: HTTPS web frontend (self-signed) + server-side token endpoint
- nginx serves the UI over HTTPS on 8090 with a self-signed cert
  (browsers require a secure context for microphone access)
- added /token endpoint (tiny Python HTTP server) that signs LiveKit
  JWTs server-side, keeping the API secret out of the browser
- app.js now fetches a signed token from /token and uses wss:// when
  the page is served over HTTPS
- supervisord runs the token-server as a fourth process
2026-08-22 08:31:19 -04:00