internkk commited on
Commit
6e83a04
·
1 Parent(s): e9ef630

Rebuild with beautiful Gradio UI for HuggingFace Spaces

Browse files
Files changed (2) hide show
  1. app.py +404 -305
  2. requirements.txt +1 -1
app.py CHANGED
@@ -1,6 +1,6 @@
1
  import os
2
  import tempfile
3
- import streamlit as st
4
  import fitz # PyMuPDF
5
  import pandas as pd
6
  from docx import Document
@@ -11,29 +11,16 @@ from langchain.embeddings.base import Embeddings
11
  from langchain_chroma import Chroma
12
  from groq import Groq
13
 
14
- # ---------- Page Config ----------
15
- st.set_page_config(
16
- page_title="RAG Document Assistant",
17
- page_icon="📄",
18
- layout="wide"
19
- )
20
-
21
- st.title("📄 RAG Document Assistant")
22
- st.markdown("Upload documents, ask questions, get AI-powered answers from your files.")
23
-
24
-
25
  # ---------- Embedding Model ----------
26
- @st.cache_resource(show_spinner="Loading embedding model (first time only)...")
27
- def load_embedding_model():
28
- # Use /tmp for Databricks compatibility, falls back to home cache
29
- for cache_dir in ["/tmp/sentence_transformers", os.path.join(os.path.expanduser("~"), ".cache", "sentence_transformers")]:
30
- try:
31
- os.makedirs(cache_dir, exist_ok=True)
32
- model = SentenceTransformer("all-MiniLM-L6-v2", cache_folder=cache_dir)
33
- return model
34
- except Exception:
35
- continue
36
- return SentenceTransformer("all-MiniLM-L6-v2")
37
 
38
 
39
  class BGEEmbeddings(Embeddings):
@@ -49,347 +36,459 @@ class BGEEmbeddings(Embeddings):
49
 
50
  # ---------- Document Parsers ----------
51
  def parse_pdf(file_path):
52
- """Parse PDF - extract text from each page"""
53
  documents = []
54
  pdf = fitz.open(file_path)
55
  for page_num in range(len(pdf)):
56
  page = pdf.load_page(page_num)
57
  text = page.get_text().strip()
58
  if text:
59
- documents.append(
60
- LCDocument(
61
- page_content=text,
62
- metadata={"source": os.path.basename(file_path), "page": page_num + 1}
63
- )
64
- )
65
  return documents
66
 
67
 
68
  def parse_docx(file_path):
69
- """Parse Word document"""
70
  doc = Document(file_path)
71
- text = "\n".join(para.text for para in doc.paragraphs if para.text.strip())
72
  return [LCDocument(page_content=text, metadata={"source": os.path.basename(file_path), "page": 1})]
73
 
74
 
75
  def parse_excel(file_path):
76
- """Parse Excel file - each sheet becomes a document"""
77
  documents = []
78
  excel = pd.ExcelFile(file_path)
79
  for sheet in excel.sheet_names:
80
  df = pd.read_excel(file_path, sheet_name=sheet)
81
  text = df.astype(str).to_string(index=False)
82
- documents.append(
83
- LCDocument(page_content=text, metadata={"source": os.path.basename(file_path), "sheet": sheet})
84
- )
 
85
  return documents
86
 
87
 
88
  def parse_csv(file_path):
89
- """Parse CSV file"""
90
  df = pd.read_csv(file_path)
91
  text = df.astype(str).to_string(index=False)
92
  return [LCDocument(page_content=text, metadata={"source": os.path.basename(file_path), "page": 1})]
93
 
94
 
95
  def parse_txt(file_path):
96
- """Parse text file"""
97
  with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
98
  text = f.read()
99
  return [LCDocument(page_content=text, metadata={"source": os.path.basename(file_path), "page": 1})]
100
 
101
 
102
- # ---------- Get API Key from Secrets or UI ----------
103
- groq_api_key = ""
104
- try:
105
- groq_api_key = st.secrets["GROQ_API_KEY"]
106
- except:
107
- groq_api_key = os.getenv("GROQ_API_KEY", "")
 
 
 
 
 
 
 
108
 
109
- # ---------- Sidebar ----------
110
- with st.sidebar:
111
- st.header("⚙️ Configuration")
112
- st.markdown("**Supported formats:** PDF, DOCX, XLSX, CSV, TXT")
113
- st.markdown("---")
114
- if not groq_api_key:
115
- groq_api_key = st.text_input(
116
- "🔑 Groq API Key",
117
- type="password",
118
- placeholder="Enter your Groq API key...",
119
- help="Get yours at https://console.groq.com"
120
- )
121
- else:
122
- st.success("✅ API Key configured")
123
- st.markdown("---")
124
- st.markdown("Built with LangChain + ChromaDB + Groq")
125
-
126
-
127
- # ---------- Session State ----------
128
- if "documents" not in st.session_state:
129
- st.session_state.documents = []
130
- if "vector_store" not in st.session_state:
131
- st.session_state.vector_store = None
132
- if "chat_history" not in st.session_state:
133
- st.session_state.chat_history = []
134
-
135
-
136
- # ---------- File Upload ----------
137
- st.header("1️⃣ Upload Documents")
138
-
139
- # Show current document count
140
- if st.session_state.documents:
141
- st.info(f"📚 Current knowledge base: {len(st.session_state.documents)} document(s)")
142
-
143
- uploaded_files = st.file_uploader(
144
- "Drop your files here",
145
- type=["pdf", "docx", "xlsx", "csv", "txt"],
146
- accept_multiple_files=True
147
- )
148
-
149
- # Show two buttons if we already have documents
150
- col1, col2 = st.columns(2)
151
-
152
- with col1:
153
- process_btn = st.button("🔄 Process Documents", type="primary", disabled=not uploaded_files)
154
-
155
- with col2:
156
- if st.session_state.documents:
157
- add_btn = st.button("➕ Add to Existing", disabled=not uploaded_files)
158
- else:
159
- add_btn = False
160
-
161
- # Determine mode: new or add
162
- process_mode = None
163
- if process_btn:
164
- process_mode = "new"
165
- elif add_btn:
166
- process_mode = "add"
167
-
168
- if uploaded_files and process_mode:
169
- # Load embedding model (cached after first load)
170
- with st.spinner("Loading embedding model..."):
171
- embedding_model = load_embedding_model()
172
-
173
- all_documents = []
174
- progress = st.progress(0)
175
- status = st.status("Processing documents...", expanded=True)
176
 
177
- for i, uploaded_file in enumerate(uploaded_files):
178
- ext = os.path.splitext(uploaded_file.name)[1].lower()
 
179
 
180
- # Save to temp file
181
- with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as tmp:
182
- tmp.write(uploaded_file.read())
183
- tmp_path = tmp.name
184
 
185
- try:
186
- if ext == ".pdf":
187
- docs = parse_pdf(tmp_path)
188
- elif ext == ".docx":
189
- docs = parse_docx(tmp_path)
190
- elif ext == ".xlsx":
191
- docs = parse_excel(tmp_path)
192
- elif ext == ".csv":
193
- docs = parse_csv(tmp_path)
194
- elif ext == ".txt":
195
- docs = parse_txt(tmp_path)
196
- else:
197
- docs = []
198
- status.write(f"⚠️ Unsupported: {uploaded_file.name}")
199
 
200
- all_documents.extend(docs)
201
- status.write(f"✅ Parsed: {uploaded_file.name} ({len(docs)} chunk(s))")
 
 
 
 
202
 
 
 
 
 
 
 
 
203
  except Exception as e:
204
- status.write(f"❌ Error with {uploaded_file.name}: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
206
- finally:
207
- try:
208
- os.remove(tmp_path)
209
- except (PermissionError, FileNotFoundError):
210
- pass
 
 
 
211
 
212
- progress.progress((i + 1) / len(uploaded_files))
 
 
213
 
214
- if all_documents:
215
- # Chunk documents
216
- status.write("🧠 Chunking documents...")
217
 
218
- text_splitter = RecursiveCharacterTextSplitter(
219
- chunk_size=1000,
220
- chunk_overlap=200,
221
- separators=["\n\n", "\n", ". ", " ", ""]
222
- )
223
 
224
- chunked_documents = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  for doc in all_documents:
226
  if len(doc.page_content.strip()) > 50:
227
- chunks = text_splitter.create_documents(
228
- [doc.page_content],
229
- metadatas=[doc.metadata]
230
- )
231
- chunked_documents.extend(chunks)
232
  else:
233
- chunked_documents.append(doc)
234
 
235
- embedding_fn = BGEEmbeddings(embedding_model)
 
 
 
236
 
237
- if process_mode == "add" and st.session_state.vector_store:
238
- # Add to existing vector store
239
- status.write("📦 Adding to existing knowledge base...")
240
- st.session_state.vector_store.add_documents(chunked_documents)
241
- st.session_state.documents.extend(all_documents)
242
- status.update(label="✅ Documents added!", state="complete")
243
- st.success(f"Added {len(all_documents)} document(s) ({len(chunked_documents)} chunks) to existing knowledge base.")
244
- else:
245
- # Build new vector store
246
- status.write("📦 Building vector store...")
247
- vector_store = Chroma.from_documents(
248
- documents=chunked_documents,
249
- embedding=embedding_fn,
250
- collection_name="rag_docs"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  )
252
- st.session_state.documents = all_documents
253
- st.session_state.vector_store = vector_store
254
- status.update(label="✅ Processing complete!", state="complete")
255
- st.success(f"Processed {len(all_documents)} document(s) into {len(chunked_documents)} chunks.")
256
 
 
 
257
 
258
- # ---------- Document Preview ----------
259
- if st.session_state.documents:
260
- st.header("2️⃣ Document Preview")
261
- with st.expander(f"📋 View parsed documents ({len(st.session_state.documents)} total)", expanded=False):
262
- for i, doc in enumerate(st.session_state.documents):
263
- st.markdown(f"**Document {i+1}** — `{doc.metadata.get('source', 'unknown')}`")
264
- st.text(doc.page_content[:500] + ("..." if len(doc.page_content) > 500 else ""))
265
- st.markdown("---")
266
 
 
 
 
267
 
268
- # ---------- Chat / Q&A ----------
269
- if st.session_state.vector_store:
270
- st.header("3️⃣ Ask Questions")
 
 
 
271
 
272
- if not groq_api_key:
273
- st.warning("⚠️ Enter your Groq API key in the sidebar to enable Q&A.")
274
- else:
275
- # Display chat history
276
- for msg in st.session_state.chat_history:
277
- with st.chat_message(msg["role"]):
278
- st.markdown(msg["content"])
279
-
280
- # Add documents inline with chat
281
- with st.expander("📎 Add more documents to knowledge base"):
282
- new_files = st.file_uploader(
283
- "Upload additional documents",
284
- type=["pdf", "docx", "xlsx", "csv", "txt"],
285
- accept_multiple_files=True,
286
- key="chat_upload"
287
  )
288
- if new_files and st.button("➕ Add to Knowledge Base"):
289
- with st.spinner("Adding documents..."):
290
- embedding_model = load_embedding_model()
291
- new_docs = []
292
- for uploaded_file in new_files:
293
- ext = os.path.splitext(uploaded_file.name)[1].lower()
294
- with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as tmp:
295
- tmp.write(uploaded_file.read())
296
- tmp_path = tmp.name
297
- try:
298
- if ext == ".pdf":
299
- docs = parse_pdf(tmp_path)
300
- elif ext == ".docx":
301
- docs = parse_docx(tmp_path)
302
- elif ext == ".xlsx":
303
- docs = parse_excel(tmp_path)
304
- elif ext == ".csv":
305
- docs = parse_csv(tmp_path)
306
- elif ext == ".txt":
307
- docs = parse_txt(tmp_path)
308
- else:
309
- docs = []
310
- new_docs.extend(docs)
311
- except Exception as e:
312
- st.error(f"❌ Error: {e}")
313
- finally:
314
- try:
315
- os.remove(tmp_path)
316
- except:
317
- pass
318
-
319
- if new_docs:
320
- text_splitter = RecursiveCharacterTextSplitter(
321
- chunk_size=1000, chunk_overlap=200,
322
- separators=["\n\n", "\n", ". ", " ", ""]
323
- )
324
- chunked = []
325
- for doc in new_docs:
326
- if len(doc.page_content.strip()) > 50:
327
- chunks = text_splitter.create_documents(
328
- [doc.page_content], metadatas=[doc.metadata]
329
- )
330
- chunked.extend(chunks)
331
- else:
332
- chunked.append(doc)
333
-
334
- st.session_state.vector_store.add_documents(chunked)
335
- st.session_state.documents.extend(new_docs)
336
- st.success(f"✅ Added {len(new_docs)} document(s)!")
337
- st.rerun()
338
-
339
- # Chat input
340
- user_question = st.chat_input("Ask a question about your documents...")
341
-
342
- if user_question:
343
- st.session_state.chat_history.append({"role": "user", "content": user_question})
344
-
345
- with st.chat_message("user"):
346
- st.markdown(user_question)
347
-
348
- with st.chat_message("assistant"):
349
- with st.spinner("Thinking..."):
350
- # Retrieve relevant chunks
351
- retriever = st.session_state.vector_store.as_retriever(
352
- search_type="similarity",
353
- search_kwargs={"k": 5}
354
- )
355
- relevant_docs = retriever.invoke(user_question)
356
-
357
- # Build context
358
- context = "\n\n---\n\n".join([
359
- f"[Source: {doc.metadata.get('source', 'unknown')}, "
360
- f"Page: {doc.metadata.get('page', 'N/A')}]\n{doc.page_content}"
361
- for doc in relevant_docs
362
- ])
363
-
364
- # Query Groq
365
- client = Groq(api_key=groq_api_key)
366
- response = client.chat.completions.create(
367
- model="llama-3.1-8b-instant",
368
- messages=[
369
- {
370
- "role": "system",
371
- "content": (
372
- "You are a helpful assistant that answers questions based on the provided document context. "
373
- "Use only the information from the context below. If the answer is not in the context, "
374
- "say so clearly. Cite the source document and page when possible.\n\n"
375
- f"Context:\n{context}"
376
- )
377
- },
378
- {"role": "user", "content": user_question}
379
- ],
380
- temperature=0.3,
381
- max_tokens=1024
382
- )
383
 
384
- answer = response.choices[0].message.content
385
- st.markdown(answer)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
 
387
- # Show sources
388
- with st.expander("📚 Sources"):
389
- for doc in relevant_docs:
390
- st.markdown(
391
- f"- **{doc.metadata.get('source', 'unknown')}** "
392
- f"(Page {doc.metadata.get('page', 'N/A')})"
393
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
 
395
- st.session_state.chat_history.append({"role": "assistant", "content": answer})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import tempfile
3
+ import gradio as gr
4
  import fitz # PyMuPDF
5
  import pandas as pd
6
  from docx import Document
 
11
  from langchain_chroma import Chroma
12
  from groq import Groq
13
 
 
 
 
 
 
 
 
 
 
 
 
14
  # ---------- Embedding Model ----------
15
+ _embedding_model = None
16
+
17
+ def get_embedding_model():
18
+ global _embedding_model
19
+ if _embedding_model is None:
20
+ cache_dir = "/tmp/sentence_transformers"
21
+ os.makedirs(cache_dir, exist_ok=True)
22
+ _embedding_model = SentenceTransformer("all-MiniLM-L6-v2", cache_folder=cache_dir)
23
+ return _embedding_model
 
 
24
 
25
 
26
  class BGEEmbeddings(Embeddings):
 
36
 
37
  # ---------- Document Parsers ----------
38
  def parse_pdf(file_path):
 
39
  documents = []
40
  pdf = fitz.open(file_path)
41
  for page_num in range(len(pdf)):
42
  page = pdf.load_page(page_num)
43
  text = page.get_text().strip()
44
  if text:
45
+ documents.append(LCDocument(
46
+ page_content=text,
47
+ metadata={"source": os.path.basename(file_path), "page": page_num + 1}
48
+ ))
 
 
49
  return documents
50
 
51
 
52
  def parse_docx(file_path):
 
53
  doc = Document(file_path)
54
+ text = "\n".join(p.text for p in doc.paragraphs if p.text.strip())
55
  return [LCDocument(page_content=text, metadata={"source": os.path.basename(file_path), "page": 1})]
56
 
57
 
58
  def parse_excel(file_path):
 
59
  documents = []
60
  excel = pd.ExcelFile(file_path)
61
  for sheet in excel.sheet_names:
62
  df = pd.read_excel(file_path, sheet_name=sheet)
63
  text = df.astype(str).to_string(index=False)
64
+ documents.append(LCDocument(
65
+ page_content=text,
66
+ metadata={"source": os.path.basename(file_path), "sheet": sheet}
67
+ ))
68
  return documents
69
 
70
 
71
  def parse_csv(file_path):
 
72
  df = pd.read_csv(file_path)
73
  text = df.astype(str).to_string(index=False)
74
  return [LCDocument(page_content=text, metadata={"source": os.path.basename(file_path), "page": 1})]
75
 
76
 
77
  def parse_txt(file_path):
 
78
  with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
79
  text = f.read()
80
  return [LCDocument(page_content=text, metadata={"source": os.path.basename(file_path), "page": 1})]
81
 
82
 
83
+ def parse_file(file_path):
84
+ ext = os.path.splitext(file_path)[1].lower()
85
+ if ext == ".pdf":
86
+ return parse_pdf(file_path)
87
+ elif ext == ".docx":
88
+ return parse_docx(file_path)
89
+ elif ext in [".xlsx", ".xls"]:
90
+ return parse_excel(file_path)
91
+ elif ext == ".csv":
92
+ return parse_csv(file_path)
93
+ elif ext == ".txt":
94
+ return parse_txt(file_path)
95
+ return []
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
+ # ---------- Global State ----------
99
+ vector_store = None
100
+ processed_files = []
101
 
 
 
 
 
102
 
103
+ # ---------- Process Documents ----------
104
+ def process_documents(files):
105
+ global vector_store, processed_files
 
 
 
 
 
 
 
 
 
 
 
106
 
107
+ if not files:
108
+ return "⚠️ Please upload at least one document.", ""
109
+
110
+ all_documents = []
111
+ file_names = []
112
+ status_lines = []
113
 
114
+ for file in files:
115
+ try:
116
+ docs = parse_file(file.name)
117
+ all_documents.extend(docs)
118
+ name = os.path.basename(file.name)
119
+ file_names.append(name)
120
+ status_lines.append(f"✅ {name} → {len(docs)} section(s)")
121
  except Exception as e:
122
+ status_lines.append(f"❌ {os.path.basename(file.name)} → Error: {e}")
123
+
124
+ if not all_documents:
125
+ return "❌ No text could be extracted from the uploaded files.", ""
126
+
127
+ # Chunk
128
+ splitter = RecursiveCharacterTextSplitter(
129
+ chunk_size=1000, chunk_overlap=200,
130
+ separators=["\n\n", "\n", ". ", " ", ""]
131
+ )
132
+ chunks = []
133
+ for doc in all_documents:
134
+ if len(doc.page_content.strip()) > 50:
135
+ chunks.extend(splitter.create_documents([doc.page_content], metadatas=[doc.metadata]))
136
+ else:
137
+ chunks.append(doc)
138
 
139
+ # Build vector store
140
+ model = get_embedding_model()
141
+ embedding_fn = BGEEmbeddings(model)
142
+ vector_store = Chroma.from_documents(
143
+ documents=chunks,
144
+ embedding=embedding_fn,
145
+ collection_name="rag_docs"
146
+ )
147
 
148
+ processed_files.extend(file_names)
149
+ status_lines.append(f"\n📦 {len(chunks)} chunks indexed into vector store")
150
+ status_lines.append(f"🟢 Ready to answer questions!")
151
 
152
+ files_list = "\n".join([f"• {f}" for f in processed_files])
153
+ return "\n".join(status_lines), files_list
 
154
 
 
 
 
 
 
155
 
156
+ def add_documents(files):
157
+ global vector_store, processed_files
158
+
159
+ if not files:
160
+ return "⚠️ Please upload files to add.", "\n".join([f"• {f}" for f in processed_files])
161
+
162
+ if vector_store is None:
163
+ return process_documents(files)
164
+
165
+ all_documents = []
166
+ file_names = []
167
+ status_lines = []
168
+
169
+ for file in files:
170
+ try:
171
+ docs = parse_file(file.name)
172
+ all_documents.extend(docs)
173
+ name = os.path.basename(file.name)
174
+ file_names.append(name)
175
+ status_lines.append(f"✅ {name} → {len(docs)} section(s) added")
176
+ except Exception as e:
177
+ status_lines.append(f"❌ {os.path.basename(file.name)} → {e}")
178
+
179
+ if all_documents:
180
+ splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
181
+ chunks = []
182
  for doc in all_documents:
183
  if len(doc.page_content.strip()) > 50:
184
+ chunks.extend(splitter.create_documents([doc.page_content], metadatas=[doc.metadata]))
 
 
 
 
185
  else:
186
+ chunks.append(doc)
187
 
188
+ model = get_embedding_model()
189
+ vector_store.add_documents(chunks)
190
+ processed_files.extend(file_names)
191
+ status_lines.append(f"📦 {len(chunks)} new chunks added!")
192
 
193
+ files_list = "\n".join([f"• {f}" for f in processed_files])
194
+ return "\n".join(status_lines), files_list
195
+
196
+
197
+ # ---------- Chat ----------
198
+ def chat(message, history, groq_api_key):
199
+ global vector_store
200
+
201
+ if not groq_api_key or groq_api_key.strip() == "":
202
+ groq_api_key = os.getenv("GROQ_API_KEY", "")
203
+
204
+ if not groq_api_key:
205
+ history.append((message, "⚠️ Please enter your Groq API key in the settings panel."))
206
+ return "", history
207
+
208
+ if vector_store is None:
209
+ history.append((message, "📂 Please upload and process documents first before asking questions."))
210
+ return "", history
211
+
212
+ try:
213
+ retriever = vector_store.as_retriever(search_type="similarity", search_kwargs={"k": 5})
214
+ relevant_docs = retriever.invoke(message)
215
+
216
+ context = "\n\n---\n\n".join([
217
+ f"[Source: {doc.metadata.get('source', 'unknown')}, Page: {doc.metadata.get('page', 'N/A')}]\n{doc.page_content}"
218
+ for doc in relevant_docs
219
+ ])
220
+
221
+ client = Groq(api_key=groq_api_key)
222
+ response = client.chat.completions.create(
223
+ model="llama-3.1-8b-instant",
224
+ messages=[
225
+ {
226
+ "role": "system",
227
+ "content": (
228
+ "You are an intelligent document assistant. Answer questions based on the provided context. "
229
+ "Be concise, accurate, and cite the source document when possible. "
230
+ "If the answer is not in the context, say so clearly.\n\n"
231
+ f"Context:\n{context}"
232
+ )
233
+ },
234
+ {"role": "user", "content": message}
235
+ ],
236
+ temperature=0.3,
237
+ max_tokens=1024
238
+ )
239
+
240
+ answer = response.choices[0].message.content
241
+
242
+ sources = list(set([
243
+ f"{doc.metadata.get('source', 'unknown')} (p.{doc.metadata.get('page', 'N/A')})"
244
+ for doc in relevant_docs
245
+ ]))
246
+ sources_text = "\n\n📚 **Sources:** " + " | ".join(sources)
247
+
248
+ history.append((message, answer + sources_text))
249
+
250
+ except Exception as e:
251
+ history.append((message, f"❌ Error: {str(e)}"))
252
+
253
+ return "", history
254
+
255
+
256
+ def clear_all():
257
+ global vector_store, processed_files
258
+ vector_store = None
259
+ processed_files = []
260
+ return [], "", "", "🗑️ Knowledge base cleared."
261
+
262
+
263
+ # ---------- UI ----------
264
+ CSS = """
265
+ /* Overall theme */
266
+ .gradio-container {
267
+ font-family: 'Inter', sans-serif !important;
268
+ background: linear-gradient(135deg, #0f0c29, #302b63, #24243e) !important;
269
+ min-height: 100vh;
270
+ }
271
+
272
+ /* Header */
273
+ .header-box {
274
+ background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
275
+ border-radius: 16px;
276
+ padding: 24px 32px;
277
+ margin-bottom: 20px;
278
+ text-align: center;
279
+ box-shadow: 0 8px 32px rgba(102, 126, 234, 0.4);
280
+ }
281
+
282
+ /* Panels */
283
+ .panel {
284
+ background: rgba(255,255,255,0.05) !important;
285
+ border: 1px solid rgba(255,255,255,0.1) !important;
286
+ border-radius: 16px !important;
287
+ backdrop-filter: blur(10px) !important;
288
+ }
289
+
290
+ /* Buttons */
291
+ .btn-primary {
292
+ background: linear-gradient(90deg, #667eea, #764ba2) !important;
293
+ border: none !important;
294
+ border-radius: 10px !important;
295
+ color: white !important;
296
+ font-weight: 600 !important;
297
+ transition: all 0.3s ease !important;
298
+ }
299
+
300
+ .btn-primary:hover {
301
+ transform: translateY(-2px) !important;
302
+ box-shadow: 0 8px 20px rgba(102, 126, 234, 0.5) !important;
303
+ }
304
+
305
+ .btn-secondary {
306
+ background: rgba(255,255,255,0.1) !important;
307
+ border: 1px solid rgba(255,255,255,0.2) !important;
308
+ border-radius: 10px !important;
309
+ color: white !important;
310
+ }
311
+
312
+ /* Chatbot */
313
+ .chatbot-container {
314
+ border-radius: 16px !important;
315
+ border: 1px solid rgba(255,255,255,0.1) !important;
316
+ }
317
+
318
+ /* Status boxes */
319
+ .status-box {
320
+ background: rgba(16, 185, 129, 0.1) !important;
321
+ border: 1px solid rgba(16, 185, 129, 0.3) !important;
322
+ border-radius: 10px !important;
323
+ }
324
+
325
+ /* Input box */
326
+ input[type="text"], input[type="password"], textarea {
327
+ background: rgba(255,255,255,0.08) !important;
328
+ border: 1px solid rgba(255,255,255,0.15) !important;
329
+ border-radius: 10px !important;
330
+ color: white !important;
331
+ }
332
+
333
+ /* File upload */
334
+ .upload-area {
335
+ border: 2px dashed rgba(102, 126, 234, 0.5) !important;
336
+ border-radius: 12px !important;
337
+ background: rgba(102, 126, 234, 0.05) !important;
338
+ }
339
+ """
340
+
341
+ with gr.Blocks(
342
+ css=CSS,
343
+ title="RAG Document Assistant",
344
+ theme=gr.themes.Soft(
345
+ primary_hue="violet",
346
+ secondary_hue="purple",
347
+ neutral_hue="slate",
348
+ font=gr.themes.GoogleFont("Inter")
349
+ )
350
+ ) as demo:
351
+
352
+ # ---- Header ----
353
+ gr.HTML("""
354
+ <div class="header-box">
355
+ <h1 style="color:white; font-size:2.2em; margin:0; font-weight:700;">
356
+ 📄 RAG Document Assistant
357
+ </h1>
358
+ <p style="color:rgba(255,255,255,0.85); margin:8px 0 0 0; font-size:1.1em;">
359
+ Upload documents · Ask questions · Get AI-powered answers
360
+ </p>
361
+ </div>
362
+ """)
363
+
364
+ with gr.Row():
365
+ # ---- Left Panel ----
366
+ with gr.Column(scale=1, elem_classes="panel"):
367
+ gr.HTML("<h3 style='color:#a78bfa; margin:0 0 12px 0;'>⚙️ Configuration</h3>")
368
+
369
+ groq_key = gr.Textbox(
370
+ label="🔑 Groq API Key",
371
+ placeholder="Enter your Groq API key...",
372
+ type="password",
373
+ info="Get yours free at console.groq.com"
374
  )
 
 
 
 
375
 
376
+ gr.HTML("<hr style='border-color:rgba(255,255,255,0.1); margin:16px 0;'>")
377
+ gr.HTML("<h3 style='color:#a78bfa; margin:0 0 12px 0;'>📂 Upload Documents</h3>")
378
 
379
+ file_upload = gr.File(
380
+ label="Drop files here",
381
+ file_count="multiple",
382
+ file_types=[".pdf", ".docx", ".xlsx", ".csv", ".txt"],
383
+ elem_classes="upload-area"
384
+ )
 
 
385
 
386
+ with gr.Row():
387
+ process_btn = gr.Button("🚀 Process", variant="primary", elem_classes="btn-primary")
388
+ add_btn = gr.Button("➕ Add More", elem_classes="btn-secondary")
389
 
390
+ process_status = gr.Textbox(
391
+ label="📊 Status",
392
+ lines=6,
393
+ interactive=False,
394
+ elem_classes="status-box"
395
+ )
396
 
397
+ gr.HTML("<hr style='border-color:rgba(255,255,255,0.1); margin:16px 0;'>")
398
+ gr.HTML("<h3 style='color:#a78bfa; margin:0 0 8px 0;'>📚 Knowledge Base</h3>")
399
+
400
+ files_list = gr.Textbox(
401
+ label="Indexed Documents",
402
+ lines=4,
403
+ interactive=False,
404
+ placeholder="No documents loaded yet..."
 
 
 
 
 
 
 
405
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
 
407
+ clear_btn = gr.Button("🗑️ Clear All", variant="stop", elem_classes="btn-secondary")
408
+
409
+ gr.HTML("""
410
+ <div style='margin-top:16px; padding:12px; background:rgba(167,139,250,0.1);
411
+ border-radius:10px; border:1px solid rgba(167,139,250,0.2);'>
412
+ <p style='color:rgba(255,255,255,0.6); font-size:0.8em; margin:0;'>
413
+ <b style='color:#a78bfa;'>Supported:</b> PDF · DOCX · XLSX · CSV · TXT<br>
414
+ <b style='color:#a78bfa;'>Powered by:</b> LangChain · ChromaDB · Groq LLaMA 3.1
415
+ </p>
416
+ </div>
417
+ """)
418
+
419
+ # ---- Right Panel (Chat) ----
420
+ with gr.Column(scale=2, elem_classes="panel"):
421
+ gr.HTML("<h3 style='color:#a78bfa; margin:0 0 12px 0;'>💬 Ask Questions</h3>")
422
+
423
+ chatbot = gr.Chatbot(
424
+ label="",
425
+ height=520,
426
+ show_label=False,
427
+ avatar_images=("👤", "🤖"),
428
+ bubble_full_width=False,
429
+ elem_classes="chatbot-container"
430
+ )
431
 
432
+ with gr.Row():
433
+ msg_input = gr.Textbox(
434
+ placeholder="Ask anything about your documents...",
435
+ label="",
436
+ show_label=False,
437
+ scale=5,
438
+ container=False
439
+ )
440
+ send_btn = gr.Button("Send ➤", variant="primary", scale=1, elem_classes="btn-primary")
441
+
442
+ gr.HTML("""
443
+ <div style='text-align:center; margin-top:8px;'>
444
+ <span style='color:rgba(255,255,255,0.3); font-size:0.75em;'>
445
+ Press Enter or click Send · Powered by Groq LLaMA 3.1 8B
446
+ </span>
447
+ </div>
448
+ """)
449
+
450
+ with gr.Accordion("💡 Example Questions", open=False):
451
+ gr.Examples(
452
+ examples=[
453
+ ["What is this document about?"],
454
+ ["Summarize the key points"],
455
+ ["What are the main conclusions?"],
456
+ ["List all important dates mentioned"],
457
+ ["Who are the key people mentioned?"],
458
+ ],
459
+ inputs=msg_input,
460
+ label=""
461
+ )
462
 
463
+ # ---- Events ----
464
+ process_btn.click(
465
+ fn=process_documents,
466
+ inputs=[file_upload],
467
+ outputs=[process_status, files_list]
468
+ )
469
+
470
+ add_btn.click(
471
+ fn=add_documents,
472
+ inputs=[file_upload],
473
+ outputs=[process_status, files_list]
474
+ )
475
+
476
+ send_btn.click(
477
+ fn=chat,
478
+ inputs=[msg_input, chatbot, groq_key],
479
+ outputs=[msg_input, chatbot]
480
+ )
481
+
482
+ msg_input.submit(
483
+ fn=chat,
484
+ inputs=[msg_input, chatbot, groq_key],
485
+ outputs=[msg_input, chatbot]
486
+ )
487
+
488
+ clear_btn.click(
489
+ fn=clear_all,
490
+ outputs=[chatbot, files_list, process_status, process_status]
491
+ )
492
+
493
+ if __name__ == "__main__":
494
+ demo.launch(server_name="0.0.0.0", server_port=7860)
requirements.txt CHANGED
@@ -1,4 +1,4 @@
1
- streamlit>=1.28.0
2
  langchain>=0.2.0
3
  langchain-community>=0.2.0
4
  langchain-text-splitters>=0.2.0
 
1
+ gradio>=4.0.0
2
  langchain>=0.2.0
3
  langchain-community>=0.2.0
4
  langchain-text-splitters>=0.2.0