Chat with PDF
Build an interactive document question-answering app using Python, LangChain, ChromaDB, and Streamlit.
System Architecture
Prerequisites
- Python 3.10 or higher installed
- Basic knowledge of Python virtual environments
- OpenAI API key (or local Ollama instance)
Installation Commands
pip install streamlit langchain langchain-community langchain-openai chromadb pypdf python-dotenvFolder Structure
chat-with-pdf/
├── app.py # Main Streamlit application
├── requirements.txt # Project dependencies
├── .env.example # Environment variable template
└── README.md # Setup and usage instructionsStep-by-Step Implementation
Environment Configuration and Imports
Set up your API keys using python-dotenv and import the necessary modules from Streamlit, LangChain, and Chroma.
import os
import streamlit as st
from dotenv import load_dotenv
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate
import tempfile
load_dotenv()Document Ingestion and Chunking
Create a function to parse the uploaded PDF and split it into chunks of 1000 characters with 200 character overlap.
def process_pdf(uploaded_file):
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file:
tmp_file.write(uploaded_file.read())
tmp_path = tmp_file.name
loader = PyPDFLoader(tmp_path)
docs = loader.load()
os.remove(tmp_path)
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
length_function=len
)
chunks = text_splitter.split_documents(docs)
return chunksVector Store & Embeddings Setup
Embed the document chunks and index them into an in-memory ChromaDB vector database.
def get_vectorstore(chunks):
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(chunks, embeddings)
return vectorstoreRAG Retrieval Chain and Streamlit UI
Assemble the conversational prompt, connect the retriever with ChatOpenAI, and render a reactive chat interface.
st.set_page_config(page_title="Chat with PDF", page_icon="📄")
st.title("📄 Chat with PDF using RAG")
uploaded_file = st.sidebar.file_uploader("Upload PDF", type="pdf")
if uploaded_file:
if "vectorstore" not in st.session_state:
with st.spinner("Processing PDF..."):
chunks = process_pdf(uploaded_file)
st.session_state.vectorstore = get_vectorstore(chunks)
st.sidebar.success(f"Indexed {len(chunks)} chunks!")
system_prompt = (
"You are an assistant for question-answering tasks. "
"Use the following pieces of retrieved context to answer the question. "
"If you do not know the answer, say that you don't know.\n\n{context}"
)
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", "{input}"),
])
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
retriever = st.session_state.vectorstore.as_retriever(search_kwargs={"k": 3})
question_answer_chain = create_stuff_documents_chain(llm, prompt)
rag_chain = create_retrieval_chain(retriever, question_answer_chain)
user_query = st.chat_input("Ask a question about your document:")
if user_query:
st.chat_message("user").write(user_query)
response = rag_chain.invoke({"input": user_query})
with st.chat_message("assistant"):
st.write(response["answer"])
with st.expander("View Retrieved Sources"):
for idx, doc in enumerate(response["context"]):
st.caption(f"**Chunk {idx+1} (Page {doc.metadata.get('page', 0) + 1}):**")
st.text(doc.page_content[:300] + "...")Example Execution (Input & Output)
Why this works: The retriever extracted chunks from Page 14 with high cosine similarity, and the LLM synthesized the grounded answer with citations.
Troubleshooting Common Gotchas
OpenAI RateLimitError or quota exceeded
Verify your API credits or switch OpenAIEmbeddings to HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2") for 100% free local embeddings.
Empty text returned from PyPDFLoader on scanned PDFs
PyPDF extracts digital text layers. For scanned image-based PDFs, integrate OCR via pdf2image and pytesseract or use UnstructuredPDFLoader.
Known Limitations
- •Single-session memory by default (in-memory ChromaDB resets on server restart).
- •Does not extract complex tables or embedded charts into structured Markdown.
Security Considerations
- •Never commit hardcoded API keys; always use .env files with .gitignore.
- •Sanitize uploaded file paths to prevent directory traversal vulnerabilities.