A product qa chatbot langchain catalog implementation turns static product data into a conversational interface that answers spec questions without a human in the loop. This tutorial builds a retrieval-augmented generation (RAG) pipeline from a sample ecommerce catalog, using LangChain and an OpenAI-compatible chat model.
Prerequisites
- Python 3.10 or newer
langchain,langchain-openai,langchain-community,chromadb- An OpenAI API key (or any OpenAI-compatible endpoint)
- A product catalog as JSON lines
Install the dependencies:
pip install langchain langchain-openai langchain-community chromadb openai
Sample catalog
We use a toy catalog.jsonl with three products. In production this would be exported from your PIM or database.
{"sku":"A1","name":"TrailRunner Shoe","category":"footwear","specs":{"weight_g":280,"waterproof":true,"size_range":[38,46]},"description":"Lightweight trail shoe with Gore-Tex membrane."}
{"sku":"B2","name":"UrbanCommute Jacket","category":"apparel","specs":{"weight_g":420,"waterproof":false,"size_range":["S","M","L","XL"]},"description":"Breathable city jacket with reflective trim."}
{"sku":"C3","name":"PeakPower Backpack","category":"bags","specs":{"volume_l":30,"laptop_sleeve":true,"weight_g":900},"description":"30L hiking backpack with padded 16-inch laptop sleeve."}
Load and chunk the catalog
LangChain’s JSONLoader reads JSON lines. We flatten each record into a single text block and preserve SKU and category as metadata for later filtering.
from langchain_community.document_loaders import JSONLoader
import json
loader = JSONLoader(
file_path="catalog.jsonl",
jq_schema=".",
text_content=False
)
raw_docs = loader.load()
docs = []
for d in raw_docs:
obj = json.loads(d.page_content)
content = f"{obj['name']} ({obj['sku']}): {obj['description']} Specs: {obj['specs']}"
docs.append({
"page_content": content,
"metadata": {"sku": obj["sku"], "category": obj["category"]}
})
# Rebuild as LangChain Document objects
from langchain_core.documents import Document
documents = [Document(page_content=d["page_content"], metadata=d["metadata"]) for d in docs]
Checkpoint — inspect the first document:
print(documents[0].page_content)
print(documents[0].metadata)
Expected output:
TrailRunner Shoe (A1): Lightweight trail shoe with Gore-Tex membrane. Specs: {'weight_g': 280, 'waterproof': True, 'size_range': [38, 46]}
{'sku': 'A1', 'category': 'footwear'}
Embed and store in Chroma
We embed with text-embedding-3-small and index in an in-memory Chroma collection. For an OpenAI-compatible endpoint that honors provider cache-control hints, set base_url on the embeddings client.
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(
documents,
embeddings,
collection_name="catalog"
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
Build the retrieval chain
Define the LLM. For resilience against provider outages, an OpenAI-compatible endpoint that provides automatic fallback across providers—such as n4n.ai—keeps the chatbot online when one model is rate-limited. Set base_url if you use such a gateway.
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0,
# base_url="https://api.n4n.ai/v1" # optional OpenAI-compatible gateway
)
Now the prompt and the stuff-documents chain. The system prompt restricts answers to retrieved context.
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate
system_prompt = (
"You are a product support assistant. Answer questions using only the provided "
"catalog excerpts. If the answer is not in the context, state that you don't know. "
"Context: {context}"
)
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", "{input}")
])
qa_chain = create_stuff_documents_chain(llm, prompt)
rag_chain = create_retrieval_chain(retriever, qa_chain)
Add conversational memory
Without memory, follow-up questions like “Does it come in size 42?” fail because the model forgot the product. Wrap the chain with RunnableWithMessageHistory.
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
store = {}
def get_history(session_id):
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
conversational_rag = RunnableWithMessageHistory(
rag_chain,
get_history,
input_messages_key="input",
history_messages_key="chat_history",
output_messages_key="answer"
)
Run the product qa chatbot langchain catalog
Invoke with a stable session_id per user:
resp = conversational_rag.invoke(
{"input": "Which shoes are waterproof and under 300 grams?"},
config={"configurable": {"session_id": "u1"}}
)
print(resp["answer"])
Expected output:
The TrailRunner Shoe (A1) is waterproof (Gore-Tex membrane) and weighs 280 grams, which is under 300 grams.
Follow-up turn in the same session:
resp2 = conversational_rag.invoke(
{"input": "Does it come in size 42?"},
config={"configurable": {"session_id": "u1"}}
)
print(resp2["answer"])
Expected output:
Yes, the TrailRunner Shoe has a size range of 38 to 46, so size 42 is available.
Inspect retrieved context
Before shipping, verify the retriever returns relevant chunks. The rag_chain exposes retriever output if you call the underlying retriever directly:
hits = retriever.invoke("waterproof shoes under 300g")
for h in hits:
print(h.metadata["sku"], "->", h.page_content[:60])
Expected output:
A1 -> TrailRunner Shoe (A1): Lightweight trail shoe with Gore-Tex mem
If precision is low, increase k or add a metadata filter (e.g., category="footwear").
Handling catalog updates
Chroma persists to disk with persist_directory. For incremental updates, upsert by SKU to avoid duplicates:
vectorstore = Chroma.from_documents(
documents,
embeddings,
collection_name="catalog",
persist_directory="./chroma_db"
)
# Later: add or replace a product
new_doc = Document(
page_content="TrailRunner Shoe (A1): Updated 2025 edition with Vibram sole. Specs: {'weight_g': 275, 'waterproof': True, 'size_range': [38, 47]}",
metadata={"sku": "A1", "category": "footwear"}
)
vectorstore.add_documents([new_doc])
Because Chroma keys by internal ID, you should delete the old SKU first in production using a metadata filter.
Production considerations
- Latency: Embedding the catalog dominates cold start. Embed once, persist, and reuse. Query-time embedding is a single vector per question.
- Routing: If you serve multiple regions, honor client routing directives to keep data local and reduce round-trips.
- Metering: Per-token usage metering lets you attribute cost to each customer session. OpenAI-compatible responses include a
usagefield; log it persession_id. - Guardrails: The prompt restricts answers to context, but add a post-check that extracted SKUs exist in your catalog to catch model drift.
Extending the pipeline
The same pattern works for multilingual catalogs (use text-embedding-3-large or a multilingual model), image-attached products (store image URLs in metadata and return them in the answer), and faceted search (pass filter={"category": "bags"} to the retriever).
A product qa chatbot langchain catalog deployment is only as good as retrieval precision. Tune k, add hybrid search with BM25 for keyword-heavy queries like SKU lookups, and log failed questions to improve chunk size and metadata.