moss-agent
Moss runtime for LiveKit voice agents - a hot index cache shared across rooms and a one-line attach per call.
One process-wide MossAgent holds auth and the loaded indexes. Every conversation gets its own MossCall, and all of them query the same warm in-memory indexes.
Install
pip install moss-agent
# with LiveKit, for attach(ctx):
pip install "moss-agent[livekit]"
Use - voice (LiveKit)
from livekit.agents import AgentServer, JobContext, JobProcess
from moss_agent import MossAgent
server = AgentServer()
def prewarm(proc: JobProcess) -> None:
agent = MossAgent(
project_id="...",
project_key="...",
)
import asyncio
asyncio.run(agent.load_indexes(["product_catalog", "faq", "policies"]))
proc.userdata["moss_agent"] = agent
server.setup_fnc = prewarm
@server.rtc_session(agent_name="my-agent")
async def handle_visit(ctx: JobContext) -> None:
await ctx.connect()
agent: MossAgent = ctx.proc.userdata["moss_agent"]
call = agent.attach(ctx)
results = await call.query("product_catalog", user_question)
# ... your existing agent loop ...
attach(ctx) uses the room name as the call's room ID, the job ID as its session ID and the room's creation time as its start, and ends the call when LiveKit shuts the room down.
Use - voice (any framework)
agent = MossAgent(project_id="...", project_key="...", cache_path="/var/cache/moss")
await agent.load_indexes(["product_catalog", "faq"])
# When a conversation starts, with its ID from your voice stack:
call = agent.start_call(room_id, session_id=session_id)
results = await call.query("product_catalog", user_question)
results = await call.query_multi_index(["product_catalog", "faq"], user_question)
# When it ends:
await call.end()
start_call is idempotent while the call is active: starting the same room_id again returns a handle to the same call. started_at (a timezone-aware datetime, default now) is the call's start time; a process that joins a conversation already in progress should pass the real start time.
Ambient retrieval (STT to Moss)
Instead of waiting for the LLM to call a search tool, query Moss with every final user transcript and give the results to the LLM with the question. Each turn needs one LLM pass instead of two, and the Moss query takes a few milliseconds, so the user never waits on it.
One helper turns a transcript into a context block:
from moss_agent import MossCall, QueryOptions
async def ground(call: MossCall, transcript: str) -> str:
"""Moss results for one user turn, or "" when nothing is relevant."""
if not transcript.strip():
return ""
try:
results = await call.query(
"faq", transcript, QueryOptions(top_k=3, min_score=0.4)
)
except Exception:
return "" # a failed lookup must never break the voice turn
if not results.docs:
return ""
passages = "\n".join(f"[{i}] {doc.text}" for i, doc in enumerate(results.docs, 1))
return f"Relevant knowledge from Moss:\n{passages}"
min_score drops weak matches, so small talk such as "yes" or "thanks" adds nothing to the prompt. Tune it on your own queries.
LiveKit: on_user_turn_completed runs after the user's final transcript and before the LLM answers.
from livekit.agents import Agent, AgentSession, ChatContext, ChatMessage, JobContext
class Assistant(Agent):
def __init__(self, call: MossCall) -> None:
super().__init__(instructions="Answer using the Moss knowledge you are given.")
self._call = call
async def on_user_turn_completed(
self, turn_ctx: ChatContext, new_message: ChatMessage
) -> None:
context = await ground(self._call, new_message.text_content or "")
if context:
turn_ctx.add_message(role="assistant", content=context)
@server.rtc_session(agent_name="my-agent")
async def handle_visit(ctx: JobContext) -> None:
await ctx.connect()
call = ctx.proc.userdata["moss_agent"].attach(ctx)
session = AgentSession(...) # your STT, LLM and TTS
await session.start(agent=Assistant(call), room=ctx.room)
Any framework: call ground from your STT's final-transcript callback and send the result to the LLM with the transcript.
async def on_final_transcript(text: str) -> None:
context = await ground(call, text)
llm_input = f"{context}\n\n[Current User Question]\n{text}" if context else text
await send_to_llm(llm_input) # your framework's LLM input
Use - text (HTTP / chat / non-voice)
from moss_agent import MossAgent
# Module level - reuse across requests
agent = MossAgent(project_id="...", project_key="...")
await agent.load_indexes(["product_catalog"])
# In your HTTP / chat handler
async def handle_message(req):
results = await agent.query("product_catalog", req.user_message)
# ... feed results to your model ...
Query options
QueryOptions works the same on MossCall and on the text path:
top_k(default 5, 10 for multi-index) andalpha(default 0.8 for single and multi-index; 1.0 is embedding-only, 0.0 keyword-only).min_scoredrops results whosescore(relevance from 0 to 1) is below it. Keyword-only queries (alpha=0.0) have no embedding to score against, somin_scoreraises an error on them.candidate_depthsets how many hits a hybrid query ranks on each signal before fusion (default2 * top_k).embeddinguses your own query vector, andfilterapplies a metadata filter.
from moss_agent import QueryOptions
results = await call.query("faq", question, QueryOptions(top_k=3, min_score=0.4))
Adding documents
create_index and add_docs take documents as DocumentInfo objects, dicts with the same fields, JSON array or JSON Lines text, or file paths:
from pathlib import Path
await agent.create_index("faqs", '[{"id": "1", "text": "Track your order in your account."}]')
await agent.create_index("faqs-jsonl", Path("faqs.jsonl"))
await agent.add_docs("faqs", [{"id": "2", "text": "30-day returns.", "metadata": {"days": 30}}])
await agent.create_index("handbook", [Path("docs/handbook.md"), Path("docs/faq.txt")])
.txt,.mdand.markdownfiles are chunked natively. Chunk ids are{path}#{n}and each chunk's metadata hasparentId.- Integer ids and number or boolean metadata values become strings; booleans are stored as
"true"and"false". chunker=splits every document's text with your own function. Chunks may have at most 2,000 characters.chunking=Chunking(chunk_size=800, chunk_overlap=100)tunes the native chunker (chunk_size200 to 2000,chunk_overlap0 to half ofchunk_size,mode="pages"or"sentences",markdown_header_depth1 to 6) and also chunksDocumentInfo, dict and JSON input. Passchunkerorchunking, not both; neither works withmodel_id="custom".
create_index(..., wait=False) submits the build and returns a JobHandle right away:
job = await agent.create_index("faqs", docs, wait=False)
print((await job.status()).progress)
await job.wait() # or later: await agent.wait_for_job(job.job_id)
PDF and DOCX files go through create_index_from_files, with optional ParseOptions:
from moss_agent import ParseFileInput, ParseOptions
await agent.create_index_from_files(
"manuals",
[ParseFileInput(name="manual.pdf", content_type="application/pdf", path="manual.pdf")],
parse_options=ParseOptions(ocr_mode="auto_ocr", chunk_size=800),
)
Disk cache and offline loads
cache_path on the constructor is the default on-disk index cache for load_index / load_indexes; a per-call cache_path overrides it. Later loads reuse the cached copy while the cloud version is unchanged. With a snapshot on disk, a load that cannot reach the cloud serves the cached snapshot and starts a refresh poller; was_served_stale(name) reports whether an index is served that way.
Web sources
Crawl a site into an index and keep it refreshed. These call the Moss manage API (/v1/manage); set MOSS_CLOUD_API_BASE_URL to use a non-default host (default https://service.usemoss.dev). The constructor's base_url does not apply to web sources, so an agent pointed at another environment needs MOSS_CLOUD_API_BASE_URL set to match.
created = await agent.create_web_source(
"https://docs.example.com",
"knowledge-base",
max_pages=200,
refresh_cadence="weekly",
)
await agent.wait_for_job(created.job_id)
await agent.load_index("knowledge-base")
sources = await agent.list_web_sources(index_name="knowledge-base")
source = await agent.get_web_source(created.id)
updated = await agent.update_web_source(created.id, refresh_cadence="daily")
resync = await agent.resync_web_source(created.id)
deleted = await agent.delete_web_source(created.id)
A loaded index is a snapshot: after a crawl finishes, call load_index again, or load it once with auto_refresh=True. Failed manage calls raise ManageApiError with the HTTP status. A redirect fails the same way instead of being followed, so the project key is never sent to another URL.
Shutdown
await agent.close() ends every active call, sends the final usage telemetry and releases the agent; afterwards every method raises RuntimeError. MossAgent also works as async with and with.
In a LiveKit worker, attach already ends each call from the room's shutdown callback, so you don't need close() per room. Call close() (or use async with) wherever you own shutdown: a script, a web server's shutdown handler, or tests. If the process exits without it, the core ends open calls when the agent object is freed, which at interpreter exit is best-effort; a killed process sends nothing more.
API surface
MossAgent(project_id, project_key, base_url=None, client_id=None, *, cache_path=None)- process-wide instance.base_urlsets the index service; web sources useMOSS_CLOUD_API_BASE_URL.- Voice:
attach(ctx) -> MossCall(LiveKit) andstart_call(room_id, *, session_id=None, started_at=None) -> MossCall.MossCall.query(name, query, options=None),MossCall.query_multi_index(names, query, options=None),MossCall.end(); propertiescall_id,room_id,session_id,start_at_unix_ms,has_ended(livekit_room_id/livekit_session_idare aliases). - Text:
MossAgent.query(name, query, options=None)andMossAgent.query_multi_index(names, query, options=None). - Index CRUD:
create_index,create_index_from_files,add_docs,delete_docs,delete_index,list_indexes,get_index,get_docs,get_job_status,wait_for_job. - Cache lifecycle:
load_index,load_indexes,unload_index,unload_indexes,was_served_stale. - Web sources:
create_web_source,list_web_sources,get_web_source,update_web_source,resync_web_source,delete_web_source. close(),active_call_count().- Errors:
MossError(with stable.code),InvalidArgumentError(also aValueError),ManageApiErrorfor web sources.
Telemetry
moss-agent always sends usage telemetry to the Moss cloud. See Telemetry for what it contains and when it is sent.
Requirements
- Python 3.10+
inferedge-moss-core == 0.32.0livekit-agents >= 0.10.0(only required if you callattach(); install via the[livekit]extra)
License
Proprietary. See LICENSE.txt.
Metadata
Release files for moss-agent 2.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| moss_agent-2.0.1.tar.gz | 23.0 kB | Without provenance | Details for moss_agent-2.0.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| moss_agent-2.0.1-py3-none-any.whl | Python 3 | none | any | Details for moss_agent-2.0.1-py3-none-any.whl |
Total release size: 43.3 kB
Release files / moss_agent-2.0.1.tar.gz
| Download URL | moss_agent-2.0.1.tar.gz |
|---|---|
| Size | 23.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
669f69850ef20def6c5c08ea7e8c7daabdb108b6852d37f6f276f7d690963d62
|
|
BLAKE2b-256 checksum How to use checksums |
136fc03844470cafc7c2e25a4eff894ef0fe267c310499b80fc4dd506e17d6b3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.17
|
Release files / moss_agent-2.0.1-py3-none-any.whl
| Download URL | moss_agent-2.0.1-py3-none-any.whl |
|---|---|
| Size | 20.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
84c6c8e6dad6a26145c938cc4399cf5fcb21b371d897558a8ab0354ea4c5f9d9
|
|
BLAKE2b-256 checksum How to use checksums |
a717e65619d58c05ea8b5f9720725d4a9c1dd7eb4d9e8d3c007fb0dc4c1c42df
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.17
|
Security for moss-agent 2.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.