Skip to content

Latest commit

Β 

History

History

drive_thru

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

README.md

Drive-Thru Example

A complete drive-thru ordering system demonstrating interactive voice agents for food ordering with database integration and order management.

For setup instructions and more details, see the main examples README.

Overview

This example simulates a fast food drive-thru. It is split across three files: database.py contains the menu and formats it as system prompt text, order.py holds Pydantic models for the three order types, and agent.py defines DriveThruAgent with dynamically built ordering tools.

The full menu is loaded once per session and injected directly into the agent's instructions, so the LLM has menu context without needing to call a tool.

Menu Loading

At the start of each session, new_userdata() queries FakeDB for all item categories (drinks, combos, Happy Meals, regulars, sauces) and stores them in the Userdata dataclass alongside a fresh OrderState.

async def new_userdata() -> Userdata:
fake_db = FakeDB()
drink_items = await fake_db.list_drinks()
combo_items = await fake_db.list_combo_meals()
happy_items = await fake_db.list_happy_meals()
regular_items = await fake_db.list_regulars()
sauce_items = await fake_db.list_sauces()
order_state = OrderState(items={})
userdata = Userdata(
order=order_state,
drink_items=drink_items,
combo_items=combo_items,
happy_items=happy_items,
regular_items=regular_items,
sauce_items=sauce_items,
)
return userdata
DriveThruAgent.__init__ then formats each category using menu_instructions() and concatenates the results with COMMON_INSTRUCTIONS to build the full system prompt. This means the LLM sees the entire menu from the first turn and can answer questions or suggest items without any tool calls.
def __init__(self, *, userdata: Userdata) -> None:
instructions = (
COMMON_INSTRUCTIONS
+ "\n\n"
+ menu_instructions("drink", items=userdata.drink_items)
+ "\n\n"
+ menu_instructions("combo_meal", items=userdata.combo_items)
+ "\n\n"
+ menu_instructions("happy_meal", items=userdata.happy_items)
+ "\n\n"
+ menu_instructions("regular", items=userdata.regular_items)
+ "\n\n"
+ menu_instructions("sauce", items=userdata.sauce_items)
)
super().__init__(
instructions=instructions,
tools=[
self.build_regular_order_tool(
userdata.regular_items, userdata.drink_items, userdata.sauce_items
),
self.build_combo_order_tool(
userdata.combo_items, userdata.drink_items, userdata.sauce_items
),
self.build_happy_order_tool(
userdata.happy_items, userdata.drink_items, userdata.sauce_items
),
],
)

Dynamic Tool Building

The three ordering tools are constructed by build_combo_order_tool, build_happy_order_tool, and build_regular_order_tool. Each method closes over the relevant item lists and injects their IDs as the enum constraint in the tool's JSON schema.

def build_combo_order_tool(
self, combo_items: list[MenuItem], drink_items: list[MenuItem], sauce_items: list[MenuItem]
) -> FunctionTool:
available_combo_ids = {item.id for item in combo_items}
available_drink_ids = {item.id for item in drink_items}
available_sauce_ids = {item.id for item in sauce_items}
@function_tool
async def order_combo_meal(
ctx: RunContext[Userdata],
meal_id: Annotated[
str,
Field(
description="The ID of the combo meal the user requested.",
json_schema_extra={"enum": list(available_combo_ids)},
),
],
drink_id: Annotated[
str,
Field(
description="The ID of the drink the user requested.",
json_schema_extra={"enum": list(available_drink_ids)},
),
],
drink_size: Literal["M", "L", "null"] | None,
fries_size: Literal["M", "L"],
sauce_id: Annotated[
str,
Field(
description="The ID of the sauce the user requested.",
json_schema_extra={"enum": [*available_sauce_ids, "null"]},
),
]
| None,
):

This restricts the LLM to known IDs at the schema layer before any runtime logic runs. ToolError handles the cases that can't be caught statically β€” for example, when a drink has multiple available sizes and the customer hasn't specified one yet, the tool raises a ToolError prompting the agent to ask for clarification before retrying.

Order Types

order.py defines three Pydantic models: OrderedCombo, OrderedHappy, and OrderedRegular . A discriminated union OrderedItem is also defined. Each ordered item receives a random short order_id on creation via order_uid().

OrderState stores the current cart as a dict[str, OrderedItem] keyed by order_id, which the remove_order_item and list_order_items tools use to look up or modify existing items.

@dataclass
class OrderState:
items: dict[str, OrderedItem]
async def add(self, item: OrderedItem) -> None:
self.items[item.order_id] = item
async def remove(self, order_id: str) -> OrderedItem:
return self.items.pop(order_id)
def get(self, order_id: str) -> OrderedItem | None:
return self.items[order_id]

Managing the Order

Two tools handle cart management:

  • list_order_items returns all current cart items with their order_ids. The agent is instructed to call this first when modifying or removing an item whose order_id is unknown.
  • remove_order_item removes one or more items by order_id. Modifications (e.g., upsizing fries) are done by removing the old item and re-adding it with the new parameters.

max_tool_steps=10 is set on the session to give the agent enough budget to call list_order_items followed by remove_order_item in a single turn when needed.

Background Audio

BackgroundAudioPlayer plays an ambient drive-thru noise track (bg_noise.mp3) throughout the session to set the scene.

background_audio = BackgroundAudioPlayer(
ambient_sound=AudioConfig(
str(os.path.join(os.path.dirname(os.path.abspath(__file__)), "bg_noise.mp3")),
volume=1.0,
),
)

STT Tuning

The STT model is also initialized with keyterm hints for McDonald's brand names (e.g., "Big Mac", "McFlurry", "McCrispy") to improve transcription accuracy.

stt=inference.STT(
"deepgram/nova-3",
language="en",
extra_kwargs={
"keyterm": [
"Big Mac",
"McFlurry",
"McCrispy",
"McNuggets",
"Meal",
"Sundae",
"Oreo",
"Jalapeno Ranch",
],
},
),

Simulations

scenarios.yaml holds scenarios that run the agent against a simulated customer, covering ordering, removal, combos, sizes, quantities, unavailable items and tool failures. Run them from the repo root:

lk agent simulate text examples/drive_thru/agent.py --scenarios examples/drive_thru/scenarios.yaml

All simulation glue lives in simulation.py. Each scenario's userdata can set:

  • expected_order: the items the order must hold when the run ends (order ids are ignored). on_simulation_end fails the run on a mismatch, even if the simulator passed it.
  • unavailable_items: menu item ids marked unavailable before the agent starts.
  • failing_tools: tools that raise instead of running, mocked on the live session with mock_tools(..., session=session).