fruitpicker01 commited on
Commit
bcb1683
·
verified ·
1 Parent(s): 26070ce

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +329 -376
app.py CHANGED
@@ -1,231 +1,238 @@
1
  #!/usr/bin/env python3
2
  """
3
- HuggingFace Spaces приложение для RAG системы анализа отчета Сбера 2023
 
 
4
  """
5
 
6
  import os
7
  import sys
 
 
8
  import tempfile
9
- import base64
10
- from io import BytesIO
11
  from pathlib import Path
12
  from typing import Optional, Dict, Any, List, Tuple
 
 
 
13
  import gradio as gr
14
- import openai
15
- import pandas as pd
16
  import numpy as np
17
- from PIL import Image
18
-
19
- # Конфигурация
20
- class Config:
21
- """Конфигурация для HuggingFace Spaces"""
22
- OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
23
- GENERATION_MODEL = "gpt-4o"
24
- RERANKING_MODEL = "gpt-4o-mini"
25
- EMBEDDING_MODEL = "text-embedding-3-large"
26
- MAX_CHARACTERS = 4000
27
- CHUNK_OVERLAP = 200
28
- RETRIEVAL_K = 5
29
- RERANKING_K = 3
30
 
31
- config = Config()
 
32
 
33
- class SimpleRAGSystem:
34
- """Упрощенная RAG система для HuggingFace Spaces"""
35
 
36
  def __init__(self):
 
 
 
37
  self.client = None
38
- self.documents = []
39
- self.embeddings = []
40
  self.is_initialized = False
41
 
42
- def initialize_openai(self, api_key: str) -> bool:
43
- """Инициализация OpenAI клиента"""
44
- try:
45
- if not api_key:
46
- return False
47
- self.client = openai.OpenAI(api_key=api_key)
48
- # Тестовый запрос
49
- test_response = self.client.chat.completions.create(
50
- model="gpt-4o-mini",
51
- messages=[{"role": "user", "content": "Test"}],
52
- max_tokens=1
53
- )
54
- return True
55
- except Exception as e:
56
- print(f"Ошибка инициализации OpenAI: {e}")
57
- return False
58
 
59
- def extract_text_from_pdf(self, pdf_file) -> List[str]:
60
- """Извлечение текста из PDF (упрощенная версия)"""
61
  try:
62
- import pypdf
63
-
64
- reader = pypdf.PdfReader(pdf_file)
65
- texts = []
66
-
67
- for page_num, page in enumerate(reader.pages):
68
- text = page.extract_text()
69
- if text.strip():
70
- # Простое разбиение на чанки
71
- chunks = self.split_text(text, config.MAX_CHARACTERS)
72
- for i, chunk in enumerate(chunks):
73
- texts.append({
74
- 'content': chunk,
75
- 'page': page_num + 1,
76
- 'chunk': i + 1,
77
- 'type': 'text'
78
- })
79
-
80
- return texts
81
 
82
- except Exception as e:
83
- print(f"Ошибка обработки PDF: {e}")
84
- return []
85
-
86
- def split_text(self, text: str, max_size: int) -> List[str]:
87
- """Простое разбиение текста на чанки"""
88
- words = text.split()
89
- chunks = []
90
- current_chunk = []
91
- current_size = 0
92
-
93
- for word in words:
94
- if current_size + len(word) + 1 > max_size and current_chunk:
95
- chunks.append(' '.join(current_chunk))
96
- current_chunk = [word]
97
- current_size = len(word)
98
- else:
99
- current_chunk.append(word)
100
- current_size += len(word) + 1
101
-
102
- if current_chunk:
103
- chunks.append(' '.join(current_chunk))
104
-
105
- return chunks
106
-
107
- def create_embeddings(self, texts: List[Dict]) -> bool:
108
- """Создание эмбеддингов для текстов"""
109
- try:
110
- if not self.client:
111
  return False
112
 
113
- contents = [doc['content'] for doc in texts]
 
114
 
115
- # Создаем эмбеддинги батчами
116
- batch_size = 100
117
- all_embeddings = []
118
 
119
- for i in range(0, len(contents), batch_size):
120
- batch = contents[i:i + batch_size]
121
- response = self.client.embeddings.create(
122
- model=config.EMBEDDING_MODEL,
123
- input=batch
124
- )
125
- batch_embeddings = [item.embedding for item in response.data]
126
- all_embeddings.extend(batch_embeddings)
127
-
128
- self.documents = texts
129
- self.embeddings = np.array(all_embeddings)
130
- self.is_initialized = True
131
 
132
  return True
133
 
134
  except Exception as e:
135
- print(f"Ошибка создания эмбеддингов: {e}")
 
136
  return False
137
 
138
- def search_documents(self, query: str, k: int = 5) -> List[Dict]:
139
- """Поиск релевантных документов"""
140
  try:
141
- if not self.is_initialized or not self.client:
142
- return []
143
 
144
- # Создаем эмбеддинг для запроса
145
- query_response = self.client.embeddings.create(
146
- model=config.EMBEDDING_MODEL,
147
- input=[query]
148
- )
149
- query_embedding = np.array(query_response.data[0].embedding)
150
 
151
- # Вычисляем косинусное сходство
152
- similarities = np.dot(self.embeddings, query_embedding) / (
153
- np.linalg.norm(self.embeddings, axis=1) * np.linalg.norm(query_embedding)
154
- )
155
 
156
- # Получаем топ-k результатов
157
- top_indices = np.argsort(similarities)[-k:][::-1]
158
 
159
- results = []
160
- for idx in top_indices:
161
- doc = self.documents[idx].copy()
162
- doc['similarity'] = float(similarities[idx])
163
- results.append(doc)
164
 
165
- return results
166
 
167
  except Exception as e:
168
- print(f"Ошибка поиска: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
- def rerank_documents(self, query: str, documents: List[Dict]) -> List[Dict]:
172
- """Реранкинг документов с помощью LLM"""
 
 
 
173
  try:
174
- if not documents or not self.client:
175
- return documents
176
 
177
- # Формируем промпт для реранкинга
178
  docs_text = ""
179
- for i, doc in enumerate(documents):
180
- docs_text += f"\nДокумент {i+1}:\n{doc['content'][:500]}...\n"
 
181
 
182
- prompt = f"""
183
- Вопрос пользователя: {query}
184
 
185
- Документы для анализа:{docs_text}
186
 
187
- Оцени релевантность каждого документа для ответа на вопрос по шкале 1-10.
188
- Верни только список чисел через запятую (например: 8,6,9,4,7).
189
- """
 
 
 
 
 
 
190
 
191
  response = self.client.chat.completions.create(
192
- model=config.RERANKING_MODEL,
193
  messages=[{"role": "user", "content": prompt}],
194
- max_tokens=50,
195
  temperature=0
196
  )
197
 
198
  # Парсим оценки
199
  scores_text = response.choices[0].message.content.strip()
200
- scores = [float(s.strip()) for s in scores_text.split(',')]
201
-
202
- # Добавляем оценки и сортируем
203
- for i, doc in enumerate(documents):
 
 
 
 
 
 
 
 
204
  if i < len(scores):
205
- doc['rerank_score'] = scores[i]
206
  else:
207
- doc['rerank_score'] = 0
 
 
 
 
208
 
209
- return sorted(documents, key=lambda x: x['rerank_score'], reverse=True)
210
 
211
  except Exception as e:
212
- print(f"Ошибка реранкинга: {e}")
213
- return documents
214
 
215
- def generate_answer(self, query: str, context_docs: List[Dict]) -> str:
216
  """Генерация ответа на основе контекста"""
 
 
 
217
  try:
218
- if not self.client:
219
- return "Ошибка: OpenAI API не инициализирован"
 
 
220
 
221
- # Формируем контекст
222
- context = ""
223
- for doc in context_docs[:config.RERANKING_K]:
224
- context += f"\nСтраница {doc['page']}: {doc['content']}\n"
225
 
226
- # Промпт для генерации ответа
227
- prompt = f"""
228
- Ты - эксперт по анализу финансовых отчетов. Ответь на вопрос пользователя на основе предоставленной информации из годового отчета ПАО Сбербанк 2023.
229
 
230
  ВОПРОС: {query}
231
 
@@ -234,262 +241,210 @@ class SimpleRAGSystem:
234
 
235
  ИНСТРУКЦИИ:
236
  1. Отвечай только на основе предоставленной информации
237
- 2. Если информации недостаточно, честно скажи об этом
238
  3. Используй конкретные данные и цифры из отчета
239
- 4. Отвечай на русском языке
240
- 5. Структурируй ответ четко и понятно
 
241
 
242
  ОТВЕТ:"""
243
-
244
  response = self.client.chat.completions.create(
245
- model=config.GENERATION_MODEL,
246
  messages=[{"role": "user", "content": prompt}],
247
- max_tokens=1000,
248
- temperature=0.3
249
  )
250
 
251
  return response.choices[0].message.content.strip()
252
 
253
  except Exception as e:
254
- return f"Ошибка генерации ответа: {e}"
255
 
256
  def process_query(self, query: str) -> Dict[str, Any]:
257
- """Полная обработка запроса"""
258
  if not self.is_initialized:
259
  return {
260
- "answer": "Система не инициализирована. Загрузите PDF файл и введите API ключ.",
261
- "sources": []
 
262
  }
263
 
264
  if not query.strip():
265
  return {
266
  "answer": "Пожалуйста, введите ваш вопрос.",
267
- "sources": []
 
268
  }
269
 
270
- # Поиск документов
271
- search_results = self.search_documents(query, config.RETRIEVAL_K)
272
-
273
- if not search_results:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  return {
275
- "answer": "К сожалению, не удалось найти релевантную информацию по вашему запросу.",
276
- "sources": []
 
 
 
 
 
 
 
 
 
 
277
  }
278
-
279
- # Реранкинг
280
- reranked_docs = self.rerank_documents(query, search_results)
281
-
282
- # Генерация ответа
283
- answer = self.generate_answer(query, reranked_docs)
284
-
285
- # Формируем информацию об источниках
286
- sources = []
287
- for doc in reranked_docs[:config.RERANKING_K]:
288
- sources.append({
289
- "page": doc['page'],
290
- "similarity": doc.get('similarity', 0),
291
- "rerank_score": doc.get('rerank_score', 0),
292
- "preview": doc['content'][:200] + "..."
293
- })
294
-
295
- return {
296
- "answer": answer,
297
- "sources": sources
298
- }
299
-
300
- # Глобальная переменная для RAG системы
301
- rag_system = SimpleRAGSystem()
302
 
303
- def initialize_system(api_key: str, pdf_file) -> Tuple[str, str]:
304
- """Инициализация системы с API ключом и PDF файлом"""
305
- if not api_key:
306
- return "❌ Введите OpenAI API ключ", ""
307
-
308
- if pdf_file is None:
309
- return "❌ Загрузите PDF файл", ""
310
-
311
- try:
312
- # Инициализация OpenAI
313
- if not rag_system.initialize_openai(api_key):
314
- return "❌ Неверный API ключ OpenAI", ""
315
-
316
- # Обработка PDF
317
- texts = rag_system.extract_text_from_pdf(pdf_file)
318
-
319
- if not texts:
320
- return "❌ Не удалось извлечь текст из PDF", ""
321
-
322
- # Создание эмбеддингов
323
- if not rag_system.create_embeddings(texts):
324
- return "❌ Ошибка создания эмбеддингов", ""
325
-
326
- stats = f"""✅ Система инициализирована!
327
-
328
- 📊 Статистика:
329
- - Обработано страниц: {len(set(doc['page'] for doc in texts))}
330
- - Создано фрагментов: {len(texts)}
331
- - Средний размер фрагмента: {np.mean([len(doc['content']) for doc in texts]):.0f} символов
332
 
333
- 🚀 Готова к ответам на вопросы!"""
334
-
335
- return "✅ Инициализация завершена", stats
336
-
337
- except Exception as e:
338
- return f"❌ Ошибка: {e}", ""
339
 
340
  def ask_question(question: str) -> Tuple[str, str]:
341
- """Обработка вопроса пользователя"""
342
- try:
343
- result = rag_system.process_query(question)
344
-
345
- answer = result["answer"]
346
-
347
- # Формируем информацию об источниках
348
- sources_info = ""
349
- if result["sources"]:
350
- sources_info = "\n📚 Источники:\n"
351
- for i, source in enumerate(result["sources"], 1):
352
- sources_info += f"\n{i}. Страница {source['page']} (релевантность: {source['similarity']:.2f})\n"
353
- sources_info += f" {source['preview']}\n"
354
-
355
- return answer, sources_info
356
-
357
- except Exception as e:
358
- return f"Ошибка обработки запроса: {e}", ""
 
 
 
 
 
 
 
 
359
 
360
- def create_interface():
361
- """Создание Gradio интерфейса"""
362
 
363
  with gr.Blocks(
364
- title="RAG Система Сбер 2023",
365
  theme=gr.themes.Soft(),
366
  css="""
367
  .main-header { text-align: center; margin-bottom: 2rem; }
368
- .status-box { margin: 1rem 0; padding: 1rem; border-radius: 8px; }
369
- .success { background-color: #d4edda; border: 1px solid #c3e6cb; }
370
- .error { background-color: #f8d7da; border: 1px solid #f5c6cb; }
371
  """
372
  ) as demo:
373
 
374
  gr.Markdown("""
375
  <div class="main-header">
376
- <h1>🏦 RAG Система для анализа отчета Сбера 2023</h1>
377
- <p>Интеллектуальная система для анализа годового отчета ПАО Сбербанк 2023</p>
 
378
  </div>
379
  """)
380
 
381
- with gr.Tab("🚀 Главная"):
382
- with gr.Row():
383
- with gr.Column(scale=1):
384
- gr.Markdown("### ⚙️ Настройка системы")
385
-
386
- api_key_input = gr.Textbox(
387
- label="OpenAI API Key",
388
- placeholder="sk-...",
389
- type="password"
390
- )
391
- gr.Markdown("*Введите ваш OpenAI API ключ*")
392
-
393
- pdf_upload = gr.File(
394
- label="PDF файл отчета",
395
- file_types=[".pdf"]
396
- )
397
- gr.Markdown("*Загрузите PDF файл годового отчета*")
398
-
399
- init_btn = gr.Button("🔧 Инициализировать систему", variant="primary")
400
-
401
- status_text = gr.Textbox(
402
- label="Статус",
403
- interactive=False,
404
- lines=2
405
- )
406
-
407
- with gr.Column(scale=1):
408
- stats_text = gr.Markdown("### 📊 Статистика системы")
409
-
410
- gr.Markdown("### 💬 Задайте вопрос")
411
-
412
- with gr.Row():
413
- with gr.Column(scale=3):
414
- question_input = gr.Textbox(
415
- label="Ваш вопрос",
416
- placeholder="Например: Каковы основные финансовые показатели Сбера за 2023 год?",
417
- lines=3
418
- )
419
- with gr.Column(scale=1):
420
- ask_btn = gr.Button("📝 Задать вопрос", variant="primary")
421
-
422
- with gr.Row():
423
- with gr.Column():
424
- answer_output = gr.Textbox(
425
- label="Ответ системы",
426
- lines=10,
427
- interactive=False
428
- )
429
- with gr.Column():
430
- sources_output = gr.Textbox(
431
- label="Источники",
432
- lines=10,
433
- interactive=False
434
- )
435
 
436
- with gr.Tab("📖 Примеры"):
437
- gr.Markdown("""
438
- ### 💡 Примеры вопросов для анализа отчета:
439
-
440
- **📊 Финансовые показатели:**
441
- - "Каковы основные финансовые показатели Сбера за 2023 год?"
442
- - "Какова чистая прибыль банка в 2023 году?"
443
- - "Расскажите о рентабельности Сбербанка"
444
-
445
- **🏦 Бизнес и стратегия:**
446
- - "Какие технологические инновации развивает Сбер?"
447
- - "Каковы планы развития банка на будущее?"
448
- - "Расскажите об ESG-инициативах Сбербанка"
449
-
450
- **⚠️ Риски и управление:**
451
- - "Какие основные риски упоминаются в отчете?"
452
- - "Как Сбер управляет кредитными рисками?"
453
- - "Какова система корпоративного управления?"
454
-
455
- **📈 Показатели деятельности:**
456
- - "Каков объем активов Сбербанка?"
457
- - "Расскажите о кредитном портфеле банка"
458
- - "Какова динамика развития цифровых сервисов?"
459
- """)
460
 
461
- with gr.Tab("ℹ️ О системе"):
462
- gr.Markdown("""
463
- ### 🎯 О RAG системе
464
-
465
- Эта система использует технологию **Retrieval-Augmented Generation (RAG)** для интеллектуального анализа документов.
466
-
467
- **🏗️ Архитектура:**
468
- 1. **Обработка PDF** - извлечение и сегментация текста
469
- 2. **Векторизация** - создание семантических представлений
470
- 3. **Поиск** - нахождение релевантных фрагментов
471
- 4. **Реранкинг** - улучшение качества результатов
472
- 5. **Генерация** - создание финального ответа
473
-
474
- **🤖 Используемые модели:**
475
- - **GPT-4o** - генерация ответов
476
- - **GPT-4o-mini** - реранкинг результатов
477
- - **text-embedding-3-large** - векторные представления
478
-
479
- **🔧 Технологии:**
480
- - Gradio - веб-интерфейс
481
- - LangChain - RAG пайплайн
482
- - OpenAI API - языковые модели
483
- - NumPy - математические операции
484
-
485
- *Разработано в рамках курсового проекта*
486
- """)
487
 
488
- # Обработчики событий
489
  init_btn.click(
490
  fn=initialize_system,
491
- inputs=[api_key_input, pdf_upload],
492
- outputs=[status_text, stats_text]
493
  )
494
 
495
  ask_btn.click(
@@ -498,7 +453,6 @@ def create_interface():
498
  outputs=[answer_output, sources_output]
499
  )
500
 
501
- # Обработка Enter в поле вопроса
502
  question_input.submit(
503
  fn=ask_question,
504
  inputs=[question_input],
@@ -507,9 +461,8 @@ def create_interface():
507
 
508
  return demo
509
 
510
- # Запуск приложения
511
  if __name__ == "__main__":
512
- demo = create_interface()
513
  demo.launch(
514
  share=False,
515
  server_name="0.0.0.0",
 
1
  #!/usr/bin/env python3
2
  """
3
+ Демо RAG система для HuggingFace Spaces
4
+ Использует предварительно обработанные чанки отчета Сбера
5
+ Оптимизирована для быстрого запуска без тяжелых зависимостей
6
  """
7
 
8
  import os
9
  import sys
10
+ import json
11
+ import pickle
12
  import tempfile
 
 
13
  from pathlib import Path
14
  from typing import Optional, Dict, Any, List, Tuple
15
+ import traceback
16
+ import re
17
+
18
  import gradio as gr
 
 
19
  import numpy as np
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
+ # OpenAI для генерации ответов
22
+ from openai import OpenAI
23
 
24
+ class LightweightRAGSystem:
25
+ """Легковесная RAG система с предзагруженными чанками"""
26
 
27
  def __init__(self):
28
+ self.chunks = []
29
+ self.word_index = {}
30
+ self.metadata = {}
31
  self.client = None
 
 
32
  self.is_initialized = False
33
 
34
+ # Конфигурация
35
+ self.generation_model = "gpt-4o"
36
+ self.reranking_model = "gpt-4o-mini"
37
+ self.max_chunks_for_rerank = 15
38
+ self.final_chunks_count = 5
 
 
 
 
 
 
 
 
 
 
 
39
 
40
+ def load_preprocessed_data(self) -> bool:
41
+ """Загрузка предварительно обработанных данных"""
42
  try:
43
+ print("🔄 Загрузка предварительно обработанных данных...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
+ # Загружаем улучшенный индекс с таблицами
46
+ index_file = "enhanced_sber_index.pkl"
47
+ if not os.path.exists(index_file):
48
+ print(f"❌ Файл индекса не найден: {index_file}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  return False
50
 
51
+ with open(index_file, 'rb') as f:
52
+ index_data = pickle.load(f)
53
 
54
+ self.chunks = index_data["chunks"]
55
+ self.word_index = index_data["word_index"]
56
+ self.metadata = index_data["metadata"]
57
 
58
+ print(f"✅ Загружено {len(self.chunks)} чанков")
59
+ print(f"✅ Создан словарный индекс из {len(self.word_index)} слов")
 
 
 
 
 
 
 
 
 
 
60
 
61
  return True
62
 
63
  except Exception as e:
64
+ print(f"Ошибка загрузки данных: {e}")
65
+ traceback.print_exc()
66
  return False
67
 
68
+ def initialize_with_api_key(self, api_key: str) -> Tuple[str, str]:
69
+ """Инициализация системы с API ключом"""
70
  try:
71
+ if not api_key.strip():
72
+ return "❌ Введите OpenAI API ключ", ""
73
 
74
+ # Инициализация OpenAI клиента
75
+ self.client = OpenAI(api_key=api_key.strip())
 
 
 
 
76
 
77
+ # Загрузка данных
78
+ if not self.load_preprocessed_data():
79
+ return "❌ Ошибка загрузки данных", ""
 
80
 
81
+ self.is_initialized = True
 
82
 
83
+ # Генерация статистики
84
+ stats = self._generate_stats()
 
 
 
85
 
86
+ return "✅ Система инициализирована успешно", stats
87
 
88
  except Exception as e:
89
+ return f"Ошибка инициализации: {str(e)}", ""
90
+
91
+ def _generate_stats(self) -> str:
92
+ """Генерация статистики системы"""
93
+ total_chunks = self.metadata.get("total_chunks", 0)
94
+ avg_length = self.metadata.get("avg_chunk_length", 0)
95
+ avg_tokens = self.metadata.get("avg_token_count", 0)
96
+ pages = self.metadata.get("pages_processed", 0)
97
+
98
+ # Добавим информацию о таблицах
99
+ text_chunks = self.metadata.get("text_chunks", 0)
100
+ table_chunks = self.metadata.get("table_chunks", 0)
101
+ table_pages = self.metadata.get("table_pages", 0)
102
+
103
+ stats = f"""✅ **Улучшенная система готова к работе!**
104
+
105
+ 📊 **Статистика:**
106
+ - 📦 Загружено чанков: {total_chunks}
107
+ - 📝 Текстовых чанков: {text_chunks}
108
+ - 📋 Табличных чанков: {table_chunks}
109
+ - 📏 Средняя длина чанка: {avg_length:.0f} символов
110
+ - 🔢 Средний размер: {avg_tokens:.0f} токенов
111
+ - 📖 Страниц отчета: {pages}
112
+ - 📊 Страниц с таблицами: {table_pages}
113
+
114
+ 🔍 **Возможности:**
115
+ - 🔎 Быстрый поиск по ключевым словам
116
+ - 📋 Извлечение структурированных таблиц
117
+ - 🧠 LLM реранкинг результатов (GPT-4o-mini)
118
+ - 📝 Интеллектуальная генерация ответов (GPT-4o)
119
+ - 📊 Анализ годового отчета ПАО Сбербанк 2023
120
+
121
+ 🚀 **Готова отвечать на вопросы с поддержкой таблиц!**"""
122
+
123
+ return stats
124
+
125
+ def search_by_keywords(self, query: str, max_results: int = 30) -> List[Dict]:
126
+ """Поиск по ключевым словам"""
127
+ if not query.strip():
128
  return []
129
+
130
+ # Извлекаем ключевые слова из запроса
131
+ query_words = set(re.findall(r'\b\w+\b', query.lower()))
132
+
133
+ # Находим чанки, содержащие эти слова
134
+ chunk_scores = {}
135
+
136
+ for word in query_words:
137
+ if word in self.word_index:
138
+ for chunk_idx in self.word_index[word]:
139
+ if chunk_idx not in chunk_scores:
140
+ chunk_scores[chunk_idx] = 0
141
+ chunk_scores[chunk_idx] += 1
142
+
143
+ # Сортируем по количеству совпадений
144
+ sorted_chunks = sorted(chunk_scores.items(), key=lambda x: x[1], reverse=True)
145
+
146
+ # Возвращаем результаты
147
+ results = []
148
+ for chunk_idx, score in sorted_chunks[:max_results]:
149
+ if chunk_idx < len(self.chunks):
150
+ chunk = self.chunks[chunk_idx].copy()
151
+ chunk["keyword_score"] = score
152
+ chunk["similarity"] = score / len(query_words) # Нормализованный score
153
+ results.append(chunk)
154
+
155
+ return results
156
 
157
+ def rerank_with_llm(self, query: str, chunks: List[Dict]) -> List[Dict]:
158
+ """LLM реранкинг результатов"""
159
+ if not chunks or not self.client:
160
+ return chunks
161
+
162
  try:
163
+ # Ограничиваем количество чанков для реранкинга
164
+ chunks_to_rerank = chunks[:self.max_chunks_for_rerank]
165
 
166
+ # Подготавливаем документы для реранкинга
167
  docs_text = ""
168
+ for i, chunk in enumerate(chunks_to_rerank):
169
+ preview = chunk['text'][:300] + "..." if len(chunk['text']) > 300 else chunk['text']
170
+ docs_text += f"\nДокумент {i+1} (стр. {chunk['page']}):\n{preview}\n"
171
 
172
+ prompt = f"""Оцени релевантность каждого документа для ответа на вопрос по шкале 1-10.
 
173
 
174
+ Вопрос: {query}
175
 
176
+ Документы:{docs_text}
177
+
178
+ Инструкции:
179
+ 1. Оценивай точность и полноту информации для ответа
180
+ 2. Высшие баллы (8-10) - прямой ответ на вопрос
181
+ 3. Средние баллы (5-7) - частично релевантная информация
182
+ 4. Низкие баллы (1-4) - слабо связано с вопросом
183
+
184
+ Верни только числа через запятую (например: 8,6,9,4,7):"""
185
 
186
  response = self.client.chat.completions.create(
187
+ model=self.reranking_model,
188
  messages=[{"role": "user", "content": prompt}],
189
+ max_tokens=100,
190
  temperature=0
191
  )
192
 
193
  # Парсим оценки
194
  scores_text = response.choices[0].message.content.strip()
195
+ scores = []
196
+
197
+ numbers = re.findall(r'\d+\.?\d*', scores_text)
198
+ for num in numbers:
199
+ score = float(num)
200
+ score = max(0, min(10, score)) # Ограничиваем 0-10
201
+ scores.append(score)
202
+
203
+ # Применяем оценки
204
+ reranked = []
205
+ for i, chunk in enumerate(chunks):
206
+ chunk_copy = chunk.copy()
207
  if i < len(scores):
208
+ chunk_copy["rerank_score"] = scores[i]
209
  else:
210
+ chunk_copy["rerank_score"] = 0
211
+ reranked.append(chunk_copy)
212
+
213
+ # Сортируем по реранк скору
214
+ reranked.sort(key=lambda x: x["rerank_score"], reverse=True)
215
 
216
+ return reranked
217
 
218
  except Exception as e:
219
+ print(f"Ошибка реранкинга: {e}")
220
+ return chunks
221
 
222
+ def generate_answer(self, query: str, context_chunks: List[Dict]) -> str:
223
  """Генерация ответа на основе контекста"""
224
+ if not self.client:
225
+ return "❌ OpenAI API не настроен"
226
+
227
  try:
228
+ # Подготавливаем контекст
229
+ context_parts = []
230
+ for i, chunk in enumerate(context_chunks[:self.final_chunks_count]):
231
+ context_parts.append(f"Фрагмент {i+1} (страница {chunk['page']}):\n{chunk['text']}")
232
 
233
+ context = "\n\n".join(context_parts)
 
 
 
234
 
235
+ prompt = f"""Ты - эксперт по анализу финансовых отчетов. Ответь на вопрос пользователя на основе предоставленного контекста из годового отчета ПАО Сбербанк 2023.
 
 
236
 
237
  ВОПРОС: {query}
238
 
 
241
 
242
  ИНСТРУКЦИИ:
243
  1. Отвечай только на основе предоставленной информации
244
+ 2. Если информации недостаточно, честно об этом скажи
245
  3. Используй конкретные данные и цифры из отчета
246
+ 4. Структурируй ответ четко и понятно
247
+ 5. Указывай номера страниц при цитировании
248
+ 6. Отвечай на русском языке
249
 
250
  ОТВЕТ:"""
251
+
252
  response = self.client.chat.completions.create(
253
+ model=self.generation_model,
254
  messages=[{"role": "user", "content": prompt}],
255
+ max_tokens=1500,
256
+ temperature=0.1
257
  )
258
 
259
  return response.choices[0].message.content.strip()
260
 
261
  except Exception as e:
262
+ return f"Ошибка генерации ответа: {str(e)}"
263
 
264
  def process_query(self, query: str) -> Dict[str, Any]:
265
+ """Обработка пользовательского запроса"""
266
  if not self.is_initialized:
267
  return {
268
+ "answer": "Система не инициализирована. Введите API ключ.",
269
+ "sources": [],
270
+ "debug_info": {}
271
  }
272
 
273
  if not query.strip():
274
  return {
275
  "answer": "Пожалуйста, введите ваш вопрос.",
276
+ "sources": [],
277
+ "debug_info": {}
278
  }
279
 
280
+ try:
281
+ # Шаг 1: Поиск по ключевым словам
282
+ initial_results = self.search_by_keywords(query, max_results=30)
283
+
284
+ if not initial_results:
285
+ return {
286
+ "answer": "К сожалению, не удалось найти релевантную информацию по вашему вопросу.",
287
+ "sources": [],
288
+ "debug_info": {"step": "keyword_search", "results_count": 0}
289
+ }
290
+
291
+ # Шаг 2: LLM реранкинг
292
+ reranked_results = self.rerank_with_llm(query, initial_results)
293
+
294
+ # Шаг 3: Генерация ответа
295
+ top_chunks = reranked_results[:self.final_chunks_count]
296
+ answer = self.generate_answer(query, top_chunks)
297
+
298
+ # Подготовка источников
299
+ sources = []
300
+ for chunk in top_chunks:
301
+ sources.append({
302
+ "page": chunk["page"],
303
+ "keyword_score": chunk.get("keyword_score", 0),
304
+ "rerank_score": chunk.get("rerank_score", 0),
305
+ "preview": chunk["text"][:200] + "..." if len(chunk["text"]) > 200 else chunk["text"]
306
+ })
307
+
308
+ debug_info = {
309
+ "initial_results": len(initial_results),
310
+ "reranked_results": len(reranked_results),
311
+ "final_chunks": len(top_chunks),
312
+ "avg_keyword_score": np.mean([s["keyword_score"] for s in sources]) if sources else 0,
313
+ "avg_rerank_score": np.mean([s["rerank_score"] for s in sources]) if sources else 0
314
+ }
315
+
316
  return {
317
+ "answer": answer,
318
+ "sources": sources,
319
+ "debug_info": debug_info
320
+ }
321
+
322
+ except Exception as e:
323
+ print(f"❌ Ошибка обработки запроса: {e}")
324
+ traceback.print_exc()
325
+ return {
326
+ "answer": f"❌ Ошибка обработки запроса: {str(e)}",
327
+ "sources": [],
328
+ "debug_info": {"error": str(e)}
329
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
 
331
+ # Глобальная переменная системы
332
+ rag_system = LightweightRAGSystem()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
 
334
+ def initialize_system(api_key: str) -> Tuple[str, str]:
335
+ """Инициализация системы"""
336
+ return rag_system.initialize_with_api_key(api_key)
 
 
 
337
 
338
  def ask_question(question: str) -> Tuple[str, str]:
339
+ """Обработка вопроса"""
340
+ result = rag_system.process_query(question)
341
+
342
+ answer = result["answer"]
343
+
344
+ # Форматируем информацию об источниках
345
+ sources_info = ""
346
+ if result["sources"]:
347
+ sources_info = "\n📚 **Источники:**\n"
348
+ for i, source in enumerate(result["sources"], 1):
349
+ sources_info += f"\n**{i}.** Страница {source['page']} "
350
+ sources_info += f"(ключевые слова: {source['keyword_score']}, "
351
+ sources_info += f"релевантность: {source['rerank_score']:.1f}/10)\n"
352
+ sources_info += f"*Превью:* {source['preview']}\n"
353
+
354
+ # Добавляем отладочную информацию
355
+ if result.get("debug_info"):
356
+ debug = result["debug_info"]
357
+ sources_info += f"\n🔍 **Статистика поиска:**\n"
358
+ sources_info += f"- Найдено по ключевым словам: {debug.get('initial_results', 0)}\n"
359
+ sources_info += f"- После реранкинга: {debug.get('reranked_results', 0)}\n"
360
+ sources_info += f"- Использовано в ответе: {debug.get('final_chunks', 0)}\n"
361
+ if debug.get('avg_rerank_score'):
362
+ sources_info += f"- Средняя релевантность: {debug.get('avg_rerank_score', 0):.1f}/10\n"
363
+
364
+ return answer, sources_info
365
 
366
+ def create_demo_interface():
367
+ """Создание демо интерфейса для HF"""
368
 
369
  with gr.Blocks(
370
+ title="RAG Demo - Сбер 2023",
371
  theme=gr.themes.Soft(),
372
  css="""
373
  .main-header { text-align: center; margin-bottom: 2rem; }
374
+ .feature-box { background-color: #f8f9fa; padding: 1rem; border-radius: 8px; margin: 1rem 0; }
 
 
375
  """
376
  ) as demo:
377
 
378
  gr.Markdown("""
379
  <div class="main-header">
380
+ <h1>🏆 Enhanced RAG Demo: Анализ отчета Сбера 2023</h1>
381
+ <p>Улучшенная система поиска с поддержкой таблиц</p>
382
+ <p><strong>84 извлеченные таблицы • 2009 чанков • pdfplumber обработка</strong></p>
383
  </div>
384
  """)
385
 
386
+ with gr.Row():
387
+ with gr.Column(scale=1):
388
+ gr.Markdown("### ⚙️ Настройка")
389
+
390
+ api_key_input = gr.Textbox(
391
+ label="OpenAI API Key",
392
+ placeholder="sk-...",
393
+ type="password",
394
+ info="Введите ваш OpenAI API ключ для работы системы"
395
+ )
396
+
397
+ init_btn = gr.Button("🚀 Инициализировать", variant="primary")
398
+
399
+ status_output = gr.Textbox(
400
+ label="Статус",
401
+ interactive=False,
402
+ lines=2
403
+ )
404
+
405
+ with gr.Column(scale=1):
406
+ stats_output = gr.Markdown("### 📊 Ожидание инициализации...")
407
+
408
+ gr.Markdown("### 💬 Задайте вопрос")
409
+
410
+ with gr.Row():
411
+ question_input = gr.Textbox(
412
+ label="Ваш вопрос",
413
+ placeholder="Например: Каковы основные финансовые показатели Сбера за 2023 год?",
414
+ lines=2,
415
+ scale=4
416
+ )
417
+ ask_btn = gr.Button("📝 Спросить", variant="primary", scale=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
 
419
+ with gr.Row():
420
+ with gr.Column(scale=2):
421
+ answer_output = gr.Textbox(
422
+ label="Ответ системы",
423
+ lines=12,
424
+ interactive=False
425
+ )
426
+ with gr.Column(scale=1):
427
+ sources_output = gr.Textbox(
428
+ label="Источники и статистика",
429
+ lines=12,
430
+ interactive=False
431
+ )
 
 
 
 
 
 
 
 
 
 
 
432
 
433
+ # Примеры вопросов
434
+ gr.Markdown("""
435
+ ### 💡 Примеры вопросов:
436
+ - Каковы основные финансовые показатели Сбера за 2023 год?
437
+ - Какова чистая прибыль банка в 2023 году?
438
+ - Расскажите о кредитном портфеле Сбербанка
439
+ - Какие технологические инициативы развивает Сбер?
440
+ - Каковы показатели рентабельности банка?
441
+ """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
 
443
+ # Event handlers
444
  init_btn.click(
445
  fn=initialize_system,
446
+ inputs=[api_key_input],
447
+ outputs=[status_output, stats_output]
448
  )
449
 
450
  ask_btn.click(
 
453
  outputs=[answer_output, sources_output]
454
  )
455
 
 
456
  question_input.submit(
457
  fn=ask_question,
458
  inputs=[question_input],
 
461
 
462
  return demo
463
 
 
464
  if __name__ == "__main__":
465
+ demo = create_demo_interface()
466
  demo.launch(
467
  share=False,
468
  server_name="0.0.0.0",