Ship It
The Railway URL has been live for four days.
Arjun sent it to three beta testers on Monday morning with one instruction: use it as a real customer would. Ask what you would actually ask. Do not be kind about what breaks.
Two of the testers responded with polite, careful queries that ShopBot handled without difficulty. The third tester is Meera.
Meera runs a boutique in Pune that sells ethnic wear — kurtas, sarees, occasion suits — to a customer base that knows exactly what it wants and expects to be told clearly whether they can get it. She has been Arjun's most reliable source of inconvenient feedback since the first prototype. She does not frame problems as suggestions.
Her first message arrives Tuesday evening. "Tested it. One question."
He knew, before he read the next message, that the question would tell him something true — and he was not certain yet whether he wanted to know what it was. The chai on the desk beside his laptop has gone cold. He does not reach for it. He reads the next message.
The Question That Started Everything
Meera's query: "Does the Anarkali suit have a cotton lining?"
He opens the ShopBot logs. Finds her conversation.
ShopBot's answer: "I don't have information about the lining material for the Anarkali suit. For specific fabric details, please contact our team at support@zudyog.com."
He sits with that for a moment.
In Chapter 1, thirty-one support tickets. The first ticket: a customer who asked whether a kurta had a cotton lining. The chatbot replied confidently — "Yes, this kurta features a soft cotton lining for added comfort" — and invented a feature that did not exist. A return filed. A refund. A customer who said in writing she would not shop at zUdyog again.
Meera asked the same shape of question, ten chapters later. ShopBot admitted it did not know. Routed her to support. Did not invent a lining that does not exist.
The question that broke the old system is the question that proves the new one works.
He does not feel pride. He feels recognition — the specific weight of understanding that the system he built to fix a failure has, in fact, fixed it.
What Real Users Find
The two other beta testers found nothing that failed. Meera found three things.
Failure one — the ambiguous query.
"What's good for a function?"
In Indian English, function means a social event — a wedding, a reception, a celebration. It is the natural word a Pune customer would use. The retriever does not know this. The catalog uses festive, occasion, celebration — not function. The retriever searches for the closest semantic match and returns the linen co-ord set at 0.71, tagged as casual wear, because function without context pulls toward utility rather than festivity.
ShopBot recommends casual summer wear to a customer shopping for a wedding.
The fix is not in the model or the retriever. It is in the chunk metadata. Adding function as an occasion synonym to the festive-product chunks brings those products into the right semantic neighbourhood. Arjun updates the metadata for the silk saree, the anarkali suit, and the festive occasion chunks. Re-ingests. The linen co-ord set drops to rank four. The saree and the anarkali rise.
A vocabulary gap, closed by data — not by a smarter model.
The fix is in data/products.py. Add "function" to the occasion field for festive products, then re-ingest.
# data/products.py — update occasion field for p003 and p005
"occasion": "festive, wedding, formal, function", # p003 Silk Saree
"occasion": "festive, wedding, formal, function", # p005 Anarkali Suit
rm -rf chroma_db/
python src/ingest.py
The silk saree and the anarkali suit are now in the semantic neighbourhood of function queries. The linen co-ord set drops out of the top results.
Failure two — the multi-intent query.
"Show me something festive and warm."
The retriever is built for one concept per query. It converts the entire query into a single vector and finds the nearest chunks. The combined vector of festive and warm lands somewhere between the festive chunks and the winter chunks — close to neither. The silk saree returns at 0.79 for festivity. The woolen shawl returns at 0.74 for warmth. But the query asked for both simultaneously — a festive product that is also warm. Neither product fully answers it.
The correct solution is multi-query retrieval — split the query into two sub-queries, retrieve each separately, merge and re-rank the results. This is Book 2's work. For now, ShopBot handles it with the best single-vector result and acknowledges the limitation in its answer: "The silk saree is a festive option, though it is not specifically designed for warmth. For winter events, you may want to layer it — or contact us at support@zudyog.com for styling advice."
Not perfect. Honest.
Failure three — the out-of-scope slip.
"What jewellery goes with the saree?"
zUdyog Fashion does not sell jewellery. There is no jewellery in the catalog. The correct answer is the fallback.
The first time Meera types this, the saree chunk scores 0.62 against the query — below the 0.75 threshold. The retriever returns nothing. ShopBot gives the fallback. Correct.
But Meera notices that 0.62 is suspiciously close to the threshold. She rephrases — "jewellery for the silk saree" — and the score rises to 0.77. The retriever now returns the saree chunk as if it answers a jewellery question. It does not. The model receives a saree description and attempts to answer a styling question from it.
The fix is one line in the system prompt. If the context describes a product but does not address the type of question being asked, say you do not have that information and direct the customer to support@zudyog.com. The model now reads the saree chunk, recognises that the query is about jewellery and the chunk is about a saree, and routes correctly.
A type-mismatch problem, addressed at the prompt layer until Book 3 introduces a corrective retrieval step that handles it more systematically.
The fix is one sentence added to SHOPBOT_SYSTEM in src/prompt.py, inside the "When you do NOT have the information" block.
# src/prompt.py — add this line inside the "When you do NOT have the information" block
- If the context describes a product but does not address the type of question
being asked, say you do not have that information and direct the customer
to support@zudyog.com.
After this addition, when the retriever returns a saree chunk for a jewellery query, the model reads the chunk, recognises the mismatch, and routes to support rather than attempting to answer from irrelevant evidence.
Three failures. Three fixes. None requiring a rewrite of the core architecture. The Grounding Layer held. The failures were at the edges — vocabulary gaps, multi-intent queries, threshold-adjacent slips. All three are addressable without changing what works.
₹7.60
On Thursday, Arjun pulls the OpenAI billing dashboard.
Total API cost for the entire Book 1 build — embedding the catalog, running every test query, the twenty evaluation queries in Chapter 9, the demo scripts, the before-and-after comparisons across ten chapters.
₹7.60.
He sends the number to Krishna.
Krishna reads it. Does not respond for two minutes.
Then: "Send me the Railway hosting cost."
Arjun sends it.
Another silence.
"Scale it."
That is all. Two words. From Krishna, those two words mean: the unit economics work. Build more.
The Full Architecture
Ten chapters. Five components. One pipeline.
The journey from Chapter 1's thirty-one support tickets to a live, grounded product assistant runs through every decision made in every chapter — not sequentially, simultaneously. Every component depends on the ones before it and enables the ones after.
The customer's question arrives at the FastAPI endpoint as a JSON request. The endpoint passes it to the retriever. The retriever converts the question into an embedding using OpenAI's text-embedding-3-small — the 1,536-dimensional coordinate that represents the query's meaning in semantic space. ChromaDB receives that coordinate and returns the chunks whose embeddings are nearest — the product attributes whose meaning is most similar to the question's meaning, filtered by the 0.75 similarity threshold. The chunks that pass the threshold become the context. The prompt wraps the context and the question in a constrained instruction set and passes both to GPT-4o-mini at temperature zero. The model reads the evidence and speaks from it. The answer returns as JSON.
If the retriever finds nothing above threshold, the fallback routes the customer to support@zudyog.com. If the model receives context that does not match the query type, the updated prompt instructs it to admit the gap. If the query is ambiguous, the metadata improvements narrow the semantic neighbourhood. The system fails gracefully at its edges and reliably at its centre.
The Grounding Layer, Complete
Chapter 1 named the problem. A system that answered confidently from pattern rather than from evidence. A cotton lining that did not exist. A return policy that was wrong. An HTTP 200 that meant nothing about accuracy.
The fix was not a better model. It was an architecture — retrieved truth, placed between question and answer, before the model speaks. The Grounding Layer.
Every chapter built one part of it. The embeddings gave it a way to understand meaning. ChromaDB gave it a memory. The chunking strategy gave that memory precision. The retriever gave it a threshold — a decision about what is worth passing to the model and what is not. The prompt gave the model a constraint. The API gave the system a door. The evaluation gave the Grounding Layer a number.
The Grounding Layer is ShopBot's Pramana — the valid source it speaks from, and the boundary it does not cross.
Confident Drift is not the model's failure. It is the architecture's. A model with no Grounding Layer drifts because drifting is what pattern-completion does in the absence of evidence. A model with a Grounding Layer reads what it has been given and stops where the evidence stops. The difference between Chapter 1's chatbot and ShopBot is not intelligence. It is architecture.
A Reader Exercise
Send your deployed ShopBot URL to one person who did not build it — a friend, a colleague, anyone who will ask questions without knowing what the system was designed to answer.
Give them no instructions except this: use it as a real customer would.
Read the logs after. Find the query that produced the lowest similarity score above threshold. Examine what ShopBot said. Examine what it should have said. The gap between those two answers is the next thing to fix.
Every production system has that query. Finding it is the beginning of the work that comes next. [Effort: 30 minutes.]
What Just Happened
Ten chapters ago, thirty-one support tickets. A chatbot that invented a cotton lining, fabricated a return policy, and returned HTTP 200 for every wrong answer. Three attempted fixes — better prompts, full catalog in context, fine-tuning — each failing for the same reason: they tried to put better information inside the model instead of placing real information in front of it. The Grounding Layer is that placement. Embeddings convert meaning to geometry. ChromaDB stores the geometry and retrieves by proximity. Chunking structures the stored evidence for precision. The retriever gates what reaches the model. The prompt constrains what the model does with what it receives. The API gives the system an address. Evaluation gives the Grounding Layer a number.
Meera asked the question that started everything. ShopBot admitted it did not know. The arc is complete. The system is live. The cost was ₹7.60.
Three failures remain on the edges, and the next book begins with them. Vocabulary gaps where customers use words the catalog does not. Conversations that depend on what the customer said two messages ago. Repeated queries that the system pays full price for, every time, because nothing is cached. Each of those is its own chapter in Book 2 — The Applied AI Engineer — where ShopBot meets real traffic, real volume, real cost, and the Grounding Layer learns to hold under all three.
But that is the next book. This one ends here, on a Tuesday evening in Pune, where a boutique owner asked a chatbot whether an anarkali suit had a cotton lining and the chatbot, for the first time in this story, told the truth.
This is not a chatbot.