Smart Q&A Bot for Your Notes: Local RAG With LangChain and Chroma
By DevShelfHub
Turn personal notes into an interactive Q&A system: chunk and overlap text, embed locally with Hugging Face, store in Chroma, and answer with LangChain RAG and Groq—no embedding API key.
Smart Q&A Bot for Your Notes is an intermediate-level project that allows users to turn their personal notes into an interactive question-answering system. Users can upload a file or paste text, and the system processes it into a searchable knowledge base.
The notes are split into smaller overlapping chunks to preserve context, then converted into embeddings using local Hugging Face sentence-transformers, removing the need for external embedding APIs. These embeddings are stored in a Chroma vector database for efficient retrieval.
When a question is asked, the system uses a Retrieval-Augmented Generation (RAG) pipeline powered by LangChain to fetch the most relevant note sections and generate accurate, context-grounded answers using Groq’s language model. This project demonstrates how personal data can be transformed into an intelligent, queryable assistant using modern LLM architecture.
Purpose: practice “ask my notes” without training your own model—local embeddings, a vector store, and a fast chat API (Groq) for generation.
Typical use: one chapter of reading, a meeting transcript export, or lab instructions saved as text—then question-by-question review before an exam.
Key ideas you will touch:
- Chunking — break long notes into overlapping pieces the model can retrieve selectively.
-
Embeddings + vector store — dense vectors from a small local model
(
sentence-transformers/all-MiniLM-L6-v2), stored in Chroma for similarity search. - Retriever + QA chain — LangChain pulls top chunks, then the LLM writes an answer grounded in those chunks.
- Next steps — PDF loaders, metadata filters, evaluation, persistence for Chroma, or swapping Groq/chat and embedding models once the happy path works.
Example prompts: “What definition does the note give for X?” or “List the three action items mentioned in the second half.”
Overall flow
User pastes text or uploads .txt
↓
Clicks "Build / refresh index"
↓
Split into chunks → embed locally (sentence-transformers) → store in Chroma
↓
User types a question → clicks "Answer from my notes"
↓
Retriever finds top-k similar chunks
↓
LLM answers using those chunks (RAG)
↓
Answer (+ optional source chunks) on screen
Step-by-Step Implementation
Follow these steps in order:
-
Set up the project
- Install Python 3.10+
- Create a folder and a virtual environment
-
Install dependencies
-
streamlit,httpx,truststore,groq,langchain,langchain-core,langchain-community,langchain-text-splitters,langchain-groq,langchain-huggingface,langchain-classic(forRetrievalQA),chromadb,sentence-transformers(pulls in torch)—align pins with the repo’srequirements.txt - Pin versions in
requirements.txtonce you have a working set
-
-
Streamlit UI
- Text area +
.txtuploader for source notes - Button to build the vector index
- Question field + button to run RAG
- Optional expander to show retrieved chunks (builds trust, helps debug)
- Text area +
-
Secrets
-
Put
GROQ_API_KEYin.streamlit/secrets.tomlfor chat. Embeddings run on-device; the first index may download model weights.
-
Put
-
Chunk and embed
-
Use
RecursiveCharacterTextSplitterwith sane chunk size/overlap -
HuggingFaceEmbeddingswithsentence-transformers/all-MiniLM-L6-v2(local; no embedding API) -
Chroma.from_documentsto build an in-memory store for the session
-
Use
-
Ask with RAG
-
as_retriever(search_kwargs={"k": 4})for top chunks -
RetrievalQA.from_chain_type(..., chain_type="stuff")withChatGroq(default chat modelllama-3.3-70b-versatileinapp.py); shared Groq httpx clients usetruststorefor TLS on macOS
-
-
Guardrails
- Warn on empty notes, missing index, empty question, or missing
GROQ_API_KEY -
Wrap index and QA in
try/except; QA errors add hints for TLS, proxy, auth, and rate limits, plus an expander with tracebacks -
Cap pasted/uploaded text length (see
MAX_NOTES_CHARSinapp.py) so you do not send huge blobs by accident
- Warn on empty notes, missing index, empty question, or missing
Code implementation
The snippet below matches app.py in the project repo (requirements, README, and
.streamlit live next to it). LangChain’s package layout changes between
releases—if imports fail, align pins with the
LangChain docs
and your requirements.txt.
"""Smart Q&A Bot for Your Notes — Streamlit + LangChain + Chroma + Groq LLM (RAG app).
Run from this directory: streamlit run app.py
Set GROQ_API_KEY in .streamlit/secrets.toml (see README).
Embeddings use a small local Hugging Face model (no embedding API key).
"""
import ssl
import traceback
import httpx
import streamlit as st
import truststore
from groq import DefaultAsyncHttpxClient, DefaultHttpxClient
from langchain_classic.chains import RetrievalQA
from langchain_community.vectorstores import Chroma
from langchain_groq import ChatGroq
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
def _groq_tls_verify_bundle():
"""TLS trust for Groq HTTPS.
Homebrew Python on macOS often fails with ``certifi`` alone (issuer chain differs from
the OS). :mod:`truststore` uses the **native** trust store (Keychain on macOS),
aligning with Safari and fixing many ``CERTIFICATE_VERIFY_FAILED`` cases.
Fallback: ``certifi`` if ``truststore`` cannot build a context.
"""
try:
return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
except Exception:
import certifi
return certifi.where()
def groq_httpx_clients() -> tuple[httpx.Client, httpx.AsyncClient]:
"""Shared httpx clients for the official Groq SDK.
Uses Groq's ``DefaultHttpxClient`` / ``DefaultAsyncHttpxClient`` so limits, timeouts,
and redirects match what the SDK expects (plain ``httpx.Client`` drops those defaults).
``verify`` uses the OS trust store via ``truststore`` (see ``_groq_tls_verify_bundle``).
``trust_env=False`` ignores ``HTTP(S)_PROXY`` so a broken shell proxy cannot break
Python while ``curl`` omits it.
"""
cache = "groq_httpx_clients_truststore_v1"
if cache not in st.session_state:
timeout = httpx.Timeout(120.0, connect=45.0, pool=30.0)
common: dict = {
"timeout": timeout,
"trust_env": False,
"verify": _groq_tls_verify_bundle(),
}
st.session_state[cache] = (
DefaultHttpxClient(**common),
DefaultAsyncHttpxClient(**common),
)
return st.session_state[cache]
st.set_page_config(page_title="Notes Q&A", layout="centered")
st.title("Smart Q&A Bot for Your Notes")
st.caption("Index a small .txt file or pasted notes, then ask questions grounded in that text (RAG).")
GROQ_API_KEY = str(st.secrets["GROQ_API_KEY"]).strip()
# Groq chat models: https://console.groq.com/docs/models
CHAT_MODEL = "llama-3.3-70b-versatile"
# Local embeddings — this app uses sentence-transformers on-device; Groq is chat-only here.
EMBED_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
MAX_NOTES_CHARS = 80_000
if "vectorstore" not in st.session_state:
st.session_state.vectorstore = None
notes = st.text_area("Or paste notes here", height=160, placeholder="Lecture bullets, reading summary…")
upload = st.file_uploader("Upload a plain-text file (.txt)", type=["txt"])
if st.button("Build / refresh index", type="primary"):
raw = ""
if upload is not None:
raw = upload.read().decode("utf-8", errors="replace")
elif notes.strip():
raw = notes.strip()
if not raw.strip():
st.warning("Add a .txt upload or paste some text before indexing.")
elif len(raw) > MAX_NOTES_CHARS:
st.warning(f"Notes are too long for this demo (max {MAX_NOTES_CHARS:,} characters). Trim and try again.")
else:
with st.spinner("Chunking, embedding, storing in Chroma…"):
try:
docs = [Document(page_content=raw)]
splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=120)
chunks = splitter.split_documents(docs)
emb = HuggingFaceEmbeddings(model_name=EMBED_MODEL)
st.session_state.vectorstore = Chroma.from_documents(chunks, embedding=emb)
st.success(f"Indexed {len(chunks)} chunks.")
except Exception as err:
st.session_state.vectorstore = None
st.error(f"Index failed: {err}")
question = st.text_input("Your question", placeholder="What does the text say about…?")
if st.button("Answer from my notes"):
if st.session_state.vectorstore is None:
st.warning("Build an index first.")
elif not question.strip():
st.warning("Please enter a question.")
elif not GROQ_API_KEY:
st.error("Add `GROQ_API_KEY` to `.streamlit/secrets.toml` (see README).")
else:
with st.spinner("Retrieving chunks and calling the model…"):
try:
hx, hx_async = groq_httpx_clients()
llm = ChatGroq(
model=CHAT_MODEL,
api_key=GROQ_API_KEY,
temperature=0,
timeout=120.0,
max_retries=3,
http_client=hx,
http_async_client=hx_async,
)
retriever = st.session_state.vectorstore.as_retriever(search_kwargs={"k": 4})
qa = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
return_source_documents=True,
)
out = qa.invoke({"query": question})
st.markdown(out["result"])
with st.expander("Sources (retrieved chunks)"):
for i, doc in enumerate(out["source_documents"], start=1):
preview = doc.page_content[:700]
suffix = "…" if len(doc.page_content) > 700 else ""
st.markdown(f"**Chunk {i}**")
st.write(preview + suffix)
except Exception as err:
hint = ""
low = str(err).lower()
tb = traceback.format_exc()
if "certificate" in low or "ssl" in low or "CERTIFICATE_VERIFY" in tb:
hint = (
" TLS verification failed. Ensure `truststore` is installed (`pip install -r "
"requirements.txt`), restart Streamlit, refresh the browser. Corporate networks: "
"set `SSL_CERT_FILE` / `REQUESTS_CA_BUNDLE` to your root CA PEM. See README."
)
elif "connection" in low or "connect" in low:
hint = (
" If `curl` to Groq works but the app fails, unset broken proxy env vars "
"(`HTTPS_PROXY`, `HTTP_PROXY`) in the terminal before `streamlit run`, or fix "
"VPN/firewall — see README. Status: https://status.groq.com/"
)
elif "401" in low or "unauthorized" in low or "invalid" in low and "api" in low:
hint = " Check that `GROQ_API_KEY` in `.streamlit/secrets.toml` is valid."
elif "429" in low or "rate" in low:
hint = " Rate limited — wait a moment and try again."
st.error(f"**{type(err).__name__}:** {err}{hint}")
with st.expander("Technical details"):
chain = str(err)
c = err.__cause__
while c is not None:
chain += f"\n\nCaused by: {type(c).__name__}: {c}"
c = c.__cause__
chain += "\n\n" + traceback.format_exc()
st.code(chain, language="text")
Complete code — Project link (GitHub)
Demo
Screen recording: index notes, ask a question, and open Sources to see retrieved chunks.
📖 How the Code Works (Step-by-Step)
This section explains the implementation in simple terms, from top to bottom.
1. Streamlit shell
-
st.set_page_config,st.title, andst.captionframe the app. -
st.session_statekeeps the Chroma vector store between clicks so you do not rebuild on every rerun unless the user presses “Build / refresh index”. -
Groq HTTP clients are cached in
session_statetoo, with TLS verification wired throughtruststorefor fewer macOS certificate surprises.
2. Ingest notes
-
Decode uploaded
.txtbytes as UTF-8 (with replacement for odd characters). - If there is no upload, fall back to the pasted text area.
-
Wrap the raw string in a
Documentso LangChain can track metadata later if you extend the app.
3. Chunking
-
RecursiveCharacterTextSplitterbreaks text into overlapping windows so a fact split across two paragraphs can still appear whole inside one chunk. - Smaller chunks → more precise retrieval; larger chunks → more context per hit. Tune for your note style.
4. Embeddings and Chroma
-
HuggingFaceEmbeddingsturns each chunk into a vector on-device via sentence-transformers. -
Chroma.from_documentsstores vectors and text for similarity search.
5. Retrieval QA
-
The retriever returns the top-
kchunks whose embeddings are closest to the question embedding. -
RetrievalQAwithchain_type="stuff"concatenates those chunks into the prompt forChatGroq. -
return_source_documents=Truelets you show evidence in an expander.
6. Errors and limits
- User-facing checks avoid empty queries and missing indexes.
-
try/exceptcatches embedding failures, Groq errors (TLS, proxy, auth, rate limits), and surfaces optional technical details—without crashing Streamlit.
Tips & Production Considerations
Tune chunk size and overlap for your note style
The default chunk_size=500 and chunk_overlap=50
work for short lecture notes. For dense technical material or long-form transcripts, increase both values
(e.g. 1000/150) so the retriever surfaces more context per hit. Too-small chunks return fragments that confuse the
LLM; too-large chunks dilute relevance. Experiment with your own notes and compare answer quality.
First run downloads the embedding model
sentence-transformers/all-MiniLM-L6-v2 is about 90 MB. The initial
HuggingFaceEmbeddings call downloads and caches it in
~/.cache/huggingface. After that, indexing works fully offline. If you deploy
to a container, bake the model into the image so cold starts stay under a few seconds.
Groq rate limits and fallback
Groq's free tier allows roughly 30 requests per minute on Llama models. For personal study sessions this is plenty, but if you share the app with a class, concurrent users can hit 429 errors. Adding a short retry with exponential back-off in the QA path, or switching to a self-hosted model via Ollama, avoids dropped answers during peak study hours.
Persist Chroma to disk for longer sessions
The default in-memory Chroma collection vanishes when Streamlit reruns. Pass a
persist_directory to
Chroma.from_documents and the index survives restarts. This also lets you
load multiple note files over time and build a cumulative knowledge base instead of re-indexing each session.
Show source chunks to build trust
return_source_documents=True gives you the exact chunks the answer is
grounded in. Surfacing them in a Streamlit expander lets users verify the LLM did not hallucinate and trains
them to write better questions when the retrieved context looks irrelevant.
Notes Q&A Bot FAQ
What is the Notes Q&A Bot?
The Notes Q&A Bot is an intermediate Streamlit project that turns your personal notes into a chat-style question and answer system. It chunks your text with overlap, embeds it locally with a Hugging Face Sentence Transformer, stores vectors in Chroma, and answers via LangChain RAG using Groq for fast LLM responses.
Is the Notes Q&A Bot free to use?
The source code is free and open on GitHub. Embeddings run fully locally with Hugging Face, so there is no embedding API key or per-query cost. You only pay for Groq LLM usage, and Groq offers a generous free tier that comfortably covers personal study sessions.
What tech stack does the Notes Q&A Bot use?
Python and Streamlit for the UI, LangChain for the RAG pipeline, Chroma as the local vector store, Sentence Transformers from Hugging Face for embeddings, and Groq for chat completions. No paid embedding API is required.
How does retrieval work in the Notes Q&A Bot?
Your notes are split into overlapping chunks, embedded with the all-MiniLM-L6-v2 model, and stored in a local Chroma collection. At query time, the bot embeds your question, fetches the top-k most similar chunks, and passes them as context to a Groq model with a focused prompt.
Can I run the Notes Q&A Bot offline?
Embeddings already run offline. To make the entire app offline, swap the Groq client for a local LLM such as Ollama or llama.cpp and update the LangChain LLM wrapper. Chroma persistence and Hugging Face embeddings already work without internet after the model has been downloaded once.
What are alternatives to the Notes Q&A Bot?
For a tutor-style chat with quizzes over PDFs, see the Personal Study Tutor. To chat with web pages instead of notes, see the Website RAG Chatbot. For a faster one-shot summary instead of Q&A, see the Smart Study Assistant.