Resham2987 commited on
Commit
6e1e0ae
ยท
verified ยท
1 Parent(s): ce74469

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +12 -0
  2. app.py +280 -0
  3. requirements.txt +6 -0
README.md ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: PDF RAG Chat Bot
3
+ emoji: ๐Ÿ’ป
4
+ colorFrom: green
5
+ colorTo: red
6
+ sdk: gradio
7
+ sdk_version: 5.49.1
8
+ app_file: app.py
9
+ pinned: false
10
+ ---
11
+
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import asyncio
3
+ import json
4
+ import hashlib
5
+ import shutil
6
+ from io import BytesIO
7
+ from typing import List, Tuple
8
+
9
+ import gradio as gr
10
+ import numpy as np
11
+ import faiss
12
+ import requests
13
+ from sentence_transformers import SentenceTransformer
14
+ import fitz # PyMuPDF
15
+
16
+ # ---------------- Config ----------------
17
+ OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
18
+ OPENROUTER_MODEL = "nvidia/nemotron-nano-12b-v2-vl:free"
19
+ EMBEDDING_MODEL_NAME = "all-MiniLM-L6-v2"
20
+ CACHE_DIR = "./cache"
21
+ SYSTEM_PROMPT = "You are a helpful assistant."
22
+
23
+ os.makedirs(CACHE_DIR, exist_ok=True)
24
+
25
+ embedder = SentenceTransformer(EMBEDDING_MODEL_NAME)
26
+
27
+ DOCS: List[str] = []
28
+ FILENAMES: List[str] = []
29
+ EMBEDDINGS: np.ndarray = None
30
+ FAISS_INDEX = None
31
+ CURRENT_CACHE_KEY: str = ""
32
+
33
+
34
+ # ---------------- Periodic cache cleanup ----------------
35
+ async def clear_cache_every_5min():
36
+ while True:
37
+ await asyncio.sleep(300)
38
+ try:
39
+ if os.path.exists(CACHE_DIR):
40
+ shutil.rmtree(CACHE_DIR)
41
+ os.makedirs(CACHE_DIR, exist_ok=True)
42
+ print("๐Ÿงน Cache cleared.")
43
+ except Exception as e:
44
+ print(f"[Cache cleanup error] {e}")
45
+
46
+ asyncio.get_event_loop().create_task(clear_cache_every_5min())
47
+
48
+
49
+ # ---------------- PDF extraction ----------------
50
+ def extract_text_from_pdf(file_bytes: bytes) -> str:
51
+ try:
52
+ doc = fitz.open(stream=file_bytes, filetype="pdf")
53
+ return "\n".join(page.get_text() for page in doc)
54
+ except Exception as e:
55
+ return f"[PDF extraction error] {e}"
56
+
57
+
58
+ # ---------------- Cache + FAISS helpers ----------------
59
+ def make_cache_key(files: List[Tuple[str, bytes]]) -> str:
60
+ h = hashlib.sha256()
61
+ for name, b in sorted(files, key=lambda x: x[0]):
62
+ h.update(name.encode())
63
+ h.update(str(len(b)).encode())
64
+ h.update(hashlib.sha256(b).digest())
65
+ return h.hexdigest()
66
+
67
+ def cache_save(cache_key: str, embeddings: np.ndarray, filenames: List[str]):
68
+ np.savez_compressed(os.path.join(CACHE_DIR, f"{cache_key}.npz"),
69
+ embeddings=embeddings, filenames=np.array(filenames))
70
+
71
+ def cache_load(cache_key: str):
72
+ path = os.path.join(CACHE_DIR, f"{cache_key}.npz")
73
+ if not os.path.exists(path): return None
74
+ try:
75
+ data = np.load(path, allow_pickle=True)
76
+ return data["embeddings"], data["filenames"].tolist()
77
+ except:
78
+ return None
79
+
80
+ def build_faiss(emb: np.ndarray):
81
+ global FAISS_INDEX
82
+ if emb is None or len(emb) == 0:
83
+ FAISS_INDEX = None
84
+ return None
85
+ emb = emb.astype("float32")
86
+ index = faiss.IndexFlatL2(emb.shape[1])
87
+ index.add(emb)
88
+ FAISS_INDEX = index
89
+ return index
90
+
91
+ def search(query: str, k: int = 3):
92
+ if FAISS_INDEX is None:
93
+ return []
94
+ q_emb = embedder.encode([query], convert_to_numpy=True).astype("float32")
95
+ D, I = FAISS_INDEX.search(q_emb, k)
96
+ return [
97
+ {"index": int(i), "distance": float(d), "text": DOCS[i], "source": FILENAMES[i]}
98
+ for d, i in zip(D[0], I[0]) if i >= 0
99
+ ]
100
+
101
+
102
+ # ---------------- OpenRouter API ----------------
103
+ def call_openrouter(prompt: str):
104
+ if not OPENROUTER_API_KEY:
105
+ return "[OpenRouter error] Missing OPENROUTER_API_KEY."
106
+
107
+ url = "https://openrouter.ai/api/v1/chat/completions"
108
+ headers = {
109
+ "Authorization": f"Bearer {OPENROUTER_API_KEY}",
110
+ "Content-Type": "application/json",
111
+ }
112
+
113
+ payload = {
114
+ "model": OPENROUTER_MODEL,
115
+ "messages": [
116
+ {"role": "system",
117
+ "content": SYSTEM_PROMPT + " Always respond in plain text. Avoid markdown."},
118
+ {"role": "user", "content": prompt},
119
+ ],
120
+ }
121
+
122
+ try:
123
+ r = requests.post(url, headers=headers, json=payload, timeout=60)
124
+ r.raise_for_status()
125
+ obj = r.json()
126
+
127
+ if "choices" in obj and obj["choices"]:
128
+ text = obj["choices"][0]["message"]["content"]
129
+ return text.strip().replace("```", "")
130
+ return "[Unexpected OpenRouter response]"
131
+ except Exception as e:
132
+ return f"[OpenRouter request error] {e}"
133
+
134
+ # ---------- Helper to read bytes from various Gradio file shapes ----------
135
+ def read_file_bytes(f) -> Tuple[str, bytes]:
136
+ """
137
+ Accepts the variety of file objects Gradio may pass:
138
+ - file-like objects with .name and .read()
139
+ - objects with .name and .value (NamedString)
140
+ - tuples like (name, bytes)
141
+ - dicts that may contain 'name' and 'data' or temporary path keys
142
+ - string filesystem paths
143
+ Returns (filename, bytes)
144
+ Raises ValueError for unsupported shapes.
145
+ """
146
+ # tuple (name, bytes)
147
+ if isinstance(f, tuple) and len(f) == 2 and isinstance(f[1], (bytes, bytearray)):
148
+ return f[0], bytes(f[1])
149
+
150
+ # dict-like (from some frontends)
151
+ if isinstance(f, dict):
152
+ name = f.get("name") or f.get("filename") or "uploaded"
153
+ # raw bytes/content
154
+ data = f.get("data") or f.get("content") or f.get("value") or f.get("file")
155
+ if isinstance(data, (bytes, bytearray)):
156
+ return name, bytes(data)
157
+ if isinstance(data, str):
158
+ # data could be text content
159
+ try:
160
+ return name, data.encode("utf-8")
161
+ except Exception:
162
+ pass
163
+ # maybe a temp file path
164
+ tmp_path = f.get("tmp_path") or f.get("path") or f.get("file")
165
+ if tmp_path and isinstance(tmp_path, str) and os.path.exists(tmp_path):
166
+ with open(tmp_path, "rb") as fh:
167
+ return os.path.basename(tmp_path), fh.read()
168
+
169
+ # file-like object with read()
170
+ if hasattr(f, "name") and hasattr(f, "read"):
171
+ try:
172
+ name = os.path.basename(f.name) if getattr(f, "name", None) else "uploaded"
173
+ return name, f.read()
174
+ except Exception:
175
+ pass
176
+
177
+ # NamedString-like: has .name and .value
178
+ if hasattr(f, "name") and hasattr(f, "value"):
179
+ name = os.path.basename(getattr(f, "name") or "uploaded")
180
+ v = getattr(f, "value")
181
+ if isinstance(v, (bytes, bytearray)):
182
+ return name, bytes(v)
183
+ if isinstance(v, str):
184
+ return name, v.encode("utf-8")
185
+
186
+ # string path
187
+ if isinstance(f, str) and os.path.exists(f):
188
+ with open(f, "rb") as fh:
189
+ return os.path.basename(f), fh.read()
190
+
191
+ raise ValueError(f"Unsupported file object type: {type(f)}")
192
+
193
+
194
+ # ---------------- PDF Upload & Index (fixed) ----------------
195
+ def upload_and_index(files):
196
+ global DOCS, FILENAMES, EMBEDDINGS, CURRENT_CACHE_KEY
197
+
198
+ if not files:
199
+ return "No PDF uploaded.", ""
200
+
201
+ processed = []
202
+ # files may be a single object or a list; normalize
203
+ if not isinstance(files, (list, tuple)):
204
+ files = [files]
205
+
206
+ try:
207
+ for f in files:
208
+ name, b = read_file_bytes(f)
209
+ processed.append((name, b))
210
+ except ValueError as e:
211
+ # return a clear message to the UI so user can debug what Gradio passed
212
+ return f"Upload error: {e}", ""
213
+
214
+ # preview for UI
215
+ preview = [{"name": n, "size": len(b)} for n, b in processed]
216
+
217
+ # cache key
218
+ cache_key = make_cache_key(processed)
219
+ CURRENT_CACHE_KEY = cache_key
220
+
221
+ cached = cache_load(cache_key)
222
+ if cached:
223
+ EMBEDDINGS, FILENAMES = cached
224
+ EMBEDDINGS = np.array(EMBEDDINGS)
225
+ DOCS = [extract_text_from_pdf(b) for _, b in processed]
226
+ build_faiss(EMBEDDINGS)
227
+ return f"Loaded cached embeddings ({len(FILENAMES)} PDFs).", json.dumps(preview)
228
+
229
+ # extract text and index
230
+ DOCS = [extract_text_from_pdf(b) for _, b in processed]
231
+ FILENAMES = [n for n, _ in processed]
232
+
233
+ EMBEDDINGS = embedder.encode(DOCS, convert_to_numpy=True).astype("float32")
234
+ cache_save(cache_key, EMBEDDINGS, FILENAMES)
235
+ build_faiss(EMBEDDINGS)
236
+
237
+ return f"Uploaded + indexed {len(DOCS)} PDFs.", json.dumps(preview)
238
+
239
+
240
+ # ---------------- Question Answering ----------------
241
+ def ask(question: str):
242
+ if not question:
243
+ return "Please enter a question."
244
+ if not DOCS:
245
+ return "No PDFs indexed."
246
+
247
+ results = search(question)
248
+
249
+ if not results:
250
+ return "No relevant text found."
251
+
252
+ context = "\n".join(
253
+ f"Source: {r['source']}\n\n{r['text'][:15000]}\n---\n"
254
+ for r in results
255
+ )
256
+
257
+ prompt = f"Use this context to answer briefly:\n\n{context}\nQuestion: {question}\nAnswer:"
258
+ return call_openrouter(prompt)
259
+
260
+
261
+ # ---------------- Gradio UI ----------------
262
+ with gr.Blocks(title="PDF RAG Bot") as demo:
263
+ gr.Markdown("# ๐Ÿ“„ PDF-Only RAG Bot\nUpload PDFs โ†’ Ask Questions โ†’ AI Answers from PDF content.")
264
+
265
+ file_input = gr.File(label="Upload PDF files", file_count="multiple", file_types=[".pdf"])
266
+ upload_btn = gr.Button("Upload & Index")
267
+ status = gr.Textbox(label="Status", interactive=False)
268
+ preview = gr.Textbox(label="Upload preview (JSON)", interactive=False)
269
+
270
+ upload_btn.click(upload_and_index, inputs=[file_input], outputs=[status, preview])
271
+
272
+ gr.Markdown("### Ask a Question")
273
+ q = gr.Textbox(label="Your question", lines=3)
274
+ ask_btn = gr.Button("Ask PDF Bot")
275
+ answer = gr.Textbox(label="Answer", lines=15)
276
+
277
+ ask_btn.click(ask, inputs=[q], outputs=[answer])
278
+
279
+ if __name__ == "__main__":
280
+ demo.launch(server_name="0.0.0.0", server_port=7860, debug=True)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio
2
+ numpy
3
+ faiss-cpu
4
+ requests
5
+ sentence-transformers
6
+ PyMuPDF