Files
2026-07-13 13:39:38 +08:00

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)}."
)