The full application
Every LiveKit worker has one entrypoint. It is where you build the agents, register them in shared state, and start the session on the triage agent. This is the moment the architecture collapses from three separate classes into one running system.
from livekit.agents import JobContext, WorkerOptions, cli
from livekit.agents.voice import AgentSession
from livekit.agents.llm import function_tool
async def entrypoint(ctx: JobContext):
# Only pick up rooms this worker is responsible for
if not ctx.room.name.startswith("medical_"):
return
await ctx.connect()
# Build shared state and register all specialists
userdata = UserData(ctx=ctx)
triage = TriageAgent()
support = SupportAgent()
billing = BillingAgent()
userdata.personas.update({
"triage": triage,
"support": support,
"billing": billing,
})
session = AgentSession[UserData](userdata=userdata)
# Caller always lands on triage first
await session.start(agent=triage, room=ctx.room)The entrypoint builds every agent once per call, registers them in personas, and starts the session on the triage agent. The prefix check keeps this worker from grabbing rooms meant for other demos.
from livekit.agents.llm import function_tool
from livekit.agents.voice import Agent
from livekit.plugins import deepgram, silero
from utils.livekit_utils import get_livekit_llm
from base_agent import BaseAgent
from user_data import RunContext_T
from prompt_utils import load_prompt
class BillingAgent(BaseAgent):
"""Handles insurance, copays, and payment plans."""
def __init__(self) -> None:
super().__init__(
instructions=load_prompt("billing_prompt.yaml"),
stt=deepgram.STT(),
llm=get_livekit_llm(),
tts=deepgram.TTS(model="aura-asteria-en"),
vad=silero.VAD.load(),
)
@function_tool
async def transfer_to_support(self, context: RunContext_T) -> Agent:
"""Transfer to Support if the caller has medical service questions."""
await self.session.say(
"Let me transfer you to Patient Support for that."
)
return await self._transfer_to_agent("support", context)
@function_tool
async def transfer_to_triage(self, context: RunContext_T) -> Agent:
"""Transfer back to Triage if the caller needs re-routing."""
return await self._transfer_to_agent("triage", context)Every specialist follows the same shape. Inherit BaseAgent, load the domain prompt, declare the transfer tools it needs. Adding a new specialist is now a ten minute job.
Matching exercise: Match each piece to its role in the entrypoint
Loading practice…
Callers rarely know which specialist they need. They have a problem and want help. The triage agent asks a couple of clarifying questions and routes correctly the first time. It also gives you one place to add policies like authentication, fraud checks, or priority routing without touching the specialists.