ekaterina-simonova commited on
Commit
8e79d28
·
verified ·
1 Parent(s): 7a09aab

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +163 -215
app.py CHANGED
@@ -18,7 +18,7 @@ import time
18
  from huggingface_hub import model_info
19
  from datetime import datetime
20
 
21
- # 1. Первым делом - настройка логирования (до всех операций)
22
  logging.basicConfig(
23
  level=logging.INFO,
24
  format='%(asctime)s - %(levelname)s - %(message)s',
@@ -29,17 +29,17 @@ logging.basicConfig(
29
  )
30
  logger = logging.getLogger()
31
 
32
- # 2. Проверка загрузки модели (сразу после логирования)
33
  try:
34
  logger.info("="*50)
35
  logger.info("Начало принудительной проверки модели")
36
  test_model = SentenceTransformer(
37
  "cointegrated/LaBSE-en-ru",
38
  device='cpu',
39
- cache_folder="/tmp/hf_cache_force" # Отдельный кеш для теста
40
  )
41
  logger.info(f"Модель загружена. Размерность: {test_model.get_sentence_embedding_dimension()}")
42
- del test_model # Освобождаем память
43
  except Exception as e:
44
  logger.critical(f"Тестовая загрузка модели провалилась: {str(e)}", exc_info=True)
45
  st.error("""
@@ -51,23 +51,23 @@ except Exception as e:
51
  """)
52
  raise
53
 
54
- # 3. Инициализация NLTK (после проверки модели)
55
  nltk.download('punkt', quiet=True)
56
  nltk.download('stopwords', quiet=True)
57
 
58
- # 4. Остальные константы
59
  XLSX_FILE_PATH = "Test_questions_from_diagnostpb (1).xlsx"
60
  SQLITE_DB_PATH = "knowledge_base_v1.db"
61
  VECTOR_DB_DIR = "vectorized_knowledge_base"
62
  VECTOR_DB_PATH = os.path.join(VECTOR_DB_DIR, "processed_knowledge_base_v1.db")
63
  FAISS_INDEX_PATH = os.path.join(VECTOR_DB_DIR, "faiss_index.bin")
64
  LOG_FILE = "chat_logs.json"
65
- EMBEDDING_MODEL = "cointegrated/LaBSE-en-ru" # Теперь мы уверены, что модель работает
66
 
67
- # 5. Инициализация OpenAI (если нужно)
68
  openai_api_key = os.getenv('VSEGPT_API_KEY')
69
  if openai_api_key is None:
70
- logger.error("Переменная окружения VSEGPT_API_KEY не установлена")
71
  st.warning("Не настроен API-ключ для OpenAI")
72
  raise ValueError("Переменная окружения VSEGPT_API_KEY не установена")
73
 
@@ -95,264 +95,203 @@ class HybridSearch:
95
  self._init_bm25()
96
 
97
  def _init_bm25(self):
98
- """Инициализация BM25 с данными из базы"""
99
  try:
100
  logger.info(f"Инициализация BM25 для базы: {self.db_path}")
101
 
102
  # Проверка существования файла
103
  if not os.path.exists(self.db_path):
104
  logger.error(f"Файл базы данных не существует: {self.db_path}")
 
105
  return
106
 
107
- conn = get_db_connection(self.db_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  cursor = conn.cursor()
109
 
110
- # Проверка таблицы content
111
- cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='content';")
112
- if not cursor.fetchone():
 
 
 
113
  logger.error("Таблица 'content' не найдена в базе данных!")
 
114
  conn.close()
115
  return
116
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  cursor.execute("""
118
  SELECT c.id, c.chunk_text
119
  FROM content c
 
 
120
  """)
121
 
122
  rows = cursor.fetchall()
123
  logger.info(f"Получено строк из БД: {len(rows)}")
124
 
125
- if not rows:
126
- logger.warning("Таблица content пуста!")
127
- conn.close()
128
- return
129
-
130
- for i, row in enumerate(rows):
131
  text = row['chunk_text']
132
  tokens = self._preprocess_text(text)
133
-
134
- # Логируем первый и последний чанки для проверки
135
- if i == 0 or i == len(rows) - 1:
136
- logger.info(f"Чанк #{i} (ID: {row['id']})")
137
- logger.info(f" Оригинал: {text[:100]}{'...' if len(text) > 100 else ''}")
138
- logger.info(f" Токены: {tokens}")
139
- logger.info(f" Кол-во токенов: {len(tokens)}")
140
-
141
- self.corpus.append(tokens)
142
- self.doc_ids.append(row['id'])
 
 
143
 
144
  conn.close()
145
 
146
  if self.corpus:
 
147
  self.bm25 = BM25Okapi(self.corpus)
148
- logger.info(f"BM25 инициализирован! Документов: {len(self.corpus)}")
149
- logger.info(f"Пример первого документа в корпусе: {self.corpus[0][:5]}...")
 
 
 
 
 
 
 
 
 
150
  else:
151
- logger.warning("Корпус BM25 пуст после обработки!")
 
152
 
 
 
 
 
 
153
  except Exception as e:
154
- logger.error(f"Ошибка инициализации BM25: {str(e)}", exc_info=True)
 
 
 
155
 
156
  def _preprocess_text(self, text):
157
- """Предварительная обработка текста для BM25"""
158
  try:
159
- # Логируем исходный текст для отладки
160
  if not text or not isinstance(text, str):
161
  logger.warning(f"Получен пустой или нестроковый текст: {type(text)} - {str(text)[:50]}")
162
  return []
163
 
164
- tokens = word_tokenize(text.lower(), language='russian')
165
- filtered_tokens = [token for token in tokens if token not in self.stop_words and token.isalnum()]
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  return filtered_tokens
167
  except Exception as e:
168
  logger.error(f"Ошибка обработки текста: {str(e)} | Текст: '{text[:50]}...'", exc_info=True)
169
  return []
170
 
171
  def search(self, query, top_k=5):
172
- """Выполняет поиск BM25"""
173
  if not self.bm25:
 
174
  return []
175
 
176
- tokenized_query = self._preprocess_text(query)
177
- scores = self.bm25.get_scores(tokenized_query)
178
- top_indices = np.argsort(scores)[-top_k:][::-1]
179
-
180
- results = []
181
- conn = get_db_connection(self.db_path)
182
-
183
- for idx in top_indices:
184
- if scores[idx] <= 0:
185
- continue
186
 
187
- doc_id = self.doc_ids[idx]
188
- cursor = conn.cursor()
189
- cursor.execute("""
190
- SELECT c.chunk_text, d.doc_type_short, d.doc_number, d.file_name
191
- FROM content c
192
- JOIN documents d ON c.document_id = d.id
193
- WHERE c.id = ?
194
- """, (doc_id,))
195
 
196
- row = cursor.fetchone()
197
- if row:
198
- source_parts = [
199
- str(row['doc_type_short']) if row['doc_type_short'] else None,
200
- str(row['doc_number']) if row['doc_number'] else None,
201
- str(row['file_name']) if row['file_name'] else None
202
- ]
203
- source = " ".join(filter(None, source_parts)) or "Неизвестный источник"
204
-
205
- results.append({
206
- "text": row['chunk_text'],
207
- "source": source,
208
- "score": float(scores[idx]),
209
- "type": "bm25"
210
- })
211
-
212
- conn.close()
213
- return results
214
-
215
- # Загрузка данных из XLSX
216
- @st.cache_data
217
- def load_models():
218
- """Загрузка моделей с подробным логированием и обработкой ошибок"""
219
- def log_model_info():
220
- """Вспомогательная функция для логирования информации о модели"""
221
- try:
222
- info = model_info(EMBEDDING_MODEL)
223
- logger.info(f"Информация о модели: размер={info.size}, обновление={info.lastModified}")
224
- return True
225
- except Exception as e:
226
- logger.warning(f"Не удалось получить информацию о модели: {str(e)}")
227
- return False
228
-
229
- try:
230
- logger.info("="*80)
231
- logger.info(f"Начало загрузки модели: {EMBEDDING_MODEL}")
232
-
233
- # 1. Загрузка модели SentenceTransformer
234
- start_time = time.time()
235
- try:
236
- model = SentenceTransformer(
237
- EMBEDDING_MODEL,
238
- device='cpu',
239
- cache_folder=os.path.expanduser("~/.cache/huggingface/hub")
240
- )
241
- logger.info(f"Модель загружена за {time.time()-start_time:.2f} сек")
242
- logger.info(f"Размерность эмбеддингов: {model.get_sentence_embedding_dimension()}")
243
- except Exception as e:
244
- logger.critical(f"Ошибка загрузки модели: {str(e)}")
245
- raise RuntimeError("Не удалось загрузить модель") from e
246
-
247
- # 2. Загрузка FAISS индекса
248
- logger.info(f"Загрузка FAISS индекса: {FAISS_INDEX_PATH}")
249
- if not os.path.exists(FAISS_INDEX_PATH):
250
- error_msg = f"Индекс не найден: {FAISS_INDEX_PATH}"
251
- logger.error(error_msg)
252
- raise FileNotFoundError(error_msg)
253
-
254
- try:
255
- faiss_index = faiss.read_index(FAISS_INDEX_PATH)
256
- logger.info(f"Индекс загружен (размерность: {faiss_index.d}, векторов: {faiss_index.ntotal})")
257
- except Exception as e:
258
- logger.critical(f"Ошибка чтения индекса: {str(e)}")
259
- raise RuntimeError("Неверный формат FAISS индекса") from e
260
-
261
- # 3. Инициализация гибридного поиска (BM25)
262
- logger.info(f"Инициализация гибридного поиска: {VECTOR_DB_PATH}")
263
- hybrid_search = None
264
-
265
- if os.path.exists(VECTOR_DB_PATH):
266
- db_size = os.path.getsize(VECTOR_DB_PATH)
267
- last_modified = datetime.fromtimestamp(os.path.getmtime(VECTOR_DB_PATH)).strftime('%Y-%m-%d %H:%M:%S')
268
 
269
- logger.info(f"Проверка базы для BM25:")
270
- logger.info(f" Размер файла: {db_size} байт")
271
- logger.info(f" Последнее изменение: {last_modified}")
272
 
273
- try:
274
- # Проверка структуры базы
275
- conn = sqlite3.connect(VECTOR_DB_PATH)
276
- cursor = conn.cursor()
277
-
278
- # Проверка существования таблиц
279
- cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
280
- tables = [row[0] for row in cursor.fetchall()]
281
- logger.info(f" Таблицы в базе: {tables}")
282
-
283
- if 'content' in tables:
284
- cursor.execute("SELECT COUNT(*) FROM content")
285
- count = cursor.fetchone()[0]
286
- logger.info(f" Записей в content: {count}")
287
 
288
- if count > 0:
289
- cursor.execute("SELECT chunk_text FROM content LIMIT 1")
290
- sample = cursor.fetchone()
291
- logger.info(f" Пример текста: {sample[0][:50] if sample else 'НЕТ ДАННЫХ'}")
292
-
293
- conn.close()
294
-
295
- # Инициализация HybridSearch
296
- hybrid_search = HybridSearch(VECTOR_DB_PATH)
297
 
298
- if hybrid_search and hybrid_search.bm25:
299
- logger.info(f"BM25 успешно инициализирован! Документов: {len(hybrid_search.corpus)}")
300
- else:
301
- logger.error("Не удалось инициализировать BM25!")
 
 
 
 
302
 
303
- except Exception as e:
304
- logger.error(f"Ошибка при проверке базы данных: {str(e)}", exc_info=True)
305
- if 'conn' in locals():
306
- conn.close()
307
- else:
308
- logger.error(f"Файл базы данных не найден: {VECTOR_DB_PATH}")
309
-
310
- return model, faiss_index, hybrid_search
311
-
312
- except Exception as e:
313
- logger.critical(f"Фатальная ошибка при загрузке: {str(e)}", exc_info=True)
314
- st.error("""
315
- Критическая ошибка инициализации системы. Проверьте:
316
- 1. Наличие всех файлов данных
317
- 2. Логи в model_loading.log
318
- 3. Доступ к интернету для загрузки моделей
319
- """)
320
- raise
321
-
322
- def run_bm25_diagnostic(hybrid_search, question, top_k=5):
323
- """Отдельная функция для диагностики BM25 с явной передачей hybrid_search"""
324
- try:
325
- logger.info(f"Запуск диагностики BM25 для вопроса: '{question}'")
326
-
327
- if not hybrid_search:
328
- error_msg = "Гибридный поиск (hybrid_search) не инициализирован"
329
- logger.error(error_msg)
330
- raise ValueError(error_msg)
331
-
332
- if not hybrid_search.bm25:
333
- error_msg = "BM25 не был создан при инициализации HybridSearch"
334
- logger.error(error_msg)
335
- raise ValueError(error_msg)
336
-
337
- # Запуск поиска с подробным логированием
338
- logger.info(f"Поиск BM25 с top_k={top_k}")
339
- results = hybrid_search.search(question, top_k=top_k)
340
-
341
- if not results:
342
- logger.info("BM25 вернул 0 результатов (возможно, низкие оценки)")
343
- else:
344
- logger.info(f"Найдено результатов BM25: {len(results)}")
345
- for i, res in enumerate(results, 1):
346
- logger.info(
347
- f"Результат #{i}: Оценка={res['score']:.2f}, "
348
- f"Тип={res.get('type', 'unknown')}, "
349
- f"Текст={res['text'][:50]}..."
350
- )
351
 
352
- return results
353
- except Exception as e:
354
- logger.error(f"Ошибка в run_bm25_diagnostic: {str(e)}", exc_info=True)
355
- raise
356
 
357
  # Подключение к SQLite базе
358
  def get_db_connection(db_path):
@@ -565,6 +504,15 @@ def save_log(question, answer):
565
  except Exception as e:
566
  logger.error(f"Ошибка при сохранении лога: {e}")
567
 
 
 
 
 
 
 
 
 
 
568
  # Поиск ответа
569
  def get_answer(question):
570
  # 1. Проверка специальных случаев
@@ -618,10 +566,10 @@ def get_answer(question):
618
  answer = f"🤖 Сгенерированный ответ:\n\n{gpt_answer}\n\n"
619
  answer += "🔍 Использованные фрагменты документов:\n\n"
620
 
621
- # for i, res in enumerate(hybrid_results, 1):
622
- # answer += f"### Фрагмент {i} (метод: {res['type']}, оценка: {res['combined_score']:.2f})\n"
623
- # answer += f"{res['text']}\n"
624
- # answer += f"\n📚 Источник: {res['source']}\n\n"
625
 
626
  save_log(question, answer)
627
  return answer
 
18
  from huggingface_hub import model_info
19
  from datetime import datetime
20
 
21
+ # 1. Настройка логирования
22
  logging.basicConfig(
23
  level=logging.INFO,
24
  format='%(asctime)s - %(levelname)s - %(message)s',
 
29
  )
30
  logger = logging.getLogger()
31
 
32
+ # 2. Проверка загрузки модели
33
  try:
34
  logger.info("="*50)
35
  logger.info("Начало принудительной проверки модели")
36
  test_model = SentenceTransformer(
37
  "cointegrated/LaBSE-en-ru",
38
  device='cpu',
39
+ cache_folder="/tmp/hf_cache_force"
40
  )
41
  logger.info(f"Модель загружена. Размерность: {test_model.get_sentence_embedding_dimension()}")
42
+ del test_model
43
  except Exception as e:
44
  logger.critical(f"Тестовая загрузка модели провалилась: {str(e)}", exc_info=True)
45
  st.error("""
 
51
  """)
52
  raise
53
 
54
+ # 3. Инициализация NLTK
55
  nltk.download('punkt', quiet=True)
56
  nltk.download('stopwords', quiet=True)
57
 
58
+ # 4. Константы
59
  XLSX_FILE_PATH = "Test_questions_from_diagnostpb (1).xlsx"
60
  SQLITE_DB_PATH = "knowledge_base_v1.db"
61
  VECTOR_DB_DIR = "vectorized_knowledge_base"
62
  VECTOR_DB_PATH = os.path.join(VECTOR_DB_DIR, "processed_knowledge_base_v1.db")
63
  FAISS_INDEX_PATH = os.path.join(VECTOR_DB_DIR, "faiss_index.bin")
64
  LOG_FILE = "chat_logs.json"
65
+ EMBEDDING_MODEL = "cointegrated/LaBSE-en-ru"
66
 
67
+ # 5. Инициализация OpenAI
68
  openai_api_key = os.getenv('VSEGPT_API_KEY')
69
  if openai_api_key is None:
70
+ logger.error("Переменная окружения VSEGPT_API_KEY не установена")
71
  st.warning("Не настроен API-ключ для OpenAI")
72
  raise ValueError("Переменная окружения VSEGPT_API_KEY не установена")
73
 
 
95
  self._init_bm25()
96
 
97
  def _init_bm25(self):
98
+ """Инициализация BM25 с подробной диагностикой"""
99
  try:
100
  logger.info(f"Инициализация BM25 для базы: {self.db_path}")
101
 
102
  # Проверка существования файла
103
  if not os.path.exists(self.db_path):
104
  logger.error(f"Файл базы данных не существует: {self.db_path}")
105
+ st.error(f"Файл базы данных не найден: {self.db_path}")
106
  return
107
 
108
+ # Проверка размера файла
109
+ db_size = os.path.getsize(self.db_path)
110
+ logger.info(f"Размер файла БД: {db_size} байт")
111
+ if db_size == 0:
112
+ logger.error("Файл базы данных пуст!")
113
+ st.error("Файл базы данных пуст!")
114
+ return
115
+
116
+ # Проверка прав доступа
117
+ try:
118
+ perm = oct(os.stat(self.db_path).st_mode)[-3:]
119
+ logger.info(f"Права доступа к файлу: {perm}")
120
+ except Exception as e:
121
+ logger.warning(f"Не удалось проверить права доступа: {str(e)}")
122
+
123
+ conn = sqlite3.connect(self.db_path)
124
+ conn.row_factory = sqlite3.Row
125
  cursor = conn.cursor()
126
 
127
+ # Проверка структуры базы
128
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
129
+ tables = [row[0] for row in cursor.fetchall()]
130
+ logger.info(f"Таблицы в базе: {tables}")
131
+
132
+ if 'content' not in tables:
133
  logger.error("Таблица 'content' не найдена в базе данных!")
134
+ st.error("Таблица 'content' не найдена в б��зе данных!")
135
  conn.close()
136
  return
137
 
138
+ # Проверка количества записей
139
+ cursor.execute("SELECT COUNT(*) FROM content")
140
+ count = cursor.fetchone()[0]
141
+ logger.info(f"Количество записей в content: {count}")
142
+
143
+ if count == 0:
144
+ logger.error("Таблица content пуста!")
145
+ st.error("Таблица content пуста!")
146
+ conn.close()
147
+ return
148
+
149
+ # Получение данных
150
  cursor.execute("""
151
  SELECT c.id, c.chunk_text
152
  FROM content c
153
+ ORDER BY c.id
154
+ LIMIT 1000 # Ограничиваем для теста
155
  """)
156
 
157
  rows = cursor.fetchall()
158
  logger.info(f"Получено строк из БД: {len(rows)}")
159
 
160
+ # Тестовый вывод первых 3 записей
161
+ for i, row in enumerate(rows[:3]):
 
 
 
 
162
  text = row['chunk_text']
163
  tokens = self._preprocess_text(text)
164
+ logger.info(f"Пример #{i+1} (ID: {row['id']}):")
165
+ logger.info(f" Текст: {text[:100]}{'...' if len(text) > 100 else ''}")
166
+ logger.info(f" Токены: {tokens[:10]}{'...' if len(tokens) > 10 else ''}")
167
+ logger.info(f" Всего токенов: {len(tokens)}")
168
+
169
+ # Обработка всех записей
170
+ for row in rows:
171
+ text = row['chunk_text']
172
+ tokens = self._preprocess_text(text)
173
+ if tokens: # Только если есть токены после обработки
174
+ self.corpus.append(tokens)
175
+ self.doc_ids.append(row['id'])
176
 
177
  conn.close()
178
 
179
  if self.corpus:
180
+ logger.info(f"Создание индекса BM25 для {len(self.corpus)} документов")
181
  self.bm25 = BM25Okapi(self.corpus)
182
+ logger.info("BM25 успешно инициализирован!")
183
+
184
+ # Тестовая проверка поиска
185
+ test_query = "метрология"
186
+ tokenized_query = self._preprocess_text(test_query)
187
+ if tokenized_query:
188
+ scores = self.bm25.get_scores(tokenized_query)
189
+ logger.info(f"Тестовый поиск по запросу '{test_query}':")
190
+ logger.info(f" Макс. оценка: {max(scores):.2f}")
191
+ logger.info(f" Мин. оценка: {min(scores):.2f}")
192
+ logger.info(f" Средняя оценка: {np.mean(scores):.2f}")
193
  else:
194
+ logger.error("Корпус для BM25 пуст после обработки!")
195
+ st.error("Не удалось подготовить данные для поиска BM25")
196
 
197
+ except sqlite3.Error as e:
198
+ logger.error(f"Ошибка SQLite: {str(e)}", exc_info=True)
199
+ st.error(f"Ошибка базы данных: {str(e)}")
200
+ if 'conn' in locals():
201
+ conn.close()
202
  except Exception as e:
203
+ logger.error(f"Общая ошибка инициализации BM25: {str(e)}", exc_info=True)
204
+ st.error(f"Ошибка инициализации поиска: {str(e)}")
205
+ if 'conn' in locals():
206
+ conn.close()
207
 
208
  def _preprocess_text(self, text):
209
+ """Предварительная обработка текста с улучшенной обработкой ошибок"""
210
  try:
 
211
  if not text or not isinstance(text, str):
212
  logger.warning(f"Получен пустой или нестроковый текст: {type(text)} - {str(text)[:50]}")
213
  return []
214
 
215
+ # Удаление специальных символов и чисел
216
+ clean_text = re.sub(r'[^\w\s]', ' ', text)
217
+ clean_text = re.sub(r'\d+', '', clean_text)
218
+
219
+ tokens = word_tokenize(clean_text.lower(), language='russian')
220
+ filtered_tokens = [
221
+ token for token in tokens
222
+ if token not in self.stop_words
223
+ and len(token) > 2 # Игнорируем очень короткие слова
224
+ and token.isalpha() # Только буквенные токены
225
+ ]
226
+
227
+ if not filtered_tokens:
228
+ logger.debug(f"Нет токенов после обработки в тексте: {text[:50]}...")
229
+
230
  return filtered_tokens
231
  except Exception as e:
232
  logger.error(f"Ошибка обработки текста: {str(e)} | Текст: '{text[:50]}...'", exc_info=True)
233
  return []
234
 
235
  def search(self, query, top_k=5):
236
+ """Выполняет поиск BM25 с улучшенной обработкой ошибок"""
237
  if not self.bm25:
238
+ logger.warning("BM25 не инициализирован, поиск невозможен")
239
  return []
240
 
241
+ try:
242
+ tokenized_query = self._preprocess_text(query)
243
+ if not tokenized_query:
244
+ logger.warning(f"Запрос '{query}' не содержит значимых токенов после обработки")
245
+ return []
 
 
 
 
 
246
 
247
+ logger.info(f"Поиск BM25 по запросу: '{query}'")
248
+ logger.info(f"Токены запроса: {tokenized_query}")
 
 
 
 
 
 
249
 
250
+ scores = self.bm25.get_scores(tokenized_query)
251
+ top_indices = np.argsort(scores)[-top_k:][::-1]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
+ results = []
254
+ conn = get_db_connection(self.db_path)
255
+ cursor = conn.cursor()
256
 
257
+ for idx in top_indices:
258
+ if scores[idx] <= 0:
259
+ continue
 
 
 
 
 
 
 
 
 
 
 
260
 
261
+ doc_id = self.doc_ids[idx]
262
+ cursor.execute("""
263
+ SELECT c.chunk_text, d.doc_type_short, d.doc_number, d.file_name
264
+ FROM content c
265
+ JOIN documents d ON c.document_id = d.id
266
+ WHERE c.id = ?
267
+ """, (doc_id,))
 
 
268
 
269
+ row = cursor.fetchone()
270
+ if row:
271
+ source_parts = [
272
+ str(row['doc_type_short']) if row['doc_type_short'] else None,
273
+ str(row['doc_number']) if row['doc_number'] else None,
274
+ str(row['file_name']) if row['file_name'] else None
275
+ ]
276
+ source = " ".join(filter(None, source_parts)) or "Неизвестный источник"
277
 
278
+ results.append({
279
+ "text": row['chunk_text'],
280
+ "source": source,
281
+ "score": float(scores[idx]),
282
+ "type": "bm25"
283
+ })
284
+
285
+ conn.close()
286
+
287
+ if not results:
288
+ logger.info("BM25 не нашел результатов с положительной оценкой")
289
+
290
+ return results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
 
292
+ except Exception as e:
293
+ logger.error(f"Ошибка при выполнении поиска BM25: {str(e)}", exc_info=True)
294
+ return []
 
295
 
296
  # Подключение к SQLite базе
297
  def get_db_connection(db_path):
 
504
  except Exception as e:
505
  logger.error(f"Ошибка при сохранении лога: {e}")
506
 
507
+ # Загрузка данных из XLSX
508
+ @st.cache_data
509
+ def load_data():
510
+ try:
511
+ return pd.read_excel(XLSX_FILE_PATH)
512
+ except Exception as e:
513
+ logger.error(f"Ошибка загрузки XLSX файла: {e}")
514
+ return pd.DataFrame()
515
+
516
  # Поиск ответа
517
  def get_answer(question):
518
  # 1. Проверка специальных случаев
 
566
  answer = f"🤖 Сгенерированный ответ:\n\n{gpt_answer}\n\n"
567
  answer += "🔍 Использованные фрагменты документов:\n\n"
568
 
569
+ for i, res in enumerate(hybrid_results, 1):
570
+ answer += f"### Фрагмент {i} (метод: {res['type']}, оценка: {res['combined_score']:.2f})\n"
571
+ answer += f"{res['text']}\n"
572
+ answer += f"\n📚 Источник: {res['source']}\n\n"
573
 
574
  save_log(question, answer)
575
  return answer