Marriam855 commited on
Commit
08f7d21
·
verified ·
1 Parent(s): 49601f6

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +428 -0
  2. requirements.txt +0 -0
app.py ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict, Tuple
2
+ import numpy as np
3
+ from sentence_transformers import SentenceTransformer, CrossEncoder
4
+ from rank_bm25 import BM25Okapi
5
+ from groq import Groq
6
+ import gradio as gr
7
+ from dataclasses import dataclass
8
+ import re
9
+
10
+
11
+ @dataclass
12
+ class Chunk:
13
+ id: int
14
+ text: str
15
+ position: int
16
+ metadata: Dict = None
17
+
18
+ def __post_init__(self):
19
+ if self.metadata is None:
20
+ self.metadata = {}
21
+
22
+
23
+ class DocumentChunker:
24
+ def __init__(self, chunk_size: int = 500, overlap: int = 100):
25
+ self.chunk_size = chunk_size
26
+ self.overlap = overlap
27
+
28
+ def chunk_text(self, text: str) -> List[Chunk]:
29
+ # Розбиття на речення
30
+ sentences = re.split(r'[.!?]+', text)
31
+ sentences = [s.strip() for s in sentences if s.strip()]
32
+
33
+ chunks = []
34
+ current_chunk = ""
35
+ chunk_id = 0
36
+
37
+ for sentence in sentences:
38
+ if len(current_chunk) + len(sentence) > self.chunk_size and current_chunk:
39
+ chunks.append(Chunk(
40
+ id=chunk_id,
41
+ text=current_chunk.strip(),
42
+ position=chunk_id,
43
+ metadata={'sentence_count': len(current_chunk.split('.'))}
44
+ ))
45
+
46
+ # Створення overlap
47
+ words = current_chunk.split()
48
+ overlap_words = words[-int(self.overlap / 5):] if len(words) > int(self.overlap / 5) else words
49
+ current_chunk = ' '.join(overlap_words) + ' ' + sentence
50
+ chunk_id += 1
51
+ else:
52
+ current_chunk += ' ' + sentence
53
+
54
+ # Додавання останнього чанка
55
+ if current_chunk.strip():
56
+ chunks.append(Chunk(
57
+ id=chunk_id,
58
+ text=current_chunk.strip(),
59
+ position=chunk_id,
60
+ metadata={'sentence_count': len(current_chunk.split('.'))}
61
+ ))
62
+
63
+ return chunks
64
+
65
+
66
+ class HybridRetriever:
67
+ """Гібридний retriever з BM25 та semantic search"""
68
+
69
+ def __init__(self, model_name: str = 'sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2'):
70
+ self.embedding_model = SentenceTransformer(model_name)
71
+ self.bm25 = None
72
+ self.chunks = []
73
+ self.embeddings = None
74
+ self.tokenized_corpus = []
75
+
76
+ def index_documents(self, chunks: List[Chunk]):
77
+ self.chunks = chunks
78
+ texts = [chunk.text for chunk in chunks]
79
+
80
+ self.tokenized_corpus = [self._tokenize(text) for text in texts]
81
+ self.bm25 = BM25Okapi(self.tokenized_corpus)
82
+
83
+ print("Створення embeddings...")
84
+ self.embeddings = self.embedding_model.encode(texts, show_progress_bar=True)
85
+
86
+ def _tokenize(self, text: str) -> List[str]:
87
+ text = text.lower()
88
+ text = re.sub(r'[^\wа-яїієґ\s]', ' ', text)
89
+ tokens = text.split()
90
+ return [t for t in tokens if len(t) > 2]
91
+
92
+ def bm25_search(self, query: str, top_k: int = 10) -> List[Tuple[Chunk, float]]:
93
+ if self.bm25 is None:
94
+ return []
95
+
96
+ tokenized_query = self._tokenize(query)
97
+ scores = self.bm25.get_scores(tokenized_query)
98
+
99
+ top_indices = np.argsort(scores)[::-1][:top_k]
100
+ results = [(self.chunks[i], scores[i]) for i in top_indices]
101
+ return results
102
+
103
+ def semantic_search(self, query: str, top_k: int = 10) -> List[Tuple[Chunk, float]]:
104
+ if self.embeddings is None:
105
+ return []
106
+
107
+ query_embedding = self.embedding_model.encode([query])[0]
108
+
109
+ # Косинусна подібність
110
+ similarities = np.dot(self.embeddings, query_embedding) / (
111
+ np.linalg.norm(self.embeddings, axis=1) * np.linalg.norm(query_embedding)
112
+ )
113
+
114
+ top_indices = np.argsort(similarities)[::-1][:top_k]
115
+ results = [(self.chunks[i], similarities[i]) for i in top_indices]
116
+ return results
117
+
118
+ def hybrid_search(self, query: str, top_k: int = 10,
119
+ alpha: float = 0.5) -> List[Tuple[Chunk, float]]:
120
+ bm25_results = self.bm25_search(query, top_k * 2)
121
+ semantic_results = self.semantic_search(query, top_k * 2)
122
+
123
+ bm25_scores = {chunk.id: score for chunk, score in bm25_results}
124
+ semantic_scores = {chunk.id: score for chunk, score in semantic_results}
125
+
126
+ combined_scores = {}
127
+ all_ids = set(bm25_scores.keys()) | set(semantic_scores.keys())
128
+
129
+ for chunk_id in all_ids:
130
+ bm25_score = bm25_scores.get(chunk_id, 0)
131
+ semantic_score = semantic_scores.get(chunk_id, 0)
132
+
133
+ if bm25_results:
134
+ max_bm25 = max(bm25_scores.values())
135
+ bm25_score = bm25_score / max_bm25 if max_bm25 > 0 else 0
136
+
137
+ combined_scores[chunk_id] = alpha * bm25_score + (1 - alpha) * semantic_score
138
+
139
+ sorted_ids = sorted(combined_scores.items(), key=lambda x: x[1], reverse=True)[:top_k]
140
+ results = [(next(c for c in self.chunks if c.id == cid), score)
141
+ for cid, score in sorted_ids]
142
+ return results
143
+
144
+
145
+ class Reranker:
146
+ def __init__(self, model_name: str = 'cross-encoder/ms-marco-MiniLM-L-6-v2'):
147
+ self.model = CrossEncoder(model_name)
148
+
149
+ def rerank(self, query: str, chunks: List[Tuple[Chunk, float]],
150
+ top_k: int = 5) -> List[Tuple[Chunk, float]]:
151
+ if not chunks:
152
+ return []
153
+
154
+ pairs = [[query, chunk.text] for chunk, _ in chunks]
155
+
156
+ scores = self.model.predict(pairs)
157
+
158
+ results = list(zip([chunk for chunk, _ in chunks], scores))
159
+ results.sort(key=lambda x: x[1], reverse=True)
160
+ return results[:top_k]
161
+
162
+
163
+ class RAGSystem:
164
+ def __init__(self, api_key: str = None, model: str = "llama-3.3-70b-versatile"):
165
+ self.chunker = DocumentChunker()
166
+ self.retriever = HybridRetriever()
167
+ self.reranker = Reranker()
168
+ self.client = Groq(api_key=api_key) if api_key else None
169
+ self.model = model
170
+ self.chunks = []
171
+
172
+ def load_document(self, text: str) -> str:
173
+ # Chunking
174
+ self.chunks = self.chunker.chunk_text(text)
175
+
176
+ # Індексація
177
+ self.retriever.index_documents(self.chunks)
178
+
179
+ return f"Документ успішно завантажено. Створено {len(self.chunks)} чанків."
180
+
181
+ def answer_question(self, question: str, retrieval_method: str = "hybrid",
182
+ use_reranker: bool = True, show_citations: bool = True) -> Tuple[str, List[Dict]]:
183
+ """Відповідь на запитання"""
184
+ if not self.chunks:
185
+ return "Спочатку завантажте документ!", []
186
+
187
+ # Retrieval
188
+ if retrieval_method == "bm25":
189
+ retrieved = self.retriever.bm25_search(question, top_k=10)
190
+ elif retrieval_method == "semantic":
191
+ retrieved = self.retriever.semantic_search(question, top_k=10)
192
+ else: # hybrid
193
+ retrieved = self.retriever.hybrid_search(question, top_k=10)
194
+
195
+ # Reranking
196
+ if use_reranker and retrieved:
197
+ retrieved = self.reranker.rerank(question, retrieved, top_k=5)
198
+
199
+ # Генерація відповіді з Groq
200
+ if self.client is None:
201
+ return "API ключ не налаштовано!", []
202
+
203
+ context = "\n\n".join([
204
+ f"[{i + 1}] {chunk.text}"
205
+ for i, (chunk, _) in enumerate(retrieved)
206
+ ])
207
+
208
+ prompt = f"""На основі наведеного контексту дайте відповідь на запитання.
209
+ Обов'язково вказуйте номери джерел у квадратних дужках [1], [2] тощо.
210
+
211
+ Контекст:
212
+ {context}
213
+
214
+ Запитання: {question}
215
+
216
+ Відповідь (з цитуванням):"""
217
+
218
+ try:
219
+ chat_completion = self.client.chat.completions.create(
220
+ messages=[
221
+ {
222
+ "role": "system",
223
+ "content": "Ви - помічник, який відповідає на запитання на основі наданого контексту. Завжди цитуйте джерела у квадратних дужках."
224
+ },
225
+ {
226
+ "role": "user",
227
+ "content": prompt
228
+ }
229
+ ],
230
+ model=self.model,
231
+ temperature=0.3,
232
+ max_tokens=2048,
233
+ )
234
+
235
+ answer = chat_completion.choices[0].message.content
236
+
237
+ except Exception as e:
238
+ return f"Помилка при генерації відповіді: {str(e)}", []
239
+
240
+ # Формуємо цитування
241
+ citations = []
242
+ if show_citations:
243
+ citations = [
244
+ {"id": i + 1, "text": chunk.text, "score": float(score)}
245
+ for i, (chunk, score) in enumerate(retrieved)
246
+ ]
247
+
248
+ return answer, citations
249
+
250
+
251
+ def create_gradio_interface():
252
+ rag_system = None
253
+
254
+ def load_file(file, api_key, model):
255
+ nonlocal rag_system
256
+
257
+ if not api_key:
258
+ return "Введіть Groq API ключ!", "", ""
259
+
260
+ try:
261
+ filename = file.name.lower()
262
+
263
+ if filename.endswith(".txt"):
264
+ with open(file.name, 'r', encoding='utf-8') as f:
265
+ text = f.read()
266
+
267
+ elif filename.endswith(".pdf"):
268
+ import pdfplumber
269
+ text = ""
270
+ with pdfplumber.open(file.name) as pdf:
271
+ for page in pdf.pages:
272
+ text += page.extract_text() + "\n"
273
+
274
+ else:
275
+ return "Формат файлу не підтримується! Завантажте .txt або .pdf", "", ""
276
+
277
+ rag_system = RAGSystem(api_key=api_key, model=model)
278
+
279
+ status = rag_system.load_document(text)
280
+ return status, "", ""
281
+
282
+ except Exception as e:
283
+ return f"Помилка завантаження файлу: {str(e)}", "", ""
284
+
285
+ def answer(question, retrieval_method, use_reranker, show_citations):
286
+ if rag_system is None:
287
+ return "Спочатку завантажте документ!", ""
288
+
289
+ try:
290
+ answer_text, citations = rag_system.answer_question(
291
+ question,
292
+ retrieval_method.lower().replace(" ", ""),
293
+ use_reranker,
294
+ show_citations
295
+ )
296
+
297
+ citations_text = ""
298
+ if citations and show_citations:
299
+ citations_text = "\n\n📚 Джерела:\n\n"
300
+ for cit in citations:
301
+ citations_text += f"[{cit['id']}] {cit['text'][:200]}...\n"
302
+ citations_text += f"Score: {cit['score']:.3f}\n\n"
303
+
304
+ return answer_text, citations_text
305
+ except Exception as e:
306
+ return f"Помилка: {str(e)}", ""
307
+
308
+ # Створення інтерфейсу
309
+ with gr.Blocks() as demo:
310
+ gr.Markdown("""
311
+ # ⚡ RAG Question Answering System з Groq API
312
+
313
+ Швидка система для відповідей на запитання з використанням RAG підходу та Groq LLMs.
314
+ Завантажте українську книгу та отримайте відповіді на свої запитання!
315
+ """)
316
+
317
+ with gr.Row():
318
+ with gr.Column(scale=2):
319
+ api_key_input = gr.Textbox(
320
+ label="🔑 Groq API Key",
321
+ type="password",
322
+ placeholder="gsk_...",
323
+ info="Отримайте безкоштовний ключ на console.groq.com"
324
+ )
325
+
326
+ model_select = gr.Dropdown(
327
+ label="🤖 Модель Groq",
328
+ choices=[
329
+ "llama-3.3-70b-versatile",
330
+ "llama-3.1-70b-versatile",
331
+ "mixtral-8x7b-32768",
332
+ "gemma2-9b-it"
333
+ ],
334
+ value="llama-3.3-70b-versatile",
335
+ info="Llama 3.3 70B рекомендується для кращої якості"
336
+ )
337
+
338
+ file_input = gr.File(
339
+ label="📁 Завантажте книгу (.txt або .pdf)",
340
+ file_types=["text", ".txt", ".pdf"]
341
+ )
342
+
343
+ load_btn = gr.Button("📥 Завантажити документ", variant="primary", size="lg")
344
+ status_output = gr.Textbox(label="Статус", interactive=False)
345
+
346
+ with gr.Column(scale=1):
347
+ gr.Markdown("### ⚙️ Налаштування пошуку")
348
+
349
+ retrieval_method = gr.Radio(
350
+ ["BM25", "Semantic", "Hybrid"],
351
+ label="Метод пошуку",
352
+ value="Hybrid",
353
+ info="Hybrid комбінує обидва методи"
354
+ )
355
+
356
+ use_reranker = gr.Checkbox(
357
+ label="Використовувати Reranker",
358
+ value=True,
359
+ info="Покращує точність результатів"
360
+ )
361
+
362
+ show_citations = gr.Checkbox(
363
+ label="Показувати цитування",
364
+ value=True,
365
+ info="Відображає джерела інформації"
366
+ )
367
+
368
+ gr.Markdown("---")
369
+
370
+ question_input = gr.Textbox(
371
+ label="❓ Ваше запитання",
372
+ placeholder="Введіть запитання про книгу...",
373
+ lines=2
374
+ )
375
+
376
+ ask_btn = gr.Button("🔍 Знайти відповідь", variant="primary", size="lg")
377
+
378
+ with gr.Row():
379
+ with gr.Column():
380
+ answer_output = gr.Textbox(
381
+ label="💡 Відповідь",
382
+ lines=10,
383
+ )
384
+
385
+ with gr.Column():
386
+ citations_output = gr.Textbox(
387
+ label="📚 Джерела",
388
+ lines=10,
389
+ )
390
+
391
+ gr.Examples(
392
+ examples=[
393
+ "Про що ця книга?",
394
+ "Хто головний герой?",
395
+ "Що сталося в кінці?",
396
+ "Які основні теми розглядаються?",
397
+ ],
398
+ inputs=question_input,
399
+ label="💭 Приклади запитань"
400
+ )
401
+
402
+ load_btn.click(
403
+ load_file,
404
+ inputs=[file_input, api_key_input, model_select],
405
+ outputs=[status_output, answer_output, citations_output]
406
+ )
407
+
408
+ ask_btn.click(
409
+ answer,
410
+ inputs=[question_input, retrieval_method, use_reranker, show_citations],
411
+ outputs=[answer_output, citations_output]
412
+ )
413
+
414
+ question_input.submit(
415
+ answer,
416
+ inputs=[question_input, retrieval_method, use_reranker, show_citations],
417
+ outputs=[answer_output, citations_output]
418
+ )
419
+
420
+ return demo
421
+
422
+
423
+ if __name__ == "__main__":
424
+ print("Запуск RAG системи з Groq API")
425
+
426
+ demo = create_gradio_interface()
427
+ demo.launch(share=True)
428
+
requirements.txt ADDED
Binary file (2.93 kB). View file