Booking Agent Demo

A bilingual (English / 中文) restaurant booking agent that never books until the guest says yes, and never fills in a value the guest didn't give.

Kaihuan HuangBased on Nalu, the guest assistant I rebuilt at IPOTIndependent reimplementation · all data is synthetic

Try it

Rules, not a model, read the date, time, party size and name. Anything unclear becomes a question. The only action, create_reservation, runs after an explicit yes.

What the rules found

Actions taken

None yet.

How Nalu is built

Nalu is a restaurant group's guest assistant for questions and table bookings in English and Chinese, now in pre-launch testing. The booking path makes no model call at all; the model only handles what's left, and it can never write a booking field.

BOOKING PATH · NO MODEL CALL Guest messageEnglish or 中文 Rules + validatorsdate, time, party,name, phone, email Draftpure state machine Slot checktime is listedand not full Summarybare, explicit yes"ok" / "好" asks again Createpending, with anidempotency key EVERYTHING ELSE FAQ answersowner-provided fixed text Local LLM (qwen2.5:7b)classifies and answers; sees onlythe last 30 minutes of chat Fixed templatesstatus, phone, handoff:nothing it can't do Event logone row per turn,field names, no values
Answers are grounded read-only on the POS API; bookings go through the Reservation API. The reply says "confirmed" only when the Reservation API returns confirmed; otherwise it says the request is pending.

No model in the booking path

Because
A pilot with the model reading bookings got 17 of 30 messages fully right (57%) and invented values: lisa@example.com became the name "Lisa", "what time do you close?" became the time "close", "I want to book…" became the name "I".
Bonus
The model runs on a local GPU that serves one request at a time (about 3 s each), so every call skipped saves 1–3 s per turn. The cheapest call is the one you don't make.

Own date and time resolvers

Because
A standard date-parsing library returned nothing for 4 of 12 test date phrases and for every time phrase. Dates resolve in the restaurant's time zone, not the server's.

Only a bare, explicit yes books

Because
"ok", "sure", "好" and "嗯" are too vague for an action that holds a table. A negative word inside a question ("wait, what time do you close?") is not a cancel either.

Never claim what isn't true

Because
A real browser session showed the model promising "I will send a confirmation email" three hours after the booking draft had expired. The fix: the model sees only the last 30 minutes, status and contact replies became fixed templates, and "confirmed" is said only when the Reservation API says so.

Behind a flag, default off

Because
The old flow had over a hundred tests and a safety fix depending on it. The new flow shipped beside it; turning the flag off rolls back in about a minute.

Keep personal data short-lived

Because
Booking drafts hold phone numbers and emails, so expired drafts are purged on every save, and logs record which fields were found, never their values.

Measured on Nalu

Model-only pilot (qwen2.5:7b)17 of 30 messages fully correct (57%); 3 invented values
Rules + confirmation gate, untuned held-out set33 of 39 fully correct (85%), 0 wrong or invented values; all 6 misses were omissions, so the guest was simply asked again
Development set35 of 35, gated in CI on every change to the agent. It was tuned against, so it is a regression suite, not an accuracy claim
Live replay23 conversations × 3 runs, 114 turns, 0 failures; p95 1.27 s on a later 38-turn replay
Local GPUNVIDIA DGX Spark (GB10): about 47 tokens/s warm, about 10 s cold start, concurrent requests served one after another. Hence at most one model call per turn, none on yes / no
Honest limit54 non-booking conversations with zero wrong bookings only bound the rate below about 5.5% (rule of three). A safety claim needs 100+ adversarial trials

Compare with an LLM on your machine

The first version let a language model read the booking details. Measured, it filled in values the guest never gave, so the model came out of the booking path. If you run Ollama, this page can ask your local model to read the same conversation and show where it would have guessed. Nothing leaves your computer.

What is inside that model: architecture card for Qwen2.5-7B-Instruct. 7.62B parameters over 28 layers, grouped-query attention (28 heads, 4 KV heads), 14.2 GiB of weights in bf16 and 56 KiB of KV cache per token, so 32K tokens of context cost 1.75 GiB and the GPU budget goes to weights, not conversation history. Generated from the config alone, no weights downloaded.

# once, then restart Ollama
ollama pull qwen2.5:7b
OLLAMA_ORIGINS="https://kaihuan-huang.github.io" ollama serve

Chrome may ask to let this page reach your local network; allow it. The model reads only your messages in the chat above.

Sample run on this page's test conversations

qwen2.5:7b on Ollama, temperature 0, 20 conversations, 25 Sep 2026. In 2 it chose AM or PM on its own when the guest hadn't said; in 1 it put "下周五" (next Friday) on a Tuesday. The rules asked instead, or got the date right.

Tests that gate every change

Every bug fix starts as a failing test. These synthetic conversations run in your browser against the code on this page, with a fixed clock (Fri 25 Sep 2026, 10:00). Each one also checks that no reply promises an email, text or call.