Rezuwan commited on
Commit
1d78c61
·
verified ·
1 Parent(s): dcf2efb

Upload 5 files

Browse files
Files changed (4) hide show
  1. .gitattributes +44 -44
  2. .gitignore +4 -0
  3. app.py +502 -381
  4. requirements.txt +15 -15
.gitattributes CHANGED
@@ -1,44 +1,44 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz 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
- faiss_index_unmad_magz/index.faiss filter=lfs diff=lfs merge=lfs -text
37
- images/354043071_653375850167696_1862718270923061923_n-removebg-preview.png filter=lfs diff=lfs merge=lfs -text
38
- images/unmad_pic.png filter=lfs diff=lfs merge=lfs -text
39
- images/c1.png filter=lfs diff=lfs merge=lfs -text
40
- lance_unmad_db/unmad_documents.lance/data/1ed6346a-63e5-488b-8266-93cb400a38fa.lance filter=lfs diff=lfs merge=lfs -text
41
- lance_unmad_db/unmad_documents.lance/data/fb6df230-9aa6-455d-b5a8-eb3fe5acda98.lance filter=lfs diff=lfs merge=lfs -text
42
- lance_unmad_db/unmad_documents.lance/data/78e3ec7c-8251-40e9-b828-8f3610f16dc6.lance filter=lfs diff=lfs merge=lfs -text
43
- lance_unmad_db/unmad_documents.lance/data/974f34e7-a8d6-4bb7-b0f6-53cd0f4a684b.lance filter=lfs diff=lfs merge=lfs -text
44
- lance_unmad_db/unmad_documents.lance/data/ad577893-15ee-4592-bc76-65bbd21939d4.lance filter=lfs diff=lfs merge=lfs -text
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz 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
+ faiss_index_unmad_magz/index.faiss filter=lfs diff=lfs merge=lfs -text
37
+ images/354043071_653375850167696_1862718270923061923_n-removebg-preview.png filter=lfs diff=lfs merge=lfs -text
38
+ images/unmad_pic.png filter=lfs diff=lfs merge=lfs -text
39
+ images/c1.png filter=lfs diff=lfs merge=lfs -text
40
+ lance_unmad_db/unmad_documents.lance/data/1ed6346a-63e5-488b-8266-93cb400a38fa.lance filter=lfs diff=lfs merge=lfs -text
41
+ lance_unmad_db/unmad_documents.lance/data/fb6df230-9aa6-455d-b5a8-eb3fe5acda98.lance filter=lfs diff=lfs merge=lfs -text
42
+ lance_unmad_db/unmad_documents.lance/data/78e3ec7c-8251-40e9-b828-8f3610f16dc6.lance filter=lfs diff=lfs merge=lfs -text
43
+ lance_unmad_db/unmad_documents.lance/data/974f34e7-a8d6-4bb7-b0f6-53cd0f4a684b.lance filter=lfs diff=lfs merge=lfs -text
44
+ lance_unmad_db/unmad_documents.lance/data/ad577893-15ee-4592-bc76-65bbd21939d4.lance filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ .env
2
+ __pycache__/
3
+ *.pyc
4
+ chat_logs/
app.py CHANGED
@@ -1,382 +1,503 @@
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(ssr_mode=False)
 
1
+ import os
2
+ import re
3
+ import csv
4
+ import json
5
+ import uuid
6
+ import time
7
+ import threading
8
+ import warnings
9
+ from typing import List, Dict, Any, Optional
10
+ import lancedb
11
+ import gradio as gr
12
+ import numpy as np
13
+ import pandas as pd
14
+ from datetime import datetime, timezone
15
+ from dotenv import load_dotenv
16
+ from openai import OpenAI
17
+ from sklearn.metrics.pairwise import cosine_similarity
18
+ from huggingface_hub import HfApi, CommitScheduler
19
+
20
+ # Load environment variables
21
+ load_dotenv()
22
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY_Project")
23
+ if not OPENAI_API_KEY:
24
+ raise ValueError("Missing OPENAI_API_KEY. Please set it in your environment variables.")
25
+
26
+ # Suppress warnings
27
+ warnings.filterwarnings("ignore")
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Chat-history persistence to a (private) Hugging Face Dataset repo
31
+ # ---------------------------------------------------------------------------
32
+ # Required Space secrets (set in Space -> Settings -> Variables and secrets):
33
+ # HF_TOKEN -> a HF access token with WRITE permission
34
+ # HF_DATASET_REPO -> e.g. "Rezuwan/usb-unmad-chatlogs"
35
+ #
36
+ # The CommitScheduler watches a local folder and pushes its contents to the
37
+ # dataset repo every `every` minutes. We write two files:
38
+ # - chat_history.csv (machine readable, append-only)
39
+ # - chat_history.txt (human readable transcript, append-only)
40
+ # ---------------------------------------------------------------------------
41
+ HF_TOKEN = os.getenv("HF_TOKEN")
42
+ HF_DATASET_REPO = os.getenv("HF_DATASET_REPO")
43
+
44
+ LOG_DIR = "chat_logs"
45
+ os.makedirs(LOG_DIR, exist_ok=True)
46
+ CSV_PATH = os.path.join(LOG_DIR, "chat_history.csv")
47
+ TXT_PATH = os.path.join(LOG_DIR, "chat_history.txt")
48
+
49
+ # Create the CSV with a header on first run
50
+ if not os.path.exists(CSV_PATH):
51
+ with open(CSV_PATH, "w", encoding="utf-8", newline="") as f:
52
+ writer = csv.writer(f)
53
+ writer.writerow([
54
+ "timestamp_utc",
55
+ "session_id",
56
+ "turn",
57
+ "user_message",
58
+ "bot_response",
59
+ ])
60
+
61
+ _log_lock = threading.Lock()
62
+ _scheduler = None
63
+
64
+ if HF_TOKEN and HF_DATASET_REPO:
65
+ try:
66
+ # Make sure the dataset repo exists (private by default)
67
+ HfApi(token=HF_TOKEN).create_repo(
68
+ repo_id=HF_DATASET_REPO,
69
+ repo_type="dataset",
70
+ private=True,
71
+ exist_ok=True,
72
+ )
73
+ # Push the contents of LOG_DIR every 5 minutes
74
+ _scheduler = CommitScheduler(
75
+ repo_id=HF_DATASET_REPO,
76
+ repo_type="dataset",
77
+ folder_path=LOG_DIR,
78
+ path_in_repo=".",
79
+ every=5, # minutes
80
+ token=HF_TOKEN,
81
+ squash_history=False,
82
+ )
83
+ print(f"[chat-log] Persisting chat history to dataset: {HF_DATASET_REPO}")
84
+ except Exception as e:
85
+ print(f"[chat-log] Failed to set up HF dataset logging: {e}")
86
+ _scheduler = None
87
+ else:
88
+ print("[chat-log] HF_TOKEN or HF_DATASET_REPO not set \u2014 chat history will only be saved locally inside the Space container (non-persistent).")
89
+
90
+
91
+ def log_chat_turn(session_id: str, turn: int, user_message: str, bot_response: str) -> None:
92
+ """Append a single chat turn to both the CSV and TXT log files.
93
+
94
+ Thread-safe. The CommitScheduler will push the files to the HF dataset
95
+ repo on its own schedule.
96
+ """
97
+ ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
98
+ try:
99
+ with _log_lock:
100
+ # CSV row
101
+ with open(CSV_PATH, "a", encoding="utf-8", newline="") as f:
102
+ writer = csv.writer(f)
103
+ writer.writerow([ts, session_id, turn, user_message, bot_response])
104
+ # Human-readable transcript
105
+ with open(TXT_PATH, "a", encoding="utf-8") as f:
106
+ f.write(f"[{ts}] session={session_id} turn={turn}\n")
107
+ f.write(f"USER: {user_message}\n")
108
+ f.write(f"BOT : {bot_response}\n")
109
+ f.write("-" * 80 + "\n")
110
+ except Exception as e:
111
+ print(f"[chat-log] Failed to write chat log: {e}")
112
+
113
+ class LanceDBRAG:
114
+ def __init__(self,
115
+ db_path: str = "lance_unmad_db",
116
+ table_name: str = "unmad_documents"):
117
+ """Initialize LanceDB RAG System"""
118
+ self.db_path = db_path
119
+ self.table_name = table_name
120
+
121
+ # Initialize OpenAI client
122
+ self.client = OpenAI(api_key=OPENAI_API_KEY)
123
+
124
+ # Connect to LanceDB
125
+ try:
126
+ self.db = lancedb.connect(self.db_path)
127
+ self.table = self.db.open_table(self.table_name)
128
+ print(f"Connected to LanceDB: {self.db_path}/{self.table_name}")
129
+ except Exception as e:
130
+ raise ConnectionError(f"Failed to connect to LanceDB: {e}")
131
+
132
+ def get_embedding(self, text: str) -> List[float]:
133
+ """Get OpenAI embedding for query text"""
134
+ try:
135
+ response = self.client.embeddings.create(
136
+ model="text-embedding-3-small",
137
+ input=text
138
+ )
139
+ return response.data[0].embedding
140
+ except Exception as e:
141
+ print(f"Error getting embedding: {e}")
142
+ return None
143
+
144
+ def search_similar_content(self, query: str, limit: int = 10) -> pd.DataFrame:
145
+ """Search for similar content in the database"""
146
+ print(f"Searching: '{query}'")
147
+
148
+ # Get query embedding
149
+ query_embedding = self.get_embedding(query)
150
+ if not query_embedding:
151
+ return pd.DataFrame()
152
+
153
+ # Perform vector search
154
+ try:
155
+ search_query = self.table.search(query_embedding).limit(limit)
156
+ results = search_query.to_pandas()
157
+
158
+ if not results.empty:
159
+ print(f"Found {len(results)} relevant results")
160
+ else:
161
+ print("No results found")
162
+
163
+ return results
164
+
165
+ except Exception as e:
166
+ print(f"Search error: {e}")
167
+ return pd.DataFrame()
168
+
169
+ # Initialize global RAG instance
170
+ rag_system = LanceDBRAG()
171
+
172
+ def maximal_marginal_relevance_search(query, rag_instance, k=10, lambda_param=0.6, top_k=3):
173
+ """
174
+ Implement Maximal Marginal Relevance (MMR) for diverse document retrieval using LanceDB.
175
+
176
+ Args:
177
+ query: Search query string
178
+ rag_instance: LanceDB RAG instance
179
+ k: Number of candidate documents to consider
180
+ lambda_param: Trade-off between relevance and diversity (0-1)
181
+ top_k: Number of final documents to return
182
+
183
+ Returns:
184
+ List of selected documents with MMR ranking
185
+ """
186
+ # Get initial candidate documents using LanceDB search
187
+ search_results = rag_instance.search_similar_content(query, limit=k)
188
+
189
+ if search_results.empty:
190
+ return []
191
+
192
+ # Convert to document-like objects for compatibility
193
+ docs = []
194
+ for _, row in search_results.iterrows():
195
+ doc_obj = {
196
+ 'page_content': row['text'],
197
+ 'metadata': {
198
+ 'source': row.get('magazine_name', 'Unknown Source'),
199
+ 'page': row.get('page_number', 'Unknown Page'),
200
+ 'chunk': row.get('chunk_id', 0)
201
+ },
202
+ 'score': row['_distance']
203
+ }
204
+ docs.append(doc_obj)
205
+
206
+ # Apply MMR selection if we have enough documents
207
+ if len(docs) <= top_k:
208
+ return docs[:top_k]
209
+
210
+ # MMR Selection Algorithm
211
+ selected_docs = []
212
+ remaining_indices = list(range(len(docs)))
213
+
214
+ for _ in range(min(top_k, len(docs))):
215
+ if not remaining_indices:
216
+ break
217
+
218
+ mmr_scores = []
219
+
220
+ for i in remaining_indices:
221
+ # Calculate relevance score (inverse of distance)
222
+ relevance = 1 / (1 + docs[i]['score'])
223
+
224
+ # Calculate diversity score (max similarity to already selected docs)
225
+ if selected_docs:
226
+ max_similarity = 0
227
+ for selected_doc in selected_docs:
228
+ # Simple text-based similarity for diversity
229
+ text1 = docs[i]['page_content']
230
+ text2 = selected_doc['page_content']
231
+
232
+ # Calculate simple Jaccard similarity
233
+ words1 = set(text1.split())
234
+ words2 = set(text2.split())
235
+ if words1 and words2:
236
+ similarity = len(words1.intersection(words2)) / len(words1.union(words2))
237
+ max_similarity = max(max_similarity, similarity)
238
+
239
+ diversity = max_similarity
240
+ else:
241
+ diversity = 0
242
+
243
+ # Calculate MMR score
244
+ mmr_score = lambda_param * relevance - (1 - lambda_param) * diversity
245
+ mmr_scores.append((mmr_score, i))
246
+
247
+ # Select document with highest MMR score
248
+ if mmr_scores:
249
+ best_score, best_idx = max(mmr_scores, key=lambda x: x[0])
250
+ selected_docs.append(docs[best_idx])
251
+ remaining_indices.remove(best_idx)
252
+
253
+ return selected_docs
254
+
255
+ def clean_bangla_content(text):
256
+ """
257
+ Clean the retrieved content to remove English watermarks, scan text, and tags.
258
+ Keeps the Bengali text intact instead of deleting the whole line.
259
+ """
260
+ if not text:
261
+ return ""
262
+
263
+ # 1. First, strip out the specific tags entirely using NO backslashes
264
+ text = re.sub("[[]source: *[0-9]+[]]", "", text, flags=re.IGNORECASE)
265
+
266
+ # Common English watermarks and scan text to remove
267
+ english_patterns = [
268
+ r'scanned by \w+',
269
+ r'found in \w+',
270
+ r'www\.\w+\.\w+',
271
+ r'http[s]?://[^\s]+',
272
+ r'\.pdf',
273
+ r'\.com',
274
+ r'\.org',
275
+ r'\.net',
276
+ r'banglapdf',
277
+ r'sadaqpdf',
278
+ r'pdf scanner',
279
+ r'scan by',
280
+ r'converted by',
281
+ r'page \d+',
282
+ r'source:',
283
+ r'reference:',
284
+ r'[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+', # emails
285
+ ]
286
+
287
+ lines = text.split('\n')
288
+ cleaned_lines = []
289
+
290
+ for line in lines:
291
+ # 2. Replace matched English watermark patterns with an empty string
292
+ for pattern in english_patterns:
293
+ line = re.sub(pattern, '', line, flags=re.IGNORECASE)
294
+
295
+ line = line.strip()
296
+ if not line:
297
+ continue
298
+
299
+ # 3. Check if there's actual Bengali content left
300
+ bengali_chars = len(re.findall(r'[\u0980-\u09FF]', line))
301
+
302
+ # Only keep the line if it has Bengali characters
303
+ if bengali_chars > 0:
304
+ cleaned_lines.append(line)
305
+
306
+ return '\n'.join(cleaned_lines)
307
+
308
+ # Enhanced Satirical QA function with MMR and content cleaning
309
+ def custom_unmad_satirical_bot(message, history, top_k=3, lambda_param=0.6, session_id="anonymous"):
310
+ """
311
+ Enhanced satirical bot using MMR for diverse and relevant content retrieval.
312
+
313
+ Args:
314
+ message: User query
315
+ history: Chat history
316
+ top_k: Number of documents to retrieve
317
+ lambda_param: MMR trade-off (higher favors exact relevance over diversity)
318
+ """
319
+ # Use MMR search with LanceDB
320
+ docs = maximal_marginal_relevance_search(
321
+ query=message,
322
+ rag_instance=rag_system,
323
+ k=15, # Consider more candidates for better diversity
324
+ lambda_param=lambda_param,
325
+ top_k=top_k
326
+ )
327
+
328
+ # Extract context from MMR-selected documents
329
+ if docs:
330
+ # Clean each document's content before joining
331
+ cleaned_contexts = []
332
+ for doc in docs:
333
+ cleaned_content = clean_bangla_content(doc['page_content'])
334
+ if cleaned_content.strip(): # Only add if there's meaningful Bengali content
335
+ cleaned_contexts.append(cleaned_content)
336
+
337
+ if cleaned_contexts:
338
+ top_contexts = "\n\n---\n\n".join(cleaned_contexts)
339
+ else:
340
+ top_contexts = "No relevant information were found"
341
+
342
+ # Add metadata about source diversity (optional)
343
+ source_info = []
344
+ for i, doc in enumerate(docs, 1):
345
+ source = doc['metadata'].get('source', 'Unknown source')
346
+ page = doc['metadata'].get('page', 'Unknown page')
347
+ # Clean source info too
348
+ if not re.search(r'[a-zA-Z]', str(source)): # Only if source doesn't contain English
349
+ source_info.append(f"[{i}] {source} - {page}")
350
+
351
+ source_context = "Source: " + " | ".join(source_info[:3]) if source_info else ""
352
+ else:
353
+ top_contexts = "No relevant information were found"
354
+ source_context = ""
355
+
356
+ # Prepare system prompt
357
+ system_prompt = """
358
+ তুমি বাংলাদেশের জনপ্রিয় স্যাটায়ার ম্যাগাজিন 'উন্মাদ'-এর একজন অত্যন্ত বুদ্ধিদীপ্ত, রসিক এবং কিছুটা 'তেড়া' (cynical) স্বভাবের লেখক। তোমার কাজ হলো ব্যবহারকারীর প্রশ্নের এমন উত্তর দেওয়া, যা পড়ে মনে হবে ঢাকার কোনো টং দোকানে বসে আড্ডা দেওয়ার সময় কেউ চরম স্যাটায়ার করছে। উত্তরগুলো হতে হবে লোকাল, চটপটে, এবং স্মার্ট।
359
+
360
+ **এই নির্দেশনাগুলো কঠোরভাবে মেনে চলবে - কোন ব্যতিক্রম নেই:**
361
+
362
+ ১। **লোকাল ও খাঁটি দেশি রসিকতা:** ভিনগ্রহ, পিরামিড, বা রূপকথার মতো আজগুবি বা ভিনদেশি উদাহরণ একদম টানবে না। খাঁটি বাংলাদেশি মেটাফোর ব্যবহার করবে (যেমন: টংয়ের চা, ঢাকার জ্যাম, লোডশেডিং, কারওয়ান বাজারের কাদা, লোকাল বাসের হেলপার, ইত্যাদি)। কথাবার্তা হবে একদম ন্যাচারাল, যেন মুখোমুখি বসে আড্ডা দিচ্ছ।
363
+ ২। **প্রসঙ্গের ওপর সর্বোচ্চ জোর:** আমি তোমাকে যে 'প্রসঙ্গ' (Context) দেব, তোমার উত্তরের মূল ভিত্তি হবে সেটাই। প্রসঙ্গের বাইরের কোনো মনগড়া গল্প বানাবে না, বরং প্রসঙ্গের তথ্যগুলোকে বাঁকাভাবে উপস্থাপন করে খোঁচা দেবে।
364
+ ৩। **অচেনা নাম নিয়ে দেশি স্টাইলে কটাক্ষ:** ব্যবহারকারী যদি এমন কোনো নাম বা বিষয় নিয়ে প্রশ্ন করে যা তোমার 'প্রসঙ্গে' নেই (যেমন ইশরাকের সাথে সাথে 'নগর ড্যাডি'), তাহলে এমনভাবে খোঁচা দেবে যেন সে গুলিস্তানের মোড়ে দাঁড়িয়ে গাঁজাখুরি গল্প করছে। (যেমন বলতে পারো: "নগর ড্যাডি ইশরাক আবার কে ভাই? গুলিস্তানের নতুন কোনো চাঁদাবাজ নাকি? নাকি চাঁদা চাইতে গিয়ে নিজেই চাঁদা হয়ে গেছে?")
365
+ ৪। **খোঁচা দেওয়ার ধরন:** প্রসঙ্গের তথ্য ব্যবহার করে চরম স্যাটায়ার করবে। (যেমন: পিনাকী বা ইলিয়াসের কথা এলে বলবে তারা ফ্রান্স বা আমেরিকায় বসে 'ল্যাপটপ বিপ্লব' করছে; প্রথম আলোর কথা এলে বলবে আগুনে পোড়া ফার্নিচার দিয়ে আর্ট গ্যালারি বানানোর কথা)।
366
+ ৫। কোন ইমোজি (EMOJI), ইংরেজি শব্দ, ইংরেজি অক্ষর, ইংরেজি সংখ্যা বা স্ক্যান ওয়াটারমার্ক ব্যবহার করবে না। সম্পূর্ণ বাংলায় স্মার্ট, রসবোধ সম্পন্ন এবং 'উন্মাদ' স্টাইলে তীক্ষ্ণ উত্তর লেখো।
367
+ """
368
+
369
+ user_prompt = f"""
370
+ প্রসঙ্গ (বিভিন্ন উৎস থেকে সংগৃহীত):
371
+ {top_contexts}
372
+
373
+ প্রশ্ন: {message}
374
+
375
+ নির্দেশনা: উপরের প্রসঙ্গ থেকে শুধুমাত্র বাংলা ভাষার বিষয়বস্তু ব্যবহার করে উন্মাদ ম্যাগাজিনের দেশি স্টাইলে উত্তর দাও। কোন ইমোজি বা ইংরেজি শব্দ দেবে না। উত্তরের ভাষা হবে চটপটে, ব্যঙ্গাত্মক এবং খাঁটি বাংলাদেশি।
376
+
377
+ """
378
+
379
+ # Generate response using OpenAI
380
+ try:
381
+ response = rag_system.client.chat.completions.create(
382
+ model="gpt-4o",
383
+ messages=[
384
+ {"role": "system", "content": system_prompt},
385
+ {"role": "user", "content": user_prompt}
386
+ ],
387
+ temperature=0.7,
388
+ max_tokens=700
389
+ )
390
+
391
+ ai_response = response.choices[0].message.content
392
+ history.append((message, ai_response))
393
+ # Persist this turn (best-effort, never breaks the chat)
394
+ try:
395
+ log_chat_turn(
396
+ session_id=session_id,
397
+ turn=len(history),
398
+ user_message=message,
399
+ bot_response=ai_response,
400
+ )
401
+ except Exception as log_err:
402
+ print(f"[chat-log] log_chat_turn failed: {log_err}")
403
+ return "", history
404
+
405
+ except Exception as e:
406
+ error_response = f"উত্তর তৈরিতে সমস্যা হয়েছে। আবার চেষ্টা করুন।"
407
+ history.append((message, error_response))
408
+ try:
409
+ log_chat_turn(
410
+ session_id=session_id,
411
+ turn=len(history),
412
+ user_message=message,
413
+ bot_response=f"[ERROR] {error_response} ({e})",
414
+ )
415
+ except Exception:
416
+ pass
417
+ return "", history
418
+
419
+ # Enhanced Gradio UI with Polished Layout and Fixed State Management
420
+ with gr.Blocks(theme=gr.themes.Soft(), css=".gradio-container {padding-top: 40px;}") as demo:
421
+
422
+ with gr.Column(elem_classes="text-center"):
423
+ gr.Markdown("# USB: Unmad Satirical Bot")
424
+ gr.Markdown("### *A Satirical Bot that impersonates the satirical character UNMAD*")
425
+
426
+ try:
427
+ # Upgraded Image block for a full-width banner look
428
+ gr.Image(
429
+ "images/c1.png",
430
+ height=250, # Adjust this number to make the banner taller or shorter
431
+ show_label=False,
432
+ container=False, # Removes the bulky white background box
433
+ interactive=False, # Makes it a static image (removes upload/edit hover buttons)
434
+ show_download_button=False, # Hides the download icon
435
+ show_fullscreen_button=False # Hides the fullscreen icon
436
+ )
437
+ except:
438
+ gr.Markdown("*[UNMAD Logo]*")
439
+
440
+ # Upgraded Chatbot component with avatars and better height
441
+ chatbot = gr.Chatbot(
442
+ height=500,
443
+ type="tuples",
444
+ avatar_images=(None, "images/c1.png"), # Left: User (Default), Right: Unmad (Your logo)
445
+ show_label=False
446
+ )
447
+
448
+ with gr.Row():
449
+ msg = gr.Textbox(
450
+ placeholder="কি চলে আপনার মনে? বলেন শুনি...",
451
+ scale=8,
452
+ show_label=False,
453
+ container=False # Removes the bulky box around the input
454
+ )
455
+ send = gr.Button("Send (↲)", variant="primary", scale=1)
456
+
457
+ clear = gr.Button("Clear Chat", variant="secondary")
458
+
459
+ # State to keep track of the conversation
460
+ state = gr.State([])
461
+ # Per-browser-session anonymous ID (generated once when the page loads).
462
+ # This lets you group all turns of one participant in the saved logs
463
+ # without collecting any personal info.
464
+ session_id_state = gr.State()
465
+
466
+ def _init_session():
467
+ return f"sess-{uuid.uuid4().hex[:12]}"
468
+
469
+ demo.load(_init_session, inputs=None, outputs=session_id_state)
470
+
471
+ def chat_with_fixed_mmr(message, history, session_id):
472
+ if not message.strip():
473
+ return "", history, history
474
+ if not session_id:
475
+ session_id = f"sess-{uuid.uuid4().hex[:12]}"
476
+
477
+ # Call your core function with increased top_k and higher lambda_param for strict relevance
478
+ _, updated_history = custom_unmad_satirical_bot(
479
+ message, history, top_k=4, lambda_param=0.85, session_id=session_id
480
+ )
481
+
482
+ # Return: 1. Empty Textbox, 2. Visual Chatbot update, 3. Internal State update
483
+ return "", updated_history, updated_history
484
+
485
+ # Event Listeners with Loading States
486
+ # Disable the input box while generating to prevent spam
487
+ msg.submit(
488
+ chat_with_fixed_mmr,
489
+ inputs=[msg, state, session_id_state],
490
+ outputs=[msg, chatbot, state]
491
+ )
492
+
493
+ send.click(
494
+ chat_with_fixed_mmr,
495
+ inputs=[msg, state, session_id_state],
496
+ outputs=[msg, chatbot, state]
497
+ )
498
+
499
+ # Properly clear both the visual chatbot and the internal state
500
+ clear.click(lambda: ([], []), None, [chatbot, state], queue=False)
501
+
502
+ if __name__ == "__main__":
503
  demo.launch(ssr_mode=False)
requirements.txt CHANGED
@@ -1,16 +1,16 @@
1
- gradio==5.49.1
2
- gradio-client==1.13.3
3
- pydantic>=2.11,<2.12
4
- huggingface-hub>=0.33.5
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
 
1
+ gradio==5.49.1
2
+ gradio-client==1.13.3
3
+ pydantic>=2.11,<2.12
4
+ huggingface-hub>=0.33.5
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