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()
|
|
|
|
|
|
|
|
|
|