wassim2433 commited on
Commit
fed9d9d
·
1 Parent(s): 1516699
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.pdf filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,12 +1,17 @@
1
  ---
2
- title: RAG1
3
- emoji: 🚀
4
- colorFrom: pink
5
- colorTo: gray
6
  sdk: gradio
7
- sdk_version: 6.13.0
8
  app_file: app.py
9
  pinned: false
 
 
 
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: RAG
3
+ emoji: 💬
4
+ colorFrom: yellow
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 6.5.1
8
  app_file: app.py
9
  pinned: false
10
+ hf_oauth: true
11
+ hf_oauth_scopes:
12
+ - inference-api
13
+ license: apache-2.0
14
+ short_description: 'RAG-Palestine '
15
  ---
16
 
17
+ An example chatbot using [Gradio](https://gradio.app), [`huggingface_hub`](https://huggingface.co/docs/huggingface_hub/v0.22.2/en/index), and the [Hugging Face Inference API](https://huggingface.co/docs/api-inference/index).
__pycache__/app.cpython-313.pyc ADDED
Binary file (20.7 kB). View file
 
__pycache__/config.cpython-313.pyc ADDED
Binary file (1.03 kB). View file
 
__pycache__/document_processor.cpython-313.pyc ADDED
Binary file (4.38 kB). View file
 
__pycache__/extensions.cpython-313.pyc ADDED
Binary file (12.6 kB). View file
 
__pycache__/rag_pipeline.cpython-313.pyc ADDED
Binary file (14.9 kB). View file
 
app.py ADDED
@@ -0,0 +1,429 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import json
3
+ import os
4
+ from rag_pipeline import (
5
+ initialise_pipeline,
6
+ add_pdf_to_index,
7
+ query_rag,
8
+ summarise_document,
9
+ compare_documents,
10
+ analyse_discourse
11
+ )
12
+ from extensions import (
13
+ generate_map,
14
+ generate_timeline,
15
+ generate_wordcloud,
16
+ get_statistics,
17
+ advanced_analytics,
18
+ text_to_speech,
19
+ speech_to_text,
20
+ translate_text,
21
+ export_chat_history
22
+ )
23
+
24
+ # Initialize the pipeline
25
+ initialise_pipeline()
26
+
27
+ MODELS = [
28
+ "gpt-oss-120b",
29
+ "google/gemma-4-31B",
30
+ "openrouter/auto"
31
+ ]
32
+
33
+ def get_document_list():
34
+ """Read document filenames from disk — always fresh, supports Unicode/Arabic names."""
35
+ docs_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "documents")
36
+ if not os.path.exists(docs_dir):
37
+ return []
38
+ return sorted(
39
+ [f for f in os.listdir(docs_dir) if f.lower().endswith(".pdf")],
40
+ key=lambda x: x.lower()
41
+ )
42
+
43
+ def refresh_doc_dropdowns():
44
+ """Return gr.update() calls for all document dropdowns."""
45
+ docs = get_document_list()
46
+ return (
47
+ gr.update(choices=docs),
48
+ gr.update(choices=docs),
49
+ gr.update(choices=docs),
50
+ gr.update(choices=["All"] + docs),
51
+ )
52
+
53
+ def respond_advanced(message, audio_path, history, model_id):
54
+ if audio_path and not message:
55
+ message = speech_to_text(audio_path)
56
+
57
+ if not message:
58
+ return "", history
59
+
60
+ answer, sources = query_rag(message, model_id=model_id)
61
+
62
+ if sources:
63
+ source_text = "\n\n**Sources:**\n"
64
+ for i, s in enumerate(sources):
65
+ source_text += f"- **{s['source']}** (Page {s['page']}, Score: {s['score']})\n"
66
+ answer += source_text
67
+
68
+ history.append({"role": "user", "content": message})
69
+ history.append({"role": "assistant", "content": answer})
70
+ return "", history
71
+
72
+ def clear_chat():
73
+ return []
74
+
75
+ def toggle_translation(history):
76
+ if not history:
77
+ return history
78
+ last_msg = history[-1]
79
+ last_bot_raw = last_msg["content"] if isinstance(last_msg, dict) else getattr(last_msg, "content", "")
80
+
81
+ # In Gradio 5.x, content can sometimes be a tuple or list for multimodal messages.
82
+ if isinstance(last_bot_raw, (list, tuple)):
83
+ last_bot = last_bot_raw[0] if len(last_bot_raw) > 0 else ""
84
+ if isinstance(last_bot, dict) and "text" in last_bot:
85
+ last_bot = last_bot["text"]
86
+ elif hasattr(last_bot, "text"):
87
+ last_bot = last_bot.text
88
+ else:
89
+ last_bot = str(last_bot_raw)
90
+
91
+ # Separate the answer from the sources to avoid translating metadata and hitting 5000 char limits
92
+ parts = last_bot.split("\n\n**Sources:**\n")
93
+ main_text = parts[0]
94
+ sources_text = "\n\n**Sources:**\n" + parts[1] if len(parts) > 1 else ""
95
+
96
+ has_arabic = any("\u0600" <= c <= "\u06FF" for c in main_text)
97
+ target_lang = 'en' if has_arabic else 'ar'
98
+
99
+ # Translate only the main text
100
+ translated_main = translate_text(main_text, target_lang=target_lang)
101
+ translated_full = translated_main + sources_text
102
+
103
+ if isinstance(last_msg, dict):
104
+ history[-1]["content"] = translated_full
105
+ else:
106
+ history[-1].content = translated_full
107
+
108
+ return list(history)
109
+
110
+ def get_audio_for_last_response(history):
111
+ if not history:
112
+ return None
113
+ last_msg = history[-1]
114
+ last_bot_raw = last_msg["content"] if isinstance(last_msg, dict) else getattr(last_msg, "content", "")
115
+
116
+ if isinstance(last_bot_raw, (list, tuple)):
117
+ last_bot = last_bot_raw[0] if len(last_bot_raw) > 0 else ""
118
+ if isinstance(last_bot, dict) and "text" in last_bot:
119
+ last_bot = last_bot["text"]
120
+ elif hasattr(last_bot, "text"):
121
+ last_bot = last_bot.text
122
+ else:
123
+ last_bot = str(last_bot_raw)
124
+
125
+ # Safely strip out sources block
126
+ main_text = last_bot.split("\n\n**Sources:**\n")[0]
127
+ clean_text = main_text.replace('*', '').replace('#', '')
128
+
129
+ has_arabic = any("\u0600" <= c <= "\u06FF" for c in clean_text)
130
+ lang = 'ar' if has_arabic else 'en'
131
+ return text_to_speech(clean_text, lang=lang)
132
+
133
+ PALESTINE_CSS = """
134
+ /* Palestinian Theme */
135
+ @import url('https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700&family=Inter:wght@400;500;600&display=swap');
136
+
137
+ /* Keffiyeh/Checker Background */
138
+ body, .gradio-container, .main, .wrap {
139
+ background-color: #0B120E !important;
140
+ background-image: url("data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 20 L20 0 L40 20 L20 40 Z' fill='none' stroke='%23ffffff' stroke-opacity='0.07' stroke-width='1'/%3E%3C/svg%3E") !important;
141
+ background-repeat: repeat !important;
142
+ background-attachment: fixed !important;
143
+ font-family: 'Inter', 'Cairo', sans-serif !important;
144
+ color: #e8f5e8 !important;
145
+ }
146
+
147
+ /* Palestinian Flag Top Border */
148
+ .gradio-container {
149
+ border-top: 12px solid !important;
150
+ border-image: linear-gradient(to right, #000000 33%, #FFFFFF 33%, #FFFFFF 66%, #007A3D 66%) 1 !important;
151
+ }
152
+
153
+ /* Chatbot Container */
154
+ #pali-chatbot {
155
+ background: rgba(18, 28, 22, 0.7) !important;
156
+ border: 2px solid #007A3D !important;
157
+ }
158
+
159
+ /* Tabs */
160
+ .tab-nav button {
161
+ background: #0B120E !important;
162
+ color: #9dc99d !important;
163
+ border-bottom: 2px solid transparent !important;
164
+ font-weight: 500;
165
+ transition: all 0.2s ease;
166
+ }
167
+ .tab-nav button.selected {
168
+ color: #ffffff !important;
169
+ border-bottom: 3px solid #007A3D !important;
170
+ background: rgba(0,122,61,0.12) !important;
171
+ }
172
+ .tab-nav button:hover {
173
+ color: #ffffff !important;
174
+ background: rgba(0,122,61,0.08) !important;
175
+ }
176
+ .tab-nav {
177
+ border-bottom: 2px solid #CE1126 !important;
178
+ }
179
+
180
+ /* Primary Buttons */
181
+ button.primary, .gr-button-primary, button[variant="primary"] {
182
+ background: linear-gradient(135deg, #007A3D, #009e50) !important;
183
+ border: none !important;
184
+ color: white !important;
185
+ font-weight: 600;
186
+ transition: opacity 0.2s;
187
+ }
188
+ button.primary:hover, button[variant="primary"]:hover { opacity: 0.88; }
189
+
190
+ /* Secondary Buttons */
191
+ button.secondary, .gr-button-secondary, button[variant="secondary"] {
192
+ background: #0a0f0a !important;
193
+ border: 1px solid #007A3D !important;
194
+ color: #7ecf7e !important;
195
+ }
196
+
197
+ /* Inputs, Textareas, and Dropdowns */
198
+ textarea, input[type=text], input[type=search], select, div.wrap-inner, div[role="listbox"], div[role="combobox"] {
199
+ background-color: #000000 !important;
200
+ border: 1px solid #A49966 !important;
201
+ color: #ffffff !important;
202
+ caret-color: #007A3D;
203
+ }
204
+ textarea:focus, input[type=text]:focus, input[type=search]:focus {
205
+ border-color: #007A3D !important;
206
+ outline: none !important;
207
+ box-shadow: 0 0 0 2px rgba(0,122,61,0.25) !important;
208
+ }
209
+
210
+ /* Chatbot Message Bubbles */
211
+ .message.user { background: rgba(0,122,61,0.20) !important; }
212
+ .message.bot { background: rgba(30,61,30,0.50) !important; }
213
+
214
+ /* Labels */
215
+ label span, .gr-form label, label > span {
216
+ color: #9dc99d !important;
217
+ font-weight: 600;
218
+ font-size: 0.85rem;
219
+ letter-spacing: 0.03em;
220
+ }
221
+
222
+ /* Dataframes */
223
+ .dataframe th { background: #007A3D !important; color: white !important; }
224
+ .dataframe tr:nth-child(even) { background: #0f1a10 !important; }
225
+ .dataframe tr:nth-child(odd) { background: #111c12 !important; }
226
+
227
+ /* Scrollbars */
228
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
229
+ ::-webkit-scrollbar-track { background: #0B120E; }
230
+ ::-webkit-scrollbar-thumb { background: #007A3D; border-radius: 3px; }
231
+ ::-webkit-scrollbar-thumb:hover { background: #009e50; }
232
+
233
+ /* Title glow */
234
+ h1 { text-shadow: 0 0 20px rgba(0,122,61,0.4); }
235
+
236
+ footer { visibility: hidden !important; }
237
+ """
238
+
239
+ PROFILE_PIC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "profile_picture.png")
240
+
241
+ with gr.Blocks(title="Palestinian Agentic RAG Platform") as demo:
242
+ gr.Markdown(
243
+ """
244
+ <div style="display:flex;align-items:center;gap:16px;padding:12px 0;">
245
+ <img src="/file=profile_picture.png" style="width:70px;height:70px;border-radius:50%;border:3px solid #007A3D;" />
246
+ <div>
247
+ <h1 style="margin:0;font-size:1.6rem;color:#ffffff;font-family:'Cairo',sans-serif;"> AI for a Free Palestine </h1>
248
+ <p style="margin:0;color:#9dc99d;font-size:0.9rem;"> Ask . Learn . Understand — in support of a free Palestine.</p>
249
+ </div>
250
+ </div>
251
+ """
252
+ )
253
+
254
+ with gr.Tabs():
255
+ # 1. SMART CHAT
256
+ with gr.TabItem("💬 Smart Chat"):
257
+ model_chat = gr.Dropdown(choices=MODELS, value="gpt-oss-120b", label="Select Model")
258
+ chatbot = gr.Chatbot(
259
+ label="Agentic RAG Chatbot",
260
+ height=520,
261
+ avatar_images=(None, PROFILE_PIC),
262
+ elem_id="pali-chatbot",
263
+ )
264
+ with gr.Row():
265
+ with gr.Column(scale=8):
266
+ msg = gr.Textbox(label="Type your question here...", placeholder="What is the history of the Balfour Declaration?")
267
+ with gr.Column(scale=1):
268
+ audio_in = gr.Audio(sources=["microphone", "upload"], type="filepath", label="Voice Input")
269
+
270
+ with gr.Row():
271
+ submit_btn = gr.Button("Submit", variant="primary")
272
+ clear_btn = gr.Button("Clear Chat")
273
+
274
+ with gr.Row():
275
+ translate_btn = gr.Button("🔄 Auto-Translate Last Answer")
276
+ speak_btn = gr.Button("🔊 Speak Last Answer")
277
+ export_btn = gr.Button("💾 Export Chat")
278
+
279
+ audio_out = gr.Audio(label="Voice Output", interactive=False)
280
+ export_file = gr.File(label="Download Chat Export", interactive=False)
281
+
282
+ submit_btn.click(respond_advanced, inputs=[msg, audio_in, chatbot, model_chat], outputs=[msg, chatbot])
283
+ msg.submit(respond_advanced, inputs=[msg, audio_in, chatbot, model_chat], outputs=[msg, chatbot])
284
+ clear_btn.click(clear_chat, outputs=chatbot)
285
+
286
+ translate_btn.click(toggle_translation, inputs=chatbot, outputs=chatbot)
287
+ speak_btn.click(get_audio_for_last_response, inputs=chatbot, outputs=audio_out)
288
+ export_btn.click(export_chat_history, inputs=chatbot, outputs=export_file)
289
+
290
+ # 2. DISCOURSE ANALYSIS
291
+ with gr.TabItem("🔍 Discourse Analysis"):
292
+ gr.Markdown("### Detect bias, framing, and text orientation")
293
+ topic_input = gr.Textbox(label="Topic or Text to Analyze")
294
+ model_discourse = gr.Dropdown(choices=MODELS, value="gpt-oss-120b", label="Select Model")
295
+ analyse_btn = gr.Button("Analyze Discourse")
296
+ analyse_output = gr.Textbox(label="Discourse Analysis Report", lines=15)
297
+ analyse_btn.click(fn=analyse_discourse, inputs=[topic_input, model_discourse], outputs=analyse_output)
298
+
299
+ # 3. COMPARE DOCUMENTS
300
+ with gr.TabItem("⚖️ Compare Documents"):
301
+ gr.Markdown("### Side-by-side comparison of two documents or topics")
302
+ with gr.Row():
303
+ doc1_input = gr.Dropdown(choices=get_document_list(), label="Document 1", allow_custom_value=True)
304
+ doc2_input = gr.Dropdown(choices=get_document_list(), label="Document 2", allow_custom_value=True)
305
+ aspect_input = gr.Textbox(label="Aspect to compare", value="Main themes and biases")
306
+ model_compare_docs = gr.Dropdown(choices=MODELS, value="gpt-oss-120b", label="Select Model")
307
+ compare_btn = gr.Button("Compare")
308
+ compare_output = gr.Textbox(label="Comparison Result", lines=15)
309
+ compare_btn.click(fn=compare_documents, inputs=[doc1_input, doc2_input, aspect_input, model_compare_docs], outputs=compare_output)
310
+
311
+ # 4. DOCUMENT SUMMARY
312
+ with gr.TabItem("📝 Document Summary"):
313
+ gr.Markdown("### Auto-summarize any document")
314
+ doc_name_input = gr.Dropdown(choices=get_document_list(), label="Select Document", allow_custom_value=True)
315
+ model_summary = gr.Dropdown(choices=MODELS, value="gpt-oss-120b", label="Select Model")
316
+ summarise_btn = gr.Button("Generate Summary")
317
+ summarise_output = gr.Textbox(label="Summary", lines=10)
318
+ summarise_btn.click(fn=summarise_document, inputs=[doc_name_input, model_summary], outputs=summarise_output)
319
+
320
+ # 5. INTERACTIVE MAP
321
+ with gr.TabItem("🗺️ Interactive Map"):
322
+ gr.Markdown("### Palestinian historical locations and context")
323
+ map_btn = gr.Button("Load Interactive Map")
324
+ map_html = gr.HTML(label="Map View")
325
+
326
+ def load_map_iframe():
327
+ path = generate_map()
328
+ with open(path, "r", encoding="utf-8") as f:
329
+ html_data = f.read()
330
+ escaped_html = html_data.replace('"', '&quot;')
331
+ return f'<iframe srcdoc="{escaped_html}" width="100%" height="600px" style="border:none;"></iframe>'
332
+
333
+ map_btn.click(load_map_iframe, outputs=map_html)
334
+
335
+ # 6. HISTORICAL TIMELINE
336
+ with gr.TabItem("⏳ Historical Timeline"):
337
+ gr.Markdown("### Key Events Timeline")
338
+ timeline_html = gr.HTML(value=generate_timeline())
339
+
340
+ # 7. WORD CLOUD
341
+ with gr.TabItem("☁️ Word Cloud"):
342
+ gr.Markdown("### Word frequency visualization")
343
+ wc_doc_name = gr.Dropdown(choices=["All"] + get_document_list(), value="All", label="Select Document (or 'All' for entire corpus)", allow_custom_value=True)
344
+ wc_btn = gr.Button("Generate Word Cloud")
345
+ wc_img = gr.Image(label="Word Cloud")
346
+ wc_btn.click(fn=generate_wordcloud, inputs=wc_doc_name, outputs=wc_img)
347
+
348
+ # 8. STATISTICS
349
+ with gr.TabItem("📊 Statistics"):
350
+ gr.Markdown("### Corpus metrics and distribution")
351
+ stats_btn = gr.Button("Refresh Statistics")
352
+ with gr.Row():
353
+ stats_summary = gr.Dataframe(label="Document Summaries")
354
+ stats_raw = gr.Dataframe(label="Raw Chunks")
355
+ stats_btn.click(fn=get_statistics, outputs=[stats_summary, stats_raw])
356
+
357
+ # 9. UPLOAD PDF
358
+ with gr.TabItem("📄 Upload PDF"):
359
+ gr.Markdown("### Add a new PDF to the index instantly")
360
+ file_input = gr.File(label="Upload PDF", file_types=[".pdf"])
361
+ with gr.Row():
362
+ upload_btn = gr.Button("Add to Index", variant="primary")
363
+ refresh_btn = gr.Button("🔄 Refresh Document Lists")
364
+ upload_output = gr.Textbox(label="Status")
365
+
366
+ def upload_and_refresh(file):
367
+ status = add_pdf_to_index(file)
368
+ d1, d2, ds, wc = refresh_doc_dropdowns()
369
+ return status, d1, d2, ds, wc
370
+
371
+ upload_btn.click(
372
+ fn=upload_and_refresh,
373
+ inputs=file_input,
374
+ outputs=[upload_output, doc1_input, doc2_input, doc_name_input, wc_doc_name]
375
+ )
376
+ refresh_btn.click(
377
+ fn=refresh_doc_dropdowns,
378
+ outputs=[doc1_input, doc2_input, doc_name_input, wc_doc_name]
379
+ )
380
+
381
+ # 10. ADVANCED ANALYTICS
382
+ with gr.TabItem("📈 Advanced Analytics"):
383
+ gr.Markdown("### Sentiment trends and entity frequencies")
384
+ analytics_input = gr.Textbox(label="Text to Analyze (Paste text or query)")
385
+ analytics_btn = gr.Button("Run Analytics")
386
+ analytics_output = gr.Markdown(label="Analytics Report")
387
+ analytics_btn.click(fn=advanced_analytics, inputs=analytics_input, outputs=analytics_output)
388
+
389
+ # 11. MULTI-MODEL COMPARISON
390
+ with gr.TabItem("🤖 Multi-Model Comparison"):
391
+ gr.Markdown("### Compare LLM Outputs (AI Grid vs OpenRouter)")
392
+ mm_query = gr.Textbox(label="Query")
393
+ with gr.Row():
394
+ m1_dropdown = gr.Dropdown(choices=MODELS, value="gpt-oss-120b", label="Primary Model")
395
+ m2_dropdown = gr.Dropdown(choices=MODELS, value="google/gemma-4-31B", label="Secondary Model")
396
+ mm_btn = gr.Button("Compare")
397
+ with gr.Row():
398
+ mm_out1 = gr.Textbox(label="Primary Output", lines=10)
399
+ mm_out2 = gr.Textbox(label="Secondary Output", lines=10)
400
+
401
+ def compare_models(query, m1, m2):
402
+ ans1, _ = query_rag(query, model_id=m1)
403
+ ans2, _ = query_rag(query, model_id=m2)
404
+ return ans1, ans2
405
+
406
+ mm_btn.click(fn=compare_models, inputs=[mm_query, m1_dropdown, m2_dropdown], outputs=[mm_out1, mm_out2])
407
+
408
+ # 12. ABOUT
409
+ with gr.TabItem("ℹ️ About"):
410
+ gr.Markdown(
411
+ """
412
+ ### About the Project
413
+ This Agentic RAG Platform is exclusively built to answer questions regarding the Palestinian cause based entirely on 15 official, verified documents.
414
+
415
+ **Methodology:**
416
+ - Uses `LlamaIndex` for retrieval-augmented generation.
417
+ - Strict system prompts to prevent hallucination and reliance on external knowledge.
418
+ - Employs `intfloat/multilingual-e5-base` for dense multilingual embeddings.
419
+ - Supports AI Grid and OpenRouter models.
420
+
421
+ **Features:**
422
+ - Multi-lingual Smart Chat with exact citations.
423
+ - Map, Timeline, Word Cloud, and Statistical visualization.
424
+ - Discourse & Sentiment Analysis.
425
+ """
426
+ )
427
+
428
+ if __name__ == "__main__":
429
+ demo.launch(css=PALESTINE_CSS)
config.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ # ── API Keys ──────────────────────────────────────────────
4
+ AIGRID_API_KEY_GPT = os.getenv("AIGRID_API_KEY_GPT", "sk-XaDrxkmJNvrp04SfkHT2ig")
5
+ AIGRID_API_KEY_GEMMA = os.getenv("AIGRID_API_KEY_GEMMA", "sk-eQYZ67KgWjIMcPZb6SwwKg")
6
+ OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "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")
7
+
8
+ # ── API Bases ─────────────────────────────────────────────
9
+ AIGRID_API_BASE = "http://app.ai-grid.io:4000/v1"
10
+ OPENROUTER_API_BASE = "https://openrouter.ai/api/v1"
11
+
12
+ # ── Model Settings ────────────────────────────────────────
13
+ LLM_MODEL = "gpt-oss-120b"
14
+ # EMBEDDING_MODEL = "intfloat/multilingual-e5-base"
15
+ EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
16
+ LLM_TEMPERATURE = 0.1 # Low = less hallucination
17
+ MAX_TOKENS = 1024
18
+
19
+ # ── Chunking Settings ─────────────────────────────────────
20
+ CHUNK_SIZE = 500
21
+ CHUNK_OVERLAP = 100
22
+
23
+ # ── Retrieval Settings ────────────────────────────────────
24
+ TOP_K = 5
25
+ SIMILARITY_CUTOFF = 0.3 # Below this = "not found"
26
+
27
+ # ── Paths ─────────────────────────────────────────────────
28
+ DOCUMENTS_DIR = "./documents"
29
+ FAISS_INDEX_PATH = "/data/faiss_index"
document_processor.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import fitz # PyMuPDF
3
+ from langdetect import detect
4
+ from config import DOCUMENTS_DIR, CHUNK_SIZE, CHUNK_OVERLAP
5
+
6
+
7
+ # ── Detect Language ───────────────────────────────────────────────────────────
8
+ def detect_language(text: str) -> str:
9
+ try:
10
+ lang = detect(text[:500])
11
+ return "ar" if lang == "ar" else "en"
12
+ except Exception:
13
+ return "en"
14
+
15
+
16
+ # ── Load a Single PDF ─────────────────────────────────────────────────────────
17
+ def load_pdf(pdf_path: str) -> list[dict]:
18
+ """
19
+ Returns a list of page dicts:
20
+ { text, page_number, source, language }
21
+ """
22
+ pages = []
23
+ doc_name = os.path.splitext(os.path.basename(pdf_path))[0]
24
+
25
+ try:
26
+ doc = fitz.open(pdf_path)
27
+ for i, page in enumerate(doc):
28
+ text = page.get_text().strip()
29
+ if not text: # skip empty pages
30
+ continue
31
+ pages.append({
32
+ "text" : text,
33
+ "page_number": i + 1,
34
+ "source" : doc_name,
35
+ "language" : detect_language(text),
36
+ })
37
+ doc.close()
38
+ except Exception as e:
39
+ print(f"[ERROR] Could not load {pdf_path}: {e}")
40
+
41
+ return pages
42
+
43
+
44
+ # ── Chunk a List of Pages ─────────────────────────────────────────────────────
45
+ def chunk_pages(pages: list[dict]) -> list[dict]:
46
+ """
47
+ Splits page text into overlapping chunks.
48
+ Each chunk keeps the source metadata.
49
+ """
50
+ chunks = []
51
+
52
+ for page in pages:
53
+ text = page["text"]
54
+ words = text.split()
55
+ start = 0
56
+
57
+ while start < len(words):
58
+ end = start + CHUNK_SIZE
59
+ chunk_text = " ".join(words[start:end])
60
+
61
+ chunks.append({
62
+ "text" : chunk_text,
63
+ "page_number": page["page_number"],
64
+ "source" : page["source"],
65
+ "language" : page["language"],
66
+ })
67
+
68
+ start += CHUNK_SIZE - CHUNK_OVERLAP # overlap
69
+
70
+ return chunks
71
+
72
+
73
+ # ── Load ALL PDFs in the documents/ folder ───────────────────────────────────
74
+ def load_all_documents() -> list[dict]:
75
+ all_chunks = []
76
+
77
+ if not os.path.exists(DOCUMENTS_DIR):
78
+ os.makedirs(DOCUMENTS_DIR)
79
+ print(f"[INFO] Created '{DOCUMENTS_DIR}' — add your PDFs there.")
80
+ return all_chunks
81
+
82
+ pdf_files = [
83
+ f for f in os.listdir(DOCUMENTS_DIR)
84
+ if f.lower().endswith(".pdf")
85
+ ]
86
+
87
+ if not pdf_files:
88
+ print(f"[WARN] No PDFs found in '{DOCUMENTS_DIR}'.")
89
+ return all_chunks
90
+
91
+ for pdf_file in pdf_files:
92
+ path = os.path.join(DOCUMENTS_DIR, pdf_file)
93
+ pages = load_pdf(path)
94
+ chunks = chunk_pages(pages)
95
+ all_chunks.extend(chunks)
96
+ print(f"[INFO] Loaded '{pdf_file}' → {len(chunks)} chunks")
97
+
98
+ print(f"[INFO] Total chunks: {len(all_chunks)}")
99
+ return all_chunks
100
+
101
+
102
+ # ── Load a Single Uploaded PDF (for the Upload Tab) ──────────────────────────
103
+ def load_uploaded_pdf(pdf_path: str) -> list[dict]:
104
+ pages = load_pdf(pdf_path)
105
+ chunks = chunk_pages(pages)
106
+ print(f"[INFO] Uploaded PDF → {len(chunks)} chunks")
107
+ return chunks
documents/20241106-Gaza-Update-Report-OPT.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ca826df600cf7a276355416eb5b0f5ec38b37f31f0173ce5d97124b81ecd263a
3
+ size 1766016
documents/2024_04_20_UNRWA-final-technical_report.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:704638634b5678058fcec8bddab10a2b00f46cc120e099e0d6856a67e241f68e
3
+ size 435406
documents/Humanitarian-Situation-Update-176-_-Gaza-Strip-_-United-Nations-Office-for-the-Coordination-of-Humanitarian-Affairs-occupied-Palestinian-territory.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c6c6086889da21082a49b31fdbbd162af2677b953c71672b13cf966c980e8f56
3
+ size 307785
documents/Israel-Palestine-History-Timeline-2024-25-update.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:55300e2f63012ef8cbac8f4af81e5bd61360dd6e55d2efac45d8e0f67bbac2d7
3
+ size 1466687
documents/Khalidi-Rashid-Palestinian-Identity.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1a544e1b992ca4fc4298fe7d4ee082eb3b6acb12873f6f2983851774539e3bc8
3
+ size 10389182
documents/Palestinian-History-Calendar.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f760914b73636e9d1b609295b46963b1d73a9a35fb187d0b59349caaee36e026
3
+ size 750558
documents/The Hundred Years’ War on Palestine.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:af5e7b82c534dc20fedf4f2d4ab106927b710615a4dcbc5935e4e9b65695b738
3
+ size 2734756
documents/ga_res_1941948.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:55c90a3a1124692a6fe9e7cbbac41730b7eae9ed749fe5e61f767a650a6d6d41
3
+ size 27740
documents/تقرير غزة الإنساني 2024.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:df98cf9c8662ea534c9166200f7b26d4144958fd606605857fdf9b6a730579dc
3
+ size 137750
documents/ذاكرة المكان.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c04d2e13d7dde437a00d2252f3f09e58ef3bd31d18124a901820422a89ee4f07
3
+ size 718225
documents/شخصيات فلسطينية.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:94de96d73ff18a7b774f185e1e11b12ec2802b006bfaad050f4a92ae1480bc3b
3
+ size 189648
documents/فلسطين العربية.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1013f41ba06a1a6361e51ad36e494f685c07b5ec6ce91014b73947006c3a3f06
3
+ size 2989231
documents/فلسطين.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3e13ec4a75f910c406a9479996a4f75dd60f43a4310e63ab1f11aa2a780c07e2
3
+ size 3516274
documents/كتاب-النخبة-1-.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b45c816283ff0382827e6f73d00a6e0d31d19b955ef8197d6cb3a42dc4321162
3
+ size 7813088
documents/كتاب-النخبة-2-.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ddd54a3b02b5fad12b4b138b9c69dc8c4cd040a9492408519df03fe9e35f0635
3
+ size 12651217
extensions.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import subprocess
3
+ import pandas as pd
4
+ import folium
5
+ from wordcloud import WordCloud
6
+ import matplotlib.pyplot as plt
7
+ from textblob import TextBlob
8
+ from gtts import gTTS
9
+ import speech_recognition as sr
10
+ from deep_translator import GoogleTranslator
11
+ from collections import Counter
12
+ import re
13
+ import json
14
+
15
+ # We will import _index from rag_pipeline to get documents
16
+ import rag_pipeline
17
+
18
+ # ── Map Generation ──────────────────────────────────────────────
19
+ LOCATIONS = [
20
+ {"name": "Jerusalem (Al-Quds)", "lat": 31.7683, "lon": 35.2137,
21
+ "query": "Jerusalem Al-Quds occupation history destruction",
22
+ "desc": "The capital of Palestine, central to its history, culture, and religious identity."},
23
+ {"name": "Gaza", "lat": 31.5017, "lon": 34.4668,
24
+ "query": "Gaza destruction casualties humanitarian crisis displaced",
25
+ "desc": "One of the oldest cities; subject of military operations and humanitarian siege."},
26
+ {"name": "Ramallah", "lat": 31.9038, "lon": 35.2034,
27
+ "query": "Ramallah West Bank Palestinian Authority",
28
+ "desc": "A major Palestinian cultural and political center in the West Bank."},
29
+ {"name": "Hebron (Al-Khalil)", "lat": 31.5326, "lon": 35.0998,
30
+ "query": "Hebron Al-Khalil settlements occupation",
31
+ "desc": "A historic city known for the Ibrahimi Mosque and traditional crafts."},
32
+ {"name": "Nablus", "lat": 32.2211, "lon": 35.2544,
33
+ "query": "Nablus West Bank raids settlements",
34
+ "desc": "Famous for its traditional soap, knafeh, and historic old city."},
35
+ {"name": "Haifa", "lat": 32.7940, "lon": 34.9896,
36
+ "query": "Haifa Nakba 1948 Palestinian expelled",
37
+ "desc": "A historic coastal city, largely depopulated during the 1948 Nakba."},
38
+ {"name": "Jaffa (Yafa)", "lat": 32.0504, "lon": 34.7522,
39
+ "query": "Jaffa Yafa Nakba 1948 destruction port expelled",
40
+ "desc": "Historically one of Palestine's most important port cities, depopulated in 1948."},
41
+ {"name": "Rafah", "lat": 31.2956, "lon": 34.2527,
42
+ "query": "Rafah crossing humanitarian aid evacuation bombardment",
43
+ "desc": "A border city in southern Gaza; key crossing for humanitarian aid."},
44
+ {"name": "Khan Yunis", "lat": 31.3436, "lon": 34.3061,
45
+ "query": "Khan Yunis destruction bombardment casualties",
46
+ "desc": "One of Gaza's largest cities, heavily affected by military operations."},
47
+ {"name": "Jenin", "lat": 32.4641, "lon": 35.2961,
48
+ "query": "Jenin refugee camp military operation incursion",
49
+ "desc": "Home to one of the West Bank's largest refugee camps."},
50
+ ]
51
+
52
+
53
+ def _get_location_facts(query: str) -> str:
54
+ """Retrieve document excerpts relevant to a location. Returns formatted HTML."""
55
+ if rag_pipeline._retriever is None:
56
+ return ""
57
+ try:
58
+ nodes = rag_pipeline._retriever.retrieve(query)
59
+ if not nodes:
60
+ return ""
61
+ snippets = []
62
+ seen = set()
63
+ for node in nodes[:3]:
64
+ text = node.node.get_content()[:280].strip().replace("\n", " ")
65
+ source = node.node.metadata.get("source", "")
66
+ page = node.node.metadata.get("page_number", "?")
67
+ key = (source, page)
68
+ if key in seen:
69
+ continue
70
+ seen.add(key)
71
+ src_label = (source[:45] + "...") if len(source) > 45 else source
72
+ snippets.append(
73
+ f'<blockquote style="font-size:11px;margin:4px 0;border-left:3px solid #c00;'
74
+ f'padding-left:6px;color:#222;">'
75
+ f'"{text}..."<br>'
76
+ f'<i style="color:#666;">&#8212; {src_label}, p.{page}</i>'
77
+ f'</blockquote>'
78
+ )
79
+ return "".join(snippets)
80
+ except Exception:
81
+ return ""
82
+
83
+
84
+ def generate_map():
85
+ m = folium.Map(location=[31.5, 34.8], zoom_start=8, tiles="CartoDB positron")
86
+
87
+ for loc in LOCATIONS:
88
+ doc_facts = _get_location_facts(loc["query"])
89
+
90
+ popup_html = (
91
+ f'<div style="font-family:Arial,sans-serif;max-width:340px;direction:auto;">'
92
+ f'<h4 style="margin:0 0 6px;color:#1a1a1a;">{loc["name"]}</h4>'
93
+ f'<p style="font-size:12px;color:#333;margin:0 0 8px;">{loc["desc"]}</p>'
94
+ )
95
+ if doc_facts:
96
+ popup_html += (
97
+ f'<hr style="border:none;border-top:1px solid #ddd;margin:6px 0;">'
98
+ f'<p style="font-size:11px;font-weight:bold;color:#c00;margin:0 0 4px;">'
99
+ f'&#128196; From the Documents:</p>'
100
+ f'{doc_facts}'
101
+ )
102
+ popup_html += "</div>"
103
+
104
+ folium.Marker(
105
+ location=[loc["lat"], loc["lon"]],
106
+ popup=folium.Popup(popup_html, max_width=360),
107
+ tooltip=folium.Tooltip(loc["name"], sticky=True),
108
+ icon=folium.Icon(color="red", icon="info-sign"),
109
+ ).add_to(m)
110
+
111
+ map_path = "palestine_map.html"
112
+ m.save(map_path)
113
+ return map_path
114
+
115
+ # ── Timeline Generation ──────────────────────────────────────────────
116
+ def generate_timeline():
117
+ timeline_html = """
118
+ <div style="font-family: Arial, sans-serif; padding: 20px;">
119
+ <h3>Historical Timeline of the Palestinian Cause</h3>
120
+ <ul style="border-left: 2px solid #333; padding-left: 20px;">
121
+ <li style="margin-bottom: 10px;"><b>1917:</b> Balfour Declaration issued by the British government.</li>
122
+ <li style="margin-bottom: 10px;"><b>1947:</b> UN General Assembly adopts Resolution 181 (Partition Plan).</li>
123
+ <li style="margin-bottom: 10px;"><b>1948:</b> The Nakba (Catastrophe); hundreds of thousands of Palestinians displaced.</li>
124
+ <li style="margin-bottom: 10px;"><b>1967:</b> The Naksa (Setback); occupation of the West Bank, Gaza, and East Jerusalem.</li>
125
+ <li style="margin-bottom: 10px;"><b>1987:</b> The First Intifada begins.</li>
126
+ <li style="margin-bottom: 10px;"><b>1993:</b> Oslo Accords signed.</li>
127
+ <li style="margin-bottom: 10px;"><b>2000:</b> The Second Intifada begins.</li>
128
+ <li style="margin-bottom: 10px;"><b>Present:</b> Ongoing struggle for self-determination and human rights.</li>
129
+ </ul>
130
+ </div>
131
+ """
132
+ return timeline_html
133
+
134
+ # ── Word Cloud Generation ──────────────────────────────────────────────
135
+ def generate_wordcloud(doc_name="All"):
136
+ if rag_pipeline._index is None:
137
+ return None
138
+
139
+ docstore = rag_pipeline._index.docstore
140
+ nodes = list(docstore.docs.values())
141
+
142
+ text = ""
143
+ for node in nodes:
144
+ if doc_name is None or doc_name == "All" or node.metadata.get("source") == doc_name:
145
+ text += node.get_content() + " "
146
+
147
+ if not text.strip():
148
+ # Fallback if no text
149
+ text = "Palestine History Culture Rights Peace Justice Freedom"
150
+
151
+ wordcloud = WordCloud(width=800, height=400, background_color='white').generate(text)
152
+ plt.figure(figsize=(10, 5))
153
+ plt.imshow(wordcloud, interpolation='bilinear')
154
+ plt.axis('off')
155
+ img_path = "wordcloud.png"
156
+ plt.savefig(img_path, bbox_inches='tight')
157
+ plt.close()
158
+ return img_path
159
+
160
+ # ── Statistics Generation ──────────────────────────────────────────────
161
+ def get_statistics():
162
+ if rag_pipeline._index is None:
163
+ return pd.DataFrame(), pd.DataFrame()
164
+
165
+ docstore = rag_pipeline._index.docstore
166
+ nodes = list(docstore.docs.values())
167
+
168
+ data = []
169
+ for node in nodes:
170
+ source = node.metadata.get("source", "Unknown")
171
+ page = node.metadata.get("page_number", 0)
172
+ length = len(node.get_content())
173
+ data.append({"Source": source, "Page": page, "Length": length})
174
+
175
+ df = pd.DataFrame(data)
176
+
177
+ if df.empty:
178
+ return pd.DataFrame(), pd.DataFrame()
179
+
180
+ stats_df = df.groupby('Source').agg(
181
+ Chunks=('Source', 'count'),
182
+ Total_Length=('Length', 'sum'),
183
+ Avg_Length=('Length', 'mean')
184
+ ).reset_index()
185
+
186
+ return stats_df, df
187
+
188
+ # ── Advanced Analytics ──────────────────────────────────────────────
189
+ def advanced_analytics(text):
190
+ if not text or not text.strip():
191
+ return "No text provided for analysis."
192
+
193
+ blob = TextBlob(text)
194
+ sentiment = blob.sentiment
195
+ sentiment_str = f"Polarity: {sentiment.polarity:.2f} (Negative < 0 < Positive), Subjectivity: {sentiment.subjectivity:.2f} (Objective < 0.5 < Subjective)"
196
+
197
+ words = re.findall(r'\b[A-Z][a-z]+\b', text)
198
+ freq = Counter(words)
199
+ common_entities = freq.most_common(10)
200
+
201
+ analytics_report = f"### Sentiment Analysis\n{sentiment_str}\n\n"
202
+ analytics_report += "### Frequent Capitalized Entities (Heuristic)\n"
203
+ for ent, count in common_entities:
204
+ analytics_report += f"- **{ent}**: {count}\n"
205
+
206
+ return analytics_report
207
+
208
+ # ── Audio Features ──────────────────────────────────────────────
209
+ def text_to_speech(text, lang='en'):
210
+ try:
211
+ tts = gTTS(text=text, lang=lang)
212
+ output_path = "output_audio.mp3"
213
+ tts.save(output_path)
214
+ return output_path
215
+ except Exception as e:
216
+ print(f"TTS Error: {e}")
217
+ return None
218
+
219
+ def speech_to_text(audio_path):
220
+ if not audio_path:
221
+ return ""
222
+
223
+ wav_path = audio_path
224
+ if not audio_path.lower().endswith(".wav"):
225
+ wav_path = "temp_stt.wav"
226
+ try:
227
+ subprocess.run(["ffmpeg", "-y", "-i", audio_path, wav_path], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
228
+ except Exception as e:
229
+ error_msg = f"[Voice Input Error: Audio conversion failed: {str(e)}]"
230
+ print(error_msg)
231
+ return error_msg
232
+
233
+ r = sr.Recognizer()
234
+ try:
235
+ with sr.AudioFile(wav_path) as source:
236
+ audio_data = r.record(source)
237
+ text = r.recognize_google(audio_data)
238
+ return text
239
+ except Exception as e:
240
+ error_msg = f"[Voice Input Error: {str(e)}]"
241
+ print(error_msg)
242
+ return error_msg
243
+
244
+ # ── Translation ──────────────────────────────────────────────
245
+ def translate_text(text, target_lang='en'):
246
+ try:
247
+ translator = GoogleTranslator(source='auto', target=target_lang)
248
+ return translator.translate(text)
249
+ except Exception as e:
250
+ print(f"Translation Error: {e}")
251
+ return text
252
+
253
+ # ── Export Chat ──────────────────────────────────────────────
254
+ def export_chat_history(history):
255
+ if not history:
256
+ return None
257
+
258
+ # Convert objects to dicts if necessary for JSON serialization
259
+ cleaned_history = []
260
+ for msg in history:
261
+ if isinstance(msg, dict):
262
+ cleaned_history.append(msg)
263
+ else:
264
+ cleaned_history.append({"role": getattr(msg, "role", "unknown"), "content": getattr(msg, "content", "")})
265
+
266
+ file_path = "chat_history.json"
267
+ with open(file_path, "w", encoding="utf-8") as f:
268
+ json.dump(cleaned_history, f, ensure_ascii=False, indent=4)
269
+ return file_path
profile_picture.png ADDED
rag_pipeline.py ADDED
@@ -0,0 +1,371 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pickle
3
+ from langdetect import detect
4
+
5
+ from llama_index.core import VectorStoreIndex, Document, Settings
6
+ from llama_index.core.retrievers import VectorIndexRetriever
7
+ from llama_index.embeddings.huggingface import HuggingFaceEmbedding
8
+ from llama_index.llms.openai_like import OpenAILike
9
+ from llama_index.core.node_parser import SimpleNodeParser
10
+
11
+ from document_processor import load_all_documents, load_uploaded_pdf
12
+ from config import (
13
+ AIGRID_API_KEY_GPT, AIGRID_API_KEY_GEMMA, AIGRID_API_BASE,
14
+ OPENROUTER_API_KEY, OPENROUTER_API_BASE,
15
+ LLM_MODEL, EMBEDDING_MODEL,
16
+ LLM_TEMPERATURE, MAX_TOKENS, TOP_K, SIMILARITY_CUTOFF,
17
+ FAISS_INDEX_PATH,
18
+ )
19
+
20
+ # ─────────────────────────────────────────────────────────────────────────────
21
+ # Global state
22
+ # ─────────────────────────────────────────────────────────────────────────────
23
+ _index = None # LlamaIndex VectorStoreIndex
24
+ _retriever = None # Retriever object
25
+
26
+
27
+ # ─────────────────────────────────────────────────────────────────────────────
28
+ # LLM Factory
29
+ # ─────────────────────────────────────────────────────────────────────────────
30
+ def get_llm(model_id: str):
31
+ if model_id == "gpt-oss-120b":
32
+ return OpenAILike(
33
+ model = model_id,
34
+ api_key = AIGRID_API_KEY_GPT,
35
+ api_base = AIGRID_API_BASE,
36
+ temperature = LLM_TEMPERATURE,
37
+ max_tokens = MAX_TOKENS,
38
+ is_chat_model=True,
39
+ )
40
+ elif model_id == "google/gemma-4-31B":
41
+ return OpenAILike(
42
+ model = model_id,
43
+ api_key = AIGRID_API_KEY_GEMMA,
44
+ api_base = AIGRID_API_BASE,
45
+ temperature = LLM_TEMPERATURE,
46
+ max_tokens = MAX_TOKENS,
47
+ is_chat_model=True,
48
+ )
49
+ else:
50
+ # Assume it's an OpenRouter model
51
+ return OpenAILike(
52
+ model = model_id,
53
+ api_key = OPENROUTER_API_KEY,
54
+ api_base = OPENROUTER_API_BASE,
55
+ temperature = LLM_TEMPERATURE,
56
+ max_tokens = MAX_TOKENS,
57
+ is_chat_model=True,
58
+ )
59
+
60
+
61
+ # ─────────────────────────────────────────────────────────────────────────────
62
+ # Initialise models
63
+ # ─────────────────────────────────────────────────────────────────────────────
64
+ def _init_models():
65
+ embed_model = HuggingFaceEmbedding(model_name=EMBEDDING_MODEL)
66
+
67
+ # We use a default LLM for any global index operations if necessary
68
+ default_llm = get_llm(LLM_MODEL)
69
+
70
+ # Apply globally to LlamaIndex
71
+ Settings.embed_model = embed_model
72
+ Settings.llm = default_llm
73
+ Settings.chunk_size = 512 # internal safety
74
+
75
+ print("[INFO] Models initialised.")
76
+
77
+
78
+ # ─────────────────────────────────────────────────────────────────────────────
79
+ # Build index from chunk dicts
80
+ # ─────────────────────────────────────────────────────────────────────────────
81
+ def _build_index(chunks: list[dict]) -> VectorStoreIndex:
82
+ documents = []
83
+ for chunk in chunks:
84
+ doc = Document(
85
+ text = chunk["text"],
86
+ metadata = {
87
+ "source" : chunk["source"],
88
+ "page_number": chunk["page_number"],
89
+ "language" : chunk["language"],
90
+ },
91
+ )
92
+ documents.append(doc)
93
+
94
+ index = VectorStoreIndex.from_documents(
95
+ documents,
96
+ show_progress=True,
97
+ )
98
+ return index
99
+
100
+
101
+ # ─────────────────────────────────────────────────────────────────────────────
102
+ # Public: initialise everything at startup
103
+ # ─────���───────────────────────────────────────────────────────────────────────
104
+ def initialise_pipeline():
105
+ global _index, _retriever
106
+
107
+ _init_models()
108
+
109
+ chunks = load_all_documents()
110
+
111
+ if not chunks:
112
+ print("[WARN] No documents loaded — index will be empty.")
113
+ chunks = [{"text": "placeholder", "page_number": 1,
114
+ "source": "none", "language": "en"}]
115
+
116
+ _index = _build_index(chunks)
117
+ _retriever = VectorIndexRetriever(index=_index, similarity_top_k=TOP_K)
118
+
119
+ print("[INFO] Pipeline ready.")
120
+
121
+
122
+ # ─────────────────────────────────────────────────────────────────────────────
123
+ # Public: add a new uploaded PDF to the existing index
124
+ # ─────────────────────────────────────────────────────────────────────────────
125
+ def add_pdf_to_index(pdf_path: str) -> str:
126
+ global _index, _retriever
127
+
128
+ if _index is None:
129
+ return "❌ Pipeline not initialised yet."
130
+
131
+ new_chunks = load_uploaded_pdf(pdf_path)
132
+
133
+ if not new_chunks:
134
+ return "❌ Could not extract text from this PDF."
135
+
136
+ for chunk in new_chunks:
137
+ doc = Document(
138
+ text = chunk["text"],
139
+ metadata = {
140
+ "source" : chunk["source"],
141
+ "page_number": chunk["page_number"],
142
+ "language" : chunk["language"],
143
+ },
144
+ )
145
+ _index.insert(doc)
146
+
147
+ _retriever = VectorIndexRetriever(index=_index, similarity_top_k=TOP_K)
148
+
149
+ return (
150
+ f"✅ PDF indexed successfully!\n"
151
+ f"📄 {len(new_chunks)} chunks added.\n"
152
+ f"🔍 Ready to query!"
153
+ )
154
+
155
+
156
+ # ─────────────────────────────────────────────────────────────────────────────
157
+ # Internal: retrieve relevant nodes
158
+ # ─────────────────────────────────────────────────────────────────────────────
159
+ def _retrieve(query: str):
160
+ if _retriever is None:
161
+ return []
162
+ nodes = _retriever.retrieve(query)
163
+ return nodes
164
+
165
+
166
+ # ─────────────────────────────────────────────────────────────────────────────
167
+ # Internal: detect query language
168
+ # ─────────────────────────────────────────────────────────────────────────────
169
+ def _detect_lang(text: str) -> str:
170
+ try:
171
+ lang = detect(text[:500])
172
+ return "ar" if lang == "ar" else "en"
173
+ except Exception:
174
+ return "en"
175
+
176
+
177
+ # ─────────────────────────────────────────────────────────────────────────────
178
+ # Internal: build answer prompt
179
+ # ─────────────────────────────────────────────────────────────────────────────
180
+ def _build_prompt(query: str, nodes, lang: str) -> str:
181
+ context_parts = []
182
+ for i, node in enumerate(nodes):
183
+ meta = node.metadata
184
+ source = meta.get("source", "Unknown")
185
+ page = meta.get("page_number", "?")
186
+ text = node.get_content()
187
+ context_parts.append(f"[{i+1}] Source: {source} | Page: {page}\n{text}")
188
+
189
+ context = "\n\n".join(context_parts)
190
+
191
+ if lang == "ar":
192
+ instruction = (
193
+ "أنت مساعد متخصص في الوثائق الفلسطينية.\n"
194
+ "القواعد الصارمة:\n"
195
+ "1. أجب فقط بناءً على الوثائق المسترجعة\n"
196
+ "2. اذكر دائماً: اسم الوثيقة + رقم الصفحة\n"
197
+ "3. إذا لم تجد الإجابة، قل: 'لم يتم العثور على هذه المعلومات في الوثائق المتاحة'\n"
198
+ "4. لا تستخدم معرفتك الخارجية أبداً\n"
199
+ )
200
+ else:
201
+ instruction = (
202
+ "You are a document-based assistant specialising in Palestinian documents.\n"
203
+ "STRICT RULES:\n"
204
+ "1. Answer ONLY from the retrieved document chunks below\n"
205
+ "2. ALWAYS cite: Document Title + Page Number\n"
206
+ "3. If the answer is NOT in the documents, say exactly: "
207
+ "'This information was not found in the provided documents.'\n"
208
+ "4. NEVER use external knowledge\n"
209
+ "5. Respond in the SAME language as the question\n"
210
+ )
211
+
212
+ prompt = (
213
+ f"{instruction}\n"
214
+ f"Retrieved Context:\n{context}\n\n"
215
+ f"Question: {query}\n"
216
+ f"Answer:"
217
+ )
218
+ return prompt
219
+
220
+
221
+ # ─────────────────────────────────────────────────────────────────────────────
222
+ # Public: main query function
223
+ # ─────────────────────────────────────────────────────────────────────────────
224
+ def query_rag(question: str, model_id: str = "gpt-oss-120b") -> tuple[str, list[dict]]:
225
+ """
226
+ Returns (answer_text, list_of_source_dicts)
227
+ """
228
+ if not question.strip():
229
+ return "Please enter a question.", []
230
+
231
+ lang = _detect_lang(question)
232
+ nodes = _retrieve(question)
233
+
234
+ # ── Anti-hallucination gate ───────────────────────────────────────────────
235
+ if not nodes:
236
+ if lang == "ar":
237
+ return "لم يتم العثور على هذه المعلومات في الوثائق المتاحة.", []
238
+ return "This information was not found in the provided documents.", []
239
+
240
+ # Check similarity scores
241
+ top_score = max((n.score for n in nodes if n.score is not None), default=0)
242
+ if top_score < SIMILARITY_CUTOFF:
243
+ if lang == "ar":
244
+ return "لم يتم العثور على هذه المعلومات في الوثائق المتاحة.", []
245
+ return "This information was not found in the provided documents.", []
246
+
247
+ # ── Build prompt & call LLM ───────────────────────────────────────────────
248
+ prompt = _build_prompt(question, nodes, lang)
249
+ llm = get_llm(model_id)
250
+
251
+ try:
252
+ response = llm.complete(prompt)
253
+ answer = str(response)
254
+ except Exception as e:
255
+ return f"❌ LLM error: {e}", []
256
+
257
+ # ── Build sources list ────────────────────────────────────────────────────
258
+ sources = []
259
+ seen = set()
260
+ for node in nodes:
261
+ meta = node.metadata
262
+ key = (meta.get("source", ""), meta.get("page_number", ""))
263
+ if key not in seen:
264
+ seen.add(key)
265
+ sources.append({
266
+ "source" : meta.get("source", "Unknown"),
267
+ "page" : meta.get("page_number", "?"),
268
+ "language": meta.get("language", "en"),
269
+ "score" : round(node.score, 3) if node.score else 0,
270
+ "snippet" : node.get_content()[:200] + "...",
271
+ })
272
+
273
+ return answer, sources
274
+
275
+
276
+ # ─────────────────────────────────────────────────────────────────────────────
277
+ # Public: summarise a specific document
278
+ # ─────────────────────────────────────────────────────────────────────────────
279
+ def summarise_document(doc_name: str, model_id: str = "gpt-oss-120b") -> str:
280
+ if _retriever is None:
281
+ return "Pipeline not ready."
282
+
283
+ query = f"summarize the document {doc_name}"
284
+ nodes = _retriever.retrieve(query)
285
+
286
+ if not nodes:
287
+ return f"No content found for '{doc_name}'."
288
+
289
+ context = "\n\n".join(
290
+ [n.get_content()[:300] for n in nodes[:5]]
291
+ )
292
+
293
+ prompt = (
294
+ f"Summarise the following excerpts from the document '{doc_name}' "
295
+ f"in 5–7 bullet points. Be concise and factual.\n\n"
296
+ f"Content:\n{context}\n\nSummary:"
297
+ )
298
+
299
+ llm = get_llm(model_id)
300
+ try:
301
+ response = llm.complete(prompt)
302
+ return str(response)
303
+ except Exception as e:
304
+ return f"❌ Error: {e}"
305
+
306
+
307
+ # ─────────────────────────────────────────────────────────────────────────────
308
+ # Public: compare two documents
309
+ # ────────────────────────────────────────────────────────��────────────────────
310
+ def compare_documents(doc1: str, doc2: str, aspect: str = "main themes", model_id: str = "gpt-oss-120b") -> str:
311
+ if _retriever is None:
312
+ return "Pipeline not ready."
313
+
314
+ nodes1 = _retriever.retrieve(f"content of {doc1}")
315
+ nodes2 = _retriever.retrieve(f"content of {doc2}")
316
+
317
+ ctx1 = "\n".join([n.get_content()[:200] for n in nodes1[:3]])
318
+ ctx2 = "\n".join([n.get_content()[:200] for n in nodes2[:3]])
319
+
320
+ prompt = (
321
+ f"Compare these two documents regarding '{aspect}'.\n\n"
322
+ f"Document 1 — {doc1}:\n{ctx1}\n\n"
323
+ f"Document 2 — {doc2}:\n{ctx2}\n\n"
324
+ f"Provide a structured comparison with:\n"
325
+ f"- Similarities\n"
326
+ f"- Differences\n"
327
+ f"- Key Takeaways\n"
328
+ f"Comparison:"
329
+ )
330
+
331
+ llm = get_llm(model_id)
332
+ try:
333
+ response = llm.complete(prompt)
334
+ return str(response)
335
+ except Exception as e:
336
+ return f"❌ Error: {e}"
337
+
338
+
339
+ # ─────────────────────────────────────────────────────────────────────────────
340
+ # Public: discourse / bias analysis
341
+ # ─────────────────────────────────────────────────────────────────────────────
342
+ def analyse_discourse(query: str, model_id: str = "gpt-oss-120b") -> str:
343
+ if _retriever is None:
344
+ return "Pipeline not ready."
345
+
346
+ nodes = _retriever.retrieve(query)
347
+
348
+ if not nodes:
349
+ return "No relevant content found."
350
+
351
+ context = "\n\n".join([n.get_content()[:300] for n in nodes[:4]])
352
+
353
+ prompt = (
354
+ f"Perform a discourse analysis on the following text from Palestinian documents.\n"
355
+ f"Identify:\n"
356
+ f"1. 🏷️ Key Terminology & Framing\n"
357
+ f"2. ⚖️ Bias indicators (if any)\n"
358
+ f"3. 🔁 Repeated Narratives\n"
359
+ f"4. 💬 Tone & Language\n"
360
+ f"5. 🎯 Apparent Purpose/Agenda\n\n"
361
+ f"Topic/Query: {query}\n\n"
362
+ f"Text:\n{context}\n\n"
363
+ f"Analysis:"
364
+ )
365
+
366
+ llm = get_llm(model_id)
367
+ try:
368
+ response = llm.complete(prompt)
369
+ return str(response)
370
+ except Exception as e:
371
+ return f"❌ Error: {e}"
requirements.txt ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core
2
+ gradio>=4.0.0
3
+ python-dotenv
4
+
5
+ # LlamaIndex
6
+ llama-index-core
7
+ llama-index-llms-openai-like
8
+ llama-index-embeddings-huggingface
9
+
10
+ # PDF Processing
11
+ pymupdf
12
+ pdfplumber
13
+
14
+ # Language Detection & Translation
15
+ langdetect
16
+ deep-translator
17
+
18
+ # Visualisation
19
+ plotly
20
+ matplotlib
21
+ wordcloud
22
+ folium
23
+ pandas
24
+ Pillow
25
+ numpy
26
+
27
+ # Analytics & Voice
28
+ textblob
29
+ gTTS
30
+ SpeechRecognition
31
+
32
+ # ML / Embeddings
33
+ sentence-transformers
34
+ torch
35
+ transformers
test.wav ADDED
Binary file (88.2 kB). View file