Website RAG Chatbot: Chat With Any URL Using LangChain and Chroma
By DevShelfHub
Intermediate project: paste a URL to scrape, chunk, and embed page text locally (Hugging Face + Chroma), then chat via LangChain RAG and Groq—Python and Streamlit.
Website RAG Chatbot is an intermediate-level project that enables users to interact directly with the content of any public webpage. By simply providing a URL, the system extracts and processes the page content, making it searchable and conversational.
The pipeline begins by scraping the webpage using BeautifulSoup, then splitting the text into overlapping chunks for better context retention. These chunks are converted into embeddings using local Hugging Face sentence-transformers, eliminating the need for external embedding APIs. The embeddings are stored in a Chroma vector database, enabling efficient similarity search.
When a user asks a question, the system retrieves the most relevant context using a Retrieval-Augmented Generation (RAG) workflow powered by LangChain, and generates accurate answers using Groq’s language model. This project demonstrates how modern LLM systems combine scraping, vector databases, and retrieval pipelines to turn static web content into an interactive knowledge source.
Purpose: learn how to turn arbitrary web content into a queryable knowledge base—the same pipeline that powers many AI research and customer-support tools.
Typical use: point it at a long documentation page, a product landing page, or a news article and ask follow-up questions without re-reading the whole thing.
Key ideas you will touch:
-
Web scraping —
requests+ aBeautifulSouptag-removal pass to strip navigation, scripts, and boilerplate before indexing. -
Chunking —
RecursiveCharacterTextSplitterwith overlap so facts that span paragraph boundaries land in a single retrievable chunk. -
Local embeddings —
sentence-transformers/all-MiniLM-L6-v2viaHuggingFaceEmbeddings; no separate embedding API key needed. - Vector store + retrieval — Chroma stores the chunk vectors and returns the top-4 matches for each question.
-
RAG generation —
RetrievalQA(chain type “stuff”) passes retrieved chunks toChatGroqand surfaces source chunks in an expander.
Example prompts: “What pricing plans are available?” or “Summarise the main argument in the second section.”
Overall flow
User enters a URL → clicks "Scrape & Build Index"
↓
requests fetches the page HTML
↓
BeautifulSoup strips scripts / nav / footer / boilerplate
↓
RecursiveCharacterTextSplitter → chunks (500 chars, 80 overlap)
↓
HuggingFaceEmbeddings (local) → Chroma vector store
↓
"Indexed N chars" confirmation
↓
User types a question → clicks "Ask"
↓
Retriever finds top-4 similar chunks
↓
ChatGroq (Groq) generates grounded answer
↓
Answer + optional Source Chunks on screen
Demo
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,requests,beautifulsoup4,groq,langchain,langchain-core,langchain-community,langchain-text-splitters,langchain-groq,langchain-huggingface,chromadb,sentence-transformers -
Pin versions in
requirements.txt; the first run downloads the embedding model weights (~22 MB)
-
-
Secrets
-
Put
GROQ_API_KEYin.streamlit/secrets.toml. No embedding key needed.
-
Put
-
Scrape and clean
-
requests.getwith a browser-likeUser-Agentheader reduces bot-blocking -
Decompose
script,style,nav,footer,aside,noscript, andformtags before extracting text -
Join non-empty lines and cap at
MAX_SCRAPED_CHARSto keep prompts manageable
-
-
Build the index
-
RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=80) -
HuggingFaceEmbeddingswithall-MiniLM-L6-v2— runs on-device -
Chroma.from_documentsstores the in-memory index inst.session_state
-
-
Ask with RAG
-
as_retriever(search_kwargs={"k": 4})for top chunks -
RetrievalQA.from_chain_typewithchain_type="stuff"andreturn_source_documents=True - Store each Q&A turn in
st.session_state["history"]
-
-
Guardrails
- Warn on empty URL, empty scraped text, and empty question
-
Both
_scrapeand_askpaths are wrapped intry/exceptwithst.erroron failure
Code implementation
The snippet below is the complete app.py. LangChain package layout changes
between releases—if imports fail, align pins with the
LangChain docs
and your requirements.txt.
# Website RAG Chatbot — Streamlit + LangChain + Chroma + Groq
# Save as app.py, add GROQ_API_KEY to .streamlit/secrets.toml, then: streamlit run app.py
import ssl
import httpx
import streamlit as st
import truststore
MAX_SCRAPED_CHARS = 50_000
st.set_page_config(page_title="Website RAG Chatbot", layout="centered")
st.title("Website RAG Chatbot")
st.caption(
"Enter any public URL, scrape its content, and ask questions about it "
"using Retrieval-Augmented Generation."
)
_ssl_ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
def _scrape(url: str) -> str:
import requests
from bs4 import BeautifulSoup
headers = {"User-Agent": "Mozilla/5.0 (compatible; DevShelfHubRAGBot/1.0)"}
resp = requests.get(url, headers=headers, timeout=15)
resp.raise_for_status()
soup = BeautifulSoup(resp.text, "html.parser")
for tag in soup(["script", "style", "nav", "footer", "header", "aside", "noscript", "form"]):
tag.decompose()
text = soup.get_text(separator="\n")
lines = [line.strip() for line in text.splitlines() if line.strip()]
return "\n".join(lines)[:MAX_SCRAPED_CHARS]
def _build_index(text: str):
from langchain_community.vectorstores import Chroma
from langchain_core.documents import Document
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=80)
docs = splitter.split_documents([Document(page_content=text)])
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
return Chroma.from_documents(docs, embeddings)
def _ask(vector_store, question: str) -> dict:
from langchain.chains import RetrievalQA
from langchain_groq import ChatGroq
llm = ChatGroq(
api_key=st.secrets["GROQ_API_KEY"],
model_name="llama-3.3-70b-versatile",
http_client=httpx.Client(timeout=120.0, verify=_ssl_ctx),
)
retriever = vector_store.as_retriever(search_kwargs={"k": 4})
qa = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
return_source_documents=True,
)
return qa.invoke({"query": question})
url_input = st.text_input("Website URL", placeholder="https://example.com/page")
if st.button("Scrape & Build Index", type="primary"):
if not url_input.strip():
st.warning("Please enter a URL first.")
else:
with st.spinner("Scraping and indexing — first run downloads embedding model weights…"):
try:
raw_text = _scrape(url_input.strip())
if not raw_text.strip():
st.error("Could not extract any text from that URL. Try a different page.")
else:
st.session_state["vs"] = _build_index(raw_text)
st.session_state["scraped_url"] = url_input.strip()
st.session_state["scraped_chars"] = len(raw_text)
st.session_state.pop("history", None)
st.success(
f"Indexed {len(raw_text):,} characters from {url_input.strip()}"
)
except Exception as err:
st.error(f"Scraping failed: {err}")
if "vs" in st.session_state:
st.markdown("---")
st.markdown(
f"**Ready.** Ask anything about `{st.session_state['scraped_url']}` "
f"({st.session_state['scraped_chars']:,} chars indexed)."
)
question = st.text_input("Your question", placeholder="What is this page about?")
if st.button("Ask", type="primary"):
if not question.strip():
st.warning("Please type a question.")
else:
with st.spinner("Retrieving and generating answer…"):
try:
result = _ask(st.session_state["vs"], question.strip())
answer = (result.get("result") or "").strip()
sources = result.get("source_documents", [])
st.session_state.setdefault("history", []).append(
{"q": question.strip(), "a": answer}
)
st.markdown("**Answer:**")
st.markdown(answer)
if sources:
with st.expander("Source chunks"):
for i, doc in enumerate(sources, 1):
st.caption(f"Chunk {i}")
st.text(doc.page_content[:400])
except Exception as err:
st.error(f"Something went wrong: {err}")
history = st.session_state.get("history", [])
if len(history) > 1:
with st.expander("Earlier Q&A"):
for turn in reversed(history[:-1]):
st.markdown(f"**Q:** {turn['q']}")
st.markdown(f"**A:** {turn['a']}")
st.divider()
Complete code — Project link (GitHub)
📖 How the Code Works (Step-by-Step)
A walkthrough of app.py from top to bottom.
1. Streamlit shell
-
truststorewires the OS certificate store into anhttpx.Clientpassed toChatGroq—same fix used in the notes-qa-bot to avoid macOS TLS surprises. -
The Chroma index lives in
st.session_state["vs"]so it persists across reruns without rebuilding on every button click.
2. Scraping
-
A custom
User-Agentheader mimics a browser and reduces 403 errors on sites that block plainpython-requestsstrings. -
Decomposing structural tags (nav, footer, aside) before calling
get_textremoves boilerplate that would pollute embeddings and waste context window on the answer side.
3. Chunking and embedding
- Chunk size 500 / overlap 80 is a reasonable default for web prose; smaller chunks improve retrieval precision, larger ones improve context richness—tune for your target pages.
-
HuggingFaceEmbeddingsdownloadsall-MiniLM-L6-v2once and caches it locally; subsequent runs skip the download.
4. Retrieval QA
- The retriever returns the 4 chunks whose embeddings are closest to the question embedding.
-
chain_type="stuff"concatenates those chunks directly into theChatGroqprompt—no summarization or map-reduce step. -
return_source_documents=Truegives users transparency into what the model actually read before answering.
5. Chat history
-
Each Q&A turn is appended to
st.session_state["history"]; the “Earlier Q&A” expander shows all but the most recent turn in reverse order. -
Scraping a new URL clears history with
st.session_state.pop("history", None)so you start fresh after switching pages.
6. Error handling
- Empty URL, empty scraped text, and empty question each get a targeted warning before any external call.
-
Both the scrape/index path and the QA path are wrapped in
try/except; the exception message surfaces inst.error()without crashing Streamlit.
Tips & Production Considerations
JavaScript-heavy sites need a headless browser
BeautifulSoup only sees server-rendered HTML. Single-page apps built with React, Vue, or Next.js client-only routes return an empty shell. For those sites, swap the scraper for Playwright or a service like Firecrawl that executes JavaScript before returning content.
Cache indexed URLs to avoid re-scraping
Pass a persist_directory to Chroma and key collections by URL hash.
When a user re-enters the same URL, load the existing collection instead of scraping and embedding again.
This saves time and avoids redundant API calls during repeated chat sessions.
Strip navigation and boilerplate before chunking
Raw HTML includes headers, footers, sidebars, and cookie banners. Passing all of that to the chunker
pollutes retrieval with irrelevant text. The app already strips nav,
footer, and script tags, but for
noisy pages you may need to target the main content container by CSS selector.
Respect robots.txt and rate limits
If you extend the app to crawl multiple pages from a site, check
robots.txt before scraping and add a delay between requests. Aggressive
crawling can get your IP blocked and violates most sites' terms of service.
Show retrieved chunks so users can verify answers
RAG answers are only as good as the retrieved context. Surfacing the source chunks in a Streamlit expander lets users spot when the retriever pulled irrelevant content and rephrase their question instead of trusting a hallucinated answer.
Website RAG Chatbot FAQ
What is the Website RAG Chatbot?
The Website RAG Chatbot is an intermediate Streamlit project that lets you chat with any URL. It scrapes the page with BeautifulSoup, chunks and embeds it locally with Hugging Face, stores vectors in Chroma, and answers via LangChain RAG using Groq.
Is the Website RAG Chatbot 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 cost. You only pay for Groq LLM usage, which has a generous free tier.
What tech stack does the Website RAG Chatbot use?
Python and Streamlit for the UI, BeautifulSoup for HTML scraping, LangChain for the RAG pipeline, Chroma as the local vector store, Sentence Transformers from Hugging Face for embeddings, and Groq for the LLM.
How does scraping work in the Website RAG Chatbot?
When you paste a URL, the app fetches the HTML, strips navigation and footer noise with BeautifulSoup, splits the cleaned text into overlapping chunks, embeds them, and writes the result to a Chroma collection keyed by the URL so re-chatting the same page is instant.
Does the Website RAG Chatbot work on JavaScript-heavy sites?
Not by default—BeautifulSoup only sees server-rendered HTML. For SPAs (React/Vue/Next.js client-only routes) you'd need a headless browser like Playwright or a service like Firecrawl. Static blogs, docs, and Wikipedia work out of the box.
What are alternatives to the Website RAG Chatbot?
For RAG over personal notes instead of websites, see the Notes Q&A Bot. For RAG over PDFs with quizzes, see the Personal Study Tutor. For a customer-support flavor with auto-escalation, see the Customer Support Agent Simulator.