All work
AI / RAG

Digital Concierge: A Trip Planner That Actually Knows Abu Dhabi

20268 min read
Digital Concierge: A Trip Planner That Actually Knows Abu Dhabi

Ask Abu Dhabi and Digital Concierge came out of the same work at EdgeFirm for UAE's Department of Culture and Tourism, and they run on the same backend, but they're two different products. Ask Abu Dhabi is the assistant embedded in the visitabudhabi.ae website. Digital Concierge is the standalone version, a full trip planning workspace where the conversation, your itinerary and a live map all sit on screen at once. This is the one I'll walk through, because it's the more ambitious of the two on the front end.

What it actually is

Think of a travel concierge you can hold a real conversation with. You ask what to do with the kids for a week and it comes back with actual attractions pulled from the department's own data, each one with photos, ratings and opening hours, dropped as pins on a map you can pan around. Ask it to turn that into a plan and it builds a day by day itinerary you can reorder, edit, save and share. Every conversation is kept in a sidebar, so you can come back to something from last week and carry on from where you stopped.

The backend does the thinking

The whole thing runs on FastAPI with the agent built on LangGraph. Rather than hand wiring a graph of nodes, the core is a ReAct agent that reasons about what the user wants and reaches for tools when it needs them. Before a message ever hits that agent it goes through a short pipeline. One step detects the language and holds the assistant to English. The next classifies the query, whether it needs the user's location, whether it's a factual lookup, a distance question, an itinerary request or just small talk, and whether it looks suspicious. Only if it looks suspicious does a dedicated injection tester run, so the common case stays fast.

Retrieval is the part that actually matters. Content comes out of the department's documents and listings, gets chunked and embedded, and lives in Postgres with pgvector for semantic search and PostGIS for anything geographic. When someone asks what's near their hotel, the system isn't guessing, it's doing a real proximity search that widens its radius in steps until it finds enough good results, then works out actual driving distances with Google Maps. The model answers from what we gave it and is allowed to say it doesn't have something.

Keeping a public assistant honest

A bot with a government name on it gets tested constantly, so a lot of the work went into what it refuses to do. Moderation is the very first tool the agent can reach, and the responses for sensitive categories aren't improvised by the model, they're read from the database so the department controls the exact wording. The injection tester has a proper rulebook behind it: attempts to switch its persona or language, fake authority or update claims, requests to dump the system prompt, and, this being Abu Dhabi, anything that looks like trying to track a public figure. When it isn't sure, it fails closed.

One decision that paid off repeatedly: the prompts aren't in the code. System prompts, the starting suggestions and the moderation responses all live in Postgres and are editable from an admin panel, so the team can tune the assistant's behaviour without a redeploy.

The itinerary engine

Itineraries are where the product earns its name, and they're harder than they look. There are two paths. Curated itineraries are prebuilt multi day plans the agent searches for and filters by duration and group. Custom itineraries are built on the fly by a separate, tool restricted graph. When you ask for a seven day trip, the day count is derived exactly, the search radius opens up to cover the whole emirate, and the model is forced to produce exactly that many days with a sensible number of places each and titles copied word for word from the real listings so nothing drifts into fiction. Each place carries its coordinates, rating and thumbnail, the days never get shuffled around each other, and the finished plan knows its own total days and spot count. Save it and you get a shareable link, with a QR code for the ones that are public.

The map

The map is Mapbox, and it's tied to the conversation rather than bolted on beside it. Top attractions get hero markers with labels, and because labels overlap horribly at city zoom, they run through a greedy collision check that hides the ones that would collide. Everything else clusters with Supercluster, so a busy area shows a count bubble you can click to zoom into instead of a pile of overlapping pins. The map opens as a panel next to the chat with a card carousel along the bottom that stays in sync, click a marker and the right card scrolls into view, pick a card and the map flies to it. There's a filter drawer for narrowing by category, and a full screen mode when you just want the map.

The front end holding it together

The app is Next.js and React with a deliberately split state model, Redux for the UI and user state and React Query for everything that comes from the server, with the query cache persisted to IndexedDB so a reload comes back instantly and a flaky connection degrades gracefully. Streaming is done over plain fetch reading a Server Sent Events stream, and rather than dumping tokens as they arrive I run them through a small buffer that releases text at a steady reading speed, so the answer types out smoothly instead of stuttering. You can stop a response mid stream and whatever had arrived is kept. Auth is Azure B2C wired up by hand as an OIDC flow with server minted nonce and state rather than pulling in a heavy library, and sign in stays in an HttpOnly cookie that a proxy attaches to backend calls. Itinerary editing uses drag and drop to reorder activities, and sharing supports email, print and a branded QR code.

Making it fast enough

An assistant that takes half a minute to answer isn't one people use twice. A location heavy question was landing around thirty five seconds, so I sat down and traced exactly where the time went: language detection, classification, the agent and its tools, and a final distance sort. Most of it was in the agent step and, worse, in Google Maps calls being repeated across each radius expansion and then again at the end. The fixes were unglamorous and effective. Cache and reuse the distances within a request, collapse two separate pre agent model calls into one, and stop recomputing what we already had. The two products also run at wildly different scales, since the same backend feeds the public widget on the main tourism site, so it had to be built to run under real concurrency, not just a demo load.

What I took from it

This is the project where the plumbing and the product were equally demanding. The retrieval, the guardrails and the performance work are what make the AI trustworthy, but the itinerary builder, the synced map and the streaming that actually feels good are what make it something a person wants to use. Getting both halves right on the same project, for a client whose name is on every answer, is the bar I now hold my work to.

A look inside
Curated, day-by-day itineraries built from a single request, filtered by how long you're staying.
Curated, day-by-day itineraries built from a single request, filtered by how long you're staying.
Every attraction opens to its ratings, hours and a way to book, without leaving the conversation.
Every attraction opens to its ratings, hours and a way to book, without leaving the conversation.
Answers come back as attraction cards with live ratings and a one-tap jump to the map.
Answers come back as attraction cards with live ratings and a one-tap jump to the map.
Saved itineraries you can reopen, edit, reorder and share by link or QR code.
Saved itineraries you can reopen, edit, reorder and share by link or QR code.

© 2026 Bilal

All work