Skip to content

24 June 2026 · 3 min read

A hotel sales agent that can't make up prices

Designing a WhatsApp sales agent for hotels where the model understands and phrases, while code owns prices, dates and leads, and evals gate every change.

I built a conversational sales agent for hotels: guests message on WhatsApp, ask about rooms, food, directions and prices, and the agent answers, recommends and captures booking enquiries. The interesting problem isn't getting a language model to sound friendly. It's making sure it never invents a price, a date or a policy, and never drops a lead.

The rule: the model talks, code commits

Anything a guest might act on (prices, availability, dates, reference numbers, policies) comes from deterministic code, not from the model's memory or its paraphrase. The model's job is understanding and phrasing. That one rule shaped the whole design.

Three stages instead of one big prompt

text
guest message ─► decoder ─► router ─► workflow ─► composer ─► reply
                (structured)  (code)   (tools/RAG)  (grounded)

1. Decoder: understand, as data. One model call with structured output turns a free-form message into fields the rest of the system can trust:

ts
interface Decoded {
  category: 'hotel' | 'booking' | 'location' | 'knowledge' | 'off_topic' | 'unsafe' | 'social' | 'support';
  salesAction: 'answer' | 'collect_booking_details' | 'recommend_room' | 'bridge_to_booking' | 'clarify' | 'handoff';
  slots: { checkIn?: string; checkOut?: string; guests?: number; roomType?: string };
  sentiment: 'positive' | 'neutral' | 'confused' | 'frustrated';
}

2. Router: decide, in code. A plain switch on the decoded intent picks the workflow. No model decides which code path runs, so the paths are testable and predictable.

3. Workflows: facts from tools. Availability and rates come from the booking providers' APIs; relative dates ("this Friday") are resolved by code against the hotel's timezone; prices are formatted inside the tool output, so the model receives "₹6,500 per night" as a finished string it has no reason to rewrite.

4. Composer: phrase, grounded. The final reply is written from the workflow's facts, in the hotel's voice and the guest's language.

Knowledge without hallucinations

Each hotel has a knowledge base (amenities, policies, food, the area). Retrieval is hybrid: vector search (pgvector) plus keyword search, merged with reciprocal rank fusion, which handles both "is there a pool?" and exact phrases like "early check-in". Then:

  • Ground, verify, escalate. The answer is generated only from retrieved passages, checked against them, and if the knowledge base doesn't cover the question, the agent says so and notifies staff instead of guessing.
  • Gaps become content. Unanswered questions are logged, so the hotel knows exactly what to add to its knowledge base.
  • Answer cache. Exact and semantically similar questions reuse verified answers, which cuts latency and cost for the questions every guest asks.

Guardrails as code

Input and output checks run around the model: off-topic and unsafe requests are refused with a bridge back to the stay; privacy rules stop the agent from revealing other guests' data; replies are checked before they're sent. Some policies can't live in a prompt; they're enforced deterministically.

Never drop a lead

Every conversation persists its state (extracted facts, booking details collected so far), so a guest can leave mid-enquiry and pick up later. When details are complete, the enquiry is captured and staff are notified, whether or not the guest replies again. Nudges follow up on enquiries that stall.

Changes ship only if the evals say so

Prompts and models change behaviour in surprising ways, so nothing ships on vibes:

  • Routing benchmark. Labelled messages measure intent accuracy, slot extraction and latency per model.
  • Model-as-judge. Full conversations are scored A/B, old against new.
  • Transcript tests. Scripted multi-turn conversations run end to end.

A change merges only if it matches or beats the current pipeline. That's also how we plan bigger moves, like adopting LangGraph's agent runtime (tool loop, middleware for retries and call limits, a Postgres checkpointer for resumable conversations): behind a feature flag, one phase at a time, each gated by the same evals.

What I'd tell anyone building one

  • Decide what the model is not allowed to do before deciding what it should do.
  • Make the model's output data (structured), and let code act on it.
  • Put facts in tools, formatted, so there's nothing to paraphrase.
  • Build the evals before the second prompt change, not after the tenth.