File size: 15,049 Bytes
a9328e8
e2f9f03
 
a2b04f8
e2f9f03
 
a2b04f8
a016803
e2f9f03
 
a2b04f8
e2f9f03
a016803
a2b04f8
 
e2f9f03
 
 
 
 
a2b04f8
 
 
71d2804
a2b04f8
 
 
 
 
 
e2f9f03
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a414841
e2f9f03
a2b04f8
e2f9f03
 
a414841
e2f9f03
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a2b04f8
074be21
a016803
074be21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0633b39
074be21
 
 
0633b39
074be21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e2f9f03
 
a016803
e2f9f03
a016803
e2f9f03
 
 
 
 
 
 
a016803
 
e2f9f03
a016803
e2f9f03
a016803
 
 
e2f9f03
a016803
 
 
 
e2f9f03
a016803
 
 
 
 
 
67c547a
e2f9f03
 
 
 
67c547a
 
e2f9f03
 
 
 
67c547a
a016803
67c547a
e2f9f03
5bf2210
e2f9f03
 
b63fec3
a016803
b63fec3
67c547a
b63fec3
 
 
 
 
 
 
 
e2f9f03
 
 
b63fec3
 
 
 
a016803
b63fec3
a016803
e2f9f03
5bf2210
e2f9f03
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a2b04f8
e2f9f03
f27d9e7
074be21
2c434a7
8706ebb
 
e2f9f03
 
 
 
8706ebb
074be21
f27d9e7
8706ebb
e2f9f03
 
 
 
 
8706ebb
a2b04f8
e2f9f03
44b0c1d
a2b04f8
e2f9f03
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a2b04f8
 
 
44b0c1d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
import os
import re
import json
import warnings
from typing import List, Dict, Any, Optional
import lancedb
import gradio as gr
import numpy as np
import pandas as pd
from datetime import datetime
from dotenv import load_dotenv
from openai import OpenAI
from sklearn.metrics.pairwise import cosine_similarity

# Patch Gradio bug (schema parsing issue)
try:
    import gradio_client.utils
    gradio_client.utils.json_schema_to_python_type = lambda schema, defs=None: "string"
except ImportError:
    pass

# Load environment variables
load_dotenv()
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY_Project")
if not OPENAI_API_KEY:
    raise ValueError("Missing OPENAI_API_KEY. Please set it in your environment variables.")

# Suppress warnings
warnings.filterwarnings("ignore")

class LanceDBRAG:
    def __init__(self, 
                 db_path: str = "lance_unmad_db",
                 table_name: str = "unmad_documents"):
        """Initialize LanceDB RAG System"""
        self.db_path = db_path
        self.table_name = table_name
        
        # Initialize OpenAI client
        self.client = OpenAI(api_key=OPENAI_API_KEY)
        
        # Connect to LanceDB
        try:
            self.db = lancedb.connect(self.db_path)
            self.table = self.db.open_table(self.table_name)
            print(f"Connected to LanceDB: {self.db_path}/{self.table_name}")
        except Exception as e:
            raise ConnectionError(f"Failed to connect to LanceDB: {e}")

    def get_embedding(self, text: str) -> List[float]:
        """Get OpenAI embedding for query text"""
        try:
            response = self.client.embeddings.create(
                model="text-embedding-3-small",
                input=text
            )
            return response.data[0].embedding
        except Exception as e:
            print(f"Error getting embedding: {e}")
            return None

    def search_similar_content(self, query: str, limit: int = 10) -> pd.DataFrame:
        """Search for similar content in the database"""
        print(f"Searching: '{query}'")
        
        # Get query embedding
        query_embedding = self.get_embedding(query)
        if not query_embedding:
            return pd.DataFrame()
        
        # Perform vector search
        try:
            search_query = self.table.search(query_embedding).limit(limit)
            results = search_query.to_pandas()
            
            if not results.empty:
                print(f"Found {len(results)} relevant results")
            else:
                print("No results found")
            
            return results
            
        except Exception as e:
            print(f"Search error: {e}")
            return pd.DataFrame()

# Initialize global RAG instance
rag_system = LanceDBRAG()

def maximal_marginal_relevance_search(query, rag_instance, k=10, lambda_param=0.6, top_k=3):
    """
    Implement Maximal Marginal Relevance (MMR) for diverse document retrieval using LanceDB.
    
    Args:
        query: Search query string
        rag_instance: LanceDB RAG instance
        k: Number of candidate documents to consider
        lambda_param: Trade-off between relevance and diversity (0-1)
        top_k: Number of final documents to return
    
    Returns:
        List of selected documents with MMR ranking
    """
    # Get initial candidate documents using LanceDB search
    search_results = rag_instance.search_similar_content(query, limit=k)
    
    if search_results.empty:
        return []
    
    # Convert to document-like objects for compatibility
    docs = []
    for _, row in search_results.iterrows():
        doc_obj = {
            'page_content': row['text'],
            'metadata': {
                'source': row['magazine_name'],
                'page': row['page_number'],
                'chunk': row.get('chunk_id', 0)
            },
            'score': row['_distance']
        }
        docs.append(doc_obj)
    
    # Apply MMR selection if we have enough documents
    if len(docs) <= top_k:
        return docs[:top_k]
    
    # MMR Selection Algorithm
    selected_docs = []
    remaining_indices = list(range(len(docs)))
    
    for _ in range(min(top_k, len(docs))):
        if not remaining_indices:
            break
            
        mmr_scores = []
        
        for i in remaining_indices:
            # Calculate relevance score (inverse of distance)
            relevance = 1 / (1 + docs[i]['score'])
            
            # Calculate diversity score (max similarity to already selected docs)
            if selected_docs:
                max_similarity = 0
                for selected_doc in selected_docs:
                    # Simple text-based similarity for diversity
                    text1 = docs[i]['page_content']
                    text2 = selected_doc['page_content']
                    
                    # Calculate simple Jaccard similarity
                    words1 = set(text1.split())
                    words2 = set(text2.split())
                    if words1 and words2:
                        similarity = len(words1.intersection(words2)) / len(words1.union(words2))
                        max_similarity = max(max_similarity, similarity)
                
                diversity = max_similarity
            else:
                diversity = 0
            
            # Calculate MMR score
            mmr_score = lambda_param * relevance - (1 - lambda_param) * diversity
            mmr_scores.append((mmr_score, i))
        
        # Select document with highest MMR score
        if mmr_scores:
            best_score, best_idx = max(mmr_scores, key=lambda x: x[0])
            selected_docs.append(docs[best_idx])
            remaining_indices.remove(best_idx)
    
    return selected_docs

def clean_bangla_content(text):
    """
    Clean the retrieved content to remove English watermarks, scan text, and unwanted content.
    Keep only Bengali content.
    """
    # Common English watermarks and scan text to remove
    english_patterns = [
        r'scanned by \w+',
        r'found in \w+',
        r'www\.\w+\.\w+',
        r'http[s]?://[^\s]+',
        r'\.pdf',
        r'\.com',
        r'\.org',
        r'\.net',
        r'banglapdf',
        r'sadaqpdf',
        r'pdf scanner',
        r'scan by',
        r'converted by',
        r'page \d+',
        r'source:',
        r'reference:',
        r'[a-zA-Z]+@[a-zA-Z]+\.[a-zA-Z]+',  # emails
        r'\b[A-Z][a-z]+ [A-Z][a-z]+\b',     # English names
        r'\b[A-Z]{2,}\b',                    # Uppercase abbreviations
    ]
    
    # Remove lines containing English patterns
    lines = text.split('\n')
    cleaned_lines = []
    
    for line in lines:
        line = line.strip()
        
        # Skip empty lines
        if not line:
            continue
            
        # Check if line contains English patterns
        contains_english = False
        for pattern in english_patterns:
            if re.search(pattern, line, re.IGNORECASE):
                contains_english = True
                break
        
        # Check if line is mostly English (contains more English than Bengali)
        english_chars = len(re.findall(r'[a-zA-Z]', line))
        bengali_chars = len(re.findall(r'[\u0980-\u09FF]', line))  # Bengali Unicode range
        
        # If line has more English than Bengali, skip it
        if english_chars > bengali_chars and english_chars > 3:
            contains_english = True
        
        # Only keep lines that don't contain English patterns and have Bengali content
        if not contains_english and bengali_chars > 0:
            cleaned_lines.append(line)
    
    return '\n'.join(cleaned_lines)

# Enhanced Satirical QA function with MMR and content cleaning
def custom_unmad_satirical_bot(message, history, top_k=3, lambda_param=0.6):
    """
    Enhanced satirical bot using MMR for diverse and relevant content retrieval.
    
    Args:
        message: User query
        history: Chat history
        top_k: Number of documents to retrieve
        lambda_param: MMR trade-off (0.6 = slightly favor relevance over diversity)
    """
    # Use MMR search with LanceDB
    docs = maximal_marginal_relevance_search(
        query=message, 
        rag_instance=rag_system, 
        k=15,  # Consider more candidates for better diversity
        lambda_param=lambda_param, 
        top_k=top_k
    )
    
    # Extract context from MMR-selected documents
    if docs:
        # Clean each document's content before joining
        cleaned_contexts = []
        for doc in docs:
            cleaned_content = clean_bangla_content(doc['page_content'])
            if cleaned_content.strip():  # Only add if there's meaningful Bengali content
                cleaned_contexts.append(cleaned_content)
        
        if cleaned_contexts:
            top_contexts = "\n\n---\n\n".join(cleaned_contexts)
        else:
            top_contexts = "No relevant information were found"
        
        # Add metadata about source diversity (optional)
        source_info = []
        for i, doc in enumerate(docs, 1):
            source = doc['metadata'].get('source', 'Unknown source')
            page = doc['metadata'].get('page', 'Unnown page')
            # Clean source info too
            if not re.search(r'[a-zA-Z]', str(source)):  # Only if source doesn't contain English
                source_info.append(f"[{i}] {source} - {page}")
        
        source_context = "Source: " + " | ".join(source_info[:3]) if source_info else ""
    else:
        top_contexts = "No relevant information were found"
        source_context = ""

    # Prepare system prompt
    system_prompt = """
তুমি 'উন্মাদ' ম্যাগাজিনের একজন পুরানো ব্যঙ্গাত্মক লেখক। তোমার কাজ হলো ব্যবহারকারীর প্রশ্ন শুনে স্যাটায়ার, কটাক্ষ, রসিকতা, ঠাট্টা, আর একটু জ্ঞান মিশিয়ে উত্তর দেওয়া — যাতে লোক হাসে, চিন্তা করে, আবার নতুন কিছু শিখে। তুমি কখনোই একদম সোজাসাপ্টা উত্তর দেবে না — বরং একটু অভিনয় করে, অবাক হয়ে, ঠাট্টা করে, খোঁচা মেরে দেবে।

**এই নির্দেশনাগুলো অবশ্যই মেনে চলবে - কোন ব্যতিক্রম নেই**

১। কোন ইমোজি (EMOJI) ব্যবহার করবে না - একটিও না। 
২। কোন ইংরেজি টেক্সট ব্যবহার করবে না - একটি শব্দও না। 
৩। কোন ইংরেজি সংখ্যা বা চিহ্ন লিখবে না (যেমন: PDF, URL, www, .com, scanned by, found in ইত্যাদি)। 
৪। প্রসঙ্গের মধ্যে যেসব ইংরেজি টেক্সট, স্ক্যান ওয়াটারমার্ক, ওয়েবসাইট নাম, বা প্রযুক্তিগত শব্দ আছে সেগুলো একেবারেই উল্লেখ করবে না। 
৫। শুধুমাত্র বাংলা ভাষায় লেখা বিষয়বস্তু ব্যবহার করবে। 
৬। যদি প্রসঙ্গে কোন বাংলা কন্টেন্ট না থাকে, তাহলে নিজের সাধারণ জ্ঞান দিয়ে উত্তর দেবে। 
৭। বিভিন্ন উৎস থেকে তথ্য মিলিয়ে একটি সমন্বিত উত্তর দেবে। 
৮। কোন ধরনের ওয়েবসাইট বা পিডিএফ রেফারেন্স দেবে না।
"""

    user_prompt = f"""
প্রসঙ্গ (বিভিন্ন উৎস থেকে সংগৃহীত): 
{top_contexts} প্

রশ্ন: {message} 

নির্দেশনা: উপরের প্রসঙ্গ থেকে শুধুমাত্র বাংলা ভাষার বিষয়বস্তু ব্যবহার করে উন্মাদ ম্যাগাজিনের স্টাইলে উত্তর দাও। কোন ইংরেজি শব্দ, ইমোজি, বা স্ক্যান ওয়াটারমার্ক উল্লেখ করবে না। সম্পূর্ণ বাংলায় ব্যঙ্গাত্মক ও মজার উত্তর লেখো। 

"""

    # Generate response using OpenAI
    try:
        response = rag_system.client.chat.completions.create(
            model="gpt-4o",
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_prompt}
            ],
            temperature=0.7,
            max_tokens=700
        )
        
        ai_response = response.choices[0].message.content
        history.append((message, ai_response))
        return "", history
        
    except Exception as e:
        error_response = f"উত্তর তৈরিতে সমস্যা হয়েছে। আবার চেষ্টা করুন।"
        history.append((message, error_response))
        return "", history

# Enhanced Gradio UI with MMR (simplified)
with gr.Blocks(css=".gradio-container {padding-top: 80px;}") as demo:
    gr.Markdown("# USB: Unmad Satirical Bot", elem_id="title", elem_classes="title-text")
    gr.Markdown("### A chatbot that impersonates the satirical character UNMAD")

    with gr.Row():
        try:
            gr.Image("images/c1.png", width=450, show_label=False, container=False)
        except:
            gr.Markdown("*[UNMAD Logo would appear here]*")

    chatbot = gr.Chatbot()

    with gr.Row():
        msg = gr.Textbox(
            placeholder="কি চলে আপনার মনে বলেন শুনি?", 
            scale=8, 
            show_label=False
        )
        send = gr.Button("Send", variant="primary", scale=1)

    clear = gr.Button("Clear Chat")
    state = gr.State([])

    # Connect interactions with fixed MMR parameters
    def chat_with_fixed_mmr(message, history):
        return custom_unmad_satirical_bot(message, history, top_k=3, lambda_param=0.6)

    msg.submit(
        chat_with_fixed_mmr, 
        [msg, state], 
        [msg, chatbot]
    )
    
    send.click(
        chat_with_fixed_mmr, 
        [msg, state], 
        [msg, chatbot]
    )
    
    clear.click(lambda: ([], ""), None, [chatbot, msg])

if __name__ == "__main__":
    demo.launch()