A Retrieval-Augmented Generation (RAG) backend chatbot designed to parse, index, and search workout spreadsheets (.xlsx) using Gemini 2.5 Flash, Hybrid Ensemble Retrieval, Cross-Encoder Reranking, and Dynamic Spreadsheet Sizing.
┌───────────────────────────────────────────────────────────────────────────────────────────┐
│ 1. Ingestion Layer │
│ │
│ [ Raw Spreadsheet (.xlsx) ] ──► [ OpenPyXL Dump ] ──► [ LLM Parser ] │
│ │ │
│ [ Metadata Normalization ] ◄── [ Extracted Exercise JSON ] ◄┘ │
└─────────────────────────────────────────────┬─────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────────────────────────┐
│ 2. Document Pipeline │
│ │
│ ┌──► Dynamic Sizing Engine (K_retrieve, N_rerank) │
│ │ │
│ [ Document Pool ] ──► Sparse Vector Index (BM25 Keyword Search) ─┐ │
│ │ ├─► EnsembleRetriever │
│ └──► Dense Vector Index (ChromaDB) ─────┘ (40/60 Weighted) │
└─────────────────────────────────────────────┬─────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────────────────────────┐
│ 3. Query Chain │
│ │
│ [ User Question + History ] ──► [ Standalone Query Condensation ] │
│ │ │
│ ▼ │
│ [ Intent Classifier Routing ] │
│ / | \ │
│ ┌──────────────┘ │ └──────────────┐ │
│ ▼ ▼ ▼ │
│ [ History Intent ] [ Detail/Search Intent ] [ Session Search ]│
│ (Exhaustive Scan) (Alias Filtered Dense) (Ensemble Search)│
└─────────────────────────┬──────────────────────────┬──────────────────────────┬───────────┘
│ │ │
└──────────────────────────┼──────────────────────────┘
▼
┌───────────────────────────────────────────────────────────────────────────────────────────┐
│ 4. Reranking Layer │
│ │
│ [ Candidate Documents ] ──► Cross-Encoder (MS-Marco MiniLM) ──► Top N Cutoff Candidate │
└─────────────────────────────────────────────┬─────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────────────────────────┐
│ 5. Response │
│ │
│ [ Reranked Context + Condensed Query ] ──► [ Gemini 2.5 Flash LLM ] ──► [ User Response ]│
└───────────────────────────────────────────────────────────────────────────────────────────┘
Traditional RAG text splitters fail on spreadsheet grids because row/column boundaries disconnect exercises from their set/rep headers.
- Cell Dumps: Raw
.xlsxsheets are converted into cell coordinate tuples (row R | col C | value) and processed via Gemini 2.5 Flash with temperature0. - Chunking: Sessions are broken down into exercise-level Documents (each containing approximately 100 tokens of structured metadata).
Static retrieval limits (K) cause context window bloat on small queries and recall drops on large macrocycles. The system automatically inspects program metadata on startup:
- Calculates the total program span in weeks (
W) and maximum daily exercise volume (V). - Determines the Target Footprint (
T_max) required to satisfy both daily lookups and multi-week trend queries:
- Dynamically sets initial retrieval depth (
K_retrieve) and reranker cutoff (N_rerank) using safety margins (M = 4,P = 3):
In conversational turns (e.g., User: "What did I squat in Week 1?" → "How about Week 2?"), the query condensation module rephrases follow-up questions into context-aware search queries before triggering the retriever.
Queries pass through an intent classifier that evaluates user intent and applies query expansion (exercise_aliases.py):
- History/Trend Intent (e.g., "Show my bench progression over time"): Triggers
get_exercise_history, performing an exhaustive scan over all matching exercise records across the entire dataset. - Detail/Qualitative Intent (e.g., "What notes did I leave on deadlifts?"): Triggers
search_by_exercise, executing an alias-filtered dense search combined with Cross-Encoder reranking. - General Intent (e.g., "What exercises did I perform on Wednesday?"): Executes full hybrid sparse/dense search (
search_sessions).
- Bi-Encoder Layer: Combines sparse BM25 (weight
0.4) and dense ChromaDB embeddings (weight0.6, modelmodels/text-embedding-004). - Cross-Encoder Layer: Candidate chunks pass through a sentence-transformer cross-encoder (
cross-encoder/ms-marco-MiniLM-L-6-v2). This scores query-document attention pairs directly, resolving bi-encoder ranking limitations and boosting Mean Reciprocal Rank (MRR).
- Python 3.10+ (Python 3.13 supported)
- Google Gemini API Key (
GEMINI_API_KEYorGOOGLE_API_KEY)
- Clone the repository and enter the directory:
git clone https://github.com/your-username/WorkoutChatBot.git
cd WorkoutChatBot- Create and activate a virtual environment:
# Windows (PowerShell)
python -m venv venv
.\venv\Scripts\Activate.ps1# Linux/macOS
python3 -m venv venv
source venv/bin/activate- Install dependencies:
pip install fastapi uvicorn openpyxl langchain-core langchain-google-genai langchain-community langchain-chroma sentence-transformers huggingface_hub requests python-dotenv- Configure your Environment Variables:
Create a .env file in the root directory:
GEMINI_API_KEY=your_actual_gemini_api_key_hereTo start the REST API backend server with live reload:
python app.pyThe server will initialize at:
http://127.0.0.1:8000
To run the RAG system directly from the terminal without starting the web server:
python backend/core/main.py --xlsx "path/to/your_program.xlsx" --sources- URL:
GET /status - Description: Returns metadata on the indexed workout dataset and dynamic sizing bounds.
- Response (HTTP 200):
{
"is_indexed": true,
"total_documents": 96,
"weeks": 3,
"max_daily_exercises": 8,
"suggested_k_retrieve": 26,
"suggested_n_rerank": 13
}- URL:
POST /upload - Headers:
X-API-Key: <OPTIONAL_KEY> - Body:
multipart/form-data(file:.xlsxfile) - Description: Parses spreadsheet sheets via LLM, updates local JSON cache, and rebuilds vector and sparse indices.
- URL:
POST /query - Headers:
Content-Type: application/json - Body Example:
{
"question": "How did that compare to Week 2?",
"chat_history": [
{
"role": "user",
"content": "What did I do for Bench Press in Week 1?"
},
{
"role": "assistant",
"content": "On Week 1, you benched 185 lbs for 3 sets of 8 reps."
}
],
"use_reranker": true,
"model": "gemini-2.5-flash"
}- Response Example:
{
"standalone_query": "What did I do for Bench Press on Week 2?",
"answer": "On Week 2, you increased your weight on Bench Press to 195 lbs for 3 sets of 8 reps.",
"sources": [
{
"content": "Week: Week 2\nDay: Day 1\nWorkout: Upper Push\nExercise: Barbell Bench Press...",
"week": "Week 2",
"day": "Day 1",
"exercise_name": "barbell bench press",
"session_name": "Upper Push"
}
]
}- URL:
POST /reset - Description: Programmatically wipes ChromaDB collections, clears in-memory states, triggers garbage collection to release Windows SQLite handles, and removes local JSON caches.
The project includes an automated evaluation harness (eval_pipeline.py) that tests retrieval quality against a 20-scenario golden evaluation dataset (eval_queries.py).
- Mean Reciprocal Rank (MRR): Measures how quickly the first relevant chunk appears in retrieved candidate results.
- Precision@K & Recall@K: Evaluates hit ratios across variable cutoff points.
- F1-Score Curve: Mathematically identifies the optimal value of
K.
Note: Close any running
app.pyserver instances before executing evaluations to release file locks on ChromaDB.
python backend/core/eval_pipeline.py======================================================================
COMPARISON — RERANKING IMPACT
======================================================================
Metric No Rerank With Rerank Δ
Mean Reciprocal Rank (MRR) 0.8125 0.9643 +0.1518
Recall@1 62.50% 87.50% +25.00%
Recall@3 81.25% 93.75% +12.50%
Recall@5 87.50% 100.00% +12.50%
======================================================================
- PostgreSQL + pgvector Cloud Database Migration (Neon)
- Upstash Redis Caching
- Backend Hosting (Railway)
- Frontend Dashboard (React)
- Frontend Hosting (Vercel)