KalooAI commited on
Commit
0135e8c
·
verified ·
1 Parent(s): be89ea1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +111 -78
app.py CHANGED
@@ -2,7 +2,6 @@ import os
2
  import time
3
  import gradio as gr
4
  import numpy as np
5
- import faiss
6
  from PyPDF2 import PdfReader
7
  from sentence_transformers import SentenceTransformer
8
  from huggingface_hub import InferenceClient
@@ -21,16 +20,14 @@ HF_TOKEN = os.getenv("HF_TOKEN", None)
21
  # LLM_MODEL ="HuggingFaceH4/zephyr-7b-beta"
22
  # LLM_MODEL = "NousResearch/Hermes-2-Pro-Mistral-7B" Does not work
23
 
24
- # Were Great
25
  # LLM_MODEL = "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B"
26
  # LLM_MODEL = "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B"
27
 
28
- # Great LLM
29
- ##LLM_MODEL = 'meta-llama/Llama-3.1-8B-Instruct'
30
  LLM_MODEL = "deepseek-ai/DeepSeek-V3"
31
 
32
  EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
33
-
34
  embedding_model = SentenceTransformer(EMBEDDING_MODEL)
35
 
36
  # IMPORTANT: do NOT pass model=... here
@@ -83,31 +80,42 @@ def chunk_text(text, chunk_size=800, overlap=200):
83
  return chunks
84
 
85
 
86
- def build_vectorstore(chunks):
87
- if not chunks:
88
- return None, None
89
 
90
- embeddings = embedding_model.encode(chunks, convert_to_numpy=True)
91
- dim = embeddings.shape[1]
 
 
 
 
 
92
 
93
- index = faiss.IndexFlatL2(dim)
94
- index.add(embeddings)
95
 
96
- return index, embeddings
 
 
97
 
 
 
98
 
99
- def retrieve_context(question, state, k=15):
100
- index = state.get("index")
101
- chunks = state.get("chunks")
102
 
103
- if index is None or chunks is None:
104
- return ""
105
 
106
- q_emb = embedding_model.encode([question], convert_to_numpy=True)
107
- distances, indices = index.search(q_emb, k)
 
108
 
109
- selected = [chunks[i] for i in indices[0] if i < len(chunks)]
110
- return "\n\n---\n\n".join(selected)
 
 
111
 
112
 
113
  # ============================================================
@@ -136,7 +144,7 @@ def generate_answer(question, context):
136
  temperature=0.25,
137
  top_p=0.9,
138
  frequency_penalty=0.1,
139
- presence_penalty=0.0
140
  )
141
 
142
  return response.choices[0].message["content"].strip()
@@ -147,96 +155,113 @@ def generate_answer(question, context):
147
  # ============================================================
148
 
149
  def load_pdf(files, state):
150
- # files is the *current* list from the UI (after add/remove)
151
- # We rebuild everything from scratch to stay in sync.
152
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  # If user cleared all files
154
  if not files:
155
- state["chunks"] = []
156
  state["files"] = {}
157
  state["pdf_count"] = 0
158
- state["index"] = None
159
 
160
  return (
161
  "No file uploaded.",
162
  state,
163
- gr.update(choices=[], value=None) # empty dropdown
164
  )
165
 
166
  try:
167
- # Reset state and rebuild from current files only
168
- state["chunks"] = []
169
  state["files"] = {}
170
  state["pdf_count"] = 0
 
171
 
172
  # Deduplicate while preserving order
173
  unique_files = list(dict.fromkeys(files))
174
 
175
  for file in unique_files:
176
- # In Gradio 6, gr.Files usually gives you temp paths (strings)
177
- # PdfReader accepts a path or file-like object, so we can pass `file` directly.
178
  filename = os.path.basename(str(file))
179
 
180
  text = read_pdf(file)
181
- new_chunks = chunk_text(text)
182
 
183
- if not new_chunks:
184
  continue
185
 
186
- state["files"][filename] = new_chunks
187
- state["chunks"].extend(new_chunks)
 
 
 
 
 
188
  state["pdf_count"] += 1
 
189
 
190
- # Rebuild FAISS
191
- index, _ = build_vectorstore(state["chunks"])
192
- state["index"] = index
 
193
 
194
  return (
195
- f"{state['pdf_count']} PDF(s) loaded. Total chunks: {len(state['chunks'])}",
196
  state,
197
- gr.update(choices=list(state["files"].keys()), value=None) # <-- key fix
198
  )
199
 
200
  except Exception as e:
201
  return (
202
  f"Error processing PDF: {e}",
203
  state,
204
- gr.update(choices=list(state["files"].keys()), value=None)
205
  )
206
-
 
207
  def remove_pdf(filename, state):
208
  if filename not in state["files"]:
209
  return (
210
  f"{filename} not found.",
211
  state,
212
- gr.update(choices=list(state["files"].keys()), value=None)
213
  )
214
 
215
  # Remove the file entry
216
  state["files"].pop(filename, None)
217
 
218
- # Rebuild chunks from remaining files
219
- all_chunks = []
220
- for chunks in state["files"].values():
221
- all_chunks.extend(chunks)
222
 
223
- state["chunks"] = all_chunks
224
- state["pdf_count"] = len(state["files"])
225
 
226
- if state["chunks"]:
227
- index, _ = build_vectorstore(state["chunks"])
228
- state["index"] = index
229
- else:
230
- state["index"] = None
231
 
232
  return (
233
- f"Removed {filename}. Total chunks: {len(state['chunks'])}",
234
  state,
235
- gr.update(choices=list(state["files"].keys()), value=None)
236
  )
237
 
 
238
  def answer_question(question, state):
239
-
240
  print("HF_TOKEN loaded:", HF_TOKEN is not None)
241
 
242
  if HF_TOKEN:
@@ -248,10 +273,10 @@ def answer_question(question, state):
248
  if not question.strip():
249
  return "Please enter a question."
250
 
251
- if state.get("index") is None:
252
- return "Please upload a PDF first."
253
 
254
- context = retrieve_context(question, state)
255
  answer = generate_answer(question, context)
256
  return answer
257
 
@@ -261,26 +286,30 @@ def answer_question(question, state):
261
  # ============================================================
262
 
263
  with gr.Blocks(title="PDF Q&A Chatbot") as demo:
264
-
265
  gr.Markdown("## 📄 PDF Q&A Chatbot\nUpload PDFs and ask questions about their content.")
266
 
267
- state = gr.State({
268
- "chunks": [],
269
- "index": None,
270
- "pdf_count": 0,
271
- "files": {}
272
- })
 
273
 
274
  with gr.Row():
275
  # LEFT COLUMN — PDF upload + removal
276
  with gr.Column(scale=1):
277
- pdf_input = gr.Files(label="Upload PDF(s)", file_types=[".pdf"])
 
 
 
 
278
  pdf_status = gr.Textbox(label="Status", interactive=False)
279
 
280
  file_selector = gr.Dropdown(
281
  label="Remove a PDF",
282
  choices=[],
283
- interactive=True
284
  )
285
  remove_button = gr.Button("Remove Selected PDF")
286
 
@@ -290,31 +319,35 @@ with gr.Blocks(title="PDF Q&A Chatbot") as demo:
290
  question = gr.Textbox(
291
  label="Your question",
292
  lines=2,
293
- scale=3
294
  )
295
  clear_button = gr.Button(
296
  "Clear",
297
  scale=1,
298
  min_width=80,
299
- elem_classes="clear-btn"
300
  )
301
 
302
  answer = gr.Textbox(label="Answer", lines=8)
303
  ask_button = gr.Button("Ask")
304
 
305
  # Bind events AFTER components exist
306
- pdf_input.change(load_pdf, inputs=[pdf_input, state], outputs=[pdf_status, state, file_selector])
 
 
 
 
307
 
308
  remove_button.click(
309
  remove_pdf,
310
  inputs=[file_selector, state],
311
- outputs=[pdf_status, state, file_selector]
312
  )
313
 
314
  ask_button.click(
315
  answer_question,
316
  inputs=[question, state],
317
- outputs=answer
318
  )
319
 
320
  clear_button.click(lambda: "", outputs=question)
@@ -322,5 +355,5 @@ with gr.Blocks(title="PDF Q&A Chatbot") as demo:
322
 
323
  if __name__ == "__main__":
324
  demo.launch(
325
- css=".clear-btn button { height: 100%; min-height: 52px; }"
326
- )
 
2
  import time
3
  import gradio as gr
4
  import numpy as np
 
5
  from PyPDF2 import PdfReader
6
  from sentence_transformers import SentenceTransformer
7
  from huggingface_hub import InferenceClient
 
20
  # LLM_MODEL ="HuggingFaceH4/zephyr-7b-beta"
21
  # LLM_MODEL = "NousResearch/Hermes-2-Pro-Mistral-7B" Does not work
22
 
23
+ # Were Great LLM
24
  # LLM_MODEL = "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B"
25
  # LLM_MODEL = "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B"
26
 
27
+ # LLM model
 
28
  LLM_MODEL = "deepseek-ai/DeepSeek-V3"
29
 
30
  EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
 
31
  embedding_model = SentenceTransformer(EMBEDDING_MODEL)
32
 
33
  # IMPORTANT: do NOT pass model=... here
 
80
  return chunks
81
 
82
 
83
+ # ============================================================
84
+ # PER-FILE RETRIEVAL
85
+ # ============================================================
86
 
87
+ def retrieve_context_per_file(question, state, k=10):
88
+ """
89
+ For each file, retrieve top-k chunks separately,
90
+ then merge all selected chunks into a single context.
91
+ """
92
+ if not state["files"]:
93
+ return ""
94
 
95
+ q_emb = embedding_model.encode([question], convert_to_numpy=True)[0]
96
+ all_selected = []
97
 
98
+ for filename, data in state["files"].items():
99
+ chunks = data["chunks"]
100
+ embeddings = data["embeddings"]
101
 
102
+ if len(chunks) == 0:
103
+ continue
104
 
105
+ # Compute L2 distances to the query embedding
106
+ distances = np.linalg.norm(embeddings - q_emb, axis=1)
 
107
 
108
+ # Get top-k indices for this file
109
+ top_k_idx = distances.argsort()[: min(k, len(chunks))]
110
 
111
+ # Collect chunks (optionally tag with filename)
112
+ selected = [f"[{filename}]\n{chunks[i]}" for i in top_k_idx]
113
+ all_selected.extend(selected)
114
 
115
+ if not all_selected:
116
+ return ""
117
+
118
+ return "\n\n---\n\n".join(all_selected)
119
 
120
 
121
  # ============================================================
 
144
  temperature=0.25,
145
  top_p=0.9,
146
  frequency_penalty=0.1,
147
+ presence_penalty=0.0,
148
  )
149
 
150
  return response.choices[0].message["content"].strip()
 
155
  # ============================================================
156
 
157
  def load_pdf(files, state):
158
+ """
159
+ files: current list from the UI (after add/remove).
160
+ We rebuild state from scratch to stay in sync.
161
+ State structure:
162
+
163
+ state = {
164
+ "files": {
165
+ "filename.pdf": {
166
+ "chunks": [...],
167
+ "embeddings": np.array([...])
168
+ },
169
+ ...
170
+ },
171
+ "pdf_count": int,
172
+ "total_chunks": int
173
+ }
174
+ """
175
  # If user cleared all files
176
  if not files:
 
177
  state["files"] = {}
178
  state["pdf_count"] = 0
179
+ state["total_chunks"] = 0
180
 
181
  return (
182
  "No file uploaded.",
183
  state,
184
+ gr.update(choices=[], value=None),
185
  )
186
 
187
  try:
188
+ # Reset and rebuild from current files only
 
189
  state["files"] = {}
190
  state["pdf_count"] = 0
191
+ state["total_chunks"] = 0
192
 
193
  # Deduplicate while preserving order
194
  unique_files = list(dict.fromkeys(files))
195
 
196
  for file in unique_files:
 
 
197
  filename = os.path.basename(str(file))
198
 
199
  text = read_pdf(file)
200
+ chunks = chunk_text(text)
201
 
202
+ if not chunks:
203
  continue
204
 
205
+ embeddings = embedding_model.encode(chunks, convert_to_numpy=True)
206
+
207
+ state["files"][filename] = {
208
+ "chunks": chunks,
209
+ "embeddings": embeddings,
210
+ }
211
+
212
  state["pdf_count"] += 1
213
+ state["total_chunks"] += len(chunks)
214
 
215
+ status_msg = (
216
+ f"{state['pdf_count']} PDF(s) loaded. "
217
+ f"Total chunks: {state['total_chunks']}"
218
+ )
219
 
220
  return (
221
+ status_msg,
222
  state,
223
+ gr.update(choices=list(state["files"].keys()), value=None),
224
  )
225
 
226
  except Exception as e:
227
  return (
228
  f"Error processing PDF: {e}",
229
  state,
230
+ gr.update(choices=list(state["files"].keys()), value=None),
231
  )
232
+
233
+
234
  def remove_pdf(filename, state):
235
  if filename not in state["files"]:
236
  return (
237
  f"{filename} not found.",
238
  state,
239
+ gr.update(choices=list(state["files"].keys()), value=None),
240
  )
241
 
242
  # Remove the file entry
243
  state["files"].pop(filename, None)
244
 
245
+ # Recompute counts
246
+ pdf_count = len(state["files"])
247
+ total_chunks = sum(len(data["chunks"]) for data in state["files"].values())
 
248
 
249
+ state["pdf_count"] = pdf_count
250
+ state["total_chunks"] = total_chunks
251
 
252
+ status_msg = (
253
+ f"Removed {filename}. "
254
+ f"Total PDFs: {pdf_count}. Total chunks: {total_chunks}"
255
+ )
 
256
 
257
  return (
258
+ status_msg,
259
  state,
260
+ gr.update(choices=list(state["files"].keys()), value=None),
261
  )
262
 
263
+
264
  def answer_question(question, state):
 
265
  print("HF_TOKEN loaded:", HF_TOKEN is not None)
266
 
267
  if HF_TOKEN:
 
273
  if not question.strip():
274
  return "Please enter a question."
275
 
276
+ if not state["files"]:
277
+ return "Please upload at least one PDF first."
278
 
279
+ context = retrieve_context_per_file(question, state, k=10)
280
  answer = generate_answer(question, context)
281
  return answer
282
 
 
286
  # ============================================================
287
 
288
  with gr.Blocks(title="PDF Q&A Chatbot") as demo:
 
289
  gr.Markdown("## 📄 PDF Q&A Chatbot\nUpload PDFs and ask questions about their content.")
290
 
291
+ state = gr.State(
292
+ {
293
+ "files": {}, # filename -> {chunks, embeddings}
294
+ "pdf_count": 0,
295
+ "total_chunks": 0,
296
+ }
297
+ )
298
 
299
  with gr.Row():
300
  # LEFT COLUMN — PDF upload + removal
301
  with gr.Column(scale=1):
302
+ pdf_input = gr.File(
303
+ label="Upload PDF(s)",
304
+ file_types=[".pdf"],
305
+ file_count="multiple", # proper multi-upload in Gradio 6
306
+ )
307
  pdf_status = gr.Textbox(label="Status", interactive=False)
308
 
309
  file_selector = gr.Dropdown(
310
  label="Remove a PDF",
311
  choices=[],
312
+ interactive=True,
313
  )
314
  remove_button = gr.Button("Remove Selected PDF")
315
 
 
319
  question = gr.Textbox(
320
  label="Your question",
321
  lines=2,
322
+ scale=3,
323
  )
324
  clear_button = gr.Button(
325
  "Clear",
326
  scale=1,
327
  min_width=80,
328
+ elem_classes="clear-btn",
329
  )
330
 
331
  answer = gr.Textbox(label="Answer", lines=8)
332
  ask_button = gr.Button("Ask")
333
 
334
  # Bind events AFTER components exist
335
+ pdf_input.change(
336
+ load_pdf,
337
+ inputs=[pdf_input, state],
338
+ outputs=[pdf_status, state, file_selector],
339
+ )
340
 
341
  remove_button.click(
342
  remove_pdf,
343
  inputs=[file_selector, state],
344
+ outputs=[pdf_status, state, file_selector],
345
  )
346
 
347
  ask_button.click(
348
  answer_question,
349
  inputs=[question, state],
350
+ outputs=answer,
351
  )
352
 
353
  clear_button.click(lambda: "", outputs=question)
 
355
 
356
  if __name__ == "__main__":
357
  demo.launch(
358
+ css=".clear-btn button { height: 100%; min-height: 52px; }"
359
+ )