MrSimple07 commited on
Commit
09d215a
·
1 Parent(s): a2d37cb

added the new version with row + chunk based chunking for tables

Browse files
Files changed (6) hide show
  1. app.py +72 -26
  2. config.py +24 -20
  3. documents_prep.py +477 -414
  4. index_retriever.py +37 -72
  5. table_prep.py +102 -16
  6. utils.py +5 -41
app.py CHANGED
@@ -1,7 +1,7 @@
1
  import gradio as gr
2
  import os
3
  from llama_index.core import Settings
4
- from documents_prep import load_json_documents, load_table_data, load_image_data, load_csv_chunks
5
  from utils import get_llm_model, get_embedding_model, get_reranker_model, answer_question
6
  from my_logging import log_message
7
  from index_retriever import create_vector_index, create_query_engine
@@ -11,17 +11,46 @@ from config import (
11
  JSON_FILES_DIR, TABLE_DATA_DIR, IMAGE_DATA_DIR, DEFAULT_MODEL, AVAILABLE_MODELS
12
  )
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  def create_chunks_display_html(chunk_info):
15
  if not chunk_info:
16
  return "<div style='padding: 20px; text-align: center; color: black;'>Нет данных о чанках</div>"
17
 
 
 
18
  html = "<div style='max-height: 500px; overflow-y: auto; padding: 10px; color: black;'>"
19
- html += f"<h4 style='color: black;'>Найдено релевантных чанков: {len(chunk_info)}</h4>"
20
 
21
- for i, chunk in enumerate(chunk_info):
22
  bg_color = "#f8f9fa" if i % 2 == 0 else "#e9ecef"
23
-
24
- # Get section display info
25
  section_display = get_section_display(chunk)
26
  formatted_content = get_formatted_content(chunk)
27
 
@@ -96,7 +125,6 @@ def initialize_system(repo_id, hf_token, download_dir, chunks_filename=None,
96
  json_files_dir=None, table_data_dir=None, image_data_dir=None,
97
  use_json_instead_csv=False):
98
  try:
99
- from documents_prep import process_documents_with_chunking
100
  log_message("Инициализация системы")
101
  os.makedirs(download_dir, exist_ok=True)
102
  from config import CHUNK_SIZE, CHUNK_OVERLAP
@@ -120,49 +148,64 @@ def initialize_system(repo_id, hf_token, download_dir, chunks_filename=None,
120
 
121
  all_documents = []
122
  chunks_df = None
123
- chunk_info = []
124
 
125
  if use_json_instead_csv and json_files_dir:
126
  log_message("Используем JSON файлы вместо CSV")
127
- json_documents, json_chunk_info = load_json_documents(repo_id, hf_token, json_files_dir, download_dir)
128
- all_documents.extend(json_documents)
129
- chunk_info.extend(json_chunk_info)
 
 
 
 
130
  else:
131
  if chunks_filename:
132
  log_message("Загружаем данные из CSV")
133
- csv_documents, chunks_df = load_csv_chunks(repo_id, hf_token, chunks_filename, download_dir)
134
- all_documents.extend(csv_documents)
135
 
136
  if table_data_dir:
137
  log_message("Добавляю табличные данные")
138
- table_documents = load_table_data(repo_id, hf_token, table_data_dir)
139
- log_message(f"Загружено {len(table_documents)} табличных документов")
140
 
141
- # Process table documents through chunking
142
- chunked_table_docs, table_chunk_info = process_documents_with_chunking(table_documents)
143
- all_documents.extend(chunked_table_docs)
144
- chunk_info.extend(table_chunk_info)
145
 
146
  if image_data_dir:
147
  log_message("Добавляю данные изображений")
148
- image_documents = load_image_data(repo_id, hf_token, image_data_dir)
149
- log_message(f"Загружено {len(image_documents)} документов изображений")
150
 
151
- # Process image documents through chunking
152
- chunked_image_docs, image_chunk_info = process_documents_with_chunking(image_documents)
153
- all_documents.extend(chunked_image_docs)
154
- chunk_info.extend(image_chunk_info)
155
 
156
  log_message(f"Всего документов после всей обработки: {len(all_documents)}")
157
 
158
  vector_index = create_vector_index(all_documents)
159
  query_engine = create_query_engine(vector_index)
160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  log_message(f"Система успешно инициализирована")
162
  return query_engine, chunks_df, reranker, vector_index, chunk_info
163
 
164
  except Exception as e:
165
  log_message(f"Ошибка инициализации: {str(e)}")
 
 
166
  return None, None, None, None, []
167
 
168
  def switch_model(model_name, vector_index):
@@ -320,9 +363,12 @@ def main_switch_model(model_name):
320
 
321
  def main():
322
  global query_engine, chunks_df, reranker, vector_index, current_model
323
-
 
 
 
 
324
  log_message("Запуск AIEXP - AI Expert для нормативной документации")
325
-
326
  query_engine, chunks_df, reranker, vector_index, chunk_info = initialize_system(
327
  repo_id=HF_REPO_ID,
328
  hf_token=HF_TOKEN,
 
1
  import gradio as gr
2
  import os
3
  from llama_index.core import Settings
4
+ from documents_prep import load_json_documents, load_table_documents, load_image_documents
5
  from utils import get_llm_model, get_embedding_model, get_reranker_model, answer_question
6
  from my_logging import log_message
7
  from index_retriever import create_vector_index, create_query_engine
 
11
  JSON_FILES_DIR, TABLE_DATA_DIR, IMAGE_DATA_DIR, DEFAULT_MODEL, AVAILABLE_MODELS
12
  )
13
 
14
+
15
+ def merge_table_chunks(chunk_info):
16
+ merged = {}
17
+
18
+ for chunk in chunk_info:
19
+ doc_type = chunk.get('type', 'text')
20
+ doc_id = chunk.get('document_id', 'unknown')
21
+
22
+ if doc_type == 'table' or doc_type == 'table_row':
23
+ table_num = chunk.get('table_number', '')
24
+ key = f"{doc_id}_{table_num}"
25
+
26
+ if key not in merged:
27
+ merged[key] = {
28
+ 'document_id': doc_id,
29
+ 'type': 'table',
30
+ 'table_number': table_num,
31
+ 'section_id': chunk.get('section_id', 'unknown'),
32
+ 'chunk_text': chunk.get('chunk_text', '')
33
+ }
34
+ else:
35
+ merged[key]['chunk_text'] += '\n' + chunk.get('chunk_text', '')
36
+ else:
37
+ unique_key = f"{doc_id}_{chunk.get('section_id', '')}_{chunk.get('chunk_id', 0)}"
38
+ merged[unique_key] = chunk
39
+
40
+ return list(merged.values())
41
+
42
+
43
  def create_chunks_display_html(chunk_info):
44
  if not chunk_info:
45
  return "<div style='padding: 20px; text-align: center; color: black;'>Нет данных о чанках</div>"
46
 
47
+ merged_chunks = merge_table_chunks(chunk_info)
48
+
49
  html = "<div style='max-height: 500px; overflow-y: auto; padding: 10px; color: black;'>"
50
+ html += f"<h4 style='color: black;'>Найдено релевантных чанков: {len(merged_chunks)}</h4>"
51
 
52
+ for i, chunk in enumerate(merged_chunks):
53
  bg_color = "#f8f9fa" if i % 2 == 0 else "#e9ecef"
 
 
54
  section_display = get_section_display(chunk)
55
  formatted_content = get_formatted_content(chunk)
56
 
 
125
  json_files_dir=None, table_data_dir=None, image_data_dir=None,
126
  use_json_instead_csv=False):
127
  try:
 
128
  log_message("Инициализация системы")
129
  os.makedirs(download_dir, exist_ok=True)
130
  from config import CHUNK_SIZE, CHUNK_OVERLAP
 
148
 
149
  all_documents = []
150
  chunks_df = None
 
151
 
152
  if use_json_instead_csv and json_files_dir:
153
  log_message("Используем JSON файлы вместо CSV")
154
+ from documents_prep import load_json_documents, chunk_text_documents
155
+
156
+ # Load JSON docs (returns list of Documents)
157
+ json_documents = load_json_documents(repo_id, hf_token, json_files_dir)
158
+ # Chunk them
159
+ json_chunks = chunk_text_documents(json_documents)
160
+ all_documents.extend(json_chunks)
161
  else:
162
  if chunks_filename:
163
  log_message("Загружаем данные из CSV")
164
+
 
165
 
166
  if table_data_dir:
167
  log_message("Добавляю табличные данные")
168
+ from documents_prep import load_table_documents
 
169
 
170
+ # load_table_documents already returns chunked documents
171
+ table_chunks = load_table_documents(repo_id, hf_token, table_data_dir)
172
+ log_message(f"Загружено {len(table_chunks)} табличных чанков")
173
+ all_documents.extend(table_chunks)
174
 
175
  if image_data_dir:
176
  log_message("Добавляю данные изображений")
177
+ from documents_prep import load_image_documents
 
178
 
179
+ # load_image_documents returns documents (no chunking needed)
180
+ image_documents = load_image_documents(repo_id, hf_token, image_data_dir)
181
+ log_message(f"Загружено {len(image_documents)} документов изображений")
182
+ all_documents.extend(image_documents)
183
 
184
  log_message(f"Всего документов после всей обработки: {len(all_documents)}")
185
 
186
  vector_index = create_vector_index(all_documents)
187
  query_engine = create_query_engine(vector_index)
188
 
189
+ # Create chunk_info for display (extract from documents metadata)
190
+ chunk_info = []
191
+ for doc in all_documents:
192
+ chunk_info.append({
193
+ 'document_id': doc.metadata.get('document_id', 'unknown'),
194
+ 'section_id': doc.metadata.get('section_id', 'unknown'),
195
+ 'type': doc.metadata.get('type', 'text'),
196
+ 'chunk_text': doc.text[:200] + '...' if len(doc.text) > 200 else doc.text,
197
+ 'table_number': doc.metadata.get('table_number', ''),
198
+ 'image_number': doc.metadata.get('image_number', ''),
199
+ 'section': doc.metadata.get('section', ''),
200
+ })
201
+
202
  log_message(f"Система успешно инициализирована")
203
  return query_engine, chunks_df, reranker, vector_index, chunk_info
204
 
205
  except Exception as e:
206
  log_message(f"Ошибка инициализации: {str(e)}")
207
+ import traceback
208
+ log_message(traceback.format_exc())
209
  return None, None, None, None, []
210
 
211
  def switch_model(model_name, vector_index):
 
363
 
364
  def main():
365
  global query_engine, chunks_df, reranker, vector_index, current_model
366
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "")
367
+ if GOOGLE_API_KEY:
368
+ log_message("Использование Google API для модели генерации текста")
369
+ else:
370
+ log_message("Google API ключ не найден, использование локальной модели")
371
  log_message("Запуск AIEXP - AI Expert для нормативной документации")
 
372
  query_engine, chunks_df, reranker, vector_index, chunk_info = initialize_system(
373
  repo_id=HF_REPO_ID,
374
  hf_token=HF_TOKEN,
config.py CHANGED
@@ -1,7 +1,6 @@
1
  import os
2
 
3
  EMBEDDING_MODEL = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
4
- RETRIEVER_TOP_K = 15
5
  SIMILARITY_THRESHOLD = 0.7
6
  RAG_FILES_DIR = "rag_files"
7
  PROCESSED_DATA_FILE = "processed_chunks.csv"
@@ -13,8 +12,6 @@ TABLE_DATA_DIR = "Табличные данные_JSON"
13
  IMAGE_DATA_DIR = "Изображения"
14
  DOWNLOAD_DIR = "rag_files"
15
  JSON_FILES_DIR ="JSON"
16
- HF_TOKEN = os.getenv('HF_TOKEN')
17
-
18
 
19
  GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY')
20
  OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
@@ -52,8 +49,11 @@ AVAILABLE_MODELS = {
52
 
53
  DEFAULT_MODEL = "Gemini 2.5 Flash"
54
 
55
- CHUNK_SIZE = 25000
56
- CHUNK_OVERLAP = 256
 
 
 
57
 
58
  CUSTOM_PROMPT = """
59
  Вы являетесь высокоспециализированным Ассистентом для анализа нормативных документов (AIEXP). Ваша цель - предоставлять точные, корректные и контекстно релевантные ответы исключительно на основе предоставленного контекста из нормативной документации.
@@ -90,42 +90,46 @@ CUSTOM_PROMPT = """
90
 
91
  ПРАВИЛА ФОРМИРОВАНИЯ ОТВЕТОВ:
92
 
93
- 1. ОБЯЗАТЕЛЬНОЕ УКАЗАНИЕ ИСТОЧНИКОВ:
94
- - Всегда указывайте конкретный документ (ГОСТ, раздел, пункт)
95
- - Формат: "Согласно [Документ], раздел [X], пункт [X.X]: [информация]"
96
- - При цитировании: используйте кавычки и точные ссылки
 
97
 
98
- 2. СТРУКТУРА ОТВЕТА:
99
  - Начинайте с прямого ответа на вопрос
100
  - Затем указывайте нормативные основания
101
  - Завершайте ссылками на конкретные документы и разделы
102
 
103
- 3. РАБОТА С КОНТЕКСТОМ:
104
  - Если информация найдена в контексте - предоставьте полный ответ
105
  - Если информация не найдена: "Информация по вашему запросу не найдена в доступной нормативной документации"
106
  - Не делайте предположений за пределами контекста
107
  - Не используйте общие знания
108
 
109
- 4. ТЕРМИНОЛОГИЯ И ЦИТИРОВАНИЕ:
110
  - Сохраняйте официальную терминологию НД
111
  - Цитируйте точные формулировки ключевых требований
112
  - При множественных источниках - укажите все релевантные
113
 
114
- 5. ФОРМАТИРОВАНИЕ:
115
  - Для перечислений: используйте нумерованные списки
116
  - Выделяйте критически важные требования
117
  - Структурируйте ответ логически
118
 
119
- ПРИМЕРЫ ПРАВИЛЬНОГО ФОРМАТИРОВАНИЯ:
 
 
120
 
121
- Вопрос: каких случаях могут быть признаны протоколы испытаний?"
122
- Ответ: "Протоколы испытаний могут быть признаны в следующих случаях:
123
 
124
- 1. Если они проведены испытательными лабораториями (центрами), аккредитованными в области использования атомной энергии (ГОСТ Р 50.08.04-2022, раздел 6 )
125
- 2. Если они проведены лабораториями, аккредитованными национальным органом Российской Федерации по аккредитации (ГОСТ Р 50.08.04-2022, пункт 4.1)
126
- 3. Если лаборатории прошли оценку состояния измерений
127
 
128
- Также допускается признание результатов испытаний, выполненных испытательными центрами (лабораториями), аккредитованными в национальных системах аккредитации страны изготовителя (ГОСТ Р 50.04.08-2019)."
 
 
129
 
130
  Контекст: {context_str}
131
 
 
1
  import os
2
 
3
  EMBEDDING_MODEL = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
 
4
  SIMILARITY_THRESHOLD = 0.7
5
  RAG_FILES_DIR = "rag_files"
6
  PROCESSED_DATA_FILE = "processed_chunks.csv"
 
12
  IMAGE_DATA_DIR = "Изображения"
13
  DOWNLOAD_DIR = "rag_files"
14
  JSON_FILES_DIR ="JSON"
 
 
15
 
16
  GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY')
17
  OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
 
49
 
50
  DEFAULT_MODEL = "Gemini 2.5 Flash"
51
 
52
+ CHUNK_SIZE = 1500
53
+ CHUNK_OVERLAP = 128
54
+
55
+ MAX_CHARS_TABLE = 2500
56
+ MAX_ROWS_TABLE = 10
57
 
58
  CUSTOM_PROMPT = """
59
  Вы являетесь высокоспециализированным Ассистентом для анализа нормативных документов (AIEXP). Ваша цель - предоставлять точные, корректные и контекстно релевантные ответы исключительно на основе предоставленного контекста из нормативной документации.
 
90
 
91
  ПРАВИЛА ФОРМИРОВАНИЯ ОТВЕТОВ:
92
 
93
+ Работай исключительно с информацией из предоставленного контекста. Запрещено использовать:
94
+ - Общие знания
95
+ - Информацию из интернета
96
+ - Данные из предыдущих диалогов
97
+ - Собственные предположения
98
 
99
+ 1. СТРУКТУРА ОТВЕТА:
100
  - Начинайте с прямого ответа на вопрос
101
  - Затем указывайте нормативные основания
102
  - Завершайте ссылками на конкретные документы и разделы
103
 
104
+ 2. РАБОТА С КОНТЕКСТОМ:
105
  - Если информация найдена в контексте - предоставьте полный ответ
106
  - Если информация не найдена: "Информация по вашему запросу не найдена в доступной нормативной документации"
107
  - Не делайте предположений за пределами контекста
108
  - Не используйте общие знания
109
 
110
+ 3. ТЕРМИНОЛОГИЯ И ЦИТИРОВАНИЕ:
111
  - Сохраняйте официальную терминологию НД
112
  - Цитируйте точные формулировки ключевых требований
113
  - При множественных источниках - укажите все релевантные
114
 
115
+ 4. ФОРМАТИРОВАНИЕ:
116
  - Для перечислений: используйте нумерованные списки
117
  - Выделяйте критически важные требования
118
  - Структурируйте ответ логически
119
 
120
+ # КАК РАБОТАТЬ С ЗАПРОСОМ
121
+
122
+ **Шаг 1:** Определи, что именно ищет пользователь (термин, требование, процедура, условие)
123
 
124
+ **Шаг 2:** Найди релевантную информацию в контексте
 
125
 
126
+ **Шаг 3:** Сформируй ответ:
127
+ - Если нашел: укажи документ и пункт, процитируй нужную часть
128
+ - Если не нашел: четко сообщи об отсутствии информации
129
 
130
+ **Шаг 4:** При наличии нескольких источников:
131
+ - Представь их последовательно с указанием источника каждого
132
+ - Если источников много (>4) — сначала дай их список, потом цитаты
133
 
134
  Контекст: {context_str}
135
 
documents_prep.py CHANGED
@@ -3,462 +3,525 @@ import zipfile
3
  import pandas as pd
4
  from huggingface_hub import hf_hub_download, list_repo_files
5
  from llama_index.core import Document
6
- from my_logging import log_message
7
  from llama_index.core.text_splitter import SentenceSplitter
8
- from config import CHUNK_SIZE, CHUNK_OVERLAP
9
- from table_prep import table_to_document, load_table_data
10
-
11
 
12
- def chunk_document(doc, chunk_size=None, chunk_overlap=None):
13
- if chunk_size is None:
14
- chunk_size = CHUNK_SIZE
15
- if chunk_overlap is None:
16
- chunk_overlap = CHUNK_OVERLAP
17
  text_splitter = SentenceSplitter(
18
- chunk_size=chunk_size,
19
- chunk_overlap=chunk_overlap,
20
- separator=" "
21
  )
22
 
23
- text_chunks = text_splitter.split_text(doc.text)
24
-
25
- chunked_docs = []
26
- for i, chunk_text in enumerate(text_chunks):
27
- chunk_metadata = doc.metadata.copy()
28
- chunk_metadata.update({
29
- "chunk_id": i,
30
- "total_chunks": len(text_chunks),
31
- "chunk_size": len(chunk_text),
32
- "original_doc_id": doc.id_ if hasattr(doc, 'id_') else None
33
- })
34
-
35
- chunked_doc = Document(
36
- text=chunk_text,
37
- metadata=chunk_metadata
38
- )
39
- chunked_docs.append(chunked_doc)
 
40
 
41
- return chunked_docs
42
 
43
- def process_documents_with_chunking(documents):
44
- all_chunked_docs = []
45
- chunk_info = []
46
- table_count = 0
47
- image_count = 0
48
- text_chunks_count = 0
 
 
 
49
 
50
- for doc in documents:
51
- doc_type = doc.metadata.get('type', 'text')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
- if doc_type == 'table':
54
- # Add tables as-is, no chunking
55
- table_count += 1
56
- all_chunked_docs.append(doc)
57
 
58
- chunk_info.append({
59
- 'document_id': doc.metadata.get('document_id', 'unknown'),
60
- 'section_id': doc.metadata.get('section_id', 'unknown'),
61
- 'chunk_id': 0,
62
- 'chunk_size': len(doc.text),
63
- 'chunk_preview': doc.text[:200] + "..." if len(doc.text) > 200 else doc.text,
64
  'type': 'table',
65
- 'table_number': doc.metadata.get('table_number', 'unknown')
66
- })
 
 
 
 
 
 
 
 
 
 
67
 
68
- elif doc_type == 'image':
69
- image_count += 1
70
- doc_size = len(doc.text)
71
- if doc_size > CHUNK_SIZE:
72
- chunked_docs = chunk_document(doc)
73
- all_chunked_docs.extend(chunked_docs)
74
-
75
- for i, chunk_doc in enumerate(chunked_docs):
76
- chunk_info.append({
77
- 'document_id': chunk_doc.metadata.get('document_id', 'unknown'),
78
- 'section_id': chunk_doc.metadata.get('section_id', 'unknown'),
79
- 'chunk_id': i,
80
- 'chunk_size': len(chunk_doc.text),
81
- 'chunk_preview': chunk_doc.text[:200] + "..." if len(chunk_doc.text) > 200 else chunk_doc.text,
82
- 'type': 'image',
83
- 'image_number': chunk_doc.metadata.get('image_number', 'unknown')
84
- })
85
- else:
86
- all_chunked_docs.append(doc)
87
- chunk_info.append({
88
- 'document_id': doc.metadata.get('document_id', 'unknown'),
89
- 'section_id': doc.metadata.get('section_id', 'unknown'),
90
- 'chunk_id': 0,
91
- 'chunk_size': doc_size,
92
- 'chunk_preview': doc.text[:200] + "..." if len(doc.text) > 200 else doc.text,
93
- 'type': 'image',
94
- 'image_number': doc.metadata.get('image_number', 'unknown')
95
- })
96
 
97
- else:
98
- doc_size = len(doc.text)
99
- if doc_size > CHUNK_SIZE:
100
- chunked_docs = chunk_document(doc)
101
- all_chunked_docs.extend(chunked_docs)
102
- text_chunks_count += len(chunked_docs)
103
-
104
- for i, chunk_doc in enumerate(chunked_docs):
105
- chunk_info.append({
106
- 'document_id': chunk_doc.metadata.get('document_id', 'unknown'),
107
- 'section_id': chunk_doc.metadata.get('section_id', 'unknown'),
108
- 'chunk_id': i,
109
- 'chunk_size': len(chunk_doc.text),
110
- 'chunk_preview': chunk_doc.text[:200] + "..." if len(chunk_doc.text) > 200 else chunk_doc.text,
111
- 'type': 'text'
112
- })
113
- else:
114
- all_chunked_docs.append(doc)
115
- chunk_info.append({
116
- 'document_id': doc.metadata.get('document_id', 'unknown'),
117
- 'section_id': doc.metadata.get('section_id', 'unknown'),
118
- 'chunk_id': 0,
119
- 'chunk_size': doc_size,
120
- 'chunk_preview': doc.text[:200] + "..." if len(doc.text) > 200 else doc.text,
121
- 'type': 'text'
122
- })
123
 
124
- log_message(f"\n{'='*60}")
125
- log_message(f"ИТОГО ОБРАБОТАНО ДОКУМЕНТОВ:")
126
- log_message(f" • Таблицы: {table_count} (добавлены целиком)")
127
- log_message(f" • Изображения: {image_count}")
128
- log_message(f" • Текстовые чанки: {text_chunks_count}")
129
- log_message(f" • Всего документов: {len(all_chunked_docs)}")
130
- log_message(f"{'='*60}\n")
131
-
132
- return all_chunked_docs, chunk_info
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
- def extract_text_from_json(data, document_id, document_name):
135
- documents = []
 
 
 
 
 
 
136
 
137
- if 'sections' in data:
138
- for section in data['sections']:
139
- section_id = section.get('section_id', 'Unknown')
140
- section_text = section.get('section_text', '')
141
-
142
- section_path = f"{section_id}"
143
- section_title = extract_section_title(section_text)
144
-
145
- if section_text.strip():
146
- doc = Document(
147
- text=section_text,
148
- metadata={
149
- "type": "text",
150
- "document_id": document_id,
151
- "document_name": document_name,
152
- "section_id": section_id,
153
- "section_text": section_title[:200],
154
- "section_path": section_path,
155
- "level": "section"
156
- }
157
- )
158
- documents.append(doc)
159
-
160
- if 'subsections' in section:
161
- for subsection in section['subsections']:
162
- subsection_id = subsection.get('subsection_id', 'Unknown')
163
- subsection_text = subsection.get('subsection_text', '')
164
- subsection_title = extract_section_title(subsection_text)
165
- subsection_path = f"{section_path}.{subsection_id}"
166
-
167
- if subsection_text.strip():
168
- doc = Document(
169
- text=subsection_text,
170
- metadata={
171
- "type": "text",
172
- "document_id": document_id,
173
- "document_name": document_name,
174
- "section_id": subsection_id,
175
- "section_text": subsection_title[:200],
176
- "section_path": subsection_path,
177
- "level": "subsection",
178
- "parent_section": section_id,
179
- "parent_title": section_title[:100]
180
- }
181
- )
182
- documents.append(doc)
183
-
184
- if 'sub_subsections' in subsection:
185
- for sub_subsection in subsection['sub_subsections']:
186
- sub_subsection_id = sub_subsection.get('sub_subsection_id', 'Unknown')
187
- sub_subsection_text = sub_subsection.get('sub_subsection_text', '')
188
- sub_subsection_title = extract_section_title(sub_subsection_text)
189
- sub_subsection_path = f"{subsection_path}.{sub_subsection_id}"
190
-
191
- if sub_subsection_text.strip():
192
- doc = Document(
193
- text=sub_subsection_text,
194
- metadata={
195
- "type": "text",
196
- "document_id": document_id,
197
- "document_name": document_name,
198
- "section_id": sub_subsection_id,
199
- "section_text": sub_subsection_title[:200],
200
- "section_path": sub_subsection_path,
201
- "level": "sub_subsection",
202
- "parent_section": subsection_id,
203
- "parent_title": subsection_title[:100]
204
- }
205
- )
206
- documents.append(doc)
207
-
208
- if 'sub_sub_subsections' in sub_subsection:
209
- for sub_sub_subsection in sub_subsection['sub_sub_subsections']:
210
- sub_sub_subsection_id = sub_sub_subsection.get('sub_sub_subsection_id', 'Unknown')
211
- sub_sub_subsection_text = sub_sub_subsection.get('sub_sub_subsection_text', '')
212
- sub_sub_subsection_title = extract_section_title(sub_sub_subsection_text)
213
-
214
- if sub_sub_subsection_text.strip():
215
- doc = Document(
216
- text=sub_sub_subsection_text,
217
- metadata={
218
- "type": "text",
219
- "document_id": document_id,
220
- "document_name": document_name,
221
- "section_id": sub_sub_subsection_id,
222
- "section_text": sub_sub_subsection_title[:200],
223
- "section_path": f"{sub_subsection_path}.{sub_sub_subsection_id}",
224
- "level": "sub_sub_subsection",
225
- "parent_section": sub_subsection_id,
226
- "parent_title": sub_subsection_title[:100]
227
- }
228
- )
229
- documents.append(doc)
230
 
231
- return documents
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
 
233
- def load_json_documents(repo_id, hf_token, json_files_dir, download_dir):
234
- log_message("Начинаю загрузку JSON документов")
 
 
 
 
 
235
 
236
- try:
237
- files = list_repo_files(repo_id=repo_id, repo_type="dataset", token=hf_token)
238
- zip_files = [f for f in files if f.startswith(json_files_dir) and f.endswith('.zip')]
239
- json_files = [f for f in files if f.startswith(json_files_dir) and f.endswith('.json')]
240
-
241
- log_message(f"Найдено {len(zip_files)} ZIP файлов и {len(json_files)} прямых JSON файлов")
242
-
243
- all_documents = []
244
-
245
- for zip_file_path in zip_files:
246
- try:
247
- log_message(f"Загружаю ZIP архив: {zip_file_path}")
248
- local_zip_path = hf_hub_download(
249
- repo_id=repo_id,
250
- filename=zip_file_path,
251
- local_dir=download_dir,
252
- repo_type="dataset",
253
- token=hf_token
254
- )
255
-
256
- documents = extract_zip_and_process_json(local_zip_path)
257
- all_documents.extend(documents)
258
- log_message(f"Извлечено {len(documents)} документов из ZIP архива {zip_file_path}")
259
-
260
- except Exception as e:
261
- log_message(f"Ошибка обработки ZIP файла {zip_file_path}: {str(e)}")
262
- continue
263
-
264
- for file_path in json_files:
265
- try:
266
- log_message(f"Обрабатываю прямой JSON файл: {file_path}")
267
- local_path = hf_hub_download(
268
- repo_id=repo_id,
269
- filename=file_path,
270
- local_dir=download_dir,
271
- repo_type="dataset",
272
- token=hf_token
273
- )
274
-
275
- with open(local_path, 'r', encoding='utf-8') as f:
276
- json_data = json.load(f)
277
-
278
- document_metadata = json_data.get('document_metadata', {})
279
- document_id = document_metadata.get('document_id', 'unknown')
280
- document_name = document_metadata.get('document_name', 'unknown')
281
-
282
- documents = extract_text_from_json(json_data, document_id, document_name)
283
- all_documents.extend(documents)
284
 
285
- log_message(f"Извлечено {len(documents)} документов из {file_path}")
 
286
 
287
- except Exception as e:
288
- log_message(f"Ошибка обработки файла {file_path}: {str(e)}")
289
- continue
290
-
291
- log_message(f"Всего создано {len(all_documents)} исходных документов из JSON файлов")
292
-
293
- # Process documents through chunking function
294
- chunked_documents, chunk_info = process_documents_with_chunking(all_documents)
295
-
296
- log_message(f"После chunking получено {len(chunked_documents)} чанков из JSON данных")
297
-
298
- return chunked_documents, chunk_info
299
-
300
- except Exception as e:
301
- log_message(f"Ошибка загрузки JSON документов: {str(e)}")
302
- return [], []
303
 
304
- def extract_section_title(section_text):
305
- if not section_text.strip():
306
- return ""
 
307
 
308
- lines = section_text.strip().split('\n')
309
- first_line = lines[0].strip()
310
 
311
- if len(first_line) < 200 and not first_line.endswith('.'):
312
- return first_line
 
313
 
314
- # Otherwise, extract first sentence
315
- sentences = first_line.split('.')
316
- if len(sentences) > 1:
317
- return sentences[0].strip()
318
 
319
- return first_line[:100] + "..." if len(first_line) > 100 else first_line
320
-
321
- def extract_zip_and_process_json(zip_path):
322
  documents = []
 
323
 
324
- try:
325
- with zipfile.ZipFile(zip_path, 'r') as zip_ref:
326
- zip_files = zip_ref.namelist()
327
- json_files = [f for f in zip_files if f.endswith('.json') and not f.startswith('__MACOSX')]
 
 
 
 
 
328
 
329
- log_message(f"Найдено {len(json_files)} JSON файлов в архиве")
 
 
 
 
 
 
 
330
 
331
- for json_file in json_files:
332
- try:
333
- log_message(f"Обрабатываю файл из архива: {json_file}")
334
-
335
- with zip_ref.open(json_file) as f:
336
- json_data = json.load(f)
337
-
338
- document_metadata = json_data.get('document_metadata', {})
339
- document_id = document_metadata.get('document_id', 'unknown')
340
- document_name = document_metadata.get('document_name', 'unknown')
341
-
342
- docs = extract_text_from_json(json_data, document_id, document_name)
343
- documents.extend(docs)
344
-
345
- log_message(f"Извлечено {len(docs)} документов из {json_file}")
346
-
347
- except Exception as e:
348
- log_message(f"Ошибка обработки файла {json_file}: {str(e)}")
349
- continue
350
 
351
- except Exception as e:
352
- log_message(f"Ошибка извлечения ZIP архива {zip_path}: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
 
354
  return documents
355
 
356
- def load_image_data(repo_id, hf_token, image_data_dir):
357
- log_message("Начинаю загрузку данных изображений")
 
358
 
359
- image_files = []
360
  try:
361
- files = list_repo_files(repo_id=repo_id, repo_type="dataset", token=hf_token)
362
- for file in files:
363
- if file.startswith(image_data_dir) and file.endswith('.csv'):
364
- image_files.append(file)
365
 
366
- log_message(f"Найдено {len(image_files)} CSV файлов с изображениями")
367
 
368
- image_documents = []
369
- for file_path in image_files:
370
- try:
371
- log_message(f"Обрабатываю файл изображений: {file_path}")
372
- local_path = hf_hub_download(
373
- repo_id=repo_id,
374
- filename=file_path,
375
- local_dir='',
376
- repo_type="dataset",
377
- token=hf_token
378
- )
379
-
380
- df = pd.read_csv(local_path)
381
- log_message(f"Загружено {len(df)} записей изображений из файла {file_path}")
382
-
383
- # Обработка с правильными названиями колонок
384
- for _, row in df.iterrows():
385
- section_value = row.get('Раздел документа', 'Неизвестно')
386
-
387
- content = f"Изображение: {row.get('№ Изображения', 'Неизвестно')}\n"
388
- content += f"Название: {row.get('Название изображения', 'Неизвестно')}\n"
389
- content += f"Описание: {row.get('Описание изображение', 'Неизвестно')}\n" # Опечатка в названии колонки
390
- content += f"Документ: {row.get('Обозначение документа', 'Неизвестно')}\n"
391
- content += f"Раздел: {section_value}\n"
392
- content += f"Файл: {row.get('Файл изображения', 'Неизвестно')}\n"
393
-
394
- doc = Document(
395
- text=content,
396
  metadata={
397
- "type": "image",
398
- "image_number": str(row.get('№ Изображения', 'unknown')),
399
- "image_title": str(row.get('Название изображения', 'unknown')),
400
- "image_description": str(row.get('Описание изображение', 'unknown')),
401
- "document_id": str(row.get('Обозначение документа', 'unknown')),
402
- "file_path": str(row.get('Файл изображения', 'unknown')),
403
- "section": str(section_value),
404
- "section_id": str(section_value)
405
  }
406
- )
407
- image_documents.append(doc)
408
-
409
- except Exception as e:
410
- log_message(f"Ошибка обработки файла {file_path}: {str(e)}")
411
- continue
412
-
413
- log_message(f"Создано {len(image_documents)} документов из изображений")
414
- return image_documents
415
-
 
 
 
 
416
  except Exception as e:
417
- log_message(f"Ошибка загрузки данных изображений: {str(e)}")
418
- return []
 
419
 
420
 
421
- def load_csv_chunks(repo_id, hf_token, chunks_filename, download_dir):
422
- log_message("Загружаю данные чанков из CSV")
 
423
 
424
- try:
425
- chunks_csv_path = hf_hub_download(
426
- repo_id=repo_id,
427
- filename=chunks_filename,
428
- local_dir=download_dir,
429
- repo_type="dataset",
430
- token=hf_token
431
- )
432
-
433
- chunks_df = pd.read_csv(chunks_csv_path)
434
- log_message(f"Загружено {len(chunks_df)} чанков из CSV")
435
-
436
- text_column = None
437
- for col in chunks_df.columns:
438
- if 'text' in col.lower() or 'content' in col.lower() or 'chunk' in col.lower():
439
- text_column = col
440
- break
441
-
442
- if text_column is None:
443
- text_column = chunks_df.columns[0]
444
-
445
- log_message(f"Использую колонку: {text_column}")
446
-
447
- documents = []
448
- for i, (_, row) in enumerate(chunks_df.iterrows()):
449
- doc = Document(
450
- text=str(row[text_column]),
451
- metadata={
452
- "chunk_id": row.get('chunk_id', i),
453
- "document_id": row.get('document_id', 'unknown'),
454
- "type": "text"
455
- }
456
  )
457
- documents.append(doc)
458
-
459
- log_message(f"Создано {len(documents)} текстовых документов из CSV")
460
- return documents, chunks_df
461
-
462
- except Exception as e:
463
- log_message(f"Ошибка загрузки CSV данных: {str(e)}")
464
- return [], None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import pandas as pd
4
  from huggingface_hub import hf_hub_download, list_repo_files
5
  from llama_index.core import Document
 
6
  from llama_index.core.text_splitter import SentenceSplitter
7
+ from my_logging import log_message
8
+ from config import CHUNK_SIZE, CHUNK_OVERLAP, MAX_CHARS_TABLE, MAX_ROWS_TABLE
 
9
 
10
+ def chunk_text_documents(documents):
 
 
 
 
11
  text_splitter = SentenceSplitter(
12
+ chunk_size=CHUNK_SIZE,
13
+ chunk_overlap=CHUNK_OVERLAP
 
14
  )
15
 
16
+ chunked = []
17
+ for doc in documents:
18
+ chunks = text_splitter.get_nodes_from_documents([doc])
19
+ for i, chunk in enumerate(chunks):
20
+ chunk.metadata.update({
21
+ 'chunk_id': i,
22
+ 'total_chunks': len(chunks),
23
+ 'chunk_size': len(chunk.text) # Add chunk size
24
+ })
25
+ chunked.append(chunk)
26
+
27
+ # Log statistics
28
+ if chunked:
29
+ avg_size = sum(len(c.text) for c in chunked) / len(chunked)
30
+ min_size = min(len(c.text) for c in chunked)
31
+ max_size = max(len(c.text) for c in chunked)
32
+ log_message(f"✓ Text: {len(documents)} docs → {len(chunked)} chunks")
33
+ log_message(f" Size stats: avg={avg_size:.0f}, min={min_size}, max={max_size} chars")
34
 
35
+ return chunked
36
 
37
+
38
+ def chunk_table_by_content(table_data, doc_id, max_chars=MAX_CHARS_TABLE, max_rows=MAX_ROWS_TABLE):
39
+ headers = table_data.get('headers', [])
40
+ rows = table_data.get('data', [])
41
+ table_num = table_data.get('table_number', 'unknown')
42
+ table_title = table_data.get('table_title', '')
43
+ section = table_data.get('section', '')
44
+
45
+ table_num_clean = str(table_num).strip()
46
 
47
+ import re
48
+ if 'приложени' in section.lower():
49
+ appendix_match = re.search(r'приложени[еия]\s*(\d+|[а-яА-Я])', section.lower())
50
+ if appendix_match:
51
+ appendix_num = appendix_match.group(1).upper()
52
+ table_identifier = f"{table_num_clean} Приложение {appendix_num}"
53
+ else:
54
+ table_identifier = table_num_clean
55
+ else:
56
+ table_identifier = table_num_clean
57
+
58
+ if not rows:
59
+ return []
60
+
61
+ log_message(f" 📊 Processing: {doc_id} - {table_identifier} ({len(rows)} rows)")
62
+
63
+ # Calculate base metadata size
64
+ base_content = format_table_header(doc_id, table_identifier, table_num, table_title, section, headers)
65
+ base_size = len(base_content)
66
+ available_space = max_chars - base_size - 200
67
+
68
+ # If entire table fits, return as one chunk
69
+ full_rows_content = format_table_rows([{**row, '_idx': i+1} for i, row in enumerate(rows)])
70
+ if base_size + len(full_rows_content) <= max_chars and len(rows) <= max_rows:
71
+ content = base_content + full_rows_content + format_table_footer(table_identifier, doc_id)
72
+
73
+ metadata = {
74
+ 'type': 'table',
75
+ 'document_id': doc_id,
76
+ 'table_number': table_num_clean,
77
+ 'table_identifier': table_identifier,
78
+ 'table_title': table_title,
79
+ 'section': section,
80
+ 'total_rows': len(rows),
81
+ 'chunk_size': len(content),
82
+ 'is_complete_table': True
83
+ }
84
+
85
+ log_message(f" Single chunk: {len(content)} chars, {len(rows)} rows")
86
+ return [Document(text=content, metadata=metadata)]
87
+
88
+ chunks = []
89
+ current_rows = []
90
+ current_size = 0
91
+ chunk_num = 0
92
+
93
+ for i, row in enumerate(rows):
94
+ row_text = format_single_row(row, i + 1)
95
+ row_size = len(row_text)
96
+
97
+ should_split = (current_size + row_size > available_space or len(current_rows) >= max_rows) and current_rows
98
 
99
+ if should_split:
100
+ content = base_content + format_table_rows(current_rows)
101
+ content += f"\n\nСтроки {current_rows[0]['_idx']}-{current_rows[-1]['_idx']} из {len(rows)}\n"
102
+ content += format_table_footer(table_identifier, doc_id)
103
 
104
+ metadata = {
 
 
 
 
 
105
  'type': 'table',
106
+ 'document_id': doc_id,
107
+ 'table_number': table_num_clean,
108
+ 'table_identifier': table_identifier,
109
+ 'table_title': table_title,
110
+ 'section': section,
111
+ 'chunk_id': chunk_num,
112
+ 'row_start': current_rows[0]['_idx'] - 1,
113
+ 'row_end': current_rows[-1]['_idx'],
114
+ 'total_rows': len(rows),
115
+ 'chunk_size': len(content),
116
+ 'is_complete_table': False
117
+ }
118
 
119
+ chunks.append(Document(text=content, metadata=metadata))
120
+ log_message(f" Chunk {chunk_num + 1}: {len(content)} chars, {len(current_rows)} rows")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
+ chunk_num += 1
123
+ current_rows = []
124
+ current_size = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
+ # Add row with index
127
+ row_copy = row.copy() if isinstance(row, dict) else {'data': row}
128
+ row_copy['_idx'] = i + 1
129
+ current_rows.append(row_copy)
130
+ current_size += row_size
131
+
132
+ # Add final chunk
133
+ if current_rows:
134
+ content = base_content + format_table_rows(current_rows)
135
+ content += f"\n\nСтроки {current_rows[0]['_idx']}-{current_rows[-1]['_idx']} из {len(rows)}\n"
136
+ content += format_table_footer(table_identifier, doc_id)
137
+
138
+ metadata = {
139
+ 'type': 'table',
140
+ 'document_id': doc_id,
141
+ 'table_number': table_num_clean,
142
+ 'table_identifier': table_identifier,
143
+ 'table_title': table_title,
144
+ 'section': section,
145
+ 'chunk_id': chunk_num,
146
+ 'row_start': current_rows[0]['_idx'] - 1,
147
+ 'row_end': current_rows[-1]['_idx'],
148
+ 'total_rows': len(rows),
149
+ 'chunk_size': len(content),
150
+ 'is_complete_table': False
151
+ }
152
+
153
+ chunks.append(Document(text=content, metadata=metadata))
154
+ log_message(f" Chunk {chunk_num + 1}: {len(content)} chars, {len(current_rows)} rows")
155
+
156
+ return chunks
157
 
158
+
159
+ def format_table_header(doc_id, table_identifier, table_num, table_title, section, headers):
160
+ content = f"ТАБЛИЦА {table_identifier} из {doc_id}\n"
161
+ if table_title:
162
+ content += f"НАЗВАНИЕ: {table_title}\n"
163
+ if section:
164
+ content += f"РАЗДЕЛ: {section}\n"
165
+ content += f"{'='*70}\n"
166
 
167
+ if headers:
168
+ header_str = ' | '.join(str(h) for h in headers)
169
+ content += f"ЗАГОЛОВКИ: {header_str}\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
+ content += "ДАННЫЕ:\n"
172
+ return content
173
+
174
+
175
+ def format_single_row(row, idx):
176
+ """Format a single row"""
177
+ if isinstance(row, dict):
178
+ parts = [f"{k}: {v}" for k, v in row.items()
179
+ if v and str(v).strip() and str(v).lower() not in ['nan', 'none', '']]
180
+ if parts:
181
+ return f"{idx}. {' | '.join(parts)}\n"
182
+ elif isinstance(row, list):
183
+ parts = [str(v) for v in row if v and str(v).strip() and str(v).lower() not in ['nan', 'none', '']]
184
+ if parts:
185
+ return f"{idx}. {' | '.join(parts)}\n"
186
+ return ""
187
+
188
+
189
+ def format_table_rows(rows):
190
+ """Format multiple rows"""
191
+ content = ""
192
+ for row in rows:
193
+ idx = row.get('_idx', 0)
194
+ content += format_single_row(row, idx)
195
+ return content
196
 
197
+
198
+ def format_table_footer(table_identifier, doc_id):
199
+ """Format table footer"""
200
+ return f"\n{'='*70}\nКОНЕЦ ТАБЛИЦЫ {table_identifier} ИЗ {doc_id}\n"
201
+
202
+ def load_table_documents(repo_id, hf_token, table_dir):
203
+ log_message("Loading tables...")
204
 
205
+ files = list_repo_files(repo_id=repo_id, repo_type="dataset", token=hf_token)
206
+ table_files = [f for f in files if f.startswith(table_dir) and f.endswith('.json')]
207
+
208
+ all_chunks = []
209
+ for file_path in table_files:
210
+ try:
211
+ local_path = hf_hub_download(
212
+ repo_id=repo_id,
213
+ filename=file_path,
214
+ repo_type="dataset",
215
+ token=hf_token
216
+ )
217
+
218
+ with open(local_path, 'r', encoding='utf-8') as f:
219
+ data = json.load(f)
220
+
221
+ file_doc_id = data.get('document_id', data.get('document', 'unknown'))
222
+
223
+ for sheet in data.get('sheets', []):
224
+ sheet_doc_id = sheet.get('document_id', sheet.get('document', file_doc_id))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
+ chunks = chunk_table_by_content(sheet, sheet_doc_id, max_chars=1000)
227
+ all_chunks.extend(chunks)
228
 
229
+ except Exception as e:
230
+ log_message(f"Error loading {file_path}: {e}")
231
+
232
+ log_message(f"✓ Loaded {len(all_chunks)} table chunks")
233
+ return all_chunks
234
+
 
 
 
 
 
 
 
 
 
 
235
 
236
+ def load_json_documents(repo_id, hf_token, json_dir):
237
+ import zipfile
238
+ import tempfile
239
+ import os
240
 
241
+ log_message("Loading JSON documents...")
 
242
 
243
+ files = list_repo_files(repo_id=repo_id, repo_type="dataset", token=hf_token)
244
+ json_files = [f for f in files if f.startswith(json_dir) and f.endswith('.json')]
245
+ zip_files = [f for f in files if f.startswith(json_dir) and f.endswith('.zip')]
246
 
247
+ log_message(f"Found {len(json_files)} JSON files and {len(zip_files)} ZIP files")
 
 
 
248
 
 
 
 
249
  documents = []
250
+ stats = {'success': 0, 'failed': 0, 'empty': 0}
251
 
252
+ for file_path in json_files:
253
+ try:
254
+ log_message(f" Loading: {file_path}")
255
+ local_path = hf_hub_download(
256
+ repo_id=repo_id,
257
+ filename=file_path,
258
+ repo_type="dataset",
259
+ token=hf_token
260
+ )
261
 
262
+ docs = extract_sections_from_json(local_path)
263
+ if docs:
264
+ documents.extend(docs)
265
+ stats['success'] += 1
266
+ log_message(f" ✓ Extracted {len(docs)} sections")
267
+ else:
268
+ stats['empty'] += 1
269
+ log_message(f" ⚠ No sections found")
270
 
271
+ except Exception as e:
272
+ stats['failed'] += 1
273
+ log_message(f" Error: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
+ for zip_path in zip_files:
276
+ try:
277
+ log_message(f" Processing ZIP: {zip_path}")
278
+ local_zip = hf_hub_download(
279
+ repo_id=repo_id,
280
+ filename=zip_path,
281
+ repo_type="dataset",
282
+ token=hf_token
283
+ )
284
+
285
+ with zipfile.ZipFile(local_zip, 'r') as zf:
286
+ json_files_in_zip = [f for f in zf.namelist()
287
+ if f.endswith('.json')
288
+ and not f.startswith('__MACOSX')
289
+ and not f.startswith('.')
290
+ and not '._' in f]
291
+
292
+ log_message(f" Found {len(json_files_in_zip)} JSON files in ZIP")
293
+
294
+ for json_file in json_files_in_zip:
295
+ try:
296
+ file_content = zf.read(json_file)
297
+
298
+ # Skip if file is too small
299
+ if len(file_content) < 10:
300
+ log_message(f" ✗ Skipping: {json_file} (file too small)")
301
+ stats['failed'] += 1
302
+ continue
303
+
304
+ # Try UTF-8 first (most common)
305
+ try:
306
+ text_content = file_content.decode('utf-8')
307
+ except UnicodeDecodeError:
308
+ try:
309
+ text_content = file_content.decode('utf-8-sig')
310
+ except UnicodeDecodeError:
311
+ try:
312
+ # Try UTF-16 (the issue you're seeing)
313
+ text_content = file_content.decode('utf-16')
314
+ except UnicodeDecodeError:
315
+ try:
316
+ text_content = file_content.decode('windows-1251')
317
+ except UnicodeDecodeError:
318
+ log_message(f" ✗ Skipping: {json_file} (encoding failed)")
319
+ stats['failed'] += 1
320
+ continue
321
+
322
+ # Validate JSON structure
323
+ if not text_content.strip().startswith('{') and not text_content.strip().startswith('['):
324
+ log_message(f" ✗ Skipping: {json_file} (not valid JSON)")
325
+ stats['failed'] += 1
326
+ continue
327
+
328
+ with tempfile.NamedTemporaryFile(mode='w', delete=False,
329
+ suffix='.json', encoding='utf-8') as tmp:
330
+ tmp.write(text_content)
331
+ tmp_path = tmp.name
332
+
333
+ docs = extract_sections_from_json(tmp_path)
334
+ if docs:
335
+ documents.extend(docs)
336
+ stats['success'] += 1
337
+ log_message(f" ✓ {json_file}: {len(docs)} sections")
338
+ else:
339
+ stats['empty'] += 1
340
+ log_message(f" ⚠ {json_file}: No sections")
341
+
342
+ os.unlink(tmp_path)
343
+
344
+ except json.JSONDecodeError as e:
345
+ stats['failed'] += 1
346
+ log_message(f" ✗ {json_file}: Invalid JSON")
347
+ except Exception as e:
348
+ stats['failed'] += 1
349
+ log_message(f" ✗ {json_file}: {str(e)[:100]}")
350
+
351
+ except Exception as e:
352
+ log_message(f" ✗ Error with ZIP: {e}")
353
+
354
+ log_message(f"="*60)
355
+ log_message(f"JSON Loading Stats:")
356
+ log_message(f" Success: {stats['success']}")
357
+ log_message(f" Empty: {stats['empty']}")
358
+ log_message(f" Failed: {stats['failed']}")
359
+ log_message(f" Total sections: {len(documents)}")
360
+ log_message(f"="*60)
361
 
362
  return documents
363
 
364
+ def extract_sections_from_json(json_path):
365
+ """Extract sections from a single JSON file"""
366
+ documents = []
367
 
 
368
  try:
369
+ with open(json_path, 'r', encoding='utf-8') as f:
370
+ data = json.load(f)
 
 
371
 
372
+ doc_id = data.get('document_metadata', {}).get('document_id', 'unknown')
373
 
374
+ # Extract all section levels
375
+ for section in data.get('sections', []):
376
+ if section.get('section_text', '').strip():
377
+ documents.append(Document(
378
+ text=section['section_text'],
379
+ metadata={
380
+ 'type': 'text',
381
+ 'document_id': doc_id,
382
+ 'section_id': section.get('section_id', '')
383
+ }
384
+ ))
385
+
386
+ # Subsections
387
+ for subsection in section.get('subsections', []):
388
+ if subsection.get('subsection_text', '').strip():
389
+ documents.append(Document(
390
+ text=subsection['subsection_text'],
 
 
 
 
 
 
 
 
 
 
 
391
  metadata={
392
+ 'type': 'text',
393
+ 'document_id': doc_id,
394
+ 'section_id': subsection.get('subsection_id', '')
 
 
 
 
 
395
  }
396
+ ))
397
+
398
+ # Sub-subsections
399
+ for sub_sub in subsection.get('sub_subsections', []):
400
+ if sub_sub.get('sub_subsection_text', '').strip():
401
+ documents.append(Document(
402
+ text=sub_sub['sub_subsection_text'],
403
+ metadata={
404
+ 'type': 'text',
405
+ 'document_id': doc_id,
406
+ 'section_id': sub_sub.get('sub_subsection_id', '')
407
+ }
408
+ ))
409
+
410
  except Exception as e:
411
+ log_message(f"Error extracting from {json_path}: {e}")
412
+
413
+ return documents
414
 
415
 
416
+ def load_table_documents(repo_id, hf_token, table_dir):
417
+ """Load and chunk tables"""
418
+ log_message("Loading tables...")
419
 
420
+ files = list_repo_files(repo_id=repo_id, repo_type="dataset", token=hf_token)
421
+ table_files = [f for f in files if f.startswith(table_dir) and f.endswith('.json')]
422
+
423
+ all_chunks = []
424
+ for file_path in table_files:
425
+ try:
426
+ local_path = hf_hub_download(
427
+ repo_id=repo_id,
428
+ filename=file_path,
429
+ repo_type="dataset",
430
+ token=hf_token
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  )
432
+
433
+ with open(local_path, 'r', encoding='utf-8') as f:
434
+ data = json.load(f)
435
+
436
+ # Extract file-level document_id
437
+ file_doc_id = data.get('document_id', data.get('document', 'unknown'))
438
+
439
+ for sheet in data.get('sheets', []):
440
+ # Use sheet-level document_id if available, otherwise use file-level
441
+ sheet_doc_id = sheet.get('document_id', sheet.get('document', file_doc_id))
442
+
443
+ # CRITICAL: Pass document_id to chunk function
444
+ chunks = chunk_table_by_content(sheet, sheet_doc_id)
445
+ all_chunks.extend(chunks)
446
+
447
+ except Exception as e:
448
+ log_message(f"Error loading {file_path}: {e}")
449
+
450
+ log_message(f"✓ Loaded {len(all_chunks)} table chunks")
451
+ return all_chunks
452
+
453
+
454
+ def load_image_documents(repo_id, hf_token, image_dir):
455
+ """Load image descriptions"""
456
+ log_message("Loading images...")
457
+
458
+ files = list_repo_files(repo_id=repo_id, repo_type="dataset", token=hf_token)
459
+ csv_files = [f for f in files if f.startswith(image_dir) and f.endswith('.csv')]
460
+
461
+ documents = []
462
+ for file_path in csv_files:
463
+ try:
464
+ local_path = hf_hub_download(
465
+ repo_id=repo_id,
466
+ filename=file_path,
467
+ repo_type="dataset",
468
+ token=hf_token
469
+ )
470
+
471
+ df = pd.read_csv(local_path)
472
+
473
+ for _, row in df.iterrows():
474
+ content = f"Документ: {row.get('Обозначение документа', 'unknown')}\n"
475
+ content += f"Рисунок: {row.get('№ Изображения', 'unknown')}\n"
476
+ content += f"Название: {row.get('Название изображения', '')}\n"
477
+ content += f"Описание: {row.get('Описание изображение', '')}\n"
478
+ content += f"Раздел: {row.get('Раздел документа', '')}\n"
479
+
480
+ chunk_size = len(content)
481
+
482
+ documents.append(Document(
483
+ text=content,
484
+ metadata={
485
+ 'type': 'image',
486
+ 'document_id': str(row.get('Обозначение документа', 'unknown')),
487
+ 'image_number': str(row.get('№ Изображения', 'unknown')),
488
+ 'section': str(row.get('Раздел документа', '')),
489
+ 'chunk_size': chunk_size
490
+ }
491
+ ))
492
+ except Exception as e:
493
+ log_message(f"Error loading {file_path}: {e}")
494
+
495
+ if documents:
496
+ avg_size = sum(d.metadata['chunk_size'] for d in documents) / len(documents)
497
+ log_message(f"✓ Loaded {len(documents)} images (avg size: {avg_size:.0f} chars)")
498
+
499
+ return documents
500
+
501
+
502
+ def load_all_documents(repo_id, hf_token, json_dir, table_dir, image_dir):
503
+ """Main loader - combines all document types"""
504
+ log_message("="*60)
505
+ log_message("STARTING DOCUMENT LOADING")
506
+ log_message("="*60)
507
+
508
+ # Load text sections
509
+ text_docs = load_json_documents(repo_id, hf_token, json_dir)
510
+ text_chunks = chunk_text_documents(text_docs)
511
+
512
+ # Load tables (already chunked)
513
+ table_chunks = load_table_documents(repo_id, hf_token, table_dir)
514
+
515
+ # Load images (no chunking needed)
516
+ image_docs = load_image_documents(repo_id, hf_token, image_dir)
517
+
518
+ all_docs = text_chunks + table_chunks + image_docs
519
+
520
+ log_message("="*60)
521
+ log_message(f"TOTAL DOCUMENTS: {len(all_docs)}")
522
+ log_message(f" Text chunks: {len(text_chunks)}")
523
+ log_message(f" Table chunks: {len(table_chunks)}")
524
+ log_message(f" Images: {len(image_docs)}")
525
+ log_message("="*60)
526
+
527
+ return all_docs
index_retriever.py CHANGED
@@ -12,28 +12,58 @@ def create_vector_index(documents):
12
  log_message("Строю векторный индекс")
13
  return VectorStoreIndex.from_documents(documents)
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  def create_query_engine(vector_index):
16
  try:
 
 
17
  bm25_retriever = BM25Retriever.from_defaults(
18
  docstore=vector_index.docstore,
19
- similarity_top_k=20
20
  )
21
 
22
  vector_retriever = VectorIndexRetriever(
23
  index=vector_index,
24
- similarity_top_k=30,
25
- similarity_cutoff=0.65
26
  )
27
 
28
  hybrid_retriever = QueryFusionRetriever(
29
  [vector_retriever, bm25_retriever],
30
- similarity_top_k=40,
31
  num_queries=1
32
  )
33
 
34
- custom_prompt_template = PromptTemplate(PROMPT_SIMPLE_POISK)
35
  response_synthesizer = get_response_synthesizer(
36
- response_mode=ResponseMode.TREE_SUMMARIZE,
37
  text_qa_template=custom_prompt_template
38
  )
39
 
@@ -47,69 +77,4 @@ def create_query_engine(vector_index):
47
 
48
  except Exception as e:
49
  log_message(f"Ошибка создания query engine: {str(e)}")
50
- raise
51
-
52
- def rerank_nodes(query, nodes, reranker, top_k=20, min_score_threshold=0.5, diversity_penalty=0.3):
53
- if not nodes or not reranker:
54
- return nodes[:top_k]
55
-
56
- try:
57
- log_message(f"Переранжирую {len(nodes)} узлов")
58
-
59
- pairs = [[query, node.text] for node in nodes]
60
- scores = reranker.predict(pairs)
61
- scored_nodes = list(zip(nodes, scores))
62
-
63
- scored_nodes.sort(key=lambda x: x[1], reverse=True)
64
-
65
- if min_score_threshold is not None:
66
- scored_nodes = [(node, score) for node, score in scored_nodes
67
- if score >= min_score_threshold]
68
- log_message(f"После фильтрации по порогу {min_score_threshold}: {len(scored_nodes)} узлов")
69
-
70
- if not scored_nodes:
71
- log_message("Нет узлов после фильтрации, снижаю порог")
72
- scored_nodes = list(zip(nodes, scores))
73
- scored_nodes.sort(key=lambda x: x[1], reverse=True)
74
- min_score_threshold = scored_nodes[0][1] * 0.6
75
- scored_nodes = [(node, score) for node, score in scored_nodes
76
- if score >= min_score_threshold]
77
-
78
- selected_nodes = []
79
- selected_docs = set()
80
- selected_sections = set()
81
-
82
- for node, score in scored_nodes:
83
- if len(selected_nodes) >= top_k:
84
- break
85
-
86
- metadata = node.metadata if hasattr(node, 'metadata') else {}
87
- doc_id = metadata.get('document_id', 'unknown')
88
- section_key = f"{doc_id}_{metadata.get('section_path', metadata.get('section_id', ''))}"
89
-
90
- # Apply diversity penalty
91
- penalty = 0
92
- if doc_id in selected_docs:
93
- penalty += diversity_penalty * 0.5
94
- if section_key in selected_sections:
95
- penalty += diversity_penalty
96
-
97
- adjusted_score = score * (1 - penalty)
98
-
99
- # Add if still competitive
100
- if not selected_nodes or adjusted_score >= selected_nodes[0][1] * 0.6:
101
- selected_nodes.append((node, score))
102
- selected_docs.add(doc_id)
103
- selected_sections.add(section_key)
104
-
105
- log_message(f"Выбрано {len(selected_nodes)} узлов с разнообразием")
106
- log_message(f"Уникальных документов: {len(selected_docs)}, секций: {len(selected_sections)}")
107
-
108
- if selected_nodes:
109
- log_message(f"Score range: {selected_nodes[0][1]:.3f} to {selected_nodes[-1][1]:.3f}")
110
-
111
- return [node for node, score in selected_nodes]
112
-
113
- except Exception as e:
114
- log_message(f"Ошибка переранжировки: {str(e)}")
115
- return nodes[:top_k]
 
12
  log_message("Строю векторный индекс")
13
  return VectorStoreIndex.from_documents(documents)
14
 
15
+ def rerank_nodes(query, nodes, reranker, top_k=25, min_score_threshold=0.5):
16
+ if not nodes or not reranker:
17
+ return nodes[:top_k]
18
+
19
+ try:
20
+ log_message(f"Переранжирую {len(nodes)} узлов")
21
+
22
+ pairs = [[query, node.text] for node in nodes]
23
+ scores = reranker.predict(pairs)
24
+ scored_nodes = list(zip(nodes, scores))
25
+
26
+ scored_nodes.sort(key=lambda x: x[1], reverse=True)
27
+
28
+ # Apply threshold
29
+ filtered = [(node, score) for node, score in scored_nodes if score >= min_score_threshold]
30
+
31
+ if not filtered:
32
+ # Lower threshold if nothing passes
33
+ filtered = scored_nodes[:top_k]
34
+
35
+ log_message(f"Выбрано {min(len(filtered), top_k)} узлов")
36
+
37
+ return [node for node, score in filtered[:top_k]]
38
+
39
+ except Exception as e:
40
+ log_message(f"Ошибка переранжировки: {str(e)}")
41
+ return nodes[:top_k]
42
+
43
  def create_query_engine(vector_index):
44
  try:
45
+ from config import CUSTOM_PROMPT
46
+
47
  bm25_retriever = BM25Retriever.from_defaults(
48
  docstore=vector_index.docstore,
49
+ similarity_top_k=70
50
  )
51
 
52
  vector_retriever = VectorIndexRetriever(
53
  index=vector_index,
54
+ similarity_top_k=70,
55
+ similarity_cutoff=0.55
56
  )
57
 
58
  hybrid_retriever = QueryFusionRetriever(
59
  [vector_retriever, bm25_retriever],
60
+ similarity_top_k=70,
61
  num_queries=1
62
  )
63
 
64
+ custom_prompt_template = PromptTemplate(CUSTOM_PROMPT)
65
  response_synthesizer = get_response_synthesizer(
66
+ response_mode=ResponseMode.TREE_SUMMARIZE,
67
  text_qa_template=custom_prompt_template
68
  )
69
 
 
77
 
78
  except Exception as e:
79
  log_message(f"Ошибка создания query engine: {str(e)}")
80
+ raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
table_prep.py CHANGED
@@ -4,6 +4,9 @@ from huggingface_hub import hf_hub_download, list_repo_files
4
  from llama_index.core import Document
5
  from my_logging import log_message
6
 
 
 
 
7
  def create_table_content(table_data):
8
  """Create formatted content from table data"""
9
  doc_id = table_data.get('document_id', table_data.get('document', 'Неизвестно'))
@@ -29,38 +32,120 @@ def create_table_content(table_data):
29
 
30
  return content
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  def table_to_document(table_data, document_id=None):
33
- """Convert table data to a single Document"""
 
34
  if not isinstance(table_data, dict):
35
  return []
36
 
37
- doc_id = document_id or table_data.get('document_id', table_data.get('document', 'Неизвестно'))
38
  table_num = table_data.get('table_number', 'Неизвестно')
39
  table_title = table_data.get('table_title', 'Неизвестно')
40
  section = table_data.get('section', 'Неизвестно')
 
41
 
42
- content = create_table_content(table_data)
43
- content_size = len(content)
44
 
45
- # Log table addition
46
- row_count = len(table_data.get('data', [])) if 'data' in table_data else 0
47
- log_message(f" ДОБАВЛЕНА: Таблица {table_num} из документа '{doc_id}' | "
48
- f"Размер: {content_size} символов | Строк: {row_count}")
 
49
 
50
- return [Document(
 
 
 
 
 
 
 
 
 
 
 
51
  text=content,
52
  metadata={
53
  "type": "table",
54
  "table_number": table_num,
55
- "table_title": table_title,
56
  "document_id": doc_id,
57
- "section": section,
58
- "section_id": section,
59
- "total_rows": row_count,
60
- "content_size": content_size
61
  }
62
- )]
63
-
 
 
 
 
 
 
64
  def load_table_data(repo_id, hf_token, table_data_dir):
65
  log_message("=" * 60)
66
  log_message("НАЧАЛО ЗАГРУЗКИ ТАБЛИЧНЫХ ДАННЫХ")
@@ -114,6 +199,7 @@ def load_table_data(repo_id, hf_token, table_data_dir):
114
  stats['total_size'] += size
115
  stats['by_document'][document_id]['count'] += 1
116
  stats['by_document'][document_id]['size'] += size
 
117
  else:
118
  docs_list = table_to_document(table_data, document_id)
119
  table_documents.extend(docs_list)
 
4
  from llama_index.core import Document
5
  from my_logging import log_message
6
 
7
+ MAX_ROWS_PER_CHUNK = 10
8
+ MAX_CHUNK_SIZE = 4000
9
+
10
  def create_table_content(table_data):
11
  """Create formatted content from table data"""
12
  doc_id = table_data.get('document_id', table_data.get('document', 'Неизвестно'))
 
32
 
33
  return content
34
 
35
+ def chunk_table_document(doc, max_chunk_size=MAX_CHUNK_SIZE, max_rows_per_chunk=MAX_ROWS_PER_CHUNK):
36
+ lines = doc.text.strip().split('\n')
37
+
38
+ # Separate header and data rows
39
+ header_lines = []
40
+ data_rows = []
41
+ in_data = False
42
+
43
+ for line in lines:
44
+ if line.startswith('Данные таблицы:'):
45
+ in_data = True
46
+ header_lines.append(line)
47
+ elif in_data and line.startswith('Строка'):
48
+ data_rows.append(line)
49
+ elif not in_data:
50
+ header_lines.append(line)
51
+
52
+ header = '\n'.join(header_lines) + '\n'
53
+
54
+ if not data_rows:
55
+ return [doc]
56
+
57
+ chunks = []
58
+ current_rows = []
59
+ current_size = len(header)
60
+
61
+ for row in data_rows:
62
+ row_size = len(row) + 1
63
+ # Check both limits: chunk size and row count
64
+ if ((current_size + row_size > max_chunk_size or len(current_rows) >= max_rows_per_chunk) and current_rows):
65
+ chunk_text = header + '\n'.join(current_rows)
66
+ chunks.append(chunk_text)
67
+ log_message(f"Создана часть таблицы размером {len(chunk_text)} символов с {len(current_rows)} строками")
68
+ current_rows = []
69
+ current_size = len(header)
70
+
71
+ current_rows.append(row)
72
+ current_size += row_size
73
+ log_message(f"Добавлена строка к текущему чанку, текущий размер {current_size} символов")
74
+
75
+ # Add final chunk
76
+ if current_rows:
77
+ chunk_text = header + '\n'.join(current_rows)
78
+ chunks.append(chunk_text)
79
+ log_message(f"Создана финальная часть таблицы размером {len(chunk_text)} символов с {len(current_rows)} строками")
80
+
81
+ # Create Document objects
82
+ chunked_docs = []
83
+ for i, chunk_text in enumerate(chunks):
84
+ chunk_doc = Document(
85
+ text=chunk_text,
86
+ metadata={
87
+ "type": "table",
88
+ "table_number": doc.metadata.get('table_number'),
89
+ "document_id": doc.metadata.get('document_id'),
90
+ "section": doc.metadata.get('section'),
91
+ "chunk_id": i,
92
+ "total_chunks": len(chunks),
93
+ "is_chunked": True
94
+ }
95
+ )
96
+ chunked_docs.append(chunk_doc)
97
+
98
+ return chunked_docs
99
+
100
+
101
  def table_to_document(table_data, document_id=None):
102
+ """Convert table data to Document, chunk if needed"""
103
+
104
  if not isinstance(table_data, dict):
105
  return []
106
 
107
+ doc_id = document_id or table_data.get('document_id') or table_data.get('document', 'Неизвестно')
108
  table_num = table_data.get('table_number', 'Неизвестно')
109
  table_title = table_data.get('table_title', 'Неизвестно')
110
  section = table_data.get('section', 'Неизвестно')
111
+ table_rows = table_data.get('data', [])
112
 
113
+ if not table_rows:
114
+ return []
115
 
116
+ # Build table content
117
+ content = f"Таблица: {table_num}\n"
118
+ content += f"Название: {table_title}\n"
119
+ content += f"Документ: {doc_id}\n"
120
+ content += f"Раздел: {section}\n"
121
 
122
+ headers = table_data.get('headers', [])
123
+ if headers:
124
+ content += f"\nЗаголовки: {' | '.join(headers)}\n"
125
+
126
+ content += "\nДанные таблицы:\n"
127
+ for row_idx, row in enumerate(table_rows, start=1):
128
+ if isinstance(row, dict):
129
+ row_text = " | ".join([f"{k}: {v}" for k, v in row.items() if v])
130
+ content += f"Строка {row_idx}: {row_text}\n"
131
+
132
+ # Create base document
133
+ base_doc = Document(
134
  text=content,
135
  metadata={
136
  "type": "table",
137
  "table_number": table_num,
 
138
  "document_id": doc_id,
139
+ "section": section
 
 
 
140
  }
141
+ )
142
+ if len(content) > 4000:
143
+ chunks = chunk_table_document(base_doc)
144
+ log_message(f"Таблица {table_num} разбита на {len(chunks)} частей")
145
+ return chunk_table_document(base_doc)
146
+ return [base_doc]
147
+
148
+
149
  def load_table_data(repo_id, hf_token, table_data_dir):
150
  log_message("=" * 60)
151
  log_message("НАЧАЛО ЗАГРУЗКИ ТАБЛИЧНЫХ ДАННЫХ")
 
199
  stats['total_size'] += size
200
  stats['by_document'][document_id]['count'] += 1
201
  stats['by_document'][document_id]['size'] += size
202
+ log_message(f"Добавлена таблица {sheet.get('table_number', 'Неизвестно')} из документа {document_id}, размер {size} символов")
203
  else:
204
  docs_list = table_to_document(table_data, document_id)
205
  table_documents.extend(docs_list)
utils.py CHANGED
@@ -225,32 +225,6 @@ def generate_sources_html(nodes, chunks_df=None):
225
 
226
  html += "</div>"
227
  return html
228
-
229
- def expand_query(question, llm_model):
230
- """
231
- Generate multiple query variations for better retrieval
232
- """
233
- expansion_prompt = f"""Дан вопрос: "{question}"
234
-
235
- Сгенерируй 2 альтернативные формулировки этого вопроса для поиска в базе данных.
236
- Используй синонимы и разные формулировки, сохраняя смысл.
237
-
238
- Формат ответа (только вопросы, по одному на строку):
239
- 1. [первая формулировка]
240
- 2. [вторая формулировка]"""
241
-
242
- try:
243
- response = llm_model.complete(expansion_prompt)
244
- expanded = [q.strip() for q in response.text.split('\n') if q.strip() and not q.strip().startswith('1.') and not q.strip().startswith('2.')]
245
- # Clean up
246
- expanded = [q.lstrip('12. ').strip() for q in expanded if len(q) > 10][:2]
247
- log_message(f"Query expansion: {len(expanded)} вариантов")
248
- return [question] + expanded
249
- except Exception as e:
250
- log_message(f"Ошибка расширения запроса: {str(e)}")
251
- return [question]
252
-
253
-
254
  def answer_question(question, query_engine, reranker, current_model, chunks_df=None):
255
  if query_engine is None:
256
  return "<div style='background-color: #e53e3e; color: white; padding: 20px; border-radius: 10px;'>Система не инициализирована</div>", "", ""
@@ -260,26 +234,16 @@ def answer_question(question, query_engine, reranker, current_model, chunks_df=N
260
 
261
  llm = get_llm_model(current_model)
262
 
263
- query_variations = expand_query(question, llm)
264
-
265
- all_nodes = []
266
- seen_node_ids = set()
267
-
268
- for query_var in query_variations:
269
- retrieved = query_engine.retriever.retrieve(query_var)
270
- for node in retrieved:
271
- node_id = f"{node.node_id if hasattr(node, 'node_id') else hash(node.text)}"
272
- if node_id not in seen_node_ids:
273
- all_nodes.append(node)
274
- seen_node_ids.add(node_id)
275
 
276
- log_message(f"Получено {len(all_nodes)} уникальных узлов из {len(query_variations)} запросов")
277
 
278
  reranked_nodes = rerank_nodes(
279
  question,
280
- all_nodes,
281
  reranker,
282
- top_k=20,
283
  min_score_threshold=0.5,
284
  diversity_penalty=0.3
285
  )
 
225
 
226
  html += "</div>"
227
  return html
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  def answer_question(question, query_engine, reranker, current_model, chunks_df=None):
229
  if query_engine is None:
230
  return "<div style='background-color: #e53e3e; color: white; padding: 20px; border-radius: 10px;'>Система не инициализирована</div>", "", ""
 
234
 
235
  llm = get_llm_model(current_model)
236
 
237
+ # Direct retrieval without query expansion
238
+ retrieved_nodes = query_engine.retriever.retrieve(question)
 
 
 
 
 
 
 
 
 
 
239
 
240
+ log_message(f"Получено {len(retrieved_nodes)} узлов")
241
 
242
  reranked_nodes = rerank_nodes(
243
  question,
244
+ retrieved_nodes,
245
  reranker,
246
+ top_k=40,
247
  min_score_threshold=0.5,
248
  diversity_penalty=0.3
249
  )