Spaces:
Sleeping
Sleeping
Upload 31 files
Browse files- .gitignore +7 -0
- Build_journey.md +75 -0
- Dockerfile +21 -0
- ProjectStructure.txt +23 -0
- Readme.md +72 -0
- app.py +118 -0
- index.html +205 -0
- pyproject.toml +11 -0
- requirements.txt +13 -0
- setup.py +9 -0
- src/__init__.py +0 -0
- src/__pycache__/__init__.cpython-311.pyc +0 -0
- src/__pycache__/__init__.cpython-312.pyc +0 -0
- src/__pycache__/embeddings.cpython-311.pyc +0 -0
- src/__pycache__/embeddings.cpython-312.pyc +0 -0
- src/__pycache__/loader.cpython-311.pyc +0 -0
- src/__pycache__/loader.cpython-312.pyc +0 -0
- src/__pycache__/logger.cpython-311.pyc +0 -0
- src/__pycache__/logger.cpython-312.pyc +0 -0
- src/__pycache__/rag_chain.cpython-311.pyc +0 -0
- src/__pycache__/rag_chain.cpython-312.pyc +0 -0
- src/__pycache__/splitter.cpython-311.pyc +0 -0
- src/__pycache__/splitter.cpython-312.pyc +0 -0
- src/__pycache__/vectorstore.cpython-311.pyc +0 -0
- src/__pycache__/vectorstore.cpython-312.pyc +0 -0
- src/embeddings.py +22 -0
- src/loader.py +23 -0
- src/logger.py +44 -0
- src/rag_chain.py +62 -0
- src/splitter.py +29 -0
- src/vectorstore.py +27 -0
.gitignore
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
link.txt
|
| 3 |
+
data
|
| 4 |
+
temp_uploads
|
| 5 |
+
data
|
| 6 |
+
logs
|
| 7 |
+
__pycache__
|
Build_journey.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
The Build Journey: Engineering a Full-Stack RAG Pipeline
|
| 2 |
+
|
| 3 |
+
This document serves as an exact record of the development process, architectural decisions, and the intense debugging journey I went through to build this dynamic Retrieval-Augmented Generation (RAG) application from scratch.
|
| 4 |
+
|
| 5 |
+
Phase 1: Architectural Design & Modularization
|
| 6 |
+
|
| 7 |
+
Instead of writing a monolithic script, I deliberately separated the backend logic into a clean, enterprise-grade src/ directory.
|
| 8 |
+
|
| 9 |
+
loader.py & splitter.py: Configured to ingest PDFs and cleanly chop them into 1000-character chunks with 200-character overlaps.
|
| 10 |
+
|
| 11 |
+
embeddings.py: Swapped legacy tools for the modern langchain-huggingface package, utilizing the lightweight all-MiniLM-L6-v2 model.
|
| 12 |
+
|
| 13 |
+
vectorstore.py: Set up ChromaDB to run ephemerally (in-memory). This was a crucial architectural decision to ensure the app remains stateless between sessions and doesn't eat up disk space on cloud deployments.
|
| 14 |
+
|
| 15 |
+
rag_chain.py: Wired the vector database up to Google's Gemini 1.5 Flash using LangChain Expression Language (LCEL).
|
| 16 |
+
|
| 17 |
+
Phase 2: The Dependency Wars
|
| 18 |
+
|
| 19 |
+
Once the modules were wired into FastAPI (app.py), the environment debugging began.
|
| 20 |
+
|
| 21 |
+
Conflict 1: The Protobuf Clash: Installing the LangChain Google integration brought in protobuf 6.33.2, which triggered errors against an existing local tensorflow environment (which demanded protobuf<6.0.0). I safely ignored this for the RAG app, noting that a downgrade to 5.x would patch it globally if needed.
|
| 22 |
+
|
| 23 |
+
Conflict 2: The Missing Neural Network (NameError 'nn' is not defined): The Hugging Face transformers library crashed upon startup. I diagnosed this as an out-of-sync local PyTorch installation and executed a forced upgrade: pip install torch accelerate transformers --upgrade.
|
| 24 |
+
|
| 25 |
+
Conflict 3: Breaking Computer Vision: Upgrading PyTorch to 2.12.0 triggered dependency warnings for existing facenet-pytorch and torchvision libraries. I made the engineering decision to ignore these warnings, correctly identifying that my text-based RAG pipeline did not rely on those vision libraries.
|
| 26 |
+
|
| 27 |
+
Phase 3: The Windows "Boss Fights"
|
| 28 |
+
|
| 29 |
+
Running a complex AI application on local Windows environments introduced a series of highly specific OS-level bugs that required immediate patching.
|
| 30 |
+
|
| 31 |
+
Boss 1: The Emoji Crash (UnicodeEncodeError)
|
| 32 |
+
|
| 33 |
+
The Bug: The server crashed immediately during the startup event with UnicodeEncodeError: 'charmap' codec can't encode character '\U0001f680'.
|
| 34 |
+
|
| 35 |
+
The Diagnosis: The Windows terminal (cp1252 encoding) panicked when my Python logger tried to print a rocket emoji (🚀).
|
| 36 |
+
|
| 37 |
+
The Fix: Removed the emoji from the console print statement and updated the logger.py RotatingFileHandler to explicitly use encoding="utf-8" to prevent future crashes when reading complex PDF text.
|
| 38 |
+
|
| 39 |
+
Boss 2: The Hot-Reload Crash (forrtl: error (200))
|
| 40 |
+
|
| 41 |
+
The Bug: The server booted, but crashed with a severe libifcoremd.dll error whenever an endpoint was hit.
|
| 42 |
+
|
| 43 |
+
The Diagnosis: I identified a known architectural bug where the C++ backends of PyTorch and NumPy clash with Uvicorn's --reload (WatchFiles) threading on Windows, causing a false Control-C abort sequence.
|
| 44 |
+
|
| 45 |
+
The Fix: Disabled hot-reloading (uvicorn app:app), which completely stabilized the server memory.
|
| 46 |
+
|
| 47 |
+
Boss 3: The File System Collision (WinError 183)
|
| 48 |
+
|
| 49 |
+
The Bug: Uploading a PDF triggered FileExistsError: [WinError 183] Cannot create a file when that file already exists: 'data'.
|
| 50 |
+
|
| 51 |
+
The Diagnosis: I had written os.makedirs("data", exist_ok=True). However, a local file named data (no extension) already existed in the directory. Windows threw a fatal error because it couldn't create a folder with the same name as a file.
|
| 52 |
+
|
| 53 |
+
The Fix: Refactored the temp-file routing to use a highly specific temp_uploads/ directory, permanently bypassing the collision.
|
| 54 |
+
|
| 55 |
+
Phase 4: The Phantom Cache
|
| 56 |
+
|
| 57 |
+
The Bug: The pipeline failed midway through processing with ImportError: Could not import sentence_transformers. However, running pip install returned "Requirement already satisfied".
|
| 58 |
+
|
| 59 |
+
The Diagnosis: A network timeout earlier in the build process had created the sentence-transformers folder in the pip cache, but failed to download the actual code. Pip was being tricked by an empty folder.
|
| 60 |
+
|
| 61 |
+
The Fix: Used the "sledgehammer" command: pip install --force-reinstall --no-cache-dir sentence-transformers to bypass the corrupted local cache and force a fresh binary download. This successfully unblocked the embedding pipeline.
|
| 62 |
+
|
| 63 |
+
Phase 5: The "No-React" Full-Stack Pivot
|
| 64 |
+
|
| 65 |
+
With the backend fully operational, I needed a frontend. Instead of context-switching to a completely different language and framework (React) and managing two separate servers, I engineered an all-in-one solution.
|
| 66 |
+
|
| 67 |
+
I wrote a beautiful, single-page Vanilla HTML/JS frontend styled with Tailwind CSS.
|
| 68 |
+
|
| 69 |
+
I embedded it directly into the FastAPI application using HTMLResponse.
|
| 70 |
+
|
| 71 |
+
This allowed me to serve a complete, professional web application from a single Python server, drastically simplifying the deployment process.
|
| 72 |
+
|
| 73 |
+
Conclusion
|
| 74 |
+
|
| 75 |
+
This build journey evolved from writing simple Python scripts to engineering a robust, modular microservice architecture. By systematically hunting down and resolving complex OS-level threading issues, file system quirks, and corrupted dependency caches, I successfully delivered a production-ready AI application.
|
Dockerfile
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use a lightweight Python image
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
+
|
| 4 |
+
# Set the working directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Copy the requirements file
|
| 8 |
+
COPY requirements.txt .
|
| 9 |
+
|
| 10 |
+
# Install dependencies (We force CPU-only PyTorch to save massive amounts of space!)
|
| 11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
+
RUN pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
|
| 13 |
+
|
| 14 |
+
# Copy all your code into the container
|
| 15 |
+
COPY . .
|
| 16 |
+
|
| 17 |
+
# Hugging Face requires apps to run on port 7860
|
| 18 |
+
ENV PORT=7860
|
| 19 |
+
|
| 20 |
+
# Command to run your FastAPI app
|
| 21 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
ProjectStructure.txt
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
rag-app/
|
| 2 |
+
│
|
| 3 |
+
├── app.py # FastAPI application
|
| 4 |
+
├── requirements.txt # Python dependencies
|
| 5 |
+
├── render.yaml # Optional Render configuration
|
| 6 |
+
├── .gitignore
|
| 7 |
+
├── .env # Local development only
|
| 8 |
+
│
|
| 9 |
+
├── data/
|
| 10 |
+
│ ├── document1.pdf
|
| 11 |
+
│ ├── document2.pdf
|
| 12 |
+
│ └── ...
|
| 13 |
+
│
|
| 14 |
+
├── chroma_db/ # Generated vector database
|
| 15 |
+
│
|
| 16 |
+
├── src/
|
| 17 |
+
│ ├── loader.py # Load PDFs/documents
|
| 18 |
+
│ ├── splitter.py # Text chunking
|
| 19 |
+
│ ├── embeddings.py # Embedding model
|
| 20 |
+
│ ├── vectorstore.py # ChromaDB setup
|
| 21 |
+
│ └── rag_chain.py # LangChain retrieval chain
|
| 22 |
+
│ |-- logger.py
|
| 23 |
+
└── README.md
|
Readme.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Dynamic RAG Engine 🚀
|
| 2 |
+
|
| 3 |
+
A full-stack, ephemeral Retrieval-Augmented Generation (RAG) API built with FastAPI, LangChain, and Google's Gemini 1.5 Flash.
|
| 4 |
+
|
| 5 |
+
This application allows users to upload PDF documents dynamically, vectorizes the text in real-time using local Hugging Face embeddings, and serves a chat interface to query the document using an LLM.
|
| 6 |
+
|
| 7 |
+
🏗️ Architecture
|
| 8 |
+
|
| 9 |
+
Backend Framework: FastAPI (Asynchronous, High-Performance)
|
| 10 |
+
|
| 11 |
+
Orchestration: LangChain
|
| 12 |
+
|
| 13 |
+
Embedding Model: all-MiniLM-L6-v2 (via Hugging Face)
|
| 14 |
+
|
| 15 |
+
Vector Database: ChromaDB (Ephemeral / In-Memory for session security)
|
| 16 |
+
|
| 17 |
+
LLM: Google Gemini 1.5 Flash
|
| 18 |
+
|
| 19 |
+
Frontend: Vanilla HTML/JS with Tailwind CSS (Served via FastAPI)
|
| 20 |
+
|
| 21 |
+
✨ Features
|
| 22 |
+
|
| 23 |
+
Zero-Footprint DB: Uses an in-memory ChromaDB instance that wipes clean after the session, ensuring data privacy and saving server storage.
|
| 24 |
+
|
| 25 |
+
Modular Pipeline: Document loading, text splitting, embedding, and chain building are separated into clean, maintainable micro-modules (src/).
|
| 26 |
+
|
| 27 |
+
Custom Logging: Built-in rotating file loggers and middleware for precise API request tracing.
|
| 28 |
+
|
| 29 |
+
Integrated UI: A modern, single-page application built directly into the root API endpoint.
|
| 30 |
+
|
| 31 |
+
🚀 Quick Start (Local Deployment)
|
| 32 |
+
|
| 33 |
+
1. Clone the repository
|
| 34 |
+
|
| 35 |
+
git clone [https://github.com/yourusername/dynamic-rag-fastapi.git](https://github.com/yourusername/dynamic-rag-fastapi.git)
|
| 36 |
+
cd dynamic-rag-fastapi
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
2. Install dependencies
|
| 40 |
+
|
| 41 |
+
It is recommended to use a virtual environment.
|
| 42 |
+
|
| 43 |
+
pip install -r requirements.txt
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
3. Set your Environment Variables
|
| 47 |
+
|
| 48 |
+
Create a .env file in the root directory or export the variable in your terminal:
|
| 49 |
+
|
| 50 |
+
export GOOGLE_API_KEY="your_gemini_api_key_here"
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
4. Run the Server
|
| 54 |
+
|
| 55 |
+
Note for Windows users: Avoid using --reload to prevent Uvicorn threading clashes with local PyTorch installations.
|
| 56 |
+
|
| 57 |
+
uvicorn app:app
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
5. Access the App
|
| 61 |
+
|
| 62 |
+
Web UI: http://127.0.0.1:8000/
|
| 63 |
+
|
| 64 |
+
Interactive API Docs (Swagger): http://127.0.0.1:8000/docs
|
| 65 |
+
|
| 66 |
+
📡 API Endpoints
|
| 67 |
+
|
| 68 |
+
GET /: Serves the frontend web interface.
|
| 69 |
+
|
| 70 |
+
POST /upload: Accepts a multipart/form-data PDF, chunks the text, creates embeddings, and initializes the RAG chain.
|
| 71 |
+
|
| 72 |
+
POST /chat: Accepts a JSON payload {"message": "string"} and returns the LLM's context-aware response.
|
app.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, UploadFile, File, HTTPException, Request
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
import os
|
| 5 |
+
import shutil
|
| 6 |
+
import time
|
| 7 |
+
|
| 8 |
+
# 1. Import our custom modules from the src/ folder
|
| 9 |
+
from src.logger import logger
|
| 10 |
+
from src.loader import load_pdf
|
| 11 |
+
from src.splitter import split_text
|
| 12 |
+
from src.embeddings import get_embeddings
|
| 13 |
+
from src.vectorstore import create_vectorstore
|
| 14 |
+
from src.rag_chain import build_chain
|
| 15 |
+
from fastapi.responses import HTMLResponse
|
| 16 |
+
# 2. Initialize FastAPI
|
| 17 |
+
app = FastAPI(title="Dynamic PDF RAG API")
|
| 18 |
+
|
| 19 |
+
# Allow frontend applications (like React) to communicate with this API
|
| 20 |
+
app.add_middleware(
|
| 21 |
+
CORSMiddleware,
|
| 22 |
+
allow_origins=["*"],
|
| 23 |
+
allow_credentials=True,
|
| 24 |
+
allow_methods=["*"],
|
| 25 |
+
allow_headers=["*"],
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# ==========================================
|
| 29 |
+
# AUTO-LOGGING MIDDLEWARE
|
| 30 |
+
# ==========================================
|
| 31 |
+
@app.middleware("http")
|
| 32 |
+
async def log_requests(request: Request, call_next):
|
| 33 |
+
start_time = time.time()
|
| 34 |
+
logger.info(f"Incoming request: {request.method} {request.url.path}")
|
| 35 |
+
response = await call_next(request)
|
| 36 |
+
process_time = (time.time() - start_time) * 1000
|
| 37 |
+
logger.info(f"Completed {request.method} {request.url.path} - Status: {response.status_code} - Time: {process_time:.2f}ms")
|
| 38 |
+
return response
|
| 39 |
+
|
| 40 |
+
# ==========================================
|
| 41 |
+
# GLOBAL STATE & MODELS
|
| 42 |
+
# ==========================================
|
| 43 |
+
class ChatRequest(BaseModel):
|
| 44 |
+
message: str
|
| 45 |
+
|
| 46 |
+
# This holds our LangChain pipeline in memory for the active session
|
| 47 |
+
current_chain = None
|
| 48 |
+
|
| 49 |
+
@app.on_event("startup")
|
| 50 |
+
async def startup_event():
|
| 51 |
+
logger.info("Starting up Dynamic RAG API Server...")
|
| 52 |
+
|
| 53 |
+
# ==========================================
|
| 54 |
+
# API ENDPOINTS
|
| 55 |
+
# ==========================================
|
| 56 |
+
@app.post("/upload")
|
| 57 |
+
async def upload_pdf(file: UploadFile = File(...)):
|
| 58 |
+
"""Accepts a PDF, processes it through the RAG pipeline, and readies the chat."""
|
| 59 |
+
global current_chain
|
| 60 |
+
logger.info(f"Received file upload: {file.filename}")
|
| 61 |
+
|
| 62 |
+
# 1. Save the file temporarily in a specific uploads folder
|
| 63 |
+
upload_dir = "temp_uploads"
|
| 64 |
+
temp_file_path = f"{upload_dir}/{file.filename}"
|
| 65 |
+
|
| 66 |
+
# Create the directory safely
|
| 67 |
+
os.makedirs(upload_dir, exist_ok=True)
|
| 68 |
+
|
| 69 |
+
try:
|
| 70 |
+
with open(temp_file_path, "wb") as buffer:
|
| 71 |
+
shutil.copyfileobj(file.file, buffer)
|
| 72 |
+
|
| 73 |
+
logger.info("Starting document processing pipeline...")
|
| 74 |
+
|
| 75 |
+
# 2. THE PIPELINE EXECUTES HERE
|
| 76 |
+
docs = load_pdf(temp_file_path)
|
| 77 |
+
chunks = split_text(docs)
|
| 78 |
+
embeddings = get_embeddings()
|
| 79 |
+
vectorstore = create_vectorstore(chunks, embeddings)
|
| 80 |
+
current_chain = build_chain(vectorstore)
|
| 81 |
+
|
| 82 |
+
logger.info(f"Successfully processed {file.filename} and activated RAG chain.")
|
| 83 |
+
return {"status": "success", "message": f"{file.filename} processed! You can now chat."}
|
| 84 |
+
|
| 85 |
+
except Exception as e:
|
| 86 |
+
logger.error(f"Failed to process PDF: {str(e)}", exc_info=True)
|
| 87 |
+
raise HTTPException(status_code=500, detail=f"Failed to process PDF: {str(e)}")
|
| 88 |
+
|
| 89 |
+
finally:
|
| 90 |
+
# 3. Clean up the temporary PDF file to save server space
|
| 91 |
+
if os.path.exists(temp_file_path):
|
| 92 |
+
os.remove(temp_file_path)
|
| 93 |
+
logger.debug(f"Cleaned up temporary file: {temp_file_path}")
|
| 94 |
+
|
| 95 |
+
@app.post("/chat")
|
| 96 |
+
async def chat_endpoint(request: ChatRequest):
|
| 97 |
+
"""Answers questions based on the currently uploaded PDF."""
|
| 98 |
+
global current_chain
|
| 99 |
+
|
| 100 |
+
if current_chain is None:
|
| 101 |
+
logger.warning("User attempted to chat without uploading a PDF first.")
|
| 102 |
+
raise HTTPException(status_code=400, detail="No PDF uploaded yet. Please upload a document first.")
|
| 103 |
+
|
| 104 |
+
try:
|
| 105 |
+
logger.info(f"User asked: '{request.message}'")
|
| 106 |
+
answer = current_chain.invoke(request.message)
|
| 107 |
+
logger.debug("Successfully generated LLM response.")
|
| 108 |
+
return {"reply": answer}
|
| 109 |
+
except Exception as e:
|
| 110 |
+
logger.error(f"Error during chat generation: {str(e)}", exc_info=True)
|
| 111 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 112 |
+
|
| 113 |
+
@app.get("/")
|
| 114 |
+
async def serve_frontend():
|
| 115 |
+
"""Serves the frontend HTML UI."""
|
| 116 |
+
with open("index.html", "r", encoding="utf-8") as f:
|
| 117 |
+
html_content = f.read()
|
| 118 |
+
return HTMLResponse(content=html_content)
|
index.html
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Robotics AI Assistant</title>
|
| 7 |
+
<!-- Use Tailwind CSS for instant, beautiful styling without needing a CSS file -->
|
| 8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
+
<style>
|
| 10 |
+
/* A small animation for the loading dots */
|
| 11 |
+
.dot-flashing {
|
| 12 |
+
animation: dotFlashing 1s infinite linear alternate;
|
| 13 |
+
}
|
| 14 |
+
.dot-flashing:nth-child(2) { animation-delay: 0.2s; }
|
| 15 |
+
.dot-flashing:nth-child(3) { animation-delay: 0.4s; }
|
| 16 |
+
@keyframes dotFlashing {
|
| 17 |
+
0% { opacity: 0.2; transform: scale(0.8); }
|
| 18 |
+
100% { opacity: 1; transform: scale(1.2); }
|
| 19 |
+
}
|
| 20 |
+
</style>
|
| 21 |
+
</head>
|
| 22 |
+
<body class="bg-slate-50 h-screen flex flex-col items-center justify-center p-4 font-sans">
|
| 23 |
+
|
| 24 |
+
<div class="w-full max-w-3xl bg-white rounded-2xl shadow-xl overflow-hidden flex flex-col h-[90vh]">
|
| 25 |
+
|
| 26 |
+
<!-- Header -->
|
| 27 |
+
<div class="bg-slate-900 text-white p-5 flex items-center justify-between">
|
| 28 |
+
<div>
|
| 29 |
+
<h1 class="font-bold text-xl flex items-center gap-2">
|
| 30 |
+
🤖 Robotics AI Assistant
|
| 31 |
+
</h1>
|
| 32 |
+
<p class="text-sm text-slate-400 mt-1">FastAPI + LangChain + Gemini</p>
|
| 33 |
+
</div>
|
| 34 |
+
|
| 35 |
+
<!-- Upload Section -->
|
| 36 |
+
<div class="flex items-center gap-2 bg-slate-800 p-2 rounded-lg">
|
| 37 |
+
<input type="file" id="pdf-upload" accept="application/pdf" class="text-sm text-slate-300 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-600 file:text-white hover:file:bg-blue-700 cursor-pointer">
|
| 38 |
+
<button onclick="uploadPDF()" id="upload-btn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-full text-sm font-semibold transition-colors">
|
| 39 |
+
Upload
|
| 40 |
+
</button>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
<!-- System Message Bar -->
|
| 45 |
+
<div id="status-bar" class="bg-blue-50 text-blue-800 text-sm p-3 text-center border-b border-blue-100 hidden font-medium">
|
| 46 |
+
System status goes here...
|
| 47 |
+
</div>
|
| 48 |
+
|
| 49 |
+
<!-- Chat Area -->
|
| 50 |
+
<div id="chat-box" class="flex-1 overflow-y-auto p-6 space-y-4 bg-slate-50">
|
| 51 |
+
<!-- Initial Bot Message -->
|
| 52 |
+
<div class="flex gap-4">
|
| 53 |
+
<div class="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center shrink-0 text-white text-sm">AI</div>
|
| 54 |
+
<div class="bg-white p-4 rounded-2xl rounded-tl-none shadow-sm border border-slate-100 text-slate-700 max-w-[80%]">
|
| 55 |
+
Hello! Please upload your PDF document using the button in the top right, and then ask me anything about it.
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
|
| 60 |
+
<!-- Input Area -->
|
| 61 |
+
<div class="p-4 bg-white border-t border-slate-200">
|
| 62 |
+
<form onsubmit="sendMessage(event)" class="flex gap-3">
|
| 63 |
+
<input type="text" id="user-input" placeholder="Ask a question about the PDF..." disabled
|
| 64 |
+
class="flex-1 px-4 py-3 bg-slate-50 border border-slate-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all disabled:opacity-50 disabled:cursor-not-allowed">
|
| 65 |
+
<button type="submit" id="send-btn" disabled
|
| 66 |
+
class="px-6 py-3 bg-slate-900 text-white rounded-xl hover:bg-slate-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed font-semibold shadow-sm">
|
| 67 |
+
Send
|
| 68 |
+
</button>
|
| 69 |
+
</form>
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
<script>
|
| 74 |
+
// --- Logic to handle File Upload ---
|
| 75 |
+
async function uploadPDF() {
|
| 76 |
+
const fileInput = document.getElementById('pdf-upload');
|
| 77 |
+
const file = fileInput.files[0];
|
| 78 |
+
const uploadBtn = document.getElementById('upload-btn');
|
| 79 |
+
const statusBar = document.getElementById('status-bar');
|
| 80 |
+
|
| 81 |
+
if (!file) {
|
| 82 |
+
alert("Please select a PDF file first.");
|
| 83 |
+
return;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// Update UI to show loading
|
| 87 |
+
uploadBtn.innerText = "Processing...";
|
| 88 |
+
uploadBtn.disabled = true;
|
| 89 |
+
statusBar.innerText = `Processing ${file.name}... This may take a moment.`;
|
| 90 |
+
statusBar.classList.remove('hidden', 'bg-red-50', 'text-red-800');
|
| 91 |
+
statusBar.classList.add('bg-blue-50', 'text-blue-800');
|
| 92 |
+
|
| 93 |
+
const formData = new FormData();
|
| 94 |
+
formData.append("file", file);
|
| 95 |
+
|
| 96 |
+
try {
|
| 97 |
+
// Send the file to our FastAPI /upload endpoint
|
| 98 |
+
const response = await fetch('/upload', {
|
| 99 |
+
method: 'POST',
|
| 100 |
+
body: formData
|
| 101 |
+
});
|
| 102 |
+
|
| 103 |
+
const data = await response.json();
|
| 104 |
+
|
| 105 |
+
if (response.ok) {
|
| 106 |
+
statusBar.innerText = "✅ PDF processed successfully! You can now chat.";
|
| 107 |
+
statusBar.classList.add('bg-green-50', 'text-green-800');
|
| 108 |
+
document.getElementById('user-input').disabled = false;
|
| 109 |
+
document.getElementById('send-btn').disabled = false;
|
| 110 |
+
uploadBtn.innerText = "Uploaded";
|
| 111 |
+
} else {
|
| 112 |
+
throw new Error(data.detail || "Failed to upload");
|
| 113 |
+
}
|
| 114 |
+
} catch (error) {
|
| 115 |
+
statusBar.innerText = `❌ Error: ${error.message}`;
|
| 116 |
+
statusBar.classList.add('bg-red-50', 'text-red-800');
|
| 117 |
+
uploadBtn.innerText = "Upload";
|
| 118 |
+
uploadBtn.disabled = false;
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// --- Logic to handle Chat Messages ---
|
| 123 |
+
async function sendMessage(event) {
|
| 124 |
+
event.preventDefault(); // Prevent page reload
|
| 125 |
+
|
| 126 |
+
const inputField = document.getElementById('user-input');
|
| 127 |
+
const message = inputField.value.trim();
|
| 128 |
+
const chatBox = document.getElementById('chat-box');
|
| 129 |
+
const sendBtn = document.getElementById('send-btn');
|
| 130 |
+
|
| 131 |
+
if (!message) return;
|
| 132 |
+
|
| 133 |
+
// 1. Add User message to UI
|
| 134 |
+
inputField.value = '';
|
| 135 |
+
inputField.disabled = true;
|
| 136 |
+
sendBtn.disabled = true;
|
| 137 |
+
|
| 138 |
+
chatBox.innerHTML += `
|
| 139 |
+
<div class="flex gap-4 flex-row-reverse">
|
| 140 |
+
<div class="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center shrink-0 text-white text-sm">U</div>
|
| 141 |
+
<div class="bg-blue-600 text-white p-4 rounded-2xl rounded-tr-none shadow-sm max-w-[80%]">
|
| 142 |
+
${message}
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
+
`;
|
| 146 |
+
chatBox.scrollTop = chatBox.scrollHeight;
|
| 147 |
+
|
| 148 |
+
// 2. Add Loading Indicator
|
| 149 |
+
const loadingId = "loading-" + Date.now();
|
| 150 |
+
chatBox.innerHTML += `
|
| 151 |
+
<div id="${loadingId}" class="flex gap-4">
|
| 152 |
+
<div class="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center shrink-0 text-white text-sm">AI</div>
|
| 153 |
+
<div class="bg-white p-4 rounded-2xl rounded-tl-none shadow-sm border border-slate-100 text-slate-700 flex gap-1 items-center h-12">
|
| 154 |
+
<div class="w-2 h-2 bg-slate-400 rounded-full dot-flashing"></div>
|
| 155 |
+
<div class="w-2 h-2 bg-slate-400 rounded-full dot-flashing"></div>
|
| 156 |
+
<div class="w-2 h-2 bg-slate-400 rounded-full dot-flashing"></div>
|
| 157 |
+
</div>
|
| 158 |
+
</div>
|
| 159 |
+
`;
|
| 160 |
+
chatBox.scrollTop = chatBox.scrollHeight;
|
| 161 |
+
|
| 162 |
+
try {
|
| 163 |
+
// 3. Send message to our FastAPI /chat endpoint
|
| 164 |
+
const response = await fetch('/chat', {
|
| 165 |
+
method: 'POST',
|
| 166 |
+
headers: { 'Content-Type': 'application/json' },
|
| 167 |
+
body: JSON.stringify({ message: message })
|
| 168 |
+
});
|
| 169 |
+
|
| 170 |
+
const data = await response.json();
|
| 171 |
+
|
| 172 |
+
// Remove loading indicator
|
| 173 |
+
document.getElementById(loadingId).remove();
|
| 174 |
+
|
| 175 |
+
if (response.ok) {
|
| 176 |
+
// Add Bot response to UI
|
| 177 |
+
chatBox.innerHTML += `
|
| 178 |
+
<div class="flex gap-4">
|
| 179 |
+
<div class="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center shrink-0 text-white text-sm">AI</div>
|
| 180 |
+
<div class="bg-white p-4 rounded-2xl rounded-tl-none shadow-sm border border-slate-100 text-slate-700 max-w-[80%] whitespace-pre-wrap leading-relaxed">${data.reply}</div>
|
| 181 |
+
</div>
|
| 182 |
+
`;
|
| 183 |
+
} else {
|
| 184 |
+
throw new Error(data.detail || "Failed to get response");
|
| 185 |
+
}
|
| 186 |
+
} catch (error) {
|
| 187 |
+
document.getElementById(loadingId).remove();
|
| 188 |
+
chatBox.innerHTML += `
|
| 189 |
+
<div class="flex gap-4">
|
| 190 |
+
<div class="w-8 h-8 rounded-full bg-red-500 flex items-center justify-center shrink-0 text-white text-sm">!</div>
|
| 191 |
+
<div class="bg-red-50 text-red-800 p-4 rounded-2xl rounded-tl-none shadow-sm border border-red-100 max-w-[80%]">
|
| 192 |
+
Error communicating with server: ${error.message}
|
| 193 |
+
</div>
|
| 194 |
+
</div>
|
| 195 |
+
`;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
inputField.disabled = false;
|
| 199 |
+
sendBtn.disabled = false;
|
| 200 |
+
inputField.focus();
|
| 201 |
+
chatBox.scrollTop = chatBox.scrollHeight;
|
| 202 |
+
}
|
| 203 |
+
</script>
|
| 204 |
+
</body>
|
| 205 |
+
</html>
|
pyproject.toml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "src"
|
| 3 |
+
version = "0.0.1"
|
| 4 |
+
description = "A dynamic RAG backend API using FastAPI, LangChain, and ChromaDB"
|
| 5 |
+
authors = [{name = "Abdul Rauf", email = "raufyawar@gmail.com"}]
|
| 6 |
+
|
| 7 |
+
[tool.setuptools]
|
| 8 |
+
packages = {find = {}}
|
| 9 |
+
|
| 10 |
+
[tool.setuptools.dynamic]
|
| 11 |
+
dependencies = {file = "requirements.txt"}
|
requirements.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
langchain-community
|
| 2 |
+
pypdf
|
| 3 |
+
langchain-text-splitters
|
| 4 |
+
langchain-huggingface
|
| 5 |
+
langchain-chroma
|
| 6 |
+
langchain-core
|
| 7 |
+
langchain-google-genai
|
| 8 |
+
fastapi
|
| 9 |
+
sentence-transformers
|
| 10 |
+
python-multipart
|
| 11 |
+
pysqlite3-binary
|
| 12 |
+
uvicorn
|
| 13 |
+
-e .
|
setup.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from setuptools import setup, find_packages
|
| 2 |
+
|
| 3 |
+
setup(
|
| 4 |
+
name="src",
|
| 5 |
+
version="0.0.1",
|
| 6 |
+
author="Abdul Rauf",
|
| 7 |
+
author_email="raufyawar@gmail.com",
|
| 8 |
+
packages=find_packages()
|
| 9 |
+
)
|
src/__init__.py
ADDED
|
File without changes
|
src/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (181 Bytes). View file
|
|
|
src/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (169 Bytes). View file
|
|
|
src/__pycache__/embeddings.cpython-311.pyc
ADDED
|
Binary file (1.23 kB). View file
|
|
|
src/__pycache__/embeddings.cpython-312.pyc
ADDED
|
Binary file (1.11 kB). View file
|
|
|
src/__pycache__/loader.cpython-311.pyc
ADDED
|
Binary file (1.28 kB). View file
|
|
|
src/__pycache__/loader.cpython-312.pyc
ADDED
|
Binary file (1.15 kB). View file
|
|
|
src/__pycache__/logger.cpython-311.pyc
ADDED
|
Binary file (1.67 kB). View file
|
|
|
src/__pycache__/logger.cpython-312.pyc
ADDED
|
Binary file (1.54 kB). View file
|
|
|
src/__pycache__/rag_chain.cpython-311.pyc
ADDED
|
Binary file (3.17 kB). View file
|
|
|
src/__pycache__/rag_chain.cpython-312.pyc
ADDED
|
Binary file (2.92 kB). View file
|
|
|
src/__pycache__/splitter.cpython-311.pyc
ADDED
|
Binary file (1.48 kB). View file
|
|
|
src/__pycache__/splitter.cpython-312.pyc
ADDED
|
Binary file (1.32 kB). View file
|
|
|
src/__pycache__/vectorstore.cpython-311.pyc
ADDED
|
Binary file (1.27 kB). View file
|
|
|
src/__pycache__/vectorstore.cpython-312.pyc
ADDED
|
Binary file (1.16 kB). View file
|
|
|
src/embeddings.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 2 |
+
from src.logger import logger
|
| 3 |
+
|
| 4 |
+
def get_embeddings(model_name: str = "all-MiniLM-L6-v2"):
|
| 5 |
+
"""
|
| 6 |
+
Initializes and returns the HuggingFace embedding model.
|
| 7 |
+
This model translates text chunks into mathematical vectors.
|
| 8 |
+
"""
|
| 9 |
+
logger.info(f"Initializing embedding model: {model_name}")
|
| 10 |
+
|
| 11 |
+
try:
|
| 12 |
+
# Initialize the HuggingFace embeddings model
|
| 13 |
+
embeddings = HuggingFaceEmbeddings(model_name=model_name)
|
| 14 |
+
|
| 15 |
+
logger.info("Successfully loaded embedding model.")
|
| 16 |
+
|
| 17 |
+
# Return the embedding object so ChromaDB can use it
|
| 18 |
+
return embeddings
|
| 19 |
+
|
| 20 |
+
except Exception as e:
|
| 21 |
+
logger.error(f"Failed to initialize embeddings: {str(e)}", exc_info=True)
|
| 22 |
+
raise e
|
src/loader.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_community.document_loaders import PyPDFLoader
|
| 2 |
+
from src.logger import logger
|
| 3 |
+
|
| 4 |
+
def load_pdf(file_path: str):
|
| 5 |
+
"""
|
| 6 |
+
Loads a PDF file from the given path and returns a list of LangChain Document objects.
|
| 7 |
+
"""
|
| 8 |
+
logger.info(f"Attempting to load PDF from: {file_path}")
|
| 9 |
+
try:
|
| 10 |
+
# Initialize the loader
|
| 11 |
+
loader = PyPDFLoader(file_path)
|
| 12 |
+
|
| 13 |
+
# Load the document
|
| 14 |
+
docs = loader.load()
|
| 15 |
+
|
| 16 |
+
logger.info(f"Successfully loaded {len(docs)} pages from the PDF.")
|
| 17 |
+
|
| 18 |
+
# Return the docs so the splitter can use them
|
| 19 |
+
return docs
|
| 20 |
+
|
| 21 |
+
except Exception as e:
|
| 22 |
+
logger.error(f"PDF Loader failed to read {file_path}: {str(e)}", exc_info=True)
|
| 23 |
+
raise e
|
src/logger.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import sys
|
| 3 |
+
from logging.handlers import RotatingFileHandler
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
# Create a logs directory if it doesn't exist
|
| 7 |
+
os.makedirs("logs", exist_ok=True)
|
| 8 |
+
|
| 9 |
+
def setup_logger():
|
| 10 |
+
# 1. Create a custom logger
|
| 11 |
+
logger = logging.getLogger("rag_app")
|
| 12 |
+
logger.setLevel(logging.DEBUG) # Capture everything from DEBUG and above
|
| 13 |
+
|
| 14 |
+
# Avoid duplicate logs if this function is called multiple times
|
| 15 |
+
if logger.handlers:
|
| 16 |
+
return logger
|
| 17 |
+
|
| 18 |
+
# 2. Create formatting (How the log looks)
|
| 19 |
+
# Example: 2026-06-17 10:45:01,123 - INFO - rag_app - Something happened
|
| 20 |
+
formatter = logging.Formatter(
|
| 21 |
+
"%(asctime)s - %(levelname)s - %(name)s - %(message)s"
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
# 3. Console Handler (Prints to your terminal)
|
| 25 |
+
console_handler = logging.StreamHandler(sys.stdout)
|
| 26 |
+
console_handler.setLevel(logging.INFO) # Keep console clean (INFO, WARNING, ERROR)
|
| 27 |
+
console_handler.setFormatter(formatter)
|
| 28 |
+
|
| 29 |
+
# 4. File Handler (Saves to logs/app.log)
|
| 30 |
+
# Auto-rotates when the file hits 5MB, keeps 3 backups max
|
| 31 |
+
file_handler = RotatingFileHandler(
|
| 32 |
+
"logs/app.log", maxBytes=5*1024*1024, backupCount=3, encoding="utf-8"
|
| 33 |
+
)
|
| 34 |
+
file_handler.setLevel(logging.DEBUG) # Save EVERYTHING to the file
|
| 35 |
+
file_handler.setFormatter(formatter)
|
| 36 |
+
|
| 37 |
+
# 5. Add handlers to our logger
|
| 38 |
+
logger.addHandler(console_handler)
|
| 39 |
+
logger.addHandler(file_handler)
|
| 40 |
+
|
| 41 |
+
return logger
|
| 42 |
+
|
| 43 |
+
# Create a globally accessible logger object
|
| 44 |
+
logger = setup_logger()
|
src/rag_chain.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 3 |
+
from langchain_core.prompts import PromptTemplate
|
| 4 |
+
from langchain_core.runnables import RunnablePassthrough
|
| 5 |
+
from langchain_core.output_parsers import StrOutputParser
|
| 6 |
+
from src.logger import logger
|
| 7 |
+
|
| 8 |
+
def build_chain(vectorstore):
|
| 9 |
+
"""
|
| 10 |
+
Takes the populated ChromaDB vector store and builds the LangChain
|
| 11 |
+
retrieval-augmented generation (RAG) pipeline using Gemini 1.5 Flash.
|
| 12 |
+
"""
|
| 13 |
+
logger.info("Building RAG chain...")
|
| 14 |
+
|
| 15 |
+
try:
|
| 16 |
+
# 1. Verify API Key
|
| 17 |
+
api_key = os.environ.get("GOOGLE_API_KEY")
|
| 18 |
+
if not api_key:
|
| 19 |
+
logger.error("GOOGLE_API_KEY environment variable is missing!")
|
| 20 |
+
raise ValueError("GOOGLE_API_KEY is not set. Please set it before running the app.")
|
| 21 |
+
|
| 22 |
+
# 2. Initialize the LLM
|
| 23 |
+
logger.debug("Initializing Gemini 1.5 Flash model...")
|
| 24 |
+
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash-lite", google_api_key=api_key)
|
| 25 |
+
|
| 26 |
+
# 3. Setup the Retriever
|
| 27 |
+
# k=4 ensures it pulls the top 4 most relevant chunks from ChromaDB
|
| 28 |
+
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
|
| 29 |
+
|
| 30 |
+
# 4. Define the Prompt Template (Guardrails against hallucinations)
|
| 31 |
+
template = PromptTemplate.from_template("""
|
| 32 |
+
You are a helpful AI assistant. Answer the user's question using ONLY the provided context from the uploaded document.
|
| 33 |
+
If you cannot find the answer in the text, politely say "I cannot find the answer to that in the provided document."
|
| 34 |
+
|
| 35 |
+
<context>
|
| 36 |
+
{context}
|
| 37 |
+
</context>
|
| 38 |
+
|
| 39 |
+
Question: {query}
|
| 40 |
+
Answer:
|
| 41 |
+
""")
|
| 42 |
+
|
| 43 |
+
# 5. Helper function to combine document chunks into a single string
|
| 44 |
+
def format_docs(docs):
|
| 45 |
+
return "\n\n".join(doc.page_content for doc in docs)
|
| 46 |
+
|
| 47 |
+
# 6. Build the LangChain Expression Language (LCEL) Pipeline
|
| 48 |
+
chain = (
|
| 49 |
+
{"context": retriever | format_docs, "query": RunnablePassthrough()}
|
| 50 |
+
| template
|
| 51 |
+
| llm
|
| 52 |
+
| StrOutputParser()
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
logger.info("Successfully built RAG chain.")
|
| 56 |
+
|
| 57 |
+
# Return the fully compiled chain back to app.py
|
| 58 |
+
return chain
|
| 59 |
+
|
| 60 |
+
except Exception as e:
|
| 61 |
+
logger.error(f"Failed to build RAG chain: {str(e)}", exc_info=True)
|
| 62 |
+
raise e
|
src/splitter.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 2 |
+
from src.logger import logger
|
| 3 |
+
|
| 4 |
+
def split_text(docs, chunk_size=1000, chunk_overlap=200):
|
| 5 |
+
"""
|
| 6 |
+
Takes a list of LangChain Document objects and splits them into smaller,
|
| 7 |
+
manageable chunks for the vector database.
|
| 8 |
+
"""
|
| 9 |
+
logger.info(f"Starting text splitting: chunk_size={chunk_size}, overlap={chunk_overlap}")
|
| 10 |
+
|
| 11 |
+
try:
|
| 12 |
+
# Initialize the LangChain text splitter
|
| 13 |
+
text_splitter = RecursiveCharacterTextSplitter(
|
| 14 |
+
chunk_size=chunk_size,
|
| 15 |
+
chunk_overlap=chunk_overlap,
|
| 16 |
+
separators=["\n\n", "\n", " ", ""] # Splits by paragraph, then line, then word
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
# Split the documents
|
| 20 |
+
chunks = text_splitter.split_documents(docs)
|
| 21 |
+
|
| 22 |
+
logger.info(f"Successfully split the document into {len(chunks)} individual chunks.")
|
| 23 |
+
|
| 24 |
+
# Return the chunks so the embedding model can vectorize them
|
| 25 |
+
return chunks
|
| 26 |
+
|
| 27 |
+
except Exception as e:
|
| 28 |
+
logger.error(f"Text splitting failed: {str(e)}", exc_info=True)
|
| 29 |
+
raise e
|
src/vectorstore.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_chroma import Chroma
|
| 2 |
+
from src.logger import logger
|
| 3 |
+
|
| 4 |
+
def create_vectorstore(chunks, embeddings):
|
| 5 |
+
"""
|
| 6 |
+
Takes the split text chunks and the embedding model,
|
| 7 |
+
and builds an ephemeral (in-memory) ChromaDB vector store.
|
| 8 |
+
"""
|
| 9 |
+
logger.info(f"Creating vector store for {len(chunks)} chunks...")
|
| 10 |
+
|
| 11 |
+
try:
|
| 12 |
+
# Initialize the Chroma vector store from the document chunks.
|
| 13 |
+
# By omitting 'persist_directory', the database is built in-memory,
|
| 14 |
+
# which is much faster and perfect for temporary session-based files.
|
| 15 |
+
vectorstore = Chroma.from_documents(
|
| 16 |
+
documents=chunks,
|
| 17 |
+
embedding=embeddings
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
logger.info("Successfully built Chroma vector store.")
|
| 21 |
+
|
| 22 |
+
# Return the vectorstore so the RAG chain can use it as a retriever
|
| 23 |
+
return vectorstore
|
| 24 |
+
|
| 25 |
+
except Exception as e:
|
| 26 |
+
logger.error(f"Failed to create vector store: {str(e)}", exc_info=True)
|
| 27 |
+
raise e
|