Building the greeter agent
Building the greeter agent
Every great restaurant experience starts at the front door. In this chapter, you will build the Greeter Agent — the first thing a customer interacts with. It welcomes diners, answers quick questions, and hands off to the Order Taker when the guest is ready. Along the way, you will learn how to subclass Agent, use on_enter() for automatic greetings, and create a tool that triggers an agent handoff.
What you'll learn
- How to create a custom agent by subclassing
Agent - How to use
on_enter()to greet the user automatically - How to define a tool that triggers an agent handoff
- How to wire up the agent with a session entrypoint
Subclassing Agent
In Course 1.1, you created agents by passing instructions directly to the Agent() constructor. That works for simple cases, but when an agent needs its own tools, lifecycle hooks, and custom behavior, subclassing is cleaner.
from livekit.agents import Agent, function_tool, RunContext
class GreeterAgent(Agent):
def __init__(self):
super().__init__(
instructions="""You are a friendly host at Bella Vista, an Italian restaurant.
Your job is to welcome guests, answer quick questions about hours
and wait times, and transfer them to the order taker when they are
ready to order.
Keep responses warm and brief — one to two sentences.
Never use markdown, bullet points, or emoji.
If the guest asks to see the menu or place an order, transfer them
to the order taker right away.""",
)import { Agent, functionTool, RunContext } from "@livekit/agents";
class GreeterAgent extends Agent {
constructor() {
super({
instructions: `You are a friendly host at Bella Vista, an Italian restaurant.
Your job is to welcome guests, answer quick questions about hours
and wait times, and transfer them to the order taker when they are
ready to order.
Keep responses warm and brief — one to two sentences.
Never use markdown, bullet points, or emoji.
If the guest asks to see the menu or place an order, transfer them
to the order taker right away.`,
});
}
}Subclassing Agent lets you co-locate instructions, tools, and lifecycle hooks in a single class. The super().__init__() call passes your instructions to the base class. Everything else — tools, on_enter(), on_exit() — becomes a method on the class.
Greeting with on_enter()
When an agent takes over a conversation — either at the start or after a handoff — LiveKit calls its on_enter() method. This is where you put automatic behavior that should happen before the user speaks.
For the greeter, you want an immediate welcome message so the customer does not sit in silence wondering if anyone is there.
from livekit.agents import Agent, function_tool, RunContext
class GreeterAgent(Agent):
def __init__(self):
super().__init__(
instructions="""You are a friendly host at Bella Vista, an Italian restaurant.
Your job is to welcome guests, answer quick questions about hours
and wait times, and transfer them to the order taker when they are
ready to order.
Keep responses warm and brief — one to two sentences.
Never use markdown, bullet points, or emoji.
If the guest asks to see the menu or place an order, transfer them
to the order taker right away.""",
)
async def on_enter(self):
self.session.generate_reply(
instructions="Greet the guest warmly. Welcome them to Bella Vista and ask how you can help them today."
)import { Agent, functionTool, RunContext } from "@livekit/agents";
class GreeterAgent extends Agent {
constructor() {
super({
instructions: `You are a friendly host at Bella Vista, an Italian restaurant.
Your job is to welcome guests, answer quick questions about hours
and wait times, and transfer them to the order taker when they are
ready to order.
Keep responses warm and brief — one to two sentences.
Never use markdown, bullet points, or emoji.
If the guest asks to see the menu or place an order, transfer them
to the order taker right away.`,
});
}
async onEnter() {
this.session.generateReply({
instructions: "Greet the guest warmly. Welcome them to Bella Vista and ask how you can help them today.",
});
}
}generate_reply() is non-blocking
generate_reply() tells the LLM to produce a response using the given instructions as additional context. It does not block — the method returns immediately and the LLM generates the greeting asynchronously. The instructions parameter is a one-time addition to the system prompt for just this generation; it does not change the agent's base instructions.
Adding tools to the greeter
The greeter needs two informational tools and one handoff tool. The informational tools let the LLM answer questions about hours and wait times using real data instead of guessing.
class GreeterAgent(Agent):
def __init__(self):
super().__init__(
instructions="""You are a friendly host at Bella Vista, an Italian restaurant.
Your job is to welcome guests, answer quick questions about hours
and wait times, and transfer them to the order taker when they are
ready to order.
Keep responses warm and brief — one to two sentences.
Never use markdown, bullet points, or emoji.
If the guest asks to see the menu or place an order, transfer them
to the order taker right away.
You have tools to check restaurant hours and current wait times.
Always use them rather than guessing.""",
)
async def on_enter(self):
self.session.generate_reply(
instructions="Greet the guest warmly. Welcome them to Bella Vista and ask how you can help them today."
)
@function_tool
async def check_hours(self, context: RunContext) -> str:
"""Check the restaurant's operating hours."""
return "Bella Vista is open Monday through Saturday, 11 AM to 10 PM. Closed on Sundays."
@function_tool
async def check_wait_time(self, context: RunContext) -> str:
"""Check the current estimated wait time for a table."""
# In production, query your reservation system
return "Current wait time is approximately 15 minutes."class GreeterAgent extends Agent {
constructor() {
super({
instructions: `You are a friendly host at Bella Vista, an Italian restaurant.
Your job is to welcome guests, answer quick questions about hours
and wait times, and transfer them to the order taker when they are
ready to order.
Keep responses warm and brief — one to two sentences.
Never use markdown, bullet points, or emoji.
If the guest asks to see the menu or place an order, transfer them
to the order taker right away.
You have tools to check restaurant hours and current wait times.
Always use them rather than guessing.`,
});
}
async onEnter() {
this.session.generateReply({
instructions: "Greet the guest warmly. Welcome them to Bella Vista and ask how you can help them today.",
});
}
@functionTool({ description: "Check the restaurant's operating hours." })
async checkHours(context: RunContext): Promise<string> {
return "Bella Vista is open Monday through Saturday, 11 AM to 10 PM. Closed on Sundays.";
}
@functionTool({ description: "Check the current estimated wait time for a table." })
async checkWaitTime(context: RunContext): Promise<string> {
return "Current wait time is approximately 15 minutes.";
}
}When tools are defined as methods on an Agent subclass using the @function_tool decorator, they are automatically registered with that agent. You do not need to pass them in a tools=[] list — the framework discovers them through the decorator.
The handoff tool
The most important tool on the greeter is the one that transfers the guest to the order taker. In LiveKit Agents, you trigger a handoff by calling self.hand_off() with an instance of the target agent.
from order_taker_agent import OrderTakerAgent
class GreeterAgent(Agent):
# ... instructions, on_enter, check_hours, check_wait_time ...
@function_tool
async def transfer_to_order_taker(self, context: RunContext) -> str:
"""Transfer the guest to the order taker when they are ready to order."""
self.hand_off(OrderTakerAgent())
return "Transferring you to our order taker now."import { OrderTakerAgent } from "./orderTakerAgent";
class GreeterAgent extends Agent {
// ... instructions, onEnter, checkHours, checkWaitTime ...
@functionTool({ description: "Transfer the guest to the order taker when they are ready to order." })
async transferToOrderTaker(context: RunContext): Promise<string> {
this.handOff(new OrderTakerAgent());
return "Transferring you to our order taker now.";
}
}The LLM decides to hand off
When the guest says something like "I'd like to order" or "Can I see the menu?", the LLM reads the instructions ("transfer them to the order taker right away") and decides to call transfer_to_order_taker.
hand_off() is called
self.hand_off(OrderTakerAgent()) creates a new instance of the Order Taker and tells the session to switch to it. The current agent's on_exit() will fire, then the new agent's on_enter() will fire.
The return value is spoken
The string "Transferring you to our order taker now." is fed back to the LLM, which uses it to generate a natural transition message before the new agent takes over.
Always instantiate the target agent
self.hand_off() takes an instance, not a class. Write self.hand_off(OrderTakerAgent()), not self.hand_off(OrderTakerAgent). Passing the class itself will raise an error.
Wiring up the entrypoint
With the GreeterAgent complete, connect it to a session entrypoint so it runs when a user connects.
from livekit.agents import AgentServer, AgentSession
from livekit.plugins import openai, deepgram, cartesia
from greeter_agent import GreeterAgent
server = AgentServer()
@server.rtc_session
async def entrypoint(session: AgentSession):
await session.start(
agent=GreeterAgent(),
room=session.room,
stt=deepgram.STT(model="nova-3"),
llm=openai.LLM(model="gpt-4o-mini"),
tts=cartesia.TTS(voice="<voice-id>"),
)
if __name__ == "__main__":
server.run()import { AgentServer, AgentSession } from "@livekit/agents";
import { DeepgramSTT } from "@livekit/plugins-deepgram";
import { OpenAILLM } from "@livekit/plugins-openai";
import { CartesiaTTS } from "@livekit/plugins-cartesia";
import { GreeterAgent } from "./greeterAgent";
const server = new AgentServer();
server.rtcSession(async (session: AgentSession) => {
await session.start({
agent: new GreeterAgent(),
room: session.room,
stt: new DeepgramSTT({ model: "nova-3" }),
llm: new OpenAILLM({ model: "gpt-4o-mini" }),
tts: new CartesiaTTS({ voice: "<voice-id>" }),
});
});
server.run();The entrypoint creates a GreeterAgent instance and passes it as the starting agent. When the session begins, the framework calls on_enter() on the greeter, which triggers the welcome message. From there, the conversation flows naturally until the guest is ready to order and the handoff tool fires.
What you learned
- Subclassing
Agentlets you co-locate instructions, tools, and lifecycle hooks in one class on_enter()runs automatically when an agent takes over — use it for greetings and setupgenerate_reply()asks the LLM to produce a response with optional one-time instructions- Tools defined as decorated methods on the agent subclass are auto-registered
self.hand_off(TargetAgent())transfers the conversation to a new agent instance
Test your knowledge
Question 1 of 3
What does on_enter() do when an agent takes over a conversation?
Next up
The greeter's tools are simple — string in, string out. In the next chapter, you will go beyond decorators and learn how to create tools programmatically, define complex parameter schemas, manage tools with Toolset, and control tool behavior with ToolFlag.