An AI concierge that lives in your hotel room 24/7. No cameras, no microphones — only ambient signals (door open/close, booking metadata, flight
- traffic lookups) and the guest's persona keywords. Two agents shipping today, a third pluggable.
- Trip planner — a custom-trained personality-aligned neural network compresses the guest's 15-keyword profile into a 384-D vector, KNN's them against 61,824 synthetic families, and aggregates which activity each similar family chose at this slot. Probabilistic itinerary planner with per-slot bands ("45% of similar families chose Stanford"), bootstrap CIs, and Claude Haiku 4.5-narrated reasoning per pick. Jet-lag aware via the Forger–Jewett–Kronauer 1999 circadian limit-cycle oscillator — the model down-weights high-energy activities at slots that fall on the guest's body-clock night.
- Staff assistant — ambient room-event ingester. Door open/close + itinerary + AviationStack flight data + Google Maps traffic → Haiku-narrated action cards for housekeeping, room service, and arrival prep. "Quick turn now, 24 min before return", "Start sous-vide 12:51 to hit door hot at 1:22", "Welcome ready 2:20 PM, UA742 delayed 35 min."
Both share a single 15-keyword family encoder + a sentence-transformers activity bank, so a third agent (restaurant picker, concierge chat) plugs in by swapping the bank.
Benney also has a voice. The mic button on the prism wires browser STT →
/voice endpoint → Claude Haiku 4.5 (with Rosewood Sand Hill property
context + the guest's persona + current itinerary state) → ElevenLabs TTS.
The cat rig has 35+ animation states; emotion tags returned by Claude
(<emotion>excited</emotion>) drive the visual reaction live as Benney
speaks. Without API keys the endpoint falls back to a silent stub so the
demo still works.
Voice flow:
mic press → SpeechRecognition (browser, free) → POST /voice
→ Claude reply with <emotion>tag</emotion>
→ ElevenLabs TTS (base64 mp3)
→ cat shifts to listening/thinking/speaking/happy as states unfold
Recommendations come from the behaviour of similar guests, not from an LLM
guessing what's nice. Each synthetic family in the cohort has its own
30-slot itinerary, generated by a scheduler that respects open hours,
slot-of-day, geographic continuity, and a graduated repetition penalty. When
a new guest queries /next-slot, the aggregator finds their nearest 1,000
synthetic neighbours, filters those down by Jaccard trajectory match to
their picks so far, and tallies what that subpopulation chose. The
recommendation is a probability distribution with confidence intervals, not
a single answer.
In production we'd swap the synthetic cohort for real prior-guest data and the recommendations become continuously more accurate.
Benney the cat's name + design come from Rosewood Amsterdam, where the real Benney is the property's mascot.
hotel_agents/
├── shared/ — Family schema, FamilyEncoder, embedding cache
├── trip_planner/ — FitScorer, scheduler, PopulationAggregator, /next-slot
├── staff_assistant/ — state machine, predictors, AviationStack + Google Maps, /staff-feed
├── scripts/ — augment_cohort.py, merge_*.py, train_fit_scorer.py
├── checkpoints/ — family_encoder.pt, fit_scorer.pt, activity_bank.pt
└── data/ — activities_bay.json (190), families.jsonl (384 cleaned)
frontend/ — drop-in React components for the prism UI
├── TripPlannerLive.tsx + .css (?trip=1)
└── StaffBoardLive.tsx + .css (?staff=1)
# 1. Python env
pip install -r hotel_agents/requirements.txt
# 2. Build the activity bank (sentence-transformers embeddings → checkpoints/activity_bank.pt)
python -m hotel_agents.scripts.build_activity_bank
# 3. Generate the cohort (61k families × 30 slots, ~30 min CPU / ~10 min on a Blackwell)
python -m hotel_agents.scripts.augment_cohort --keep-seeds
# 4. Run the servers
uvicorn hotel_agents.trip_planner.server:app --port 7878 # trip planner
uvicorn hotel_agents.staff_assistant.server:app --port 7879 # staff assistant
# 5. Seed the staff demo
python -m hotel_agents.staff_assistant.seed_demoDrop frontend/*.tsx + .css into a Vite/React app's src/sides/. Route
?trip=1 → TripPlannerLive, ?staff=1 → StaffBoardLive. Both poll the
servers above; configure VITE_BENNEY_API and VITE_STAFF_API if not on
localhost.
Guest 15-kw profile ─┐
├─▶ FamilyEncoder ─▶ family_vec (384-D)
│
Activity description ─┘─▶ Sentence-Transformers ─▶ activity_vec (384-D)
family_vec + activity_vec + slot_idx + history_vec
│
▼
FitScorer (ResNet MLP, 91% val accuracy on Sonnet-cleaned data)
│
▼
Schedule(family) ─────────────► 30-slot itinerary ─┐
│
Augment(seed × 160) × Schedule ─► 61k cohort npz ◄───┘
│
▼
PopulationAggregator
│
▼
GET /next-slot ─► probabilities
POST /reasoning ─► Haiku narration
For the staff assistant:
door_open / door_close / food_order / flight_update
│
▼
RoomState (in_room, slot_idx, pending_order, arrival_flight)
│
▼
Predictor (heuristic, no ML) ─► Prediction
(return_time, cleaning_window,
food_cook_start, arrival_setup)
│
▼
Narrator (Haiku 4.5, cached) ─► ActionCard
- Closed-vocabulary 15-kw family profile so the encoder learns clean decision boundaries — no free-text drift.
- KNN + Jaccard trajectory filter so the per-slot probabilities reflect families whose first N slots match the current guest's, not the whole cohort.
- Bootstrap CIs + vs-baseline annotation so the UI can render "popular / standard / niche / buried" bands and stop pretending every recommendation is equally confident.
- Hard slot-open mask + tag boost + distance penalty + graduated repetition penalty at schedule time, so the model doesn't put museums at breakfast or send the guest from SF to Napa for lunch then back to SF for dinner.
MIT.