DEV Community

Venkatesh Jonnagadla
Venkatesh Jonnagadla

Posted on Edited on

RAG Application Using Python & HTML


This is the final output -

We need to use the python 3.12.0 version to acheive this as there are dependent libraries.

Your folder structure should look like -
WorkspaceFolder/
├── chroma_db/
├── uploads/
├── templates/
│ └── index.html
└── requirements.txt
├── app.py

Frontend

HTML5
CSS3
Bootstrap 5
JQuery
AJAX

Backend

Python 3.12.0
Flask

index.html looks like below -

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Venkatesh RAG Chatbot</title>

<!-- Markdown + syntax highlighting -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/highlight.js@11/styles/atom-one-dark.min.css"/>
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/common.min.js"></script>

<style>
:root{
  --bg:#0e1117; --panel:#161b22; --panel2:#1c2330;
  --border:#2a3140; --text:#e6edf3; --muted:#8b949e;
  --accent:#4f8cff; --danger:#ff5c5c; --ok:#3fb950;
}
*{box-sizing:border-box}
html,body{height:100%;margin:0;font-family:system-ui,Segoe UI,Roboto,sans-serif;
          background:var(--bg);color:var(--text)}
#app{display:flex;height:100vh}

/* ---------- Sidebar ---------- */
#sidebar{width:300px;background:var(--panel);border-right:1px solid var(--border);
         display:flex;flex-direction:column;transition:width .2s}
#sidebar.collapsed{width:44px}
#sidebar.collapsed .side-content{display:none}
.side-head{display:flex;align-items:center;justify-content:space-between;
           padding:12px;border-bottom:1px solid var(--border)}
.side-head h3{font-size:14px;margin:0;color:var(--muted)}
.side-content{padding:12px;overflow:auto;flex:1;display:flex;flex-direction:column;gap:10px}

.btn{background:var(--panel2);border:1px solid var(--border);color:var(--text);
     padding:8px 10px;border-radius:6px;cursor:pointer;font-size:13px}
.btn:hover{border-color:var(--accent)}
.btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
.btn.danger{color:var(--danger);border-color:var(--danger)}
.path{font-size:11px;color:var(--muted);word-break:break-all;min-height:14px}
.row{display:flex;gap:6px}

/* file list */
#fileList{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:6px;
          max-height:45vh;overflow-y:auto}
#fileList li{display:flex;align-items:center;justify-content:space-between;
             background:var(--panel2);border:1px solid var(--border);
             border-radius:6px;padding:6px 8px;font-size:12px}
#fileList li span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.x{cursor:pointer;color:var(--danger);font-weight:700;margin-left:8px}
#collapseBtn{background:none;border:0;color:var(--muted);font-size:18px;cursor:pointer}

/* ---------- Chat ---------- */
#main{flex:1;display:flex;flex-direction:column}
#header{padding:14px 20px;border-bottom:1px solid var(--border);
        display:flex;align-items:center;gap:10px}
#header h1{font-size:15px;margin:0}
#chat{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:16px}
.msg{max-width:820px;padding:12px 16px;border-radius:12px;line-height:1.55;
     font-size:14px;white-space:normal}
.msg.user{align-self:flex-end;background:#23324a}
.msg.bot{align-self:flex-start;background:var(--panel);border:1px solid var(--border)}
.msg code{background:#000;color:#e6edf3;padding:1px 4px;border-radius:4px;font-size:12px}
.msg pre{position:relative;background:#000;border-radius:8px;padding:12px;overflow-x:auto;
         border:1px solid var(--border)}
.msg pre code{background:none;padding:0}
.copy-code{position:absolute;top:6px;right:6px;font-size:11px;padding:2px 6px;
           background:#222;color:#ddd;border:1px solid #444;border-radius:4px;cursor:pointer}
.copy-code:hover{background:#333}
.sources{font-size:11px;color:var(--muted);margin-top:8px}
.meta{font-size:11px;color:var(--muted);margin-top:4px}

/* input bar */
#inputBar{display:flex;gap:8px;padding:14px 20px;border-top:1px solid var(--border);
          background:var(--panel)}
#query{flex:1;resize:none;background:var(--panel2);color:var(--text);
       border:1px solid var(--border);border-radius:8px;padding:12px;font-size:14px;
       min-height:52px;max-height:150px}
#query:focus{outline:none;border-color:var(--accent)}
#send{width:52px;height:52px;border-radius:50%;background:var(--accent);border:0;
      color:#fff;font-size:22px;cursor:pointer}
#send:disabled{opacity:.5;cursor:not-allowed}
.hint{font-size:11px;color:var(--muted);padding:0 22px 10px;background:var(--panel)}
</style>
</head>
<body>
<div id="app">

  <!-- ================= SIDEBAR ================= -->
  <aside id="sidebar">
    <div class="side-head">
      <h3>Knowledge Base</h3>
      <button id="collapseBtn" title="Toggle">≡</button>
    </div>

    <div class="side-content">
      <!-- Choose Folder -->
      <div class="row">
        <button class="btn" id="chooseFolder">Choose Folder</button>
      </div>
      <div class="path" id="folderPath">No folder selected</div>
      <input type="file" id="folderInput" webkitdirectory directory multiple hidden/>

      <!-- Choose File -->
      <div class="row">
        <button class="btn" id="chooseFile">Choose File</button>
      </div>
      <div class="path" id="filePath">No file selected</div>
      <input type="file" id="fileInput" accept=".pdf,.docx,.xlsx,.pptx" multiple hidden/>

      <!-- Upload -->
      <div class="row">
        <button class="btn primary" id="uploadBtn" disabled>Upload</button>
      </div>

      <hr style="border-color:var(--border);width:100%"/>

      <!-- File list -->
      <h4 style="font-size:12px;color:var(--muted);margin:0">Uploaded Files</h4>
      <ul id="fileList"></ul>
    </div>
  </aside>

  <!-- ================= MAIN ================= -->
  <main id="main">
    <div id="header">
      <h1>Venkatesh RAG Chatbot</h1>
      <span class="meta" id="status"></span>
    </div>

    <div id="chat">
      <div class="msg bot">
        👋 Hi! Upload PDFs, Word, Excel or PowerPoint files on the left,
        then ask me anything about them.
      </div>
    </div>

    <div id="inputBar">
      <textarea id="query" placeholder="Ask something about your documents…"></textarea>
      <button id="send" title="Send">&gt;</button>
    </div>
    <div class="hint">Shift+Enter for newline · Enter to send</div>
  </main>
</div>

<script>
/* ======================= STATE ======================= */
let pendingFiles = [];
const $ = id => document.getElementById(id);

/* ======================= SIDEBAR COLLAPSE ======================= */
/* $('collapseBtn').onclick = () => $('sidebar').classList.toggle('collapsed'); */

/* ======================= CHOOSE FOLDER ======================= */
$('chooseFolder').onclick = () => $('folderInput').click();
$('folderInput').onchange = e => {
  const files = [...e.target.files].filter(f =>
      /\.(pdf|docx|xlsx|pptx)$/i.test(f.name));
  pendingFiles = files;
  $('folderPath').textContent = files.length
      ? `${e.target.files[0].webkitRelativePath.split('/')[0]}/ (${files.length} supported files)`
      : 'No supported files in folder';
  $('filePath').textContent = 'No file selected';
  $('uploadBtn').disabled = files.length === 0;
};

/* ======================= CHOOSE FILE ======================= */
$('chooseFile').onclick = () => $('fileInput').click();
$('fileInput').onchange = e => {
  const files = [...e.target.files];
  pendingFiles = files;
  $('filePath').textContent = files.length === 1
      ? files[0].name
      : `${files.length} files selected`;
  $('folderPath').textContent = 'No folder selected';
  $('uploadBtn').disabled = files.length === 0;
};

/* ======================= UPLOAD ======================= */
$('uploadBtn').onclick = async () => {
  if (!pendingFiles.length) return;
  const fd = new FormData();
  pendingFiles.forEach(f => fd.append('files', f, f.name));

  $('uploadBtn').disabled = true;
  $('uploadBtn').textContent = 'Uploading…';
  try {
    const r = await fetch('/api/upload', {method:'POST', body:fd});
    const data = await r.json();
    if (data.error) throw new Error(data.error);
    addBotMsg(`✅ Uploaded. **${data.total_chunks}** chunks embedded.`);
    await refreshFileList();
  } catch (e) {
    addBotMsg(`❌ Upload failed: \`${e.message}\``);
  } finally {
    $('uploadBtn').disabled = false;
    $('uploadBtn').textContent = 'Upload';
    pendingFiles = [];
    $('folderPath').textContent = 'No folder selected';
    $('filePath').textContent = 'No file selected';
    $('folderInput').value = '';
    $('fileInput').value = '';
  }
};

/* ======================= FILE LIST ======================= */
async function refreshFileList() {
  const r = await fetch('/api/files');
  const {files} = await r.json();
  const ul = $('fileList');
  ul.innerHTML = '';
  if (!files.length) {
    ul.innerHTML = '<li style="justify-content:center;color:var(--muted)">No files yet</li>';
    return;
  }
  files.forEach(name => {
    const li = document.createElement('li');
    li.innerHTML = `<span title="${name}">${name}</span>
                    <span class="x" title="Delete">✕</span>`;
    li.querySelector('.x').onclick = () => deleteFile(name);
    ul.appendChild(li);
  });
}

async function deleteFile(name) {
  if (!confirm(`Delete "${name}" and its embeddings?`)) return;
  await fetch('/api/files', {
    method:'DELETE',
    headers:{'Content-Type':'application/json'},
    body: JSON.stringify({name})
  });
  refreshFileList();
  addBotMsg(`🗑️ Deleted **${name}** from the vector database.`);
}

/* ======================= CHAT ======================= */
function addUserMsg(text) {
  const d = document.createElement('div');
  d.className = 'msg user';
  d.textContent = text;
  $('chat').appendChild(d);
  scrollDown();
}

function addBotMsg(markdown, sources = []) {
  const d = document.createElement('div');
  d.className = 'msg bot';
  d.innerHTML = marked.parse(markdown);
  if (sources.length) {
    const s = document.createElement('div');
    s.className = 'sources';
    s.textContent = 'Sources: ' + sources
        .map(x => `${x.source} (${x.score})`).join(', ');
    d.appendChild(s);
  }
  $('chat').appendChild(d);
  enhanceCodeBlocks(d);
  scrollDown();
}

function enhanceCodeBlocks(root) {
  root.querySelectorAll('pre').forEach(pre => {
    hljs.highlightElement(pre.querySelector('code') || pre);
    const btn = document.createElement('button');
    btn.className = 'copy-code';
    btn.textContent = 'Copy';
    btn.onclick = () => {
      navigator.clipboard.writeText(pre.innerText).then(() => {
        btn.textContent = 'Copied!';
        setTimeout(() => btn.textContent = 'Copy', 1200);
      });
    };
    pre.appendChild(btn);
  });
}

function scrollDown() {
  $('chat').scrollTop = $('chat').scrollHeight;
}

async function sendQuery() {
  const q = $('query').value.trim();
  if (!q) return;
  addUserMsg(q);
  $('query').value = '';
  $('send').disabled = true;
  $('status').textContent = 'thinking…';

  // typing indicator
  const typing = document.createElement('div');
  typing.className = 'msg bot';
  typing.textContent = 'AI is analyzing...';
  $('chat').appendChild(typing);
  scrollDown();

  try {
    const r = await fetch('/api/chat', {
      method:'POST',
      headers:{'Content-Type':'application/json'},
      body: JSON.stringify({query:q})
    });
    const data = await r.json();
    typing.remove();
    if (data.error) throw new Error(data.error);
    addBotMsg(data.answer, data.sources || []);
  } catch (e) {
    typing.remove();
    addBotMsg(`❌ \`${e.message}\``);
  } finally {
    $('send').disabled = false;
    $('status').textContent = '';
    $('query').focus();
  }
}

$('send').onclick = sendQuery;
$('query').addEventListener('keydown', e => {
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault();
    sendQuery();
  }
});

/* ======================= INIT ======================= */
refreshFileList();
</script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

The Python file app.py which connects to the LLM, vector db looks like this -

import os
import re
import shutil
import uuid
import tempfile

from flask import Flask, request, jsonify, render_template
from flask_cors import CORS

# ----- Vector DB -----
import chromadb
from chromadb.config import Settings

# ----- Embeddings -----
from sentence_transformers import SentenceTransformer

# ----- Keyword search -----
from rank_bm25 import BM25Okapi

# ----- File parsers -----
from pypdf import PdfReader
import docx
import openpyxl
from pptx import Presentation

# ----- LLM -----
import google.generativeai as genai

# =========================================================
# CONFIG
# =========================================================
GEMINI_API_KEY = "yourgoogleAPIKEY"   # hardcoded per spec
MODEL_NAME     = "gemini-3.5-flash-lite"      # swap if unavailable
EMBED_MODEL    = "all-MiniLM-L6-v2"
COLLECTION     = "venkatesh_docs"
UPLOAD_DIR     = "uploads"
CHROMA_DIR     = "chroma_db"

os.makedirs(UPLOAD_DIR, exist_ok=True)
os.makedirs(CHROMA_DIR, exist_ok=True)

genai.configure(api_key=GEMINI_API_KEY)
llm = genai.GenerativeModel(MODEL_NAME)

app = Flask(__name__)
CORS(app)

# =========================================================
# CHROMA + EMBEDDER (loaded once)
# =========================================================
chroma_client = chromadb.PersistentClient(
    path=CHROMA_DIR, settings=Settings(anonymized_telemetry=False)
)
collection = chroma_client.get_or_create_collection(
    name=COLLECTION, metadata={"hnsw:space": "cosine"}
)

embedder = SentenceTransformer(EMBED_MODEL)

# In-memory BM25 index rebuilt on demand
_bm25 = {"index": None, "ids": [], "docs": []}


def rebuild_bm25():
    """Rebuild BM25 from everything currently in ChromaDB."""
    data = collection.get(include=["documents"])
    docs = data.get("documents") or []
    ids = data.get("ids") or []
    if not docs:
        _bm25.update(index=None, ids=[], docs=[])
        return
    tokenized = [re.findall(r"\w+", d.lower()) for d in docs]
    _bm25.update(index=BM25Okapi(tokenized), ids=ids, docs=docs)


# =========================================================
# FILE EXTRACTION
# =========================================================
def chunk_text(text, size=800, overlap=100):
    text = re.sub(r"\s+", " ", text).strip()
    chunks, i = [], 0
    while i < len(text):
        chunks.append(text[i : i + size])
        i += size - overlap
    return [c for c in chunks if c.strip()]


def extract_pdf(path):
    reader = PdfReader(path)
    return "\n".join((p.extract_text() or "") for p in reader.pages)


def extract_docx(path):
    d = docx.Document(path)
    parts = [p.text for p in d.paragraphs]
    for t in d.tables:
        for row in t.rows:
            parts.append(" | ".join(c.text for c in row.cells))
    return "\n".join(parts)


def extract_xlsx(path):
    wb = openpyxl.load_workbook(path, data_only=True)
    out = []
    for ws in wb.worksheets:
        out.append(f"### Sheet: {ws.title}")
        for row in ws.iter_rows(values_only=True):
            out.append(" | ".join("" if v is None else str(v) for v in row))
    return "\n".join(out)


def extract_pptx(path):
    prs = Presentation(path)
    out = []
    for i, slide in enumerate(prs.slides, 1):
        out.append(f"### Slide {i}")
        for shape in slide.shapes:
            if hasattr(shape, "text") and shape.text.strip():
                out.append(shape.text)
    return "\n".join(out)


EXTRACTORS = {
    ".pdf":  extract_pdf,
    ".docx": extract_docx,
    ".xlsx": extract_xlsx,
    ".pptx": extract_pptx,
}


def ingest_file(filepath, original_name):
    ext = os.path.splitext(filepath)[1].lower()
    if ext not in EXTRACTORS:
        return 0
    try:
        raw = EXTRACTORS[ext](filepath)
    except Exception as e:
        print(f"[extract error] {original_name}: {e}")
        return 0

    chunks = chunk_text(raw)
    if not chunks:
        return 0

    ids = [f"{original_name}::{uuid.uuid4().hex[:8]}::{i}" for i in range(len(chunks))]
    metas = [{"source": original_name, "chunk": i} for i in range(len(chunks))]
    embs = embedder.encode(chunks, show_progress_bar=False).tolist()

    collection.add(ids=ids, documents=chunks, embeddings=embs, metadatas=metas)
    return len(chunks)


# =========================================================
# HYBRID SEARCH
# =========================================================
def hybrid_search(query, k=5, alpha=0.6):
    """
    Fuse dense (cosine similarity) and BM25 keyword scores.
    alpha = weight for dense; 1-alpha = weight for BM25.
    """
    if collection.count() == 0:
        return []

    q_emb = embedder.encode([query]).tolist()
    dense = collection.query(
        query_embeddings=q_emb, n_results=min(k * 3, collection.count())
    )

    dense_ids   = dense["ids"][0]
    dense_docs  = dense["documents"][0]
    dense_metas = dense["metadatas"][0]
    dense_dists = dense["distances"][0]          # cosine distance → similarity
    dense_scores = {i: 1 - d for i, d in zip(dense_ids, dense_dists)}

    bm25_scores = {}
    if _bm25["index"] is not None:
        toks = re.findall(r"\w+", query.lower())
        scores = _bm25["index"].get_scores(toks)
        max_s = max(scores) if len(scores) else 0
        for i, s in zip(_bm25["ids"], scores):
            bm25_scores[i] = (s / max_s) if max_s > 0 else 0.0

    # Normalise dense scores to [0,1]
    if dense_scores:
        dmax = max(dense_scores.values()) or 1
        dense_scores = {k: v / dmax for k, v in dense_scores.items()}

    all_ids = set(dense_scores) | set(bm25_scores)
    fused = {
        i: alpha * dense_scores.get(i, 0) + (1 - alpha) * bm25_scores.get(i, 0)
        for i in all_ids
    }

    top = sorted(fused.items(), key=lambda x: x[1], reverse=True)[:k]

    id_to_meta = {i: m for i, m in zip(dense_ids, dense_metas)}
    id_to_doc  = {i: d for i, d in zip(dense_ids, dense_docs)}

    results = []
    for cid, score in top:
        if cid in id_to_doc:
            results.append(
                {"text": id_to_doc[cid], "meta": id_to_meta.get(cid, {}), "score": score}
            )
    return results


# =========================================================
# ROUTES
# =========================================================
@app.route("/")
def index():
    return render_template("index.html")


@app.route("/api/upload", methods=["POST"])
def upload():
    """Accepts multiple files from 'files' field. Returns per-file status."""
    files = request.files.getlist("files")
    if not files:
        return jsonify({"error": "No files provided"}), 400

    saved, total_chunks = [], 0
    for f in files:
        if not f.filename:
            continue
        ext = os.path.splitext(f.filename)[1].lower()
        if ext not in EXTRACTORS:
            saved.append({"name": f.filename, "status": "unsupported"})
            continue
        dest = os.path.join(UPLOAD_DIR, f.filename)
        f.save(dest)
        n = ingest_file(dest, f.filename)
        total_chunks += n
        saved.append({"name": f.filename, "status": "ok", "chunks": n})

    rebuild_bm25()
    return jsonify({"files": saved, "total_chunks": total_chunks})


@app.route("/api/files", methods=["GET"])
def list_files():
    """Distinct source file names currently embedded."""
    data = collection.get(include=["metadatas"])
    names = sorted({m.get("source") for m in (data.get("metadatas") or []) if m})
    return jsonify({"files": names})


@app.route("/api/files", methods=["DELETE"])
def delete_file():
    name = (request.json or {}).get("name")
    if not name:
        return jsonify({"error": "name required"}), 400

    data = collection.get(include=["metadatas"])
    to_del = [
        cid for cid, m in zip(data["ids"], data["metadatas"])
        if m.get("source") == name
    ]
    if to_del:
        collection.delete(ids=to_del)

    # also remove the raw file if present
    raw = os.path.join(UPLOAD_DIR, name)
    if os.path.exists(raw):
        try:
            os.remove(raw)
        except OSError:
            pass

    rebuild_bm25()
    return jsonify({"deleted": len(to_del), "file": name})


@app.route("/api/chat", methods=["POST"])
def chat():
    q = (request.json or {}).get("query", "").strip()
    if not q:
        return jsonify({"error": "empty query"}), 400

    hits = hybrid_search(q, k=5)
    context = "\n\n---\n\n".join(
        f"[{h['meta'].get('source')}]\n{h['text']}" for h in hits
    ) or "(no relevant context found)"

    prompt = (
        "You are Venkatesh RAG Chatbot, a helpful assistant. "
        "Answer using the CONTEXT below. If the answer is not in the context, "
        "say so clearly.\n\n"
        f"CONTEXT:\n{context}\n\n"
        f"QUESTION: {q}\n\n"
        "Format the answer in clean Markdown. Use fenced code blocks for code."
    )

    try:
        resp = llm.generate_content(prompt)
        answer = resp.text
    except Exception as e:
        answer = f"**LLM error:** `{e}`\n\nContext retrieved:\n\n{context}"

    return jsonify({
        "answer": answer,
        "sources": [{"source": h["meta"].get("source"), "score": round(h["score"], 3)} for h in hits],
    })


if __name__ == "__main__":
    rebuild_bm25()
    app.run(host="0.0.0.0", port=5000, debug=True)
Enter fullscreen mode Exit fullscreen mode

requirements.txt should have the below ones -

flask
flask-cors
chromadb
sentence-transformers
rank-bm25
python-docx
openpyxl
python-pptx
pypdf
google-generativeai
numpy

Then run the application -
1/ open Folder in VS Code
2/ Open terminal
3/ Create Python environment
python -m venv myEnv
.\myEnv\Scripts\Activate.ps1
In case of issue,
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
4/ Install all supported software
python -m pip install -r requirements.txt
5/ After successful installation you should see the supporting exes under the scripts folder
6/ Finally run the application
python app.py


In case if you run into an issue please follow below commands accordingly


For this project use Python 3.12.0

deactivate
Remove-Item -Recurse -Force myEnv
python -m venv myEnv
.\myEnv\Scripts\Activate.ps1

Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned

python -m pip install --upgrade pip
python -m pip install -r requirements.txt

pip install flask
pip install flask-cors
pip install chromadb
pip install sentence-transformers
pip install rank-bm25
pip install python-docx
pip install openpyxl
pip install python-pptx
pip install pypdf
pip install google-generativeai
pip install numpy

General Erros -
A. torch / sentence-transformers build errors (very common on Windows)

python -m pip install torch --index-url https://download.pytorch.org/whl/cpu
python -m pip install -r requirements.txt

B. chromadb compile errors (hnswlib, chroma-hnswlib)

deactivate
Remove-Item -Recurse -Force myEnv
py -3.12 -m venv myEnv # or "python3.12" / "py -3.11"
.\myEnv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install -r requirements.txt

C. google-generativeai — quick alternative

python -m pip install flask flask-cors chromadb sentence-transformers rank-bm25 python-docx openpyxl python-pptx pypdf numpy

in APP.py

try:
import google.generativeai as genai
genai.configure(api_key=GEMINI_API_KEY)
llm = genai.GenerativeModel(MODEL_NAME)
except Exception as _e:
print(f"[warn] Gemini not available: {_e}")
llm = None

and in /api/chat guard with if llm is None: return jsonify({"answer": "...retrieved context...", ...}).

TL;DR for your exact error

inside the folder, with (myEnv) active
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python app.py

python -c "import flask, chromadb, sentence_transformers, rank_bm25, docx, openpyxl, pptx, pypdf, google.generativeai; print('all good')"
python -m pip list | findstr /I "flask chromadb sentence"

python app.py

Top comments (0)