DS DevShelfHub Projects · AI tools

Website RAG Chatbot: Chat With Any URL Using LangChain and Chroma

Intermediate

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.

Python Streamlit LangChain Chroma Groq Hugging Face RAG BeautifulSoup

View on GitHub

Website RAG Chatbot — LangChain + Chroma + Groq chat for any URL

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 scrapingrequests + a BeautifulSoup tag-removal pass to strip navigation, scripts, and boilerplate before indexing.
  • ChunkingRecursiveCharacterTextSplitter with overlap so facts that span paragraph boundaries land in a single retrievable chunk.
  • Local embeddingssentence-transformers/all-MiniLM-L6-v2 via HuggingFaceEmbeddings; no separate embedding API key needed.
  • Vector store + retrieval — Chroma stores the chunk vectors and returns the top-4 matches for each question.
  • RAG generationRetrievalQA (chain type “stuff”) passes retrieved chunks to ChatGroq and 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:

  1. Set up the project
    • Install Python 3.10+
    • Create a folder and a virtual environment
  2. 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)
  3. Secrets
    • Put GROQ_API_KEY in .streamlit/secrets.toml. No embedding key needed.
  4. Scrape and clean
    • requests.get with a browser-like User-Agent header reduces bot-blocking
    • Decompose script, style, nav, footer, aside, noscript, and form tags before extracting text
    • Join non-empty lines and cap at MAX_SCRAPED_CHARS to keep prompts manageable
  5. Build the index
    • RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=80)
    • HuggingFaceEmbeddings with all-MiniLM-L6-v2 — runs on-device
    • Chroma.from_documents stores the in-memory index in st.session_state
  6. Ask with RAG
    • as_retriever(search_kwargs={"k": 4}) for top chunks
    • RetrievalQA.from_chain_type with chain_type="stuff" and return_source_documents=True
    • Store each Q&A turn in st.session_state["history"]
  7. Guardrails
    • Warn on empty URL, empty scraped text, and empty question
    • Both _scrape and _ask paths are wrapped in try/except with st.error on 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.

python

# 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

  • truststore wires the OS certificate store into an httpx.Client passed to ChatGroq—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-Agent header mimics a browser and reduces 403 errors on sites that block plain python-requests strings.
  • Decomposing structural tags (nav, footer, aside) before calling get_text removes 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.
  • HuggingFaceEmbeddings downloads all-MiniLM-L6-v2 once 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 the ChatGroq prompt—no summarization or map-reduce step.
  • return_source_documents=True gives 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 in st.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.

Browse all →