Spaces:
Sleeping
Sleeping
Upload 3 files
Browse files- README.md +12 -12
- app.py +381 -382
- 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:
|
| 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 |
-
|
| 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 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 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==
|
| 2 |
-
gradio-client==1.
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
langchain==0.3.
|
| 8 |
-
langchain-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 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
|