lvvignesh2122 commited on
Commit
9d21791
·
1 Parent(s): 775a7d0

Add frontend UI and document upload for RAG app

Browse files
Files changed (4) hide show
  1. frontend/index.html +257 -0
  2. main.py +111 -48
  3. rag_store.py +45 -34
  4. requirements.txt +1 -0
frontend/index.html ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Gemini RAG Assistant</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+
8
+ <!-- Fonts -->
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
10
+
11
+ <style>
12
+ :root {
13
+ --bg: radial-gradient(1200px 600px at top, #e0e7ff 0%, #f8fafc 60%);
14
+ --card: rgba(255,255,255,0.85);
15
+ --border: rgba(15,23,42,0.08);
16
+ --primary: #4f46e5;
17
+ --secondary: #0ea5e9;
18
+ --text: #0f172a;
19
+ --muted: #64748b;
20
+ --error: #dc2626;
21
+ }
22
+
23
+ * { box-sizing: border-box; font-family: Inter, sans-serif; }
24
+
25
+ body {
26
+ margin: 0;
27
+ min-height: 100vh;
28
+ background: var(--bg);
29
+ display: flex;
30
+ justify-content: center;
31
+ padding: 40px 16px;
32
+ color: var(--text);
33
+ }
34
+
35
+ .container {
36
+ width: 100%;
37
+ max-width: 980px;
38
+ background: var(--card);
39
+ backdrop-filter: blur(16px);
40
+ border-radius: 24px;
41
+ padding: 36px;
42
+ border: 1px solid var(--border);
43
+ box-shadow: 0 40px 120px rgba(15,23,42,.15);
44
+ }
45
+
46
+ h1 {
47
+ font-size: 2.2rem;
48
+ margin: 0;
49
+ font-weight: 700;
50
+ background: linear-gradient(135deg, #4f46e5, #06b6d4);
51
+ -webkit-background-clip: text;
52
+ -webkit-text-fill-color: transparent;
53
+ }
54
+
55
+ .subtitle {
56
+ margin-top: 8px;
57
+ color: var(--muted);
58
+ font-size: 1rem;
59
+ }
60
+
61
+ .card {
62
+ margin-top: 28px;
63
+ background: white;
64
+ border-radius: 18px;
65
+ padding: 24px;
66
+ border: 1px solid var(--border);
67
+ }
68
+
69
+ .card h3 {
70
+ margin-top: 0;
71
+ margin-bottom: 16px;
72
+ font-size: 1.1rem;
73
+ }
74
+
75
+ input[type="file"], textarea {
76
+ width: 100%;
77
+ padding: 14px;
78
+ border-radius: 14px;
79
+ border: 1px solid var(--border);
80
+ font-size: 0.95rem;
81
+ }
82
+
83
+ textarea {
84
+ min-height: 120px;
85
+ resize: vertical;
86
+ }
87
+
88
+ .row {
89
+ display: flex;
90
+ gap: 12px;
91
+ margin-top: 12px;
92
+ flex-wrap: wrap;
93
+ }
94
+
95
+ button {
96
+ padding: 12px 18px;
97
+ border-radius: 14px;
98
+ border: none;
99
+ background: var(--primary);
100
+ color: white;
101
+ font-weight: 600;
102
+ cursor: pointer;
103
+ transition: all .2s ease;
104
+ }
105
+
106
+ button.secondary { background: var(--secondary); }
107
+
108
+ button:disabled {
109
+ opacity: .5;
110
+ cursor: not-allowed;
111
+ }
112
+
113
+ button:hover:not(:disabled) {
114
+ transform: translateY(-1px);
115
+ box-shadow: 0 10px 25px rgba(79,70,229,.35);
116
+ }
117
+
118
+ .status {
119
+ margin-top: 10px;
120
+ font-size: .9rem;
121
+ color: var(--muted);
122
+ }
123
+
124
+ .answer {
125
+ margin-top: 24px;
126
+ padding: 20px;
127
+ border-radius: 16px;
128
+ background: #f8fafc;
129
+ border: 1px solid var(--border);
130
+ white-space: pre-wrap;
131
+ line-height: 1.6;
132
+ }
133
+
134
+ .error {
135
+ color: var(--error);
136
+ margin-top: 10px;
137
+ font-weight: 500;
138
+ }
139
+
140
+ .loader {
141
+ font-weight: 600;
142
+ color: var(--primary);
143
+ animation: pulse 1.2s infinite;
144
+ }
145
+
146
+ @keyframes pulse {
147
+ 0% { opacity: .4 }
148
+ 50% { opacity: 1 }
149
+ 100% { opacity: .4 }
150
+ }
151
+
152
+ footer {
153
+ text-align: center;
154
+ margin-top: 28px;
155
+ font-size: .8rem;
156
+ color: var(--muted);
157
+ }
158
+ </style>
159
+ </head>
160
+
161
+ <body>
162
+ <div class="container">
163
+ <h1>Gemini RAG Assistant</h1>
164
+ <div class="subtitle">
165
+ Upload documents · Ask questions · Get grounded answers
166
+ </div>
167
+
168
+ <!-- Upload -->
169
+ <div class="card">
170
+ <h3>📄 Upload documents</h3>
171
+ <input type="file" id="files" multiple />
172
+ <div class="row">
173
+ <button id="uploadBtn" onclick="upload()">Upload & Index</button>
174
+ </div>
175
+ <div id="uploadStatus" class="status"></div>
176
+ </div>
177
+
178
+ <!-- Ask -->
179
+ <div class="card">
180
+ <h3>💬 Ask or summarize</h3>
181
+ <textarea id="question" placeholder="Ask something about your documents…"></textarea>
182
+ <div class="row">
183
+ <button id="askBtn" onclick="ask()">Ask</button>
184
+ <button class="secondary" id="sumBtn" onclick="summarize()">Summarize</button>
185
+ </div>
186
+ </div>
187
+
188
+ <!-- Answer -->
189
+ <div id="answerBox" class="answer" style="display:none;"></div>
190
+ <div id="errorBox" class="error"></div>
191
+
192
+ <footer>
193
+ Built with FastAPI · FAISS · Gemini
194
+ </footer>
195
+ </div>
196
+
197
+ <script>
198
+ let busy = false;
199
+
200
+ function setBusy(state) {
201
+ busy = state;
202
+ document.getElementById("askBtn").disabled = state;
203
+ document.getElementById("sumBtn").disabled = state;
204
+ document.getElementById("uploadBtn").disabled = state;
205
+ }
206
+
207
+ async function upload() {
208
+ const files = document.getElementById("files").files;
209
+ if (!files.length) return;
210
+
211
+ setBusy(true);
212
+ document.getElementById("uploadStatus").innerText = "Indexing documents…";
213
+
214
+ const fd = new FormData();
215
+ for (let f of files) fd.append("files", f);
216
+
217
+ const res = await fetch("/upload", { method: "POST", body: fd });
218
+ const data = await res.json();
219
+
220
+ document.getElementById("uploadStatus").innerText = data.message || "Done ✅";
221
+ setBusy(false);
222
+ }
223
+
224
+ async function ask() {
225
+ const q = document.getElementById("question").value.trim();
226
+ if (!q || busy) return;
227
+
228
+ setBusy(true);
229
+ document.getElementById("errorBox").innerText = "";
230
+ document.getElementById("answerBox").style.display = "block";
231
+ document.getElementById("answerBox").innerHTML = "<span class='loader'>Thinking…</span>";
232
+
233
+ try {
234
+ const res = await fetch("/ask", {
235
+ method: "POST",
236
+ headers: { "Content-Type": "application/json" },
237
+ body: JSON.stringify({ prompt: q })
238
+ });
239
+
240
+ const data = await res.json();
241
+ document.getElementById("answerBox").innerText = data.answer;
242
+ } catch {
243
+ document.getElementById("errorBox").innerText =
244
+ "⚠️ LLM quota exceeded. Please wait ~1 minute and retry.";
245
+ }
246
+
247
+ setBusy(false);
248
+ }
249
+
250
+ function summarize() {
251
+ document.getElementById("question").value =
252
+ "Summarize the uploaded documents in 5 bullet points.";
253
+ ask();
254
+ }
255
+ </script>
256
+ </body>
257
+ </html>
main.py CHANGED
@@ -1,75 +1,138 @@
1
  import os
2
- from fastapi import FastAPI
 
 
 
 
3
  from pydantic import BaseModel
4
  from dotenv import load_dotenv
5
  import google.generativeai as genai
6
- from rag_store import search_knowledge
7
 
8
- load_dotenv()
9
 
 
 
 
 
10
  genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
11
 
12
- app = FastAPI(title="AI RAG Backend with Gemini")
13
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  class PromptRequest(BaseModel):
15
  prompt: str
16
 
17
- @app.get("/")
18
- def home():
19
- return {"message": "AI backend is running 🚀"}
20
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  @app.post("/ask")
22
  async def ask(data: PromptRequest):
23
- results = search_knowledge(data.prompt)
 
 
 
 
 
 
 
24
 
 
25
  if not results:
26
- return {
27
  "answer": "I don't know based on the provided documents.",
28
  "confidence": 0.0,
29
  "citations": []
30
  }
 
 
31
 
32
- # -------- Context
33
- context_text = "\n".join(r["text"] for r in results)
34
 
35
  prompt = f"""
36
- Answer the question strictly using the context.
37
- If unsure, say "I don't know".
 
 
 
38
 
39
  Question:
40
  {data.prompt}
41
-
42
- Context:
43
- {context_text}
44
  """
45
 
46
- model = genai.GenerativeModel("gemini-2.5-flash")
47
- response = model.generate_content(prompt)
48
-
49
- # -------- Confidence scoring
50
- avg_distance = sum(r["distance"] for r in results) / len(results)
51
-
52
- if avg_distance < 0.6:
53
- confidence = 0.9
54
- elif avg_distance < 1.2:
55
- confidence = 0.7
56
- else:
57
- confidence = 0.4
58
-
59
- # -------- Citations
60
- citations = []
61
- seen = set()
62
- for r in results:
63
- key = (r["metadata"]["source"], r["metadata"]["page"])
64
- if key not in seen:
65
- seen.add(key)
66
- citations.append({
67
- "source": r["metadata"]["source"],
68
- "page": r["metadata"]["page"]
69
- })
70
-
71
- return {
72
- "answer": response.text,
73
- "confidence": round(confidence, 2),
74
- "citations": citations
75
- }
 
1
  import os
2
+ from time import time
3
+ from fastapi import FastAPI, UploadFile, File
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from fastapi.responses import HTMLResponse, JSONResponse
6
+ from fastapi.staticfiles import StaticFiles
7
  from pydantic import BaseModel
8
  from dotenv import load_dotenv
9
  import google.generativeai as genai
 
10
 
11
+ from rag_store import ingest_documents, search_knowledge
12
 
13
+ # -----------------------
14
+ # Setup
15
+ # -----------------------
16
+ load_dotenv()
17
  genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
18
 
19
+ app = FastAPI(
20
+ title="Gemini RAG FastAPI",
21
+ docs_url="/docs",
22
+ redoc_url="/redoc"
23
+ )
24
+
25
+ # -----------------------
26
+ # CORS
27
+ # -----------------------
28
+ app.add_middleware(
29
+ CORSMiddleware,
30
+ allow_origins=["*"],
31
+ allow_methods=["*"],
32
+ allow_headers=["*"],
33
+ )
34
+
35
+ # -----------------------
36
+ # Frontend
37
+ # -----------------------
38
+ app.mount("/frontend", StaticFiles(directory="frontend"), name="frontend")
39
+
40
+ # -----------------------
41
+ # Cache (protect quota)
42
+ # -----------------------
43
+ CACHE_TTL = 300 # seconds
44
+ answer_cache = {}
45
+
46
+ # -----------------------
47
+ # Models
48
+ # -----------------------
49
  class PromptRequest(BaseModel):
50
  prompt: str
51
 
52
+ # -----------------------
53
+ # Routes
54
+ # -----------------------
55
+
56
+ @app.get("/", response_class=HTMLResponse)
57
+ def serve_ui():
58
+ with open("frontend/index.html", "r", encoding="utf-8") as f:
59
+ return f.read()
60
+
61
+ # -----------------------
62
+ # Upload
63
+ # -----------------------
64
+ @app.post("/upload")
65
+ async def upload(files: list[UploadFile] = File(...)):
66
+ try:
67
+ chunks = ingest_documents(files)
68
+ return {"message": f"Indexed {chunks} chunks from {len(files)} file(s)."}
69
+ except Exception as e:
70
+ return JSONResponse(status_code=400, content={"error": str(e)})
71
+
72
+ # -----------------------
73
+ # Ask
74
+ # -----------------------
75
  @app.post("/ask")
76
  async def ask(data: PromptRequest):
77
+ prompt_key = data.prompt.strip().lower()
78
+ now = time()
79
+
80
+ # 🔁 Cache
81
+ if prompt_key in answer_cache:
82
+ ts, cached = answer_cache[prompt_key]
83
+ if now - ts < CACHE_TTL:
84
+ return cached
85
 
86
+ results = search_knowledge(data.prompt)
87
  if not results:
88
+ response = {
89
  "answer": "I don't know based on the provided documents.",
90
  "confidence": 0.0,
91
  "citations": []
92
  }
93
+ answer_cache[prompt_key] = (now, response)
94
+ return response
95
 
96
+ context = "\n\n".join(r["text"] for r in results)
 
97
 
98
  prompt = f"""
99
+ Answer strictly using the context below.
100
+ If not found, say "I don't know".
101
+
102
+ Context:
103
+ {context}
104
 
105
  Question:
106
  {data.prompt}
 
 
 
107
  """
108
 
109
+ try:
110
+ model = genai.GenerativeModel("gemini-2.5-flash")
111
+ llm_response = model.generate_content(prompt)
112
+
113
+ response = {
114
+ "answer": llm_response.text,
115
+ "confidence": round(min(1.0, len(results) / 5), 2),
116
+ "citations": [
117
+ {"source": r["metadata"]["source"], "page": r["metadata"]["page"]}
118
+ for r in results
119
+ ]
120
+ }
121
+
122
+ answer_cache[prompt_key] = (now, response)
123
+ return response
124
+
125
+ except Exception as e:
126
+ return JSONResponse(
127
+ status_code=429,
128
+ content={"error": "LLM quota exceeded. Please wait and retry."}
129
+ )
130
+
131
+ # -----------------------
132
+ # Summarize
133
+ # -----------------------
134
+ @app.post("/summarize")
135
+ async def summarize():
136
+ return await ask(PromptRequest(
137
+ prompt="Summarize the uploaded documents in 5 concise bullet points."
138
+ ))
rag_store.py CHANGED
@@ -1,67 +1,78 @@
1
  import os
2
  import faiss
3
  import numpy as np
4
- from sentence_transformers import SentenceTransformer
5
  from pypdf import PdfReader
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
- DATA_DIR = "data"
8
- INDEX_FILE = "vector.index"
9
- DOCS_FILE = "documents.npy"
10
- META_FILE = "metadata.npy"
11
-
12
- model = SentenceTransformer("all-MiniLM-L6-v2")
13
-
14
- # -------------------------
15
- # Load or build index
16
- # -------------------------
17
- if os.path.exists(INDEX_FILE):
18
- print("🔁 Loading FAISS index from disk...")
19
- index = faiss.read_index(INDEX_FILE)
20
- documents = np.load(DOCS_FILE, allow_pickle=True)
21
- metadata = np.load(META_FILE, allow_pickle=True)
22
- else:
23
- print("🧠 Building FAISS index...")
24
  texts = []
25
  meta = []
26
 
27
- for file in os.listdir(DATA_DIR):
28
- if file.endswith(".pdf"):
29
- reader = PdfReader(os.path.join(DATA_DIR, file))
 
 
30
  for i, page in enumerate(reader.pages):
31
  text = page.extract_text()
32
  if text:
33
  texts.append(text)
34
  meta.append({
35
- "source": file,
36
  "page": i + 1
37
  })
38
 
39
- embeddings = model.encode(texts)
 
 
 
 
 
 
 
 
 
 
 
 
40
  index = faiss.IndexFlatL2(embeddings.shape[1])
41
  index.add(np.array(embeddings))
42
 
43
- np.save(DOCS_FILE, texts)
44
- np.save(META_FILE, meta)
45
- faiss.write_index(index, INDEX_FILE)
46
-
47
  documents = texts
48
  metadata = meta
49
 
50
- print("✅ FAISS index saved to disk.")
51
 
52
- # -------------------------
53
  # Search
54
- # -------------------------
55
  def search_knowledge(query, top_k=5):
56
- query_vec = model.encode([query])
 
 
 
57
  distances, indices = index.search(query_vec, top_k)
58
 
59
  results = []
60
- for dist, idx in zip(distances[0], indices[0]):
61
  results.append({
62
  "text": documents[idx],
63
- "metadata": metadata[idx],
64
- "distance": float(dist)
65
  })
66
 
67
  return results
 
1
  import os
2
  import faiss
3
  import numpy as np
 
4
  from pypdf import PdfReader
5
+ from sentence_transformers import SentenceTransformer
6
+
7
+ # -----------------------
8
+ # Global in-memory state
9
+ # -----------------------
10
+ index = None
11
+ documents = []
12
+ metadata = []
13
+
14
+ embedder = SentenceTransformer("all-MiniLM-L6-v2")
15
+
16
+ # -----------------------
17
+ # Ingest uploaded files
18
+ # -----------------------
19
+ def ingest_documents(files):
20
+ global index, documents, metadata
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  texts = []
23
  meta = []
24
 
25
+ for file in files:
26
+ filename = file.filename
27
+
28
+ if filename.endswith(".pdf"):
29
+ reader = PdfReader(file.file)
30
  for i, page in enumerate(reader.pages):
31
  text = page.extract_text()
32
  if text:
33
  texts.append(text)
34
  meta.append({
35
+ "source": filename,
36
  "page": i + 1
37
  })
38
 
39
+ elif filename.endswith(".txt"):
40
+ content = file.file.read().decode("utf-8")
41
+ texts.append(content)
42
+ meta.append({
43
+ "source": filename,
44
+ "page": "N/A"
45
+ })
46
+
47
+ if not texts:
48
+ raise ValueError("No readable text found.")
49
+
50
+ embeddings = embedder.encode(texts)
51
+
52
  index = faiss.IndexFlatL2(embeddings.shape[1])
53
  index.add(np.array(embeddings))
54
 
 
 
 
 
55
  documents = texts
56
  metadata = meta
57
 
58
+ return len(texts)
59
 
60
+ # -----------------------
61
  # Search
62
+ # -----------------------
63
  def search_knowledge(query, top_k=5):
64
+ if index is None:
65
+ return []
66
+
67
+ query_vec = embedder.encode([query])
68
  distances, indices = index.search(query_vec, top_k)
69
 
70
  results = []
71
+ for idx, dist in zip(indices[0], distances[0]):
72
  results.append({
73
  "text": documents[idx],
74
+ "distance": float(dist),
75
+ "metadata": metadata[idx]
76
  })
77
 
78
  return results
requirements.txt CHANGED
@@ -6,3 +6,4 @@ faiss-cpu
6
  sentence-transformers
7
  pypdf
8
  numpy
 
 
6
  sentence-transformers
7
  pypdf
8
  numpy
9
+ python-multipart