polite-oil-10264
02/28/2026, 7:19 PMtall-airline-16756
02/28/2026, 7:19 PMtall-airline-16756
02/28/2026, 7:19 PMpolite-oil-10264
02/28/2026, 7:21 PMtall-airline-16756
02/28/2026, 7:21 PMget_document(doc_id) — returns full doc or specific sections (auth-protected).
◦ list_releases() — returns recent changelog entries (dynamic).
1. Register provider tools
◦ Add WebSearch provider plugin to agent so the LLM can call it when needed.
1. on_user_turn_completed
◦ Run vector search with embeddings; get top-k.
◦ If vector results confidence low or user asks for latest info, enable WebSearch.
◦ Inject passages into chat context as assistant/system message before response generation.
1. Response & citation
◦ Always attach sources: internal doc IDs with permalinks and external URLs from web search.
◦ If answer used live web results, indicate that the info came from a web search and show link(s).
1. UX considerations
◦ Show “sources” UI panel with clickable docs.
◦ Allow users to ask “show me the doc” which calls get_document.
◦ Provide follow-ups like “Would you like step-by-step setup?” to keep conversation actionable.
Best practices and safeguards
• Limit RAG injection size: pass only top relevant passages to avoid context bloat.
• Rate-limit or gate web searches to control cost and spurious results.
• Always surface citations and let users request original docs.
• Use function tools for private data; never expose private content through provider tools.
• Pre-warm frequently used docs in memory for low-latency responses.
• Add a confidence note when the LLM relied primarily on web search rather than internal docs.
Example conceptual code snippets
• RAG injection in on_user_turn_completed:
async def on_user_turn_completed(self, turn_ctx, new_message):
query = new_message.text_content()
rag_passages = await vector_search(query, top_k=4)
turn_ctx.add_message(role="assistant",
content=f"RAG_CONTEXT_START\n{rag_passages}\nRAG_CONTEXT_END")
• Function tool skeleton:
@function_tool()
async def get_document(context, doc_id: str) -> str:
# auth checks...
return await fetch_full_document_text(doc_id)
• Registering WebSearch provider:
agent = Agent(llm=xai.realtime.RealtimeModel(),
tools=[xai.realtime.WebSearch(), get_document])
Metrics to monitor
• Answer accuracy (user feedback / thumbs up/down).
• Latency (RAG lookup + response time).
• Tool usage frequency (how often WebSearch vs internal docs were used).
• Source click-through to ensure citations are useful.
Next steps I can provide
• A tailored sequence diagram for your specific platform (include components like SSO, S3, or private KB).
• A sample repo-level starter that wires a LiveKit Agent with vector DB + WebSearch + document tools.
• Guidance on chunking/embedding parameters for best RAG performance.
Which of those next steps would you like me to produce? If you want the sample starter, please tell me your preferred LLM provider and which document types/storage you’ll use (S3, Google Drive, Notion, etc.).
Sources: xAI Grok Voice Agent API plugin | LiveKit Documentation | External data and RAG | LiveKit Documentation | Pipeline nodes and hooks | LiveKit Documentation | GetEmailTask | LiveKit Documentation | Building multi-agent architectures with LiveKit agents | LiveKitpolite-oil-10264
02/28/2026, 7:22 PMtall-airline-16756
02/28/2026, 7:23 PMpolite-oil-10264
02/28/2026, 7:24 PMtall-airline-16756
02/28/2026, 7:24 PMpolite-oil-10264
02/28/2026, 7:25 PMtall-airline-16756
02/28/2026, 7:25 PM# config.py
GROQ_API_KEY = "YOUR_GROQ_API_KEY"
GROQ_ENDPOINT = "<https://api.groq.ai/v1>" # adjust if needed
AWS_ACCESS_KEY_ID = "YOUR_AWS_KEY"
AWS_SECRET_ACCESS_KEY = "YOUR_AWS_SECRET"
AWS_REGION = "us-east-1"
S3_BUCKET = "your-private-doc-bucket"
VECTOR_INDEX_PATH = "faiss_index.pkl"
EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
WEBSEARCH_ENABLED = True
TTS_ENABLED = True
# Groq model name
GROQ_MODEL = "groq-model-name" # replace with actual model id
2) rag.py — chunking, embeddings, FAISS index helpers
# rag.py
import os
import pickle
import numpy as np
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
import faiss
from config import EMBEDDING_MODEL, VECTOR_INDEX_PATH
EMB_MODEL = SentenceTransformer(EMBEDDING_MODEL)
def chunk_text(text, max_tokens=500, overlap=50):
# simple character-based chunker tuned for moderate sizes
words = text.split()
chunks = []
i = 0
while i < len(words):
chunk = words[i:i+max_tokens]
chunks.append(" ".join(chunk))
i += max_tokens - overlap
return chunks
def embed_texts(texts):
return EMB_MODEL.encode(texts, show_progress_bar=False)
def build_faiss_index(docs):
# docs: list of dict {doc_id, section_id, text}
texts = [d["text"] for d in docs]
embeddings = embed_texts(texts).astype("float32")
dim = embeddings.shape[1]
index = faiss.IndexFlatIP(dim)
faiss.normalize_L2(embeddings)
index.add(embeddings)
meta = {"docs": docs}
with open(VECTOR_INDEX_PATH, "wb") as f:
pickle.dump((index, meta), f)
return index, meta
def load_faiss_index():
if not os.path.exists(VECTOR_INDEX_PATH):
return None, None
with open(VECTOR_INDEX_PATH, "rb") as f:
index, meta = pickle.load(f)
return index, meta
def vector_search(query, top_k=4):
index, meta = load_faiss_index()
if index is None:
return []
q_emb = embed_texts([query]).astype("float32")
faiss.normalize_L2(q_emb)
D, I = index.search(q_emb, top_k)
results = []
for idx in I[0]:
if idx < 0: continue
results.append(meta["docs"][idx])
return results
3) ingest/ingest_s3.py — download S3 docs, parse, chunk, build index
# ingest/ingest_s3.py
import boto3
import os
import io
import mimetypes
from botocore import UNSIGNED
from botocore.client import Config
from rag import chunk_text, build_faiss_index
from config import AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, S3_BUCKET
s3 = boto3.client(
"s3",
aws_access_key_id=AWS_ACCESS_KEY_ID,
aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
region_name=AWS_REGION,
)
def list_bucket_objects(prefix=""):
resp = s3.list_objects_v2(Bucket=S3_BUCKET, Prefix=prefix)
for item in resp.get("Contents", []):
yield item["Key"]
def download_text(key):
obj = s3.get_object(Bucket=S3_BUCKET, Key=key)
content_type = obj.get("ContentType", "")
raw = obj["Body"].read()
# naive handling:
if content_type.startswith("text/") or key.endswith(".txt"):
return raw.decode("utf-8")
elif key.endswith(".pdf"):
# minimal PDF handling using PyPDF2 if installed
try:
from PyPDF2 import PdfReader
reader = PdfReader(io.BytesIO(raw))
text = "\n".join(p.extract_text() or "" for p in reader.pages)
return text
except Exception:
return ""
elif key.endswith(".docx"):
try:
import docx
doc = docx.Document(io.BytesIO(raw))
return "\n".join(p.text for p in doc.paragraphs)
except Exception:
return ""
else:
return raw.decode("utf-8", errors="ignore")
def ingest_all():
docs = []
for key in list_bucket_objects():
print("Downloading", key)
text = download_text(key)
if not text:
continue
chunks = chunk_text(text, max_tokens=300, overlap=50)
for i, c in enumerate(chunks):
docs.append({"doc_id": key, "section_id": f"{key}#chunk{i}", "text": c})
build_faiss_index(docs)
if __name__ == "__main__":
ingest_all()
4) tools.py — S3 fetch function tool, web-search wrapper
# tools.py
import boto3
from config import AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, S3_BUCKET, WEBSEARCH_ENABLED
import aiohttp
import asyncio
s3 = boto3.client(
"s3",
aws_access_key_id=AWS_ACCESS_KEY_ID,
aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
region_name=AWS_REGION,
)
async def get_document_from_s3(doc_id: str) -> str:
# doc_id is S3 key
loop = asyncio.get_running_loop()
resp = await loop.run_in_executor(None, s3.get_object, S3_BUCKET, doc_id)
raw = resp["Body"].read()
try:
return raw.decode("utf-8")
except Exception:
return raw.decode("utf-8", errors="ignore")
# Simple web search wrapper that returns top snippet(s)
async def web_search(query: str, max_results: int = 3) -> list:
if not WEBSEARCH_ENABLED:
return []
# Placeholder: replace with provider integration; here we call a generic websearch API or a simple SERP API.
# Example uses an imaginary endpoint — replace with a real provider or plugin call.
SEARCH_API = "<https://api.example-search.com/search>"
async with aiohttp.ClientSession() as sess:
async with sess.get(SEARCH_API, params={"q": query, "num": max_results}) as r:
if r.status != 200:
return []
data = await r.json()
return data.get("results", [])
Note: Replace web_search with actual provider plugin code or appropriate provider library (xAI/Gemini) that the agent can call directly. The example shows where that integration belongs.
5) tts.py — simple TTS using an offline TTS or external API
# tts.py
# Simple placeholder: use any TTS provider; here's an example using pyttsx3 (offline).
import pyttsx3
from config import TTS_ENABLED
_engine = None
def speak(text):
if not TTS_ENABLED:
return
global _engine
if _engine is None:
_engine = pyttsx3.init()
_engine.say(text)
_engine.runAndWait()
6) agent_runtime.py — main Agent logic with on_user_turn_completed
# agent_runtime.py
import asyncio
from rag import vector_search
from tools import get_document_from_s3, web_search
from tts import speak
from config import GROQ_API_KEY, GROQ_MODEL, GROQ_ENDPOINT
import aiohttp
import json
#
# agent_runtime.py
import asyncio
from rag import vector_search
from tools import get_document_from_s3, web_search
from tts import speak
from config import GROQ_API_KEY, GROQ_MODEL, GROQ_ENDPOINT
import aiohttp
import json
#