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">></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>
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)
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)