fruitpicker01 commited on
Commit
12bd40e
·
verified ·
1 Parent(s): c7690ff

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +248 -154
app.py CHANGED
@@ -1,12 +1,10 @@
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
@@ -15,157 +13,276 @@ 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
 
@@ -190,51 +307,35 @@ class LightweightRAGSystem:
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
- # Очищаем текст от проблемных символов
232
- clean_text = chunk['text'].encode('utf-8', errors='ignore').decode('utf-8')
233
- context_parts.append(f"Фрагмент {i+1} (страница {chunk['page']}):\n{clean_text}")
234
 
235
  context = "\n\n".join(context_parts)
236
-
237
- # Очищаем запрос
238
  clean_query = query.encode('utf-8', errors='ignore').decode('utf-8')
239
 
240
  prompt = f"""Ты - эк��перт по анализу финансовых отчетов. Ответь на вопрос пользователя на основе предоставленного контекста из годового отчета ПАО Сбербанк 2023.
@@ -261,11 +362,7 @@ class LightweightRAGSystem:
261
  temperature=0.1
262
  )
263
 
264
- answer = response.choices[0].message.content
265
- if answer:
266
- return answer.strip()
267
- else:
268
- return "Получен пустой ответ от модели"
269
 
270
  except Exception as e:
271
  return f"❌ Ошибка генерации ответа: {str(e)}"
@@ -287,39 +384,37 @@ class LightweightRAGSystem:
287
  }
288
 
289
  try:
290
- # Шаг 1: Поиск по ключевым словам
291
- initial_results = self.search_by_keywords(query, max_results=30)
292
 
293
- if not initial_results:
294
  return {
295
  "answer": "К сожалению, не удалось найти релевантную информацию по вашему вопросу.",
296
  "sources": [],
297
- "debug_info": {"step": "keyword_search", "results_count": 0}
298
  }
299
 
300
- # Шаг 2: LLM реранкинг
301
- reranked_results = self.rerank_with_llm(query, initial_results)
302
 
303
- # Шаг 3: Генерация ответа
304
- top_chunks = reranked_results[:self.final_chunks_count]
305
- answer = self.generate_answer(query, top_chunks)
306
 
307
  # Подготовка источников
308
  sources = []
309
- for chunk in top_chunks:
310
  sources.append({
311
  "page": chunk["page"],
312
- "keyword_score": chunk.get("keyword_score", 0),
313
- "rerank_score": chunk.get("rerank_score", 0),
314
  "preview": chunk["text"][:200] + "..." if len(chunk["text"]) > 200 else chunk["text"]
315
  })
316
 
317
  debug_info = {
318
- "initial_results": len(initial_results),
319
  "reranked_results": len(reranked_results),
320
- "final_chunks": len(top_chunks),
321
- "avg_keyword_score": np.mean([s["keyword_score"] for s in sources]) if sources else 0,
322
- "avg_rerank_score": np.mean([s["rerank_score"] for s in sources]) if sources else 0
323
  }
324
 
325
  return {
@@ -338,7 +433,7 @@ class LightweightRAGSystem:
338
  }
339
 
340
  # Глобальная переменная системы
341
- rag_system = LightweightRAGSystem()
342
 
343
  def initialize_system(api_key: str) -> Tuple[str, str]:
344
  """Инициализация системы"""
@@ -356,7 +451,7 @@ def ask_question(question: str) -> Tuple[str, str]:
356
  sources_info = "\n📚 **Источники:**\n"
357
  for i, source in enumerate(result["sources"], 1):
358
  sources_info += f"\n**{i}.** Страница {source['page']} "
359
- sources_info += f"(ключевые слова: {source['keyword_score']}, "
360
  sources_info += f"релевантность: {source['rerank_score']:.1f}/10)\n"
361
  sources_info += f"*Превью:* {source['preview']}\n"
362
 
@@ -364,19 +459,22 @@ def ask_question(question: str) -> Tuple[str, str]:
364
  if result.get("debug_info"):
365
  debug = result["debug_info"]
366
  sources_info += f"\n🔍 **Статистика поиска:**\n"
367
- sources_info += f"- Найдено по ключевым словам: {debug.get('initial_results', 0)}\n"
 
368
  sources_info += f"- После реранкинга: {debug.get('reranked_results', 0)}\n"
369
  sources_info += f"- Использовано в отв��те: {debug.get('final_chunks', 0)}\n"
370
- if debug.get('avg_rerank_score'):
371
- sources_info += f"- Средняя релевантность: {debug.get('avg_rerank_score', 0):.1f}/10\n"
372
 
373
  return answer, sources_info
374
 
375
  def create_demo_interface():
376
- """Создание демо интерфейса для HF"""
 
 
 
 
377
 
378
  with gr.Blocks(
379
- title="RAG Demo - Сбер 2023",
380
  theme=gr.themes.Soft(),
381
  css="""
382
  .main-header { text-align: center; margin-bottom: 2rem; }
@@ -386,9 +484,9 @@ def create_demo_interface():
386
 
387
  gr.Markdown("""
388
  <div class="main-header">
389
- <h1>🏆 Enhanced RAG Demo: Анализ отчета Сбера 2023</h1>
390
- <p>Улучшенная система поиска с поддержкой таблиц</p>
391
- <p><strong>84 извлеченные таблицы2009 чанковpdfplumber обработка</strong></p>
392
  </div>
393
  """)
394
 
@@ -398,9 +496,9 @@ def create_demo_interface():
398
 
399
  api_key_input = gr.Textbox(
400
  label="OpenAI API Key",
401
- placeholder="sk-...",
402
  type="password",
403
- info="Введите ваш OpenAI API ключ для работы системы"
404
  )
405
 
406
  init_btn = gr.Button("🚀 Инициализировать", variant="primary")
@@ -423,7 +521,7 @@ def create_demo_interface():
423
  lines=2,
424
  scale=4
425
  )
426
- ask_btn = gr.Button("📝 Спросить", variant="primary", scale=1)
427
 
428
  with gr.Row():
429
  with gr.Column(scale=2):
@@ -447,6 +545,7 @@ def create_demo_interface():
447
  - Расскажите о кредитном портфеле Сбербанка
448
  - Какие технологические инициативы развивает Сбер?
449
  - Каковы показатели рентабельности банка?
 
450
  """)
451
 
452
  # Event handlers
@@ -470,11 +569,6 @@ def create_demo_interface():
470
 
471
  return demo
472
 
473
- if __name__ == "__main__":
474
- demo = create_demo_interface()
475
- demo.launch(
476
- share=False,
477
- server_name="0.0.0.0",
478
- server_port=7860,
479
- show_error=True
480
- )
 
1
  #!/usr/bin/env python3
2
  """
3
+ Финальная векторная RAG система для HuggingFace Spaces
4
+ Адаптированная версия с поддержкой векторного поиска и резервным режимом
 
5
  """
6
 
7
  import os
 
8
  import json
9
  import pickle
10
  import tempfile
 
13
  import traceback
14
  import re
15
 
16
+ try:
17
+ import numpy as np
18
+ import faiss
19
+ HAS_FAISS = True
20
+ except ImportError:
21
+ HAS_FAISS = False
22
+ print("⚠️ FAISS не установлен, будет использован поиск по ключевым словам")
23
+
24
+ try:
25
+ import gradio as gr
26
+ HAS_GRADIO = True
27
+ except ImportError:
28
+ HAS_GRADIO = False
29
+ print("⚠️ Gradio не установлен")
30
 
 
31
  from openai import OpenAI
32
 
33
+ class VectorRAGSystem:
34
+ """RAG система с векторным поиском и резервным режимом"""
35
 
36
  def __init__(self):
37
  self.chunks = []
38
  self.word_index = {}
39
+ self.faiss_index = None
40
  self.metadata = {}
41
  self.client = None
42
  self.is_initialized = False
43
 
44
+ # Модели и параметры
45
+ self.embedding_model = "text-embedding-3-large"
46
+ self.embedding_dim = 3072
47
  self.generation_model = "gpt-4o"
48
  self.reranking_model = "gpt-4o-mini"
49
+
50
+ # Параметры поиска
51
  self.max_chunks_for_rerank = 15
52
  self.final_chunks_count = 5
53
+ self.vector_search_k = 20
54
+
55
+ # Режим работы
56
+ self.vector_mode = HAS_FAISS
57
 
58
+ def initialize_with_api_key(self, api_key: str) -> Tuple[str, str]:
59
+ """Инициализация системы с API ключом"""
60
  try:
61
+ if not api_key.strip():
62
+ return "❌ Введите OpenAI API ключ", ""
63
 
64
+ # Инициализация OpenAI клиента
65
+ self.client = OpenAI(api_key=api_key.strip())
66
+
67
+ # Загрузка данных
68
+ if not self.load_data():
69
+ return "❌ Ошибка загрузки данных", ""
70
+
71
+ self.is_initialized = True
72
+ stats = self._generate_stats()
73
+
74
+ return "✅ Векторная RAG система инициализирована", stats
75
+
76
+ except Exception as e:
77
+ return f"❌ Ошибка инициализации: {str(e)}", ""
78
+
79
+ def load_data(self) -> bool:
80
+ """Загрузка данных (векторных или обычных)"""
81
+ try:
82
+ # Сначала пробуем загрузить векторные данные
83
+ if self.vector_mode and self.load_vector_data():
84
+ return True
85
+
86
+ # Если не удалось, загружаем обычные данные
87
+ return self.load_fallback_data()
88
+
89
+ except Exception as e:
90
+ print(f"❌ Ошибка загрузки данных: {e}")
91
+ return False
92
+
93
+ def load_vector_data(self) -> bool:
94
+ """Загрузка векторных данных"""
95
+ try:
96
+ print("🔄 Попытка загрузки векторных данных...")
97
+
98
+ # Файлы векторных данных
99
+ chunks_file = "vector_enhanced_sber_chunks.pkl"
100
+ metadata_file = "vector_enhanced_sber_metadata.json"
101
+ faiss_file = "vector_enhanced_sber_faiss.index"
102
+
103
+ if not all(os.path.exists(f) for f in [chunks_file, metadata_file, faiss_file]):
104
+ print("📁 Файлы векторных данных не найдены")
105
  return False
106
 
107
+ # Загружаем чанки
108
+ with open(chunks_file, 'rb') as f:
109
+ chunks_data = pickle.load(f)
110
+
111
+ self.chunks = []
112
+ for chunk_data in chunks_data:
113
+ self.chunks.append({
114
+ "text": chunk_data["text"],
115
+ "page": chunk_data["page"],
116
+ "chunk_index": chunk_data["chunk_index"],
117
+ "embedding": np.array(chunk_data["embedding"]) if chunk_data.get("embedding") else None,
118
+ "metadata": chunk_data.get("metadata", {}),
119
+ "full_page_text": chunk_data.get("full_page_text", chunk_data["text"])
120
+ })
121
 
122
+ # Загружаем метаданные
123
+ with open(metadata_file, 'r', encoding='utf-8') as f:
124
+ self.metadata = json.load(f)
125
 
126
+ # Загружаем FAISS индекс
127
+ if HAS_FAISS:
128
+ self.faiss_index = faiss.read_index(faiss_file)
129
 
130
+ print(f"✅ Загружены векторные данные: {len(self.chunks)} чанков")
131
  return True
132
 
133
  except Exception as e:
134
+ print(f"❌ Ошибка загрузки векторных данных: {e}")
 
135
  return False
136
 
137
+ def load_fallback_data(self) -> bool:
138
+ """Загрузка обычных данных"""
139
  try:
140
+ print("🔄 Загрузка резервных данных...")
 
141
 
142
+ index_file = "enhanced_sber_index.pkl"
143
+ if not os.path.exists(index_file):
144
+ print(f"❌ Файл резервных данных не найден: {index_file}")
145
+ return False
146
 
147
+ with open(index_file, 'rb') as f:
148
+ index_data = pickle.load(f)
 
149
 
150
+ # Конвертируем в формат чанков
151
+ self.chunks = []
152
+ chunk_texts = index_data.get("chunks", [])
153
+
154
+ for i, chunk_text in enumerate(chunk_texts):
155
+ chunk = {
156
+ "text": chunk_text,
157
+ "page": index_data.get("metadata", {}).get("chunk_pages", {}).get(str(i), 1),
158
+ "chunk_index": i,
159
+ "embedding": None,
160
+ "metadata": {},
161
+ "full_page_text": chunk_text
162
+ }
163
+ self.chunks.append(chunk)
164
 
165
+ # Создаем словарный индекс для поиска
166
+ self.word_index = index_data.get("word_index", {})
167
+ self.metadata = index_data.get("metadata", {})
168
 
169
+ self.vector_mode = False # Отключаем векторный режим
170
+
171
+ print(f"✅ Загружены резервные данные: {len(self.chunks)} чанков")
172
+ return True
173
 
174
  except Exception as e:
175
+ print(f"❌ Ошибка загрузки резервных данных: {e}")
176
+ return False
177
 
178
  def _generate_stats(self) -> str:
179
  """Генерация статистики системы"""
180
+ total_chunks = len(self.chunks)
181
+ mode = "Векторный поиск" if self.vector_mode and self.faiss_index else "Поиск по ключевым словам"
 
 
 
 
 
 
 
182
 
183
+ stats = f"""✅ **RAG система готова!**
184
 
185
  📊 **Статистика:**
186
  - 📦 Загружено чанков: {total_chunks}
187
+ - 🔍 Режим поиска: {mode}
188
+ - 🧠 Модель генерации: {self.generation_model}
189
+ - 🎯 Реранкинг: {self.reranking_model}
 
 
 
190
 
191
  🔍 **Возможности:**
192
+ - 🔎 Семантический/ключевой поиск
193
+ - 📄 Контекстное обогащение
194
+ - 🧠 LLM реранкинг результатов
195
+ - 📝 Интеллектуальная генерация ответов
196
  - 📊 Анализ годового отчета ПАО Сбербанк 2023
197
 
198
+ 🚀 **Готова к работе!**"""
199
 
200
  return stats
201
 
202
+ def search(self, query: str, k: int = 20) -> List[Tuple[Dict, float]]:
203
+ """Основной метод поиска"""
204
+ if self.vector_mode and self.faiss_index and self.client:
205
+ return self.vector_search(query, k)
206
+ else:
207
+ return self.keyword_search(query, k)
208
+
209
+ def vector_search(self, query: str, k: int = 20) -> List[Tuple[Dict, float]]:
210
+ """Векторный поиск по запросу"""
211
+ if not self.faiss_index or not self.client:
212
+ return self.keyword_search(query, k)
213
 
214
+ try:
215
+ # Создаем эмбеддинг для запроса
216
+ response = self.client.embeddings.create(
217
+ model=self.embedding_model,
218
+ input=[query]
219
+ )
220
+
221
+ query_embedding = np.array(response.data[0].embedding, dtype=np.float32)
222
+ query_embedding = query_embedding.reshape(1, -1)
223
+
224
+ # Нормализуем для Inner Product
225
+ faiss.normalize_L2(query_embedding)
226
+
227
+ # Поиск в FAISS индексе
228
+ scores, indices = self.faiss_index.search(query_embedding, k)
229
+
230
+ # Формируем результаты
231
+ results = []
232
+ for score, idx in zip(scores[0], indices[0]):
233
+ if 0 <= idx < len(self.chunks):
234
+ chunk = self.chunks[idx]
235
+ results.append((chunk, float(score)))
236
+
237
+ return results
238
+
239
+ except Exception as e:
240
+ print(f"❌ Ошибка векторного поиска: {e}")
241
+ return self.keyword_search(query, k)
242
+
243
+ def keyword_search(self, query: str, k: int = 20) -> List[Tuple[Dict, float]]:
244
+ """Поиск по ключевым словам"""
245
  query_words = set(re.findall(r'\b\w+\b', query.lower()))
246
 
247
+ if self.word_index:
248
+ # Используем готовый индекс
249
+ chunk_scores = {}
250
+ for word in query_words:
251
+ if word in self.word_index:
252
+ for chunk_idx in self.word_index[word]:
253
+ if chunk_idx not in chunk_scores:
254
+ chunk_scores[chunk_idx] = 0
255
+ chunk_scores[chunk_idx] += 1
256
+ else:
257
+ # Создаем индекс на лету
258
+ chunk_scores = {}
259
+ for i, chunk in enumerate(self.chunks):
260
+ text_words = set(re.findall(r'\b\w+\b', chunk["text"].lower()))
261
+ score = len(query_words.intersection(text_words))
262
+ if score > 0:
263
+ chunk_scores[i] = score
264
 
265
+ # Сортируем по скору
266
  sorted_chunks = sorted(chunk_scores.items(), key=lambda x: x[1], reverse=True)
267
 
 
268
  results = []
269
+ for chunk_idx, score in sorted_chunks[:k]:
270
  if chunk_idx < len(self.chunks):
271
+ chunk = self.chunks[chunk_idx]
272
+ results.append((chunk, float(score)))
 
 
273
 
274
  return results
275
 
276
+ def rerank_with_llm(self, query: str, chunks: List[Tuple[Dict, float]]) -> List[Tuple[Dict, float]]:
277
  """LLM реранкинг результатов"""
278
  if not chunks or not self.client:
279
  return chunks
280
 
281
  try:
 
282
  chunks_to_rerank = chunks[:self.max_chunks_for_rerank]
283
 
 
284
  docs_text = ""
285
+ for i, (chunk, _) in enumerate(chunks_to_rerank):
286
  preview = chunk['text'][:300] + "..." if len(chunk['text']) > 300 else chunk['text']
287
  docs_text += f"\nДокумент {i+1} (стр. {chunk['page']}):\n{preview}\n"
288
 
 
307
  temperature=0
308
  )
309
 
 
310
  scores_text = response.choices[0].message.content.strip()
 
 
311
  numbers = re.findall(r'\d+\.?\d*', scores_text)
312
+ scores = [max(0, min(10, float(num))) for num in numbers]
 
 
 
313
 
 
314
  reranked = []
315
+ for i, (chunk, original_score) in enumerate(chunks):
316
+ rerank_score = scores[i] if i < len(scores) else 0
317
+ reranked.append((chunk, rerank_score))
 
 
 
 
 
 
 
318
 
319
+ reranked.sort(key=lambda x: x[1], reverse=True)
320
  return reranked
321
 
322
  except Exception as e:
323
  print(f"❌ Ошибка реранкинга: {e}")
324
  return chunks
325
 
326
+ def generate_answer(self, query: str, context_chunks: List[Tuple[Dict, float]]) -> str:
327
  """Генерация ответа на основе контекста"""
328
  if not self.client:
329
  return "❌ OpenAI API не настроен"
330
 
331
  try:
 
332
  context_parts = []
333
+ for i, (chunk, score) in enumerate(context_chunks[:self.final_chunks_count]):
334
+ text = chunk.get('full_page_text', chunk['text'])
335
+ clean_text = text.encode('utf-8', errors='ignore').decode('utf-8')
336
+ context_parts.append(f"Фрагмент {i+1} (страница {chunk['page']}, релевантность: {score:.2f}):\n{clean_text}")
337
 
338
  context = "\n\n".join(context_parts)
 
 
339
  clean_query = query.encode('utf-8', errors='ignore').decode('utf-8')
340
 
341
  prompt = f"""Ты - эк��перт по анализу финансовых отчетов. Ответь на вопрос пользователя на основе предоставленного контекста из годового отчета ПАО Сбербанк 2023.
 
362
  temperature=0.1
363
  )
364
 
365
+ return response.choices[0].message.content.strip()
 
 
 
 
366
 
367
  except Exception as e:
368
  return f"❌ Ошибка генерации ответа: {str(e)}"
 
384
  }
385
 
386
  try:
387
+ # Поиск
388
+ search_results = self.search(query, k=self.vector_search_k)
389
 
390
+ if not search_results:
391
  return {
392
  "answer": "К сожалению, не удалось найти релевантную информацию по вашему вопросу.",
393
  "sources": [],
394
+ "debug_info": {"step": "search", "results_count": 0}
395
  }
396
 
397
+ # Реранкинг
398
+ reranked_results = self.rerank_with_llm(query, search_results)
399
 
400
+ # Генерация ответа
401
+ answer = self.generate_answer(query, reranked_results)
 
402
 
403
  # Подготовка источников
404
  sources = []
405
+ for chunk, score in reranked_results[:self.final_chunks_count]:
406
  sources.append({
407
  "page": chunk["page"],
408
+ "search_score": search_results[0][1] if search_results else 0,
409
+ "rerank_score": score,
410
  "preview": chunk["text"][:200] + "..." if len(chunk["text"]) > 200 else chunk["text"]
411
  })
412
 
413
  debug_info = {
414
+ "search_results": len(search_results),
415
  "reranked_results": len(reranked_results),
416
+ "final_chunks": len(sources),
417
+ "search_method": "vector" if self.vector_mode else "keyword"
 
418
  }
419
 
420
  return {
 
433
  }
434
 
435
  # Глобальная переменная системы
436
+ rag_system = VectorRAGSystem()
437
 
438
  def initialize_system(api_key: str) -> Tuple[str, str]:
439
  """Инициализация системы"""
 
451
  sources_info = "\n📚 **Источники:**\n"
452
  for i, source in enumerate(result["sources"], 1):
453
  sources_info += f"\n**{i}.** Страница {source['page']} "
454
+ sources_info += f"(поиск: {source['search_score']:.3f}, "
455
  sources_info += f"релевантность: {source['rerank_score']:.1f}/10)\n"
456
  sources_info += f"*Превью:* {source['preview']}\n"
457
 
 
459
  if result.get("debug_info"):
460
  debug = result["debug_info"]
461
  sources_info += f"\n🔍 **Статистика поиска:**\n"
462
+ sources_info += f"- Метод поиска: {debug.get('search_method', 'unknown')}\n"
463
+ sources_info += f"- Найдено результатов: {debug.get('search_results', 0)}\n"
464
  sources_info += f"- После реранкинга: {debug.get('reranked_results', 0)}\n"
465
  sources_info += f"- Использовано в отв��те: {debug.get('final_chunks', 0)}\n"
 
 
466
 
467
  return answer, sources_info
468
 
469
  def create_demo_interface():
470
+ """Создание демо интерфейса"""
471
+
472
+ if not HAS_GRADIO:
473
+ print("❌ Gradio не установлен. Установите: pip install gradio")
474
+ return None
475
 
476
  with gr.Blocks(
477
+ title="Vector RAG Demo - Сбер 2023",
478
  theme=gr.themes.Soft(),
479
  css="""
480
  .main-header { text-align: center; margin-bottom: 2rem; }
 
484
 
485
  gr.Markdown("""
486
  <div class="main-header">
487
+ <h1>🚀 Advanced RAG Demo: Анализ отчета Сбера 2023</h1>
488
+ <p>Умная система с векторным поиском и адаптивным режимом</p>
489
+ <p><strong>OpenAI embeddings FAISS IndexFlatIP LLM rerankingFallback mode</strong></p>
490
  </div>
491
  """)
492
 
 
496
 
497
  api_key_input = gr.Textbox(
498
  label="OpenAI API Key",
499
+ placeholder="sk-proj-...",
500
  type="password",
501
+ info="Введите ваш OpenAI API ключ"
502
  )
503
 
504
  init_btn = gr.Button("🚀 Инициализировать", variant="primary")
 
521
  lines=2,
522
  scale=4
523
  )
524
+ ask_btn = gr.Button("🔍 Поиск", variant="primary", scale=1)
525
 
526
  with gr.Row():
527
  with gr.Column(scale=2):
 
545
  - Расскажите о кредитном портфеле Сбербанка
546
  - Какие технологические инициативы развивает Сбер?
547
  - Каковы показатели рентабельности банка?
548
+ - Какие ESG инициативы реализует Сбер?
549
  """)
550
 
551
  # Event handlers
 
569
 
570
  return demo
571
 
572
+ # Запуск для Hugging Face Spaces
573
+ demo = create_demo_interface()
574
+ demo.launch()