irajkoohi commited on
Commit
fef2e14
·
1 Parent(s): 9ff89e1

Remove all files to clean the space

Browse files
Files changed (10) hide show
  1. .DS_Store +0 -0
  2. Agentic_Rag4_dep_space +0 -1
  3. Dockerfile +0 -39
  4. README.md +0 -30
  5. app.py +0 -954
  6. config.yaml +0 -9
  7. helpers_HF.py +0 -123
  8. helpers_LOCAL.py +0 -165
  9. helpers_SHARED.py +0 -407
  10. requirements.txt +0 -17
.DS_Store DELETED
Binary file (6.15 kB)
 
Agentic_Rag4_dep_space DELETED
@@ -1 +0,0 @@
1
- Subproject commit 167f63c9f5f6b9e30d021dc08bbb7d99f67a46a4
 
 
Dockerfile DELETED
@@ -1,39 +0,0 @@
1
- # Use Python 3.12 slim image
2
- FROM python:3.12-slim
3
-
4
- # Set environment variables
5
- ENV PYTHONDONTWRITEBYTECODE=1
6
- ENV PYTHONUNBUFFERED=1
7
- ENV GRADIO_SERVER_NAME=0.0.0.0
8
- ENV GRADIO_SERVER_PORT=7860
9
-
10
- # Set working directory
11
- WORKDIR /app
12
-
13
- # Install system dependencies
14
- RUN apt-get update && apt-get install -y --no-install-recommends \
15
- build-essential \
16
- git \
17
- && rm -rf /var/lib/apt/lists/*
18
-
19
- # Copy requirements first for better caching
20
- COPY requirements.txt .
21
-
22
- # Install Python dependencies
23
- RUN pip install --no-cache-dir --upgrade pip && \
24
- pip install --no-cache-dir -r requirements.txt
25
-
26
- # Copy application code
27
- COPY app.py .
28
- COPY helpers_SHARED.py .
29
- COPY helpers_HF.py .
30
- COPY helpers_LOCAL.py .
31
-
32
- # Create data directories
33
- RUN mkdir -p data/embeddings
34
-
35
- # Expose port
36
- EXPOSE 7860
37
-
38
- # Run the application
39
- CMD ["python", "app.py"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md DELETED
@@ -1,30 +0,0 @@
1
- ---
2
- title: Agentic RAG 4
3
- emoji: 🚀
4
- colorFrom: indigo
5
- colorTo: purple
6
- sdk: gradio
7
- sdk_version: 6.0.2
8
- app_file: app.py
9
- pinned: false
10
- ---
11
- ---
12
-
13
- # 🤖 Agentic RAG 3
14
-
15
- An intelligent RAG (Retrieval-Augmented Generation) agent with document analysis capabilities.
16
-
17
- ## Features
18
- - Upload and process PDF documents
19
- - Intelligent document search using FAISS
20
- - Agent-powered question answering
21
- - Multiple tool support (list, count, search documents)
22
-
23
- ## Usage
24
- 1. Upload your PDF documents
25
- 2. Ask questions about your documents
26
- 3. Get intelligent, context-aware answers
27
-
28
- ## Environment
29
- - Local: Uses Ollama (llama3.2)
30
- - HF Space: Uses Llama-3.2-3B-Instruct API
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py DELETED
@@ -1,954 +0,0 @@
1
- """
2
- uv init
3
- uv venv --python 3.12
4
- source .venv/bin/activate
5
- uv pip install -r requirements.txt
6
- """
7
-
8
- # Note: HuggingFace Spaces reads configuration from the README.md frontmatter, not from a separate YAML file.
9
- # The config.yaml is for your reference/organization, but the actual Space config must remain in README.md.
10
-
11
- # The Space was created with Docker SDK and README.md frontmatter specifies sdk: docker:
12
- # huggingface-cli repo create Agentic_Rag3_dep_space --type space --space_sdk docker
13
-
14
- # Without Docker, we use the Gradio SDK option in README.md frontmatter:
15
- # ---
16
- # sdk: gradio
17
- # sdk_version: "6.0.1"
18
- # python_version: "3.12"
19
- # app_file: app.py
20
- # ---
21
-
22
- # Or:
23
- # huggingface-cli repo create Agentic_Rag3_dep_space --type space --space_sdk gradio
24
-
25
- # AGENT DEPLOYMENT NOTES:
26
- # =====================
27
- # - Local Environment: Uses Ollama (llama3.2) for development
28
- # - HF Space Environment: Uses Llama-3.2-3B-Instruct (cloud API) for production
29
- # - Environment Auto-Detection: Via SPACE_ID environment variable
30
- # - Agent Tools Available: Document listing, counting, RAG search
31
- # - Storage: Temporary (files lost on restart) or persistent (paid plans)
32
- # - UI Features: Tool-powered sample questions, environment indicators
33
- # - Security: Token stored as Space secret (HF_token), not in code
34
- # - Space URL: https://huggingface.co/spaces/irajkoohi/Agentic_Rag3_dep_space
35
-
36
- # A) If you want to run app.py locally:
37
- """
38
- cd /Users/ik/UVcodes/Deployed_Agents_4 && clear && lsof -ti:7860 | xargs kill -9 2>/dev/null; sleep 2 && source .venv/bin/activate && python app.py
39
- """
40
-
41
- # B) If you want to run app.py on Hugging Face Space:
42
- """
43
- https://huggingface.co/spaces/irajkoohi/Agentic_Rag4_dep_space
44
- """
45
-
46
- # Create and Upload RAG Agent to HF Space Agentic_Rag4_dep_space (Docker SDK)
47
- """
48
- # huggingface-cli repo create Agentic_Rag4_dep_space --type space --space_sdk docker 2>&1
49
- Create new token with Write role at: https://huggingface.co/settings/tokens
50
- Add token to Space secrets at: https://huggingface.co/spaces/irajkoohi/Agentic_Rag4_dep_space/settings
51
-
52
- clear
53
- rm -rf Agentic_Rag4_dep_space && git clone https://huggingface.co/spaces/irajkoohi/Agentic_Rag4_dep_space
54
- cd /Users/ik/UVcodes/Deployed_Agents_4/Agentic_Rag4_dep_space && cp ../app.py . && cp ../helpers_SHARED.py . && cp ../helpers_HF.py . && cp ../helpers_LOCAL.py . && cp ../requirements.txt . && cp ../README.md . && cp ../Dockerfile . && cp ../config.yaml .
55
- mkdir -p data/embeddings
56
- git add . && git commit -m "Deploy RAG Agent with Dockerfile to HF space"
57
- git push --force
58
- https://huggingface.co/spaces/irajkoohi/Agentic_Rag4_dep_space
59
- """
60
-
61
- # if you want to upload all files:
62
- """
63
- clear
64
- cd /Users/ik/UVcodes/Deployed_Agents_4/Agentic_Rag4_dep_space
65
- cp ../app.py .
66
- cp ../helpers_SHARED.py .
67
- cp ../helpers_HF.py .
68
- cp ../helpers_LOCAL.py .
69
- cp ../requirements.txt .
70
- # cp ../README.md .
71
- cp ../Dockerfile .
72
- cp ../config.yaml .
73
- git add .
74
- git commit -m "Update all files"
75
- git push
76
- """
77
-
78
- # If you want to delete all files on HF space
79
- """
80
- cd /Users/ik/UVcodes/Deployed_Agents_4
81
- rm -rf Agentic_Rag4_dep_space && git clone https://huggingface.co/spaces/irajkoohi/Agentic_Rag4_dep_space
82
- cd Agentic_Rag4_dep_space && find . -maxdepth 1 -not -name '.git' -not -name '.' -delete
83
- rm -rf data embeddings
84
- git add -A && git commit -m "Remove all files to clean the space"
85
- git push
86
- ls -la && pwd
87
- """
88
-
89
- # If you want to delete a space on HF website
90
- """
91
- 1. Go to: https://huggingface.co/spaces/irajkoohi/Agentic_Rag4_dep_space/settings
92
- 2. Scroll down to "Delete this Space"
93
- 4. Type: irajkoohi/Agentic_Rag4_dep_space
94
- 4. Click "Delete"
95
- """
96
-
97
- # if you want to sync changes of some files (like app.py and helpers_SHARED.py):
98
- """
99
- cp ../app.py . && cp ../helpers_SHARED.py .
100
- git add app.py helpers_SHARED.py
101
- git commit -m "Sync app.py and helpers_SHARED.py with latest changes" && git push
102
- """
103
-
104
- #%%
105
- import os
106
- import shutil
107
- import warnings
108
- from datetime import datetime
109
- from fastapi import FastAPI, UploadFile, File, HTTPException
110
- from pydantic import BaseModel
111
- import gradio as gr
112
-
113
- # Suppress warnings for cleaner output on HF Spaces
114
- warnings.filterwarnings("ignore", category=UserWarning)
115
-
116
- # Fix event loop issues on HF Spaces
117
- if os.getenv("SPACE_ID") is not None:
118
- try:
119
- import nest_asyncio
120
- nest_asyncio.apply()
121
- except ImportError:
122
- pass
123
-
124
- # ============================================================================
125
- # IMPORT FROM HELPER MODULES
126
- # ============================================================================
127
-
128
- from helpers_SHARED import (
129
- # Configuration
130
- CONFIG, IS_HF_SPACE, DATA_DIR, EMBEDDINGS_DIR,
131
- HAS_PERSISTENT_STORAGE, STORAGE_WARNING,
132
- # Memory functions
133
- add_to_memory, get_memory_context, search_memory, clear_memory,
134
- # Utility functions
135
- get_timestamp, create_elapsed_timer, format_progress_bar,
136
- # PDF helpers
137
- get_pdf_list, get_pdf_list_ui, make_pdf_dropdown,
138
- # Vectorstore
139
- build_vectorstore, get_vectorstore, set_vectorstore, embeddings,
140
- # Agent tools
141
- AGENT_TOOLS, list_documents, count_documents, search_documents,
142
- # Sample questions
143
- SAMPLE_Q1, SAMPLE_Q2, SAMPLE_Q3, SAMPLE_Q4, SAMPLE_Q5, SAMPLE_Q6, SAMPLE_Q7,
144
- )
145
- from helpers_SHARED import floating_progress_bar_html
146
-
147
- # Import environment-specific helpers
148
- if IS_HF_SPACE:
149
- from helpers_HF import (
150
- init_hf_llm, hf_generate_chat_response, hf_generate_text_response,
151
- get_hf_client, get_hf_llm_name
152
- )
153
- # Initialize HF LLM (default model from config)
154
- hf_client, LLM_NAME = init_hf_llm(CONFIG["hf_model"] if "hf_model" in CONFIG else None)
155
- ollama_llm = None
156
- agent_executor = None
157
- else:
158
- from helpers_LOCAL import (
159
- init_ollama_llm, ollama_generate_response, run_agent,
160
- create_langchain_agent, get_ollama_llm, get_local_llm_name, get_agent_executor
161
- )
162
- # Initialize Ollama LLM
163
- ollama_llm, LLM_NAME = init_ollama_llm()
164
- hf_client = None
165
-
166
- # Create directories
167
- os.makedirs(DATA_DIR, exist_ok=True)
168
- os.makedirs(EMBEDDINGS_DIR, exist_ok=True)
169
-
170
- # Build initial vectorstore
171
- vs = build_vectorstore()
172
-
173
- # Create agent (local only)
174
- if not IS_HF_SPACE and ollama_llm is not None:
175
- agent_executor = create_langchain_agent()
176
- else:
177
- agent_executor = None
178
-
179
- # Debug: Print initial state
180
- print(f"🐛 DEBUG: Initial vectorstore state: {vs is not None}")
181
- print(f"🐛 DEBUG: IS_HF_SPACE: {IS_HF_SPACE}")
182
- print(f"🐛 DEBUG: DATA_DIR: {DATA_DIR}")
183
- print(f"🐛 DEBUG: EMBEDDINGS_DIR: {EMBEDDINGS_DIR}")
184
- if IS_HF_SPACE:
185
- print(f"🐛 DEBUG: /data exists: {os.path.exists('/data')}")
186
- print(f"🐛 DEBUG: HF token available: {os.getenv('HF_token') is not None}")
187
- print(f"🐛 DEBUG: LLM available: {(hf_client is not None) if IS_HF_SPACE else (ollama_llm is not None)}")
188
-
189
- # ============================================================================
190
- # FASTAPI APP (FastAPI is only used for local runs, not on HuggingFace Spaces)
191
- # ============================================================================
192
-
193
- app = FastAPI(title="RAG Chatbot API")
194
-
195
- class Prompt(BaseModel):
196
- prompt: str
197
-
198
- @app.get("/pdfs")
199
- def list_pdfs():
200
- return {"pdfs": get_pdf_list()}
201
-
202
- @app.post("/upload")
203
- async def upload_pdf(file: UploadFile = File(...)):
204
- if not file.filename or not file.filename.endswith(".pdf"):
205
- raise HTTPException(status_code=400, detail="Only PDFs allowed.")
206
-
207
- filepath = os.path.join(DATA_DIR, file.filename)
208
- with open(filepath, "wb") as f:
209
- f.write(await file.read())
210
-
211
- build_vectorstore(force_rebuild=True)
212
- return {"message": f"Added {file.filename}. Embeddings updated."}
213
-
214
- @app.delete("/delete/{filename}")
215
- def delete_pdf(filename: str):
216
- if filename not in get_pdf_list():
217
- raise HTTPException(status_code=404, detail="PDF not found.")
218
-
219
- filepath = os.path.join(DATA_DIR, filename)
220
- os.remove(filepath)
221
- build_vectorstore(force_rebuild=True)
222
- return {"message": f"Deleted {filename}. Embeddings updated."}
223
-
224
- @app.post("/generate")
225
- def generate_response(prompt: Prompt):
226
- global vs
227
- vs = get_vectorstore()
228
-
229
- if vs is None:
230
- raise HTTPException(status_code=400, detail="No PDFs loaded.")
231
-
232
- # Retrieve relevant docs (limit context size)
233
- retriever = vs.as_retriever(search_kwargs={"k": CONFIG["search_k"]})
234
- docs = retriever.invoke(prompt.prompt)
235
- # Limit to top 3 most relevant chunks
236
- top_docs = docs[:3] if len(docs) > 3 else docs
237
- context = "\n\n".join([doc.page_content for doc in top_docs])
238
-
239
- # Augment prompt
240
- full_prompt = (
241
- "Answer the following question based ONLY on the context provided below.\n"
242
- "If the answer is not present in the context, reply exactly with: 'I don't know.'\n"
243
- "Do NOT make up or guess any information that is not explicitly in the context.\n\n"
244
- f"Context:\n{context}\n\n"
245
- f"Question: {prompt.prompt}\n\n"
246
- "Answer:"
247
- )
248
-
249
- try:
250
- if IS_HF_SPACE and hf_client is not None:
251
- response = hf_generate_text_response(full_prompt, context, hf_client)
252
- return {"response": response}
253
- elif not IS_HF_SPACE and ollama_llm is not None:
254
- print(f"Generating response with Ollama ({LLM_NAME})...")
255
- try:
256
- response = ollama_llm.invoke(full_prompt)
257
- print(f"✓ Success! Response generated.")
258
- return {"response": response}
259
- except Exception as ollama_error:
260
- print(f"❌ Ollama error: {str(ollama_error)}")
261
- return {"response": f"I found relevant information in your documents:\n\n{context[:CONFIG['search_content_limit']]}..."}
262
- else:
263
- return {"response": f"I found relevant information in your documents:\n\n{context[:CONFIG['search_content_limit']]}..."}
264
- except Exception as e:
265
- print(f"LLM failed: {str(e)}")
266
- return {"response": f"I found relevant information in your documents:\n\n{context[:CONFIG['search_content_limit']]}..."}
267
-
268
- @app.get("/refresh")
269
- def refresh_embeddings():
270
- build_vectorstore(force_rebuild=True)
271
- return {"message": "Embeddings refreshed."}
272
-
273
- # ============================================================================
274
- # GRADIO UI FUNCTIONS
275
- # ============================================================================
276
-
277
- def add_pdf(files):
278
- if files is None or len(files) == 0:
279
- return (
280
- make_pdf_dropdown(),
281
- "No files selected.",
282
- "",
283
- "\n".join(get_pdf_list())
284
- )
285
-
286
- start_time = datetime.now()
287
- get_elapsed = create_elapsed_timer(start_time)
288
- results = []
289
- total_files = len(files)
290
- upload_log = []
291
-
292
- upload_log.append(f"[{get_timestamp()}] Starting upload process for {total_files} file(s)")
293
-
294
- for i, file_obj in enumerate(files, 1):
295
- filename = os.path.basename(file_obj.name)
296
-
297
- progress_percent = int((i * 2 - 1) / (total_files * 2) * 100)
298
- status_msg = f"📤 Uploading {i}/{total_files}: {filename}..."
299
- progress_display = format_progress_bar(get_elapsed(), progress_percent, status_msg)
300
-
301
- upload_log.append(f"[{get_timestamp()}] Uploading file {i}: {filename}")
302
-
303
- # Show current embedded files (before upload)
304
- yield (
305
- make_pdf_dropdown(),
306
- "\n".join(results) if results else "Starting upload...",
307
- progress_display,
308
- "\n".join(get_pdf_list())
309
- )
310
-
311
- try:
312
- dest_path = os.path.join(DATA_DIR, filename)
313
- shutil.copy2(file_obj.name, dest_path)
314
- results.append(f"✓ {filename} uploaded")
315
-
316
- upload_log.append(f"[{get_timestamp()}] Uploading file {i} completed")
317
-
318
- progress_percent = int(((i * 2) - 1) / (total_files * 2) * 100)
319
- status_msg = f"🧠 Creating embeddings for {filename}..."
320
- progress_display = format_progress_bar(get_elapsed(), progress_percent, status_msg)
321
- upload_log.append(f"[{get_timestamp()}] Embedding file {i}: {filename}")
322
-
323
- yield (
324
- make_pdf_dropdown(),
325
- "\n".join(results),
326
- progress_display,
327
- "\n".join(get_pdf_list())
328
- )
329
-
330
- try:
331
- build_vectorstore(force_rebuild=True)
332
- results[-1] = f"✅ {filename} (uploaded & embedded)"
333
- upload_log.append(f"[{get_timestamp()}] Embedding file {i} completed")
334
- upload_log.append("")
335
- # Show progress bar after embedding completes
336
- progress_percent = int((i * 2) / (total_files * 2) * 100)
337
- status_msg = f"✅ Embedded {i}/{total_files}: {filename}"
338
- progress_display = format_progress_bar(get_elapsed(), progress_percent, status_msg)
339
- # Update embedded files to show the new file
340
- yield (
341
- make_pdf_dropdown(),
342
- "\n".join(results),
343
- progress_display,
344
- "\n".join(get_pdf_list())
345
- )
346
- except Exception as embed_error:
347
- results[-1] = f"⚠️ {filename} (uploaded, embedding error: {str(embed_error)})"
348
- upload_log.append(f"[{get_timestamp()}] Embedding file {i} failed")
349
- upload_log.append("")
350
-
351
- completed_progress = int((i * 2) / (total_files * 2) * 100)
352
- status_msg = f"⚠️ File {i}/{total_files} completed with error: {filename}"
353
- progress_display = format_progress_bar(get_elapsed(), completed_progress, status_msg)
354
-
355
- yield (
356
- make_pdf_dropdown(),
357
- "\n".join(results),
358
- progress_display,
359
- "\n".join(get_pdf_list())
360
- )
361
- except Exception as e:
362
- results.append(f"❌ {filename}: {str(e)}")
363
- upload_log.append(f"[{get_timestamp()}] Uploading file {i} failed")
364
-
365
- final_message = "\n".join(results)
366
- final_progress = format_progress_bar(get_elapsed(), 100, f"🎉 All done! Processed {len(files)} file(s) successfully")
367
- upload_log.append(f"[{get_timestamp()}] All {len(files)} file(s) completed")
368
-
369
- # Only show fully embedded files in the Available Embedded Files window
370
- # Reset the progress bar to its original empty state after completion (like delete)
371
- yield (
372
- make_pdf_dropdown(),
373
- final_message,
374
- "",
375
- "\n".join(get_pdf_list())
376
- )
377
-
378
- def delete_pdf_ui(selected_pdf):
379
- import time
380
- if not selected_pdf:
381
- # Hide overlay if nothing to delete
382
- yield make_pdf_dropdown(), "\n".join(get_pdf_list()), ""
383
- return
384
- # Show progress bar immediately on click
385
- bar = format_progress_bar("", 0, "Preparing to delete files...")
386
- yield make_pdf_dropdown(), "\n".join(get_pdf_list()), bar
387
-
388
- # Support both single and multiple selection
389
- if isinstance(selected_pdf, str):
390
- selected_files = [selected_pdf]
391
- else:
392
- selected_files = list(selected_pdf)
393
-
394
- total_files = len(selected_files)
395
- for idx, file_name in enumerate(selected_files, 1):
396
- file_path = os.path.join(DATA_DIR, file_name)
397
- # Remove file and all leftovers (e.g., embeddings) before advancing progress
398
- deleted = False
399
- leftovers_removed = False
400
- # Remove file
401
- if os.path.exists(file_path):
402
- try:
403
- os.remove(file_path)
404
- deleted = True
405
- except Exception:
406
- deleted = False
407
- # Remove leftovers (add your per-file embedding removal logic here if needed)
408
- # Example: remove embedding file if it exists (customize as needed)
409
- embedding_path = os.path.join(EMBEDDINGS_DIR, file_name + ".embedding")
410
- if os.path.exists(embedding_path):
411
- try:
412
- os.remove(embedding_path)
413
- leftovers_removed = True
414
- except Exception:
415
- leftovers_removed = False
416
- else:
417
- leftovers_removed = True # No leftovers to remove
418
- # Only advance progress bar after both file and leftovers are deleted
419
- if deleted and leftovers_removed:
420
- build_vectorstore(force_rebuild=True)
421
- percent = int(idx / total_files * 100) if total_files else 100
422
- bar = format_progress_bar("", percent, f"Deleted {idx}/{total_files}: {file_name}")
423
- yield make_pdf_dropdown(), "\n".join(get_pdf_list()), bar
424
- else:
425
- bar = format_progress_bar("", int(idx / total_files * 100) if total_files else 100, f"⚠️ Error deleting {file_name}")
426
- yield make_pdf_dropdown(), "\n".join(get_pdf_list()), bar
427
- time.sleep(0.2)
428
- # Clear progress bar after all deletions
429
- yield make_pdf_dropdown(), "\n".join(get_pdf_list()), ""
430
-
431
- def toggle_delete_all_btn():
432
- # Check if there is at least one file in Available Embedded Files
433
- files = get_pdf_list()
434
- return gr.update(interactive=bool(files))
435
-
436
- def delete_all_files():
437
- import time
438
- all_files = get_pdf_list()
439
- if not all_files:
440
- bar = format_progress_bar("", 0, "No files to delete.")
441
- yield make_pdf_dropdown(), "\n".join(get_pdf_list()), bar
442
- return
443
- bar = format_progress_bar("", 0, "Preparing to delete all files...")
444
- yield make_pdf_dropdown(), "\n".join(get_pdf_list()), bar
445
- total_files = len(all_files)
446
- for idx, file_name in enumerate(all_files, 1):
447
- file_path = os.path.join(DATA_DIR, file_name)
448
- deleted = False
449
- leftovers_removed = False
450
- if os.path.exists(file_path):
451
- try:
452
- os.remove(file_path)
453
- deleted = True
454
- except Exception:
455
- deleted = False
456
- embedding_path = os.path.join(EMBEDDINGS_DIR, file_name + ".embedding")
457
- if os.path.exists(embedding_path):
458
- try:
459
- os.remove(embedding_path)
460
- leftovers_removed = True
461
- except Exception:
462
- leftovers_removed = False
463
- else:
464
- leftovers_removed = True
465
- if deleted and leftovers_removed:
466
- build_vectorstore(force_rebuild=True)
467
- percent = int(idx / total_files * 100) if total_files else 100
468
- bar = format_progress_bar("", percent, f"Deleted {idx}/{total_files}: {file_name}")
469
- yield make_pdf_dropdown(), "\n".join(get_pdf_list()), bar
470
- else:
471
- bar = format_progress_bar("", int(idx / total_files * 100) if total_files else 100, f"⚠️ Error deleting {file_name}")
472
- yield make_pdf_dropdown(), "\n".join(get_pdf_list()), bar
473
- time.sleep(0.2)
474
- yield make_pdf_dropdown(), "\n".join(get_pdf_list()), ""
475
-
476
- def show_delete_all_warning():
477
- return (
478
- gr.Markdown("**⚠️ Are you sure you want to delete ALL files? This cannot be undone. Click 'Confirm Delete All' to proceed.**", visible=True),
479
- gr.update(interactive=True, visible=True)
480
- )
481
-
482
- def hide_delete_all_warning():
483
- return (
484
- gr.Markdown(visible=False),
485
- gr.update(interactive=False, visible=False)
486
- )
487
- def analyze_query_and_use_tools(query: str) -> str:
488
- """Analyze query and use appropriate tools to gather information."""
489
- query_lower = query.lower()
490
- results = []
491
-
492
- # Check for memory-related queries first
493
- memory_keywords = ["remember", "earlier", "before", "previous", "last time", "we discussed",
494
- "you said", "i asked", "conversation", "history", "recall", "what did we"]
495
- if any(word in query_lower for word in memory_keywords):
496
- print(f"🧠 Memory query detected, fetching conversation history...")
497
- memory_result = get_memory_context(last_n=10)
498
- if memory_result and "No previous conversation" not in memory_result:
499
- results.append(f"📝 **Conversation History:**\n{memory_result}")
500
- search_result = search_memory(query)
501
- if search_result and "No conversation history" not in search_result:
502
- results.append(f"🔍 **Relevant Past Discussions:**\n{search_result}")
503
-
504
- if results:
505
- return "\n\n".join(results)
506
-
507
- # Try using LangGraph agent (local only)
508
- if not IS_HF_SPACE and agent_executor is not None:
509
- agent_result = run_agent(query)
510
- if agent_result:
511
- return agent_result
512
-
513
- # Fallback: Manual tool routing
514
- try:
515
- if any(word in query_lower for word in ["what documents", "list documents", "available documents", "what files", "documents do i have"]):
516
- results.append(list_documents.invoke({}))
517
-
518
- if any(word in query_lower for word in ["how many", "count", "number of documents"]):
519
- results.append(count_documents.invoke({}))
520
-
521
- results.append(search_documents.invoke({"query": query}))
522
-
523
- return "\n\n".join(results) if results else "No relevant information found."
524
-
525
- except Exception as e:
526
- return f"Error analyzing query: {str(e)}"
527
-
528
- def chat_response(message, history):
529
- """Agent-enhanced chat response function with visual progress tracking."""
530
- global vs
531
- if not message:
532
- return history, "", "💬 Ready for your question"
533
-
534
- start_time = datetime.now()
535
- get_elapsed = create_elapsed_timer(start_time)
536
-
537
- if not isinstance(history, list):
538
- history = []
539
-
540
- history.append({"role": "user", "content": str(message)})
541
- add_to_memory("user", message)
542
-
543
- try:
544
- yield (history, "", format_progress_bar(get_elapsed(), 33, "🔍 Analyzing your question...", bar_length=15))
545
- print(f"🤖 Agent analyzing query: {message}")
546
-
547
- try:
548
- pdf_files = get_pdf_list()
549
- print(f"🐛 DEBUG: PDF files available: {len(pdf_files)} - {pdf_files}")
550
- print(f"🐛 DEBUG: Global vectorstore state: {get_vectorstore() is not None}")
551
- except Exception as debug_error:
552
- print(f"🐛 DEBUG ERROR: {str(debug_error)}")
553
-
554
- try:
555
- tool_results = analyze_query_and_use_tools(message)
556
- print(f"🔧 Tool results: {tool_results[:100]}...")
557
- except Exception as tool_error:
558
- error_msg = f"❌ Tool execution failed: {str(tool_error)}"
559
- print(error_msg)
560
- history.append({"role": "assistant", "content": error_msg})
561
- yield (history, "", f"{get_elapsed()} | [100%] ❌ Error during tool execution")
562
- return
563
-
564
- yield (history, "", format_progress_bar(get_elapsed(), 66, "🧠 Generating intelligent response...", bar_length=15))
565
-
566
- try:
567
- memory_context = get_memory_context(last_n=5)
568
- llm_prompt = f"""
569
- You are a helpful assistant with memory of past conversations. Answer the question based on the document excerpts and conversation history.
570
-
571
- Recent Conversation History:
572
- {memory_context}
573
-
574
- Document excerpts:
575
- {tool_results}
576
-
577
- Question: {message}
578
-
579
- Answer concisely and accurately. If the user refers to previous discussions, use the conversation history to provide context:
580
- """
581
-
582
- if IS_HF_SPACE and hf_client is not None:
583
- result = hf_generate_chat_response(llm_prompt, hf_client)
584
- if result is None:
585
- result = tool_results
586
- elif not IS_HF_SPACE and ollama_llm is not None:
587
- result = ollama_generate_response(llm_prompt, ollama_llm)
588
- if result is None:
589
- result = tool_results
590
- else:
591
- result = tool_results
592
- print("ℹ️ No LLM available, returning tool results")
593
- except Exception as llm_error:
594
- print(f"❌ LLM processing error: {str(llm_error)}")
595
- result = tool_results
596
-
597
- result_str = str(result.content) if hasattr(result, 'content') else str(result)
598
- history.append({"role": "assistant", "content": result_str})
599
- add_to_memory("assistant", result_str)
600
-
601
- yield (history, "", format_progress_bar(get_elapsed(), 100, "✅ Response generated successfully!", bar_length=15))
602
- # Reset AI Processing Progress to original state
603
- yield (history, "", "💬 Ready for your question")
604
-
605
- except Exception as e:
606
- error_msg = f"🚫 System error: {str(e)}\n\nPlease try again or upload your documents again."
607
- print(f"💥 CRITICAL ERROR: {str(e)}")
608
- import traceback
609
- traceback.print_exc()
610
- history.append({"role": "assistant", "content": error_msg})
611
- yield (history, "", f"{get_elapsed()} | [100%] ❌ System error occurred")
612
-
613
-
614
- def refresh_embeddings_ui():
615
- """Refresh embeddings directly"""
616
- try:
617
- build_vectorstore(force_rebuild=True)
618
- return make_pdf_dropdown(), "Embeddings refreshed."
619
- except Exception as e:
620
- return make_pdf_dropdown(), f"Error refreshing embeddings: {str(e)}"
621
-
622
- def clear_chat_and_memory():
623
- """Clear chat history and conversation memory."""
624
- clear_memory()
625
- return [], "", "💬 Chat and memory cleared. Ready for your question"
626
-
627
- # ============================================================================
628
- # GRADIO UI
629
- # ============================================================================
630
-
631
- ENV_NAME = "🌐 HuggingFace Space" if IS_HF_SPACE else "💻 Local Environment"
632
- ENV_COLOR = "#FF6B6B" if IS_HF_SPACE else "#4ECDC4"
633
-
634
- with gr.Blocks(title="RAG Agent Chatbot") as demo:
635
- gr.Markdown(f"# 🤖 RAG Agent - AI Assistant with Tools\nUpload PDFs and interact with an intelligent agent that can search, analyze, and answer questions about your documents.")
636
- if not IS_HF_SPACE:
637
- from helpers_LOCAL import get_installed_llms, init_ollama_llm, create_langchain_agent
638
- llm_choices = get_installed_llms()
639
- if llm_choices:
640
- llm_dropdown = gr.Dropdown(
641
- label="Select Local LLM",
642
- choices=llm_choices,
643
- value=LLM_NAME if LLM_NAME in llm_choices else (llm_choices[0] if llm_choices else None),
644
- interactive=True,
645
- visible=True
646
- )
647
- current_llm_display = gr.Markdown(f"**Current LLM:** {LLM_NAME if LLM_NAME else ''}", elem_id="current-llm-display", visible=True)
648
- top_banner = gr.Markdown(
649
- f"<div style='background-color: {ENV_COLOR}; padding: 10px; border-radius: 5px; text-align: center; color: white; font-weight: bold;'>Running on: {ENV_NAME} | LLM: <span id='llm-name'>{LLM_NAME if LLM_NAME else 'None'}</span> | Agent: ✅ Active</div>",
650
- elem_id="top-llm-banner"
651
- )
652
-
653
- def update_llm(selected_label):
654
- global ollama_llm, LLM_NAME, agent_executor
655
- if selected_label:
656
- try:
657
- ollama_llm, LLM_NAME = init_ollama_llm(selected_label)
658
- agent_executor = create_langchain_agent()
659
- banner_html = f"<div style='background-color: {ENV_COLOR}; padding: 10px; border-radius: 5px; text-align: center; color: white; font-weight: bold;'>Running on: {ENV_NAME} | LLM: <span id='llm-name'>{selected_label}</span> | Agent: ✅ Active</div>"
660
- return (
661
- gr.Markdown(f"**Current LLM:** {selected_label}", elem_id="current-llm-display"),
662
- banner_html
663
- )
664
- except Exception as e:
665
- ollama_llm = None
666
- LLM_NAME = None
667
- agent_executor = None
668
- banner_html = f"<div style='background-color: {ENV_COLOR}; padding: 10px; border-radius: 5px; text-align: center; color: white; font-weight: bold;'>Running on: {ENV_NAME} | LLM: <span id='llm-name'>None</span> | Agent: ❌ Inactive</div>"
669
- return (
670
- gr.Markdown(f"**Current LLM:** (Error initializing {selected_label})", elem_id="current-llm-display"),
671
- banner_html
672
- )
673
- banner_html = f"<div style='background-color: {ENV_COLOR}; padding: 10px; border-radius: 5px; text-align: center; color: white; font-weight: bold;'>Running on: {ENV_NAME} | LLM: <span id='llm-name'>None</span> | Agent: ❌ Inactive</div>"
674
- return gr.Markdown("", elem_id="current-llm-display"), banner_html
675
-
676
- llm_dropdown.change(
677
- fn=lambda label: update_llm(label),
678
- inputs=[llm_dropdown],
679
- outputs=[current_llm_display, top_banner]
680
- )
681
- else:
682
- gr.Markdown(
683
- "<div style='background-color: #ffcccc; padding: 10px; border-radius: 5px; text-align: center; color: #b30000; font-weight: bold;'>&#9888; <b>No local LLMs are installed.</b> Please install an Ollama model to enable LLM selection and chat capabilities.</div>"
684
- )
685
- llm_dropdown = gr.Dropdown(
686
- label="Select Local LLM",
687
- choices=[],
688
- value=None,
689
- interactive=False,
690
- visible=True
691
- )
692
- current_llm_display = gr.Markdown(f"**Current LLM:** None", elem_id="current-llm-display", visible=True)
693
- gr.Markdown(f"<div style='background-color: {ENV_COLOR}; padding: 10px; border-radius: 5px; text-align: center; color: white; font-weight: bold;'>Running on: {ENV_NAME} | LLM: <span id='llm-name'>None</span> | Agent: ❌ Inactive</div>")
694
- else:
695
- # --- Hugging Face Space: dynamic LLM selection ---
696
- # Static list of free, popular LLMs on HF Inference API (can be expanded)
697
- hf_llm_choices = [
698
- "meta-llama/Meta-Llama-3-8B-Instruct",
699
- "mistralai/Mistral-7B-Instruct-v0.2",
700
- "google/gemma-7b-it",
701
- "HuggingFaceH4/zephyr-7b-beta",
702
- "Qwen/Qwen1.5-7B-Chat",
703
- "tiiuae/falcon-7b-instruct"
704
- ]
705
- llm_dropdown = gr.Dropdown(
706
- label="Select HF LLM",
707
- choices=hf_llm_choices,
708
- value=LLM_NAME if LLM_NAME in hf_llm_choices else hf_llm_choices[0],
709
- interactive=True,
710
- visible=True
711
- )
712
- current_llm_display = gr.Markdown(f"**Current LLM:** {LLM_NAME if LLM_NAME else hf_llm_choices[0]}", elem_id="current-llm-display", visible=True)
713
- top_banner = gr.Markdown(
714
- f"<div style='background-color: {ENV_COLOR}; padding: 10px; border-radius: 5px; text-align: center; color: white; font-weight: bold;'>Running on: {ENV_NAME} | LLM: <span id='llm-name'>{LLM_NAME if LLM_NAME else hf_llm_choices[0]}</span> | Agent: ✅ Active</div>",
715
- elem_id="top-llm-banner"
716
- )
717
-
718
- def update_hf_llm(selected_label):
719
- global hf_client, LLM_NAME
720
- from helpers_HF import init_hf_llm
721
- if selected_label:
722
- try:
723
- hf_client, LLM_NAME = init_hf_llm(selected_label)
724
- banner_html = f"<div style='background-color: {ENV_COLOR}; padding: 10px; border-radius: 5px; text-align: center; color: white; font-weight: bold;'>Running on: {ENV_NAME} | LLM: <span id='llm-name'>{selected_label}</span> | Agent: ✅ Active</div>"
725
- return (
726
- gr.Markdown(f"**Current LLM:** {selected_label}", elem_id="current-llm-display"),
727
- banner_html
728
- )
729
- except Exception as e:
730
- hf_client = None
731
- LLM_NAME = None
732
- banner_html = f"<div style='background-color: {ENV_COLOR}; padding: 10px; border-radius: 5px; text-align: center; color: white; font-weight: bold;'>Running on: {ENV_NAME} | LLM: <span id='llm-name'>None</span> | Agent: ❌ Inactive</div>"
733
- return (
734
- gr.Markdown(f"**Current LLM:** (Error initializing {selected_label})", elem_id="current-llm-display"),
735
- banner_html
736
- )
737
- banner_html = f"<div style='background-color: {ENV_COLOR}; padding: 10px; border-radius: 5px; text-align: center; color: white; font-weight: bold;'>Running on: {ENV_NAME} | LLM: <span id='llm-name'>None</span> | Agent: ❌ Inactive</div>"
738
- return gr.Markdown("", elem_id="current-llm-display"), banner_html
739
-
740
- llm_dropdown.change(
741
- fn=lambda label: update_hf_llm(label),
742
- inputs=[llm_dropdown],
743
- outputs=[current_llm_display, top_banner]
744
- )
745
-
746
- with gr.Row():
747
- with gr.Column(scale=1):
748
- gr.Markdown("### 📁 File Management")
749
- if IS_HF_SPACE and STORAGE_WARNING:
750
- gr.Markdown(f"**Storage Status:** {STORAGE_WARNING}")
751
- file_upload = gr.File(
752
- label="Upload Files (Multiple files supported)",
753
- file_types=[".pdf"],
754
- file_count="multiple"
755
- )
756
-
757
- upload_status = gr.Textbox(
758
- label="Upload Status",
759
- value="",
760
- interactive=False,
761
- lines=8,
762
- max_lines=8,
763
- autoscroll=True
764
- )
765
- with gr.Row():
766
- progress_bar = gr.Textbox(
767
- label="Uploading Progress",
768
- value="",
769
- interactive=False,
770
- lines=1,
771
- max_lines=1,
772
- autoscroll=True
773
- )
774
- delete_progress_bar = gr.Textbox(
775
- label="Deleting Progress",
776
- value="",
777
- interactive=False,
778
- lines=1,
779
- max_lines=1,
780
- autoscroll=True
781
- )
782
- embedded_files = gr.Textbox(
783
- label="Available Embedded Files",
784
- value="\n".join(get_pdf_list()),
785
- interactive=False,
786
- lines=8,
787
- max_lines=8,
788
- autoscroll=True
789
- )
790
-
791
- with gr.Row():
792
- pdf_dropdown = gr.Dropdown(
793
- label="Select Files to Delete",
794
- choices=get_pdf_list_ui(),
795
- interactive=True,
796
- allow_custom_value=False,
797
- multiselect=True
798
- )
799
- delete_btn = gr.Button("🗑️ Delete Files", variant="stop", interactive=False)
800
- delete_all_btn = gr.Button("🗑️ Delete All", variant="stop", interactive=bool(get_pdf_list()))
801
- delete_all_warning = gr.Markdown(visible=False)
802
- confirm_delete_all_btn = gr.Button("Confirm Delete All", variant="stop", interactive=True, visible=False)
803
- delete_progress_overlay = gr.HTML(floating_progress_bar_html())
804
-
805
- with gr.Column(scale=4):
806
- gr.Markdown("### 🤖 AI Agent Chat")
807
- gr.Markdown("**Agent Capabilities:** Search documents, list files, count documents, intelligent reasoning")
808
- chatbot = gr.Chatbot(height=CONFIG["chatbot_height"], layout="bubble")
809
-
810
- if IS_HF_SPACE and not HAS_PERSISTENT_STORAGE:
811
- gr.Markdown("⚠️ **Storage Notice:** Files are temporary and will be lost when Space restarts. To enable persistent storage, upgrade to a paid plan in Settings → Hardware.")
812
- gr.Markdown("**🛠️ Agent Commands - Try these tool-powered queries:**")
813
- with gr.Row():
814
- sample1 = gr.Button(f"📋 {SAMPLE_Q1}", size="sm")
815
- sample2 = gr.Button(f"🔍 {SAMPLE_Q2}", size="sm")
816
- sample3 = gr.Button(f"📊 {SAMPLE_Q3}", size="sm")
817
- with gr.Row():
818
- sample4 = gr.Button(f"🧠 {SAMPLE_Q4}", size="sm")
819
- sample5 = gr.Button(f"🍳 {SAMPLE_Q5}", size="sm")
820
- sample6 = gr.Button(f"🧠 {SAMPLE_Q6}", size="sm")
821
- sample7 = gr.Button(f"📝 {SAMPLE_Q7}", size="sm")
822
-
823
- msg_input = gr.Textbox(
824
- placeholder="Ask a question about your PDFs...",
825
- label="Ask about your PDFs",
826
- show_label=False
827
- )
828
-
829
- ai_status = gr.Textbox(
830
- label="📊 AI Processing Progress",
831
- value="💬 Ready for your question",
832
- interactive=False,
833
- placeholder="AI processing status with progress tracking..."
834
- )
835
-
836
- with gr.Row():
837
- submit_btn = gr.Button("Send", variant="primary", scale=1, interactive=False)
838
- clear_btn = gr.Button("Clear", scale=1, interactive=False)
839
-
840
- # Event handlers
841
- file_upload.upload(
842
- fn=add_pdf,
843
- inputs=[file_upload],
844
- outputs=[pdf_dropdown, upload_status, progress_bar, embedded_files]
845
- )
846
- delete_btn.click(
847
- fn=delete_pdf_ui,
848
- inputs=[pdf_dropdown],
849
- outputs=[pdf_dropdown, embedded_files, delete_progress_bar]
850
- )
851
- delete_all_btn.click(
852
- fn=show_delete_all_warning,
853
- inputs=[],
854
- outputs=[delete_all_warning, confirm_delete_all_btn]
855
- )
856
- # Update Delete All button state when files change
857
- demo.load(fn=toggle_delete_all_btn, outputs=[delete_all_btn])
858
- pdf_dropdown.change(fn=toggle_delete_all_btn, outputs=[delete_all_btn])
859
- embedded_files.change(fn=toggle_delete_all_btn, outputs=[delete_all_btn])
860
- confirm_delete_all_btn.click(
861
- fn=delete_all_files,
862
- inputs=[],
863
- outputs=[pdf_dropdown, embedded_files, delete_progress_bar]
864
- ).then(
865
- fn=hide_delete_all_warning,
866
- inputs=[],
867
- outputs=[delete_all_warning, confirm_delete_all_btn]
868
- )
869
- # Enable/disable delete button based on selection
870
- def toggle_delete_btn(selected):
871
- return gr.update(interactive=bool(selected))
872
- pdf_dropdown.change(
873
- fn=toggle_delete_btn,
874
- inputs=[pdf_dropdown],
875
- outputs=[delete_btn]
876
- )
877
- demo.load(fn=lambda: "\n".join(get_pdf_list()), outputs=[embedded_files])
878
- # Ensure embedded_files is updated on app start
879
- demo.load(fn=lambda: "\n".join(get_pdf_list()), outputs=[embedded_files])
880
-
881
-
882
- # Sample question handlers
883
- sample_buttons = [sample1, sample2, sample3, sample4, sample5, sample6, sample7]
884
- sample_questions = [SAMPLE_Q1, SAMPLE_Q2, SAMPLE_Q3, SAMPLE_Q4, SAMPLE_Q5, SAMPLE_Q6, SAMPLE_Q7]
885
- for btn, question in zip(sample_buttons, sample_questions):
886
- btn.click(fn=lambda q=question: q, outputs=[msg_input])
887
-
888
- msg_input.submit(
889
- fn=chat_response,
890
- inputs=[msg_input, chatbot],
891
- outputs=[chatbot, msg_input, ai_status]
892
- )
893
- submit_btn.click(
894
- fn=chat_response,
895
- inputs=[msg_input, chatbot],
896
- outputs=[chatbot, msg_input, ai_status]
897
- )
898
- # Enable/disable send button based on input
899
- def toggle_send_btn(text):
900
- return gr.update(interactive=bool(text and text.strip()))
901
- msg_input.change(
902
- fn=toggle_send_btn,
903
- inputs=[msg_input],
904
- outputs=[submit_btn]
905
- )
906
-
907
- clear_btn.click(
908
- fn=clear_chat_and_memory,
909
- outputs=[chatbot, msg_input, ai_status]
910
- )
911
- # Enable/disable clear button based on input or chat
912
- def toggle_clear_btn(text, chat):
913
- return gr.update(interactive=bool((text and text.strip()) or (chat and len(chat) > 0)))
914
- msg_input.change(
915
- fn=lambda text: toggle_clear_btn(text, chatbot.value if hasattr(chatbot, 'value') else []),
916
- inputs=[msg_input],
917
- outputs=[clear_btn]
918
- )
919
- chatbot.change(
920
- fn=lambda chat: toggle_clear_btn(msg_input.value if hasattr(msg_input, 'value') else '', chat),
921
- inputs=[chatbot],
922
- outputs=[clear_btn]
923
- )
924
-
925
- demo.load(fn=make_pdf_dropdown, outputs=[pdf_dropdown])
926
-
927
- # ============================================================================
928
- # LAUNCH application
929
- # ============================================================================
930
-
931
- if IS_HF_SPACE:
932
- try:
933
- demo.launch(
934
- server_name=CONFIG["server_host"],
935
- server_port=CONFIG["server_port"],
936
- share=True,
937
- show_error=True,
938
- quiet=False
939
- )
940
- except Exception as launch_error:
941
- print(f"Launch error: {launch_error}")
942
- demo.launch(server_name=CONFIG["server_host"], server_port=CONFIG["server_port"])
943
- else:
944
- app_with_gradio = gr.mount_gradio_app(app, demo, path="/")
945
-
946
- if __name__ == "__main__":
947
- import uvicorn
948
- import webbrowser
949
- from threading import Timer
950
-
951
- Timer(3, lambda: webbrowser.open(f"http://127.0.0.1:{CONFIG['server_port']}")).start()
952
-
953
- print("Starting server... Browser will open automatically in 3 seconds.")
954
- uvicorn.run(app_with_gradio, host=CONFIG["server_host"], port=CONFIG["server_port"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
config.yaml DELETED
@@ -1,9 +0,0 @@
1
- title: Agentic RAG 3
2
- emoji: 🤖
3
- colorFrom: blue
4
- colorTo: purple
5
- sdk: docker
6
- app_file: app.py
7
- pinned: false
8
- license: mit
9
- short_description: RAG Agent with document analysis
 
 
 
 
 
 
 
 
 
 
helpers_HF.py DELETED
@@ -1,123 +0,0 @@
1
- """
2
- HuggingFace Space specific helpers for LLM operations.
3
- Contains: HF InferenceClient initialization and response generation.
4
- """
5
-
6
- import os
7
- from typing import Optional
8
- from huggingface_hub import InferenceClient
9
-
10
- from helpers_SHARED import CONFIG, IS_HF_SPACE
11
-
12
- # ============================================================================
13
- # HUGGINGFACE LLM INITIALIZATION
14
- # ============================================================================
15
-
16
- hf_client = None
17
- LLM_NAME = None
18
-
19
- def init_hf_llm(model_name=None):
20
- """Initialize HuggingFace InferenceClient for cloud LLM. Optionally specify model_name."""
21
- global hf_client, LLM_NAME
22
-
23
- if not IS_HF_SPACE:
24
- print("ℹ️ Not running on HF Space, skipping HF client initialization")
25
- return None, None
26
-
27
- # Use provided model_name or fallback to config
28
- if model_name is not None:
29
- LLM_NAME = model_name.split("/")[-1]
30
- # Patch CONFIG for downstream use
31
- CONFIG["hf_model"] = model_name
32
- else:
33
- LLM_NAME = CONFIG["hf_model"].split("/")[-1]
34
- try:
35
- hf_client = InferenceClient(token=os.getenv("HF_token"))
36
- print(f"✓ HuggingFace InferenceClient initialized successfully for {LLM_NAME}")
37
- return hf_client, LLM_NAME
38
- except Exception as e:
39
- print(f"✗ Warning: HuggingFace InferenceClient not available: {e}")
40
- hf_client = None
41
- return None, LLM_NAME
42
-
43
- # ============================================================================
44
- # HUGGINGFACE LLM RESPONSE GENERATION
45
- # ============================================================================
46
-
47
- def hf_generate_chat_response(prompt: str, hf_client_instance=None) -> Optional[str]:
48
- """Generate a chat response using HuggingFace InferenceClient.
49
-
50
- Args:
51
- prompt: The prompt to send to the model
52
- hf_client_instance: Optional HF client instance, uses global if not provided
53
-
54
- Returns:
55
- Generated response string or None if failed
56
- """
57
- client = hf_client_instance or hf_client
58
-
59
- if client is None:
60
- print("❌ HF client not available")
61
- return None
62
-
63
- try:
64
- print(f"🧠 Generating response with {LLM_NAME}...")
65
- response = client.chat_completion(
66
- messages=[{"role": "user", "content": prompt}],
67
- model=CONFIG["hf_model"],
68
- max_tokens=CONFIG["max_tokens"],
69
- temperature=CONFIG["temperature"]
70
- )
71
- result = response.choices[0].message.content
72
- if result:
73
- result = result.strip()
74
- print(f"✓ LLM response: {result[:100]}...")
75
- return result
76
- else:
77
- print(f"⚠️ Empty LLM response")
78
- return None
79
- except Exception as hf_error:
80
- print(f"❌ HF chat_completion error: {type(hf_error).__name__}: {str(hf_error)}")
81
- return None
82
-
83
- def hf_generate_text_response(prompt: str, context: str, hf_client_instance=None) -> str:
84
- """Generate a text response using HuggingFace text_generation API.
85
-
86
- Used as fallback for simpler generation tasks.
87
-
88
- Args:
89
- prompt: The full prompt to send
90
- context: Document context for fallback response
91
- hf_client_instance: Optional HF client instance
92
-
93
- Returns:
94
- Generated response string
95
- """
96
- client = hf_client_instance or hf_client
97
-
98
- if client is None:
99
- print("❌ HF client not available")
100
- return f"I found relevant information in your documents:\n\n{context[:CONFIG['search_content_limit']]}..."
101
-
102
- try:
103
- print(f"Generating response with {LLM_NAME}...")
104
- response = client.text_generation(
105
- prompt,
106
- model=CONFIG["hf_model"],
107
- max_new_tokens=CONFIG["max_tokens"],
108
- temperature=CONFIG["temperature_fallback"],
109
- return_full_text=False
110
- )
111
- print(f"✓ Success! Response generated.")
112
- return response
113
- except Exception as hf_error:
114
- print(f"❌ HF API error: {str(hf_error)}")
115
- return f"I found relevant information in your documents:\n\n{context[:CONFIG['search_content_limit']]}..."
116
-
117
- def get_hf_client():
118
- """Get the HF client instance."""
119
- return hf_client
120
-
121
- def get_hf_llm_name():
122
- """Get the HF LLM name."""
123
- return LLM_NAME
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
helpers_LOCAL.py DELETED
@@ -1,165 +0,0 @@
1
- # Utility: Get installed Ollama LLMs as a list (for UI)
2
- def get_installed_llms():
3
- """Returns a list of locally installed Ollama LLMs (model names)."""
4
- import subprocess
5
- try:
6
- result = subprocess.run(["ollama", "list"], capture_output=True, text=True)
7
- lines = result.stdout.splitlines()
8
- models = []
9
- for line in lines:
10
- if line.strip() and not line.startswith("NAME"):
11
- name = line.split()[0]
12
- models.append(name)
13
- return models
14
- except Exception as e:
15
- print(f"Error listing local LLMs: {e}")
16
- return []
17
-
18
- # Utility: Display installed Ollama LLMs in terminal (for CLI use)
19
- def display_installed_llms():
20
- """Prints a list of locally installed Ollama LLMs to the terminal."""
21
- models = get_installed_llms()
22
- if models:
23
- print("Available local LLMs:")
24
- for m in models:
25
- print(f"- {m}")
26
- else:
27
- print("No local LLMs found.")
28
- """
29
- Local environment (Ollama) specific helpers for LLM operations.
30
- Contains: Ollama/ChatOllama initialization, agent creation, and response generation.
31
- """
32
-
33
- from helpers_SHARED import CONFIG, IS_HF_SPACE, AGENT_TOOLS
34
-
35
- # ============================================================================
36
- # OLLAMA LLM INITIALIZATION
37
- # ============================================================================
38
-
39
- ollama_llm = None
40
- LLM_NAME = None
41
-
42
- def init_ollama_llm(model_name=None):
43
- """Initialize Ollama (ChatOllama) for local LLM. Accepts a model name for dynamic selection."""
44
- global ollama_llm, LLM_NAME
45
-
46
- if IS_HF_SPACE:
47
- print("ℹ️ Running on HF Space, skipping Ollama initialization")
48
- return None, None
49
-
50
- from langchain_ollama import ChatOllama
51
-
52
- if model_name is None:
53
- model_name = CONFIG["ollama_model"]
54
- LLM_NAME = model_name
55
- try:
56
- ollama_llm = ChatOllama(model=model_name, base_url=CONFIG["ollama_base_url"])
57
- print(f"✓ Ollama (ChatOllama) initialized successfully with {LLM_NAME}")
58
- return ollama_llm, LLM_NAME
59
- except Exception as e:
60
- print(f"✗ Warning: Ollama not available: {e}")
61
- ollama_llm = None
62
- return None, LLM_NAME
63
-
64
- # ============================================================================
65
- # LANGCHAIN AGENT (LOCAL ONLY)
66
- # ============================================================================
67
-
68
- agent_executor = None
69
-
70
- def create_langchain_agent():
71
- """Create a LangGraph ReAct agent with the available tools.
72
-
73
- Only works in local environment with Ollama.
74
- Returns None on HF Spaces.
75
- """
76
- global agent_executor, ollama_llm
77
-
78
- if IS_HF_SPACE:
79
- print("ℹ️ HF Space detected - using manual tool routing (HF InferenceClient doesn't support LangChain agents)")
80
- return None
81
-
82
- if ollama_llm is None:
83
- print("❌ Ollama LLM not initialized, cannot create agent")
84
- return None
85
-
86
- from langgraph.prebuilt import create_react_agent # type: ignore
87
-
88
- try:
89
- agent_executor = create_react_agent(
90
- model=ollama_llm,
91
- tools=AGENT_TOOLS,
92
- )
93
- print("✅ LangGraph ReAct Agent created successfully with Ollama")
94
- return agent_executor
95
- except Exception as e:
96
- print(f"❌ Failed to create LangGraph agent: {e}")
97
- return None
98
-
99
- # ============================================================================
100
- # OLLAMA RESPONSE GENERATION
101
- # ============================================================================
102
-
103
- def ollama_generate_response(prompt: str, ollama_instance=None) -> str:
104
- """Generate a response using Ollama.
105
-
106
- Args:
107
- prompt: The prompt to send to the model
108
- ollama_instance: Optional Ollama instance, uses global if not provided
109
-
110
- Returns:
111
- Generated response string or None if failed
112
- """
113
- llm = ollama_instance or ollama_llm
114
-
115
- if llm is None:
116
- print("❌ Ollama not available")
117
- return None
118
-
119
- try:
120
- print(f"🧠 Generating response with Ollama ({LLM_NAME})...")
121
- response = llm.invoke(prompt)
122
- print(f"✓ Agent response generated.")
123
- return response
124
- except Exception as ollama_error:
125
- print(f"❌ Ollama error: {str(ollama_error)}")
126
- return None
127
-
128
- def run_agent(query: str) -> str:
129
- """Run the LangGraph agent on a query.
130
-
131
- Args:
132
- query: User query to process
133
-
134
- Returns:
135
- Agent response string or None if agent unavailable
136
- """
137
- global agent_executor
138
-
139
- if agent_executor is None:
140
- return None
141
-
142
- try:
143
- print(f"🤖 Using LangGraph Agent to process: {query}")
144
- result = agent_executor.invoke({"messages": [{"role": "user", "content": query}]})
145
- # Extract the last AI message
146
- messages = result.get("messages", [])
147
- for msg in reversed(messages):
148
- if hasattr(msg, 'content') and msg.type == "ai":
149
- return msg.content
150
- return "No response from agent"
151
- except Exception as e:
152
- print(f"❌ Agent execution failed: {e}, falling back to manual routing")
153
- return None
154
-
155
- def get_ollama_llm():
156
- """Get the Ollama LLM instance."""
157
- return ollama_llm
158
-
159
- def get_local_llm_name():
160
- """Get the local LLM name."""
161
- return LLM_NAME
162
-
163
- def get_agent_executor():
164
- """Get the agent executor instance."""
165
- return agent_executor
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
helpers_SHARED.py DELETED
@@ -1,407 +0,0 @@
1
- """
2
- Shared helper functions used by both HuggingFace Space and Local environments.
3
- Contains: configuration, memory management, vectorstore operations, PDF helpers, and UI utilities.
4
- """
5
-
6
- import os
7
- from typing import List, Optional
8
- from datetime import datetime
9
- from collections import deque
10
- import gradio as gr
11
- from langchain_community.document_loaders import PyPDFLoader
12
- from langchain_text_splitters import RecursiveCharacterTextSplitter
13
- from langchain_huggingface import HuggingFaceEmbeddings
14
- from langchain_community.vectorstores import FAISS
15
- from langchain_core.documents import Document
16
- from langchain_core.tools import tool
17
-
18
- # ============================================================================
19
- # CONFIGURATION - All settings in one place
20
- # ============================================================================
21
-
22
- def setup():
23
- """
24
- Central configuration for the RAG Agent application.
25
- Modify these values to customize the application behavior.
26
- Returns a config dictionary with all settings.
27
- """
28
- return {
29
- # Model Configuration
30
- "embedding_model": "sentence-transformers/all-MiniLM-L6-v2",
31
- "ollama_model": "qwen2m:latest", # Local Ollama model
32
- "hf_model": "Llama-3.2-3B-Instruct", # HuggingFace cloud model
33
- "ollama_base_url": "http://localhost:11434",
34
-
35
- # Text Splitting Configuration
36
- "chunk_size": 1000,
37
- "chunk_overlap": 200,
38
-
39
- # Search Configuration
40
- "search_k": 5, # Number of documents to retrieve
41
- "search_content_limit": 500, # Max chars to show per chunk
42
-
43
- # LLM Generation Configuration
44
- "max_tokens": 512,
45
- "temperature": 0.1, # Lower = more deterministic
46
- "temperature_fallback": 0.7, # For text_generation fallback
47
-
48
- # Memory Configuration
49
- "max_memory_turns": 50, # Max conversation turns to store
50
- "memory_context_limit": 500, # Max chars per memory entry
51
-
52
- # Server Configuration
53
- "server_port": 7860,
54
- "server_host": "0.0.0.0",
55
-
56
- # UI Configuration
57
- "chatbot_height": 600,
58
- "progress_bar_length": 20,
59
- "chat_progress_bar_length": 15,
60
- }
61
-
62
- # Initialize configuration
63
- CONFIG = setup()
64
-
65
- # ============================================================================
66
- # ENVIRONMENT DETECTION
67
- # ============================================================================
68
-
69
- IS_HF_SPACE = os.getenv("SPACE_ID") is not None
70
-
71
- # Directories - use persistent storage on HF Spaces if available
72
- DATA_DIR = "/data" if (IS_HF_SPACE and os.path.exists("/data")) else "data"
73
- EMBEDDINGS_DIR = os.path.join(DATA_DIR, "embeddings")
74
-
75
- # Check storage persistence status
76
- HAS_PERSISTENT_STORAGE = IS_HF_SPACE and os.path.exists("/data")
77
- STORAGE_WARNING = "" if not IS_HF_SPACE else (
78
- "✅ Persistent storage enabled - files will survive restarts" if HAS_PERSISTENT_STORAGE else
79
- "⚠️ Temporary storage - uploaded files will be lost when Space restarts"
80
- )
81
-
82
- # Initialize embeddings (shared across environments)
83
- embeddings = HuggingFaceEmbeddings(model_name=CONFIG["embedding_model"])
84
-
85
- # Global vectorstore (will be set by build_vectorstore)
86
- vs = None
87
-
88
- # ============================================================================
89
- # CONVERSATION MEMORY
90
- # ============================================================================
91
-
92
- conversation_memory: deque = deque(maxlen=CONFIG["max_memory_turns"])
93
-
94
- def add_to_memory(role: str, content: str):
95
- """Add a message to conversation memory with timestamp."""
96
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
97
- conversation_memory.append({
98
- "timestamp": timestamp,
99
- "role": role,
100
- "content": content
101
- })
102
- print(f"💾 Memory updated: {role} message added (total: {len(conversation_memory)} turns)")
103
-
104
- def get_memory_context(last_n: int = 10) -> str:
105
- """Get the last N conversation turns as context."""
106
- if not conversation_memory:
107
- return "No previous conversation history."
108
-
109
- recent = list(conversation_memory)[-last_n:]
110
- context_parts = []
111
- for msg in recent:
112
- role_emoji = "👤" if msg["role"] == "user" else "🤖"
113
- context_parts.append(f"[{msg['timestamp']}] {role_emoji} {msg['role'].upper()}: {msg['content'][:CONFIG['memory_context_limit']]}")
114
-
115
- return "\n\n".join(context_parts)
116
-
117
- def search_memory(query: str) -> str:
118
- """Search conversation memory for relevant past discussions."""
119
- if not conversation_memory:
120
- return "No conversation history to search."
121
-
122
- query_lower = query.lower()
123
- matches = []
124
-
125
- for msg in conversation_memory:
126
- content_lower = msg["content"].lower()
127
- # Simple keyword matching
128
- if any(word in content_lower for word in query_lower.split()):
129
- role_emoji = "👤" if msg["role"] == "user" else "🤖"
130
- matches.append(f"[{msg['timestamp']}] {role_emoji} {msg['role'].upper()}: {msg['content'][:CONFIG['memory_context_limit'] - 200]}...")
131
-
132
- if matches:
133
- return f"Found {len(matches)} relevant conversation(s):\n\n" + "\n\n---\n\n".join(matches[:5])
134
- else:
135
- return f"No conversations found matching '{query}'."
136
-
137
- def clear_memory():
138
- """Clear all conversation memory."""
139
- conversation_memory.clear()
140
- print("🧹 Conversation memory cleared")
141
-
142
- # ============================================================================
143
- # UTILITY FUNCTIONS
144
- # ============================================================================
145
-
146
- def get_timestamp() -> str:
147
- """Get current timestamp in HH:MM:SS format."""
148
- return datetime.now().strftime("%H:%M:%S")
149
-
150
- def create_elapsed_timer(start_time: datetime):
151
- """Create an elapsed time function for tracking duration."""
152
- def get_elapsed() -> str:
153
- elapsed = datetime.now() - start_time
154
- return f"⏱️ {elapsed.total_seconds():.1f}s"
155
- return get_elapsed
156
-
157
- def format_progress_bar(elapsed_time: str, percentage: int, message: str, bar_length: int = 20) -> str:
158
- """Format progress with visual progress bar using Unicode blocks."""
159
- filled_length = int(bar_length * percentage / 100)
160
- bar = '█' * filled_length + '░' * (bar_length - filled_length)
161
- return f"{elapsed_time} | [{percentage:3d}%] {bar} {message}"
162
-
163
- # =========================================================================
164
- # FLOATING PROGRESS BAR HTML/JS (for Gradio UI)
165
- # =========================================================================
166
- def floating_progress_bar_html():
167
- """Return HTML+JS for a floating, borderless, fit-content progress bar overlay."""
168
- return '''
169
- <div id="floating-progress" style="
170
- display: none;
171
- position: fixed;
172
- top: 20px; left: 50%; transform: translateX(-50%);
173
- background: #222; color: #fff; padding: 8px 0; border-radius: 8px; z-index: 9999;
174
- font-family: monospace; font-size: 1.2em; box-shadow: none; border: none;
175
- width: fit-content; min-width: 0; max-width: none;
176
- ">
177
- [....................................................................................................]
178
- </div>
179
- <script>
180
- function showProgressBar(barText) {
181
- var el = document.getElementById('floating-progress');
182
- el.innerText = barText;
183
- el.style.display = 'block';
184
- }
185
- function hideProgressBar() {
186
- document.getElementById('floating-progress').style.display = 'none';
187
- }
188
- // Example usage (remove or replace with Python/Gradio event):
189
- // showProgressBar('[|||||||||||||.............]');
190
- // setTimeout(hideProgressBar, 2000);
191
- </script>
192
- '''
193
-
194
- # ============================================================================
195
- # PDF HELPERS
196
- # ============================================================================
197
-
198
- def get_pdf_list() -> List[str]:
199
- """Get list of PDF files in data folder."""
200
- return [f for f in os.listdir(DATA_DIR) if f.endswith(".pdf")]
201
-
202
- def get_pdf_list_ui() -> List[str]:
203
- """Get PDF list for UI dropdown (with error handling)."""
204
- try:
205
- return get_pdf_list()
206
- except Exception as e:
207
- print(f"Error getting PDF list: {e}")
208
- return []
209
-
210
- def make_pdf_dropdown(value=None):
211
- """Create a PDF dropdown with current file list."""
212
- return gr.Dropdown(choices=get_pdf_list_ui(), value=value)
213
-
214
- # ============================================================================
215
- # VECTORSTORE OPERATIONS
216
- # ============================================================================
217
-
218
- def build_vectorstore(force_rebuild: bool = False) -> Optional[FAISS]:
219
- """Build or load FAISS vectorstore from PDFs.
220
-
221
- Args:
222
- force_rebuild: If True, rebuild from scratch even if existing vectorstore found
223
- """
224
- global vs
225
-
226
- # Check if we should load existing vectorstore
227
- if not force_rebuild and os.path.exists(os.path.join(EMBEDDINGS_DIR, "index.faiss")):
228
- try:
229
- print("📚 Loading existing vectorstore...")
230
- vectorstore = FAISS.load_local(EMBEDDINGS_DIR, embeddings, allow_dangerous_deserialization=True)
231
- print("✅ Vectorstore loaded successfully")
232
- vs = vectorstore
233
- return vectorstore
234
- except Exception as e:
235
- print(f"❌ Error loading vectorstore: {e}, rebuilding...")
236
-
237
- # Build new vectorstore from PDFs
238
- pdf_files = get_pdf_list()
239
- if not pdf_files:
240
- print("No PDF files found to build embeddings")
241
- vs = None
242
- return None
243
-
244
- print(f"🔨 Building vectorstore from {len(pdf_files)} PDF(s): {pdf_files}")
245
-
246
- docs: List[Document] = []
247
- for filename in pdf_files:
248
- try:
249
- filepath = os.path.join(DATA_DIR, filename)
250
- print(f"📖 Loading {filename}...")
251
- loader = PyPDFLoader(filepath)
252
- file_docs = loader.load()
253
- docs.extend(file_docs)
254
- print(f"✅ Loaded {len(file_docs)} pages from {filename}")
255
- except Exception as e:
256
- print(f"❌ Error loading {filename}: {e}")
257
- continue
258
-
259
- if not docs:
260
- print("⚠️ No documents could be loaded")
261
- vs = None
262
- return None
263
-
264
- print(f"✂️ Splitting {len(docs)} pages into chunks...")
265
- splitter = RecursiveCharacterTextSplitter(
266
- chunk_size=CONFIG["chunk_size"],
267
- chunk_overlap=CONFIG["chunk_overlap"]
268
- )
269
- splits = splitter.split_documents(docs)
270
- print(f"🧩 Created {len(splits)} text chunks")
271
-
272
- print("🤖 Creating FAISS embeddings...")
273
- try:
274
- vectorstore = FAISS.from_documents(splits, embeddings)
275
-
276
- print(f"💾 Saving vectorstore to {EMBEDDINGS_DIR}...")
277
- vectorstore.save_local(EMBEDDINGS_DIR)
278
-
279
- vs = vectorstore
280
- print("✅ Vectorstore built and saved successfully")
281
- return vectorstore
282
-
283
- except Exception as e:
284
- print(f"✗ Failed to build vectorstore: {e}")
285
- import traceback
286
- traceback.print_exc()
287
- vs = None
288
- return None
289
-
290
- def get_vectorstore():
291
- """Get the current vectorstore instance."""
292
- global vs
293
- return vs
294
-
295
- def set_vectorstore(vectorstore):
296
- """Set the vectorstore instance."""
297
- global vs
298
- vs = vectorstore
299
-
300
- # ============================================================================
301
- # RAG AGENT TOOLS (LangChain @tool decorator pattern)
302
- # ============================================================================
303
-
304
- @tool
305
- def list_documents() -> str:
306
- """List all available PDF documents in the system. Use this tool when the user asks what documents are available, what files they have, or wants to see the document list."""
307
- pdfs = get_pdf_list()
308
- if pdfs:
309
- return f"📁 Available documents: {', '.join(pdfs)}"
310
- else:
311
- return "📁 No documents are currently uploaded."
312
-
313
- @tool
314
- def count_documents() -> str:
315
- """Count the total number of uploaded PDF documents. Use this tool when the user asks how many documents they have or wants a document count."""
316
- count = len(get_pdf_list())
317
- return f"📊 Total documents: {count}"
318
-
319
- @tool
320
- def search_documents(query: str) -> str:
321
- """Search document content using RAG (Retrieval Augmented Generation). Use this tool to find information within the uploaded PDF documents based on a search query."""
322
- global vs
323
-
324
- # Check if we have any PDF files first
325
- pdf_files = get_pdf_list()
326
- if not pdf_files:
327
- return "🔍 No documents are currently uploaded. Please upload PDF files first."
328
-
329
- # Force reload vectorstore from disk if files exist
330
- print(f"🔍 Checking vectorstore for {len(pdf_files)} PDF files...")
331
-
332
- # Check if FAISS files exist on disk
333
- faiss_path = os.path.join(EMBEDDINGS_DIR, "index.faiss")
334
- pkl_path = os.path.join(EMBEDDINGS_DIR, "index.pkl")
335
-
336
- if os.path.exists(faiss_path) and os.path.exists(pkl_path):
337
- print(f"📁 Found vectorstore files, loading...")
338
- try:
339
- # Force reload from disk
340
- vs = FAISS.load_local(EMBEDDINGS_DIR, embeddings, allow_dangerous_deserialization=True)
341
- print(f"✅ Vectorstore loaded successfully from disk")
342
- except Exception as e:
343
- print(f"❌ Error loading vectorstore: {e}")
344
- vs = None
345
- else:
346
- print(f"📁 No vectorstore files found, attempting to build...")
347
- vs = build_vectorstore()
348
-
349
- if vs is None:
350
- return f"🔍 Found {len(pdf_files)} document(s) but search index could not be created. Please try re-uploading your files."
351
-
352
- try:
353
- # Extract key search terms from query (remove common words)
354
- search_query = query
355
- print(f"🔍 Searching vectorstore for: {search_query}")
356
-
357
- # Use similarity_search_with_score to filter by relevance
358
- docs_with_scores = vs.similarity_search_with_score(search_query, k=CONFIG["search_k"])
359
-
360
- if docs_with_scores:
361
- # Filter by score (lower is better for L2 distance) - adjust threshold as needed
362
- # Show more content from each chunk for better context
363
- context_parts = []
364
- for doc, score in docs_with_scores:
365
- # Get source file from metadata
366
- source = doc.metadata.get('source', 'Unknown').split('/')[-1]
367
- page = doc.metadata.get('page', '?')
368
- # Include score and source in debug output
369
- print(f" 📄 Score: {score:.3f} | Source: {source} pg{page} - {doc.page_content[:50]}...")
370
- # Show more content with source info
371
- context_parts.append(f"[Source: {source}, Page: {page}, Relevance: {score:.2f}]\n{doc.page_content[:CONFIG['search_content_limit']]}")
372
-
373
- context = "\n\n---\n\n".join(context_parts)
374
- print(f"✓ Found {len(docs_with_scores)} document chunks")
375
- return f"🔍 Search results for '{query}':\n\n{context}"
376
- else:
377
- print(f"⚠️ No relevant documents found for query: {query}")
378
- return f"🔍 No relevant information found for '{query}' in your {len(pdf_files)} document(s). Try different keywords or check if your documents contain relevant content."
379
-
380
- except Exception as e:
381
- error_msg = f"🔍 Search error: {str(e)}. You have {len(pdf_files)} documents available."
382
- print(f"❌ Search error: {str(e)}")
383
- import traceback
384
- traceback.print_exc()
385
- return error_msg
386
-
387
- @tool
388
- def search_conversation_history(query: str) -> str:
389
- """Search through previous conversation history to find past discussions. Use this tool when the user asks about something they discussed before, wants to recall previous answers, or references past conversations."""
390
- return search_memory(query)
391
-
392
- @tool
393
- def get_recent_conversation(turns: int = 5) -> str:
394
- """Get the most recent conversation turns. Use this tool when the user asks what they were discussing, wants a summary of recent chat, or needs context from earlier in the conversation."""
395
- return get_memory_context(last_n=turns)
396
-
397
- # List of all available tools
398
- AGENT_TOOLS = [list_documents, count_documents, search_documents, search_conversation_history, get_recent_conversation]
399
-
400
- # Sample question texts - Enhanced for agent capabilities
401
- SAMPLE_Q1 = "How many documents are loaded? List their names and types?"
402
- SAMPLE_Q2 = "Summarize the key points of each document in 5 bullet points."
403
- SAMPLE_Q3 = "What is the attention mechanism? list the main topics."
404
- SAMPLE_Q4 = "How can I cook chicken breast with Phillips air fryer recipes?"
405
- SAMPLE_Q5 = "Summarize each document in max 10 bullet points."
406
- SAMPLE_Q6 = "What did we discuss earlier?"
407
- SAMPLE_Q7 = "Summarize it in 50 words."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt DELETED
@@ -1,17 +0,0 @@
1
- fastapi>=0.104.0
2
- uvicorn>=0.24.0
3
- langchain>=0.1.0
4
- langchain-community>=0.0.10
5
- langchain-core>=0.1.0
6
- langchain-text-splitters>=0.0.1
7
- langchain-huggingface>=0.0.1
8
- langchain-ollama>=0.1.0
9
- langgraph>=0.2.0
10
- huggingface_hub>=0.19.0
11
- gradio==6.0.1
12
- pypdf>=3.17.0
13
- sentence-transformers>=2.2.2
14
- faiss-cpu>=1.7.4
15
- requests>=2.31.0
16
- pydantic>=2.4.0
17
- python-multipart>=0.0.6