Before the First Line of Code
Arjun has a rule he has carried since his first job in Bengaluru.
He picked it up from a senior developer named Drona, who had spent three years maintaining a codebase nobody else could navigate. Not because it was complicated — it was not, particularly. Because it had grown without intention. Every feature added where there was space. Every config file placed where it was convenient. Every decision deferred because there was always something more urgent than deciding where things belong.
Drona had inherited that codebase and spent his first three months simply understanding where things were before he could change anything at all.
He made a rule. He gave it to Arjun on Arjun's third week, when Arjun had asked where to put a new utility function and Drona had answered with more seriousness than the question seemed to warrant.
"Structure is not organisation. Structure is the decision about where things belong, made once, so you never have to make it again."
Arjun had written it in his notebook. He had not fully understood it for another six months, until he was deep enough in a different project to feel what the absence of that decision cost — at 2 AM, something broken, the file he needed somewhere in a directory that had grown without a plan.
He thinks about Drona now, at 8 AM on a Friday, a fresh terminal open, ShopBot about to exist.
He does not type a single command yet. He draws first.
The Structure, Before Anything Else
On the back of an envelope — the same one the morning's grocery list is written on — Arjun sketches the project layout.
shopbot/
├── data/ ← zUdyog's product catalog lives here
├── src/ ← All Python logic
├── evaluation/ ← Test cases and evaluation scripts
├── .env ← API keys. Never committed. Never.
├── .gitignore
└── requirements.txt
Seven lines. Not elegant — functional. Every file he writes for the next nine chapters will know exactly where it belongs. When something breaks at 2 AM, and something always breaks at 2 AM, he will not waste time navigating a directory that grew without intention.
The data/ folder is where zUdyog Fashion's product catalog will live — the raw material that the Grounding Layer will be built from. Every product description, every care instruction, every size listing. This folder is the source of truth retrieval will draw from. The evidence lives here before it is embedded, stored, and searched. It earns its own directory because it is not code — it is evidence.
The src/ folder holds all the logic: the embedding pipeline, the ChromaDB setup, the retriever, the prompt chain. Everything that makes ShopBot work lives here.
The evaluation/ folder holds the test cases and scripts that will tell him whether the Grounding Layer is actually working. Building evaluation structure from day one is not caution — it is the decision that prevents shipping a system that feels right but measures wrong.
He creates the structure.
mkdir shopbot && cd shopbot
mkdir data src evaluation
Then the most important file in the project — created before any code, before any logic, before anything that could run.
touch .env .gitignore requirements.txt
He opens .gitignore immediately. Not after the first commit. Before it.
.env
venv/
__pycache__/
chroma_db/
*.pyc
.DS_Store
He has pushed an API key to a public repository once in his career. The resulting thirty minutes — finding the key, rotating it, checking whether it had been used, explaining to Krishna what happened — were among the worst thirty minutes of a professional life that has had some genuinely bad days.
Once was enough.
The Virtual Environment
python -m venv venv
source venv/bin/activate # Mac / Linux
# venv\Scripts\activate # Windows
A virtual environment is not optional. It is the line between a project that works today and a project that breaks when someone updates a system package three months from now.
Without a virtual environment, Python packages install globally — shared across every project on the machine. When one project needs LangChain 0.1 and another needs LangChain 0.2, both cannot coexist. One breaks. Usually the one being presented to a client.
There is a subtler problem too. Global installations accumulate. A package installed for a tutorial six months ago is still there, potentially conflicting with the version ShopBot needs. The error this produces — a version mismatch buried three levels inside a dependency tree — is one of the most time-consuming errors to debug because it looks like a logic error rather than an environment error.
With a virtual environment, ShopBot has its own isolated Python installation. Its dependencies are locked to the versions that work. Nothing from outside breaks in. Nothing from inside leaks out. When the project is shared with another developer or deployed to a server, the environment can be recreated exactly.
The prompt changes when the environment is active.
(venv) arjun@macbook shopbot %
Everything from this point happens inside that boundary.
The Dependencies
pip install langchain langchain-openai chromadb fastapi uvicorn python-dotenv
Six packages. Each one earning its presence.
langchain — the orchestration framework. It connects retrievers, prompts, and language models into chains. In this book, LangChain is the skeleton ShopBot's logic hangs on. It is not the only way to build a RAG system — later chapters will show what each component does at the level beneath the framework — but it makes the structure of the Grounding Layer visible: retriever, prompt, model, output. The architecture is explicit in the code.
langchain-openai — LangChain's interface to OpenAI's models. Separating this from the core framework means you can swap to a different model provider later without touching the chain logic. The Grounding Layer architecture does not depend on any specific model. Chapter 3 will demonstrate this with a local alternative.
chromadb — the vector database. This is where the Grounding Layer will be stored — where zUdyog Fashion's product catalog lives as searchable embeddings. ChromaDB runs locally, on disk, in a folder called chroma_db/. No server to set up. No Docker. No configuration files. For building and learning, it is the correct choice. Book 2 migrates to Qdrant Cloud for production. The concepts transfer entirely.
fastapi — the API framework. Chapter 8 wraps ShopBot in a FastAPI endpoint that any frontend can call. Installing it now rather than in Chapter 8 means no mid-project dependency surprises — no moment where adding a package breaks something that was already working.
uvicorn — the ASGI server that runs FastAPI. They always travel together. FastAPI defines the application. Uvicorn runs it.
python-dotenv — reads the .env file and makes its contents available as environment variables. This is how the code accesses the OpenAI API key without hardcoding it into any file that could be committed, shared, or read by anyone with access to the repository.
After installation, pin the versions.
pip freeze > requirements.txt
This records exactly which version of every package is installed. Anyone who clones the repository and runs pip install -r requirements.txt gets an identical environment. No version drift. No "it works on my machine" conversations. No debugging sessions that turn out to be dependency mismatches.
The API Key
Open .env. Add one line.
OPENAI_API_KEY=sk-your-key-here
Then stop. Do not add anything else yet. Do not run anything yet.
Open a browser. Go to platform.openai.com. Find the API key management page. Create a new key. Give it a name: shopbot-dev.
Do not use the same key for development and production. Development keys get pasted into Slack messages accidentally. They get left in notebooks. They get committed to repositories despite .gitignore being present, because someone created a file outside the project root or made a mistake with a global git config. A dedicated development key can be rotated without affecting anything live.
Copy the key. Paste it into .env. Close the browser tab.
The key is now accessible to the code and invisible to everything else — Git, GitHub, version control, future collaborators, anyone who clones the repository.
Before running a single line of code, verify that .gitignore is working.
git init
git status
The .env file must not appear in the list of tracked files. If it does, the .gitignore is not working and must be fixed before anything else.
The First Real Test
The setup must be verified before building on top of it. A misconfigured environment that appears correct is worse than an obviously broken one — it produces errors that look like logic errors when they are actually configuration errors. Debugging a retrieval problem that is actually a missing API key is a specific kind of frustrating.
Arjun writes a single verification file.
# test_setup.py
import os
from dotenv import load_dotenv
from langchain_openai import OpenAIEmbeddings
load_dotenv() # Read .env into environment variables
embeddings = OpenAIEmbeddings()
# Embed a single sentence and inspect the result
result = embeddings.embed_query("Does this kurta come in size S?")
print(f"Setup working.")
print(f"API key found: {'Yes' if os.getenv('OPENAI_API_KEY') else 'No'}")
print(f"Embedding dimensions: {len(result)}")
print(f"First three values: {result[:3]}")
He runs it.
python test_setup.py
Output:
Setup working.
API key found: Yes
Embedding dimensions: 1536
First three values: [0.021, -0.047, 0.003]
1,536 numbers.
That is what the sentence "Does this kurta come in size S?" looks like to a machine. Not words. Not meaning in any human sense. A fingerprint — a precise position in a 1,536-dimensional space where sentences with similar meaning cluster near each other.
What those numbers represent — why they are useful, how they power ShopBot's search, what it means for two sentences to be close in that space — is Chapter 3's work. Right now, the number 1,536 is proof that the setup is correct and the foundation is solid.
The chai he had poured at 8 AM was cold by the time the script printed those four lines. He did not reach for it. He stared at the output longer than he needed to, feeling something he had not felt in a year — the small private satisfaction of a foundation laid by his own hand rather than inherited.
That is not a number on the screen. That is the beginning of ShopBot.
The Free Alternative
Arjun shows the terminal to Krishna, who is in the kitchen and half paying attention.
"OpenAI costs money," Krishna says. Not a question.
"Embedding a sentence costs less than one paisa," Arjun says. "The entire zUdyog catalog — every product, every chunk we will create in Chapter 5 — will cost approximately three rupees to embed. One time. After that, only queries cost anything."
Krishna processes this. "And per query?"
"Each customer question is one embedding call. At current prices, roughly 0.002 paise per query."
"Show me the maths later," Krishna says. "What if we do not want to depend on OpenAI at all?"
Arjun has already checked. There is an alternative that costs nothing and runs entirely on the local machine.
Step 1: Install Ollama.
# Mac:
brew install ollama
# Linux:
curl -fsSL https://ollama.com/install.sh | sh
# Windows:
# Download the installer from https://ollama.com/download
Step 2: Start the Ollama service.
# Mac — run once, restarts automatically on login:
brew services start ollama
# Or start it manually in a terminal (keep the terminal open):
ollama serve
The server listens on http://127.0.0.1:11434. Leave it running in the background.
Step 3: Pull the models.
ollama pull nomic-embed-text # Embedding model — free, local, 274 MB
ollama pull llama3.2 # Language model — free, local, 2 GB
Confirm both downloaded correctly.
ollama list
NAME ID SIZE MODIFIED
nomic-embed-text:latest 0a109f422b47 274 MB just now
llama3.2:latest a80c4f17acd5 2.0 GB just now
Step 4: Install the LangChain integration package.
pip install langchain-ollama
pip freeze > requirements.txt
Step 5: Replace the embeddings and LLM references.
# Embeddings — replace OpenAIEmbeddings with this:
from langchain_ollama import OllamaEmbeddings
embeddings = OllamaEmbeddings(model="nomic-embed-text")
# Language model — replace ChatOpenAI with this:
from langchain_ollama import OllamaLLM
llm = OllamaLLM(model="llama3.2")
Step 6: Verify the local setup.
# test_setup_local.py
from langchain_ollama import OllamaEmbeddings
embeddings = OllamaEmbeddings(model="nomic-embed-text")
result = embeddings.embed_query("Does this kurta come in size S?")
print(f"Local setup working.")
print(f"Embedding dimensions: {len(result)}")
print(f"First three values: {result[:3]}")
python test_setup_local.py
Local setup working.
Embedding dimensions: 768
First three values: [0.014, -0.031, 0.008]
Notice the dimension is 768, not 1,536. nomic-embed-text produces 768-dimensional embeddings versus OpenAI's 1,536. The number of dimensions affects the ChromaDB collection — a collection created with one embedding model cannot be queried with another. Pick one model before creating the database and stay with it.
Every piece of code in this book works with either option. OpenAI is faster to set up and produces slightly better embeddings for semantic search tasks. Ollama runs entirely on your machine — no API calls, no billing, no rate limits, no data leaving the local environment.
The tradeoff is setup time and embedding quality. OpenAI's text-embedding-3-small model produces 1,536-dimensional embeddings trained on vastly more data than the typical local model. For a production system serving real customers, the quality difference matters. For learning the architecture and building the prototype, Ollama is entirely sufficient.
This book uses OpenAI throughout. If you use Ollama, replace OpenAIEmbeddings() and ChatOpenAI() wherever they appear. The rest of the code — the chunking, the ChromaDB setup, the retrieval chain, the FastAPI layer — is identical.
What the Setup Actually Is
It is tempting to treat this chapter as administrative — the part before the real work begins. It is not.
Every decision made in this chapter has consequences that reach Chapter 10.
The .gitignore created before the first commit means an API key has never been exposed and will not be. The virtual environment means a dependency conflict three months from now does not break a production system. The pinned requirements.txt means another developer — or a deployment server — can recreate this environment exactly. The test_setup.py means the next nine chapters build on a verified foundation rather than an assumed one.
None of these are sophisticated. All of them are necessary.
The Grounding Layer that ShopBot will build over the next eight chapters is only as reliable as the environment it runs in. A retrieval system that returns wrong results because of a dependency mismatch produces failures that look identical to retrieval failures — wrong answers, missed products, confused context. Debugging the Grounding Layer when the environment is the actual problem is the developer equivalent of fixing plumbing in a house with a faulty foundation.
Drona had a second rule, which he gave Arjun on his last day at that job.
"The student who skips the fundamentals borrows time at high interest, paid later, when it hurts most."
Arjun has paid that interest before. He does not intend to again.
A Reader Exercise
Complete the setup described in this chapter. Create the directory structure, activate the virtual environment, install the dependencies, configure the .env file, and run test_setup.py until it outputs 1,536.
Then run one additional test. Add these two lines to test_setup.py.
result2 = embeddings.embed_query("What is the price of the silk saree?")
print(f"\nKurta size question — first 3 values: {result[:3]}")
print(f"Saree price question — first 3 values: {result2[:3]}")
The two questions are about completely different things. The numbers will be different. They will not be randomly different — they will be systematically different, because the embedding model places these sentences in different regions of the 1,536-dimensional space.
You cannot see the full geometry in three values. But you can see that the numbers are not the same, and that they are not supposed to be. Chapter 3 explains what the geometry means, how it enables retrieval, and why it is the engine behind everything ShopBot will do. [Effort: 15 minutes.]
What Just Happened
Drona's rule: structure is decided once, at the start, so it never has to be decided again. The virtual environment, the .gitignore, the pinned dependencies, and the verified setup are not overhead — they are the decisions that prevent debugging sessions caused by environment problems disguised as logic problems. The 1,536 numbers prove the foundation holds.
Chapter 3 explains what those numbers mean, how two sentences can be measured for similarity without sharing a single word, and why that capability is the entire reason ShopBot can understand a customer who types something light for summer and match it to a product described as breathable cotton, ideal for warm weather.