← Case Studies
Applied AI

LangThang AI — Tour Advisory Chatbot

A personalised tour-advisory chatbot for the Vietnamese market that holds context across a conversation instead of treating each message as a fresh search, and hands qualified leads to human sales rather than pretending to complete a booking.

96backend tests covering unit and integration paths
2-layerNLU — rules first, LLM only when ambiguous
4-stateclosing flow from exploring to qualified lead

Context

Most retrieval-augmented chatbot demos embed the latest message, fetch the nearest documents, and stop there. That holds up for a single question and falls apart on the second one.

This project started from exactly that failure. A tester reported it plainly: they asked about Đà Lạt, and the follow-up question surfaced tours somewhere else entirely — “it feels like a Google search, not like being advised.”

Challenge

Advisory conversations are stateful. A customer names a destination once and expects it to hold while they explore duration, budget, and preferences. Semantic similarity alone does not guarantee that: an embedding of “what about something cheaper?” carries no destination at all.

The commercial side had a constraint too. The real sales process is manual — collect details, then call to confirm. So the bot could not credibly close a booking, quote a final price, or take a deposit.

Approach

Retrieval was rebuilt around conversation state rather than the latest message. Extracted slots — destination, budget, duration — are persisted per turn as JSONB, then applied as a hard SQL filter before vector search runs; embeddings are skipped entirely when the structured slots already narrow the set. Vector search is reserved for the genuinely fuzzy part: preferences and comparisons expressed in prose.

When nothing matches exactly, a deliberate relaxation ladder widens duration, then budget, then the topic-relevance threshold. Destination is never relaxed silently, and each step taken must be stated to the customer in the reply — the result set never quietly changes underneath them.

Language understanding runs in two layers. A pure-Python rule layer handles what rules do well: diacritic-insensitive matching (so “Đà Lạt”, “da lat” and “dalat” are one thing) and Vietnamese price and duration shorthand. Only the genuinely ambiguous residue reaches the LLM, which replies against a JSON schema via structured outputs — at most one model call per turn, and the rule layer stays fully unit-testable.

The closing flow is a small state machine rather than a prompt instruction: exploring, confirming, awaiting contact, closed. Buy signals are detected from keywords and intent without another model call, the assistant restates the full trip before asking anything, and it asks for exactly one contact field with a reason given. Hard guardrails at the prompt layer prevent it confirming availability, settling a final price, or accepting deposits. A completed flow writes a lead and emails the admin.

Underneath, the layering is kept strict: routers hold no business logic, repositories hold no leaked queries, and API schemas stay separate from database models — maintained through later phases as the state machine and NLU layers landed. Authentication follows a backend-for-frontend pattern, with the JWT held in an httpOnly cookie so it never reaches client-side JavaScript.

Impact

  • Conversation state (destination, budget, duration) persisted per turn in PostgreSQL JSONB
  • Hard SQL filtering on structured slots before vector search, skipping embeddings when filters suffice
  • Controlled relaxation ladder — duration, then budget, then topic threshold; destination never relaxed silently
  • Every relaxation step must be stated explicitly to the customer in the reply
  • Rule-based NLU layer is diacritic-insensitive and parses Vietnamese price and duration expressions
  • LLM layer uses structured outputs against a JSON schema, one call per turn at most
  • Four-state closing flow with hard guardrails against confirming availability, final pricing, or deposits
  • Leads persisted and emailed to an admin over SMTP, managed in an admin console

Stack

Python, FastAPI, SQLAlchemy 2.0 and Alembic on the back end; PostgreSQL with pgvector for semantic search; Next.js App Router with strict TypeScript, Tailwind CSS and shadcn/ui on the front end; OpenAI for chat, structured-output NLU and embeddings, configured through environment variables to stay provider-agnostic; Docker Compose for the full stack; pytest with 96 unit and integration tests plus Ruff, Vitest and Playwright.

Capabilities Demonstrated

  • Contextual RAG
  • pgvector semantic search
  • Conversational state machines
  • Structured LLM outputs
  • Layered backend architecture
  • BFF authentication
Product screens
LangThang AI homepage introducing the tour advisory chatbot
Homepage — the product is framed as advice, not search.
Chat consultation holding context across turns and restating trip details before asking for contact information
Consultation — the assistant restates the trip and the reference price, is explicit that availability and final price are not confirmed, and asks for a single contact field.
Admin leads table listing captured leads with tour, destination, contact and status
Admin console — captured leads with tour, destination, contact, and follow-up status for the sales team.
Mobile view of the chat at the booking confirmation step
Mobile — the same closing flow at the confirmation step.
Capabilities Demonstrated
Contextual RAGpgvector semantic searchConversational state machinesStructured LLM outputsLayered backend architectureBFF authentication