cuddly-vr-85751
03/06/2026, 4:59 AMtall-airline-16756
03/06/2026, 4:59 AMtall-airline-16756
03/06/2026, 4:59 AMcuddly-vr-85751
03/06/2026, 4:59 AMtall-airline-16756
03/06/2026, 5:00 AMcuddly-vr-85751
03/06/2026, 5:01 AMtall-airline-16756
03/06/2026, 5:01 AMcuddly-vr-85751
03/06/2026, 5:02 AMtall-airline-16756
03/06/2026, 5:02 AMfrom (phone) or provider chat ID.
• Ensure AgentSession for that room is running and connected.
• Inject the message into the session:
◦ If you have a room/text input event API, send message as a participant message so the agent receives it. Or call a session method to process inbound text (e.g., session.receive_text("...")).
• Example flow:
a. POST /webhook -> parse {from, body}
b. room = get_or_create_room(from)
c. session = ensure_session_running(room)
d. await session.handle_incoming_text(body, sender=from)
4) Agent responses -> provider API (outbound)
• When the agent produces a text reply, send it back via provider REST API:
◦ Twilio: POST Messages API to send SMS/WhatsApp.
◦ WhatsApp Business API: /v1/messages endpoint.
◦ Generic provider: call their send endpoint with recipient id.
• Ensure deduplication and delivery checks (store message ids, handle retries).
5) Voice integration (existing voice code)
• For outbound/inbound telephony you already call create_sip_participant or ctx.connect().
• Use the same room mapping so the text conversation and voice call share transcripts/state.
• Continue recording and RAG enrichment as in your voice flow.
6) Session lifecycle & cleanup
• End session on inactivity or explicit end_call function.
• On session end: stop recording, save transcript, clear room mapping.
• Optionally persist chat history to DB or RAG store.
7) Security & reliability
• Verify webhook signature (Twilio X-Twilio-Signature or equivalent).
• Rate-limit inbound messages, validate content.
• Use retries and idempotency keys when sending outbound messages.
Concrete example snippets
1) Webhook handler (conceptual, Python/flask-like)
from flask import Flask, request, jsonify
@app.route("/webhook", methods=["POST"])
def webhook():
data = request.form or request.json
from_id = data.get("From") or data.get("from")
body = data.get("Body") or data.get("text")
room = get_or_create_room_for_conversation(from_id)
session = ensure_agent_session(room)
# inject text into agent session
await session.handle_incoming_text(body, sender=from_id)
return jsonify(status="ok")
2) Sending outbound via Twilio (conceptual)
from twilio.rest import Client
client = Client(TWILIO_SID, TWILIO_TOKEN)
def send_sms(to, body):
client.messages.create(
body=body,
from_=TWILIO_NUMBER,
to=to
)
3) Starting a text-enabled session (place in your entrypoint/room creation)
session = AgentSession(
vad=None, # no VAD for text-only
preemptive_generation=False,
# enable or disable STT/TTS depending on hybrid needs
)
await session.start(agent=agent, room=room, room_options={
"audio_input": True, # keep true for hybrid voice
"audio_output": True, # keep true for hybrid voice
"text_input": True,
"text_output": True,
})
Decisions I can help you finalize (pick any)
• Exact room naming and TTL strategy.
• How to handle simultaneous voice + text messages (race conditions).
• How to store transcripts and feed them to RAG.
• Concrete provider-specific webhook and reply code (Twilio, WhatsApp Business API, etc.).
Next step If you confirm:
• You prefer one room per conversation (recommended), and
• Which provider you want code for first (e.g., Twilio),
I’ll give a ready-to-run sample: webhook receiver, mapping logic, session starter changes, and outbound reply code you can paste into your project.cuddly-vr-85751
03/06/2026, 5:05 AMtall-airline-16756
03/06/2026, 5:06 AMimport os
import asyncio
import json
from datetime import datetime
from aiohttp import web
from livekit import RoomService # conceptual; replace with your LiveKit client import
from livekit.agents import Agent, AgentSession, WorkerOptions, cli
# Replace these with the LLM/STORAGE utils you use in your project
def get_llm_from_config():
# return an LLM instance compatible with livekit agents
raise NotImplementedError
def get_agent_instructions():
return "You are a helpful text-only assistant."
# Simple in-memory mapping: conversation_key -> room_name
# For production use a persistent DB.
ROOM_MAPPING = {}
class TextAgent(Agent):
def __init__(self):
super().__init__(
instructions=get_agent_instructions(),
llm=get_llm_from_config(),
stt=None, # no speech-to-text
tts=None, # no text-to-speech
tools=[]
)
async def ensure_room_for_conversation(conversation_key, lk_admin_client):
# return existing room or create new and start an agent session in it
room_name = ROOM_MAPPING.get(conversation_key)
if room_name:
return room_name
room_name = f"conv-{conversation_key}-{int(datetime.utcnow().timestamp())}"
# Create room via LiveKit admin API (pseudo-code)
lk_admin_client.create_room(room_name) # adapt to actual client
ROOM_MAPPING[conversation_key] = room_name
# Start agent session in background
asyncio.create_task(start_agent_session(room_name, conversation_key, lk_admin_client))
return room_name
async def start_agent_session(room_name, conversation_key, lk_admin_client):
# Create and start agent in this room; session handles incoming text messages
agent = TextAgent()
session = AgentSession(
agent=agent,
# For a text-only session, don't provide VAD/STT/TTS
)
# Custom handler for inbound text (we will call session.receive_text from webhook)
async def on_user_message(user_id: str, text: str):
# Add user message to history and generate reply
session.add_user_message(role="user", content=text) # pseudo-method - adapt to actual API
reply = await session.generate_reply() # pseudo-method
# session.say or emit transcription stream as needed
# we'll rely on the webhook caller to fetch the reply via a callback below
return reply
# store session somewhere accessible by webhooks; for simplicity attach on the mapping
# In production, store sessions in a process-safe store or use an agent worker.
SESSION_REGISTRY[room_name] = {
"session": session,
"on_user_message": on_user_message
}
# Start session lifecycle (connect to LiveKit room) - pseudo-code
await session.start(room=room_name, text_enabled=True, audio_enabled=False)
# Simple registry for sessions started in this process
SESSION_REGISTRY = {}
# Webhook endpoint to receive inbound text (from Twilio/WhatsApp/SMS provider)
# For Twilio, configure Messaging webhook to point to /webhook/sms
async def sms_webhook(request):
data = await <http://request.post|request.post>() if request.content_type == 'application/x-www-form-urlencoded' else await request.json()
# Typical Twilio fields: From, To, Body
sender = data.get("From") or data.get("from")
body = data.get("Body") or data.get("body") or data.get("text")
if not sender or not body:
return web.Response(text="missing sender or body", status=400)
# Use sender as conversation key (you can normalize)
conversation_key = sender.replace("+", "")
# Ensure a LiveKit room and session exists for this conversation
# lk_admin_client would be your LiveKit admin client instance
room = await ensure_room_for_conversation(conversation_key, lk_admin_client)
# Use the session registry to pass the incoming text to the agent
reg = SESSION_REGISTRY.get(room)
if not reg:
return web.Response(text="session not ready", status=503)
reply = await reg["on_user_message"](user_id=sender, text=body)
# If using Twilio, respond with TwiML or just reply via Twilio REST API (see send_sms below)
# Here we return JSON so your integration layer can send the reply back via provider API
return web.json_response({"reply": reply})
# Example function to send SMS via Twilio (call this with the agent reply)
from twilio.rest import Client as TwilioClient
TWILIO_SID = os.getenv("TWILIO_SID")
TWILIO_TOKEN = os.getenv("TWILIO_TOKEN")
TWILIO_NUMBER = os.getenv("TWILIO_NUMBER")
twilio_client = TwilioClient(TWILIO_SID, TWILIO_TOKEN)
def send_sms_via_twilio(to_number: str, body: str):
message = twilio_client.messages.create(
body=body,
from_=TWILIO_NUMBER,
to=to_number
)
return message.sid
# Minimal app runner
async def start_app():
global lk_admin_client
# create your LiveKit admin client as needed
lk_admin_client = RoomService(base_url=os.getenv("LIVEKIT_URL"), api_key=os.getenv("LIVEKIT_API_KEY"), api_secret=os.getenv("LIVEKIT_API_SECRET"))
app = web.Application()
app.router.add_post("/webhook/sms", sms_webhook)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, "0.0.0.0", int(os.getenv("PORT", 8080)))
await site.start()
print("Webhook server started on port", os.getenv("PORT", 8080))
if __name__ == "__main__":
asyncio.run(start_app())
Notes about the example
• This file is conceptual and uses pseudo-method names like session.add_user_message, session.generate_reply, and AgentSession(... text_enabled=True) to show intent. You will need to adapt those calls to the exact methods from your livekit-agents SDK version.
• SESSION_REGISTRY is an in-process registry. In a multi-process or multi-instance deployment, store session metadata in a shared database or message bus and run agent workers that subscribe to events for a room.
• For Twilio integration, either:
◦ Have your webhook return the bot reply directly (TwiML), or
◦ Return success and call Twilio REST API to send the reply asynchronously (recommended for complex flows).
• Security: verify webhooks (Twilio signature) and add rate limiting and validation.
How the flow works
1. Provider (Twilio/WhatsApp/etc.) posts inbound message to /webhook/sms.
2. The handler ensures a LiveKit room and agent session exist (one room per sender).
3. The inbound text is passed to the agent session; the agent generates a reply.
4. You send that reply back to the user via your provider API (Twilio example provided).
If you want, I can:
• Convert the pseudo-methods to exact calls for your livekit-agents SDK version (paste the exact SDK imports you use and I’ll adapt).
• Provide a fully worked Twilio webhook + send example that returns TwiML synchronously.
• Show a production-ready pattern using Redis (or Postgres) for session mapping and background worker processes.
Which would you like next:
• Exact livekit-agents method bindings (I’ll adapt to your SDK version), or
• A full Twilio webhook + reply example (synchronous TwiML and async REST send)? Sources: Examples | livekit/livekit_composite | DeepWikicuddly-vr-85751
03/06/2026, 5:10 AMtall-airline-16756
03/06/2026, 5:11 AM# file: text_agent_webhook.py
import os
import asyncio
from aiohttp import web
from datetime import datetime
# Replace these with actual imports from your livekit-agents SDK and LLM plugin
# from livekit.agents import Agent, AgentSession, start_session_in_room
# from your_llm import get_llm
# --- Simple in-memory registries (for demo) ---
ROOM_BY_CONVERSATION = {} # conversation_key -> room_name
SESSION_BY_ROOM = {} # room_name -> session object
# --- Replace with your real LLM/Agent class ---
class SimpleTextAgent:
def __init__(self, llm):
self.llm = llm
async def generate(self, history):
# Replace with actual LLM call. Echo for demo:
last_user = history[-1]['content'] if history else ""
return f"Echo: {last_user}"
# --- Mock/adapter functions you must implement with your SDK ---
async def create_livekit_room(room_name):
# Call LiveKit admin API to create a room
# e.g., livekit_client.create_room(room_name)
return room_name
async def start_text_session(room_name, agent):
# Start an agent session connected to the LiveKit room.
# This should create whatever in-process object you use to
# submit user messages and get replies.
# For demo we create a simple session object with history.
session = {
"room": room_name,
"agent": agent,
"history": []
}
SESSION_BY_ROOM[room_name] = session
return session
async def send_reply_via_provider(conversation_key, reply_text):
# Implement provider-specific send (Twilio, WhatsApp API, etc.)
# For demo we just print — replace with API call.
print(f"[send -> {conversation_key}] {reply_text}")
# --- Core: ensure room & session for a conversation ---
async def ensure_session_for_conversation(conversation_key):
if conversation_key in ROOM_BY_CONVERSATION:
room = ROOM_BY_CONVERSATION[conversation_key]
return SESSION_BY_ROOM.get(room)
# create new room and session
room_name = f"conv-{conversation_key}-{int(datetime.utcnow().timestamp())}"
await create_livekit_room(room_name)
ROOM_BY_CONVERSATION[conversation_key] = room_name
# instantiate agent and session (use your LLM)
llm = None # replace with get_llm()
agent = SimpleTextAgent(llm)
session = await start_text_session(room_name, agent)
return session
# --- Handle inbound webhook: message -> agent -> reply ---
async def inbound_webhook(request):
# Accept form-encoded (Twilio) or JSON
data = await <http://request.post|request.post>() if request.content_type == 'application/x-www-form-urlencoded' else await request.json()
conv_key = (data.get("From") or data.get("from") or data.get("sender") or data.get("user"))
text = (data.get("Body") or data.get("body") or data.get("text") or "").strip()
if not conv_key or not text:
return web.json_response({"error": "missing from or text"}, status=400)
# 1) Ensure session
session = await ensure_session_for_conversation(conv_key)
# 2) Append user message to session history
session["history"].append({"role": "user", "content": text})
# 3) Generate reply through agent
reply = await session["agent"].generate(session["history"])
# 4) Append assistant reply to history
session["history"].append({"role": "assistant", "content": reply})
# 5) Send reply back via messaging provider (async)
await send_reply_via_provider(conv_key, reply)
# 6) Return 200 for webhook
# For Twilio you may return TwiML or 200 and send via REST. This returns JSON for clarity.
return web.json_response({"reply": reply})
# --- App runner ---
async def start_app():
app = web.Application()
app.add_routes([<http://web.post|web.post>("/webhook/message", inbound_webhook)])
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, "0.0.0.0", int(os.getenv("PORT", 8080)))
await site.start()
print("Listening on port", os.getenv("PORT", 8080))
if __name__ == "__main__":
asyncio.run(start_app())
2) Where to adapt this to real LiveKit + LLM
• create_livekit_room(room_name): call your LiveKit admin client to create a room.
• start_text_session(room_name, agent): start a real AgentSession attached to the room with audio disabled and text I/O enabled. Store the session object so the webhook can deliver messages into it.
• session["agent"].generate(...) : replace with the actual method to feed user text to the agent (usually adding user message to session and calling generation). Many SDKs expose something like await session.handle_incoming_text(text) or await session.say(text) combined with session.generate() — use your SDK's exact calls.
• send_reply_via_provider(): implement Twilio/WhatsApp provider call. Example Twilio (synchronous reply via REST):
Example Twilio send (replace in send_reply_via_provider):
from twilio.rest import Client
TW_SID = os.getenv("TWILIO_SID")
TW_TOKEN = os.getenv("TWILIO_TOKEN")
TW_FROM = os.getenv("TWILIO_FROM")
tw_client = Client(TW_SID, TW_TOKEN)
async def send_reply_via_provider(conversation_key, reply_text):
msg = tw_client.messages.create(body=reply_text, from_=TW_FROM, to=conversation_key)
return msg.sid
3) Simple flow summary
• Provider posts inbound message to /webhook/message.
• Webhook ensures a room/session exists for that conversation.
• Webhook inserts the message into the session history and calls the agent to generate a reply.
• Webhook sends the reply back through the messaging provider.
If you want, I can:
• Convert this to exact livekit-agents calls if you paste the AgentSession methods your SDK provides (e.g., how to send text into a session and get the generated reply).
• Provide a complete Twilio example that returns TwiML directly (synchronous reply).
• Show how to store ROOM_BY_CONVERSATION in Redis/Postgres for production.
Which one should I send next — exact SDK-binding code or a Twilio synchronous example?cuddly-vr-85751
03/06/2026, 5:12 AMtall-airline-16756
03/06/2026, 5:13 AMlk.chat). The agent receives that text like any other participant message.
• AI calls your backend API that forwards text into the agent session (server-side bridge). Backend either:
◦ Calls an AgentSession method to inject text (if your SDK exposes such a method), or
◦ Publishes to the LiveKit text stream programmatically using the LiveKit server/admin SDK or a headless participant connection.
• AI writes messages directly into the agent’s session store (if the agent exposes a programmatic API for adding user messages and calling generation).
Which to choose
• Join room as participant: easiest when you want the AI visible as a participant, want message metadata (participant identity), and want the same text stream mechanics as a regular user.
• Server-side bridge: simplest to implement if you already run the agent server-side and want direct control over sessions and lifecycle (recommended for most integrations).
• Direct session API: fastest and lowest-latency if your agent SDK exposes a method like session.receive_text() or session.add_user_message() and session.generate_reply().
Minimal server-side bridge example (AI -> agent -> reply) This is a concise pattern: your AI generates text, your server injects it into the agent session, the agent generates a reply, and your server returns/sends that reply back.
1) Conceptual flow
1. AI produces text (locally or via LLM).
2. Server receives the AI text and finds the correct AgentSession (one session per room/conversation).
3. Server injects the AI text into the AgentSession (as if a participant sent it).
4. Agent generates a reply; server reads that reply and uses it as needed (store/display/send).
2) Pseudocode (Python — fill in SDK-specific calls)
• Replace pseudo-methods with the methods your livekit-agents SDK exposes (e.g., session.handle_incoming_text(), session.generate_reply(), session.say()).
# ai_to_agent_bridge.py (conceptual)
# 1) AI produces text
ai_text = "Hello, I am an AI system. Please help the user."
# 2) Find session for conversation/room
room_name = lookup_room_for_conversation(conversation_key) # your mapping
session = SESSION_REGISTRY.get(room_name) # session object your process manages
if not session:
# create room & start session (text-only) first
room_name = await create_room_for(conversation_key)
session = await start_text_only_session(room_name)
SESSION_REGISTRY[room_name] = session
# 3) Inject AI text into the agent session
# Common patterns:
# - session.add_user_message(role="user", content=ai_text)
# - await session.receive_text(ai_text, participant_id="ai-bot")
session.history.append({"role": "user", "content": ai_text}) # pseudo
# then trigger generation:
reply = await session.agent.generate(session.history) # pseudo
# 4) Use the reply (log/display/send)
print("Agent reply:", reply)
# Optionally publish the reply into the room text stream so other participants see it:
# await publish_text_to_room(room_name, reply, topic="lk.transcription")
3) If you want the AI to join as a participant (LiveKit text stream)
• Create a short-lived participant token for the AI.
• Connect a headless LiveKit client (or server-side SDK that can publish data) and send text using the LiveKit client API to the lk.chat topic.
• The agent receives the message through its text-input handler.
JavaScript example to publish text as a participant:
// after obtaining a token for AI participant and connecting:
await room.localParticipant.publishData("AI: Hello user", { topic: "lk.chat" });
Receiving agent replies
• Have the agent emit replies into a text stream (commonly lk.transcription) or call session.say() so replies are available to participants.
• If using server-side bridge, read the generated reply from the session and handle/stash it where you need it.
Security & best practices
• Protect your session injection API (only trusted AI processes should be allowed).
• Use short-lived LiveKit tokens for AI participants if they join rooms.
• If multiple instances run, store session mapping in a shared database (Redis/Postgres) and use a worker process to route injected messages to the right instance.
Next step Tell me which approach you prefer:
• Server-side bridge (I’ll provide concrete Python code using your livekit-agents SDK calls if you paste those method names), or
• AI as a LiveKit participant (I’ll give exact steps to mint a token, connect headless client, and publish text).
If you want the bridge example now, confirm whether you use the livekit-agents Python SDK and paste the exact methods you use to:
• add user text to a session, and
• trigger generation or read the agent reply.
If you don’t know the method names, tell me and I’ll provide clear pseudo-to-SDK mappings you can try.
Sources: Text and transcriptions | LiveKit Documentation | How to build an agent with speech-to-text input and text-only output | Live...