GenAI: A RAG Product Assistant
Final stop. A ShopKart shopper asks in plain English: "I need cheap wireless earbuds under ₹1500 with good battery — what do you have?" We'll build an assistant that answers this grounded in ShopKart's actual catalog — not in whatever a language model happened to memorise. The technique is RAG: Retrieval-Augmented Generation, and it ties together everything you've learned: pandas, embeddings, and cosine similarity, now feeding a large language model.
Why not just ask the LLM directly?
A large language model (LLM) like Claude or GPT is brilliant at language but knows nothing about ShopKart's inventory, prices, or stock. Ask it directly and it will happily invent plausible-sounding products that don't exist — a hallucination. RAG fixes this by retrieving the relevant real data first and handing it to the model as context, so the model answers from your data instead of from memory.
RAG = Retrieval (find relevant catalog rows) + Augmented Generation (the LLM writes an answer using them). Let's build each piece.
Before you start: you need an LLM API key
This is the one lesson in the course that calls a paid service. Steps 1–3 — embedding the catalog, retrieving products, and building the prompt — run entirely on your machine with no key. Step 4 onward sends that prompt to a hosted model, and that needs an API key.
Pick either provider:
- Anthropic — create a key at [console.anthropic.com](https://console.anthropic.com), then
export ANTHROPIC_API_KEY=sk-ant-... - OpenAI — create a key at [platform.openai.com](https://platform.openai.com), then
export OPENAI_API_KEY=sk-...
# macOS / Linux — add to your shell profile to persist it
export ANTHROPIC_API_KEY="sk-ant-your-key-here"
# Windows PowerShell
$env:ANTHROPIC_API_KEY="sk-ant-your-key-here"
Both SDKs read the key from the environment automatically, so you never paste it into your code. Never commit a key to git.
What it costs: the handful of requests in this lesson are a few short prompts — cents, not rupees in the hundreds. Both providers require a payment method, and new accounts often include free starter credit. Set a low spend limit in the console before you begin. No key, no problem. You can still complete Steps 1–3 and see the retrieval half working, which is the half most production effort goes into anyway. The prompt you build in Step 3 is just a string — print it, read it, and you'll understand exactly what the model receives.Step 1: Index the catalog (retrieval side)
We reuse the embeddings from Lesson 7. Here we embed a rich text per product including price and rating so the model has real facts to cite.
> Need the data? This lesson loads the ShopKart CSVs. Download orders.csv, customers.csv and products.csv into the same folder as your notebook first — Lesson 1 covers the setup.
import pandas as pd
import numpy as np
from sentence_transformers import SentenceTransformer
products = pd.read_csv("products.csv")
products["passage"] = (
products["name"] + " | " + products["category"] + " | ₹"
+ products["price"].astype(int).astype(str)
+ " | rating " + products["rating"].astype(str)
+ " | " + products["description"].fillna("")
)
embedder = SentenceTransformer("all-MiniLM-L6-v2")
catalog_emb = embedder.encode(products["passage"].tolist(),
normalize_embeddings=True)
print(catalog_emb.shape)
(600, 384)
Step 2: Retrieve the top-k relevant products
Given a shopper question, embed it and pull the k most similar products. These become the only facts the LLM is allowed to use.
from sklearn.metrics.pairwise import cosine_similarity
def retrieve(question, k=4):
q = embedder.encode([question], normalize_embeddings=True)
scores = cosine_similarity(q, catalog_emb)[0]
top = np.argsort(scores)[::-1][:k]
return products.iloc[top]
hits = retrieve("cheap wireless earbuds under 1500 with good battery")
hits[["product_id", "name", "price", "rating"]]
product_id name price rating
PRD330 Noise Buds VS104 Earbuds 999.0 4.0
PRD204 boAt Airdopes 141 TWS Earbuds 1299.0 4.1
PRD501 Realme Buds Wireless 2 1499.0 3.9
PRD412 Boult AirBass Z20 Earbuds 1199.0 4.2
Four real, in-budget earbuds retrieved from ShopKart's own catalog — every one genuinely under ₹1,500. Now we hand them to the LLM.
One thing to internalise before we do: retrieve returned exactly four rows because we asked for k=4, not because four products matched. Vector search always hands back k results, however poor the last ones are. We got lucky here; you won't always. That's why the prompt in the next step has to explicitly permit the model to say "nothing here fits".
Step 3: Build a grounded prompt
The prompt has two jobs: give the model the retrieved context, and instruct it to answer only from that context. That instruction is your first guardrail against hallucination.
def build_prompt(question, hits):
context = "\n".join(
f"- {r.name} (₹{int(r.price)}, rating {r.rating}): {r.description}"
for r in hits.itertuples()
)
return f"""You are ShopKart's shopping assistant. Answer the customer's
question using ONLY the products listed in the context below. Recommend the
best matches, mention price and rating, and be concise. If nothing in the
context fits, say you couldn't find a match rather than inventing a product.
Context (ShopKart catalog):
{context}
Customer question: {question}
"""
prompt = build_prompt("cheap wireless earbuds under 1500 with good battery", hits)
Step 4: Generate the answer with an LLM
> This is the step that needs your API key. Everything above ran locally. If you skipped the setup at the top of this lesson, go back and set ANTHROPIC_API_KEY or OPENAI_API_KEY now — otherwise the call below fails with an authentication error.
You can use any capable LLM. Here are both Anthropic's Claude and OpenAI's GPT — they behave the same in a RAG pipeline; pick whichever your team uses.
Option A — Anthropic Claude:
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from the environment
message = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
thinking={"type": "adaptive"},
system="You are ShopKart's helpful, honest shopping assistant.",
messages=[{"role": "user", "content": prompt}],
)
answer = next(b.text for b in message.content if b.type == "text")
print(answer)
Option B — OpenAI GPT:
from openai import OpenAI
client = OpenAI() # reads OPENAI_API_KEY from the environment
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are ShopKart's shopping assistant."},
{"role": "user", "content": prompt},
],
)
print(resp.choices[0].message.content)
Either way, a grounded answer comes back. Here's a representative response — LLM output is not deterministic, so your exact wording will differ:
Great news — here are three solid wireless earbuds under ₹1500 from ShopKart:
1. Noise Buds VS104 (₹999, rating 4.0) — the most affordable pick, with a
strong battery for the price.
2. boAt Airdopes 141 TWS (₹1299, rating 4.1) — the highest-rated in your
budget, known for long playback.
3. Boult AirBass Bluetooth (₹1199, rating 4.2) — best rated overall and
great value.
If battery life is your top priority, the boAt Airdopes 141 is the
crowd favourite. All three ship within your ₹1500 budget.
Notice every product, price, and rating in the answer comes straight from the retrieved rows — the model didn't make anything up. That's RAG working as intended. When you run this yourself, check exactly that: take each number in the answer and find it in the retrieved table. Anything that isn't there is a hallucination, no matter how plausible it reads.
Step 5: Wrap it into one function
The full assistant is just the three stages chained together.
def shopkart_assistant(question, k=4):
hits = retrieve(question, k=k)
prompt = build_prompt(question, hits)
message = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
thinking={"type": "adaptive"},
system="You are ShopKart's helpful, honest shopping assistant.",
messages=[{"role": "user", "content": prompt}],
)
return next(b.text for b in message.content if b.type == "text")
print(shopkart_assistant("something to keep my coffee hot on the way to work"))
For this question, retrieval returns:
product_id name price rating
PRD091 Cello Insulated Coffee Mug 349.0 4.0
PRD088 Milton Thermosteel Flask 1L 649.0 4.4
PRD456 Sparx Running Shorts Lite 519.0 4.3
PRD209 Pigeon Non-Stick Tawa Lite 503.0 4.6
The first two are exactly right. The last two — running shorts and a tawa — are noise, retrieved only because k=4 demanded four rows. This is the failure mode from Step 2, showing up for real.
A grounded answer (again, one representative run):
For keeping coffee hot on your commute, ShopKart has two good options:
1. Cello Insulated Coffee Mug (₹349, rating 4.0) — a double-walled travel
mug with a leak-proof lid, made exactly for this.
2. Milton Thermosteel Flask 1L (₹649, rating 4.4) — vacuum-insulated steel
that holds heat for hours if you want a larger capacity.
The running shorts and tawa in our catalog aren't relevant to your question.
The model quietly ignored the two irrelevant rows. That behaviour is not free — it comes from the "answer using ONLY the products listed" instruction combined with permission to say nothing fits. Drop that instruction and a weaker model will cheerfully explain how a non-stick tawa keeps coffee warm.
Guardrails: keeping it honest
A production RAG assistant needs guardrails so it stays trustworthy:
- Ground strictly — the "use ONLY the context" instruction is essential; without it the model drifts back to its memory.
- Handle no-match — tell the model to admit when nothing fits, rather than inventing a product. Test with off-catalog queries ("do you sell live goats?") and confirm it declines gracefully.
- Show sources — return the retrieved
product_ids alongside the answer so a human (or the UI) can verify each claim links to a real product. - Set thinking and length — adaptive thinking lets Claude reason more on tricky queries;
max_tokenscaps runaway output. - Watch retrieval quality — RAG is only as good as its retrieval. If the right product isn't in the top-k, the LLM can't recommend it. Tune
kand your embeddings first when answers disappoint.
A simple grounding check: verify every product the model named actually appears in what you retrieved.
def with_sources(question, k=4):
hits = retrieve(question, k=k)
answer = shopkart_assistant(question, k=k)
sources = hits["product_id"].tolist()
return {"answer": answer, "sources": sources}
with_sources("wireless earbuds under 1500")["sources"]
['PRD330', 'PRD204', 'PRD412', 'PRD490']
Note this list differs slightly from the Step 2 retrieval — the question is shorter ("wireless earbuds under 1500" versus "cheap wireless earbuds under 1500 with good battery"), and a different phrasing produces a different embedding and therefore a different fourth result. Retrieval is sensitive to how the question is worded, which is worth remembering when a user reports that the assistant "used to" recommend something.
Full circle
Look back at what this assistant stands on: pandas loaded and cleaned the catalog (Lessons 1–2), EDA and features taught you the data (Lessons 3–4), ML gave you recommenders and predictors (Lessons 5–6), embeddings enabled semantic retrieval (Lesson 7), and RAG grounds an LLM in real ShopKart data (this lesson). That is a complete, modern data-to-GenAI stack — the same architecture behind production shopping assistants, support bots, and internal knowledge tools.
On the job
RAG is the single most common pattern for putting LLMs into production, because it solves the two things businesses fear most: hallucination and stale knowledge. Since the model answers from retrieved data, updating the assistant's knowledge means updating your database — no model retraining. Teams building these systems spend most of their effort on the retrieval half (better embeddings, chunking, re-ranking) and on guardrails (grounding, source citation, refusing off-topic queries) — exactly the levers you practised here. Master RAG and you can build assistants over any private dataset: product catalogs, policy documents, support tickets, or internal wikis.
Key takeaways
- RAG = retrieve relevant real data, then have an LLM generate an answer grounded in it.
- It prevents hallucination and keeps knowledge current without retraining — just update the data.
- The ShopKart assistant chains embed query → retrieve top-k → build prompt → LLM call.
- Use Anthropic Claude (
claude-opus-4-8, adaptive thinking) or OpenAI GPT — interchangeable in the pipeline. - Guardrails — answer only from context, admit no-match, cite sources, and monitor retrieval quality.