Skip to content

fix(hotel_receptionist): guest-services tool fixes the carve-outs left behind - #6808

Open
u9g wants to merge 10 commits into
feat/hotel-db-splitfrom
feat/hotel-services-residual
Open

fix(hotel_receptionist): guest-services tool fixes the carve-outs left behind#6808
u9g wants to merge 10 commits into
feat/hotel-db-splitfrom
feat/hotel-services-residual

Conversation

@u9g

@u9g u9g commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Stacked on #6805 β€” four of these refine code that only exists on that branch (amend_florist_order, speak_room, the florist/flight db signatures). Review the last 10 commits.

The guest-services tool fixes from #6567 that the earlier carve-outs left behind, one commit each:

  • record_followup takes the caller's real name. A followup is a note for a human about a person; "guest in 402" identifies the room. The room number stays valid as the callback number.
  • Florist delivery notes stay off the followup list. record_followup's own docstring never named the exception, so notes routed there and reached nobody.
  • A group inquiry answers what it deferred. The tool tells the agent to record first and answer after; nothing brought it back, so questions asked while collecting died with the call.
  • Spa price and duration get quoted before booking, not only after, and the read-back includes the duration and reference.
  • The florist order reference is part of the confirmation, the way every other booking's is.
  • Delivery preference is a closed set instead of free text, which let the agent write the destination back as a handling note or promise a delivery time the florist never guaranteed. The read-back speaks the chosen value rather than its stored form.
  • resend_confirmation won't email the same document twice for one ask. It suspends into booking verification and resumes; the model re-issues it in the same turn and the run lands two emails_sent rows for a single request. Same idempotency shape cancel_room_booking already uses.
  • The airline booking reference is read back labeled. Two references come out of one call with no way for the caller to tell which is which; the stored one now drops the caller's spacing and case so it matches what the carrier holds.
  • The flight's scheduled departure is asked for. fix(hotel_receptionist): hotel_db stored-state and read-back fixesΒ #6805 made book_airport_car compute the pickup margin from a departure on file, and this is the only call that captures one β€” an optional argument the model may omit leaves the car booking with nothing to check.
  • A confirmed preference becomes an actual record. "I'll note that for you" left no row anywhere; a followup is the only thing that carries a preference to the next stay, and it has to run before a booking flow takes over the tool set.

Three things from #6567 deliberately not carried:

Testing

  • .venv/bin/python -m pytest tests/test_tools.py --unit (114 passed)
  • All 33 mixin tool schemas build: departure_time is required-nullable, DeliveryPreference resolves as a $ref enum
  • ruff format --check / ruff check clean over examples/hotel_receptionist/

Follow-up

benchmark.py still lists delivery_instructions under "free text written by the agent" and drops it from expected-state grading. It's a closed enum now, so it's gradable β€” left alone here because #6567 didn't touch it either.

u9g added 10 commits August 11, 2026 16:55
…name

A followup is a note for a human about a person; a name derived from the
room number identifies the room instead.
… list

amend_florist_order puts the note where the florist reads it; record_followup
routes it to nobody.
The tool tells the agent to record first and answer after; nothing brought it
back to the deferred questions, so they died with the call.
Both are in the catalog the agent already looked up, and the caller agreeing to
a booking they haven't been quoted isn't agreement.
The reference is how the caller reaches the order later, so it belongs in the
confirmation the way every other booking's does.
… set

Free text let the agent write the destination back as a handling note, or
promise a delivery time the florist never guaranteed. The read-back has to
speak the chosen value, not its stored form.
resend_confirmation suspends into booking verification and resumes; the model
re-issues it in the same turn and the run gets two emails_sent rows for a single
caller request.
…eled

Two references come out of one call and the caller has no way to tell which is
which; the stored one drops the caller's spacing and case so it matches what the
carrier holds.
The pickup margin book_airport_car computes needs a departure on file, and this
is the only call that captures one - an optional argument the model may omit
leaves the later car booking unable to check anything.
"I'll note that for you" left no row anywhere; a followup is the only thing that
carries a preference to the next stay, and it has to run before a booking flow
takes over the tool set.
@u9g
u9g requested a review from a team as a code owner August 11, 2026 21:03

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Open in Devin Review

import os
import sys
from datetime import date, time
from enum import StrEnum

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”΄ Example crashes on Python 3.10 because of a newer-only enum type

The example imports a string-enum type that only exists in Python 3.11+ (from enum import StrEnum at examples/hotel_receptionist/tools_services.py:7) even though the project declares support back to Python 3.10, so the whole receptionist example fails to start on 3.10.
Impact: Anyone running the example on Python 3.10 gets an immediate startup failure instead of a working agent.

Version floor vs. StrEnum availability

enum.StrEnum was added in CPython 3.11. examples/hotel_receptionist/pyproject.toml declares requires-python = ">=3.10" and livekit-agents/pyproject.toml:11 declares >=3.10,<3.15; AGENTS.md's Code Style section states "Python 3.10+ compatibility required". Importing tools_services on 3.10 raises ImportError: cannot import name 'StrEnum' from 'enum'. A portable equivalent is class DeliveryPreference(str, Enum).

Prompt for agents
tools_services.py imports enum.StrEnum, which is only available on Python 3.11+, while the hotel_receptionist example and livekit-agents both declare support for Python 3.10 (and AGENTS.md requires 3.10 compatibility). On 3.10 the module import fails outright. Replace DeliveryPreference's base with a 3.10-compatible string enum (e.g. class DeliveryPreference(str, Enum)) and verify the generated tool schema still resolves the enum values the same way.
Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Comment on lines +482 to +495
if (
ctx.userdata.caller_turns_at_last_resend >= 0
and _count_caller_turns(self.session.history)
<= ctx.userdata.caller_turns_at_last_resend
):
return (
"that document already went out moments ago - do NOT send it again. "
f"Relay the outcome to the caller: {ctx.userdata.last_resend_message}"
)
booking = await self._verified_booking(ctx)
await ctx.userdata.db.send_email(recipient=booking.email, kind=kind)
return f"Sent to the address on file, {booking.email.strip().lower()}."
msg = f"Sent to the address on file, {booking.email.strip().lower()}."
ctx.userdata.last_resend_message = msg
ctx.userdata.caller_turns_at_last_resend = _count_caller_turns(self.session.history)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 Second requested document is never emailed when a caller asks for two at once

The re-send guard blocks any repeat send based only on whether the caller has spoken since the previous send (caller_turns_at_last_resend check at examples/hotel_receptionist/tools_services.py:482-490) without looking at which document was asked for, so a caller who asks for both documents in one breath only ever receives the first one.
Impact: A guest asking for their confirmation and folio together gets only one of them, while being told the other was sent.

Guard ignores the `kind` argument

resend_confirmation takes kind: Literal["booking_confirmation", "folio"]. The idempotency check copied from cancel_room_booking (examples/hotel_receptionist/tools_rooms.py:316-324) is parameterless there, so turn-count alone is a sufficient key. Here a single caller turn can legitimately require two distinct sends ("can you email me the confirmation and the folio?"); the second invocation is short-circuited and returns "that document already went out moments ago", referencing last_resend_message from the first document. Tracking the last-sent kind (e.g. a per-kind turn map) would fix it.

Prompt for agents
resend_confirmation's new idempotency guard keys only on the caller-turn count, but unlike cancel_room_booking the tool has a `kind` argument (booking_confirmation vs folio). When a caller asks for both documents in one turn, the second call is suppressed and the agent claims it was already sent. Consider storing the last-sent kind (or a mapping kind -> caller-turn count / message) in Userdata (examples/hotel_receptionist/common.py) and only short-circuiting when the same kind is re-requested with no caller turn since.
Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant