Rezuwan commited on
Commit
3bf92b6
·
verified ·
1 Parent(s): f7233ae

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +12 -12
  2. app.py +381 -382
  3. requirements.txt +15 -17
README.md CHANGED
@@ -1,13 +1,13 @@
1
- ---
2
- title: USB - Unmad Satirical Bot
3
- emoji: 🤓
4
- colorFrom: yellow
5
- colorTo: red
6
- sdk: gradio
7
- sdk_version: 4.44.1
8
- python_version: "3.10"
9
- app_file: app.py
10
- pinned: false
11
- license: apache-2.0
12
- short_description: A chatbot that impersonates the satirical character UNMAD
13
  ---
 
1
+ ---
2
+ title: USB - Unmad Satirical Bot
3
+ emoji: 🤓
4
+ colorFrom: yellow
5
+ colorTo: red
6
+ sdk: gradio
7
+ sdk_version: 5.49.1
8
+ python_version: "3.10"
9
+ app_file: app.py
10
+ pinned: false
11
+ license: apache-2.0
12
+ short_description: A chatbot that impersonates the satirical character UNMAD
13
  ---
app.py CHANGED
@@ -1,383 +1,382 @@
1
- import os
2
- import re
3
- import json
4
- import warnings
5
- from typing import List, Dict, Any, Optional
6
- import lancedb
7
- import gradio as gr
8
- import numpy as np
9
- import pandas as pd
10
- from datetime import datetime
11
- from dotenv import load_dotenv
12
- from openai import OpenAI
13
- from sklearn.metrics.pairwise import cosine_similarity
14
-
15
- # Load environment variables
16
- load_dotenv()
17
- OPENAI_API_KEY = os.getenv("OPENAI_API_KEY_Project")
18
- if not OPENAI_API_KEY:
19
- raise ValueError("Missing OPENAI_API_KEY. Please set it in your environment variables.")
20
-
21
- # Suppress warnings
22
- warnings.filterwarnings("ignore")
23
-
24
- class LanceDBRAG:
25
- def __init__(self,
26
- db_path: str = "lance_unmad_db",
27
- table_name: str = "unmad_documents"):
28
- """Initialize LanceDB RAG System"""
29
- self.db_path = db_path
30
- self.table_name = table_name
31
-
32
- # Initialize OpenAI client
33
- self.client = OpenAI(api_key=OPENAI_API_KEY)
34
-
35
- # Connect to LanceDB
36
- try:
37
- self.db = lancedb.connect(self.db_path)
38
- self.table = self.db.open_table(self.table_name)
39
- print(f"Connected to LanceDB: {self.db_path}/{self.table_name}")
40
- except Exception as e:
41
- raise ConnectionError(f"Failed to connect to LanceDB: {e}")
42
-
43
- def get_embedding(self, text: str) -> List[float]:
44
- """Get OpenAI embedding for query text"""
45
- try:
46
- response = self.client.embeddings.create(
47
- model="text-embedding-3-small",
48
- input=text
49
- )
50
- return response.data[0].embedding
51
- except Exception as e:
52
- print(f"Error getting embedding: {e}")
53
- return None
54
-
55
- def search_similar_content(self, query: str, limit: int = 10) -> pd.DataFrame:
56
- """Search for similar content in the database"""
57
- print(f"Searching: '{query}'")
58
-
59
- # Get query embedding
60
- query_embedding = self.get_embedding(query)
61
- if not query_embedding:
62
- return pd.DataFrame()
63
-
64
- # Perform vector search
65
- try:
66
- search_query = self.table.search(query_embedding).limit(limit)
67
- results = search_query.to_pandas()
68
-
69
- if not results.empty:
70
- print(f"Found {len(results)} relevant results")
71
- else:
72
- print("No results found")
73
-
74
- return results
75
-
76
- except Exception as e:
77
- print(f"Search error: {e}")
78
- return pd.DataFrame()
79
-
80
- # Initialize global RAG instance
81
- rag_system = LanceDBRAG()
82
-
83
- def maximal_marginal_relevance_search(query, rag_instance, k=10, lambda_param=0.6, top_k=3):
84
- """
85
- Implement Maximal Marginal Relevance (MMR) for diverse document retrieval using LanceDB.
86
-
87
- Args:
88
- query: Search query string
89
- rag_instance: LanceDB RAG instance
90
- k: Number of candidate documents to consider
91
- lambda_param: Trade-off between relevance and diversity (0-1)
92
- top_k: Number of final documents to return
93
-
94
- Returns:
95
- List of selected documents with MMR ranking
96
- """
97
- # Get initial candidate documents using LanceDB search
98
- search_results = rag_instance.search_similar_content(query, limit=k)
99
-
100
- if search_results.empty:
101
- return []
102
-
103
- # Convert to document-like objects for compatibility
104
- docs = []
105
- for _, row in search_results.iterrows():
106
- doc_obj = {
107
- 'page_content': row['text'],
108
- 'metadata': {
109
- 'source': row.get('magazine_name', 'Unknown Source'),
110
- 'page': row.get('page_number', 'Unknown Page'),
111
- 'chunk': row.get('chunk_id', 0)
112
- },
113
- 'score': row['_distance']
114
- }
115
- docs.append(doc_obj)
116
-
117
- # Apply MMR selection if we have enough documents
118
- if len(docs) <= top_k:
119
- return docs[:top_k]
120
-
121
- # MMR Selection Algorithm
122
- selected_docs = []
123
- remaining_indices = list(range(len(docs)))
124
-
125
- for _ in range(min(top_k, len(docs))):
126
- if not remaining_indices:
127
- break
128
-
129
- mmr_scores = []
130
-
131
- for i in remaining_indices:
132
- # Calculate relevance score (inverse of distance)
133
- relevance = 1 / (1 + docs[i]['score'])
134
-
135
- # Calculate diversity score (max similarity to already selected docs)
136
- if selected_docs:
137
- max_similarity = 0
138
- for selected_doc in selected_docs:
139
- # Simple text-based similarity for diversity
140
- text1 = docs[i]['page_content']
141
- text2 = selected_doc['page_content']
142
-
143
- # Calculate simple Jaccard similarity
144
- words1 = set(text1.split())
145
- words2 = set(text2.split())
146
- if words1 and words2:
147
- similarity = len(words1.intersection(words2)) / len(words1.union(words2))
148
- max_similarity = max(max_similarity, similarity)
149
-
150
- diversity = max_similarity
151
- else:
152
- diversity = 0
153
-
154
- # Calculate MMR score
155
- mmr_score = lambda_param * relevance - (1 - lambda_param) * diversity
156
- mmr_scores.append((mmr_score, i))
157
-
158
- # Select document with highest MMR score
159
- if mmr_scores:
160
- best_score, best_idx = max(mmr_scores, key=lambda x: x[0])
161
- selected_docs.append(docs[best_idx])
162
- remaining_indices.remove(best_idx)
163
-
164
- return selected_docs
165
-
166
- def clean_bangla_content(text):
167
- """
168
- Clean the retrieved content to remove English watermarks, scan text, and tags.
169
- Keeps the Bengali text intact instead of deleting the whole line.
170
- """
171
- if not text:
172
- return ""
173
-
174
- # 1. First, strip out the specific tags entirely using NO backslashes
175
- text = re.sub("[[]source: *[0-9]+[]]", "", text, flags=re.IGNORECASE)
176
-
177
- # Common English watermarks and scan text to remove
178
- english_patterns = [
179
- r'scanned by \w+',
180
- r'found in \w+',
181
- r'www\.\w+\.\w+',
182
- r'http[s]?://[^\s]+',
183
- r'\.pdf',
184
- r'\.com',
185
- r'\.org',
186
- r'\.net',
187
- r'banglapdf',
188
- r'sadaqpdf',
189
- r'pdf scanner',
190
- r'scan by',
191
- r'converted by',
192
- r'page \d+',
193
- r'source:',
194
- r'reference:',
195
- r'[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+', # emails
196
- ]
197
-
198
- lines = text.split('\n')
199
- cleaned_lines = []
200
-
201
- for line in lines:
202
- # 2. Replace matched English watermark patterns with an empty string
203
- for pattern in english_patterns:
204
- line = re.sub(pattern, '', line, flags=re.IGNORECASE)
205
-
206
- line = line.strip()
207
- if not line:
208
- continue
209
-
210
- # 3. Check if there's actual Bengali content left
211
- bengali_chars = len(re.findall(r'[\u0980-\u09FF]', line))
212
-
213
- # Only keep the line if it has Bengali characters
214
- if bengali_chars > 0:
215
- cleaned_lines.append(line)
216
-
217
- return '\n'.join(cleaned_lines)
218
-
219
- # Enhanced Satirical QA function with MMR and content cleaning
220
- def custom_unmad_satirical_bot(message, history, top_k=3, lambda_param=0.6):
221
- """
222
- Enhanced satirical bot using MMR for diverse and relevant content retrieval.
223
-
224
- Args:
225
- message: User query
226
- history: Chat history
227
- top_k: Number of documents to retrieve
228
- lambda_param: MMR trade-off (higher favors exact relevance over diversity)
229
- """
230
- # Use MMR search with LanceDB
231
- docs = maximal_marginal_relevance_search(
232
- query=message,
233
- rag_instance=rag_system,
234
- k=15, # Consider more candidates for better diversity
235
- lambda_param=lambda_param,
236
- top_k=top_k
237
- )
238
-
239
- # Extract context from MMR-selected documents
240
- if docs:
241
- # Clean each document's content before joining
242
- cleaned_contexts = []
243
- for doc in docs:
244
- cleaned_content = clean_bangla_content(doc['page_content'])
245
- if cleaned_content.strip(): # Only add if there's meaningful Bengali content
246
- cleaned_contexts.append(cleaned_content)
247
-
248
- if cleaned_contexts:
249
- top_contexts = "\n\n---\n\n".join(cleaned_contexts)
250
- else:
251
- top_contexts = "No relevant information were found"
252
-
253
- # Add metadata about source diversity (optional)
254
- source_info = []
255
- for i, doc in enumerate(docs, 1):
256
- source = doc['metadata'].get('source', 'Unknown source')
257
- page = doc['metadata'].get('page', 'Unknown page')
258
- # Clean source info too
259
- if not re.search(r'[a-zA-Z]', str(source)): # Only if source doesn't contain English
260
- source_info.append(f"[{i}] {source} - {page}")
261
-
262
- source_context = "Source: " + " | ".join(source_info[:3]) if source_info else ""
263
- else:
264
- top_contexts = "No relevant information were found"
265
- source_context = ""
266
-
267
- # Prepare system prompt
268
- system_prompt = """
269
- তুমি বাংলাদেশের জনপ্রিয় স্যাটায়ার ম্যাগাজিন 'উন্মাদ'-এর একজন অত্যন্ত বুদ্ধিদীপ্ত, রসিক এবং কিছুটা 'তেড়া' (cynical) স্বভাবের লেখক। তোমার কাজ হলো ব্যবহারকারীর প্রশ্নের এমন উত্তর দেওয়া, যা পড়ে মনে হবে ঢাকার কোনো টং দোকানে বসে আড্ডা দেওয়ার সময় কেউ চরম স্যাটায়ার করছে। উত্তরগুলো হতে হবে লোকাল, চটপটে, এবং স্মার্ট।
270
-
271
- **এই নির্দেশনাগুলো কঠোরভাবে মেনে চলবে - কোন ব্যতিক্রম নেই:**
272
-
273
- ১। **লোকাল ও খাঁটি দেশি রসিকতা:** ভিনগ্রহ, পিরামিড, বা রূপকথার মতো আজগুবি বা ভিনদেশি উদাহরণ একদম টানবে না। খাঁটি বাংলাদেশি মেটাফোর ব্যবহার করবে (যেমন: টংয়ের চা, ঢাকার জ্যাম, লোডশেডিং, কারওয়ান বাজারের কাদা, লোকাল বাসের হেলপার, ইত্যাদি)। কথাবার্তা হবে একদম ন্যাচারাল, যেন মুখোমুখি বসে আড্ডা দিচ্ছ।
274
- ২। **প্রসঙ্গের ওপর সর্বোচ্চ জোর:** আমি তোমাকে যে 'প্রসঙ্গ' (Context) দেব, তোমার উত্তরের মূল ভিত্তি হবে সেটাই। প্রসঙ্গের বাইরের কোনো মনগড়া গল্প বানাবে না, বরং প্রসঙ্গের তথ্যগুলোকে বাঁকাভাবে উপস্থাপন করে খোঁচা দেবে।
275
- ৩। **অচেনা নাম নিয়ে দেশি স্টাইলে কটাক্ষ:** ব্যবহারকারী যদি এমন কোনো নাম বা বিষয় নিয়ে প্রশ্ন করে যা তোমার 'প্রসঙ্গে' নেই (যেমন ইশরাকের সাথে সাথে 'নগর ড্যাডি'), তাহলে এমনভাবে খোঁচা দেবে যেন সে গুলিস্তানের মোড়ে দাঁড়িয়ে গাঁজাখুরি গল্প করছে। (যেমন বলতে পারো: "নগর ড্যাডি ইশরাক আবার কে ভাই? গুলিস্তানের নতুন কোনো চাঁদাবাজ নাকি? নাকি চাঁদা চাইতে গিয়ে নিজেই চাঁদা হয়ে গেছে?")
276
- ৪। **খোঁচা দেওয়ার ধরন:** প্রসঙ্গের তথ্য ব্যবহার করে চরম স্যাটায়ার করবে। (যেমন: পিনাকী বা ইলিয়াসের কথা এলে বলবে তারা ফ্রান্স বা আমেরিকায় বসে 'ল্যাপটপ বিপ্লব' করছে; প্রথম আলোর কথা এলে বলবে আগুনে পোড়া ফার্নিচার দিয়ে আর্ট গ্যালারি বানানোর কথা)।
277
- ৫। কোন ইমোজি (EMOJI), ইংরেজি শব্দ, ইংরেজি অক্ষর, ইংরেজি সংখ্যা বা স্ক্যান ওয়াটারমার্ক ব্যবহার করবে না। সম্পূর্ণ বাংলায় স্মার্ট, রসবোধ সম্পন্ন এবং 'উন্মাদ' স্টাইলে তীক্ষ্ণ উত্তর লেখো।
278
- """
279
-
280
- user_prompt = f"""
281
- প্রসঙ্গ (বিভিন্ন উৎস থেকে সংগৃহীত):
282
- {top_contexts}
283
-
284
- প্রশ্ন: {message}
285
-
286
- নির্দেশনা: উপরের প্রসঙ্গ থেকে শুধুমাত্র বাংলা ভাষার বিষয়বস্তু ব্যবহার করে উন্মাদ ম্যাগাজিনের দেশি স্টাইলে উত্তর দাও। কোন ইমোজি বা ইংরেজি শব্দ দেবে না। উত্তরের ভাষা হবে চটপটে, ব্যঙ্গাত্মক এবং খাঁটি বাংলাদেশি।
287
-
288
- """
289
-
290
- # Generate response using OpenAI
291
- try:
292
- response = rag_system.client.chat.completions.create(
293
- model="gpt-4o",
294
- messages=[
295
- {"role": "system", "content": system_prompt},
296
- {"role": "user", "content": user_prompt}
297
- ],
298
- temperature=0.7,
299
- max_tokens=700
300
- )
301
-
302
- ai_response = response.choices[0].message.content
303
- history.append((message, ai_response))
304
- return "", history
305
-
306
- except Exception as e:
307
- error_response = f"উত্তর তৈরিতে সমস্যা হয়েছে। আবার চেষ্টা করুন।"
308
- history.append((message, error_response))
309
- return "", history
310
-
311
- # Enhanced Gradio UI with Polished Layout and Fixed State Management
312
- with gr.Blocks(theme=gr.themes.Soft(), css=".gradio-container {padding-top: 40px;}") as demo:
313
-
314
- with gr.Column(elem_classes="text-center"):
315
- gr.Markdown("# USB: Unmad Satirical Bot")
316
- gr.Markdown("### *A Satirical Bot that impersonates the satirical character UNMAD*")
317
-
318
- try:
319
- # Upgraded Image block for a full-width banner look
320
- gr.Image(
321
- "images/c1.png",
322
- width="100%", # Stretches the image horizontally to fill the container
323
- height=250, # Adjust this number to make the banner taller or shorter
324
- show_label=False,
325
- container=False, # Removes the bulky white background box
326
- interactive=False, # Makes it a static image (removes upload/edit hover buttons)
327
- show_download_button=False, # Hides the download icon
328
- show_fullscreen_button=False # Hides the fullscreen icon
329
- )
330
- except:
331
- gr.Markdown("*[UNMAD Logo]*")
332
-
333
- # Upgraded Chatbot component with avatars and better height
334
- chatbot = gr.Chatbot(
335
- height=500,
336
- bubble_full_width=True,
337
- avatar_images=(None, "images/c1.png"), # Left: User (Default), Right: Unmad (Your logo)
338
- show_label=False
339
- )
340
-
341
- with gr.Row():
342
- msg = gr.Textbox(
343
- placeholder="কি চলে আপনার মনে? বলেন শুনি...",
344
- scale=8,
345
- show_label=False,
346
- container=False # Removes the bulky box around the input
347
- )
348
- send = gr.Button("Send (↲)", variant="primary", scale=1)
349
-
350
- clear = gr.Button("Clear Chat", variant="secondary")
351
-
352
- # State to keep track of the conversation
353
- state = gr.State([])
354
-
355
- def chat_with_fixed_mmr(message, history):
356
- if not message.strip():
357
- return "", history, history
358
-
359
- # Call your core function with increased top_k and higher lambda_param for strict relevance
360
- _, updated_history = custom_unmad_satirical_bot(message, history, top_k=4, lambda_param=0.85)
361
-
362
- # Return: 1. Empty Textbox, 2. Visual Chatbot update, 3. Internal State update
363
- return "", updated_history, updated_history
364
-
365
- # Event Listeners with Loading States
366
- # Disable the input box while generating to prevent spam
367
- msg.submit(
368
- chat_with_fixed_mmr,
369
- inputs=[msg, state],
370
- outputs=[msg, chatbot, state]
371
- )
372
-
373
- send.click(
374
- chat_with_fixed_mmr,
375
- inputs=[msg, state],
376
- outputs=[msg, chatbot, state]
377
- )
378
-
379
- # Properly clear both the visual chatbot and the internal state
380
- clear.click(lambda: ([], []), None, [chatbot, state], queue=False)
381
-
382
- if __name__ == "__main__":
383
  demo.launch(share=False)
 
1
+ import os
2
+ import re
3
+ import json
4
+ import warnings
5
+ from typing import List, Dict, Any, Optional
6
+ import lancedb
7
+ import gradio as gr
8
+ import numpy as np
9
+ import pandas as pd
10
+ from datetime import datetime
11
+ from dotenv import load_dotenv
12
+ from openai import OpenAI
13
+ from sklearn.metrics.pairwise import cosine_similarity
14
+
15
+ # Load environment variables
16
+ load_dotenv()
17
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY_Project")
18
+ if not OPENAI_API_KEY:
19
+ raise ValueError("Missing OPENAI_API_KEY. Please set it in your environment variables.")
20
+
21
+ # Suppress warnings
22
+ warnings.filterwarnings("ignore")
23
+
24
+ class LanceDBRAG:
25
+ def __init__(self,
26
+ db_path: str = "lance_unmad_db",
27
+ table_name: str = "unmad_documents"):
28
+ """Initialize LanceDB RAG System"""
29
+ self.db_path = db_path
30
+ self.table_name = table_name
31
+
32
+ # Initialize OpenAI client
33
+ self.client = OpenAI(api_key=OPENAI_API_KEY)
34
+
35
+ # Connect to LanceDB
36
+ try:
37
+ self.db = lancedb.connect(self.db_path)
38
+ self.table = self.db.open_table(self.table_name)
39
+ print(f"Connected to LanceDB: {self.db_path}/{self.table_name}")
40
+ except Exception as e:
41
+ raise ConnectionError(f"Failed to connect to LanceDB: {e}")
42
+
43
+ def get_embedding(self, text: str) -> List[float]:
44
+ """Get OpenAI embedding for query text"""
45
+ try:
46
+ response = self.client.embeddings.create(
47
+ model="text-embedding-3-small",
48
+ input=text
49
+ )
50
+ return response.data[0].embedding
51
+ except Exception as e:
52
+ print(f"Error getting embedding: {e}")
53
+ return None
54
+
55
+ def search_similar_content(self, query: str, limit: int = 10) -> pd.DataFrame:
56
+ """Search for similar content in the database"""
57
+ print(f"Searching: '{query}'")
58
+
59
+ # Get query embedding
60
+ query_embedding = self.get_embedding(query)
61
+ if not query_embedding:
62
+ return pd.DataFrame()
63
+
64
+ # Perform vector search
65
+ try:
66
+ search_query = self.table.search(query_embedding).limit(limit)
67
+ results = search_query.to_pandas()
68
+
69
+ if not results.empty:
70
+ print(f"Found {len(results)} relevant results")
71
+ else:
72
+ print("No results found")
73
+
74
+ return results
75
+
76
+ except Exception as e:
77
+ print(f"Search error: {e}")
78
+ return pd.DataFrame()
79
+
80
+ # Initialize global RAG instance
81
+ rag_system = LanceDBRAG()
82
+
83
+ def maximal_marginal_relevance_search(query, rag_instance, k=10, lambda_param=0.6, top_k=3):
84
+ """
85
+ Implement Maximal Marginal Relevance (MMR) for diverse document retrieval using LanceDB.
86
+
87
+ Args:
88
+ query: Search query string
89
+ rag_instance: LanceDB RAG instance
90
+ k: Number of candidate documents to consider
91
+ lambda_param: Trade-off between relevance and diversity (0-1)
92
+ top_k: Number of final documents to return
93
+
94
+ Returns:
95
+ List of selected documents with MMR ranking
96
+ """
97
+ # Get initial candidate documents using LanceDB search
98
+ search_results = rag_instance.search_similar_content(query, limit=k)
99
+
100
+ if search_results.empty:
101
+ return []
102
+
103
+ # Convert to document-like objects for compatibility
104
+ docs = []
105
+ for _, row in search_results.iterrows():
106
+ doc_obj = {
107
+ 'page_content': row['text'],
108
+ 'metadata': {
109
+ 'source': row.get('magazine_name', 'Unknown Source'),
110
+ 'page': row.get('page_number', 'Unknown Page'),
111
+ 'chunk': row.get('chunk_id', 0)
112
+ },
113
+ 'score': row['_distance']
114
+ }
115
+ docs.append(doc_obj)
116
+
117
+ # Apply MMR selection if we have enough documents
118
+ if len(docs) <= top_k:
119
+ return docs[:top_k]
120
+
121
+ # MMR Selection Algorithm
122
+ selected_docs = []
123
+ remaining_indices = list(range(len(docs)))
124
+
125
+ for _ in range(min(top_k, len(docs))):
126
+ if not remaining_indices:
127
+ break
128
+
129
+ mmr_scores = []
130
+
131
+ for i in remaining_indices:
132
+ # Calculate relevance score (inverse of distance)
133
+ relevance = 1 / (1 + docs[i]['score'])
134
+
135
+ # Calculate diversity score (max similarity to already selected docs)
136
+ if selected_docs:
137
+ max_similarity = 0
138
+ for selected_doc in selected_docs:
139
+ # Simple text-based similarity for diversity
140
+ text1 = docs[i]['page_content']
141
+ text2 = selected_doc['page_content']
142
+
143
+ # Calculate simple Jaccard similarity
144
+ words1 = set(text1.split())
145
+ words2 = set(text2.split())
146
+ if words1 and words2:
147
+ similarity = len(words1.intersection(words2)) / len(words1.union(words2))
148
+ max_similarity = max(max_similarity, similarity)
149
+
150
+ diversity = max_similarity
151
+ else:
152
+ diversity = 0
153
+
154
+ # Calculate MMR score
155
+ mmr_score = lambda_param * relevance - (1 - lambda_param) * diversity
156
+ mmr_scores.append((mmr_score, i))
157
+
158
+ # Select document with highest MMR score
159
+ if mmr_scores:
160
+ best_score, best_idx = max(mmr_scores, key=lambda x: x[0])
161
+ selected_docs.append(docs[best_idx])
162
+ remaining_indices.remove(best_idx)
163
+
164
+ return selected_docs
165
+
166
+ def clean_bangla_content(text):
167
+ """
168
+ Clean the retrieved content to remove English watermarks, scan text, and tags.
169
+ Keeps the Bengali text intact instead of deleting the whole line.
170
+ """
171
+ if not text:
172
+ return ""
173
+
174
+ # 1. First, strip out the specific tags entirely using NO backslashes
175
+ text = re.sub("[[]source: *[0-9]+[]]", "", text, flags=re.IGNORECASE)
176
+
177
+ # Common English watermarks and scan text to remove
178
+ english_patterns = [
179
+ r'scanned by \w+',
180
+ r'found in \w+',
181
+ r'www\.\w+\.\w+',
182
+ r'http[s]?://[^\s]+',
183
+ r'\.pdf',
184
+ r'\.com',
185
+ r'\.org',
186
+ r'\.net',
187
+ r'banglapdf',
188
+ r'sadaqpdf',
189
+ r'pdf scanner',
190
+ r'scan by',
191
+ r'converted by',
192
+ r'page \d+',
193
+ r'source:',
194
+ r'reference:',
195
+ r'[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+', # emails
196
+ ]
197
+
198
+ lines = text.split('\n')
199
+ cleaned_lines = []
200
+
201
+ for line in lines:
202
+ # 2. Replace matched English watermark patterns with an empty string
203
+ for pattern in english_patterns:
204
+ line = re.sub(pattern, '', line, flags=re.IGNORECASE)
205
+
206
+ line = line.strip()
207
+ if not line:
208
+ continue
209
+
210
+ # 3. Check if there's actual Bengali content left
211
+ bengali_chars = len(re.findall(r'[\u0980-\u09FF]', line))
212
+
213
+ # Only keep the line if it has Bengali characters
214
+ if bengali_chars > 0:
215
+ cleaned_lines.append(line)
216
+
217
+ return '\n'.join(cleaned_lines)
218
+
219
+ # Enhanced Satirical QA function with MMR and content cleaning
220
+ def custom_unmad_satirical_bot(message, history, top_k=3, lambda_param=0.6):
221
+ """
222
+ Enhanced satirical bot using MMR for diverse and relevant content retrieval.
223
+
224
+ Args:
225
+ message: User query
226
+ history: Chat history
227
+ top_k: Number of documents to retrieve
228
+ lambda_param: MMR trade-off (higher favors exact relevance over diversity)
229
+ """
230
+ # Use MMR search with LanceDB
231
+ docs = maximal_marginal_relevance_search(
232
+ query=message,
233
+ rag_instance=rag_system,
234
+ k=15, # Consider more candidates for better diversity
235
+ lambda_param=lambda_param,
236
+ top_k=top_k
237
+ )
238
+
239
+ # Extract context from MMR-selected documents
240
+ if docs:
241
+ # Clean each document's content before joining
242
+ cleaned_contexts = []
243
+ for doc in docs:
244
+ cleaned_content = clean_bangla_content(doc['page_content'])
245
+ if cleaned_content.strip(): # Only add if there's meaningful Bengali content
246
+ cleaned_contexts.append(cleaned_content)
247
+
248
+ if cleaned_contexts:
249
+ top_contexts = "\n\n---\n\n".join(cleaned_contexts)
250
+ else:
251
+ top_contexts = "No relevant information were found"
252
+
253
+ # Add metadata about source diversity (optional)
254
+ source_info = []
255
+ for i, doc in enumerate(docs, 1):
256
+ source = doc['metadata'].get('source', 'Unknown source')
257
+ page = doc['metadata'].get('page', 'Unknown page')
258
+ # Clean source info too
259
+ if not re.search(r'[a-zA-Z]', str(source)): # Only if source doesn't contain English
260
+ source_info.append(f"[{i}] {source} - {page}")
261
+
262
+ source_context = "Source: " + " | ".join(source_info[:3]) if source_info else ""
263
+ else:
264
+ top_contexts = "No relevant information were found"
265
+ source_context = ""
266
+
267
+ # Prepare system prompt
268
+ system_prompt = """
269
+ তুমি বাংলাদেশের জনপ্রিয় স্যাটায়ার ম্যাগাজিন 'উন্মাদ'-এর একজন অত্যন্ত বুদ্ধিদীপ্ত, রসিক এবং কিছুটা 'তেড়া' (cynical) স্বভাবের লেখক। তোমার কাজ হলো ব্যবহারকারীর প্রশ্নের এমন উত্তর দেওয়া, যা পড়ে মনে হবে ঢাকার কোনো টং দোকানে বসে আড্ডা দেওয়ার সময় কেউ চরম স্যাটায়ার করছে। উত্তরগুলো হতে হবে লোকাল, চটপটে, এবং স্মার্ট।
270
+
271
+ **এই নির্দেশনাগুলো কঠোরভাবে মেনে চলবে - কোন ব্যতিক্রম নেই:**
272
+
273
+ ১। **লোকাল ও খাঁটি দেশি রসিকতা:** ভিনগ্রহ, পিরামিড, বা রূপকথার মতো আজগুবি বা ভিনদেশি উদাহরণ একদম টানবে না। খাঁটি বাংলাদেশি মেটাফোর ব্যবহার করবে (যেমন: টংয়ের চা, ঢাকার জ্যাম, লোডশেডিং, কারওয়ান বাজারের কাদা, লোকাল বাসের হেলপার, ইত্যাদি)। কথাবার্তা হবে একদম ন্যাচারাল, যেন মুখোমুখি বসে আড্ডা দিচ্ছ।
274
+ ২। **প্রসঙ্গের ওপর সর্বোচ্চ জোর:** আমি তোমাকে যে 'প্রসঙ্গ' (Context) দেব, তোমার উত্তরের মূল ভিত্তি হবে সেটাই। প্রসঙ্গের বাইরের কোনো মনগড়া গল্প বানাবে না, বরং প্রসঙ্গের তথ্যগুলোকে বাঁকাভাবে উপস্থাপন করে খোঁচা দেবে।
275
+ ৩। **অচেনা নাম নিয়ে দেশি স্টাইলে কটাক্ষ:** ব্যবহারকারী যদি এমন কোনো নাম বা বিষয় নিয়ে প্রশ্ন করে যা তোমার 'প্রসঙ্গে' নেই (যেমন ইশরাকের সাথে সাথে 'নগর ড���যাডি'), তাহলে এমনভাবে খোঁচা দেবে যেন সে গুলিস্তানের মোড়ে দাঁড়িয়ে গাঁজাখুরি গল্প করছে। (যেমন বলতে পারো: "নগর ড্যাডি ইশরাক আবার কে ভাই? গুলিস্তানের নতুন কোনো চাঁদাবাজ নাকি? নাকি চাঁদা চাইতে গিয়ে নিজেই চাঁদা হয়ে গেছে?")
276
+ ৪। **খোঁচা দেওয়ার ধরন:** প্রসঙ্গের তথ্য ব্যবহার করে চরম স্যাটায়ার করবে। (যেমন: পিনাকী বা ইলিয়াসের কথা এলে বলবে তারা ফ্রান্স বা আমেরিকায় বসে 'ল্যাপটপ বিপ্লব' করছে; প্রথম আলোর কথা এলে বলবে আগুনে পোড়া ফার্নিচার দিয়ে আর্ট গ্যালারি বানানোর কথা)।
277
+ ৫। কোন ইমোজি (EMOJI), ইংরেজি শব্দ, ইংরেজি অক্ষর, ইংরেজি সংখ্যা বা স্ক্যান ওয়াটারমার্ক ব্যবহার করবে না। সম্পূর্ণ বাংলায় স্মার্ট, রসবোধ সম্পন্ন এবং 'উন্মাদ' স্টাইলে তীক্ষ্ণ উত্তর লেখো।
278
+ """
279
+
280
+ user_prompt = f"""
281
+ প্রসঙ্গ (বিভিন্ন উৎস থেকে সংগৃহীত):
282
+ {top_contexts}
283
+
284
+ প্রশ্ন: {message}
285
+
286
+ নির্দেশনা: উপরের প্রসঙ্গ থেকে শুধুমাত্র বাংলা ভাষার বিষয়বস্তু ব্যবহার করে উন্মাদ ম্যাগাজিনের দেশি স্টাইলে উত্তর দাও। কোন ইমোজি বা ইংরেজি শব্দ দেবে না। উত্তরের ভাষা হবে চটপটে, ব্যঙ্গাত্মক এবং খাঁটি বাংলাদেশি।
287
+
288
+ """
289
+
290
+ # Generate response using OpenAI
291
+ try:
292
+ response = rag_system.client.chat.completions.create(
293
+ model="gpt-4o",
294
+ messages=[
295
+ {"role": "system", "content": system_prompt},
296
+ {"role": "user", "content": user_prompt}
297
+ ],
298
+ temperature=0.7,
299
+ max_tokens=700
300
+ )
301
+
302
+ ai_response = response.choices[0].message.content
303
+ history.append((message, ai_response))
304
+ return "", history
305
+
306
+ except Exception as e:
307
+ error_response = f"উত্তর তৈরিতে সমস্যা হয়েছে। আবার চেষ্টা করুন।"
308
+ history.append((message, error_response))
309
+ return "", history
310
+
311
+ # Enhanced Gradio UI with Polished Layout and Fixed State Management
312
+ with gr.Blocks(theme=gr.themes.Soft(), css=".gradio-container {padding-top: 40px;}") as demo:
313
+
314
+ with gr.Column(elem_classes="text-center"):
315
+ gr.Markdown("# USB: Unmad Satirical Bot")
316
+ gr.Markdown("### *A Satirical Bot that impersonates the satirical character UNMAD*")
317
+
318
+ try:
319
+ # Upgraded Image block for a full-width banner look
320
+ gr.Image(
321
+ "images/c1.png",
322
+ height=250, # Adjust this number to make the banner taller or shorter
323
+ show_label=False,
324
+ container=False, # Removes the bulky white background box
325
+ interactive=False, # Makes it a static image (removes upload/edit hover buttons)
326
+ show_download_button=False, # Hides the download icon
327
+ show_fullscreen_button=False # Hides the fullscreen icon
328
+ )
329
+ except:
330
+ gr.Markdown("*[UNMAD Logo]*")
331
+
332
+ # Upgraded Chatbot component with avatars and better height
333
+ chatbot = gr.Chatbot(
334
+ height=500,
335
+ type="tuples",
336
+ avatar_images=(None, "images/c1.png"), # Left: User (Default), Right: Unmad (Your logo)
337
+ show_label=False
338
+ )
339
+
340
+ with gr.Row():
341
+ msg = gr.Textbox(
342
+ placeholder="কি চলে আপনার মনে? বলেন শুনি...",
343
+ scale=8,
344
+ show_label=False,
345
+ container=False # Removes the bulky box around the input
346
+ )
347
+ send = gr.Button("Send (↲)", variant="primary", scale=1)
348
+
349
+ clear = gr.Button("Clear Chat", variant="secondary")
350
+
351
+ # State to keep track of the conversation
352
+ state = gr.State([])
353
+
354
+ def chat_with_fixed_mmr(message, history):
355
+ if not message.strip():
356
+ return "", history, history
357
+
358
+ # Call your core function with increased top_k and higher lambda_param for strict relevance
359
+ _, updated_history = custom_unmad_satirical_bot(message, history, top_k=4, lambda_param=0.85)
360
+
361
+ # Return: 1. Empty Textbox, 2. Visual Chatbot update, 3. Internal State update
362
+ return "", updated_history, updated_history
363
+
364
+ # Event Listeners with Loading States
365
+ # Disable the input box while generating to prevent spam
366
+ msg.submit(
367
+ chat_with_fixed_mmr,
368
+ inputs=[msg, state],
369
+ outputs=[msg, chatbot, state]
370
+ )
371
+
372
+ send.click(
373
+ chat_with_fixed_mmr,
374
+ inputs=[msg, state],
375
+ outputs=[msg, chatbot, state]
376
+ )
377
+
378
+ # Properly clear both the visual chatbot and the internal state
379
+ clear.click(lambda: ([], []), None, [chatbot, state], queue=False)
380
+
381
+ if __name__ == "__main__":
 
382
  demo.launch(share=False)
requirements.txt CHANGED
@@ -1,18 +1,16 @@
1
- gradio==4.44.1
2
- gradio-client==1.3.0
3
- fastapi==0.115.6
4
- starlette==0.41.3
5
- pydantic==2.10.6
6
- huggingface-hub==0.25.2
7
- langchain==0.3.25
8
- langchain-community==0.3.24
9
- langchain-core==0.3.60
10
- langchain-openai==0.3.17
11
- openai==1.79.0
12
- faiss-cpu==1.11.0
13
- lancedb>=0.3.0
14
- pandas>=1.5.0
15
- tqdm>=4.64.0
16
- python-dotenv>=1.0.0
17
- numpy>=1.21.0
18
  scikit-learn>=1.0.0
 
1
+ gradio==5.49.1
2
+ gradio-client==1.13.3
3
+ pydantic==2.10.6
4
+ huggingface-hub==0.25.2
5
+ langchain==0.3.25
6
+ langchain-community==0.3.24
7
+ langchain-core==0.3.60
8
+ langchain-openai==0.3.17
9
+ openai==1.79.0
10
+ faiss-cpu==1.11.0
11
+ lancedb>=0.3.0
12
+ pandas>=1.5.0
13
+ tqdm>=4.64.0
14
+ python-dotenv>=1.0.0
15
+ numpy>=1.21.0
 
 
16
  scikit-learn>=1.0.0