Build a PDF Q&A Bot with LangChain
Build a RAG pipeline that answers questions from any PDF using LangChain and Gemini.
Introduction
30 Second Summary
Imagine you have a 100-page report and need to find the answer to one specific question. Instead of flipping through every page, what if you had an AI assistant that could read the whole thing and answer instantly?
In this project, you will build a PDF Q&A bot powered by RAG (Retrieval-Augmented Generation). Your bot loads any PDF, breaks it into searchable chunks, and uses Google Gemini to generate answers grounded in the document's actual content.
What You'll Build
A terminal-based Python application that implements a complete RAG pipeline using LangChain and Google Gemini.
By the end of this project, you'll have:
- A Python script (rag_bot.py) that loads any PDF, splits it into chunks, and stores those chunks in a local vector store.
- A ChromaDB vector database storing your PDF's content as searchable embeddings.
- An LCEL chain that wires retrieval, prompting, and generation together using LangChain's | pipe operator.
- Secret Mission: Add source citations so every answer shows which PDF pages it came from.
Are there any prerequisites?
This project assumes basic familiarity with Python syntax and comfort using pip and virtual environments. You do not need any prior experience with LangChain, RAG, or AI APIs.
Set Up Your Environment
Before building your RAG pipeline, you need a clean Python environment with all the right packages installed. You also need an API key for Google's Gemini, which powers both the text generation and the embeddings in this project.
In this step, you'll create a project folder, install LangChain and its dependencies, and configure your Gemini API key.
In this step, get ready to:
- Create a project folder and Python virtual environment.
- Install LangChain, ChromaDB, and all required packages.
- Set up your Google Gemini API key and prepare a test PDF.
Create your project folder and virtual environment
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Open the integrated terminal by pressing Ctrl+` (the backtick key above Tab).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir pdf-rag-bot
cd pdf-rag-bot
- Confirm the folder was created by running:
ls
You should see no output since the folder is empty. If you got a No such file or directory error on the cd command, re-run the mkdir line first.
Now open this folder in VS Code so the Explorer sidebar shows your project.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the pdf-rag-bot folder, and click Open.
VS Code reloads with pdf-rag-bot as your workspace. You should see the folder name in the Explorer sidebar on the left.
- Open the integrated terminal again by pressing Ctrl+`.
A virtual environment is an isolated copy of Python that keeps this project's packages separate from your system Python. This prevents version conflicts between projects.
- Create a virtual environment by running this command:
python -m venv venv
What does this command do?
python -m venv tells Python to run its built-in virtual environment module. The second venv is the name of the folder where the environment is stored.
- Confirm the virtual environment was created by running:
ls
You should see a venv folder in the output.
Seeing 'python' is not recognized?
On some systems, the command is python3 instead of python. Try running python3 -m venv venv instead.
If neither works, Python may not be installed. This project requires Python 3.10 or higher. Download it from python.org and re-run the command.
Help me troubleshoot my Python installation
Now activate the virtual environment. The activation command differs by operating system.
macOS
- Activate the virtual environment by running:
source venv/bin/activate
Windows
- Activate the virtual environment by running:
.\venv\Scripts\activate
You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.
Not seeing (venv) in your prompt?
On Windows, PowerShell may block the activation script due to execution policy restrictions. Run this command first, then try activating again:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
On macOS, make sure you typed source at the beginning of the command. Without it, the activation script will not run.
Help me activate my Python virtual environment
Install the required packages
Your project needs several packages. LangChain provides the RAG framework. The Gemini integration package adds the LLM and embedding models. ChromaDB stores your document vectors locally.
- Install all the packages in one command by running:
pip install langchain langchain-google-genai langchain-community langchain-chroma langchain-text-splitters pypdf python-dotenv
What are all these packages?
- langchain is the core framework for building LLM-powered applications.
- langchain-google-genai connects LangChain to Google's Gemini models for chat and embeddings.
- langchain-community provides document loaders like PyPDFLoader.
- langchain-chroma integrates the ChromaDB vector database with LangChain.
- langchain-text-splitters provides tools for breaking documents into smaller chunks.
- pypdf is the underlying library that reads PDF files.
- python-dotenv loads environment variables from a .env file so your API key stays out of your code.
The install takes about a minute. When it finishes, you should see Successfully installed followed by a list of all the packages and their versions.
Seeing an installation error?
Make sure (venv) is visible in your terminal prompt. If it is not, go back and activate the virtual environment first.
If you see a permissions error, try running pip install --user followed by the package names. If the issue is a network timeout, check your internet connection and try again.
Help me fix my pip install error
Get your Google API key and create the environment file
Gemini needs an API key to authenticate your requests. Google offers a free tier with up to 1,000 requests per day, so this project costs nothing.
- Open Google AI Studio in your browser.
- Sign in with your Google account if prompted.
- Click Create API Key.
- Copy the generated API key. You will need it in the next step.
What is an API key?
An API key is a unique string that identifies your application to a service. When your script sends a request to Gemini, the key tells Google who is making the request and whether they are allowed to.
Now store your API key in a .env file. This keeps your key out of your code so you never accidentally share it.
- In the VS Code Explorer sidebar, click the New File icon (the page icon with a plus sign) at the top of the file list.
- Type .env as the file name and press Enter.
- Paste the following into your new .env file, replacing your-api-key-here with the API key you just copied:
GOOGLE_API_KEY=your-api-key-here
Why use a .env file?
The .env file keeps sensitive values like API keys separate from your code. The python-dotenv package loads these values into your script at runtime so you never hard-code secrets.
- Save .env by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Finally, you need a PDF for your bot to answer questions about. This can be any multi-page document: a resume, a report, a research paper, or a manual.
- Find a PDF on your computer. If you do not have one handy, download any freely available PDF online.
- Drag the PDF file into the VS Code Explorer sidebar, directly into the pdf-rag-bot folder.
- Rename the file to sample.pdf by right-clicking it in the Explorer sidebar and selecting Rename.
Pick a PDF with real text
PDFs that are scanned images (like photographed receipts) will not work well. Choose a PDF where the text is selectable, because the loader needs to extract actual text content from each page.
Your Explorer sidebar should now show three items: .env, sample.pdf, and the venv folder.
Don't see all three items?
Make sure you opened the pdf-rag-bot folder (not your Desktop or a parent folder) in VS Code. The Explorer sidebar title should read PDF-RAG-BOT.
If .env is missing, it may be hidden. Click the gear icon in the Explorer sidebar and make sure hidden files are visible.
Help me find my project files in VS Code
Your environment is ready. Next up, you'll write the first half of the RAG pipeline: loading your PDF and splitting it into chunks that the vector store can search.
Load and Chunk Your PDF
Your environment is set up with all the packages you need. Now it's time to start building the actual RAG pipeline.
The first stage of any RAG pipeline is data preparation. You need to get your PDF's text into a format LangChain can work with, then break it into smaller pieces for precise retrieval.
In this step, get ready to:
- Create rag_bot.py and load your PDF with a Document Loader.
- Split the loaded pages into smaller chunks for retrieval.
- Run the script to verify your loader and splitter work.
Create rag_bot.py and load the PDF with PyPDFLoader
LangChain uses Document Loaders to turn raw files into structured data that the rest of your pipeline can process. PyPDFLoader is the loader for PDF files. It reads your PDF and creates one Document object per page.
- In your code editor, create a new file called rag_bot.py in your pdf-rag-bot folder.
- Set up your script with imports and PDF loading code by pasting the following into rag_bot.py:
import os
from dotenv import load_dotenv
from langchain_google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
# Load environment variables
load_dotenv()
# --- Load the PDF ---
loader = PyPDFLoader("sample.pdf")
docs = loader.load()
print(f"Loaded {len(docs)} pages from the PDF")
What does this code do?
- The import lines bring in every LangChain component you'll use across this project. You won't use all of them in this step, but having them ready avoids going back to add imports later.
- load_dotenv() reads your .env file so the GOOGLE_API_KEY is available to LangChain when it needs to call the Gemini API.
- PyPDFLoader("sample.pdf") creates a loader pointed at your PDF file.
- loader.load() reads the entire PDF and returns a list of Document objects. Each Document has page_content (the text) and metadata (like the page number and source file).
You won't see any output yet. The next substep adds the text splitter, then you'll run the script to test everything.
Split the documents into chunks with RecursiveCharacterTextSplitter
Full PDF pages are often too long and too broad for accurate retrieval. A text splitter breaks them into smaller, focused chunks that a vector store can search more precisely.
- In rag_bot.py, add this code below the print(f"Loaded {len(docs)} pages from the PDF") line:
# --- Split into chunks ---
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
)
chunks = text_splitter.split_documents(docs)
print(f"Split into {len(chunks)} chunks")
What does this code do?
- chunk_size=1000 sets the maximum length of each chunk to 1000 characters.
- chunk_overlap=200 means neighboring chunks share 200 characters of text. This overlap prevents context from being lost at chunk boundaries.
- split_documents(docs) takes your list of page-level Document objects and returns a longer list of smaller chunk Document objects.
- Save rag_bot.py.
You'll run the script in the next substep to see both the page count and chunk count printed.
✔️ Awesome, I've got everything!
Great. Double check you've saved rag_bot.py before moving on.
ⓧ I'd like to double check the full code
Here is the full rag_bot.py file so far. Compare it with yours and make sure everything matches.
import os
from dotenv import load_dotenv
from langchain_google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
# Load environment variables
load_dotenv()
# --- Load the PDF ---
loader = PyPDFLoader("sample.pdf")
docs = loader.load()
print(f"Loaded {len(docs)} pages from the PDF")
# --- Split into chunks ---
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
)
chunks = text_splitter.split_documents(docs)
print(f"Split into {len(chunks)} chunks")
Run the script to verify everything works
- Run your script by entering this command in your terminal:
python rag_bot.py
You should see two lines of output. The first shows how many pages were loaded from your PDF, and the second shows how many chunks the splitter created.
What should I see?
The exact numbers depend on your PDF. A 10-page document might show something like Loaded 10 pages from the PDF and Split into 35 chunks. The chunk count is always higher than the page count because each page gets broken into multiple smaller pieces.
Seeing an error?
- ModuleNotFoundError: Your virtual environment might not be activated. Check that you see (venv) at the start of your terminal prompt. If not, activate it again with .\venv\Scripts\activate (Windows) or source venv/bin/activate (macOS/Linux).
- FileNotFoundError for sample.pdf: Make sure your PDF file is named exactly sample.pdf and is placed directly in the pdf-rag-bot folder, not inside a subfolder.
- SyntaxError: Double check that your code matches the full file in the double-check tab above. A missing parenthesis or quotation mark can cause this.
Help me debug this error when running my RAG bot script
Your PDF is loaded and chunked. Next up, you'll turn those chunks into embeddings and store them in a vector database so your bot can search through them.
Embed and Store in ChromaDB
Your PDF is loaded and split into chunks. Right now, those chunks are just raw text sitting in memory with no way to search them by meaning.
To make them searchable, you need to convert each chunk into a numerical vector called an embedding. Gemini's embedding model handles the conversion, and ChromaDB stores the vectors locally on your machine.
In this step, get ready to:
- Create embeddings for your PDF chunks using Gemini's embedding model.
- Store the embedded chunks in a local ChromaDB vector database.
Create the embeddings model and vector store
This is the indexing step of your RAG pipeline. Each chunk gets converted into a numerical vector that captures its meaning, and ChromaDB stores those vectors so you can search them later.
- Add the embeddings and vector store code to rag_bot.py by copying the following below your existing code:
# --- Embed and store in ChromaDB ---
embeddings = GoogleGenerativeAIEmbeddings(model="models/gemini-embedding-001")
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db",
collection_name="pdf_rag",
)
print(f"Stored {len(chunks)} chunks in ChromaDB")
What does this code do?
- GoogleGenerativeAIEmbeddings creates an instance of Gemini's embedding model. This model converts text into numerical vectors. Similar text produces vectors that are close together, which is how the system finds relevant chunks later.
- Chroma.from_documents() takes your chunks, runs each one through the embedding model, and stores the resulting vectors in a local database.
- persist_directory="./chroma_db" saves the database to a folder on disk. Without this, your vectors would vanish when the script ends.
- collection_name="pdf_rag" names this group of vectors inside the database, like naming a table in a SQL database.
- Save rag_bot.py.
You will run the script in the next substep to verify everything works.
Code not looking right?
Make sure the new code starts on the line directly after print(f"Split into {len(chunks)} chunks"). Leave one blank line between the print statement and the new comment.
Check that the indentation inside Chroma.from_documents() matches the example exactly. Python is sensitive to whitespace.
Help me check my rag_bot.py embeddings and vector store code
✔️ Awesome, I've got everything!
Double check you have saved rag_bot.py before moving on.
ⓧ I'd like to double check the full code
Here is what your complete rag_bot.py should look like at this point.
import os
from dotenv import load_dotenv
from langchain_google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
# Load environment variables
load_dotenv()
# --- Load the PDF ---
loader = PyPDFLoader("sample.pdf")
docs = loader.load()
print(f"Loaded {len(docs)} pages from the PDF")
# --- Split into chunks ---
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
)
chunks = text_splitter.split_documents(docs)
print(f"Split into {len(chunks)} chunks")
# --- Embed and store in ChromaDB ---
embeddings = GoogleGenerativeAIEmbeddings(model="models/gemini-embedding-001")
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db",
collection_name="pdf_rag",
)
print(f"Stored {len(chunks)} chunks in ChromaDB")
Run the script and verify the vector store was created
- Run your script by entering this command in your terminal:
python rag_bot.py
- Check your terminal output. You should see three lines: the page count, the chunk count, and a confirmation that chunks were stored in ChromaDB.
What should I see?
Your terminal output will look something like this (the exact numbers depend on your PDF):
Loaded 12 pages from the PDF
Split into 34 chunks
Stored 34 chunks in ChromaDB
The third line is the new one. It confirms that all your chunks were converted into embeddings and saved to the local database.
- Confirm the vector database folder was created by running:
ls
- You should see a new chroma_db folder in the output alongside rag_bot.py, sample.pdf, and your .env file. This folder IS your vector database.
Seeing an error instead?
If you see Invalid API key or an authentication error, open your .env file and make sure your GOOGLE_API_KEY is pasted correctly with no extra spaces or quotes around it.
If you see ModuleNotFoundError, your virtual environment might not be activated. Run .\venv\Scripts\activate (Windows) or source venv/bin/activate (macOS/Linux) and try again.
If the script hangs for a long time, it may be embedding a large PDF. Give it a minute. The embedding step sends each chunk to Gemini's API, so it depends on your internet connection and PDF size.
Help me debug my rag_bot.py script
Your PDF's knowledge is now indexed and searchable. Next up, you will build the RAG chain that retrieves relevant chunks and generates answers using Gemini.
Build the LCEL RAG Chain and Query Your PDF
Your PDF is loaded, chunked, and stored as vectors in ChromaDB. The indexing half of your RAG pipeline is done.
Now it's time to build the brain of your bot. You'll wire a retriever, a prompt, and Gemini together into a single chain that answers questions from your PDF.
In this step, get ready to:
- Create a retriever, LLM, and prompt template.
- Build the LCEL RAG chain with the pipe operator.
- Test the bot by asking questions about your PDF.
Create the retriever, LLM, and prompt template
Before you can build the chain, you need three building blocks. A retriever searches your vector store for relevant chunks. An LLM generates the answer. A prompt template tells the LLM how to use those chunks.
- In rag_bot.py, find the line that says print(f"Stored {len(chunks)} chunks in ChromaDB"), then copy the following code below it:
# --- Create the retriever ---
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 4},
)
# --- Set up the LLM ---
llm = ChatGoogleGenerativeAI(
model="gemini-2.5-flash",
temperature=0.2,
)
# --- Create the prompt template ---
prompt = ChatPromptTemplate.from_template(
"""You are a helpful assistant that answers questions based on the provided context.
Use only the information from the context to answer.
If the context does not contain enough information to answer, say so honestly.
Context:
{context}
Question: {question}
Answer:"""
)
What does this code do?
- retriever wraps your vector store in a search interface. When you pass it a question, it finds the 4 most similar chunks (k=4) using cosine similarity.
- llm is the Gemini 2.5 Flash model. The low temperature=0.2 keeps answers factual instead of creative.
- prompt is the instruction template with two placeholder variables: {context} (where the retrieved chunks go) and {question} (where the user's question goes). It also tells the LLM to admit when it doesn't have enough information.
- Save rag_bot.py.
You won't see a change yet. The next chunk wires these components into a working chain.
Seeing a red underline on ChatGoogleGenerativeAI?
- Make sure the import line at the top of your file reads from langchain_google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings. Both classes come from the same import.
- Confirm your virtual environment is still active. You should see (venv) at the start of your terminal prompt.
- My code editor shows an error on the LLM or prompt setup
Build the LCEL chain with the pipe operator
LCEL stands for LangChain Expression Language. It lets you connect components using the | pipe operator, the same way you pipe commands in a terminal.
- In rag_bot.py, add the following code below the prompt template you just created:
# --- Format retrieved documents into a single string ---
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
# --- Build the LCEL RAG chain ---
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
What does this chain do?
The chain reads from left to right, just like a pipeline. Here's what each piece does:
- format_docs is a helper function that takes the list of retrieved Document objects and joins their text into one big string.
- The dictionary {"context": ..., "question": ...} runs two things in parallel. retriever | format_docs fetches relevant chunks and formats them. RunnablePassthrough() forwards the user's question unchanged.
- prompt fills in the template with the context and question. Then llm generates the answer. Finally, StrOutputParser() extracts just the text from the LLM's response object.
Why pipes instead of function calls?
LangChain used to use chain classes like LLMChain and SequentialChain. Those are now deprecated.
LCEL replaces them with the | pipe operator. Every component in the chain automatically gets invoke(), stream(), and batch() methods for free. Every LangChain project you build from here uses this same syntax.
- Save rag_bot.py.
You won't see a change yet. The next chunk adds the query loop so you can actually talk to your bot.
Seeing a syntax error in the chain?
- Check that rag_chain starts with an opening parenthesis ( on the same line as the = sign. The closing ) goes on its own line after StrOutputParser().
- Make sure each | pipe is at the start of a new line, indented inside the parentheses.
- Help me fix a syntax error in my LCEL chain
Add an interactive query loop and test the bot
The chain is built, but there's no way for a user to interact with it yet. A simple while True loop turns your script into an interactive Q&A session.
- In rag_bot.py, add the following code below the rag_chain you just created:
# --- Interactive query loop ---
print("\n--- PDF Q&A Bot Ready! ---")
print("Type 'quit' to exit.\n")
while True:
question = input("Ask a question: ")
if question.lower() == "quit":
break
answer = rag_chain.invoke(question)
print(f"\nAnswer: {answer}\n")
What does this code do?
- The while True loop keeps the bot running until you type quit.
- rag_chain.invoke(question) sends your question through the entire pipeline: retrieval, formatting, prompting, generation, and parsing. One call does it all.
- Save rag_bot.py.
- Run the bot by entering this command in your terminal:
python rag_bot.py
You should see the page count, chunk count, and storage confirmation from the previous steps. Then you should see --- PDF Q&A Bot Ready! --- followed by an Ask a question: prompt.
- Ask a specific question about your PDF. For example, if your PDF is a report, try something like What are the key findings?.
- Try asking a question your PDF cannot answer, like What is the capital of France?. The bot should tell you it doesn't have enough information.
- Type quit to exit the bot when you're done testing.
Bot not responding or showing an error?
- If you see Invalid API key or an authentication error, open your .env file and double check that GOOGLE_API_KEY is set to your actual API key with no extra spaces or quotes around it.
- If the bot hangs for a long time, the Gemini free tier may be rate-limiting your requests. Wait a minute and try again.
- If you see ModuleNotFoundError, make sure your virtual environment is active. You should see (venv) in your terminal prompt.
- Help me debug my PDF Q&A bot
✔️ Awesome, I've got everything!
Make sure you've saved rag_bot.py and your bot is answering questions from your PDF.
ⓧ I'd like to double check the full code
Here is the full rag_bot.py file. Compare it against yours to spot any differences.
import os
from dotenv import load_dotenv
from langchain_google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
# Load environment variables
load_dotenv()
# --- Load the PDF ---
loader = PyPDFLoader("sample.pdf")
docs = loader.load()
print(f"Loaded {len(docs)} pages from the PDF")
# --- Split into chunks ---
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
)
chunks = text_splitter.split_documents(docs)
print(f"Split into {len(chunks)} chunks")
# --- Embed and store in ChromaDB ---
embeddings = GoogleGenerativeAIEmbeddings(model="models/gemini-embedding-001")
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db",
collection_name="pdf_rag",
)
print(f"Stored {len(chunks)} chunks in ChromaDB")
# --- Create the retriever ---
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 4},
)
# --- Set up the LLM ---
llm = ChatGoogleGenerativeAI(
model="gemini-2.5-flash",
temperature=0.2,
)
# --- Create the prompt template ---
prompt = ChatPromptTemplate.from_template(
"""You are a helpful assistant that answers questions based on the provided context.
Use only the information from the context to answer.
If the context does not contain enough information to answer, say so honestly.
Context:
{context}
Question: {question}
Answer:"""
)
# --- Format retrieved documents into a single string ---
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
# --- Build the LCEL RAG chain ---
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# --- Interactive query loop ---
print("\n--- PDF Q&A Bot Ready! ---")
print("Type 'quit' to exit.\n")
while True:
question = input("Ask a question: ")
if question.lower() == "quit":
break
answer = rag_chain.invoke(question)
print(f"\nAnswer: {answer}\n")
Your bot answers questions, but it doesn't tell you where those answers came from. The Secret Mission adds source citations so you can trace every response back to specific pages in your PDF.
Secret mission
Add Source Citations to Every Answer
Your bot answers questions about your PDF, but it never tells you where those answers came from. Add source citations that show the exact PDF page numbers and text previews behind every answer, so you can trace and verify what the bot says.
Before you go
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with the Gemini free tier, so there are no ongoing costs.
Resources you used:
- pdf-rag-bot project folder with Python virtual environment.
- rag_bot.py script and .env file containing your Google API key.
- chroma_db folder (your local ChromaDB vector database).
- sample.pdf (your test PDF document).
- Google API key from Google AI Studio.
Keep everything running
No action needed. Choose this if you are still actively experimenting with different PDFs or want to modify the bot further.
- Your project folder, virtual environment, and ChromaDB data all stay on your machine.
- Your Google API key remains active and free to use within the Gemini free tier limits.
Pause - I'll come back to this later
Deactivate the virtual environment to free up your terminal session. Your files and data stay on disk for when you return.
- Deactivate your virtual environment by running:
deactivate
- When you come back, navigate to your pdf-rag-bot folder and reactivate the virtual environment with .\venv\Scripts\activate (Windows) or source venv/bin/activate (macOS/Linux).
- Your Google API key stays active. No changes needed there.
Delete - I don't want to use this again
Remove all project files and optionally revoke your API key for a clean slate.
- Delete the entire pdf-rag-bot folder from your Desktop. This removes your script, virtual environment, ChromaDB data, and .env file all at once.
- To revoke your Google API key, go to Google AI Studio and delete the key you created for this project.
Mission accomplished
That's a Wrap!
Nice work! You just built a complete RAG pipeline from scratch using LangChain and Gemini. Your terminal bot can take any PDF, understand its contents, and answer questions grounded in the actual document.
Here's what you learned:
- Loaded a PDF with PyPDFLoader, chunked it with RecursiveCharacterTextSplitter, and stored the embedded chunks in a local ChromaDB vector database using Gemini embeddings.
- Built a complete RAG chain using LCEL pipe syntax that wires a retriever, prompt template, LLM, and output parser together with the | operator.
- Created an interactive terminal bot that answers questions using only the content from your PDF, proving the full retrieval-augmented generation workflow end to end.
- Added source citations that show which PDF pages each answer came from.
Every LangChain RAG project you build from here uses these same components. The loaders, splitters, embeddings, and vector stores might change for different file types or databases, but the LCEL chain pattern stays the same.
Ready to quiz yourself?