168 lines
7.0 KiB
Python
168 lines
7.0 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import sys
|
|
from datetime import date, time
|
|
from typing import Annotated
|
|
|
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from book_restaurant import BookRestaurantTask
|
|
from common import Userdata, _speak_code
|
|
from context import speech_only
|
|
from hotel_db import (
|
|
MAX_PARTY_SIZE,
|
|
TODAY,
|
|
Unavailable,
|
|
speak_time,
|
|
)
|
|
from pydantic import Field
|
|
|
|
from livekit.agents import RunContext, ToolError, function_tool
|
|
|
|
logger = logging.getLogger("hotel-receptionist")
|
|
|
|
|
|
class RestaurantToolsMixin:
|
|
@function_tool
|
|
async def check_restaurant_availability(
|
|
self,
|
|
ctx: RunContext[Userdata],
|
|
on_date: date,
|
|
party_size: Annotated[int, Field(ge=1, le=MAX_PARTY_SIZE)],
|
|
) -> str:
|
|
"""Check restaurant time slots for a date. Read-only browsing - to actually book a table, call start_restaurant_booking.
|
|
|
|
Args:
|
|
on_date: The date to check, in ISO YYYY-MM-DD format (e.g. "2026-01-20").
|
|
party_size: Number of guests (must be >= 1; ask the caller if not specified).
|
|
"""
|
|
slots = await ctx.userdata.db.list_restaurant_availability(
|
|
on_date=on_date, party_size=party_size
|
|
)
|
|
open_slots = [s for s in slots if s.available_table_ids]
|
|
if not open_slots:
|
|
return f"fully booked on {on_date.strftime('%A, %B %-d')}"
|
|
return ", ".join(speak_time(s.time) for s in open_slots)
|
|
|
|
@function_tool
|
|
async def start_restaurant_booking(self, ctx: RunContext[Userdata]) -> str | None:
|
|
"""Start the restaurant-reservation flow. Call it the moment the caller wants a table - the flow collects date, party size, time, name, and phone itself. Its return is the FINAL result of the reservation: relay it and move on - nothing further to confirm or call afterwards."""
|
|
reservation = await BookRestaurantTask(
|
|
db=ctx.userdata.db, chat_ctx=speech_only(self.chat_ctx)
|
|
)
|
|
return (
|
|
f"You're set for {speak_time(reservation.time)} on "
|
|
f"{reservation.date.strftime('%A, %B %-d')} for "
|
|
f"{reservation.party_size} guest{'s' if reservation.party_size != 1 else ''}. "
|
|
f"Confirmation code: {_speak_code(reservation.code)}. "
|
|
"| reservation complete - relay this to the caller; no further tool call is needed."
|
|
)
|
|
|
|
@function_tool
|
|
async def lookup_restaurant_reservation(
|
|
self,
|
|
ctx: RunContext[Userdata],
|
|
last_name: str,
|
|
confirmation_code: str,
|
|
) -> str:
|
|
"""Read-only lookup of a confirmed restaurant reservation. Use this when the caller wants
|
|
to check or recall their reservation details (date, time, party size, notes) without
|
|
changing or cancelling it.
|
|
|
|
Args:
|
|
last_name: caller's last name.
|
|
confirmation_code: confirmation code like 'RES-X9Y2'.
|
|
"""
|
|
code = confirmation_code.replace(" ", "").upper()
|
|
reservation = await ctx.userdata.db.find_restaurant_reservation(
|
|
last_name=last_name, confirmation_code=code
|
|
)
|
|
if not reservation or reservation.status != "confirmed":
|
|
raise ToolError("Couldn't find a matching confirmed reservation.")
|
|
notes_part = f", note: {reservation.notes}" if reservation.notes else ""
|
|
return (
|
|
f"Reservation for {reservation.first_name} {reservation.last_name}, "
|
|
f"{speak_time(reservation.time)} on {reservation.date.strftime('%A, %B %-d')}, "
|
|
f"party of {reservation.party_size}{notes_part}."
|
|
)
|
|
|
|
@function_tool
|
|
async def cancel_restaurant_reservation(
|
|
self,
|
|
ctx: RunContext[Userdata],
|
|
last_name: str,
|
|
confirmation_code: str,
|
|
) -> str:
|
|
"""Cancel a restaurant reservation. Restaurants verify with last name + confirmation code (no card, no email — the code is what we print when the table is booked).
|
|
|
|
Args:
|
|
last_name: caller's last name.
|
|
confirmation_code: confirmation code like 'RES-X9Y2'.
|
|
"""
|
|
code = confirmation_code.replace(" ", "").upper()
|
|
reservation = await ctx.userdata.db.find_restaurant_reservation(
|
|
last_name=last_name, confirmation_code=code
|
|
)
|
|
if not reservation or reservation.status != "confirmed":
|
|
raise ToolError("Couldn't find a matching confirmed reservation.")
|
|
await ctx.userdata.db.cancel_restaurant_reservation(reservation.code)
|
|
return (
|
|
f"Reservation for {speak_time(reservation.time)} on "
|
|
f"{reservation.date.strftime('%A, %B %-d')} cancelled."
|
|
)
|
|
|
|
@function_tool
|
|
async def modify_restaurant_reservation(
|
|
self,
|
|
ctx: RunContext[Userdata],
|
|
last_name: str,
|
|
confirmation_code: str,
|
|
new_date: date,
|
|
new_time: str,
|
|
new_party_size: Annotated[int, Field(ge=1, le=MAX_PARTY_SIZE)] | None = None,
|
|
) -> str:
|
|
"""Move an existing confirmed restaurant reservation to a new date/time (and
|
|
optionally a new party size), keeping the same confirmation code. Restaurants
|
|
verify with last name + confirmation code (no card, no email). Read the new
|
|
details back to the caller before calling this.
|
|
|
|
Args:
|
|
last_name: caller's last name.
|
|
confirmation_code: confirmation code like 'RES-X9Y2'.
|
|
new_date: the new date, in ISO YYYY-MM-DD format (e.g. "2026-01-20").
|
|
new_time: the new time, in 24-hour HH:MM format (e.g. "18:00").
|
|
new_party_size: new number of guests; omit to keep the current party size.
|
|
"""
|
|
if new_date < TODAY:
|
|
raise ToolError("the new date can't be in the past")
|
|
code = confirmation_code.replace(" ", "").upper()
|
|
reservation = await ctx.userdata.db.find_restaurant_reservation(
|
|
last_name=last_name, confirmation_code=code
|
|
)
|
|
if not reservation or reservation.status != "confirmed":
|
|
raise ToolError("Couldn't find a matching confirmed reservation.")
|
|
try:
|
|
at_time = time.fromisoformat(new_time)
|
|
except ValueError:
|
|
raise ToolError("Please give the new time as 24-hour HH:MM, e.g. 18:00.") from None
|
|
try:
|
|
updated = await ctx.userdata.db.modify_restaurant_reservation(
|
|
code=reservation.code,
|
|
on_date=new_date,
|
|
at_time=at_time,
|
|
party_size=new_party_size,
|
|
)
|
|
except Unavailable:
|
|
raise ToolError(
|
|
f"No table for a party of {new_party_size or reservation.party_size} "
|
|
f"at {speak_time(at_time)} on {new_date.strftime('%A, %B %-d')}."
|
|
) from None
|
|
return (
|
|
f"Done - your reservation is now {speak_time(updated.time)} on "
|
|
f"{updated.date.strftime('%A, %B %-d')} for "
|
|
f"{updated.party_size} guest{'s' if updated.party_size != 1 else ''}, "
|
|
f"under confirmation code {_speak_code(updated.code)}."
|
|
)
|