Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,12 +1,10 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
-
|
| 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 |
-
|
| 19 |
-
import numpy as np
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
# OpenAI для генерации ответов
|
| 22 |
from openai import OpenAI
|
| 23 |
|
| 24 |
-
class
|
| 25 |
-
"""
|
| 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
|
| 41 |
-
"""
|
| 42 |
try:
|
| 43 |
-
|
|
|
|
| 44 |
|
| 45 |
-
#
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
return False
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
|
| 58 |
-
|
| 59 |
-
|
|
|
|
| 60 |
|
|
|
|
| 61 |
return True
|
| 62 |
|
| 63 |
except Exception as e:
|
| 64 |
-
print(f"❌ Ошибка загрузки данных: {e}")
|
| 65 |
-
traceback.print_exc()
|
| 66 |
return False
|
| 67 |
|
| 68 |
-
def
|
| 69 |
-
"""
|
| 70 |
try:
|
| 71 |
-
|
| 72 |
-
return "❌ Введите OpenAI API ключ", ""
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
| 76 |
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
return "❌ Ошибка загрузки данных", ""
|
| 80 |
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
-
#
|
| 84 |
-
|
|
|
|
| 85 |
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
except Exception as e:
|
| 89 |
-
|
|
|
|
| 90 |
|
| 91 |
def _generate_stats(self) -> str:
|
| 92 |
"""Генерация статистики системы"""
|
| 93 |
-
total_chunks = self.
|
| 94 |
-
|
| 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 |
-
-
|
| 108 |
-
-
|
| 109 |
-
-
|
| 110 |
-
- 🔢 Средний размер: {avg_tokens:.0f} токенов
|
| 111 |
-
- 📖 Страниц отчета: {pages}
|
| 112 |
-
- 📊 Страниц с таблицами: {table_pages}
|
| 113 |
|
| 114 |
🔍 **Возможности:**
|
| 115 |
-
- 🔎
|
| 116 |
-
-
|
| 117 |
-
- 🧠 LLM реранкинг результатов
|
| 118 |
-
- 📝 Интеллектуальная генерация ответов
|
| 119 |
- 📊 Анализ годового отчета ПАО Сбербанк 2023
|
| 120 |
|
| 121 |
-
🚀 **Готова
|
| 122 |
|
| 123 |
return stats
|
| 124 |
|
| 125 |
-
def
|
| 126 |
-
"""
|
| 127 |
-
if
|
| 128 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
query_words = set(re.findall(r'\b\w+\b', query.lower()))
|
| 132 |
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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[:
|
| 149 |
if chunk_idx < len(self.chunks):
|
| 150 |
-
chunk = self.chunks[chunk_idx]
|
| 151 |
-
chunk
|
| 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 |
-
|
| 207 |
-
|
| 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 =
|
| 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 |
-
|
| 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 |
-
#
|
| 291 |
-
|
| 292 |
|
| 293 |
-
if not
|
| 294 |
return {
|
| 295 |
"answer": "К сожалению, не удалось найти релевантную информацию по вашему вопросу.",
|
| 296 |
"sources": [],
|
| 297 |
-
"debug_info": {"step": "
|
| 298 |
}
|
| 299 |
|
| 300 |
-
#
|
| 301 |
-
reranked_results = self.rerank_with_llm(query,
|
| 302 |
|
| 303 |
-
#
|
| 304 |
-
|
| 305 |
-
answer = self.generate_answer(query, top_chunks)
|
| 306 |
|
| 307 |
# Подготовка источников
|
| 308 |
sources = []
|
| 309 |
-
for chunk in
|
| 310 |
sources.append({
|
| 311 |
"page": chunk["page"],
|
| 312 |
-
"
|
| 313 |
-
"rerank_score":
|
| 314 |
"preview": chunk["text"][:200] + "..." if len(chunk["text"]) > 200 else chunk["text"]
|
| 315 |
})
|
| 316 |
|
| 317 |
debug_info = {
|
| 318 |
-
"
|
| 319 |
"reranked_results": len(reranked_results),
|
| 320 |
-
"final_chunks": len(
|
| 321 |
-
"
|
| 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 =
|
| 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"(
|
| 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"-
|
|
|
|
| 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 |
-
"""Создание демо интерфейса
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|
| 390 |
-
<p>У
|
| 391 |
-
<p><strong>
|
| 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("
|
| 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 |
-
|
| 474 |
-
|
| 475 |
-
|
| 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 reranking • Fallback 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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|