Personal Study Tutor: A RAG Tutor That Reads Your PDFs and Quizzes You
By DevShelfHub
RAG-based tutor that indexes your PDFs and notes locally, answers questions from your material, and generates quizzes for active recall—LangChain, Chroma, Hugging Face embeddings, and Groq.
Personal Study Tutor is an intermediate-level project that turns your own notes and PDFs into an interactive teaching assistant. Upload study material, build a local knowledge index, then ask questions directly from the content or generate a multiple-choice quiz on any topic inside your notes.
The project uses a full RAG (Retrieval-Augmented Generation) pipeline: documents are split into overlapping chunks, embedded locally with Hugging Face sentence-transformers, stored in a Chroma vector database, and queried through a LangChain retrieval chain powered by Groq. No external embedding API is needed.
The quiz feature retrieves the most relevant chunks for a topic and instructs the model to write a multiple-choice quiz with correct answers and brief explanations—turning passive reading into active recall in a single click.
Purpose: combine RAG-based retrieval with quiz generation to replace rereading entire chapters before an exam.
Typical use: upload a chapter PDF, click “Build Index,” ask a clarifying question, then generate a five-question quiz on the section you found most difficult.
Key ideas you will touch:
-
PDF ingestion —
pypdfreads multi-page files without any cloud service. -
Chunk & embed — overlapping windows via
RecursiveCharacterTextSplitter, embedded on-device withsentence-transformers/all-MiniLM-L6-v2. -
RAG Q&A —
RetrievalQAwithchain_type="stuff"and source-document display. - Quiz generation — topic-focused retrieval feeds a structured prompt that produces numbered MCQ questions with answers and explanations.
Overall flow
User uploads PDFs / TXT or pastes notes
↓
Clicks "Build / Refresh Index"
↓
Files decoded → text concatenated → chunked → embedded (local) → stored in Chroma
↓
Tab 1 — Ask a Question:
User types question → retriever finds top-4 chunks → Groq answers grounded in chunks
↓
Tab 2 — Generate Quiz:
User picks topic + count → retriever fetches relevant chunks
→ Groq writes MCQ with answers and explanations
Demo
Step-by-Step Implementation
-
Set up the project
- Install Python 3.10+
- Create a folder and a virtual environment
-
Install dependencies
-
streamlit,groq,httpx,truststore,pypdf,langchain,langchain-community,langchain-groq,langchain-huggingface,langchain-text-splitters,chromadb,sentence-transformers
-
-
Store your API key
-
Add
GROQ_API_KEYto.streamlit/secrets.toml
-
Add
-
Build the sidebar UI
- Multi-file uploader accepting PDF and TXT
- Text area for pasted notes as a fallback
- “Build / Refresh Index” button that triggers indexing
-
Ingest and index
- Read PDFs with
pypdf.PdfReader; decode TXT as UTF-8 -
Split combined text with
RecursiveCharacterTextSplitter(chunk_size=600, overlap=80) -
Embed with
HuggingFaceEmbeddings, store inChroma.from_documents -
Save vectorstore in
st.session_stateto persist across reruns
- Read PDFs with
-
Ask tab — RAG Q&A
-
Build
RetrievalQAchain withChatGroqandk=4 - Show retrieved source chunks in an expander so users can verify the answer
-
Build
-
Quiz tab — MCQ generation
- Optional topic focus triggers a semantic search to pull relevant chunks
- Structured prompt specifies Q/A/B/C/D format with Answer and Explanation lines
-
Guardrails
-
Warn on empty input, missing index, or empty question; cap text at
MAX_PDF_CHARS - Wrap all API and index calls in
try/except
-
Warn on empty input, missing index, or empty question; cap text at
Code implementation
# Personal Study Tutor (RAG-based) — Streamlit + LangChain + Chroma + Groq
# Save as app.py, add GROQ_API_KEY to .streamlit/secrets.toml, then: streamlit run app.py
import io
import ssl
import httpx
import streamlit as st
import truststore
MAX_NOTES_CHARS = 12000
MAX_PDF_CHARS = 15000
st.set_page_config(page_title="Personal Study Tutor", layout="centered")
st.title("Personal Study Tutor")
st.caption(
"Upload your notes or PDFs, build a knowledge index, then ask questions "
"or request a quiz on any topic inside your material."
)
_ssl_ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
@st.cache_resource
def _groq_http_client():
return httpx.Client(verify=_ssl_ctx)
def _get_groq_client():
import groq
if "groq_client" not in st.session_state:
st.session_state.groq_client = groq.Groq(
api_key=st.secrets["GROQ_API_KEY"],
http_client=_groq_http_client(),
)
return st.session_state.groq_client
_CHAT_MODEL = "llama-3.3-70b-versatile"
# ── Sidebar: input source ──────────────────────────────────────────────────────
with st.sidebar:
st.header("Your Study Material")
uploaded_files = st.file_uploader(
"Upload PDFs or TXT files",
type=["pdf", "txt"],
accept_multiple_files=True,
)
pasted_notes = st.text_area(
"Or paste notes here",
height=200,
placeholder="Paste any text you want to study...",
)
build_btn = st.button("Build / Refresh Index", type="primary", use_container_width=True)
# ── Build index ────────────────────────────────────────────────────────────────
if build_btn:
from langchain.schema import Document
from langchain_community.vectorstores import Chroma
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
all_text = ""
for f in (uploaded_files or []):
if f.name.endswith(".pdf"):
import pypdf
reader = pypdf.PdfReader(io.BytesIO(f.read()))
for page in reader.pages:
t = page.extract_text() or ""
all_text += t + "\n"
else:
all_text += f.read().decode("utf-8", errors="replace") + "\n"
if pasted_notes and pasted_notes.strip():
all_text += pasted_notes.strip()
all_text = all_text[:MAX_PDF_CHARS]
if not all_text.strip():
st.sidebar.warning("Please upload a file or paste some notes first.")
else:
with st.sidebar:
with st.spinner("Building index…"):
try:
splitter = RecursiveCharacterTextSplitter(chunk_size=600, chunk_overlap=80)
docs = splitter.split_documents([Document(page_content=all_text)])
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2"
)
st.session_state.vectorstore = Chroma.from_documents(docs, embeddings)
st.session_state.doc_count = len(docs)
st.success(f"Index built — {len(docs)} chunks ready.")
except Exception as exc:
st.error(f"Index error: {exc}")
if "vectorstore" in st.session_state:
st.sidebar.caption(f"Index: {st.session_state.doc_count} chunks loaded.")
# ── Main area ──────────────────────────────────────────────────────────────────
tab_ask, tab_quiz = st.tabs(["Ask a Question", "Generate Quiz"])
# ── Tab 1: Ask ─────────────────────────────────────────────────────────────────
with tab_ask:
st.subheader("Ask Anything From Your Notes")
question = st.text_input(
"Your question",
placeholder="What is the main argument in Chapter 3?",
)
ask_btn = st.button("Get Answer", key="ask_btn")
if ask_btn:
if "vectorstore" not in st.session_state:
st.warning("Build the index first using the sidebar.")
elif not question.strip():
st.warning("Enter a question.")
else:
from langchain_groq import ChatGroq
from langchain.chains import RetrievalQA
with st.spinner("Searching notes and generating answer…"):
try:
retriever = st.session_state.vectorstore.as_retriever(
search_kwargs={"k": 4}
)
llm = ChatGroq(
model_name=_CHAT_MODEL,
groq_api_key=st.secrets["GROQ_API_KEY"],
http_client=_groq_http_client(),
)
qa = RetrievalQA.from_chain_type(
llm=llm,
retriever=retriever,
return_source_documents=True,
chain_type="stuff",
)
result = qa.invoke({"query": question})
st.markdown("### Answer")
st.markdown(result["result"])
with st.expander("Source chunks used"):
for i, doc in enumerate(result["source_documents"], 1):
st.markdown(f"**Chunk {i}:** {doc.page_content[:300]}…")
except Exception as exc:
st.error(f"Error: {exc}")
# ── Tab 2: Quiz ────────────────────────────────────────────────────────────────
with tab_quiz:
st.subheader("Test Yourself")
col1, col2 = st.columns(2)
with col1:
topic_focus = st.text_input(
"Topic or chapter focus (optional)",
placeholder="e.g. photosynthesis, Chapter 2",
)
with col2:
num_questions = st.selectbox("Number of questions", [3, 5, 10], index=1)
quiz_btn = st.button("Generate Quiz", key="quiz_btn")
if quiz_btn:
if "vectorstore" not in st.session_state:
st.warning("Build the index first using the sidebar.")
else:
context_text = ""
if topic_focus.strip():
try:
retriever = st.session_state.vectorstore.as_retriever(
search_kwargs={"k": 5}
)
docs = retriever.invoke(topic_focus)
context_text = "\n\n".join(d.page_content for d in docs)
except Exception:
context_text = ""
if not context_text:
# Fall back to raw notes if retrieval fails
context_text = st.session_state.get("raw_text", "")[:3000]
if not context_text:
st.warning("Could not retrieve relevant content. Try rebuilding the index.")
else:
system_msg = (
"You are a study tutor. Create a multiple-choice quiz with "
f"{num_questions} questions based on the provided study material. "
"Format each question as:\n"
"Q1. [Question]\n"
"A) ...\nB) ...\nC) ...\nD) ...\n"
"Answer: [letter]\nExplanation: [one sentence]\n\n"
"Focus only on content that appears in the material."
)
user_msg = f"Study material:\n{context_text[:4000]}"
if topic_focus.strip():
user_msg += f"\n\nFocus specifically on: {topic_focus}"
with st.spinner("Generating quiz…"):
try:
client = _get_groq_client()
resp = client.chat.completions.create(
model=_CHAT_MODEL,
messages=[
{"role": "system", "content": system_msg},
{"role": "user", "content": user_msg},
],
)
st.markdown("### Your Quiz")
st.markdown(resp.choices[0].message.content)
except Exception as exc:
st.error(f"Error: {exc}")
Complete code — Project link (GitHub)
📖 How the Code Works (Step-by-Step)
1. Sidebar and session state
- The sidebar holds file upload, paste area, and the index button so the main panel stays clean.
-
st.session_state.vectorstoresurvives button clicks without rebuilding unless the user explicitly presses “Build / Refresh Index.”
2. File ingestion
-
PDFs are wrapped in
io.BytesIOsopypdf.PdfReadertreats uploaded bytes as a file handle. -
TXT files are decoded with
errors="replace"to avoid crashes on unusual encodings. - All sources are concatenated and capped at
MAX_PDF_CHARS.
3. Chunking and embedding
-
RecursiveCharacterTextSplittertries paragraph boundaries first, then sentence, then character—preserving natural breaks. -
HuggingFaceEmbeddingsruns fully on-device; the first build downloads model weights once then caches them.
4. RAG Q&A chain
- The retriever fetches top-4 chunks by cosine similarity to the question embedding.
-
chain_type="stuff"concatenates all four chunks into a single context block—simple and effective for short documents. -
return_source_documents=Truesurfaces the evidence so you can verify the answer in the expander.
5. Quiz generation
- When a topic focus is provided, the retriever is called first so the quiz draws from the most relevant parts of the material—not random chunks.
- The system message specifies the exact output format (Q, A–D, Answer line, Explanation) so the model cannot produce free-form text that breaks the layout.
Tips & Production Considerations
Upload clean, text-heavy PDFs for best results
pypdf extracts the text layer. Scanned-image PDFs, heavily formatted
slides, or PDFs with embedded diagrams produce garbled or empty text. For scanned documents, run OCR with
Tesseract or a cloud OCR service before uploading.
Persist the Chroma index across sessions
By default the vector store lives in memory and vanishes on restart. Pass a
persist_directory so indexed PDFs survive between sessions. This also
lets you build a cumulative knowledge base from an entire semester of readings without re-indexing each time.
Use topic focus for targeted quizzes
When generating a quiz, specifying a topic focus triggers a retriever call that pulls only the most relevant chunks. Without a focus, the quiz draws from random chunks and may produce questions from unrelated chapters. Always set a focus when studying for a specific exam section.
Verify quiz answers against your material
The model generates quiz questions and answers from retrieved context, but it can occasionally hallucinate plausible-sounding wrong answers. Cross-check any answer you are unsure about with the original PDF. The source chunks shown in the expander make this verification quick.
Adjust chunk size for different material types
Textbooks with dense paragraphs work well with larger chunks (800-1000 chars). Slide decks or bullet-point notes need smaller chunks (300-500 chars) so each chunk captures one idea. Experiment with chunk size and overlap until Q&A answers feel grounded and specific.
Personal Study Tutor FAQ
What is the Personal Study Tutor?
The Personal Study Tutor is an intermediate Streamlit project that ingests your PDFs and notes, indexes them locally for retrieval, answers questions strictly from your material, and generates quizzes for active recall—built with LangChain RAG, Chroma, and Groq.
Is the Personal Study Tutor free to use?
The source code is free and open on GitHub. Embeddings run locally with Hugging Face, so there is no embedding API cost. You only pay for Groq LLM usage, and a typical study session with quizzes costs a fraction of a cent on Groq's free tier.
What tech stack does the Personal Study Tutor use?
Python and Streamlit for the UI, pypdf for PDF extraction, LangChain for the RAG pipeline, Chroma as the local vector store, Sentence Transformers from Hugging Face for embeddings, and Groq for chat completions.
How does quiz generation work in the Personal Study Tutor?
After indexing your PDFs, you ask the tutor for a quiz on a topic. It retrieves the top-k chunks, then asks Groq to write multiple-choice or short-answer questions grounded in those chunks. Answers are checked against the retrieved context, not the model's general knowledge.
Can I run the Personal Study Tutor entirely offline?
Embeddings already run offline. To make the whole tutor offline, swap the Groq client for a local LLM such as Ollama or llama.cpp and update the LangChain LLM wrapper. PDF extraction, Chroma persistence, and Hugging Face embeddings already work without internet after first download.
What are alternatives to the Personal Study Tutor?
For a single-shot summary instead of an ongoing tutor session, see the Smart Study Assistant. For RAG over typed notes instead of PDFs, see the Notes Q&A Bot. For RAG over any URL, see the Website RAG Chatbot.