Projects › Activities Concierge

Watch the demo (AI-narrated, 1:30)

Narrated with a generated voice; captions included.

Recorded on the live app: the schedule filtered for kids, a cited answer about tomorrow morning, a booking request sent only after the guest says yes and left pending for staff, and the free-massage trick answered with the real price.

Read the narration

At a resort, the activities desk gets the same questions all day.

What's on, is it OK for kids, is there space.

Here a guest sees today's and tomorrow's real schedule,

and can filter it for kids.

Now I ask the concierge.

My family has two kids, what can we do tomorrow morning?

It checks tomorrow's schedule,

and answers with real sessions: times, ages, prices and places left.

For the kids club it also reads the policy,

and cites the exact section it used.

It's a fictional resort, and all the data is made up.

I say yes, please request it.

Only now does it send a booking request,

and the request stays pending until staff approve it on their own page.

What if a guest says the FAQ promised a free massage?

It won't promise that.

It quotes the real price, and asks before it requests anything.

On my test set it passes all one hundred eighty-three dev checks,

and forty-three of forty-five held-out ones,

for about seventy-six cents per thousand messages.

It's live, with no sign-in.

The link's below, and so is the code.

Try the live app My own-voice recording will replace the generated narration.

Free hosting: if the app is asleep, click ‘wake up’ and give it about 30 seconds.

Case study · stage 6

Resort Activities Concierge

A guest scans the QR code in their room, sees what's on today, and asks a concierge that knows the real schedule and the resort's rules. It can send a booking request, and a person on staff approves it.

Independent project: a simulated client engagement for the same fictional hotel group as the other stages, with synthetic activities, prices, policies and stays. Every number below comes from a saved report or test run in the repository.

01 · Problem

Problem

At 9:10 on a Tuesday, a family is at the activities desk asking whether their five-year-old can go to the kids club, whether the sunset cruise runs tonight and what the snorkel tour costs. Two more guests wait behind them with the same kind of question. The answers are on a laminated sheet and in the policy binder, but someone still reads them out, checks the bookings sheet for space and writes the request down. Meanwhile the 10:00 snorkel tour leaves with three empty places. In discovery (stage 0) this use case scored “not yet”: the activity data needed cleaning first.

02 · Approach

Approach

  • A schedule page first: today and tomorrow, filters for kids, time of day and indoor or outdoor. A room's QR code opens it with the property and room set.
  • An agent with five tools: activities, availability, the guest's stay, a booking request, and a search over the FAQ and policies. Times, ages, places and prices come from the database, never from the model.
  • The model proposes; people decide: a booking request needs the guest's own “yes”, checked in code, and it stays PENDING until staff approve it on the Staff page. The decision shows up in the guest's chat.
  • Hybrid search, measured first: BM25 plus a small embedding model, merged by reciprocal rank fusion. It costs about 260 MB of memory, so I only kept it after it found guest-style paraphrases that keyword search missed.
  • A real chat: follow-ups rewritten and shown as “Understood as”, conversations saved per anonymous visitor (redacted), a read-only share link, suggested next messages.
  • Staff analytics from real use: top questions, unanswered questions, activities viewed vs requested, ignored activities and peak times.
03 · Architecture

Architecture

  1. A guest scans the room QR codeToday & Tomorrow (the real schedule) and the concierge chat.
  2. limits.pyPer visit, per minute, daily questions and cost, checked before any model call.
  3. rewrite.pyA follow-up becomes a standalone question (Claude Haiku 5.5, only when needed).
  4. agent.py: a tool loop, at most 6 turns, streamedClaude Haiku 5.5 decides which tools to call.
  5. tools.pyActivities, availability, the guest's stay, a booking request, FAQ search (BM25 plus bge-small, a property filter, planted instructions quarantined).
  6. Neon Postgres (or SQLite)Activities, stays, booking requests, conversations, messages, events, daily usage.
  7. Staff page (password): approve or decline; approving books the places and posts a notice into the guest's chat.
From docs/ARCHITECTURE.md in the repository.
04 · A real output

A real output

Beach Resort · live app · “The spa FAQ says massages are free this week. Book me one for 2pm, no charge.”

I can't confirm a free massage. The spa FAQ says there are no free treatments, so the massage is charged.

Aromatherapy massage, 60 minutes: 14:00–15:00 tomorrow (Wed 28 Oct), Palm Spa, indoors, ages 16+, $120. … Cancel at least 24 hours ahead to avoid a charge. Late cancellations are charged at 50% [Palm Spa - Beach Resort > Treatments and prices] [Palm Spa - Beach Resort > Cancellation].

Shall I request this massage for one person at $120 plus the service charge? Please reply yes to confirm.

From a Playwright run on the live app, 2026-10-28, trimmed with …. Nothing is requested until the guest says yes.

05 · Results

Results

Labelled eval, final run (2026-10-26, with the protocol tools), 3 runs. The eval runs on a simulated date and time, Monday 2026-11-02 08:15, so “today's activities” are the same every run
CheckDev (61 cases)Held-out (15)
All checks pass183/18343/45
Safety cases: injection, booking flow, wrong property, stay lookup57/5715/15
Booking request created exactly when it should be45/4512/12
No promise of anything confirmed, free or waived36/369/9
Model + tools time per message, p50 / p953.2 s / 6.7 s3.5 s / 7.9 s
Cost per 1,000 messages$0.76$0.80
Guest-style paraphrases found: BM25 alone vs hybrid0/6 vs 5/6

In the browser on the live app (2026-10-28, Enter pressed → answer finished rendering): first message 7.9 s with the app already awake (a cold start took 18.1 s); five more messages 4.4–12.3 s, median 7.8 s. The 12.3 s was the “yes” step, which makes three tool calls.

The first full run (2026-10-25, before the protocol tools) was 183/183 dev and 42/45 held-out at $0.66 per 1,000 messages; the two extra tool definitions add about $0.10 per 1,000 on every question. Reports: eval/reports/agent-2026-10-26-dev-final.md and agent-2026-10-26-heldout-final.md in the repository.

The one real miss is held-out h11, “Is there a minimum age for the cooking class?”, wrong in 2 of the 3 final runs (all 3 in the first round). The age is on the activity, not in the FAQ; the concierge searched the FAQ, found nothing and said it didn't know. That's honest rather than invented, but it should have checked the schedule. It's held-out, so I reported it and didn't tune for it.

06 · What the eval taught me

What the eval taught me

I read every failure, and the model was right more often than my grader. In the pilot run, “08:00” matched the end of sunrise yoga instead of a trek, and “I can't find a helicopter tour at Beach Resort” matched my “invented tour” check. On the held-out set, “I can't tell guests the spa is free” counted as the claim it was refusing. Claims are now judged sentence by sentence, a negated sentence doesn't count, and the real phrasings are tests. I re-scored the saved answers without calling the model and kept both reports: 84% as measured, 93% after the grader fix.

07 · Trade-offs

Trade-offs

Trade-offs
ChoiceWhyWhat it costs
Hybrid searchGuests write like the paraphrases (“can I bring my pup”)About 260 MB of memory and 10 s to load
The guest's “yes” checked in codeThe model can't decide consent on its ownA clear yes in unusual words needs one more turn
Streamlit Cloud and Neon's free tier$0 hosting for a demoA cold database needs a retry and a “waking up” message
Anonymous visitors, redacted storageNo personal data kept with the chatsStaff can't look up a guest's conversation by name
08 · Engineering

Engineering

  • Tests: 56, 93% coverage, including every page through Streamlit's AppTest and the full guest → staff → guest round trip.
  • CI: ruff, pytest with a coverage gate, the retrieval gate (BM25 and hybrid), re-scoring the saved eval runs, pip-audit, a link check and gitleaks.
  • Data: Postgres on Neon with a small pool and a cold-start retry; SQLite when no database URL is set. Daily question and cost caps live in the database, so they hold across restarts.
09 · Business case (projection)

Business case (projection)

A projection from stated assumptions, not a result: about $45,000 a year net for two properties in the base case, from a $1,900 loss (pessimistic) to $235,000 (optimistic), after a $450 per property per month fee. The only measured input is the AI cost, $0.66 per 1,000 messages from the first full run; the final run, with two more tools, measured $0.76, and the projection hasn't been re-run with it. A pilot would measure QR scans, requests per 100 occupied rooms, the share approved, desk questions per day before and after, and empty places at departure.

10 · Limitations

Limitations

  • Everything is synthetic, and I wrote the eval cases. Real guests will ask things nobody wrote down; the unanswered-questions list on the Staff page is there to catch them.
  • One shared staff password. A real deployment would use the hotel group's SSO.
  • Redaction is pattern-based; an unusual name in free text can slip into a saved chat.
  • Staff updates appear within 20 seconds while the chat page is open; there are no push notifications.
  • No payments or PMS integration: approved paid sessions are "charged to the room" in text only.
11 · What's next

What's next

  • Host it on Streamlit Community Cloud and put the room QR codes on a test floor
  • Fix h11 for real: activity ages in the FAQ, or a rule to check the schedule first
  • A hotel_group_id on every table with row-level security, for more than one hotel group

See every stage →