Build a Multi-Source RAG Chatbot
Build a LangChain chatbot that retrieves answers from PDFs, web pages, and markdown.
Introduction
30 Second Summary
Imagine having a pile of PDFs, bookmarked web pages, and markdown notes scattered across your computer. You need answers fast, but searching through each document manually takes forever.
In this project, you will build a conversational chatbot that pulls knowledge from multiple document types and answers your questions with source citations. The chatbot uses LangChain to load, chunk, and embed documents into a local vector store, then retrieves the most relevant passages to generate grounded answers.
What You'll Build
A terminal-based RAG chatbot that ingests PDFs, web pages, and markdown files into a ChromaDB vector store, then answers questions using Groq's Llama 3 model with conversation memory and source citations.
By the end of this project, you'll have:
- A document ingestion pipeline that loads PDFs, web pages, and markdown files using LangChain's document loaders.
- A persistent ChromaDB vector store with chunked and embedded documents powered by free HuggingFace embeddings.
- A conversational RAG chain that reformulates queries based on chat history and displays answers with source citations.
- Secret Mission: Add OpenRouter as a swappable LLM provider and load CSV documents as a 4th source type.
Are there any prerequisites?
You should have basic Python knowledge and some familiarity with RAG concepts (like embeddings and vector stores). You will also need a free Groq API key and VS Code installed on your machine.
Before We Start
Before We Start...
Set Up Your Project Environment
Before you can load documents and build a retrieval chain, you need a workspace with the right tools installed.
In this step, you will create a Python project, install all the LangChain packages your chatbot needs, configure your Groq API key, and prepare sample documents for ingestion.
In this step, get ready to:
- Create a project folder with a Python virtual environment and install all required packages.
- Configure your Groq API key and verify the connection.
- Prepare sample documents (markdown, PDF) for your chatbot to learn from.
Install Python packages
Your chatbot depends on over ten packages working together. A virtual environment keeps these isolated from your system Python so nothing conflicts.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir rag-chatbot
cd rag-chatbot
- Create a virtual environment by running this command:
python3 -m venv venv
- Activate the virtual environment:
macOS
source venv/bin/activate
Windows
venv\Scripts\activate
Linux
source venv/bin/activate
You should see (venv) appear at the beginning of your terminal prompt.
Don't see (venv) in your prompt?
On Windows, if you get an execution policy error, run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned in PowerShell first, then try the activate command again.
On macOS, make sure you used python3 (not python) when creating the venv.
help me activate my Python virtual environment
Now you will define all the packages your project needs in a requirements file, then install them in one go.
- Create a file called requirements.txt in your rag-chatbot folder and paste this content into it:
langchain>=1.2.0
langchain-groq>=1.2.0
langchain-huggingface>=1.2.0
langchain-chroma>=1.1.0
langchain-community>=0.3.0
langchain-text-splitters>=1.1.0
beautifulsoup4>=4.12.0
unstructured>=0.15.0
chromadb>=0.5.0
sentence-transformers>=3.0.0
python-dotenv>=1.0.0
What are these packages?
- langchain is the core framework that orchestrates all the components.
- langchain-groq connects to Groq's fast LLM inference API.
- langchain-huggingface runs the embedding model locally on your machine (no API key needed).
- langchain-chroma provides the local vector database for storing and searching embeddings.
- sentence-transformers is the underlying model library that powers the embeddings.
- Install all the packages by running this command:
pip install -r requirements.txt
This will take 1-2 minutes as it downloads all the packages and their dependencies. You should see installation progress ending with Successfully installed followed by a list of package names.
Installation failing?
If unstructured fails to install (common on Windows), try installing it separately with pip install unstructured. If it still fails, you can continue. In Step 2, you will use an alternative PDF loader that does not depend on it.
help me fix pip install errors for my LangChain RAG project
Configure your environment variables
Your chatbot needs your Groq API key to call the LLM. Storing it in a .env file keeps the key out of your source code.
- Create a file called .env in your rag-chatbot folder and add this line:
GROQ_API_KEY=your_groq_api_key_here
- Replace your_groq_api_key_here with your actual Groq API key.
Where do I find my Groq API key?
Go to console.groq.com/keys and create a new API key. Copy it immediately because Groq only shows it once.
Now create the main application file. This file will grow throughout the project, but you will start with the imports and initial configuration.
- Create a file called app.py in your rag-chatbot folder and paste in this code:
import os
from dotenv import load_dotenv
from langchain_groq import ChatGroq
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma
from langchain_community.document_loaders import (
UnstructuredPDFLoader,
WebBaseLoader,
UnstructuredMarkdownLoader,
)
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.chains.retrieval import create_retrieval_chain
from langchain.chains.history_aware_retriever import create_history_aware_retriever
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
# Load environment variables
load_dotenv()
# --- Step 1: Initialize LLM and Embeddings ---
llm = ChatGroq(
model="llama-3.3-70b-versatile",
temperature=0,
max_tokens=1024,
)
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
What does this code do?
- load_dotenv() reads your .env file and makes GROQ_API_KEY available as an environment variable.
- ChatGroq connects to Groq's inference API using the Llama 3.3 70B model. The temperature=0 setting makes responses deterministic and factual.
- HuggingFaceEmbeddings loads the all-MiniLM-L6-v2 model locally. This converts text into numerical vectors for similarity search, with zero API cost.
- Save app.py.
- Verify your Groq connection by running this test command in your terminal:
python -c "from app import llm; print(llm.invoke('Say hello in exactly one word').content)"
You should see a one-word greeting like Hello printed in the terminal. The first time you run this, it also downloads the embedding model (~80MB), so expect a brief delay.
Seeing an authentication error?
If you see AuthenticationError, double-check that your API key in .env has no extra spaces or quotation marks around it. The line should be GROQ_API_KEY=gsk_... with no quotes.
If you see ModuleNotFoundError, make sure your virtual environment is active (you should see (venv) in your prompt).
help me fix my Groq API connection in LangChain
Prepare your sample documents
Your chatbot needs documents to learn from. You will give it three types of sources: a markdown file, a PDF, and a web page.
- Create a docs/ folder inside rag-chatbot by running:
mkdir docs
- Create a file called sample.md inside the docs/ folder and paste in this content:
# LangChain Overview
LangChain is a framework for developing applications powered by large language models (LLMs).
## Key Features
- **Document Loaders**: Load data from various sources including PDFs, web pages, databases, and APIs.
- **Text Splitters**: Break large documents into smaller chunks optimized for embedding and retrieval.
- **Vector Stores**: Store and search document embeddings using databases like Chroma, Pinecone, or FAISS.
- **Chains**: Compose multiple LLM calls and tools into reusable workflows.
- **Agents**: Create autonomous systems that can use tools and make decisions.
## Use Cases
LangChain is commonly used for:
1. Question answering over documents (RAG)
2. Chatbots with memory
3. Summarization pipelines
4. Data extraction from unstructured text
## Architecture
A typical LangChain RAG application follows this flow:
Documents -> Loader -> Splitter -> Embeddings -> Vector Store -> Retriever -> LLM -> Answer
What does this markdown file do?
This file gives your chatbot knowledge about LangChain itself. Later, you will ask the chatbot questions like "What are LangChain's key features?" and it will retrieve answers from this document.
- Save docs/sample.md.
Next, you need a PDF file for your chatbot to ingest. Any short PDF will work.
- Find any short PDF document on your computer (a report, a guide, or a research paper) and copy it into the docs/ folder.
- Rename the file to sample.pdf.
Need a sample PDF?
Search for "LangChain tutorial PDF" or "Python cheat sheet PDF" online to find a free, short document you can download. Any 1-5 page PDF works perfectly for testing.
The third source type is a web page. You will not download it now. Instead, the code in Step 2 will scrape it live using its URL.
- Note this URL for later use in Step 2: https://python.langchain.com/docs/concepts/retrieval/.
- Verify your folder structure by running:
ls docs/
You should see sample.md and sample.pdf listed in the output.
✔️ Awesome, I've got everything!
Make sure you have saved both app.py and requirements.txt.
ⓧ I'd like to double check the full code
Here is the full app.py so far:
import os
from dotenv import load_dotenv
from langchain_groq import ChatGroq
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma
from langchain_community.document_loaders import (
UnstructuredPDFLoader,
WebBaseLoader,
UnstructuredMarkdownLoader,
)
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.chains.retrieval import create_retrieval_chain
from langchain.chains.history_aware_retriever import create_history_aware_retriever
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
# Load environment variables
load_dotenv()
# --- Step 1: Initialize LLM and Embeddings ---
llm = ChatGroq(
model="llama-3.3-70b-versatile",
temperature=0,
max_tokens=1024,
)
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
Here is the full requirements.txt:
langchain>=1.2.0
langchain-groq>=1.2.0
langchain-huggingface>=1.2.0
langchain-chroma>=1.1.0
langchain-community>=0.3.0
langchain-text-splitters>=1.1.0
beautifulsoup4>=4.12.0
unstructured>=0.15.0
chromadb>=0.5.0
sentence-transformers>=3.0.0
python-dotenv>=1.0.0
Here is the full .env:
GROQ_API_KEY=your_groq_api_key_here
Here is the full docs/sample.md:
# LangChain Overview
LangChain is a framework for developing applications powered by large language models (LLMs).
## Key Features
- **Document Loaders**: Load data from various sources including PDFs, web pages, databases, and APIs.
- **Text Splitters**: Break large documents into smaller chunks optimized for embedding and retrieval.
- **Vector Stores**: Store and search document embeddings using databases like Chroma, Pinecone, or FAISS.
- **Chains**: Compose multiple LLM calls and tools into reusable workflows.
- **Agents**: Create autonomous systems that can use tools and make decisions.
## Use Cases
LangChain is commonly used for:
1. Question answering over documents (RAG)
2. Chatbots with memory
3. Summarization pipelines
4. Data extraction from unstructured text
## Architecture
A typical LangChain RAG application follows this flow:
Documents -> Loader -> Splitter -> Embeddings -> Vector Store -> Retriever -> LLM -> Answer
Your environment is ready. You have a working LLM connection, local embeddings, and sample documents waiting to be ingested. Next up, you will write the code that loads all three document types into LangChain.
Load Documents from Multiple Sources
Your environment is locked and loaded. Your LLM talks to Groq, your embeddings run locally, and all your packages are installed.
Now it is time to feed your chatbot some knowledge. A RAG system is only as good as the documents it can search through. In this step, you will build a document ingestion pipeline that pulls content from three different source types: a PDF file, a live web page, and a local markdown file.
In this step, get ready to:
- Load a PDF file into LangChain Document objects.
- Scrape a web page and convert it into searchable documents.
- Load a markdown file and combine all sources into a unified document list.
Load a PDF document
LangChain uses document loaders to convert raw files into structured Document objects. Each Document has two fields: page_content (the text) and metadata (a dictionary of information about where the text came from).
- In app.py, find the line that says embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2").
- Below it, add the following code to start building your document loading function:
# --- Step 2: Load Documents from Multiple Sources ---
def load_documents():
all_docs = []
# Load PDF
pdf_path = "./docs/sample.pdf"
if os.path.exists(pdf_path):
pdf_loader = UnstructuredPDFLoader(pdf_path)
pdf_docs = pdf_loader.load()
for doc in pdf_docs:
doc.metadata["source_type"] = "pdf"
all_docs.extend(pdf_docs)
print(f"Loaded {len(pdf_docs)} document(s) from PDF")
What does this code do?
You are defining a load_documents() function that will hold all your loading logic. The all_docs list will collect documents from every source.
The PDF section uses UnstructuredPDFLoader to parse the binary PDF into text. The os.path.exists() check prevents your script from crashing if the file is missing. After loading, a loop tags each document with source_type: pdf so you can trace answers back to their origin later.
You will not see a change yet. The next chunk adds web loading to the same function.
Load a web page
Your chatbot should not be limited to local files. WebBaseLoader uses BeautifulSoup under the hood to scrape a live URL and extract its text content.
- In app.py, find the print(f"Loaded {len(pdf_docs)} document(s) from PDF") line inside your load_documents() function.
- Below it (still inside the function, at the same indentation level as the PDF section), add this code:
# Load Web Page
web_url = "https://python.langchain.com/docs/concepts/retrieval/"
web_loader = WebBaseLoader(web_url)
web_docs = web_loader.load()
for doc in web_docs:
doc.metadata["source_type"] = "web"
all_docs.extend(web_docs)
print(f"Loaded {len(web_docs)} document(s) from web")
What does this code do?
WebBaseLoader fetches the HTML from the given URL and strips it down to plain text. Unlike the PDF loader, there is no file-existence check because the URL is hardcoded and always available online.
The same tagging pattern applies. Each web document gets source_type: web in its metadata so the chatbot can cite where it found the answer.
You still will not see a change yet. The next chunk completes the function with markdown loading.
Load a markdown file and combine all documents
Markdown is the most common format for local knowledge bases, READMEs, and documentation. This final loader reads your sample.md file and returns it as a single Document.
- In app.py, find the print(f"Loaded {len(web_docs)} document(s) from web") line.
- Below it (still inside load_documents()), add this code to load the markdown file and return the combined list:
# Load Markdown
md_path = "./docs/sample.md"
if os.path.exists(md_path):
md_loader = UnstructuredMarkdownLoader(md_path, mode="single", strategy="fast")
md_docs = md_loader.load()
for doc in md_docs:
doc.metadata["source_type"] = "markdown"
all_docs.extend(md_docs)
print(f"Loaded {len(md_docs)} document(s) from markdown")
return all_docs
What does this code do?
UnstructuredMarkdownLoader parses the markdown structure into plain text. The mode="single" parameter combines all sections into one Document instead of splitting by heading. The strategy="fast" parameter skips heavy processing for faster loading.
Finally, return all_docs hands the combined list back to whatever calls this function. You now have PDF, web, and markdown documents all tagged and ready for the next step.
- Save app.py.
- To verify the function works, temporarily add this test code at the very bottom of app.py:
# Temporary test - remove after verifying
if __name__ == "__main__":
docs = load_documents()
print(f"\nTotal documents loaded: {len(docs)}")
for doc in docs:
print(f"Source type: {doc.metadata['source_type']}")
print(f"Content preview: {doc.page_content[:100]}")
print("---")
- Save the file, then run your script by entering this command in your terminal:
python app.py
You should see output showing documents loaded from each source, with their source_type metadata and a preview of the content text.
What should I see?
Your terminal should print something like:
- Loaded 1 document(s) from PDF (the number may vary depending on your PDF).
- Loaded 1 document(s) from web followed by a content preview from the LangChain retrieval docs page.
- Loaded 1 document(s) from markdown followed by text from your sample.md file.
- After confirming the output looks correct, remove the temporary test block (the if __name__ == "__main__": section) from the bottom of app.py and save the file.
Seeing an error when loading the PDF?
If you see an error mentioning unstructured or missing dependencies, the unstructured package may need additional system libraries on Windows. You can swap to a lighter PDF loader by replacing UnstructuredPDFLoader with PyPDFLoader from langchain_community.document_loaders. Install it with pip install pypdf.
If the web loader fails with a connection error, check that you have internet access and that the URL is correct.
help me debug my document loading errors
Why tag documents with source_type metadata?
When your chatbot answers a question, you want it to say where the answer came from. Adding source_type to each document's metadata means you can later display citations like "(from PDF)" or "(from web)" alongside answers. This is what makes a RAG chatbot trustworthy.
✔️ Awesome, I've got everything!
Great. Double check you have saved app.py and removed the temporary test block.
ⓧ I'd like to double check the full code
Here is the full app.py file up to this point:
import os
from dotenv import load_dotenv
from langchain_groq import ChatGroq
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma
from langchain_community.document_loaders import (
UnstructuredPDFLoader,
WebBaseLoader,
UnstructuredMarkdownLoader,
)
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.chains.retrieval import create_retrieval_chain
from langchain.chains.history_aware_retriever import create_history_aware_retriever
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
# Load environment variables
load_dotenv()
# --- Step 1: Initialize LLM and Embeddings ---
llm = ChatGroq(
model="llama-3.3-70b-versatile",
temperature=0,
max_tokens=1024,
)
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
# --- Step 2: Load Documents from Multiple Sources ---
def load_documents():
all_docs = []
# Load PDF
pdf_path = "./docs/sample.pdf"
if os.path.exists(pdf_path):
pdf_loader = UnstructuredPDFLoader(pdf_path)
pdf_docs = pdf_loader.load()
for doc in pdf_docs:
doc.metadata["source_type"] = "pdf"
all_docs.extend(pdf_docs)
print(f"Loaded {len(pdf_docs)} document(s) from PDF")
# Load Web Page
web_url = "https://python.langchain.com/docs/concepts/retrieval/"
web_loader = WebBaseLoader(web_url)
web_docs = web_loader.load()
for doc in web_docs:
doc.metadata["source_type"] = "web"
all_docs.extend(web_docs)
print(f"Loaded {len(web_docs)} document(s) from web")
# Load Markdown
md_path = "./docs/sample.md"
if os.path.exists(md_path):
md_loader = UnstructuredMarkdownLoader(md_path, mode="single", strategy="fast")
md_docs = md_loader.load()
for doc in md_docs:
doc.metadata["source_type"] = "markdown"
all_docs.extend(md_docs)
print(f"Loaded {len(md_docs)} document(s) from markdown")
return all_docs
Your documents are loaded and tagged. Next up, you will split them into bite-sized chunks and embed them into a vector store so your chatbot can actually search through them.
Chunk Documents and Build the Vector Store
In the previous step, you loaded documents from three different source types into a single list. Your chatbot now has raw content from a PDF, a web page, and a markdown file.
Raw documents are too large to send directly to an LLM. They need to be broken into small, overlapping chunks and converted into numerical representations (embeddings) that a search engine can query. In this step, you will split your documents into chunks and store them in a ChromaDB vector store for fast retrieval.
In this step, get ready to:
- Split loaded documents into overlapping chunks using RecursiveCharacterTextSplitter.
- Embed chunks and store them in a persistent ChromaDB vector store.
- Test retrieval by querying the vector store and inspecting returned documents.
Split documents into chunks
LLMs have limited context windows. Sending an entire web page or PDF in one shot wastes tokens and dilutes relevance. A text splitter breaks documents into smaller pieces so the retriever can return only the most relevant passages.
- In app.py, scroll to the bottom of the file (below the load_documents() function).
- Add the section comment and the first part of the create_vector_store function by copying this code:
# --- Step 3: Chunk and Index Documents ---
def create_vector_store(documents):
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", ".", " ", ""],
)
chunks = text_splitter.split_documents(documents)
print(f"Split into {len(chunks)} chunks")
What does this code do?
RecursiveCharacterTextSplitter tries each separator in order, starting with double newlines (paragraph breaks), then single newlines, then periods, then spaces. This keeps paragraphs and sentences intact wherever possible.
- chunk_size=500 means each chunk can be at most 500 characters.
- chunk_overlap=50 repeats the last 50 characters at the start of the next chunk. This prevents cutting context mid-thought.
- split_documents() returns new Document objects that preserve the original metadata (like source_type).
Create embeddings and build the vector store
Chunks are still plain text. To make them searchable by meaning, you need to convert each chunk into a numerical vector (an embedding) and store those vectors in a database optimized for similarity search.
- Still inside the create_vector_store function, add the following code below the print(f"Split into {len(chunks)} chunks") line:
vector_store = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db",
collection_name="multi_source_rag",
)
print("Vector store created and persisted to ./chroma_db")
return vector_store
What does this code do?
Chroma.from_documents() takes your chunks, runs each one through the embeddings model (the HuggingFace all-MiniLM-L6-v2 you initialized in Step 1), and stores the resulting vectors in a local ChromaDB database.
- persist_directory="./chroma_db" saves the database to disk so you do not need to re-embed every time you run the app.
- collection_name="multi_source_rag" names the collection inside ChromaDB. Think of it like a table name in a regular database.
Why ChromaDB over other vector stores?
ChromaDB runs locally with zero configuration. It persists to disk by default, requires no external service or API key, and has first-class LangChain support through the langchain-chroma package. For a learning project, this eliminates setup friction entirely.
- Save app.py.
Seeing indentation errors?
The vector store code must be indented inside the create_vector_store function (4 spaces). Make sure the vector_store = Chroma.from_documents(...) line aligns with the text_splitter line above it.
help me fix an indentation error in my create_vector_store function
Test retrieval from the vector store
The function is written but has not run yet. You need to call it, then query the resulting vector store to confirm that relevant chunks come back with the correct metadata.
- At the very bottom of app.py (outside any function), add this temporary test code:
# Temporary test code - remove after verifying
if __name__ == "__main__":
docs = load_documents()
vector_store = create_vector_store(docs)
retriever = vector_store.as_retriever(search_kwargs={"k": 3})
results = retriever.invoke("What is LangChain used for?")
for doc in results:
print(f"[{doc.metadata.get('source_type')}] {doc.page_content[:100]}...")
What does this test do?
- load_documents() pulls in all your source files from the previous step.
- create_vector_store(docs) splits, embeds, and stores everything in ChromaDB.
- .as_retriever(search_kwargs={"k": 3}) converts the vector store into a retriever that returns the 3 most similar chunks for any query.
- retriever.invoke() sends a test question and returns matching Document objects with their metadata.
- Run the test by executing this command in your terminal:
python app.py
The first run will download the all-MiniLM-L6-v2 embedding model (~80MB). This only happens once.
You should see output showing documents loaded, chunks created, and 3 retrieved results with their source types and content snippets.
What should I see?
Your output will look something like this (exact numbers may vary depending on your PDF):
Loaded 1 document(s) from PDF
Loaded 1 document(s) from web
Loaded 1 document(s) from markdown
Split into 42 chunks
Vector store created and persisted to ./chroma_db
[markdown] LangChain is a framework for developing applications powered by large la...
[web] Retrieval is a core concept in LangChain that allows you to...
[markdown] LangChain is commonly used for: 1. Question answering over docum...
Seeing a SQLite version warning?
ChromaDB may show a SQLite version warning on some systems. This is a non-fatal warning and your vector store still works correctly. You can safely ignore it.
If you get an error about unstructured or PDF loading failing, check that all packages from requirements.txt installed successfully. On Windows, you can replace UnstructuredPDFLoader with PyPDFLoader as a lighter alternative.
help me debug vector store creation errors
- Confirm you see results tagged with [markdown] or [web] or [pdf] in the output. This proves the metadata survived chunking and embedding.
- Check that a chroma_db folder appeared in your project directory by running:
ls chroma_db
You should see files inside the folder. This is your persisted vector database on disk.
- Now remove the temporary test code from the bottom of app.py. Delete everything from the # Temporary test code comment to the end of the file.
- Save app.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file and removed the temporary test code.
ⓧ I'd like to double check the full code
Here is the full app.py at the end of this step:
import os
from dotenv import load_dotenv
from langchain_groq import ChatGroq
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma
from langchain_community.document_loaders import (
UnstructuredPDFLoader,
WebBaseLoader,
UnstructuredMarkdownLoader,
)
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.chains.retrieval import create_retrieval_chain
from langchain.chains.history_aware_retriever import create_history_aware_retriever
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
# Load environment variables
load_dotenv()
# --- Step 1: Initialize LLM and Embeddings ---
llm = ChatGroq(
model="llama-3.3-70b-versatile",
temperature=0,
max_tokens=1024,
)
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
# --- Step 2: Load Documents from Multiple Sources ---
def load_documents():
all_docs = []
# Load PDF
pdf_path = "./docs/sample.pdf"
if os.path.exists(pdf_path):
pdf_loader = UnstructuredPDFLoader(pdf_path)
pdf_docs = pdf_loader.load()
for doc in pdf_docs:
doc.metadata["source_type"] = "pdf"
all_docs.extend(pdf_docs)
print(f"Loaded {len(pdf_docs)} document(s) from PDF")
# Load Web Page
web_url = "https://python.langchain.com/docs/concepts/retrieval/"
web_loader = WebBaseLoader(web_url)
web_docs = web_loader.load()
for doc in web_docs:
doc.metadata["source_type"] = "web"
all_docs.extend(web_docs)
print(f"Loaded {len(web_docs)} document(s) from web")
# Load Markdown
md_path = "./docs/sample.md"
if os.path.exists(md_path):
md_loader = UnstructuredMarkdownLoader(md_path, mode="single", strategy="fast")
md_docs = md_loader.load()
for doc in md_docs:
doc.metadata["source_type"] = "markdown"
all_docs.extend(md_docs)
print(f"Loaded {len(md_docs)} document(s) from markdown")
return all_docs
# --- Step 3: Chunk and Index Documents ---
def create_vector_store(documents):
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", ".", " ", ""],
)
chunks = text_splitter.split_documents(documents)
print(f"Split into {len(chunks)} chunks")
vector_store = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db",
collection_name="multi_source_rag",
)
print("Vector store created and persisted to ./chroma_db")
return vector_store
Your documents are now chunked, embedded, and stored in a searchable vector database. Next up, you will build the conversational RAG chain that queries this vector store and generates answers with full chat history.
Build the Conversational RAG Chain
You have a vector store filled with chunked documents from three different sources. The missing piece is a way to ask questions against that knowledge base and get grounded answers back.
In this step, you will build a conversational RAG chain that reformulates user questions based on chat history, retrieves relevant chunks, and generates answers with source citations.
In this step, get ready to:
- Create prompt templates for query reformulation and answer generation.
- Assemble a history-aware retrieval chain using LangChain's chain builders.
- Wrap the chain with conversation memory so it remembers previous questions.
Set up the retriever and prompts
A basic RAG pipeline retrieves documents and passes them to an LLM. A conversational RAG pipeline needs an extra step: it must first rephrase the user's latest question to make sense without the chat history.
You will create two prompt templates. One instructs the LLM to reformulate follow-up questions into standalone queries. The other instructs the LLM to answer using retrieved context and cite its sources.
- In app.py, scroll to the bottom of the file (below the create_vector_store function).
- Add the following build_rag_chain function with the retriever and both prompt templates:
# --- Step 4: Build Conversational RAG Chain ---
def build_rag_chain(vector_store):
retriever = vector_store.as_retriever(search_kwargs={"k": 3})
# Prompt for contextualizing the question based on chat history
contextualize_q_prompt = ChatPromptTemplate.from_messages([
("system",
"Given a chat history and the latest user question which might "
"reference context in the chat history, formulate a standalone "
"question which can be understood without the chat history. "
"Do NOT answer the question, just reformulate it if needed and "
"otherwise return it as is."),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
])
# Prompt for answering questions with retrieved context
qa_prompt = ChatPromptTemplate.from_messages([
("system",
"You are a helpful assistant that answers questions based on the "
"provided context. Use the following pieces of retrieved context to "
"answer the question. If you don't know the answer or the context "
"doesn't contain relevant information, say that you don't know. "
"Always cite which source document your answer came from.\n\n"
"Context:\n{context}"),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
])
What does this code do?
The function starts by converting the vector store into a retriever that returns the top 3 most relevant chunks for any query.
- contextualize_q_prompt tells the LLM to rewrite follow-up questions (like "tell me more about that") into standalone queries the retriever can understand.
- qa_prompt tells the LLM to answer strictly from the retrieved context and always cite which source document it used.
- MessagesPlaceholder("chat_history") is a slot where LangChain injects the conversation history at runtime.
You won't see output yet. The next chunk connects these prompts into a working chain.
Build the retrieval chain with chat history
LangChain provides builder functions that wire components together into a pipeline. You will use three of them to assemble the full retrieval chain.
- Directly below the qa_prompt you just added, paste the chain assembly code:
history_aware_retriever = create_history_aware_retriever(
llm, retriever, contextualize_q_prompt
)
question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)
rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)
What does this code do?
- create_history_aware_retriever wraps your retriever so it first reformulates the user's question using the contextualize prompt. This means follow-up questions like "what about its architecture?" get rewritten into standalone queries before hitting the vector store.
- create_stuff_documents_chain creates a chain that "stuffs" all retrieved documents into the prompt's {context} variable and sends it to the LLM.
- create_retrieval_chain connects the retriever to the document chain, forming the complete retrieve-then-answer pipeline.
Why this pattern instead of ConversationalRetrievalChain?
You might find older tutorials using ConversationalRetrievalChain. That class is deprecated in LangChain v1.x and moved to langchain_classic.
The modern pattern you are using here gives you explicit control over each stage of the pipeline. You can swap the retriever, change prompts, or replace the LLM without rewriting the whole chain.
You won't see output yet. The next chunk adds memory so the chain can track conversation turns.
Add conversation memory
Right now the chain can retrieve and answer, but it has no memory of what was said earlier in the conversation. You will wrap it with a session-based message history so each question-answer pair is remembered.
- Directly below the rag_chain line, add the memory management code and the return statement:
# Add message history
store = {}
def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
conversational_rag_chain = RunnableWithMessageHistory(
rag_chain,
get_session_history,
input_messages_key="input",
history_messages_key="chat_history",
output_messages_key="answer",
)
return conversational_rag_chain
What does this code do?
- store is a dictionary that holds chat histories keyed by session ID. This lets you run multiple independent conversations if needed.
- get_session_history creates a new ChatMessageHistory the first time a session ID appears, then returns the existing one on future calls.
- RunnableWithMessageHistory wraps the chain so it automatically loads and saves messages. The three key parameters tell it which fields carry the input, history, and output.
- Save app.py.
- Check that VS Code shows no red underlines or syntax errors in the build_rag_chain function.
Seeing a red underline or IndentationError?
- All code inside build_rag_chain must be indented by 4 spaces. Make sure the retriever, prompts, chain calls, and return statement all sit inside the function body.
- Check that your parentheses and brackets are balanced. Each ChatPromptTemplate.from_messages([...]) has an opening and closing square bracket inside the parentheses.
- Verify the imports at the top of your file include ChatPromptTemplate, MessagesPlaceholder, and RunnableWithMessageHistory. These were added in Step 1.
Help me debug my build_rag_chain function
✔️ Awesome, I've got everything!
Great. Double check you have saved your file.
ⓧ I'd like to double check the full code
Here is the complete app.py up to this point:
import os
from dotenv import load_dotenv
from langchain_groq import ChatGroq
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma
from langchain_community.document_loaders import (
UnstructuredPDFLoader,
WebBaseLoader,
UnstructuredMarkdownLoader,
)
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.chains.retrieval import create_retrieval_chain
from langchain.chains.history_aware_retriever import create_history_aware_retriever
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
# Load environment variables
load_dotenv()
# --- Step 1: Initialize LLM and Embeddings ---
llm = ChatGroq(
model="llama-3.3-70b-versatile",
temperature=0,
max_tokens=1024,
)
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
# --- Step 2: Load Documents from Multiple Sources ---
def load_documents():
all_docs = []
# Load PDF
pdf_path = "./docs/sample.pdf"
if os.path.exists(pdf_path):
pdf_loader = UnstructuredPDFLoader(pdf_path)
pdf_docs = pdf_loader.load()
for doc in pdf_docs:
doc.metadata["source_type"] = "pdf"
all_docs.extend(pdf_docs)
print(f"Loaded {len(pdf_docs)} document(s) from PDF")
# Load Web Page
web_url = "https://python.langchain.com/docs/concepts/retrieval/"
web_loader = WebBaseLoader(web_url)
web_docs = web_loader.load()
for doc in web_docs:
doc.metadata["source_type"] = "web"
all_docs.extend(web_docs)
print(f"Loaded {len(web_docs)} document(s) from web")
# Load Markdown
md_path = "./docs/sample.md"
if os.path.exists(md_path):
md_loader = UnstructuredMarkdownLoader(md_path, mode="single", strategy="fast")
md_docs = md_loader.load()
for doc in md_docs:
doc.metadata["source_type"] = "markdown"
all_docs.extend(md_docs)
print(f"Loaded {len(md_docs)} document(s) from markdown")
return all_docs
# --- Step 3: Chunk and Index Documents ---
def create_vector_store(documents):
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", ".", " ", ""],
)
chunks = text_splitter.split_documents(documents)
print(f"Split into {len(chunks)} chunks")
vector_store = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db",
collection_name="multi_source_rag",
)
print("Vector store created and persisted to ./chroma_db")
return vector_store
# --- Step 4: Build Conversational RAG Chain ---
def build_rag_chain(vector_store):
retriever = vector_store.as_retriever(search_kwargs={"k": 3})
# Prompt for contextualizing the question based on chat history
contextualize_q_prompt = ChatPromptTemplate.from_messages([
("system",
"Given a chat history and the latest user question which might "
"reference context in the chat history, formulate a standalone "
"question which can be understood without the chat history. "
"Do NOT answer the question, just reformulate it if needed and "
"otherwise return it as is."),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
])
history_aware_retriever = create_history_aware_retriever(
llm, retriever, contextualize_q_prompt
)
# Prompt for answering questions with retrieved context
qa_prompt = ChatPromptTemplate.from_messages([
("system",
"You are a helpful assistant that answers questions based on the "
"provided context. Use the following pieces of retrieved context to "
"answer the question. If you don't know the answer or the context "
"doesn't contain relevant information, say that you don't know. "
"Always cite which source document your answer came from.\n\n"
"Context:\n{context}"),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
])
question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)
rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)
# Add message history
store = {}
def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
conversational_rag_chain = RunnableWithMessageHistory(
rag_chain,
get_session_history,
input_messages_key="input",
history_messages_key="chat_history",
output_messages_key="answer",
)
return conversational_rag_chain
Your conversational RAG chain is fully defined. Next up, you will build an interactive chat loop that lets you ask questions and see answers with source citations in real time.
Create the Interactive Chat Interface
You have a fully functional RAG chain that retrieves context, reformulates queries, and generates answers. The missing piece is a way for you to actually talk to it.
In this step, you will build a terminal-based chat interface that ties all your components together. The interface will display the chatbot's answers alongside the source documents each answer came from.
In this step, get ready to:
- Format source citations for display alongside answers.
- Build a terminal chat loop that invokes the RAG chain.
- Test multi-turn conversations across all document sources.
Format source citations
When the RAG chain returns an answer, it also returns the retrieved documents that informed that answer. A raw document object is hard to read, so you need a helper function that extracts the useful metadata and presents it cleanly.
- In app.py, scroll to the bottom of the file (below the build_rag_chain function) and add the following code:
# --- Step 5: Interactive Chat Interface ---
def format_sources(source_docs):
if not source_docs:
return "No sources found."
sources = []
for i, doc in enumerate(source_docs, 1):
# Extract metadata fields for display
source_type = doc.metadata.get("source_type", "unknown")
source = doc.metadata.get("source", "unknown")
# Show first 100 characters as a preview snippet
snippet = doc.page_content[:100] + "..." if len(doc.page_content) > 100 else doc.page_content
sources.append(f" [{i}] ({source_type}) {source}\n \"{snippet}\"")
return "\n".join(sources)
What does this code do?
The format_sources function takes the list of retrieved Document objects and formats them into readable citation lines.
- It pulls source_type (pdf, web, or markdown) from the metadata you tagged earlier.
- It pulls the source field (file path or URL) so you know exactly where the information came from.
- It truncates each chunk to 100 characters so the terminal output stays readable.
Build the chat loop
The chat loop is the main entry point for your application. It orchestrates the full pipeline: loading documents, building the vector store, constructing the chain, and then looping to accept questions.
- Directly below the format_sources function you just added, paste the following code:
def chat():
print("Loading documents...")
documents = load_documents()
if not documents:
print("No documents loaded. Please add files to the docs/ folder.")
return
print("\nBuilding vector store...")
vector_store = create_vector_store(documents)
print("\nBuilding RAG chain...")
chain = build_rag_chain(vector_store)
# Use a fixed session ID so chat history persists within this run
session_id = "default_session"
print("\n" + "=" * 50)
print("Multi-Source RAG Chatbot Ready!")
print("Ask questions about your documents.")
print("Type 'quit' to exit.")
print("=" * 50 + "\n")
while True:
user_input = input("You: ").strip()
if not user_input:
continue
if user_input.lower() in ["quit", "exit", "q"]:
print("Goodbye!")
break
# Invoke the chain with session config for message history
response = chain.invoke(
{"input": user_input},
config={"configurable": {"session_id": session_id}},
)
print(f"\nAssistant: {response['answer']}")
print(f"\nSources:")
print(format_sources(response.get("context", [])))
print()
if __name__ == "__main__":
chat()
What does this code do?
The chat() function is the orchestrator that wires everything together.
- It calls load_documents(), create_vector_store(), and build_rag_chain() in sequence to set up the full pipeline.
- The session_id is passed to RunnableWithMessageHistory so the chain remembers all previous questions and answers within this session.
- The response dictionary contains both response['answer'] (the generated text) and response['context'] (the retrieved Document objects used to generate it).
- The if __name__ == "__main__": block ensures the chatbot starts when you run the file directly.
- Save app.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete app.py file for comparison:
import os
from dotenv import load_dotenv
from langchain_groq import ChatGroq
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma
from langchain_community.document_loaders import (
UnstructuredPDFLoader,
WebBaseLoader,
UnstructuredMarkdownLoader,
)
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.chains.retrieval import create_retrieval_chain
from langchain.chains.history_aware_retriever import create_history_aware_retriever
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
# Load environment variables
load_dotenv()
# --- Step 1: Initialize LLM and Embeddings ---
llm = ChatGroq(
model="llama-3.3-70b-versatile",
temperature=0,
max_tokens=1024,
)
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
# --- Step 2: Load Documents from Multiple Sources ---
def load_documents():
all_docs = []
# Load PDF
pdf_path = "./docs/sample.pdf"
if os.path.exists(pdf_path):
pdf_loader = UnstructuredPDFLoader(pdf_path)
pdf_docs = pdf_loader.load()
for doc in pdf_docs:
doc.metadata["source_type"] = "pdf"
all_docs.extend(pdf_docs)
print(f"Loaded {len(pdf_docs)} document(s) from PDF")
# Load Web Page
web_url = "https://python.langchain.com/docs/concepts/retrieval/"
web_loader = WebBaseLoader(web_url)
web_docs = web_loader.load()
for doc in web_docs:
doc.metadata["source_type"] = "web"
all_docs.extend(web_docs)
print(f"Loaded {len(web_docs)} document(s) from web")
# Load Markdown
md_path = "./docs/sample.md"
if os.path.exists(md_path):
md_loader = UnstructuredMarkdownLoader(md_path, mode="single", strategy="fast")
md_docs = md_loader.load()
for doc in md_docs:
doc.metadata["source_type"] = "markdown"
all_docs.extend(md_docs)
print(f"Loaded {len(md_docs)} document(s) from markdown")
return all_docs
# --- Step 3: Chunk and Index Documents ---
def create_vector_store(documents):
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", ".", " ", ""],
)
chunks = text_splitter.split_documents(documents)
print(f"Split into {len(chunks)} chunks")
vector_store = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db",
collection_name="multi_source_rag",
)
print("Vector store created and persisted to ./chroma_db")
return vector_store
# --- Step 4: Build Conversational RAG Chain ---
def build_rag_chain(vector_store):
retriever = vector_store.as_retriever(search_kwargs={"k": 3})
# Prompt for contextualizing the question based on chat history
contextualize_q_prompt = ChatPromptTemplate.from_messages([
("system",
"Given a chat history and the latest user question which might "
"reference context in the chat history, formulate a standalone "
"question which can be understood without the chat history. "
"Do NOT answer the question, just reformulate it if needed and "
"otherwise return it as is."),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
])
history_aware_retriever = create_history_aware_retriever(
llm, retriever, contextualize_q_prompt
)
# Prompt for answering questions with retrieved context
qa_prompt = ChatPromptTemplate.from_messages([
("system",
"You are a helpful assistant that answers questions based on the "
"provided context. Use the following pieces of retrieved context to "
"answer the question. If you don't know the answer or the context "
"doesn't contain relevant information, say that you don't know. "
"Always cite which source document your answer came from.\n\n"
"Context:\n{context}"),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
])
question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)
rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)
# Add message history
store = {}
def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
conversational_rag_chain = RunnableWithMessageHistory(
rag_chain,
get_session_history,
input_messages_key="input",
history_messages_key="chat_history",
output_messages_key="answer",
)
return conversational_rag_chain
# --- Step 5: Interactive Chat Interface ---
def format_sources(source_docs):
if not source_docs:
return "No sources found."
sources = []
for i, doc in enumerate(source_docs, 1):
source_type = doc.metadata.get("source_type", "unknown")
source = doc.metadata.get("source", "unknown")
snippet = doc.page_content[:100] + "..." if len(doc.page_content) > 100 else doc.page_content
sources.append(f" [{i}] ({source_type}) {source}\n \"{snippet}\"")
return "\n".join(sources)
def chat():
print("Loading documents...")
documents = load_documents()
if not documents:
print("No documents loaded. Please add files to the docs/ folder.")
return
print("\nBuilding vector store...")
vector_store = create_vector_store(documents)
print("\nBuilding RAG chain...")
chain = build_rag_chain(vector_store)
session_id = "default_session"
print("\n" + "=" * 50)
print("Multi-Source RAG Chatbot Ready!")
print("Ask questions about your documents.")
print("Type 'quit' to exit.")
print("=" * 50 + "\n")
while True:
user_input = input("You: ").strip()
if not user_input:
continue
if user_input.lower() in ["quit", "exit", "q"]:
print("Goodbye!")
break
response = chain.invoke(
{"input": user_input},
config={"configurable": {"session_id": session_id}},
)
print(f"\nAssistant: {response['answer']}")
print(f"\nSources:")
print(format_sources(response.get("context", [])))
print()
if __name__ == "__main__":
chat()
Test the full system
Your chatbot is complete. Time to run it and verify that retrieval, generation, and conversation memory all work together.
- Make sure your virtual environment is active, then run the chatbot by entering this command:
python app.py
You should see loading messages followed by the chatbot banner:
Loading documents...
Loaded 1 document(s) from PDF
Loaded 1 document(s) from web
Loaded 1 document(s) from markdown
Building vector store...
Split into X chunks
Vector store created and persisted to ./chroma_db
Building RAG chain...
==================================================
Multi-Source RAG Chatbot Ready!
Ask questions about your documents.
Type 'quit' to exit.
==================================================
You:
First run takes a moment
The first time you run the chatbot, HuggingFaceEmbeddings downloads the all-MiniLM-L6-v2 model (about 80MB). Subsequent runs use the cached model and start faster.
- Ask a question about your markdown file to test retrieval from that source:
You: What are the key features of LangChain?
The chatbot should respond with an answer drawn from your sample.md file, listing features like Document Loaders, Text Splitters, and Vector Stores. Below the answer, you will see a Sources section showing the markdown source type and a snippet of the retrieved content.
- Now test conversation memory by asking a follow-up question that references your previous question:
You: Can you tell me more about the first one?
The chatbot should understand that "the first one" refers to Document Loaders from your previous exchange. This proves the history-aware retriever is reformulating your query correctly.
- Try a question that targets web page content to verify cross-source retrieval:
You: What is retrieval in LangChain?
This time the sources section should show at least one result with source type web, confirming the web-scraped content is being retrieved.
- When you are done testing, type quit to exit the chatbot.
Chatbot not returning relevant answers?
- If you see "No sources found" for every answer, check that load_documents() printed non-zero document counts when the chatbot started.
- If the chatbot crashes with an API error, confirm your GROQ_API_KEY in the .env file is correct and your Groq account is active.
- If you see a SQLite version warning from ChromaDB, it is non-fatal. Your chatbot still works.
Help me debug my RAG chatbot
Your multi-source RAG chatbot is fully operational. You can ask questions grounded in PDFs, web pages, and markdown files, and the chatbot remembers context across your conversation. Next up, take on the Secret Mission to add a swappable LLM provider and a fourth document source.
Secret mission
Add OpenRouter as a Swappable LLM and Load CSV Documents
Your chatbot works great with Groq, but what happens when you want to compare answers from a different LLM provider? And what about all that structured data sitting in CSV files? Extend your RAG chatbot with provider-swapping and a 4th document source type.
Clean Up Your Resources
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 a free Groq API tier, so there are no ongoing costs.
Resources you used:
- Project folder with Python virtual environment and installed packages.
- ChromaDB vector store (./chroma_db folder).
- Sample documents in the docs/ folder (PDF, markdown).
- Groq account and API key (free tier).
- HuggingFace embedding model cached locally (~80MB).
Keep everything running
No action needed. Choose this if you're still actively building or plan to extend the chatbot with more documents.
- Your project folder, virtual environment, and ChromaDB vector store stay on disk.
- Your Groq API key remains active on the free tier with no charges.
- The cached HuggingFace embedding model stays in your system cache for fast reuse.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your files and vector store remain on disk for when you return.
- Close any terminal sessions running the chatbot.
- Deactivate your virtual environment by running:
deactivate
- Your project folder, chroma_db directory, and docs/ folder all stay intact.
- When you return, reactivate the virtual environment and run python app.py to pick up where you left off.
Delete - I don't want to use this again
Remove all project resources and start fresh. This deletes your vector store, documents, and virtual environment permanently.
- Close any terminal sessions running the chatbot.
- Delete the entire project folder by running:
cd ~/Desktop
rm -rf rag-chatbot
- Clear the cached HuggingFace embedding model by running:
rm -rf ~/.cache/huggingface/hub/models--sentence-transformers--all-MiniLM-L6-v2
- Optionally, revoke your Groq API key from the Groq dashboard if you no longer need it.
That's a Wrap!
That's a Wrap!
Nice work! You just built a multi-source RAG chatbot using LangChain that ingests knowledge from PDFs, web pages, and markdown files, then answers questions conversationally with source citations.
You've learned how to:
- Load documents from three different source types and chunk them into a persistent ChromaDB vector store using free HuggingFace embeddings.
- Build a conversational retrieval chain with Groq that reformulates queries based on chat history using create_history_aware_retriever.
- Create an interactive terminal chatbot that retrieves relevant passages and displays answers with source citations from each document type.
- Secret Mission: Added OpenRouter as a swappable LLM provider and CSV as a 4th document source.
Ready to quiz yourself?