ABDRauf commited on
Commit
305ef4d
·
verified ·
1 Parent(s): 433be90

Upload 31 files

Browse files
.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