Skip to main content
Integrate Moss Semantic Search SDK directly into a LiveKit Voice Agent. This setup allows your voice AI to perform ultra-low latency searches over your custom data to answer user questions in real-time. It pairs a persistent knowledge base with a per-call session that indexes the conversation as it happens.

Why Use Moss with LiveKit?

Moss delivers sub-10ms semantic retrieval, ensuring your voice agents respond naturally without noticeable delays. A session gives each call its own local index, so the agent can recall what was said earlier in the conversation with no cloud round trip.

Required Tools

Integration Guide

1

Installation

Install the Moss SDK.
Versions below 1.9.0 will stop loading models when the legacy download path is retired. Upgrade to 1.9.0+ before then.
2

Environment Setup

Create a .env file in your project root directory with your API keys.File: .env
3

Creating the Knowledge Base with Moss

Before the agent can answer questions, build the long-term knowledge base. Run this script once to upload your documents to Moss as a cloud index.File: build_index.py
Run the builder:
4

Building the Agent

This agent exposes Moss search as function tools. The LLM decides when to call them during a turn, reads the results, and uses them to answer, so it can search, refine, or skip retrieval on turns that don’t need it.Two tools are registered:
  • search_knowledge_base queries the persistent knowledge base you built above (long-term context).
  • search_conversation queries this call’s session (short-term context) to recall something said earlier.
Each user turn is also recorded into the session, and the session is pushed to the cloud when the call ends, so the conversation can be resumed or handed to another agent later.The Moss client is created once per worker process in prewarm, with a cache_path. Each call reads its knowledge base names from the job metadata and starts loading them before ctx.connect(), so the loads run while the call connects. See Process model and index loading for why.File: agent.py
The session indexes each turn locally during the call and pushes to the cloud at the end, so a later session opened with the same name resumes the conversation, which is the basis for cross-agent handoff. See Sessions for the full API.
5

Running the Agent

First, start the LiveKit server in development mode:
Then, in a separate terminal, start your worker. The necessary VAD models will handle themselves or be downloaded automatically if needed by the plugin.

Process model and index loading

By default, LiveKit Agents runs each call in its own process. The worker keeps idle processes ready, runs prewarm once in each new process before a call is assigned to it, then gives the process one call. The process exits when the call ends, so nothing loaded for a call carries over to the next one. That decides where each piece of Moss work goes:
  • prewarm creates the client. It holds what every call needs: the VAD model, and a MossClient with its cache_path.
  • The entrypoint loads the call’s indexes. Only the job knows which knowledge bases a call needs. Read their names from the job metadata and start one load_index per index under asyncio.gather before ctx.connect(), so the loads run concurrently while the call connects.
  • Index loads stay out of prewarm. It runs in every new process before any call is known, so loading every knowledge base there makes each process download, parse and hold all of them. It must also finish within initialize_process_timeout, 10 seconds by default.
  • cache_path keeps loads short. Without it, each load makes four round trips to the cloud, the last one downloading the index. With the index already in the cache it makes two and reads the index from disk. If the cloud is unreachable or answers with a temporary error, it serves the cached copy instead of failing; a 401, 403 or 404 still fails (Storage & Persistence). With a process per call, the embedding model opens fresh in every call and cannot open while the cloud is unreachable, so queries with alpha above 0 fail during an outage; keyword-only queries (alpha=0) and queries that pass their own embedding keep working. Put the directory on storage that outlives the container.
  • The first load in a process also opens the embedding model, about half a second when the model is on disk. The model is cached under ~/.cache/moss-models/. Keep that directory in the image or on persistent storage too, or every new container downloads the model (about 91 MB for moss-minilm) on its first call.
Each process holds its own embedding model, about 150 MiB, and its own copy of every index it loads. Nothing is shared between processes, even with a common cache_path. See Memory sizing to estimate a host’s memory. When many concurrent calls use the same knowledge bases, two setups share that memory:
  • The thread executor. WorkerOptions(job_executor_type=JobExecutorType.THREAD) runs every call of a worker in one process, so calls share one client, one model and one copy of each index. Calls are no longer isolated: a crash or a blocking call affects every call on the worker. In this mode prewarm runs once per call, so keep the client at module level, as below.
  • A retrieval service per host. One process holds the client and the indexes and answers queries from every worker process on the host over a local connection. It fits when you want to keep one call per process, or when several services need the same indexes.
MossClient is not tied to an event loop: every method runs the core on a worker thread, so one module-level client serves the loop of every call. Each call loads its indexes: for an index that is already loaded, load_index checks the cloud for a newer version, and each load is also where Moss checks the project’s account, for example a paused project.
With moss 1.20 and earlier, loading an index that is already loaded reads it again from cache_path. The worker keeps every index a call has used until it exits; if it can see more knowledge bases than its memory holds, unload the idle ones as in Unload idle indexes. Leave auto_refresh off in a worker like this: its periodic refresh counts as session time, so a worker that runs for days is billed for session minutes around the clock, with or without calls.

Retrieval and preemptive generation

LiveKit’s AgentSession runs preemptive generation by default: it starts drafting the reply speculatively as soon as a final transcript arrives, before the turn is confirmed. In current LiveKit Agents it is set through turn_handling (TurnHandlingOptions(preemptive_generation={"enabled": ...})); the older top-level preemptive_generation argument is deprecated. on_user_turn_completed runs later, at turn confirmation, after that draft has started. Injecting retrieval there with turn_ctx.add_message makes the draft stale, so LiveKit discards and regenerates it on every injected turn. So expose retrieval as a @function_tool (as the agent above does, and as the Founding Agent runs in production): turn_ctx stays stable at the boundary, and retrieval composes with preemptive generation. An ambient pattern that injects every turn must instead turn preemptive generation off (turn_handling=TurnHandlingOptions(preemptive_generation={"enabled": False})); see the airline customer agent.