OmidSakaki commited on
Commit
1e8a285
·
verified ·
1 Parent(s): b22885c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +605 -108
app.py CHANGED
@@ -1,119 +1,616 @@
1
  """
2
- Gradio app to run the ProfessionalRAGSystem in Hugging Face Spaces.
3
-
4
- - Indexes sample documents at startup (persists to ./tmp_index).
5
- - Provides a simple UI to ask queries and optionally provide metadata filters as JSON.
6
-
7
- Usage:
8
- - Put this file and rag_system.py in the same directory.
9
- - Ensure requirements.txt is installed in the Space.
10
- - Run Space (Gradio will serve the app).
11
  """
 
12
  import os
13
- import json
14
- import threading
15
  import time
16
- from typing import Optional, Dict, Any
17
-
18
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
- from rag_system import ProfessionalRAGSystem, create_sample_documents
 
 
 
 
 
 
 
 
 
21
 
22
- # Initialize system (use_gpu=False by default in Spaces for safety)
23
- RAG = None
24
- INDEX_DIR = "./tmp_index"
25
- SYSTEM_LOCK = threading.Lock()
 
 
 
26
 
27
- def initialize_system():
28
- global RAG
29
- with SYSTEM_LOCK:
30
- if RAG is not None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  return
32
- # You can change embedding_model to a smaller one if desired
33
- # e.g. "sentence-transformers/all-MiniLM-L6-v2" to reduce download/time.
34
- RAG = ProfessionalRAGSystem(embedding_model="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", use_gpu=False)
35
- sample_docs = create_sample_documents()
36
- RAG.index_documents(sample_docs, persist_dir=INDEX_DIR)
37
-
38
- # Initialize in background to avoid blocking startup
39
- init_thread = threading.Thread(target=initialize_system, daemon=True)
40
- init_thread.start()
41
-
42
- def wait_for_init(timeout: float = 30.0):
43
- start = time.time()
44
- while time.time() - start < timeout:
45
- if RAG is not None:
46
- return True
47
- time.sleep(0.5)
48
- return False
49
-
50
- def parse_metadata_filters(text: str) -> Optional[Dict[str, Any]]:
51
- if not text:
52
- return None
53
- try:
54
- data = json.loads(text)
55
- if isinstance(data, dict):
56
- return data
57
- except Exception:
58
- return None
59
- return None
60
-
61
- def ask_question(question: str, metadata_json: str = "") -> Dict[str, Any]:
62
- if not wait_for_init():
63
- return {"answer": "در حال آماده‌سازی سیستم... لطفاً چند لحظه صبر کنید.", "sources": [], "confidence": 0.0, "processing_time": 0.0}
64
- metadata_filters = parse_metadata_filters(metadata_json)
65
- start = time.time()
66
- res = RAG.query(question, metadata_filters=metadata_filters)
67
- res['processing_time'] = round(res.get('processing_time', time.time() - start), 3)
68
- return res
69
-
70
- def reindex_documents(docs_json: str) -> str:
71
- if not wait_for_init():
72
- return "System initializing. لطفاً بعدا تلاش کنید."
73
- try:
74
- docs = json.loads(docs_json)
75
- if not isinstance(docs, list):
76
- return "لطفاً یک لیست JSON از اسناد ارسال کنید."
77
- RAG.index_documents(docs, persist_dir=INDEX_DIR)
78
- return f"ایندکس با موفقیت ساخته شد: {len(docs)} سند"
79
- except Exception as e:
80
- return f"خطا در بارگذاری اسناد: {e}"
81
-
82
- def get_status() -> str:
83
- if RAG is None:
84
- return "در حال آماده‌سازی سیستم..."
85
- return "سیستم آماده است. می‌توانید سوالات را ارسال کنید."
86
-
87
- with gr.Blocks(title="Professional RAG (HF Space)") as demo:
88
- gr.Markdown("## سیستم RAG حرفه‌ای — نسخه Gradio برای Hugging Face Spaces")
89
- with gr.Row():
90
- with gr.Column(scale=3):
91
- question = gr.Textbox(label="سوال (به فارسی یا انگلیسی)", lines=2, placeholder="مثال: یادگیری عمیق چیست؟")
92
- metadata = gr.Textbox(label="فیلتر متادیتا (JSON) — اختیاری", lines=2, placeholder='{"source": "ویکی‌پدیا"}')
93
- ask_btn = gr.Button("پرسش کن")
94
- output = gr.JSON(label="نتیجه")
95
- with gr.Column(scale=1):
96
- reindex_area = gr.Textbox(label="بارگذاری اسناد جدید (لیست JSON از داک‌ها)", lines=12, placeholder='[{"id":"docX","text":"...","meta":{"source":"X"}}]')
97
- reindex_btn = gr.Button("ایندکس مجدد با اسناد جدید")
98
- status = gr.Textbox(label="وضعیت سیستم", interactive=False)
99
- refresh_btn = gr.Button("به‌روزرسانی وضعیت")
100
-
101
- def _ask(q, m):
102
- res = ask_question(q, m)
103
- return res
104
-
105
- def _reindex(docs_json):
106
- return reindex_documents(docs_json)
107
-
108
- def _refresh():
109
- return get_status()
110
-
111
- ask_btn.click(fn=_ask, inputs=[question, metadata], outputs=[output])
112
- reindex_btn.click(fn=_reindex, inputs=[reindex_area], outputs=[status])
113
- refresh_btn.click(fn=_refresh, inputs=None, outputs=[status])
114
-
115
- # set initial status value
116
- status.value = get_status()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
 
 
 
118
  if __name__ == "__main__":
119
- demo.launch()
 
 
 
 
 
 
 
1
  """
2
+ RAG System for Hugging Face Spaces
3
+ Optimized for deployment on HF Spaces with GPU support
 
 
 
 
 
 
 
4
  """
5
+
6
  import os
7
+ import re
 
8
  import time
9
+ import json
 
10
  import gradio as gr
11
+ from dataclasses import dataclass
12
+ from typing import List, Dict, Tuple, Any, Optional
13
+ from collections import defaultdict
14
+
15
+ import numpy as np
16
+ from tqdm.auto import tqdm
17
+
18
+ # NLP
19
+ import nltk
20
+ from nltk.tokenize import sent_tokenize, word_tokenize
21
+ import langdetect
22
+
23
+ # Embedding & ranking models
24
+ from sentence_transformers import SentenceTransformer
25
+ import faiss
26
+ from rank_bm25 import BM25Okapi
27
+
28
+ # Ensure punkt tokenizer is available
29
+ try:
30
+ nltk.download('punkt', quiet=True)
31
+ nltk.download('punkt_tab', quiet=True)
32
+ except Exception:
33
+ pass
34
+
35
+ # -------------------------
36
+ # Data classes
37
+ # -------------------------
38
+ @dataclass
39
+ class Chunk:
40
+ id: str
41
+ text: str
42
+ meta: Dict[str, Any]
43
+ chunk_id: int
44
+ embedding: Optional[np.ndarray] = None
45
+ language: str = "unknown"
46
+
47
+ # -------------------------
48
+ # Document processing
49
+ # -------------------------
50
+ class DocumentProcessor:
51
+ def __init__(self):
52
+ self.supported_languages = ['fa', 'en', 'ar', 'es', 'fr']
53
+
54
+ def detect_language(self, text: str) -> str:
55
+ if not text or not text.strip():
56
+ return 'unknown'
57
+ try:
58
+ lang = langdetect.detect(text[:500])
59
+ return lang if lang in self.supported_languages else 'unknown'
60
+ except Exception:
61
+ return 'unknown'
62
+
63
+ def clean_text(self, text: str, language: str = 'fa') -> str:
64
+ if not text:
65
+ return ""
66
+ text = str(text)
67
+ text = re.sub(r'\s+', ' ', text).strip()
68
+ return text
69
+
70
+ def smart_sent_tokenize(self, text: str, language: str) -> List[str]:
71
+ try:
72
+ if language == 'fa':
73
+ sentences = re.split(r'[.!?؟۔]+', text)
74
+ else:
75
+ sentences = sent_tokenize(text)
76
+ return [s.strip() for s in sentences if len(s.strip()) > 10]
77
+ except Exception:
78
+ return [text.strip()] if text else []
79
+
80
+ def semantic_chunking(self, text: str, doc_id: str, meta: Dict, target_chunk_size: int = 300, overlap: int = 50) -> List[Chunk]:
81
+ language = self.detect_language(text)
82
+ cleaned_text = self.clean_text(text, language)
83
+ sentences = self.smart_sent_tokenize(cleaned_text, language)
84
+
85
+ chunks: List[Chunk] = []
86
+ current_chunk: List[str] = []
87
+ current_length = 0
88
+ chunk_id = 0
89
+
90
+ for sentence in sentences:
91
+ sentence_words = max(1, len(sentence.split()))
92
+ if current_length + sentence_words > target_chunk_size and current_chunk:
93
+ chunk_text = " ".join(current_chunk)
94
+ chunks.append(Chunk(id=doc_id, text=chunk_text, meta=meta, chunk_id=chunk_id, language=language))
95
+ chunk_id += 1
96
+
97
+ overlap_sentences = current_chunk[-2:] if len(current_chunk) > 2 else current_chunk[-1:] if current_chunk else []
98
+ current_chunk = overlap_sentences + [sentence]
99
+ current_length = sum(len(s.split()) for s in current_chunk)
100
+ else:
101
+ current_chunk.append(sentence)
102
+ current_length += sentence_words
103
+
104
+ if current_chunk:
105
+ chunk_text = " ".join(current_chunk)
106
+ chunks.append(Chunk(id=doc_id, text=chunk_text, meta=meta, chunk_id=chunk_id, language=language))
107
+
108
+ return chunks
109
 
110
+ # -------------------------
111
+ # Hybrid index (BM25 + FAISS)
112
+ # -------------------------
113
+ class AdvancedHybridIndex:
114
+ def __init__(self, embedding_model: str = 'sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2'):
115
+ print(f"Loading embedding model: {embedding_model}")
116
+ try:
117
+ self.embedder = SentenceTransformer(embedding_model)
118
+ except Exception as e:
119
+ raise RuntimeError(f"Failed to load SentenceTransformer '{embedding_model}': {e}")
120
 
121
+ self.faiss_index = None
122
+ self.id_to_chunk: List[Chunk] = []
123
+ self.bm25_indices: Dict[str, BM25Okapi] = {}
124
+ self.lang_to_global_indices: Dict[str, List[int]] = defaultdict(list)
125
+ self.corpus_by_language: Dict[str, List[str]] = defaultdict(list)
126
+ self.embeddings: Optional[np.ndarray] = None
127
+ self.doc_processor = DocumentProcessor()
128
 
129
+ def _tokenize_for_bm25(self, text: str, language: str) -> List[str]:
130
+ if not text:
131
+ return []
132
+ if language == 'fa':
133
+ return re.findall(r'[\w\u0600-\u06FF]+', text.lower())
134
+ else:
135
+ try:
136
+ return [t.lower() for t in word_tokenize(text)]
137
+ except Exception:
138
+ return re.findall(r'\w+', text.lower())
139
+
140
+ def build_index(self, chunks: List[Chunk], normalize: bool = True):
141
+ print(f"Building index for {len(chunks)} chunks...")
142
+ self.id_to_chunk = chunks
143
+
144
+ # Group texts by language and build mapping
145
+ for global_idx, chunk in enumerate(chunks):
146
+ lang = chunk.language
147
+ self.corpus_by_language[lang].append(chunk.text)
148
+ self.lang_to_global_indices[lang].append(global_idx)
149
+
150
+ # BM25 per language
151
+ for lang, texts in self.corpus_by_language.items():
152
+ tokenized = [self._tokenize_for_bm25(t, lang) for t in texts]
153
+ if not tokenized:
154
+ continue
155
+ try:
156
+ self.bm25_indices[lang] = BM25Okapi(tokenized)
157
+ print(f" BM25 index built for language '{lang}' with {len(texts)} docs")
158
+ except Exception as e:
159
+ print(f" Warning: BM25 build failed for lang {lang}: {e}")
160
+
161
+ # Dense embeddings
162
+ texts = [c.text for c in chunks]
163
+ print(" Computing dense embeddings...")
164
+ try:
165
+ embeddings = self.embedder.encode(texts, show_progress_bar=False, convert_to_numpy=True, batch_size=16)
166
+ except Exception as e:
167
+ print(f" Embedding failed: {e}")
168
+ embeddings = np.random.rand(len(texts), 384).astype('float32')
169
+
170
+ if normalize and embeddings is not None and len(embeddings) > 0:
171
+ norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
172
+ norms[norms == 0] = 1.0
173
+ embeddings = embeddings / norms
174
+
175
+ self.embeddings = embeddings.astype('float32')
176
+
177
+ if self.embeddings.size and self.embeddings.shape[0] > 0:
178
+ dim = self.embeddings.shape[1]
179
+ try:
180
+ self.faiss_index = faiss.IndexFlatIP(dim)
181
+ self.faiss_index.add(self.embeddings)
182
+ print(f" FAISS index created with {self.embeddings.shape[0]} vectors (dim={dim})")
183
+ except Exception as e:
184
+ print(f" Failed to create FAISS index: {e}")
185
+ else:
186
+ self.faiss_index = None
187
+ print(" Warning: No embeddings to add to FAISS")
188
+
189
+ def search_bm25(self, query: str, language: str, top_k: int = 50) -> List[Tuple[int, float]]:
190
+ if language not in self.bm25_indices:
191
+ return []
192
+ tokenized = self._tokenize_for_bm25(query, language)
193
+ if not tokenized:
194
+ return []
195
+ try:
196
+ scores = self.bm25_indices[language].get_scores(tokenized)
197
+ except Exception:
198
+ return []
199
+ if scores is None or len(scores) == 0:
200
+ return []
201
+ top_idxs = np.argsort(scores)[::-1][:top_k]
202
+ results: List[Tuple[int, float]] = []
203
+ for local_idx in top_idxs:
204
+ score = float(scores[local_idx])
205
+ if score <= 0:
206
+ continue
207
+ try:
208
+ global_idx = self.lang_to_global_indices[language][int(local_idx)]
209
+ results.append((int(global_idx), score))
210
+ except Exception:
211
+ continue
212
+ return results
213
+
214
+ def search_dense(self, query: str, top_k: int = 50) -> List[Tuple[int, float]]:
215
+ if self.faiss_index is None or self.embeddings is None or self.embeddings.size == 0:
216
+ return []
217
+ try:
218
+ q_emb = self.embedder.encode([query], convert_to_numpy=True)
219
+ except Exception:
220
+ return []
221
+ qnorm = np.linalg.norm(q_emb, axis=1, keepdims=True)
222
+ qnorm[qnorm == 0] = 1.0
223
+ q_emb = (q_emb / qnorm).astype('float32')
224
+ try:
225
+ D, I = self.faiss_index.search(q_emb, top_k)
226
+ except Exception:
227
+ return []
228
+ results: List[Tuple[int, float]] = []
229
+ for idx, score in zip(I[0], D[0]):
230
+ if idx != -1:
231
+ results.append((int(idx), float(score)))
232
+ return results
233
+
234
+ # -------------------------
235
+ # Retrieval system with IMPROVED relevance detection
236
+ # -------------------------
237
+ class AdvancedRetrievalSystem:
238
+ def __init__(self, index: AdvancedHybridIndex, relevance_threshold: float = 0.6, semantic_threshold: float = 0.25):
239
+ self.index = index
240
+ self.relevance_threshold = relevance_threshold
241
+ self.semantic_threshold = semantic_threshold
242
+
243
+ def _calculate_semantic_similarity(self, query: str, chunk_text: str) -> float:
244
+ """Calculate semantic similarity between query and chunk"""
245
+ try:
246
+ query_emb = self.index.embedder.encode([query], convert_to_numpy=True)
247
+ chunk_emb = self.index.embedder.encode([chunk_text], convert_to_numpy=True)
248
+
249
+ similarity = np.dot(query_emb[0], chunk_emb[0]) / (
250
+ np.linalg.norm(query_emb[0]) * np.linalg.norm(chunk_emb[0])
251
+ )
252
+ return float(similarity)
253
+ except Exception:
254
+ return 0.0
255
+
256
+ def _calculate_keyword_overlap(self, query: str, chunk_text: str, language: str) -> float:
257
+ """Calculate keyword overlap between query and chunk"""
258
+ if language == 'fa':
259
+ query_words = set(re.findall(r'[\w\u0600-\u06FF]+', query.lower()))
260
+ chunk_words = set(re.findall(r'[\w\u0600-\u06FF]+', chunk_text.lower()))
261
+ else:
262
+ query_words = set(re.findall(r'\w+', query.lower()))
263
+ chunk_words = set(re.findall(r'\w+', chunk_text.lower()))
264
+
265
+ if not query_words:
266
+ return 0.0
267
+
268
+ overlap = len(query_words.intersection(chunk_words)) / len(query_words)
269
+ return overlap
270
+
271
+ def hybrid_search(self, query: str, dense_weight: float = 0.7, bm25_weight: float = 0.3) -> Optional[Tuple[Chunk, float]]:
272
+ """
273
+ Returns the highest-scoring chunk only if it meets multiple relevance criteria
274
+ """
275
+ start = time.time()
276
+ language = self.index.doc_processor.detect_language(query)
277
+
278
+ # Get results from both methods
279
+ dense_results = self.index.search_dense(query, top_k=10)
280
+ bm25_results = self.index.search_bm25(query, language, top_k=10)
281
+
282
+ combined = {}
283
+
284
+ # Process dense results
285
+ if dense_results:
286
+ dense_scores = np.array([s for _, s in dense_results])
287
+ if len(dense_scores) > 0:
288
+ if dense_scores.max() - dense_scores.min() == 0:
289
+ dense_norm = np.ones_like(dense_scores)
290
+ else:
291
+ dense_norm = (dense_scores - dense_scores.min()) / (dense_scores.max() - dense_scores.min() + 1e-8)
292
+ for (idx, _), norm in zip(dense_results, dense_norm):
293
+ combined[idx] = dense_weight * float(norm)
294
+
295
+ # Process BM25 results
296
+ if bm25_results:
297
+ bm25_scores = np.array([s for _, s in bm25_results])
298
+ if len(bm25_scores) > 0:
299
+ if bm25_scores.max() - bm25_scores.min() == 0:
300
+ bm25_norm = np.ones_like(bm25_scores)
301
+ else:
302
+ bm25_norm = (bm25_scores - bm25_scores.min()) / (bm25_scores.max() - bm25_scores.min() + 1e-8)
303
+ for (idx, _), norm in zip(bm25_results, bm25_norm):
304
+ if idx in combined:
305
+ combined[idx] += bm25_weight * float(norm)
306
+ else:
307
+ combined[idx] = bm25_weight * float(norm)
308
+
309
+ # Find the single highest-scoring chunk
310
+ if not combined:
311
+ return None
312
+
313
+ best_idx, best_score = max(combined.items(), key=lambda x: x[1])
314
+
315
+ if 0 <= best_idx < len(self.index.id_to_chunk):
316
+ best_chunk = self.index.id_to_chunk[best_idx]
317
+
318
+ # ADDITIONAL RELEVANCE CHECKS
319
+ semantic_similarity = self._calculate_semantic_similarity(query, best_chunk.text)
320
+ keyword_overlap = self._calculate_keyword_overlap(query, best_chunk.text, language)
321
+
322
+ # STRICT RELEVANCE CHECK
323
+ is_relevant = (
324
+ best_score >= self.relevance_threshold and
325
+ semantic_similarity >= self.semantic_threshold and
326
+ keyword_overlap >= 0.05 # Reduced threshold for better coverage
327
+ )
328
+
329
+ if not is_relevant:
330
+ return None
331
+
332
+ return (best_chunk, best_score)
333
+ else:
334
+ return None
335
+
336
+ # -------------------------
337
+ # Professional RAG system for HF Spaces
338
+ # -------------------------
339
+ class HuggingFaceRAGSystem:
340
+ def __init__(self):
341
+ print("🚀 Initializing RAG System for Hugging Face Spaces...")
342
+ self.doc_processor = DocumentProcessor()
343
+ self.index = AdvancedHybridIndex('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')
344
+ self.retrieval_system = AdvancedRetrievalSystem(self.index, relevance_threshold=0.6, semantic_threshold=0.25)
345
+ self.is_initialized = False
346
+ self.default_documents_loaded = False
347
+
348
+ def load_default_documents(self):
349
+ """Load default documents for demo"""
350
+ if self.default_documents_loaded:
351
  return
352
+
353
+ default_docs = [
354
+ {
355
+ "id": "doc1",
356
+ "title": "یادگیری عمیق چیست؟",
357
+ "text": "یادگیری عمیق (Deep Learning) شاخه‌ای از یادگیری ماشین است که از شبکه‌های عصبی مصنوعی با چندین لایه استفاده می‌کند. این تکنیک برای کارهایی مانند تشخیص تصویر، پردازش زبان طبیعی و تشخیص صوت بسیار مناسب است. شبکه‌های عصبی در یادگیری عمیق می‌توانند ویژگی‌های پیچیده را به طور خودکار از داده‌ها یاد بگیرند.",
358
+ "meta": {"source": "ویکی‌پدیا", "category": "هوش مصنوعی"}
359
+ },
360
+ {
361
+ "id": "doc2",
362
+ "title": "معماری Transformer",
363
+ "text": "معماری Transformer یک مدل برای پردازش زبان طبیعی است که از مکانیزم توجه (attention) استفاده می‌کند. این معماری در مدل‌هایی مانند BERT و GPT استفاده شده و در ترجمه ماشینی و درک متن کاربرد دارد. Transformerها نسبت به مدل‌های قدیمی‌تر سرعت و دقت بیشتری در پردازش متون طولانی دارند.",
364
+ "meta": {"source": "مقاله تحقیقاتی", "category": "پردازش زبان"}
365
+ },
366
+ {
367
+ "id": "doc3",
368
+ "title": "شبکه‌های عصبی کانولوشنی",
369
+ "text": "شبکه‌های عصبی کانولوشنی (CNN) مخصوص پردازش داده‌های شبکه‌ای مانند تصاویر هستند. این شبکه‌ها از لایه‌های کانولوشن برای استخراج ویژگی‌ها استفاده می‌کنند. کاربردهای اصلی CNN شامل تشخیص اشیاء، طبقه‌بندی تصاویر و بینایی کامپیوتر است.",
370
+ "meta": {"source": "کتاب آموزشی", "category": "بینایی ماشین"}
371
+ },
372
+ {
373
+ "id": "doc4",
374
+ "title": "پردازش زبان طبیعی فارسی",
375
+ "text": "پردازش زبان طبیعی برای فارسی با چالش‌هایی مانند کمبود داده‌های برچسب‌دار، پیچیدگی‌های صرفی و نحوی و نویسه‌های خاص روبرو است. با این حال اخیراً مدل‌های زیادی برای زبان فارسی توسعه یافته‌اند.",
376
+ "meta": {"source": "مقاله پژوهشی", "category": "پردازش زبان فارسی"}
377
+ },
378
+ {
379
+ "id": "doc5",
380
+ "title": "تغذیه سالم",
381
+ "text": "تغذیه سالم شامل مصرف متعادل میوه‌ها، سبزیجات، پروتئین‌ها و غلات کامل است. نوشیدن آب کافی و کاهش مصرف قند و نمک برای سلامت بدن بسیار مهم می‌باشد.",
382
+ "meta": {"source": "کتاب سلامت", "category": "تغذیه"}
383
+ },
384
+ {
385
+ "id": "doc6",
386
+ "title": "ورزش و تناسب اندام",
387
+ "text": "ورزش منظم باعث بهبود سلامت قلبی عروقی، تقویت عضلات و کاهش استرس می‌شود. پیاده‌روی، شنا و دوچرخه‌سواری از ورزش‌های مفید هستند.",
388
+ "meta": {"source": "مجله ورزشی", "category": "سلامت"}
389
+ }
390
+ ]
391
+
392
+ self.index_documents(default_docs)
393
+ self.default_documents_loaded = True
394
+ print("✅ Default documents loaded and indexed!")
395
+
396
+ def index_documents(self, documents: List[Dict]):
397
+ """Index documents"""
398
+ print(f"📚 Indexing {len(documents)} documents...")
399
+ all_chunks: List[Chunk] = []
400
+ for doc in documents:
401
+ chunks = self.doc_processor.semantic_chunking(
402
+ doc.get('text', ''),
403
+ doc.get('id', 'unknown'),
404
+ doc.get('meta', {}),
405
+ target_chunk_size=300,
406
+ overlap=50
407
+ )
408
+ all_chunks.extend(chunks)
409
+
410
+ print(f"Created {len(all_chunks)} chunks from {len(documents)} documents")
411
+ self.index.build_index(all_chunks)
412
+ self.is_initialized = True
413
+
414
+ def query(self, question: str) -> Dict[str, Any]:
415
+ """Query the RAG system"""
416
+ if not self.is_initialized:
417
+ self.load_default_documents()
418
+
419
+ start = time.time()
420
+
421
+ # Retrieve only the top chunk (if highly relevant)
422
+ result = self.retrieval_system.hybrid_search(question)
423
+
424
+ if not result:
425
+ return {
426
+ "answer": "متأسفانه اطلاعات مرتبطی در اسناد موجود برای پاسخ به این سوال یافت نشد.",
427
+ "sources": [],
428
+ "confidence": 0.0,
429
+ "processing_time": round(time.time() - start, 2),
430
+ "relevant_content_found": False
431
+ }
432
+
433
+ top_chunk, score = result
434
+
435
+ # Store score in chunk for reference
436
+ top_chunk.score = score
437
+
438
+ # Generate answer from top chunk
439
+ language = self.doc_processor.detect_language(question)
440
+
441
+ answer_text = top_chunk.text
442
+ source = top_chunk.meta.get('source', 'Unknown')
443
+ sources = [source] if source else []
444
+ confidence = min(1.0, float(score))
445
+
446
+ return {
447
+ "question": question,
448
+ "answer": answer_text,
449
+ "sources": sources,
450
+ "confidence": round(confidence, 2),
451
+ "retrieved_score": round(score, 3),
452
+ "processing_time": round(time.time() - start, 2),
453
+ "language": language,
454
+ "chunk_source": source,
455
+ "relevant_content_found": True
456
+ }
457
+
458
+ # -------------------------
459
+ # Gradio Interface
460
+ # -------------------------
461
+ class RAGInterface:
462
+ def __init__(self):
463
+ self.rag_system = HuggingFaceRAGSystem()
464
+ self.rag_system.load_default_documents()
465
+
466
+ def process_query(self, question: str, history):
467
+ """Process query and return formatted response"""
468
+ if not question.strip():
469
+ return history, "لطفاً یک سوال وارد کنید."
470
+
471
+ # Add user question to history
472
+ history.append([question, ""])
473
+
474
+ # Get response from RAG system
475
+ result = self.rag_system.query(question)
476
+
477
+ # Format response
478
+ if result['relevant_content_found']:
479
+ response = f"**🤖 پاسخ:**\n{result['answer']}\n\n"
480
+ response += f"**🏷️ منبع:** {result['chunk_source']}\n"
481
+ response += f"**🎯 امتیاز اطمینان:** {result['confidence']}\n"
482
+ response += f"**⏱️ زمان پردازش:** {result['processing_time']} ثانیه"
483
+ else:
484
+ response = f"**❌ پاسخ:**\n{result['answer']}\n\n"
485
+ response += f"**⏱️ زمان پردازش:** {result['processing_time']} ثانیه"
486
+
487
+ # Update history
488
+ history[-1][1] = response
489
+ return history, ""
490
+
491
+ def clear_chat(self):
492
+ """Clear chat history"""
493
+ return [], ""
494
+
495
+ # -------------------------
496
+ # Create and launch Gradio app
497
+ # -------------------------
498
+ def create_interface():
499
+ """Create Gradio interface"""
500
+
501
+ # Initialize RAG system
502
+ rag_interface = RAGInterface()
503
+
504
+ # Custom CSS for better styling
505
+ css = """
506
+ .gradio-container {
507
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
508
+ }
509
+ .title {
510
+ text-align: center;
511
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
512
+ -webkit-background-clip: text;
513
+ -webkit-text-fill-color: transparent;
514
+ font-weight: bold;
515
+ }
516
+ """
517
+
518
+ with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
519
+ gr.Markdown(
520
+ """
521
+ # 🧠 سیستم هوشمند پاسخگویی (RAG)
522
+ **سیستم بازیابی و تولید پاسخ مبتنی بر اسناد**
523
+
524
+ این سیستم از هوش مصنوعی برای یافتن مرتبط‌ترین اطلاعات از اسناد موجود و ارائه پاسخ دقیق استفاده می‌کند.
525
+ """
526
+ )
527
+
528
+ with gr.Row():
529
+ with gr.Column(scale=2):
530
+ chatbot = gr.Chatbot(
531
+ label="مکالمه",
532
+ height=500,
533
+ show_copy_button=True,
534
+ avatar_images=("👤", "🤖")
535
+ )
536
+
537
+ with gr.Row():
538
+ question_input = gr.Textbox(
539
+ label="سوال خود را بپرسید",
540
+ placeholder="مثلاً: یادگیری عمیق چیست؟ یا یک تمرین ورزشی پیشنهاد بده...",
541
+ lines=2,
542
+ scale=4
543
+ )
544
+ submit_btn = gr.Button("ارسال سوال 🚀", scale=1)
545
+
546
+ with gr.Row():
547
+ clear_btn = gr.Button("پاک کردن مکالمه 🗑️")
548
+ examples = gr.Examples(
549
+ examples=[
550
+ "یادگیری عمیق چیست؟",
551
+ "Transformer چیست و چه کاربردی دارد؟",
552
+ "یک تمرین ورزشی پیشنهاد بده",
553
+ "تغذیه سالم چیست؟",
554
+ "پردازش زبان فارسی چه مشکلاتی دارد؟"
555
+ ],
556
+ inputs=question_input
557
+ )
558
+
559
+ with gr.Column(scale=1):
560
+ gr.Markdown("### 📊 اطلاعات سیستم")
561
+ with gr.Accordion("اسناد موجود", open=False):
562
+ gr.Markdown("""
563
+ **موضوعات پوشش داده شده:**
564
+ - 🤖 هوش مصنوعی و یادگیری عمیق
565
+ - 🔤 پردازش زبان طبیعی
566
+ - 👁️ بینایی کامپیوتر
567
+ - 🍎 تغذیه و سلامت
568
+ - 🏃‍♂️ ورزش و تناسب اندام
569
+ """)
570
+
571
+ with gr.Accordion("راهنمای استفاده", open=True):
572
+ gr.Markdown("""
573
+ **نحوه کار سیستم:**
574
+ 1. سوال خود را به فارسی یا انگلیسی وارد کنید
575
+ 2. سیستم مرتبط‌ترین سند را پیدا می‌کند
576
+ 3. در صورت وجود اطلاعات کافی، پاسخ ارائه می‌شود
577
+ 4. در غیر این صورت، سیستم اطلاع می‌دهد
578
+
579
+ **محدودیت‌ها:**
580
+ - فقط به سوالات مرتبط با اسناد موجود پاسخ می‌دهد
581
+ - پاسخ‌ها مستقیماً از اسناد استخراج می‌شوند
582
+ - از تولید پاسخ‌های تخیلی خودداری می‌کند
583
+ """)
584
+
585
+ # Event handlers
586
+ submit_btn.click(
587
+ fn=rag_interface.process_query,
588
+ inputs=[question_input, chatbot],
589
+ outputs=[chatbot, question_input]
590
+ )
591
+
592
+ question_input.submit(
593
+ fn=rag_interface.process_query,
594
+ inputs=[question_input, chatbot],
595
+ outputs=[chatbot, question_input]
596
+ )
597
+
598
+ clear_btn.click(
599
+ fn=rag_interface.clear_chat,
600
+ inputs=[],
601
+ outputs=[chatbot]
602
+ )
603
+
604
+ return demo
605
 
606
+ # -------------------------
607
+ # Main execution for Hugging Face Spaces
608
+ # -------------------------
609
  if __name__ == "__main__":
610
+ # For Hugging Face Spaces
611
+ demo = create_interface()
612
+ demo.launch(
613
+ server_name="0.0.0.0",
614
+ share=False,
615
+ show_error=True
616
+ )