echoing-lamp-18687
12/25/2025, 6:03 AMtall-airline-16756
12/25/2025, 6:03 AMtall-airline-16756
12/25/2025, 6:04 AMon_user_turn_completed to run RAG lookup and inject retrieved context into the turn before the LLM answers.
• Implement a my_rag_lookup(query) function that: embeds the query, queries your cloud vector DB (Pinecone, Weaviate, Chroma, etc.), and returns ranked text snippets.
• Control RAG with a simple flag (agent attribute, participant metadata, env var, or API toggle). When off, skip the lookup/injection.
Minimal code sketch
class MyAgent(Agent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.rag_enabled = True # toggle; can come from config/metadata
async def on_user_turn_completed(self, turn_ctx: ChatContext, new_message: ChatMessage) -> None:
if not self.rag_enabled:
return
query = new_message.text_content()
rag_snippets = await my_rag_lookup(query) # implement per your vector DB
if not rag_snippets:
return
# Inject concise retrieved context for the LLM
turn_ctx.add_message(
role="assistant",
content=f"Retrieved information for context:\n{rag_snippets}"
)
# Optionally persist to chat context
# await self.update_chat_ctx(turn_ctx)
Notes on the my_rag_lookup function
• Steps: 1) get embedding for the query (OpenAI, local embedder, etc.), 2) call your cloud vector DB similarity search, 3) fetch and format the top results as plain text snippets with source identifiers.
• Keep snippets short and include source references so the agent can cite them or avoid hallucination.
• Consider filtering by recency, tenant, or room if your DB stores multi-tenant data.
Where to store the enable/disable toggle
• Agent attribute (self.rag_enabled) for quick toggles.
• Participant or job metadata if you need per-user control.
• Environment variable or config for deployment-wide default.
Persistence and turnover
• Messages added in on_user_turn_completed are available to the current turn. Call update_chat_ctx() if you want the retrieved context to persist across turns.
Questions to help me give exact code
• Which cloud vector DB are you using (Pinecone, Weaviate, Chroma, etc.)?
• How do you generate embeddings (OpenAI, local model)?
• Do you need per-room or per-participant toggles, or a global toggle?
• Do you want retrieved snippets persisted across turns?
Tell me those details and I’ll give a tailored implementation snippet for your stack.
Sources: Pipeline nodes and hooks | LiveKit docs | External data and RAG | LiveKit docsechoing-lamp-18687
12/25/2025, 6:31 AMtall-airline-16756
12/25/2025, 6:32 AMon_user_turn_completed lifecycle node to run a RAG lookup and inject retrieved context before the LLM responds is the recommended approach for LiveKit Python agents.
Why:
• It runs after the user’s message is complete and before the agent’s reply, so you can add relevant context into the turn.
• It cleanly isolates retrieval logic (embed → vector DB search → format snippets) from generation logic.
• You can conditionally run it (agent flag, participant/job metadata, or env/config) to support enable/disable behavior.
• Injected messages are available to the current turn and can be persisted with update_chat_ctx() if you want them to survive across turns.
If you want, tell me your vector DB (Pinecone/Weaviate/Chroma/etc.), embedding provider, and whether toggles should be per-user or global — I’ll provide a tailored code snippet.
Sources: External data and RAG | LiveKit docsechoing-lamp-18687
12/25/2025, 6:33 AMtall-airline-16756
12/25/2025, 6:33 AMfunction_tool for RAG and whether it’s a good approach per LiveKit.
Short answer
• It’s possible, but not recommended for standard RAG. LiveKit recommends using on_user_turn_completed to retrieve and inject context before the LLM’s primary inference.
Why function_tool is usually a worse fit
• Latency: The LLM must first decide to call the tool, causing an extra inference round-trip before retrieval.
• Breaks reasoning flow: Retrieval happens after the model’s initial decision, so the model can’t use the retrieved context in its first response.
• Cost & complexity: Extra tool calls increase LLM usage and conversation steps.
• UX work: You’ll likely need "thinking" feedback while the tool executes, adding UI/UX complexity.
When function_tool can be appropriate
• You need an explicit, model-driven action (e.g., user confirmation triggers a specific lookup or a side-effect like updating a DB).
• The retrieval is part of a larger action that should be logged/treated as a discrete tool call.
• You want strict, visible control of when retrieval runs (as opposed to silent background enrichment).
Recommended pattern
• Use on_user_turn_completed for standard RAG (embed → vector DB search → inject snippets into turn_ctx).
• Use function_tool only for conditional, action-oriented, or side-effectful workflows.
If you want, tell me:
• whether you need retrieval before the model’s first reply or only after a model-initiated action,
• whether the retrieval must perform side effects,
and I’ll recommend the exact implementation (code sample) for your case.
Sources: External data and RAG | LiveKit docs | Agent speech and audio | LiveKit docsechoing-lamp-18687
12/25/2025, 6:35 AMtall-airline-16756
12/25/2025, 6:35 AMon_user_turn_completed are available only for the current turn (they influence the LLM’s next reply but are not automatically persisted in the ongoing chat history).
• If you want the retrieved context to become part of the persistent conversation history, call update_chat_ctx() after adding the message to persist it.
• Use transient injection when you only need the LLM to see context for one reply (reduces noise). Persist when the info should be referenced in later turns or shown to participants.
If you want, tell me whether you prefer transient-only or persistent RAG context and I’ll give the exact code to persist or keep it transient.echoing-lamp-18687
12/25/2025, 8:55 AMtall-airline-16756
12/25/2025, 8:55 AMon_user_turn_completed.
• How you run the agent (local dev server, container, deployment); include LiveKit SDK / agent package version if known.
• How messages arrive (voice via VAD, partial transcripts, or full text messages) and whether you see other lifecycle hooks (e.g., on_new_message or on_session_started) being called.
• Any errors, logs, or warnings from the agent process when a user message is sent.
Quick checklist you can verify now:
• Confirm your agent subclass registers/loads where the runtime expects it and that the process was restarted after code changes.
• Ensure the method signature matches exactly: async def on_user_turn_completed(self, turn_ctx: ChatContext, new_message: ChatMessage) -> None:
• Verify messages reach the agent (check on_new_message or logs). If messages never reach the agent, the hook won’t run.
• If using VAD/partial transcripts, confirm the turn is actually completed (end-of-speech) so the lifecycle advances.
• Check for exception traces in startup/runtime logs; an earlier error can disable lifecycle invocation.
Share the code/logs you can, and I’ll point out the exact fix.echoing-lamp-18687
12/25/2025, 9:02 AMtall-airline-16756
12/25/2025, 9:02 AMon_user_turn_completed to perform RAG lookup, supports an enable/disable toggle, injects retrieved snippets into the turn, and optionally persists the injected context.
Notes:
• Replace the placeholder my_rag_lookup with your cloud vector DB logic (embed → similarity search → format results).
• Adjust embedding calls/DB client imports for Pinecone/Weaviate/Chroma/etc.
• Persist injected context by calling `await self.update_chat_ctx(turn_ctx)`; omit that to keep injection transient.
import os
from typing import Optional
# Import LiveKit agent base classes (adjust import path to match your package)
from livekit.agents import Agent, ChatContext, ChatMessage
# ---- Replace with your actual embedder/vector DB client ----
async def my_rag_lookup(query: str, top_k: int = 3) -> Optional[str]:
"""
Perform embedding + vector DB similarity search.
Return a formatted string of top snippets (or None/"" if no results).
"""
# Example pseudocode:
# embedding = await embed_text(query)
# results = await vector_db.query(embedding, top_k=top_k)
# if not results: return None
# formatted = "\n".join(f"{r.score:.2f}: {r.text} (source={r.source})" for r in results)
# return formatted
# Placeholder return for example
return "Snippet A from doc1 (source=doc1)\nSnippet B from doc2 (source=doc2)"
# ---- Agent implementation ----
class RagAgent(Agent):
def __init__(self, *args, rag_enabled: bool = True, persist_rag: bool = False, **kwargs):
"""
rag_enabled: default toggle for RAG. You can set this per-instance or mutate later.
persist_rag: if True, injected RAG context will be persisted across turns.
"""
super().__init__(*args, **kwargs)
self.rag_enabled = rag_enabled
self.persist_rag = persist_rag
async def on_session_started(self, session_ctx):
# Optional: initialize vector DB client or embedder here if needed
# self.vector_client = init_vector_client(...)
return await super().on_session_started(session_ctx)
async def on_user_turn_completed(self, turn_ctx: ChatContext, new_message: ChatMessage) -> None:
"""
Runs after the user finishes speaking and before the LLM's reply.
Use this to fetch RAG context and inject into turn_ctx for the LLM to consume.
"""
# 1) Quick guard: is RAG enabled for this agent/session/participant?
# You can also inspect turn_ctx/job/participant metadata for per-user toggles.
if not getattr(self, "rag_enabled", True):
return # Skip RAG entirely
# 2) Extract user's latest text (adjust for your message structure)
user_text = new_message.text_content() if hasattr(new_message, "text_content") else getattr(new_message, "text", "")
if not user_text:
return
# 3) Run your vector DB lookup (async)
try:
rag_snippets = await my_rag_lookup(user_text, top_k=3)
except Exception as e:
# Log and skip RAG if retrieval fails
self.logger.exception("RAG lookup failed", exc_info=e)
return
if not rag_snippets:
return
# 4) Inject retrieved context into the turn so the LLM sees it for the next response.
# Make injection concise and clearly marked as retrieved content.
injected_content = (
"Retrieved context (for system/assistant use):\n"
f"{rag_snippets}\n"
"End retrieved context."
)
turn_ctx.add_message(role="assistant", content=injected_content)
# 5) Optionally persist injected message into conversation history
if getattr(self, "persist_rag", False):
try:
await self.update_chat_ctx(turn_ctx)
except Exception as e:
self.logger.exception("Failed to persist injected RAG context", exc_info=e)
# Optional: expose a runtime toggle method or function_tool to flip RAG on/off
async def set_rag_enabled(self, enabled: bool):
self.rag_enabled = enabled
# ---- Example usage ----
# Instantiate agent with RAG enabled (default) and persistence optional
# agent = RagAgent(..., rag_enabled=True, persist_rag=False)
If you want, I can:
• Provide a concrete my_rag_lookup implementation for Pinecone, Weaviate, or Chroma with an embedding example (OpenAI or local embedder).
• Show how to read a per-participant toggle from job/participant metadata and use that inside on_user_turn_completed.echoing-lamp-18687
12/25/2025, 9:23 AMtall-airline-16756
12/25/2025, 9:24 AMon_user_turn_completed is available in livekit-agents~=1.3.
Quick checklist to troubleshoot why your hook isn't firing:
• Confirm your agent subclass correctly overrides the method with this exact signature:
async def on_user_turn_completed(self, turn_ctx: ChatContext, new_message: ChatMessage) -> None:
...
• Ensure your agent is running an STT → LLM → TTS pipeline or otherwise uses turn detection; this hook fires after the framework detects a completed user turn.
• Verify turn detection is configured and working (VAD/turn-detector plugin). If using partial transcripts, make sure end-of-speech is detected.
• Confirm messages reach the agent (check on_new_message or other lifecycle hooks). If earlier hooks aren’t called, the runtime may not be wired to your agent.
• Restart the agent process after code changes so the runtime loads your updated class.
• Check runtime logs for exceptions on startup or during message handling — an earlier error can prevent lifecycle invocation.
If you want, paste:
• your agent class (the override),
• how you start the agent (command/docker-compose/etc.),
• and any relevant logs —
and I’ll point out the exact fix.
Sources: Pipeline nodes and hooks | LiveKit docs | Models | LiveKit Docsechoing-lamp-18687
12/25/2025, 9:25 AMtall-airline-16756
12/25/2025, 9:25 AMon_user_turn_completed that covers RAG lookup, enable/disable toggle, injection into the turn, error handling, and optional persistence.
Paste this into your Agent subclass (replace my_rag_lookup with your vector DB logic):
async def on_user_turn_completed(self, turn_ctx: ChatContext, new_message: ChatMessage) -> None:
# 1) Check toggle (can be per-agent, per-session, or read from metadata)
if not getattr(self, "rag_enabled", True):
return
# 2) Extract user text
user_text = new_message.text_content() if hasattr(new_message, "text_content") else getattr(new_message, "text", "")
if not user_text:
return
# 3) Run RAG lookup (implement embedding + vector DB query)
try:
rag_snippets = await my_rag_lookup(user_text, top_k=3) # async function you implement
except Exception:
self.logger.exception("RAG lookup failed; skipping injection")
return
if not rag_snippets:
return
# 4) Inject retrieved context for the LLM's next reply
injected = (
"Retrieved context (for assistant only):\n"
f"{rag_snippets}\n"
"End retrieved context."
)
turn_ctx.add_message(role="assistant", content=injected)
# 5) Optionally persist injected context across turns
if getattr(self, "persist_rag", False):
try:
await self.update_chat_ctx(turn_ctx)
except Exception:
self.logger.exception("Failed to persist injected RAG context")
Notes:
• Keep my_rag_lookup responsible for embedding and querying your cloud vector DB and returning a short formatted string with sources.
• Use transient injection (no update_chat_ctx) for one-off context; set persist_rag=True to save it to conversation history.
• Ensure the method signature exactly matches the async signature above and restart your agent after changes.
If you want, I can provide a concrete my_rag_lookup example for Pinecone, Weaviate, or Chroma with OpenAI embeddings. Which vector DB and embedder are you using?echoing-lamp-18687
12/25/2025, 9:30 AMtall-airline-16756
12/25/2025, 9:30 AMon_user_turn_completed in your hierarchy (replace my_rag_lookup with your vector DB logic).
from abc import ABC
from livekit.agents import Agent, ChatContext, ChatMessage
async def my_rag_lookup(query: str, top_k: int = 3) -> str:
# Implement embedding + vector DB query; return formatted snippets or ""/None
return "example snippet"
class BaseAgent(Agent, ABC):
def __init__(self, *args, rag_enabled: bool = True, persist_rag: bool = False, **kwargs):
super().__init__(*args, **kwargs)
self.rag_enabled = rag_enabled
self.persist_rag = persist_rag
# optional shared helpers for subclasses
def is_rag_enabled_for_turn(self, turn_ctx: ChatContext, new_message: ChatMessage) -> bool:
# You can inspect participant/job metadata here for per-user toggles
return getattr(self, "rag_enabled", True)
class DynamicRagAgent(BaseAgent):
async def on_user_turn_completed(self, turn_ctx: ChatContext, new_message: ChatMessage) -> None:
# 1) Check toggle
if not self.is_rag_enabled_for_turn(turn_ctx, new_message):
return
# 2) Extract user text
user_text = new_message.text_content() if hasattr(new_message, "text_content") else getattr(new_message, "text", "")
if not user_text:
return
# 3) Run RAG lookup
try:
rag_snippets = await my_rag_lookup(user_text, top_k=3)
except Exception:
self.logger.exception("RAG lookup failed; skipping injection")
return
if not rag_snippets:
return
# 4) Inject into turn for the LLM's next reply
injected = (
"Retrieved context (for assistant use only):\n"
f"{rag_snippets}\n"
"End retrieved context."
)
turn_ctx.add_message(role="assistant", content=injected)
# 5) Optionally persist
if getattr(self, "persist_rag", False):
try:
await self.update_chat_ctx(turn_ctx)
except Exception:
self.logger.exception("Failed to persist injected RAG context")
Notes:
• Keep the exact async signature: async def on_user_turn_completed(self, turn_ctx: ChatContext, new_message: ChatMessage) -> None:.
• Place shared logic (toggles, metadata checks, init of vector client) in BaseAgent.
• Restart your agent process after changes so the runtime loads the new class.
If you share your actual BaseAgent code or how you start the agent, I can point out any mismatches that might prevent the hook from firing.tall-airline-16756
12/31/2025, 2:47 PMvad_instance = await silero.VAD.load() — silero.VAD.load() is a regular (synchronous) function or returns an object, not a coroutine, so await raises TypeError.
Fixes (pick one based on the Silero API)
• If load() is synchronous:
◦ Remove await:
vad_instance = silero.VAD.load()
• If the API provides an async loader (e.g., aload() or load_async()), call that instead:
◦ Example:
vad_instance = await silero.VAD.load_async()
• If you need to run a blocking synchronous load without blocking the event loop, run it in a thread:
import asyncio
loop = asyncio.get_running_loop()
vad_instance = await loop.run_in_executor(None, silero.VAD.load)
Quick next steps
• Try removing await first (most likely), restart the agent, and confirm the VAD loads without TypeError.
• If other errors appear, paste the updated snippet around where you load Silero (lines ~300–320) and I’ll review.hundreds-king-75485
12/31/2025, 2:49 PMtall-airline-16756
12/31/2025, 2:49 PMawait silero.VAD.load() but load() is synchronous.
• Fix: replace the await with a direct call or run it in an executor.
Options:
• Synchronous load (most likely):
vad_instance = silero.VAD.load()
• Non-blocking from async context:
import asyncio
loop = asyncio.get_running_loop()
vad_instance = await loop.run_in_executor(None, silero.VAD.load)
2) AttributeError: turn detector model import
• Problem: voice.MultilingualModel() is incorrect; the MultilingualModel is provided by the turn-detector plugin.
• Fix: install the plugin, download weights, and import the class from the plugin.
Steps:
1. Install turn-detector plugin (if not already):
uv add "livekit-agents[turn-detector]~=1.3"
1. Download model weights once:
uv run agent.py download-files
1. Use the correct import and pass the instance into your session:
from livekit.plugins.turn_detector.multilingual import MultilingualModel
from livekit.plugins import silero
# Prewarm VAD optionally
vad = silero.VAD.load()
session = AgentSession(
vad=vad,
turn_detection=MultilingualModel(),
stt="assemblyai/universal-streaming",
llm="openai/gpt-4.1-mini",
tts="cartesia/sonic-3",
)
Quick checklist after making these changes
• Restart the agent process.
• Confirm uv run agent.py download-files completed without errors.
• If you see "VAD not prewarmed, loading now..." that's a warning; prewarm if you want to avoid first-request latency.
• If any error remains, paste the updated code around where you load VAD and create the AgentSession (lines ~300–320) and the exact traceback. Sources: LiveKit turn detector plugin | LiveKit docs | Turns overview | LiveKit docs | External data and RAG | LiveKit docs | Pipeline nodes and hooks | LiveKit docshundreds-king-75485
12/31/2025, 2:51 PMVIRTUAL_ENV=venv does not match the project environment path .venv and will be ignored; use --active to target the active environment instead
error: Failed to spawn: agent.py
Caused by: No such file or directory (os error 2)tall-airline-16756
12/31/2025, 2:51 PMuv couldn't find agent.py at the path you gave.
Quick fixes:
• Run uv targeting the actual script path. If your entry file is at src/agent.py, run:
uv run src/agent.py download-files
• Or run in the active venv with --active so uv uses your current environment:
uv run --active src/agent.py download-files
Checks:
• Confirm the file exists: ls -l src/agent.py (or ls -l agent.py if it should be in the repo root).
• If the entry file has a different name, use that path instead.
Tell me:
• the exact path/name of your agent entry file (e.g., src/agent.py), and
• whether you want to run uv against the active venv,
and I’ll give the exact command to run.hundreds-king-75485
12/31/2025, 2:57 PMtall-airline-16756
12/31/2025, 2:57 PMhundreds-king-75485
12/31/2025, 3:09 PMtall-airline-16756
12/31/2025, 3:10 PMrtc.ParticipantKind and `AgentSession`/`AgentServer` setup.
• The exact line you currently use for participant_kinds.
• The command you run to start the agent and the full console output (current traceback).
• Output of these commands run in the same venv:
◦ pip show livekit-agents
◦ pip freeze | grep livekit
• The top ~20 lines of src/agent.py (imports and any prewarm/setup functions).
Once you paste those, I’ll tell you the exact code change to fix the enum/import/VAD setup and verify the environment.
Sources: Repeater | LiveKit docs | Agent session | LiveKit docs | Agents Framework | livekit/livekit_composite | DeepWiki | Uninterruptable Agent | LiveKit docs