Skip to content

fix(hotel_receptionist): hotel_db stored-state and read-back fixes - #6805

Open
u9g wants to merge 9 commits into
mainfrom
feat/hotel-db-split
Open

fix(hotel_receptionist): hotel_db stored-state and read-back fixes#6805
u9g wants to merge 9 commits into
mainfrom
feat/hotel-db-split

Conversation

@u9g

@u9g u9g commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

The examples/hotel_receptionist/hotel_db.py changes from #6567, brought in as independent semantic commits so each one stands or falls on its own. Oldest first:

  1. Pin the sim clock under lk agent simulate β€” scenarios.yaml hardcodes every date literal against HOTEL_TODAY=2026-06-08, but nothing set that env var, so a plain lk agent simulate ran the agent on the real clock and all 14 expected-state scenarios failed on date diffs.
  2. Store generated codes in the uppercase spoken form β€” shortuuid() returns a lowercase suffix while _speak_code spells every reference out in uppercase, so a by-code lookup against the stored value never matched the form the caller heard and passed back.
  3. Store caller-given times as naive wall clock β€” an LLM-supplied time can arrive tz-aware, and isoformat() then wrote an offset into columns that otherwise hold naive wall clock, breaking both expected-state comparison and time.fromisoformat round-trips.
  4. Expose a room's view β€” a booking read-back listed the room type but not its view, so a guest who had just been moved to a garden-view room heard their booking described exactly as it was before the move. _require_room splits into _normalize_room and _room_exists.
  5. Bind each room type's views to its own line β€” availability rendered as one pipe-delimited line smeared the type β†’ view pairs together in context, and the model offered a garden-view double queen, which has never existed.
  6. Speak the penthouse as a suite, not as "room PH" β€” the penthouse's room id is RM_PH, so every service tool that echoed the room back read out a room number no guest has ever been given.
  7. Capture flight departure time and compute the pickup margin β€” without a stored departure time the airport-car pickup margin could only ever be a prose instruction the model routinely skipped. It is now computed from the reconfirmed flight and handed to the agent to say back, including tight and not-before-departure warnings.
  8. Structured florist destination and order amendments β€” deliver_to was one free-text column holding whichever idea of the destination the model formed, so a room could not be validated and nothing distinguished a room from a person. It splits into room_id (FK, checked to exist) and recipient_name with a CHECK that exactly one is set, plus delivery_instructions and amend_florist_order so a handling request made after the order lands on the order the florist reads.
  9. Typed Room union replaces the room string β€” every room-taking tool took free text, so the model could pass a phrase naming no room at all and the string reached the database before anything rejected it. Room is a discriminated union of NumberedRoom and PenthouseSuite, which makes a fabricated destination inexpressible at the schema boundary and gives the penthouse one canonical spelling. room_to_id() converts at the tool edge, so the tools hand the db a canonical id and speak_room() no longer normalizes what it renders.

Two things beyond hotel_db.py are mine rather than #6567's, both forced by the changes above:

  • benchmark.py denies delivery_instructions from expected-state comparison. It is agent-written free text, and _select_sql compares every non-denied column, so a captured handling note would fail the diff against the seeded empty default.
  • The florist scenario moved off room 412, which is not in seed.py. Free-text deliver_to tolerated an unseeded room; the new FK and existence check do not.

order_flowers uses _room_exists() from commit 4 where #6567 inlines the same query β€” that helper did not exist yet at that point in its history. Semantically identical, so expect a one-hunk conflict there and nowhere else in hotel_db.py.

Testing

  • pytest --unit β€” 1942 passed, 5 skipped
  • ruff format --check and ruff check clean at every commit individually, and each commit imports on its own
  • Both tool-schema builders (legacy and strict) produce the discriminated union for all six converted tools; "the front desk", {"type": "suite"}, and a numberless room are all rejected at parse
  • Destinations round-trip end to end against a seeded db: RM_304 and RM_PH store and read back for DND, wake-up calls, dispatch, and florist orders, and an unseeded room is refused
  • mypy examples/hotel_receptionist/ reports one error, a module-path mapping issue in fake_data/seed.py, which reproduces unchanged on main. The repo's type-check gate covers livekit.agents and the plugins, not examples/.

u9g added 9 commits August 11, 2026 16:18
…nt simulate

scenarios.yaml hardcodes every date literal against HOTEL_TODAY=2026-06-08,
but nothing set that env var, so plain 'lk agent simulate' ran the agent on
the real clock and all 14 expected-state scenarios failed on date diffs.
Resolve TODAY as: HOTEL_TODAY override > SIM_TODAY when --simulation is in
argv (lk always passes it; job subprocesses inherit argv) > date.today().
…n form

shortuuid() returns a lowercase hex suffix, but _speak_code spells every
reference out in uppercase, so the caller only ever hears the uppercase form
and the model passes that form back. A by-code lookup against the lowercase
stored value never matched. Route every generated code through _new_code(),
which uppercases at the single point of creation - the seeded codes in
scenarios.yaml were already uppercase.
An LLM-supplied time can arrive tz-aware - "17:40:00Z" parses with tzinfo -
and isoformat() then wrote "17:40:00+00:00" into columns that otherwise hold
naive wall clock, so the row no longer compared equal to a scenario's
expected state and time.fromisoformat round-trips diverged. Route every
stored time through wall_clock_iso(), which drops the offset at the write.
A booking read-back listed the room type but not its view, so a guest who had
just been moved to a garden-view room heard their booking described exactly as
it was before the move. Add HotelDB.room_view() and put the view in the facts
the modify flow reads back. _require_room splits into _normalize_room (spoken
number -> id) and _room_exists so a caller that wants one without the other
isn't forced through the raising path.
Availability was rendered as one pipe-delimited line, so the type -> view
pairs smeared together in context: the model bound a neighboring type's view
to whatever type the caller picked and offered a garden-view double queen,
which has never existed. describe_room_options() puts one type per row and
states whether the view is a choice to ask about or a fact to assert, so a
single-view type stops being offered as a false choice. Both renderers -
the booking flow and read-only browsing - share it, since a smeared pair
offered while browsing survives into the booking flow.
… PH"

The penthouse's room id is RM_PH, so every service tool that echoed the room
back - wake-up calls, emergency dispatch, Do-Not-Disturb, and the not-found
errors - read it out as "room PH", a room number no guest has ever been
given. speak_room() renders a room from either its id or the spoken number,
and names the penthouse as the suite it is.
…he pickup margin

A concierge reconfirming a flight naturally captures when it departs; without
it the airport-car pickup margin could only ever be a prose instruction the
model routinely skipped. flight_reconfirmations gains a nullable
departure_time, request_flight_reconfirmation captures it, and
book_airport_car looks it up for the pickup's date and hands the agent the
computed margin to say back - including TIGHT and not-before-departure
warnings. The flight scenario's expected state now includes 17:40:00.
…endments

florist_orders.deliver_to was one free-text column holding whichever idea of
the destination the model formed - a bare room number, the prose "Penthouse
Suite" for RM_PH, or an arriving guest's name - so a room could not be
validated, and nothing distinguished a room from a person. Split it into
room_id (FK, checked to exist) and recipient_name, with a CHECK that exactly
one is set; a room wins when both arrive. Add delivery_instructions plus
amend_florist_order, so a handling request made after the order lands on the
order the florist reads rather than in a followup nobody routes. The florist
scenarios now seed a real room (RM_304, RM_PH) instead of the unseeded 412.
Every room-taking tool took a free-text room, so the model could pass a
phrase that names no room at all - "the front desk", a stand-in word, the
penthouse spelled a dozen ways - and the string reached the database before
anything rejected it. Room is a discriminated union of NumberedRoom and
PenthouseSuite, which makes a fabricated destination inexpressible at the
schema boundary and gives the penthouse one canonical spelling. room_to_id()
converts to the stored id at the tool edge, so the tools now hand the db a
canonical id and speak_room() no longer has to normalize what it renders.
@u9g
u9g requested a review from a team as a code owner August 11, 2026 20:43

@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.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines +424 to +427
try:
await ctx.userdata.db.amend_florist_order(
code=order_code, delivery_instructions=delivery_instruction
)

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.

🟑 A follow-up delivery note for a flower order can be rejected as an unknown order

The reference the caller reads back is looked up without being cleaned up first (amend_florist_order at examples/hotel_receptionist/tools_services.py:425-427), so a reference heard with spaces or in lower case is treated as unknown and the note never reaches the order.
Impact: The caller is told their flower order doesn't exist and the delivery instruction is silently lost.

Case/whitespace normalization missing versus every other code-taking tool

Generated codes are now stored uppercase (_new_code at examples/hotel_receptionist/hotel_db.py:85-89), and HotelDB.amend_florist_order (examples/hotel_receptionist/hotel_db.py:1325-1331) matches code exactly with no UPPER() on either side. Every other tool that accepts a spoken-back reference normalizes it first β€” verify_booking.py:63 and tools_restaurant.py:78,104,140 all do confirmation_code.replace(" ", "").upper(), and find_booking/find_restaurant_reservation additionally .upper() the parameter (hotel_db.py:639,654). The new tool does neither, so a transcription like "flr-ab12" or "FLR AB12" yields changed == 0 β†’ NotFound β†’ ToolError.

Suggested change
try:
await ctx.userdata.db.amend_florist_order(
code=order_code, delivery_instructions=delivery_instruction
)
try:
await ctx.userdata.db.amend_florist_order(
code=order_code.replace(" ", "").upper(), delivery_instructions=delivery_instruction
)
Open in Devin Review

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

Comment on lines +224 to +232
return "\n".join(
f"- {a.type.replace('_', ' ')}: {speak_usd(a.nightly_rate)}/night, "
+ (
f"{' or '.join(a.views)} view - ask which of those two they want"
if len(a.views) > 1
else f"{a.views[0]} view only - say so as a fact, there is no view to ask about"
)
for a in avail
)

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.

🟑 Callers can be told a room type only comes with one view when other views are merely booked up

The list of views that happen to be free for the requested dates is described to the agent as the complete set of views a room type has (describe_room_options at examples/hotel_receptionist/hotel_db.py:224-232), so the agent states an availability accident as a permanent fact about the hotel.
Impact: Guests are told, as fact, that a room type has no ocean/garden view when such rooms exist and are simply taken for those dates.

Views come from the availability query, not from inventory

_SQL_AVAILABILITY (examples/hotel_receptionist/hotel_db.py:2087-2098) groups by type over only the rooms that are unbooked for the requested range and match the smoking filter, so RoomTypeAvailability.views is an availability-derived set. The new renderer prefixes it with "the views on a type's line are the only views that type has" (book_room.py:145, tools_rooms.py:174) and, in the single-view branch, instructs the model: "{view} view only - say so as a fact, there is no view to ask about". With the seed data a king is city+ocean; if the ocean kings are booked for those dates the agent will assert kings only come with a city view. Rewording to availability terms ("the only rooms free for these dates are -view") keeps the anti-smearing benefit without asserting a false inventory fact.

Prompt for agents
describe_room_options in examples/hotel_receptionist/hotel_db.py renders availability results with wording that asserts the listed views are the only views a room type has ("the views on a type's line are the only views that type has" in book_room.py set_stay and tools_rooms.check_room_availability, and "<view> view only - say so as a fact, there is no view to ask about"). The views come from _SQL_AVAILABILITY, which only includes rooms free for the requested dates and matching the smoking filter, so a view that exists in inventory but is booked out disappears from the line. The agent will then state a false fact to the caller. Reword the per-line verdict and the header so they talk about what is available for those dates rather than what the type has, while keeping the one-row-per-type binding that this change was made for.
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