Munene1 commited on
Commit
073b297
Β·
verified Β·
1 Parent(s): 9eb9967

Create smart_library_search.py

Browse files
Files changed (1) hide show
  1. smart_library_search.py +925 -0
smart_library_search.py ADDED
@@ -0,0 +1,925 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import chromadb
2
+ from chromadb.config import Settings
3
+ from sentence_transformers import SentenceTransformer
4
+ import fitz #chroma_db PyMuPDF
5
+ from transformers import AutoTokenizer
6
+ import os
7
+ import re
8
+ import gradio as gr
9
+ import os
10
+ from flask import send_file
11
+ import google.generativeai as genai
12
+ import tempfile
13
+ import shutil
14
+
15
+ download_counts={}
16
+
17
+ # === CLOUD STORAGE URL MAPPING ===
18
+ # Add direct download links for each PDF file
19
+ ARTICLE_URLS = {
20
+ "Davit Marikyan-2019-Computing And Informatics-unified_theory_of_acceptance_and_use_of_technology.pdf": "https://1drv.ms/b/c/78b29f6d9bcf3843/ETDFRinNWmxIo6Sri8r0tFQB3MbD3M6CcdRv5a8T5jlY-g?e=vbCFca",
21
+ "enard Omallah George-2015-Computing And Informatics-Role_of_E_Resources_for_Research_Managemt.pdf": "https://1drv.ms/b/c/78b29f6d9bcf3843/ETbZst6ntatPm-pVPsOVA0YBFUpuV40t0H7c70NzqHrdRg?e=pT6Abp",
22
+ "Kamau M-2020-Education-Strategies Employed by Mount Kenya University to Achieve Competitive Advantage.pdf": "https://1drv.ms/b/c/78b29f6d9bcf3843/EYl1sNw6NqlEqUtZcCidDdMBkxQJEjEnv8VHh2giPHXL_A?e=azCxFs",
23
+ "Mahmood-2025-Education-revalence and Effects of Smartphone Use on Academic Performance of Undergraduate Student Nurses: An Analytical Cross-Sectional Study.pdf": "https://1drv.ms/b/c/78b29f6d9bcf3843/ERgGX0tf7jpOhfy5bDhWDzkBeaxL4x1RPWfyN0sHDGnuvA?e=WNUZst",
24
+ "Arana_CedeΓ±o-2022-Social Science-Wars and other current challenges for health and life.pdf": "https://1drv.ms/b/c/78b29f6d9bcf3843/Eab88y2jonpMg9aSTfgkKxIBTFSIQRUJljeeG9SYhqbMiw?e=aAz1Bv",
25
+ "A Arunprakash-2022-Education-Investigating Digital Education, A Study on Fostering Accessibility and Equity in Learning Environments.pdf:": "https://1drv.ms/b/c/78b29f6d9bcf3843/EZ1cpWglJ4NFhQZnd83Q4z8B2u0ihaSGaZILBdvCRBuJJg?e=S7PoZK",
26
+ "Anyanwu, D.-2021-Computing And Informatics-Cybersecurity in the Age of the Internet of Things A Review of Challenges.pdf" : "https://1drv.ms/b/c/78b29f6d9bcf3843/EaXYy_YTWUFPmTJp_IQuPO0BWUVes_to3J_IXYE8ZXUywg?e=mK1con",
27
+ "Ashish Makanadar-2020-Computing And Informatics-Digital surveillance capitalism and citiesdata, democracy and activism.pdf" : "https://1drv.ms/b/c/78b29f6d9bcf3843/ET4Pk0ndottEtwKUnRE18XoBqt-qG9N229QUALcjIQT0BQ?e=oW3vDN",
28
+ "Benedict Dellot-2017-Social Science-RSA The Age Of Automation Report.pdf" : "",
29
+ "Carmen G. Gonzalez-2023-Social Science-Climate Change, Race, and Migration.pdf" : "https://1drv.ms/b/c/78b29f6d9bcf3843/EfyivsAY9XxLgnOJsK_bk5IBzakhpkyVrInyv71rdUKwZg?e=xftDHo",
30
+ "D Paris-2020-Education-Culturally Sustaining Pedagogy.pdf" : "https://1drv.ms/b/c/78b29f6d9bcf3843/EbcH1oNsQS9FjD0rZ4xMv8kBFdhJ5n6rMSGh-eEE-g7t6w?e=UyBpgA",
31
+ "Ahmad Samadi-2021-Education-How Can Support and Stability Prevent Teacher Burnout and Support (1).pdf" : "https://1drv.ms/b/c/78b29f6d9bcf3843/EWMuLDLZEo5FgkTwfUTT0LMBiPJ2-A0_5N52RPEET_UGgQ?e=56zG3D",
32
+ "Lesley Bartlett-2023-Education-Debating the β€œScience of Reading” and its Impact on Policy.pdf" : "https://1drv.ms/b/c/78b29f6d9bcf3843/ESlCuNhXdwFBoJbmU4DbZ4IB902hJ1cg_BdE11W_CMdxmA?e=LkroLE",
33
+ "M. SHELLEY THOMAS-2024-Education-Trauma-Informed Practices in Schools Across Two.pdf" : "https://1drv.ms/b/c/78b29f6d9bcf3843/ERT6CkW2gHZKlXPVcRuhTzABZ-vcpXdwdwFy89eUds6zBA?e=tysA4K",
34
+ "Manuel Au-Yong-Oliveira-2021-Social Science-The Role of AI and Automation on the FutURE.pdf" : "https://1drv.ms/b/c/78b29f6d9bcf3843/EXi3c_Vib75GgygLr9U04voB8vP2jZPN5RH_66-8Sgvb0w?e=6ZehVo",
35
+ "Myra Marx Fettee-2022-Social Science-Inequality, Intersectionality and the Politics of Discourse.pdf" : "https://1drv.ms/b/c/78b29f6d9bcf3843/EUToraqEywhLgAOfKFWx_jAB5JFgUweNo3QeEBjfTptniw?e=UZFKUu",
36
+ "Paula Braveman-2023-Social Science-The Social Determinants.pdf" : "https://1drv.ms/b/c/78b29f6d9bcf3843/EeKtmqDtD-xCq-zKv56M4g4B8mvZtzV4fmh3aLZufjp4jw?e=Htlay0",
37
+ "Ruhee D’Cunha-2021-Computing And Informatics-Challenges in the use of quantum computing hardware-efficient AnsΒ¨atze.pdf" : "https://1drv.ms/b/c/78b29f6d9bcf3843/EdCAR3vs_NxFsCLdXLTKOLkBMbTfI0yEtyjn7MozZwcU5Q?e=4FwmQq",
38
+ "Samuel M. Wilson-2022-Social Science-The Anthropology of Online Communities.pdf" : "https://1drv.ms/b/c/78b29f6d9bcf3843/Ed8oce6arD1Cp74j88qGCh8B4nYqMPc8fEik1DdzPHBBPw?e=iQeOW9",
39
+ "Ullrich K. H. Ecker-2017Social Science-Why rebuttals may not work: the psychology of misinformation.pdf" : "https://1drv.ms/b/c/78b29f6d9bcf3843/EWNyYK5cK-9Fo5qve_EU4ccB_r6z5_D6s9fvT-zaG6ByNA?e=gjo72C"
40
+
41
+ # Add all your files here with their direct cloud storage URLs
42
+ }
43
+ # Initialize tokenizer for chunking
44
+ tokenizer = AutoTokenizer.from_pretrained("sentence-transformers/all-MiniLM-L6-v2")
45
+
46
+ def extract_text_from_pdf(pdf_path):
47
+ doc = fitz.open(pdf_path)
48
+ full_text = ""
49
+
50
+ print(f"Processing {os.path.basename(pdf_path)} - {len(doc)} pages")
51
+
52
+ for page_num in range(len(doc)):
53
+ page = doc.load_page(page_num)
54
+ text = page.get_text()
55
+
56
+ # Skip obviously preliminary pages (title, declarations, etc.)
57
+ if page_num < 3: # First few pages are usually preliminaries
58
+ if any(word in text.lower() for word in ['declaration', 'dedication', 'acknowledgement', 'table of content']):
59
+ print(f"Skipping preliminary page {page_num + 1}")
60
+ continue
61
+
62
+ full_text += text + "\n"
63
+
64
+ full_text = re.sub(r'\s+', ' ', full_text).strip()
65
+ print(f"Extracted {len(full_text)} characters")
66
+ return full_text
67
+
68
+ def enhanced_extract_text_from_pdf(pdf_path):
69
+ """
70
+ Enhanced PDF text extraction that handles various PDF structures
71
+ """
72
+ doc = fitz.open(pdf_path)
73
+ text = ""
74
+
75
+ for page_num, page in enumerate(doc):
76
+ # Extract text with different methods if needed
77
+ page_text = page.get_text("text")
78
+
79
+ # Check if this page has substantial content (not just headers/footers)
80
+ if len(page_text.strip()) > 100: # Avoid empty or nearly empty pages
81
+ text += f"\n--- Page {page_num + 1} ---\n{page_text}\n"
82
+
83
+ # Alternative: try different extraction methods
84
+ if len(text.strip()) < 500: # If too little text extracted
85
+ print(f"Warning: Only {len(text)} characters extracted - trying alternative method")
86
+ text = ""
87
+ for page in doc:
88
+ text += page.get_text("blocks") # Try blocks method
89
+
90
+ text = re.sub(r'\s+', ' ', text).strip()
91
+ return text
92
+
93
+ def parse_metadata_from_filename(filename):
94
+ """
95
+ Extracts metadata from filenames in various formats.
96
+ Handles: Author_Name-Year-Department-Title_Keywords.pdf
97
+ Also handles: Other_Formats-With-Different-Structures.pdf
98
+ """
99
+ # Remove the .pdf extension
100
+ name_without_ext = os.path.splitext(filename)[0]
101
+
102
+ # Default metadata
103
+ metadata = {
104
+ "source": filename,
105
+ "author": "Unknown Author",
106
+ "year": "Unknown Year",
107
+ "department": "General",
108
+ "title": name_without_ext.replace('_', ' ').title() # Fallback title
109
+ }
110
+
111
+ # Split by hyphens to get the components
112
+ parts = name_without_ext.split('-')
113
+
114
+ # Different parsing strategies based on number of parts
115
+ if len(parts) >= 4:
116
+ # Format: Author-Year-Department-Title (most structured)
117
+ metadata["author"] = parts[0].replace('_', ' ').title()
118
+ metadata["year"] = parts[1]
119
+ metadata["department"] = parts[2].replace('_', ' ').title()
120
+ metadata["title"] = ' '.join(parts[3:]).replace('_', ' ').title()
121
+
122
+ elif len(parts) == 3:
123
+ # Format: Author-Year-Title or other 3-part formats
124
+ # Check if the second part looks like a year (4 digits)
125
+ if parts[1].isdigit() and len(parts[1]) == 4:
126
+ metadata["author"] = parts[0].replace('_', ' ').title()
127
+ metadata["year"] = parts[1]
128
+ metadata["title"] = parts[2].replace('_', ' ').title()
129
+ else:
130
+ # Not a year, so probably Author-Department-Title
131
+ metadata["author"] = parts[0].replace('_', ' ').title()
132
+ metadata["department"] = parts[1].replace('_', ' ').title()
133
+ metadata["title"] = parts[2].replace('_', ' ').title()
134
+
135
+ elif len(parts) == 2:
136
+ # Format: Author-Title or Year-Title
137
+ if parts[0].isdigit() and len(parts[0]) == 4:
138
+ metadata["year"] = parts[0]
139
+ metadata["title"] = parts[1].replace('_', ' ').title()
140
+ else:
141
+ metadata["author"] = parts[0].replace('_', ' ').title()
142
+ metadata["title"] = parts[1].replace('_', ' ').title()
143
+
144
+ elif len(parts) == 1:
145
+ # No hyphens, just use the whole filename as title
146
+ metadata["title"] = name_without_ext.replace('_', ' ').title()
147
+
148
+ # Clean up author name - remove "email" prefix if present
149
+ if metadata["author"].lower().startswith("email"):
150
+ metadata["author"] = metadata["author"][5:].strip()
151
+
152
+ # Clean up any remaining underscores in all fields
153
+ for key in ["author", "department", "title"]:
154
+ if isinstance(metadata[key], str):
155
+ metadata[key] = metadata[key].replace('_', ' ')
156
+
157
+ return metadata
158
+
159
+
160
+
161
+ def chunk_text(text, chunk_size=1000, overlap=100):
162
+ """
163
+ Smart chunking that prioritizes main content
164
+ """
165
+ tokens = tokenizer.encode(text)
166
+ chunks = []
167
+
168
+ # Skip very short chunks (headers/footers)
169
+ for i in range(0, len(tokens), chunk_size - overlap):
170
+ chunk_tokens = tokens[i:i + chunk_size]
171
+ if len(chunk_tokens) < 100: # Skip very short chunks
172
+ continue
173
+
174
+ chunk_text = tokenizer.decode(chunk_tokens, skip_special_tokens=True)
175
+
176
+ # Skip chunks that are mostly preliminary content
177
+ if is_preliminary_content(chunk_text):
178
+ continue
179
+
180
+ chunks.append(chunk_text)
181
+
182
+ return chunks
183
+
184
+ def is_preliminary_content(text):
185
+ """
186
+ Identify and skip preliminary pages content
187
+ """
188
+ preliminary_keywords = [
189
+ 'declaration', 'dedication', 'acknowledgement',
190
+ 'table of content', 'abstract', 'chapter one',
191
+ 'page', 'Β©', 'all rights reserved'
192
+ ]
193
+
194
+ text_lower = text.lower()
195
+ return any(keyword in text_lower for keyword in preliminary_keywords)
196
+
197
+ def semantic_chunk_text(text, chunk_size=512):
198
+ """
199
+ Chunk at sentence boundaries for better coherence
200
+ """
201
+ import nltk
202
+ nltk.download('punkt', quiet=True)
203
+ from nltk.tokenize import sent_tokenize
204
+
205
+ sentences = sent_tokenize(text)
206
+ chunks = []
207
+ current_chunk = ""
208
+
209
+ for sentence in sentences:
210
+ if len(current_chunk) + len(sentence) < chunk_size:
211
+ current_chunk += " " + sentence
212
+ else:
213
+ if current_chunk.strip():
214
+ chunks.append(current_chunk.strip())
215
+ current_chunk = sentence
216
+
217
+ if current_chunk.strip():
218
+ chunks.append(current_chunk.strip())
219
+
220
+ return chunks
221
+
222
+ def process_and_index_with_chunks(directory_path):
223
+ """
224
+ Process documents and store them as chunks in ChromaDB
225
+ """
226
+ documents = []
227
+ metadatas = []
228
+ ids = []
229
+ embeddings_list = []
230
+
231
+ for filename in os.listdir(directory_path):
232
+ if filename.endswith(".pdf"):
233
+ file_path = os.path.join(directory_path, filename)
234
+ print(f"Processing {filename}...")
235
+
236
+ # Extract full text
237
+ text = extract_text_from_pdf(file_path)
238
+
239
+ # Get metadata
240
+ file_metadata = parse_metadata_from_filename(filename)
241
+ file_metadata["source_file"] = filename
242
+ file_metadata["full_text"] = text # Keep full text in metadata
243
+
244
+ # Split into chunks
245
+ chunks = chunk_text(text)
246
+ print(f" Split into {len(chunks)} chunks")
247
+
248
+ # Create embedding for each chunk and add to collection
249
+ for i, chunk in enumerate(chunks):
250
+ chunk_id = f"{filename}_chunk_{i}"
251
+ embedding = model.encode(chunk).tolist()
252
+
253
+ documents.append(chunk)
254
+ embeddings_list.append(embedding)
255
+ metadatas.append(file_metadata) # Same metadata for all chunks
256
+ ids.append(chunk_id)
257
+
258
+ # Add to ChromaDB
259
+ collection.add(
260
+ documents=documents,
261
+ embeddings=embeddings_list,
262
+ metadatas=metadatas,
263
+ ids=ids
264
+ )
265
+ print(f"βœ… Indexed {len(documents)} chunks from {len(os.listdir(directory_path))} documents")
266
+
267
+ def reindex_with_larger_chunks():
268
+ """
269
+ Re-index with larger chunk sizes for more content
270
+ """
271
+ print("πŸ”„ Re-indexing with larger chunks (1000 tokens)...")
272
+
273
+ # Clear existing collection
274
+ try:
275
+ chroma_client.delete_collection("mk_library_doc_ceelt")
276
+ except:
277
+ pass
278
+
279
+ global collection
280
+ collection = chroma_client.create_collection(name="mk_library_doc_ceelt")
281
+
282
+ # Process with larger chunks
283
+ process_and_index_with_chunks("pdf_store") # This will use the updated chunk_size
284
+ print("πŸŽ‰ Re-indexed with larger chunks!")
285
+ # --- RUN THIS ONCE TO POPULATE YOUR DATABASE ---
286
+
287
+ def smart_chunk_text(text, chunk_size=1000, overlap=100):
288
+ """
289
+ Smarter chunking that tries to preserve complete paragraphs
290
+ """
291
+ # Split into paragraphs first
292
+ paragraphs = [p for p in text.split('\n\n') if p.strip()]
293
+
294
+ chunks = []
295
+ current_chunk = ""
296
+
297
+ for paragraph in paragraphs:
298
+ if len(current_chunk) + len(paragraph) < chunk_size:
299
+ current_chunk += "\n\n" + paragraph
300
+ else:
301
+ if current_chunk.strip():
302
+ chunks.append(current_chunk.strip())
303
+ current_chunk = paragraph
304
+
305
+ if current_chunk.strip():
306
+ chunks.append(current_chunk.strip())
307
+
308
+ return chunks
309
+
310
+ model = SentenceTransformer('all-MiniLM-L6-v2')
311
+ chroma_client = chromadb.PersistentClient(path="chroma_db")
312
+
313
+
314
+ # === GEMINI API CONFIGURATION ===
315
+ GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
316
+ if GEMINI_API_KEY:
317
+ try:
318
+ genai.configure(api_key=GEMINI_API_KEY)
319
+
320
+ # Choose one of these working models:
321
+ gemini_model = genai.GenerativeModel('models/gemini-1.5-pro-latest') # βœ… Best option
322
+ # OR
323
+ gemini_model = genai.GenerativeModel('models/gemini-1.5-flash-latest') # βœ… Faster option
324
+ # OR
325
+ gemini_model = genai.GenerativeModel('models/gemini-2.0-flash') # βœ… Good balance
326
+
327
+ print("βœ… Gemini API configured successfully")
328
+
329
+ except Exception as e:
330
+ print(f"❌ Gemini configuration failed: {e}")
331
+ gemini_model = None
332
+ else:
333
+ gemini_model = None
334
+ print("πŸ”Ά Gemini API not configured - using local responses")
335
+
336
+ # Create (or get) a collection. Think of it as a table for your library data.
337
+ collection = chroma_client.create_collection(name="mk_library_mor")
338
+
339
+ # 🚨 RE-INDEXING STEP (RUN ONCE - THEN COMMENT OUT)
340
+ print("Starting re-indexing with full text...")
341
+ #reindex_all_documents() # This will recreate the collection with full text
342
+ print("Re-indexing completed!")
343
+
344
+
345
+ # Re-process just this problematic PDF
346
+ def reprocess_specific_pdf(filename):
347
+ pdf_path = os.path.join("pdf_store", filename)
348
+
349
+ if os.path.exists(pdf_path):
350
+ print(f"Re-processing: {filename}")
351
+
352
+ # Remove existing entry from ChromaDB
353
+ try:
354
+ collection.delete(ids=[filename])
355
+ print(f"Removed old entry for {filename}")
356
+ except:
357
+ print(f"No existing entry to remove for {filename}")
358
+
359
+ # Extract with enhanced method
360
+ text = extract_text_from_pdf(pdf_path)
361
+ print(f"Extracted text length: {len(text)}")
362
+
363
+ if len(text) > 1000:
364
+ # Get metadata
365
+ metadata = parse_metadata_from_filename(filename)
366
+ metadata["full_text"] = text
367
+
368
+ # Create embedding and add to collection
369
+ embedding = model.encode(text).tolist()
370
+ doc_snippet = text[:1000] + "..." if len(text) > 1000 else text
371
+
372
+ collection.add(
373
+ documents=[doc_snippet],
374
+ embeddings=[embedding],
375
+ metadatas=[metadata],
376
+ ids=[filename]
377
+ )
378
+ print(f"Successfully re-indexed {filename}")
379
+ return True
380
+ else:
381
+ print(f"Warning: Very little text extracted ({len(text)} chars)")
382
+ return False
383
+ return False
384
+
385
+ # Run this for the problematic PDF
386
+ reprocess_specific_pdf("Kamau M-2020-Education-Strategies Employed by Mount Kenya University to Achieve Competitive Advantage.pdf")
387
+
388
+
389
+
390
+ # Add this debug function to check what text was actually extracted
391
+ def debug_pdf_text(pdf_filename):
392
+ file_path = os.path.join("pdf_store", pdf_filename)
393
+ if os.path.exists(file_path):
394
+ full_text = extract_text_from_pdf(file_path)
395
+ print(f"=== TEXT EXTRACTED FROM {pdf_filename} ===")
396
+ print(full_text[:1000]) # First 1000 chars
397
+ return full_text
398
+ return None
399
+
400
+ # Test with the specific PDF
401
+ debug_pdf_text("Kamau M-2020-Education-Strategies Employed by Mount Kenya University to Achieve Competitive Advantage.pdf")
402
+
403
+
404
+ pdf_store = "pdf_store"
405
+ process_and_index_with_chunks(pdf_store)
406
+
407
+
408
+
409
+ def semantic_search(query, n_results=5, year_filter=None, department_filter=None, author_filter=None):
410
+ """Search that returns individual chunks"""
411
+ query_embedding = model.encode([query]).tolist()
412
+
413
+ # Build filter
414
+ where_filter = {}
415
+ if year_filter and year_filter != "All":
416
+ where_filter["year"] = {"$gte": year_filter}
417
+ if department_filter and department_filter != "All":
418
+ where_filter["department"] = {"$eq": department_filter}
419
+ if author_filter and author_filter != "All":
420
+ where_filter["author"] = {"$eq": author_filter}
421
+
422
+ # Query ChromaDB
423
+ results = collection.query(
424
+ query_embeddings=query_embedding,
425
+ n_results=n_results,
426
+ where=where_filter if where_filter else None,
427
+ include=['metadatas', 'documents', 'distances']
428
+ )
429
+
430
+ # Format results
431
+ output = []
432
+ for meta, doc_text, distance in zip(results['metadatas'][0], results['documents'][0], results['distances'][0]):
433
+ similarity_score = 1 - (distance / 2)
434
+
435
+ output.append({
436
+ "title": meta.get('title', 'Research Document'),
437
+ "author": meta.get('author', 'Unknown Author'),
438
+ "year": meta.get('year', ''),
439
+ "department": meta.get('department', 'General Studies'),
440
+ "source": meta.get('source_file', ''),
441
+ "content": doc_text, # This is now the actual chunk content
442
+ "relevance": f"{similarity_score:.1%}",
443
+ "chunk": True # Flag that this is a chunk
444
+ })
445
+
446
+ return output
447
+
448
+ def prepare_context(relevant_docs):
449
+ """Prepare context from relevant documents"""
450
+ context = "Based on the following research documents:\n\n"
451
+
452
+ for i, doc in enumerate(relevant_docs, 1):
453
+ context += f"Document {i}: {doc['title']} by {doc['author']} ({doc['year']})\n"
454
+ context += f"Content: {doc['content'][:250]}...\n\n"
455
+
456
+ return context
457
+
458
+
459
+ def generate_contextual_response(question, relevant_docs):
460
+ """Smart response with adaptive content length"""
461
+ if not relevant_docs:
462
+ return "πŸ” **I couldn't find specific research on this topic.**"
463
+
464
+ response = "**πŸ“š Research Findings:**\n\n"
465
+
466
+ for i, doc in enumerate(relevant_docs[:3], 1):
467
+ content = doc['content']
468
+
469
+ # Show more content for highly relevant results
470
+ if float(doc['relevance'].strip('%')) > 70: # Highly relevant
471
+ preview = content[:800] + "..." if len(content) > 800 else content
472
+ else: # Moderately relevant
473
+ preview = content[:400] + "..." if len(content) > 400 else content
474
+
475
+ response += f"**{i}. {doc['title']}**\n"
476
+ response += f" πŸ‘€ *{doc['author']}* ({doc['year']}) - {doc['department']}\n"
477
+ response += f" πŸ“– {preview}\n"
478
+ response += f" 🎯 Relevance: {doc['relevance']}\n\n"
479
+
480
+ response += "**πŸ“‹ Source References:**\n"
481
+ for doc in relevant_docs[:3]:
482
+ response += f"β€’ {doc['title']} by {doc['author']} ({doc['year']})\n"
483
+
484
+ return response
485
+
486
+
487
+ def generate_local_response(question, relevant_docs):
488
+ """Enhanced response formatting for better readability"""
489
+ if not relevant_docs:
490
+ return "πŸ” **I couldn't find specific research on this topic.**\n\n**Try:**\nβ€’ Using broader search terms\nβ€’ Adjusting the filters\nβ€’ Asking about general research areas"
491
+
492
+ # Start with a more natural introduction
493
+ response = "**πŸ“š I found some relevant research for you:**\n\n"
494
+
495
+ for i, doc in enumerate(relevant_docs[:3], 1):
496
+ # Create a cleaner, more readable snippet
497
+ content = doc['content']
498
+
499
+ # Remove excessive whitespace and formatting issues
500
+ content = re.sub(r'\s+', ' ', content).strip()
501
+
502
+ # Create a better preview - focus on the actual content
503
+ if len(content) > 120:
504
+ # Try to find a complete sentence
505
+ sentences = content.split('.')
506
+ if len(sentences) > 1 and len(sentences[0]) > 20:
507
+ preview = sentences[0] + '.'
508
+ else:
509
+ preview = content[:120] + '...'
510
+ else:
511
+ preview = content
512
+
513
+ response += f"**{i}. {doc['title']}**\n"
514
+ response += f" πŸ‘€ *{doc['author']}* ({doc['year']}) - {doc['department']}\n"
515
+ response += f" πŸ“– {preview}\n\n"
516
+
517
+ # Add more natural follow-up suggestions
518
+ response += "**πŸ’‘ You might want to ask:**\n"
519
+ response += "β€’ 'Can you tell me more about the first study?'\n"
520
+ response += "β€’ 'What methodology was used in this research?'\n"
521
+ response += "β€’ 'What were the main findings or conclusions?'\n"
522
+ response += "β€’ 'Are there similar studies on this topic?'"
523
+
524
+ return response
525
+
526
+ def create_text_snippet(text, max_words=10, query_terms=None):
527
+ """
528
+ Creates a clean text snippet from the full text.
529
+ - Shows the beginning of the content (not metadata like declarations)
530
+ - Highlights query terms if provided
531
+ - Limits to a specific number of words
532
+ """
533
+ # Remove extra whitespace and make lowercase for processing
534
+ clean_text = ' '.join(text.split())
535
+
536
+ # Try to find the actual content (skip declarations, acknowledgements, etc.)
537
+ # Look for common academic document sections to find the main content
538
+ content_starters = [
539
+ "abstract", "chapter", "introduction", "background",
540
+ "this study", "research", "the purpose", "objective"
541
+ ]
542
+
543
+ # Find where the actual content begins
544
+ content_start = 0
545
+ lower_text = clean_text.lower()
546
+ for starter in content_starters:
547
+ pos = lower_text.find(starter)
548
+ if pos != -1 and (content_start == 0 or pos < content_start):
549
+ content_start = pos
550
+
551
+ # If we found a content start, use that section
552
+ if content_start > 0:
553
+ snippet_text = clean_text[content_start:]
554
+ else:
555
+ snippet_text = clean_text
556
+
557
+ # Truncate to max_words
558
+ words = snippet_text.split()
559
+ if len(words) > max_words:
560
+ snippet = ' '.join(words[:max_words]) + '...'
561
+ else:
562
+ snippet = snippet_text
563
+
564
+ # Optional: Highlight query terms if provided
565
+ if query_terms:
566
+ for term in query_terms:
567
+ if term.lower() in snippet.lower():
568
+ # Simple highlighting with HTML
569
+ snippet = snippet.replace(term, f"<strong>{term}</strong>")
570
+ snippet = snippet.replace(term.lower(), f"<strong>{term.lower()}</strong>")
571
+ snippet = snippet.replace(term.upper(), f"<strong>{term.upper()}</strong>")
572
+
573
+ return snippet
574
+
575
+ def call_gemini_api(question, context):
576
+ """More specific prompt for research objectives"""
577
+ prompt = f"""As a research assistant, analyze this context to find the SPECIFIC RESEARCH OBJECTIVES.
578
+
579
+ QUESTION: {question}
580
+
581
+ CONTEXT EXCERPTS:
582
+ {context}
583
+
584
+ Instructions:
585
+ 1. Look for sections titled: "Objectives", "Research Objectives", "Study Objectives"
586
+ 2. If no specific objectives section, look for research goals or aims mentioned in introduction
587
+ 3. If found, list the specific objectives clearly
588
+ 4. If not found, state that objectives could not be located in the provided excerpts
589
+
590
+ Provide a structured response:"""
591
+
592
+ try:
593
+ response = gemini_model.generate_content(prompt)
594
+ return response.text
595
+ except Exception as e:
596
+ raise Exception(f"Gemini API call failed: {str(e)}")
597
+
598
+ def chat_with_research(question, chat_history, year_filter="All", department_filter="All", author_filter="All"):
599
+ if not question.strip():
600
+ return chat_history, ""
601
+
602
+ try:
603
+ # Find relevant research
604
+ relevant_docs = semantic_search(
605
+ question,
606
+ n_results=3,
607
+ year_filter=year_filter,
608
+ department_filter=department_filter,
609
+ author_filter=author_filter
610
+ )
611
+
612
+ if not relevant_docs:
613
+ response = "πŸ” **I couldn't find specific research on this topic.**\n\n"
614
+ response += "**Try:**\nβ€’ Using different keywords\nβ€’ Adjusting the filters\nβ€’ Asking about broader research areas"
615
+
616
+ chat_history.append((question, response))
617
+ return chat_history, ""
618
+
619
+ # === NEW: AI-GENERATED SUMMARY SECTION ===
620
+ if GEMINI_API_KEY and gemini_model:
621
+ try:
622
+ # Prepare context for AI summary
623
+ context = "Research Context:\n"
624
+ for i, doc in enumerate(relevant_docs[:3], 1):
625
+ context += f"\nDocument {i}: {doc['title']} by {doc['author']} ({doc['year']})\n"
626
+ context += f"Content: {doc['content'][:300]}...\n"
627
+
628
+ # Generate AI summary
629
+ summary_prompt = f"""Based on the following research excerpts, provide a concise summary (about 150 words) that directly answers this question: {question}
630
+
631
+ {context}
632
+
633
+ Instructions:
634
+ - Provide a direct, comprehensive answer to the question
635
+ - short in-text citation, APA 7 format
636
+ - Write in a natural, conversational tone
637
+ - Keep it around 150 words
638
+ - Style: Professional academic tone, no references, direct answer only
639
+ - Focus on the key insights from the research"""
640
+
641
+ ai_response = gemini_model.generate_content(summary_prompt)
642
+ summary = ai_response.text
643
+
644
+ # Format the response with summary first, then references
645
+ response = f"**πŸ€– AI Research Summary:**\n\n{summary}\n\n"
646
+ response += "**πŸ“š Source References:**\n"
647
+ for doc in relevant_docs:
648
+ response += f"β€’ {doc['title']} by {doc['author']} ({doc['year']})\n"
649
+
650
+ except Exception as e:
651
+ print(f"AI summary failed: {e}")
652
+ # Fallback to regular response
653
+ response = generate_contextual_response(question, relevant_docs)
654
+ else:
655
+ # Local mode without AI summary
656
+ response = generate_contextual_response(question, relevant_docs)
657
+
658
+ except Exception as e:
659
+ response = f"⚠️ **I encountered a technical issue**\n\nPlease try again or ask a different question.\n\n*Error: {str(e)}*"
660
+
661
+ chat_history.append((question, response))
662
+ return chat_history, ""
663
+
664
+ # Function to get unique values for dropdowns from the collection's metadata
665
+ def get_unique_metadata_values(metadata_field):
666
+ # Get all metadata (be cautious with very large collections)
667
+ all_metadata = collection.get(include=['metadatas'])['metadatas']
668
+ # Extract the specific field, handling missing keys
669
+ values = [meta.get(metadata_field, '') for meta in all_metadata]
670
+ # Get unique, non-empty values and sort them
671
+ unique_values = sorted(list(set(filter(None, values))))
672
+ return ["All"] + unique_values # Add "All" option
673
+
674
+ # Fetch unique values for our filters (run this once when the app starts)
675
+ unique_departments = get_unique_metadata_values('department')
676
+ unique_authors = get_unique_metadata_values('author')
677
+ # For years, we might just want a list of decades or a slider. Using a dropdown for simplicity.
678
+ unique_years = sorted(list(set(meta.get('year', '2000') for meta in collection.get(include=['metadatas'])['metadatas'])))
679
+ unique_years = ["All"] + unique_years
680
+
681
+ def run_advanced_search(query, num_results, year_filter, department_filter, author_filter):
682
+ results = semantic_search(
683
+ query,
684
+ n_results=num_results,
685
+ year_filter=year_filter,
686
+ department_filter=department_filter,
687
+ author_filter=author_filter
688
+ )
689
+
690
+ # Group results by document source
691
+ grouped_results = {}
692
+ for res in results:
693
+ source = res['source']
694
+ if source not in grouped_results:
695
+ grouped_results[source] = {
696
+ 'title': res['title'],
697
+ 'author': res['author'],
698
+ 'year': res['year'],
699
+ 'department': res['department'],
700
+ 'source': res['source'],
701
+ 'chunks': [],
702
+ 'best_relevance': 0.0 # Store as float for comparison
703
+ }
704
+
705
+ grouped_results[source]['chunks'].append(res['content'])
706
+
707
+ # Convert relevance percentage to float for comparison
708
+ current_rel = grouped_results[source]['best_relevance']
709
+ new_rel = float(res['relevance'].strip('%')) / 100 # Convert "85.0%" to 0.85
710
+
711
+ if new_rel > current_rel:
712
+ grouped_results[source]['best_relevance'] = new_rel
713
+
714
+ # Convert back to percentage string for display
715
+ for source in grouped_results:
716
+ grouped_results[source]['relevance'] = f"{grouped_results[source]['best_relevance'] * 100:.1f}%"
717
+
718
+ # Convert to list and sort by relevance
719
+ unique_docs = list(grouped_results.values())
720
+ unique_docs.sort(key=lambda x: x['best_relevance'], reverse=True)
721
+
722
+ # Now generate HTML output
723
+ output_html = """
724
+ <div style='
725
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
726
+ color: #000000 !important;
727
+ line-height: 1.6;
728
+ '>
729
+ """
730
+
731
+ if not unique_docs:
732
+ output_html += "<p style='color: #000000 !important; padding: 2em; text-align: center;'>No results found matching your criteria.</p>"
733
+ return output_html
734
+
735
+ for res in unique_docs:
736
+ # Create preview from chunks
737
+ if res['chunks']:
738
+ preview_text = " ".join([chunk[:200] for chunk in res['chunks'][:2]])
739
+ if len(preview_text) > 400:
740
+ preview_text = preview_text[:400] + "..."
741
+ if len(res['chunks']) > 2:
742
+ preview_text += f" [+{len(res['chunks'])-2} more relevant sections]"
743
+ else:
744
+ preview_text = "No content preview available"
745
+
746
+ cloud_url = ARTICLE_URLS.get(res['source'], "")
747
+
748
+ if cloud_url:
749
+ download_btn = f"""
750
+ <div style='text-align: center; margin: 15px 0;'>
751
+ <a href='{cloud_url}' target='_blank'
752
+ style='display: inline-block; padding: 12px 24px; background: #28a745; color: white; text-decoration: none; border-radius: 6px; font-weight: bold;'>
753
+ πŸ“₯ Download PDF
754
+ </a>
755
+ </div>
756
+ """
757
+ else:
758
+ download_btn = """
759
+ <div style='text-align: center; margin: 15px 0; padding: 10px; background: #ffe6e6; border-radius: 5px;'>
760
+ <p style='color: #d63031; margin: 0;'>⚠️ Download not available</p>
761
+ </div>
762
+ """
763
+
764
+ output_html += f"""
765
+ <div style='margin-bottom: 2em; padding: 1.5em; border: 2px solid #e0e0e0; border-radius: 10px; background: #ffffff;'>
766
+ <h3 style='margin-top: 0; margin-bottom: 1em; color: #000000 !important; font-size: 1.4em; padding-bottom: 0.5em; border-bottom: 3px solid #3498db;'>{res['title']}</h3>
767
+
768
+ <div style='display: grid; grid-template-columns: auto 1fr; gap: 0.5em 1em; margin-bottom: 1.5em; padding: 1em; background: #f8f9fa; border-radius: 8px;'>
769
+ <span style='font-weight: bold; color: #000000 !important;'>πŸ‘€ Author:</span>
770
+ <span style='color: #000000 !important;'>{res['author']}</span>
771
+
772
+ <span style='font-weight: bold; color: #000000 !important;'>πŸ“… Year:</span>
773
+ <span style='color: #000000 !important;'>{res['year']}</span>
774
+
775
+ <span style='font-weight: bold; color: #000000 !important;'>🏫 Department:</span>
776
+ <span style='color: #000000 !important;'>{res['department']}</span>
777
+ </div>
778
+
779
+ {download_btn}
780
+
781
+ <div style='background: #e8f4fc; padding: 1em; border-radius: 8px; margin-bottom: 1em; text-align: center;'>
782
+ <span style='color: #e74c3c !important; font-weight: bold; font-size: 1.1em;'>🎯 Relevance: {res['relevance']}</span>
783
+ </div>
784
+
785
+ <div style='background: #f8f9fa; padding: 1.2em; border-radius: 8px;'>
786
+ <h4 style='margin-top: 0; color: #000000 !important; margin-bottom: 0.5em;'>πŸ“– Preview:</h4>
787
+ <p style='margin: 0; line-height: 1.6; color: #000000 !important;'>{preview_text}</p>
788
+ </div>
789
+ </div>
790
+ """
791
+
792
+ output_html += "</div>"
793
+ return output_html
794
+
795
+ # Create the advanced interface with dropdowns
796
+ iface = gr.Interface(
797
+ fn=run_advanced_search,
798
+ inputs=[
799
+ gr.Textbox(label="Your Research Question", placeholder="e.g., impact of climate change on agriculture..."),
800
+ gr.Dropdown(choices=unique_years, label="Published After Year", value="All"),
801
+ gr.Dropdown(choices=unique_departments, label="Department", value="All"),
802
+ gr.Dropdown(choices=unique_authors, label="Author", value="All")
803
+ ],
804
+ outputs=gr.HTML(label="Filtered Search Results"),
805
+ title="πŸ›οΈ Mount Kenya University - Advanced Library Search",
806
+ description="Find relevant resources using semantic search. Filter by year, department, or author to narrow down results."
807
+ )
808
+
809
+ # Create the advanced interface with dropdowns AND result count control
810
+ iface = gr.Interface(
811
+ fn=run_advanced_search,
812
+ inputs=[
813
+ gr.Textbox(label="Your Research Question", placeholder="e.g., impact of climate change on agriculture..."),
814
+ gr.Slider(minimum=1, maximum=50, value=10, step=1, label="Number of Results"), # Slider option
815
+ # gr.Number(value=10, label="Number of Results", precision=0), # Number input option
816
+ gr.Dropdown(choices=unique_years, label="Published After Year", value="All"),
817
+ gr.Dropdown(choices=unique_departments, label="Department", value="All"),
818
+ gr.Dropdown(choices=unique_authors, label="Author", value="All")
819
+ ],
820
+ outputs=gr.HTML(label="Filtered Search Results"),
821
+ title="πŸ›οΈ Mount Kenya University - Advanced Library Search",
822
+ description="Find relevant resources using semantic search. Choose how many results to see and filter by year, department, or author."
823
+ )
824
+
825
+ # --- NEW: Create Tabbed Interface ---
826
+ with gr.Blocks(title="MKU Smart Library Search", theme=gr.themes.Default()) as demo:
827
+ gr.Markdown("# πŸ›οΈ Mount Kenya University - Smart Library Search")
828
+ gr.Markdown("Explore our academic resources through semantic search or chat with our research database.")
829
+
830
+ file_download = gr.File(visible=False, label="Download PDF")
831
+
832
+ # ====== HIDDEN COMPONENTS FOR DOCUMENT TRACKING ======
833
+ current_pdf_title = gr.Textbox(value="", visible=False)
834
+ current_pdf_filename = gr.Textbox(value="", visible=False)
835
+ # ====== END HIDDEN COMPONENTS ======
836
+
837
+
838
+ # Tab 1: Your Existing Semantic Search
839
+ with gr.Tab("πŸ” Semantic Search"):
840
+ gr.Markdown("### Search our library collection with advanced filters")
841
+ with gr.Row():
842
+ with gr.Column():
843
+ search_query = gr.Textbox(label="Your Research Question", placeholder="e.g., impact of climate change on agriculture...")
844
+ num_results = gr.Slider(minimum=1, maximum=50, value=10, step=1, label="Number of Results")
845
+ year_filter = gr.Dropdown(choices=unique_years, label="Published After Year", value="All")
846
+ department_filter = gr.Dropdown(choices=unique_departments, label="Department", value="All")
847
+ author_filter = gr.Dropdown(choices=unique_authors, label="Author", value="All")
848
+ search_btn = gr.Button("Search", variant="primary")
849
+
850
+ with gr.Column():
851
+ search_output = gr.HTML(label="Search Results")
852
+
853
+ # Connect your existing function
854
+ search_btn.click(
855
+ fn=run_advanced_search,
856
+ inputs=[search_query, num_results, year_filter, department_filter, author_filter],
857
+ outputs=search_output
858
+ )
859
+
860
+ #tab2 chat interface
861
+ with gr.Tab("πŸ’¬ Chat with Research"):
862
+ gr.Markdown("## πŸ€– Research Discussion Assistant")
863
+
864
+ # === CHAT INTERFACE ===
865
+ with gr.Row():
866
+ with gr.Column(scale=3):
867
+ chatbot = gr.Chatbot(
868
+ label="Research Conversation",
869
+ height=400,
870
+ value=[
871
+ ("πŸ‘‹", "Hello! I can help you explore research papers.")
872
+ ]
873
+ )
874
+
875
+ with gr.Column(scale=1):
876
+ gr.Markdown("### πŸ’‘ Chat Tips")
877
+ gr.Markdown("""
878
+ **Use filters to:**
879
+ - Focus on recent research
880
+ - Explore specific departments
881
+ - Find authors' work
882
+ - Narrow down results
883
+ """)
884
+
885
+ # === MESSAGE INPUT ===
886
+ with gr.Row():
887
+ msg = gr.Textbox(
888
+ label="Your research question",
889
+ placeholder="e.g., 'What are recent findings about AI in education?'",
890
+ scale=4
891
+ )
892
+ submit_btn = gr.Button("Send", variant="primary", scale=1)
893
+
894
+ clear_btn = gr.Button("πŸ”„ Clear Conversation")
895
+
896
+ # === EVENT HANDLERS ===
897
+ submit_btn.click(
898
+ chat_with_research,
899
+ inputs=[msg, chatbot, year_filter, department_filter, author_filter],
900
+ outputs=[chatbot, msg]
901
+ )
902
+
903
+ msg.submit(
904
+ chat_with_research,
905
+ inputs=[msg, chatbot, year_filter, department_filter, author_filter],
906
+ outputs=[chatbot, msg]
907
+ )
908
+
909
+ clear_btn.click(lambda: [], None, chatbot)
910
+
911
+
912
+
913
+ if __name__ == "__main__":
914
+ # Display configuration status
915
+ if GEMINI_API_KEY:
916
+ print("βœ… Hugging Face API enabled")
917
+ else:
918
+ print("πŸ”Ά Local mode - API token not set")
919
+ print("πŸ’‘ Get token: https://huggingface.co/settings/tokens")
920
+
921
+ demo.launch(
922
+ share=True,
923
+ server_name="0.0.0.0",
924
+ server_port=7861
925
+ )