Building Catalogue-Grounded AI Replies for WhatsApp Commerce
The short answer: don't let the model look things up. In the WhatsApp Commerce Copilot, a deterministic pipeline searches the store's catalogue and hands the LLM at most five candidate products. The model may only interpret language and phrase a reply over that closed set — and if it returns a product ID that was not in the set, the response is thrown away and the deterministic reply is sent instead. Grounding is enforced by code, not requested in a prompt.
The problem: a fluent model is a liability in commerce
A clothing brand's customer asks whether a shirt comes in medium. If a general-purpose model answers from its own weights, it will produce something plausible: a size, a price, a delivery window. Plausible is worthless here. A wrong size commits the brand to an order it cannot fill, and in a Cash-on-Delivery market that ends as a parcel coming back.
The usual mitigation is to put the catalogue in the prompt and ask the model nicely to stick to it. That reduces the error rate. It does not bound it, and a bounded error rate is what a shop owner actually needs before handing a bot their customers.
The approach: retrieval decides, the model phrases
The copilot inverts the usual arrangement. The deterministic layer is authoritative for everything that touches data; the model is a language interface bolted on the front and back. Concretely, a message moves through this order:
- Normalise the text — lowercasing, whitespace, alias expansion.
- Detect the language. Not with
langdetect, which fails on Roman Urdu, but with a keyword heuristic over a curated Roman Urdu vocabulary. - Detect intent with a regex and keyword classifier.
- Extract entities: product, colour, size, quantity, SKU, budget, exclusions.
- Search the catalogue — SKU, context ID, alias, description and category tokens, structured variant and price filters, with a fuzzy fallback.
- Match store policies: COD, delivery, returns, exchange.
- Build a grounded response carrying its own sources.
Only then, and only sometimes, does an LLM get involved.
Where the model is actually allowed to run
With a provider configured, two LangChain chains exist, and they have very different privileges.
The first classifies every inbound message using an LCEL chain with Pydantic structured output. Its result is advisory: the deterministic classifier remains authoritative for retrieval and for anything that mutates an order.
The second is the response chain, and it runs only when the deterministic layer is unsure — an ambiguous query, an unrecognised language, or a low-confidence match. It receives recent conversation history and at most five retrieved catalogue candidates. Nothing else. The model never issues a query; it cannot reach the database at all.
Then comes the part that makes this enforcement rather than etiquette: any product or variant ID in the model's output that is not in the candidate set is rejected, and the deterministic response is used in its place. The model cannot name a product that retrieval did not already put in front of it, no matter what it generates.
Every reply carries matched_product_id and sources, so an answer can
be traced back to the rows that produced it after the fact.
The no-key path is the default path
With no API key configured, the LLM layer simply does not run and the deterministic catalogue pipeline answers on its own. This is worth stating plainly because it inverts how most AI features are built: the AI is the enhancement, and the system is expected to work without it. A misconfigured key degrades answer phrasing. It does not take the product down.
Tradeoffs
- Coverage for correctness. Restricting generation to ambiguous queries means the bot handles a narrower range of phrasings gracefully. In exchange, the set of things it can be confidently wrong about is small and enumerable.
- Rules are work. Regex intent detection and a hand-built Roman Urdu keyword list need maintenance that a fine-tuned classifier would not. They are also inspectable at 2am, which a fine-tuned classifier is not.
- Two sources of truth for intent. Running both a deterministic classifier and an LLM classifier means reconciling them. The rule adopted — deterministic wins for anything with side effects — is simple, but it does mean the LLM's better language understanding is sometimes discarded.
- Retrieval quality becomes the ceiling. If catalogue search does not surface the right product, no amount of model capability recovers it. Search quality is where the effort goes.
Limitations
- Roman Urdu detection is a keyword heuristic. It will miss spellings outside the curated vocabulary, and Roman Urdu spelling is not standardised.
- Grounding constrains which products can be named. It does not guarantee the surrounding prose is correct.
- The five-candidate cap is a fixed number chosen for context economy, not a tuned one.
- This is an MVP built inside an 8-week programme. It has no hosted demo, the dashboard has no real authentication, and it has not run at production scale.
- Proposed, not built: semantic retrieval with embeddings alongside the current lexical search, and a measured comparison of grounded versus ungrounded answer accuracy. Neither exists in the repository today.
What I would take to the next system
Grounding beats fluency, and validation beats instruction. The single highest-leverage line in this codebase is the one that discards a generated response containing an unknown product ID. Prompt wording was tuned repeatedly and moved answer quality a little. That check moved the failure mode from "sometimes invents a product" to "cannot invent a product", which is a different category of guarantee — and it is the kind an LLM feature can actually be shipped on.
Related reading
- The full WhatsApp Commerce Copilot case study — architecture, team, results and limitations.
- Designing reliable human handoff for customer-service AI agents — what happens when the copilot should stop answering.
- Source code on GitHub — the
pipeline described here lives under
backend/app/services/.