{"slug": "lux-stay-agent-a-hotel-travel-agent-that-only-works-because-its-content-is", "title": "Lux Stay Agent — a hotel travel agent that only works because its content is structured", "summary": "A hotel operator next to Luxembourg's central station built Lux Stay Agent, a multilingual travel assistant that answers traveler questions in French, English, and German by querying a Sanity Knowledge Base of the hotel's real operational content rather than relying on an LLM's memory. The agent uses a Python function-calling loop over a Sanity Context MCP endpoint with tools for schema exploration and GROQ queries, drawing on 78 production documents covering room prices, a 54-dish menu, transport guides, and FAQs. The developer argues the system would be useless without structured content, since exact figures like daily-synced room rates and the free bus 16 airport route cannot be approximated.", "body_md": "I run a real 3-star hotel next to Luxembourg's central station. For the Sanity Challenge, instead of building a demo on fake data, I pointed a production AI agent at a Sanity Knowledge Base filled with the hotel's **actual operational content** — live room prices, the real 54-dish snack menu, transport facts, multilingual FAQs. The result: **Lux Stay Agent**, a travel assistant that answers travelers' questions (FR/EN/DE) with exact figures it cannot afford to get wrong, and that would be completely useless without structured content.\n\n**Live demo (real recorded sessions):** [https://yasha.phoenix--ai.com/agent/](https://yasha.phoenix--ai.com/agent/)\n\n**Sanity project ID:** `vxozr96i` (dataset `production`)\n\n\"How much is a double room?\" sounds trivial until you realize the price changes daily (our rates sync with Booking.com every night). \"How do I get from the airport?\" has one correct answer (bus 16, free — Luxembourg made all public transport free in 2020). \"What's on the menu?\" is 54 items with individual prices living in the hotel's production ordering system.\n\nA keyword search over a website gets you approximate, stale, or wrong answers. An LLM without grounding gets you confident nonsense. What works is an agent that *queries* structured content — exact fields, exact numbers — through a scoped, read-only window.\n\nI modeled the hotel's world as six Sanity document types:\n\n| Type | Content | Why it must be structured | \n|---|---|---|\n| `hotel` | Address, phone, check-in/out times, distances (100 m to station, 400 m to center) | Exact facts, zero tolerance for approximation | \n| `room` | 3 types, capacity, bed, size, **basePriceEur** + price-sync note | A price is a number field, not prose | \n| `menuItem` | 54 dishes, category, **priceEur** , availability, ordering note | Pulled from the live ordering system (RoomEats) | \n| `guide` | Airport/station/transport guides with `facts[]` arrays | Bus lines, durations, the free-transport rule | \n| `faq` | FR/EN/DE questions & answers | The agent answers in the user's language | \n| `attraction` | Sights with **walkingMinutesFromHotel** , UNESCO flags | \"What can I visit on foot?\" is a numeric query | \n\nEvery document traces back to a real system: the booking engine I built for the hotel (rates synced nightly from Booking.com), the production QR-ordering database for the menu, and verified local transport facts.\n\n```\nTraveler question (FR/EN/DE)\n        │\n        ▼\nLux Stay Agent (Python, OpenAI-compatible LLM, function calling)\n        │  1. fetches /initial-context over HTTP → schema-aware system prompt\n        ▼\nSanity Context MCP endpoint\nhttps://api.sanity.io/v2026-03-03/context/mcp/vxozr96i/production?embeddings=true\n        │  tools: schema_explorer, groq_query, array_field_reader\n        ▼\nSanity Content Lake — 78 real documents, embeddings enabled\n        (semantic search via text::semanticSimilarity)\n```\n\nThe agent loop is deliberately boring — that's the point. The intelligence lives in the content model:\n\n``` php\ndef run_agent(question):\n    tools = get_tools()  # MCP tools/list -> OpenAI function schemas\n    messages = [\n        {\"role\": \"system\", \"content\": SYSTEM.replace(\"{ctx}\", initial_context_http())},\n        {\"role\": \"user\", \"content\": question},\n    ]\n    for _ in range(MAX_ROUNDS):\n        msg = llm(messages, tools)\n        if not msg.get(\"tool_calls\"):\n            return msg[\"content\"]           # grounded final answer\n        for call in msg[\"tool_calls\"]:\n            result = mcp_tool(call[\"function\"][\"name\"],\n                              json.loads(call[\"function\"][\"arguments\"]))\n            messages.append({\"role\": \"tool\", \"tool_call_id\": call[\"id\"],\n                             \"content\": result})\n```\n\nThe system prompt is strict: exact figures only via `groq_query`, never from memory; cite the source document type; if the base doesn't know, say so and hand over to the hotel's phone/email; answer in the user's language.\n\nThree moments from real sessions:\n\n**\"Combien coûte une nuit en chambre double et à quelle distance de la gare ?\"**\n\nThe agent fires one `groq_query` on `room` (`basePriceEur: 95`) and one on `hotel` (`distanceToStationM: 100`). A keyword search would find a page mentioning \"double room\" and \"station\" — it would not reliably bind 95 € to *this* room type on *this* date.\n\n**\"Quels plats avec du kebab, et à quel prix ?\"**\n\nThe menu is 54 `menuItem` documents with `name` and `priceEur` as queryable fields. The agent answers with the exact list and prices — and surfaces the pricing contradiction honestly: each item carries the Wolt delivery price *and* the note that ordering in-room via QR is 15–25% cheaper. Both claims, with their sources, side by side — exactly what structured content enables.\n\n**\"Was kostet ein Einzelzimmer und wie komme ich vom Flughafen zum Hotel?\"**\n\nGerman question → German answer, because the FAQ documents are tagged `language: \"de\"`, while the room price still comes from the language-neutral numeric field. Translation happens at the LLM layer; facts stay exact at the content layer.\n\nBefore writing the agent, I ran a 15-assertion QA battery against the Context MCP endpoint itself: schema visibility for all 6 types, exact price queries, reference resolution (`room.hotel->name`), document counts (54 menu items), guide facts, semantic search with embeddings, and multilingual retrieval. **15/15.** Then a 5-question battery against the full agent loop (German, menu prices, UNESCO sights, English, free transport) asserting exact figures in the answers and actual tool usage. **5/5.** The demo page replays these unedited sessions.\n\n`/initial-context` over HTTP is a real optimization`embeddings=true`) but needed enabling per-dataset` sanity datasets embeddings enable`) — the error message was clear, the fix took a minute with the CLI once I had a token with the right grant (`deploy-studio` role for hosting, `developer` for embeddings).`walkingMinutesFromHotel` as a number turns \"what can I visit?\" into a sortable query. That modeling decision is the whole game.`vxozr96i`)\nIf you have content an agent can't afford to get wrong — prices, inventory, schedules, errata — structure it, scope it, and let the MCP endpoint do the rest. The agent is the easy part.", "url": "https://wpnews.pro/news/lux-stay-agent-a-hotel-travel-agent-that-only-works-because-its-content-is", "canonical_source": "https://dev.to/jeffreyturov/lux-stay-agent-a-hotel-travel-agent-that-only-works-because-its-content-is-structured-5ff6", "published_at": "2026-09-18 21:24:36+00:00", "updated_at": "2026-09-18 21:52:58.921824+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "structured-data", "ai-tools", "ai-products"], "entities": ["Sanity", "Lux Stay Agent", "Booking.com", "RoomEats", "OpenAI", "Luxembourg"], "alternates": {"html": "https://wpnews.pro/news/lux-stay-agent-a-hotel-travel-agent-that-only-works-because-its-content-is", "markdown": "https://wpnews.pro/news/lux-stay-agent-a-hotel-travel-agent-that-only-works-because-its-content-is.md", "text": "https://wpnews.pro/news/lux-stay-agent-a-hotel-travel-agent-that-only-works-because-its-content-is.txt", "jsonld": "https://wpnews.pro/news/lux-stay-agent-a-hotel-travel-agent-that-only-works-because-its-content-is.jsonld"}}