MrSimple01 commited on
Commit
7245a97
·
verified ·
1 Parent(s): 81f182e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +619 -451
app.py CHANGED
@@ -1,452 +1,620 @@
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
8
- import sys
9
- from config import (
10
- HF_REPO_ID, HF_TOKEN, DOWNLOAD_DIR, CHUNKS_FILENAME,
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
-
57
- html += f"""
58
- <div style='background-color: {bg_color}; padding: 10px; margin: 5px 0; border-radius: 5px; border-left: 4px solid #007bff; color: black;'>
59
- <strong style='color: black;'>Документ:</strong> <span style='color: black;'>{chunk['document_id']}</span><br>
60
- <strong style='color: black;'>Раздел:</strong> <span style='color: black;'>{section_display}</span><br>
61
- <strong style='color: black;'>Содержание:</strong><br>
62
- <div style='background-color: white; padding: 8px; margin-top: 5px; border-radius: 3px; font-family: monospace; font-size: 12px; color: black; max-height: 200px; overflow-y: auto;'>
63
- {formatted_content}
64
- </div>
65
- </div>
66
- """
67
-
68
- html += "</div>"
69
- return html
70
-
71
- def get_section_display(chunk):
72
- section_path = chunk.get('section_path', '')
73
- section_id = chunk.get('section_id', 'unknown')
74
- doc_type = chunk.get('type', 'text')
75
-
76
- if doc_type == 'table' and chunk.get('table_number'):
77
- table_num = chunk.get('table_number')
78
- if not str(table_num).startswith('№'):
79
- table_num = f"№{table_num}"
80
- return f"таблица {table_num}"
81
-
82
- if doc_type == 'image' and chunk.get('image_number'):
83
- image_num = chunk.get('image_number')
84
- if not str(image_num).startswith('№'):
85
- image_num = f"№{image_num}"
86
- return f"рисунок {image_num}"
87
-
88
- if section_path:
89
- return section_path
90
- elif section_id and section_id != 'unknown':
91
- return section_id
92
-
93
- return section_id
94
-
95
- def get_formatted_content(chunk):
96
- document_id = chunk.get('document_id', 'unknown')
97
- section_path = chunk.get('section_path', '')
98
- section_id = chunk.get('section_id', 'unknown')
99
- section_text = chunk.get('section_text', '')
100
- parent_section = chunk.get('parent_section', '')
101
- parent_title = chunk.get('parent_title', '')
102
- level = chunk.get('level', '')
103
- chunk_text = chunk.get('chunk_text', '')
104
- doc_type = chunk.get('type', 'text')
105
-
106
- # For text documents
107
- if level in ['subsection', 'sub_subsection', 'sub_sub_subsection'] and parent_section:
108
- current_section = section_path if section_path else section_id
109
- parent_info = f"{parent_section} ({parent_title})" if parent_title else parent_section
110
- return f"В разделе {parent_info} в документе {document_id}, пункт {current_section}: {chunk_text}"
111
- else:
112
- current_section = section_path if section_path else section_id
113
- clean_text = chunk_text
114
- if section_text and chunk_text.startswith(section_text):
115
- section_title = section_text
116
- elif chunk_text.startswith(f"{current_section} "):
117
- clean_text = chunk_text[len(f"{current_section} "):].strip()
118
- section_title = section_text if section_text else f"{current_section} {clean_text.split('.')[0] if '.' in clean_text else clean_text[:50]}"
119
- else:
120
- section_title = section_text if section_text else current_section
121
-
122
- return f"В разделе {current_section} в документе {document_id}, пункт {section_title}: {clean_text}"
123
-
124
- def initialize_system(repo_id, hf_token, download_dir, chunks_filename=None,
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
131
- from llama_index.core.text_splitter import TokenTextSplitter
132
-
133
- embed_model = get_embedding_model()
134
- llm = get_llm_model(DEFAULT_MODEL)
135
- reranker = get_reranker_model()
136
-
137
- Settings.embed_model = embed_model
138
- Settings.llm = llm
139
- Settings.text_splitter = TokenTextSplitter(
140
- chunk_size=CHUNK_SIZE,
141
- chunk_overlap=CHUNK_OVERLAP,
142
- separator=" ",
143
- backup_separators=["\n", ".", "!", "?"]
144
- )
145
-
146
- log_message(f"Configured chunk size: {CHUNK_SIZE} tokens")
147
- log_message(f"Configured chunk overlap: {CHUNK_OVERLAP} tokens")
148
-
149
- all_documents = []
150
- chunks_df = None
151
-
152
- # CHANGED: Use load_all_documents instead of loading separately
153
- if use_json_instead_csv and json_files_dir:
154
- log_message("Используем JSON файлы вместо CSV")
155
- from documents_prep import load_all_documents
156
-
157
- # This will handle text, tables, and images all together with proper logging
158
- all_documents = load_all_documents(
159
- repo_id=repo_id,
160
- hf_token=hf_token,
161
- json_dir=json_files_dir,
162
- table_dir=table_data_dir if table_data_dir else "",
163
- image_dir=image_data_dir if image_data_dir else ""
164
- )
165
- else:
166
- # OLD PATH: Loading separately (fallback)
167
- if chunks_filename:
168
- log_message("Загружаем данные из CSV")
169
-
170
- if table_data_dir:
171
- log_message("Добавляю табличные данные")
172
- from documents_prep import load_table_documents
173
-
174
- table_chunks = load_table_documents(repo_id, hf_token, table_data_dir)
175
- log_message(f"Загружено {len(table_chunks)} табличных чанков")
176
- all_documents.extend(table_chunks)
177
-
178
- if image_data_dir:
179
- log_message("Добавляю данные изображений")
180
- from documents_prep import load_image_documents
181
-
182
- image_documents = load_image_documents(repo_id, hf_token, image_data_dir)
183
- log_message(f"Загружено {len(image_documents)} документов изображений")
184
- all_documents.extend(image_documents)
185
-
186
- log_message(f"Всего документов после всей обработки: {len(all_documents)}")
187
-
188
- vector_index = create_vector_index(all_documents)
189
- query_engine = create_query_engine(vector_index)
190
-
191
- # Create chunk_info for display (extract from documents metadata)
192
- chunk_info = []
193
- for doc in all_documents:
194
- chunk_info.append({
195
- 'document_id': doc.metadata.get('document_id', 'unknown'),
196
- 'section_id': doc.metadata.get('section_id', 'unknown'),
197
- 'type': doc.metadata.get('type', 'text'),
198
- 'chunk_text': doc.text[:200] + '...' if len(doc.text) > 200 else doc.text,
199
- 'table_number': doc.metadata.get('table_number', ''),
200
- 'image_number': doc.metadata.get('image_number', ''),
201
- 'section': doc.metadata.get('section', ''),
202
- 'connection_type': doc.metadata.get('connection_type', '') # ADD THIS
203
- })
204
-
205
- log_message(f"Система успешно инициализирована")
206
- return query_engine, chunks_df, reranker, vector_index, chunk_info
207
-
208
- except Exception as e:
209
- log_message(f"Ошибка инициализации: {str(e)}")
210
- import traceback
211
- log_message(traceback.format_exc())
212
- return None, None, None, None, []
213
-
214
- def switch_model(model_name, vector_index):
215
- from llama_index.core import Settings
216
- from index_retriever import create_query_engine
217
-
218
- try:
219
- log_message(f"Переключение на модель: {model_name}")
220
-
221
- new_llm = get_llm_model(model_name)
222
- Settings.llm = new_llm
223
-
224
- if vector_index is not None:
225
- new_query_engine = create_query_engine(vector_index)
226
- log_message(f"Модель успешно переключена на: {model_name}")
227
- return new_query_engine, f"✅ Модель переключена на: {model_name}"
228
- else:
229
- return None, "❌ Ошибка: система не инициализирована"
230
-
231
- except Exception as e:
232
- error_msg = f"Ошибка переключения модели: {str(e)}"
233
- log_message(error_msg)
234
- return None, f"❌ {error_msg}"
235
-
236
- def main_answer_question(question):
237
- global query_engine, reranker, current_model, chunks_df
238
- if not question.strip():
239
- return ("<div style='color: black;'>Пожалуйста, введите вопрос</div>",
240
- "<div style='color: black;'>Источники появятся после обработки запроса</div>",
241
- "<div style='color: black;'>Чанки появятся после обработки запроса</div>")
242
-
243
- try:
244
- # Call the answer_question function which returns 3 values
245
- answer_html, sources_html, chunks_html = answer_question(question, query_engine, reranker, current_model, chunks_df)
246
- return answer_html, sources_html, chunks_html
247
-
248
- except Exception as e:
249
- log_message(f"Ошибка при ответе на вопрос: {str(e)}")
250
- return (f"<div style='color: red;'>Ошибка: {str(e)}</div>",
251
- "<div style='color: black;'>Источники недоступны из-за ошибки</div>",
252
- "<div style='color: black;'>Чанки недоступны из-за ошибки</div>")
253
-
254
- def retrieve_chunks(question: str, top_k: int = 20) -> list:
255
- from index_retriever import rerank_nodes
256
- global query_engine, reranker
257
-
258
- if query_engine is None:
259
- return []
260
-
261
- try:
262
- retrieved_nodes = query_engine.retriever.retrieve(question)
263
- log_message(f"Получено {len(retrieved_nodes)} узлов")
264
-
265
- reranked_nodes = rerank_nodes(
266
- question,
267
- retrieved_nodes,
268
- reranker,
269
- top_k=top_k,
270
- min_score_threshold=0.5
271
- )
272
-
273
- chunks_data = []
274
- for i, node in enumerate(reranked_nodes):
275
- metadata = node.metadata if hasattr(node, 'metadata') else {}
276
- chunk = {
277
- 'rank': i + 1,
278
- 'document_id': metadata.get('document_id', 'unknown'),
279
- 'section_id': metadata.get('section_id', ''),
280
- 'section_path': metadata.get('section_path', ''),
281
- 'section_text': metadata.get('section_text', ''),
282
- 'type': metadata.get('type', 'text'),
283
- 'table_number': metadata.get('table_number', ''),
284
- 'image_number': metadata.get('image_number', ''),
285
- 'text': node.text
286
- }
287
- chunks_data.append(chunk)
288
-
289
- log_message(f"Возвращено {len(chunks_data)} чанков")
290
- return chunks_data
291
-
292
- except Exception as e:
293
- log_message(f"Ошибка получения чанков: {str(e)}")
294
- return []
295
-
296
-
297
- def create_demo_interface(answer_question_func, switch_model_func, current_model, chunk_info=None):
298
- with gr.Blocks(title="AIEXP - AI Expert для нормативной документации", theme=gr.themes.Soft()) as demo:
299
- gr.api(retrieve_chunks, api_name="retrieve_chunks")
300
-
301
- gr.Markdown("""
302
- # AIEXP - Artificial Intelligence Expert
303
-
304
- ## Инструмент для работы с нормативной документацией
305
- """)
306
-
307
- with gr.Tab("Поиск по нормативным документам"):
308
- gr.Markdown("### Задайте вопрос по нормативной документации")
309
-
310
- with gr.Row():
311
- with gr.Column(scale=2):
312
- model_dropdown = gr.Dropdown(
313
- choices=list(AVAILABLE_MODELS.keys()),
314
- value=current_model,
315
- label="Выберите языковую модель",
316
- info="Выберите модель для генерации ответов"
317
- )
318
- with gr.Column(scale=1):
319
- switch_btn = gr.Button("Переключить модель", variant="secondary")
320
- model_status = gr.Textbox(
321
- value=f"Текущая модель: {current_model}",
322
- label="Статус модели",
323
- interactive=False
324
- )
325
-
326
- with gr.Row():
327
- with gr.Column(scale=3):
328
- question_input = gr.Textbox(
329
- label="Ваш вопрос к базе знаний",
330
- placeholder="Введите вопрос по нормативным документам...",
331
- lines=3
332
- )
333
- ask_btn = gr.Button("Найти ответ", variant="primary", size="lg")
334
-
335
- gr.Examples(
336
- examples=[
337
- "О чем этот рисунок: ГОСТ Р 50.04.07-2022 Приложение Л. Л.1.5 Рисунок Л.2",
338
- "Л.9 Формула в ГОСТ Р 50.04.07 - 2022 что и о чем там?",
339
- "Какой стандарт устанавливает порядок признания протоколов испытаний продукции в области использования атомной энергии?",
340
- "Кто несет ответственность за организацию и проведение признания протоколов испытаний продукции?",
341
- каких случаях могут быть признаны протоколы испытаний, проведенные лабораториями?",
342
- какой таблице можно найти информацию о методы исследований при аттестационных испытаниях технологии термической обработки заготовок из легированных сталей? Какой документ и какой раздел?"
343
- ],
344
- inputs=question_input
345
- )
346
-
347
- with gr.Row():
348
- with gr.Column(scale=2):
349
- answer_output = gr.HTML(
350
- label="",
351
- value=f"<div style='background-color: #2d3748; color: white; padding: 20px; border-radius: 10px; text-align: center;'>Здесь появится ответ на ваш вопрос...<br><small>Текущая модель: {current_model}</small></div>",
352
- )
353
-
354
- with gr.Column(scale=1):
355
- sources_output = gr.HTML(
356
- label="",
357
- value="<div style='background-color: #2d3748; color: white; padding: 20px; border-radius: 10px; text-align: center;'>Здесь появятся релевантные чанки...</div>",
358
- )
359
-
360
- with gr.Column(scale=1):
361
- chunks_output = gr.HTML(
362
- label="Релевантные чанки",
363
- value="<div style='background-color: #2d3748; color: white; padding: 20px; border-radius: 10px; text-align: center;'>Здесь появятся релевантные чанки...</div>",
364
- )
365
-
366
- switch_btn.click(
367
- fn=switch_model_func,
368
- inputs=[model_dropdown],
369
- outputs=[model_status]
370
- )
371
-
372
- ask_btn.click(
373
- fn=answer_question_func,
374
- inputs=[question_input],
375
- outputs=[answer_output, sources_output, chunks_output]
376
- )
377
-
378
- question_input.submit(
379
- fn=answer_question_func,
380
- inputs=[question_input],
381
- outputs=[answer_output, sources_output, chunks_output]
382
- )
383
- return demo
384
-
385
-
386
- query_engine = None
387
- chunks_df = None
388
- reranker = None
389
- vector_index = None
390
- current_model = DEFAULT_MODEL
391
-
392
- def main_answer_question(question):
393
- global query_engine, reranker, current_model, chunks_df
394
- answer_html, sources_html, chunks_html = answer_question(
395
- question, query_engine, reranker, current_model, chunks_df
396
- )
397
- return answer_html, sources_html, chunks_html
398
-
399
- def main_switch_model(model_name):
400
- global query_engine, vector_index, current_model
401
-
402
- new_query_engine, status_message = switch_model(model_name, vector_index)
403
- if new_query_engine:
404
- query_engine = new_query_engine
405
- current_model = model_name
406
-
407
- return status_message
408
-
409
-
410
-
411
-
412
- def main():
413
- global query_engine, chunks_df, reranker, vector_index, current_model
414
- GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "")
415
- if GOOGLE_API_KEY:
416
- log_message("Использование Google API для модели генерации текст��")
417
- else:
418
- log_message("Google API ключ не найден, использование локальной модели")
419
- log_message("Запуск AIEXP - AI Expert для нормативной документации")
420
- query_engine, chunks_df, reranker, vector_index, chunk_info = initialize_system(
421
- repo_id=HF_REPO_ID,
422
- hf_token=HF_TOKEN,
423
- download_dir=DOWNLOAD_DIR,
424
- json_files_dir=JSON_FILES_DIR,
425
- table_data_dir=TABLE_DATA_DIR,
426
- image_data_dir=IMAGE_DATA_DIR,
427
- use_json_instead_csv=True,
428
- )
429
-
430
- if query_engine:
431
- log_message("Запуск веб-интерфейса")
432
- demo = create_demo_interface(
433
- answer_question_func=main_answer_question,
434
- switch_model_func=main_switch_model,
435
- current_model=current_model,
436
- chunk_info=chunk_info
437
- )
438
- demo.api = "retrieve_chunks"
439
- demo.queue()
440
-
441
- demo.launch(
442
- server_name="0.0.0.0",
443
- server_port=7860,
444
- share=True,
445
- debug=False
446
- )
447
- else:
448
- log_message("Невозможно запустить приложение из-за ошибки инициализации")
449
- sys.exit(1)
450
-
451
- if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
  main()
 
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
8
+ import sys
9
+ from config import (
10
+ HF_REPO_ID, HF_TOKEN, DOWNLOAD_DIR, CHUNKS_FILENAME,
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
+
57
+ html += f"""
58
+ <div style='background-color: {bg_color}; padding: 10px; margin: 5px 0; border-radius: 5px; border-left: 4px solid #007bff; color: black;'>
59
+ <strong style='color: black;'>Документ:</strong> <span style='color: black;'>{chunk['document_id']}</span><br>
60
+ <strong style='color: black;'>Раздел:</strong> <span style='color: black;'>{section_display}</span><br>
61
+ <strong style='color: black;'>Содержание:</strong><br>
62
+ <div style='background-color: white; padding: 8px; margin-top: 5px; border-radius: 3px; font-family: monospace; font-size: 12px; color: black; max-height: 200px; overflow-y: auto;'>
63
+ {formatted_content}
64
+ </div>
65
+ </div>
66
+ """
67
+
68
+ html += "</div>"
69
+ return html
70
+
71
+ def get_section_display(chunk):
72
+ section_path = chunk.get('section_path', '')
73
+ section_id = chunk.get('section_id', 'unknown')
74
+ doc_type = chunk.get('type', 'text')
75
+
76
+ if doc_type == 'table' and chunk.get('table_number'):
77
+ table_num = chunk.get('table_number')
78
+ if not str(table_num).startswith('№'):
79
+ table_num = f"№{table_num}"
80
+ return f"таблица {table_num}"
81
+
82
+ if doc_type == 'image' and chunk.get('image_number'):
83
+ image_num = chunk.get('image_number')
84
+ if not str(image_num).startswith('№'):
85
+ image_num = f"№{image_num}"
86
+ return f"рисунок {image_num}"
87
+
88
+ if section_path:
89
+ return section_path
90
+ elif section_id and section_id != 'unknown':
91
+ return section_id
92
+
93
+ return section_id
94
+
95
+ def get_formatted_content(chunk):
96
+ document_id = chunk.get('document_id', 'unknown')
97
+ section_path = chunk.get('section_path', '')
98
+ section_id = chunk.get('section_id', 'unknown')
99
+ section_text = chunk.get('section_text', '')
100
+ parent_section = chunk.get('parent_section', '')
101
+ parent_title = chunk.get('parent_title', '')
102
+ level = chunk.get('level', '')
103
+ chunk_text = chunk.get('chunk_text', '')
104
+ doc_type = chunk.get('type', 'text')
105
+
106
+ # For text documents
107
+ if level in ['subsection', 'sub_subsection', 'sub_sub_subsection'] and parent_section:
108
+ current_section = section_path if section_path else section_id
109
+ parent_info = f"{parent_section} ({parent_title})" if parent_title else parent_section
110
+ return f"В разделе {parent_info} в документе {document_id}, пункт {current_section}: {chunk_text}"
111
+ else:
112
+ current_section = section_path if section_path else section_id
113
+ clean_text = chunk_text
114
+ if section_text and chunk_text.startswith(section_text):
115
+ section_title = section_text
116
+ elif chunk_text.startswith(f"{current_section} "):
117
+ clean_text = chunk_text[len(f"{current_section} "):].strip()
118
+ section_title = section_text if section_text else f"{current_section} {clean_text.split('.')[0] if '.' in clean_text else clean_text[:50]}"
119
+ else:
120
+ section_title = section_text if section_text else current_section
121
+
122
+ return f"В разделе {current_section} в документе {document_id}, пункт {section_title}: {clean_text}"
123
+
124
+ def initialize_system(repo_id, hf_token, download_dir, chunks_filename=None,
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
131
+ from llama_index.core.text_splitter import TokenTextSplitter
132
+
133
+ embed_model = get_embedding_model()
134
+ llm = get_llm_model(DEFAULT_MODEL)
135
+ reranker = get_reranker_model()
136
+
137
+ Settings.embed_model = embed_model
138
+ Settings.llm = llm
139
+ Settings.text_splitter = TokenTextSplitter(
140
+ chunk_size=CHUNK_SIZE,
141
+ chunk_overlap=CHUNK_OVERLAP,
142
+ separator=" ",
143
+ backup_separators=["\n", ".", "!", "?"]
144
+ )
145
+
146
+ log_message(f"Configured chunk size: {CHUNK_SIZE} tokens")
147
+ log_message(f"Configured chunk overlap: {CHUNK_OVERLAP} tokens")
148
+
149
+ all_documents = []
150
+ chunks_df = None
151
+
152
+ # CHANGED: Use load_all_documents instead of loading separately
153
+ if use_json_instead_csv and json_files_dir:
154
+ log_message("Используем JSON файлы вместо CSV")
155
+ from documents_prep import load_all_documents
156
+
157
+ # This will handle text, tables, and images all together with proper logging
158
+ all_documents = load_all_documents(
159
+ repo_id=repo_id,
160
+ hf_token=hf_token,
161
+ json_dir=json_files_dir,
162
+ table_dir=table_data_dir if table_data_dir else "",
163
+ image_dir=image_data_dir if image_data_dir else ""
164
+ )
165
+ else:
166
+ # OLD PATH: Loading separately (fallback)
167
+ if chunks_filename:
168
+ log_message("Загружаем данные из CSV")
169
+
170
+ if table_data_dir:
171
+ log_message("Добавляю табличные данные")
172
+ from documents_prep import load_table_documents
173
+
174
+ table_chunks = load_table_documents(repo_id, hf_token, table_data_dir)
175
+ log_message(f"Загружено {len(table_chunks)} табличных чанков")
176
+ all_documents.extend(table_chunks)
177
+
178
+ if image_data_dir:
179
+ log_message("Добавляю данные изображений")
180
+ from documents_prep import load_image_documents
181
+
182
+ image_documents = load_image_documents(repo_id, hf_token, image_data_dir)
183
+ log_message(f"Загружено {len(image_documents)} документов изображений")
184
+ all_documents.extend(image_documents)
185
+
186
+ log_message(f"Всего документов после всей обработки: {len(all_documents)}")
187
+
188
+ vector_index = create_vector_index(all_documents)
189
+ query_engine = create_query_engine(vector_index)
190
+
191
+ # Create chunk_info for display (extract from documents metadata)
192
+ chunk_info = []
193
+ for doc in all_documents:
194
+ chunk_info.append({
195
+ 'document_id': doc.metadata.get('document_id', 'unknown'),
196
+ 'section_id': doc.metadata.get('section_id', 'unknown'),
197
+ 'type': doc.metadata.get('type', 'text'),
198
+ 'chunk_text': doc.text[:200] + '...' if len(doc.text) > 200 else doc.text,
199
+ 'table_number': doc.metadata.get('table_number', ''),
200
+ 'image_number': doc.metadata.get('image_number', ''),
201
+ 'section': doc.metadata.get('section', ''),
202
+ 'connection_type': doc.metadata.get('connection_type', '') # ADD THIS
203
+ })
204
+
205
+ log_message(f"Система успешно инициализирована")
206
+ return query_engine, chunks_df, reranker, vector_index, chunk_info
207
+
208
+ except Exception as e:
209
+ log_message(f"Ошибка инициализации: {str(e)}")
210
+ import traceback
211
+ log_message(traceback.format_exc())
212
+ return None, None, None, None, []
213
+
214
+ def switch_model(model_name, vector_index):
215
+ from llama_index.core import Settings
216
+ from index_retriever import create_query_engine
217
+
218
+ try:
219
+ log_message(f"Переключение на модель: {model_name}")
220
+
221
+ new_llm = get_llm_model(model_name)
222
+ Settings.llm = new_llm
223
+
224
+ if vector_index is not None:
225
+ new_query_engine = create_query_engine(vector_index)
226
+ log_message(f"Модель успешно переключена на: {model_name}")
227
+ return new_query_engine, f"✅ Модель переключена на: {model_name}"
228
+ else:
229
+ return None, "❌ Ошибка: система не инициализирована"
230
+
231
+ except Exception as e:
232
+ error_msg = f"Ошибка переключения модели: {str(e)}"
233
+ log_message(error_msg)
234
+ return None, f"❌ {error_msg}"
235
+
236
+ # Add these global variables near the top with other globals
237
+ retrieval_params = {
238
+ 'vector_top_k': 50,
239
+ 'bm25_top_k': 50,
240
+ 'similarity_cutoff': 0.55,
241
+ 'hybrid_top_k': 100,
242
+ 'rerank_top_k': 20
243
+ }
244
+
245
+ # MODIFIED: Update create_query_engine call signature
246
+ def create_query_engine(vector_index, vector_top_k=50, bm25_top_k=50,
247
+ similarity_cutoff=0.55, hybrid_top_k=100):
248
+ try:
249
+ from config import CUSTOM_PROMPT
250
+ from index_retriever import create_query_engine as create_index_query_engine
251
+
252
+ # Pass parameters to the index_retriever function
253
+ query_engine = create_index_query_engine(
254
+ vector_index=vector_index,
255
+ vector_top_k=vector_top_k,
256
+ bm25_top_k=bm25_top_k,
257
+ similarity_cutoff=similarity_cutoff,
258
+ hybrid_top_k=hybrid_top_k
259
+ )
260
+
261
+ log_message(f"Query engine created with params: vector_top_k={vector_top_k}, "
262
+ f"bm25_top_k={bm25_top_k}, cutoff={similarity_cutoff}, hybrid_top_k={hybrid_top_k}")
263
+ return query_engine
264
+
265
+ except Exception as e:
266
+ log_message(f"Ошибка создания query engine: {str(e)}")
267
+ raise
268
+
269
+ # MODIFIED: Update answer_question to use global retrieval_params
270
+ def main_answer_question(question):
271
+ global query_engine, reranker, current_model, chunks_df, retrieval_params
272
+ if not question.strip():
273
+ return ("<div style='color: black;'>Пожалуйста, введите вопрос</div>",
274
+ "<div style='color: black;'>Источники появятся после обработки запроса</div>",
275
+ "<div style='color: black;'>Чанки появятся после обработки запроса</div>")
276
+
277
+ try:
278
+ answer_html, sources_html, chunks_html = answer_question(
279
+ question, query_engine, reranker, current_model, chunks_df,
280
+ rerank_top_k=retrieval_params['rerank_top_k']
281
+ )
282
+ return answer_html, sources_html, chunks_html
283
+
284
+ except Exception as e:
285
+ log_message(f"Ошибка при ответе на вопрос: {str(e)}")
286
+ return (f"<div style='color: red;'>Ошибка: {str(e)}</div>",
287
+ "<div style='color: black;'>Источники недоступны из-за ошибки</div>",
288
+ "<div style='color: black;'>Чанки недоступны из-за ошибки</div>")
289
+
290
+ # NEW: Function to update retrieval parameters and recreate query engine
291
+ def update_retrieval_params(vector_top_k, bm25_top_k, similarity_cutoff, hybrid_top_k, rerank_top_k):
292
+ global query_engine, vector_index, retrieval_params
293
+
294
+ try:
295
+ retrieval_params['vector_top_k'] = vector_top_k
296
+ retrieval_params['bm25_top_k'] = bm25_top_k
297
+ retrieval_params['similarity_cutoff'] = similarity_cutoff
298
+ retrieval_params['hybrid_top_k'] = hybrid_top_k
299
+ retrieval_params['rerank_top_k'] = rerank_top_k
300
+
301
+ # Recreate query engine with new parameters
302
+ if vector_index is not None:
303
+ query_engine = create_query_engine(
304
+ vector_index=vector_index,
305
+ vector_top_k=vector_top_k,
306
+ bm25_top_k=bm25_top_k,
307
+ similarity_cutoff=similarity_cutoff,
308
+ hybrid_top_k=hybrid_top_k
309
+ )
310
+ log_message(f"Параметры поиска обновлены: vector_top_k={vector_top_k}, "
311
+ f"bm25_top_k={bm25_top_k}, cutoff={similarity_cutoff}, "
312
+ f"hybrid_top_k={hybrid_top_k}, rerank_top_k={rerank_top_k}")
313
+ return f"✅ Параметры обновлены"
314
+ else:
315
+ return " Система не инициализирована"
316
+ except Exception as e:
317
+ error_msg = f"Ошибка обновления параметров: {str(e)}"
318
+ log_message(error_msg)
319
+ return f" {error_msg}"
320
+
321
+ def retrieve_chunks(question: str, top_k: int = 20) -> list:
322
+ from index_retriever import rerank_nodes
323
+ global query_engine, reranker
324
+
325
+ if query_engine is None:
326
+ return []
327
+
328
+ try:
329
+ retrieved_nodes = query_engine.retriever.retrieve(question)
330
+ log_message(f"Получено {len(retrieved_nodes)} узлов")
331
+
332
+ reranked_nodes = rerank_nodes(
333
+ question,
334
+ retrieved_nodes,
335
+ reranker,
336
+ top_k=top_k,
337
+ min_score_threshold=0.5
338
+ )
339
+
340
+ chunks_data = []
341
+ for i, node in enumerate(reranked_nodes):
342
+ metadata = node.metadata if hasattr(node, 'metadata') else {}
343
+ chunk = {
344
+ 'rank': i + 1,
345
+ 'document_id': metadata.get('document_id', 'unknown'),
346
+ 'section_id': metadata.get('section_id', ''),
347
+ 'section_path': metadata.get('section_path', ''),
348
+ 'section_text': metadata.get('section_text', ''),
349
+ 'type': metadata.get('type', 'text'),
350
+ 'table_number': metadata.get('table_number', ''),
351
+ 'image_number': metadata.get('image_number', ''),
352
+ 'text': node.text
353
+ }
354
+ chunks_data.append(chunk)
355
+
356
+ log_message(f"Возвращено {len(chunks_data)} чанков")
357
+ return chunks_data
358
+
359
+ except Exception as e:
360
+ log_message(f"Ошибка получения чанков: {str(e)}")
361
+ return []
362
+
363
+
364
+ def create_demo_interface(answer_question_func, switch_model_func, current_model, chunk_info=None):
365
+ with gr.Blocks(title="AIEXP - AI Expert для нормативной документации", theme=gr.themes.Soft()) as demo:
366
+ gr.api(retrieve_chunks, api_name="retrieve_chunks")
367
+
368
+ gr.Markdown("""
369
+ # AIEXP - Artificial Intelligence Expert
370
+
371
+ ## Инструмент для работы с нормативной документацией
372
+ """)
373
+
374
+ with gr.Tab("Поиск по нормативным документам"):
375
+ gr.Markdown("### Задайте вопрос по нормативной документации")
376
+
377
+ with gr.Row():
378
+ with gr.Column(scale=2):
379
+ model_dropdown = gr.Dropdown(
380
+ choices=list(AVAILABLE_MODELS.keys()),
381
+ value=current_model,
382
+ label="Выберите языковую модель",
383
+ info="Выберите модель для генерации ответов"
384
+ )
385
+ with gr.Column(scale=1):
386
+ switch_btn = gr.Button("Переключить модель", variant="secondary")
387
+ model_status = gr.Textbox(
388
+ value=f"Текущая модель: {current_model}",
389
+ label="Статус модели",
390
+ interactive=False
391
+ )
392
+
393
+ with gr.Row():
394
+ with gr.Column(scale=3):
395
+ question_input = gr.Textbox(
396
+ label="Ваш вопрос к базе знаний",
397
+ placeholder="Введите вопрос по нормативным документам...",
398
+ lines=3
399
+ )
400
+ ask_btn = gr.Button("Найти ответ", variant="primary", size="lg")
401
+
402
+ gr.Examples(
403
+ examples=[
404
+ чем этот рисунок: ГОСТ Р 50.04.07-2022 Приложение Л. Л.1.5 Рисунок Л.2",
405
+ "Л.9 Формула в ГОСТ Р 50.04.07 - 2022 что и о чем там?",
406
+ "Какой стандарт устанавливает порядок признания протоколов испытаний продукции в об��асти использования атомной энергии?",
407
+ "Кто несет ответственность за организацию и проведение признания протоколов испытаний продукции?",
408
+ "В каких случаях могут быть признаны протоколы испытаний, проведенные лабораториями?",
409
+ "В какой таблице можно найти информацию о методы исследований при аттестационных испытаниях технологии термической обработки заготовок из легированных сталей? Какой документ и какой раздел?"
410
+ ],
411
+ inputs=question_input
412
+ )
413
+
414
+ with gr.Row():
415
+ with gr.Column(scale=2):
416
+ answer_output = gr.HTML(
417
+ label="",
418
+ value=f"<div style='background-color: #2d3748; color: white; padding: 20px; border-radius: 10px; text-align: center;'>Здесь появится ответ на ваш вопрос...<br><small>Текущая модель: {current_model}</small></div>",
419
+ )
420
+
421
+ with gr.Column(scale=1):
422
+ sources_output = gr.HTML(
423
+ label="",
424
+ value="<div style='background-color: #2d3748; color: white; padding: 20px; border-radius: 10px; text-align: center;'>Здесь появятся релевантные чанки...</div>",
425
+ )
426
+
427
+ with gr.Column(scale=1):
428
+ chunks_output = gr.HTML(
429
+ label="Релевантные чанки",
430
+ value="<div style='background-color: #2d3748; color: white; padding: 20px; border-radius: 10px; text-align: center;'>Здесь появятся релевантные чанки...</div>",
431
+ )
432
+
433
+ # NEW TAB: Retrieval Parameters
434
+ with gr.Tab("⚙️ Параметры поиска"):
435
+ gr.Markdown("### Настройка параметров векторного поиска и переранжирования")
436
+
437
+ with gr.Row():
438
+ with gr.Column():
439
+ vector_top_k = gr.Slider(
440
+ minimum=10,
441
+ maximum=200,
442
+ value=50,
443
+ step=10,
444
+ label="Vector Top K",
445
+ info="Количество результатов из векторного поиска"
446
+ )
447
+
448
+ with gr.Column():
449
+ bm25_top_k = gr.Slider(
450
+ minimum=10,
451
+ maximum=200,
452
+ value=50,
453
+ step=10,
454
+ label="BM25 Top K",
455
+ info="Количество результатов из BM25 поиска"
456
+ )
457
+
458
+ with gr.Row():
459
+ with gr.Column():
460
+ similarity_cutoff = gr.Slider(
461
+ minimum=0.0,
462
+ maximum=1.0,
463
+ value=0.55,
464
+ step=0.05,
465
+ label="Similarity Cutoff",
466
+ info="Минимальный порог схожести для векторного поиска"
467
+ )
468
+
469
+ with gr.Column():
470
+ hybrid_top_k = gr.Slider(
471
+ minimum=10,
472
+ maximum=300,
473
+ value=100,
474
+ step=10,
475
+ label="Hybrid Top K",
476
+ info="Количество результатов из гибридного поиска"
477
+ )
478
+
479
+ with gr.Row():
480
+ with gr.Column():
481
+ rerank_top_k = gr.Slider(
482
+ minimum=5,
483
+ maximum=100,
484
+ value=20,
485
+ step=5,
486
+ label="Rerank Top K",
487
+ info="Количество результатов после переранжирования"
488
+ )
489
+
490
+ with gr.Column():
491
+ update_btn = gr.Button("Применить параметры", variant="primary")
492
+ update_status = gr.Textbox(
493
+ value="Параметры готовы к применению",
494
+ label="Статус",
495
+ interactive=False
496
+ )
497
+
498
+ gr.Markdown("""
499
+ ### Рекомендации:
500
+ - **Vector Top K**: Увеличьте для более полного поиска по семантике (50-100)
501
+ - **BM25 Top K**: Увеличьте для лучшего поиска по ключевым словам (30-80)
502
+ - **Similarity Cutoff**: Снизьте для более мягких критериев (0.3-0.6), повысьте для строгих (0.7-0.9)
503
+ - **Hybrid Top K**: Объединённые результаты (100-150)
504
+ - **Rerank Top K**: Финальные результаты (10-30)
505
+ """)
506
+
507
+ update_btn.click(
508
+ fn=update_retrieval_params,
509
+ inputs=[vector_top_k, bm25_top_k, similarity_cutoff, hybrid_top_k, rerank_top_k],
510
+ outputs=[update_status]
511
+ )
512
+
513
+ # Display current parameters
514
+ gr.Markdown("### Текущие параметры:")
515
+ current_params_display = gr.Textbox(
516
+ value="Vector: 50 | BM25: 50 | Cutoff: 0.55 | Hybrid: 100 | Rerank: 20",
517
+ label="",
518
+ interactive=False,
519
+ lines=2
520
+ )
521
+
522
+ def display_current_params():
523
+ return f"""Vector Top K: {retrieval_params['vector_top_k']}
524
+ BM25 Top K: {retrieval_params['bm25_top_k']}
525
+ Similarity Cutoff: {retrieval_params['similarity_cutoff']}
526
+ Hybrid Top K: {retrieval_params['hybrid_top_k']}
527
+ Rerank Top K: {retrieval_params['rerank_top_k']}"""
528
+
529
+ # Refresh params display on tab change
530
+ demo.load(
531
+ fn=display_current_params,
532
+ outputs=[current_params_display]
533
+ )
534
+
535
+ update_btn.click(
536
+ fn=display_current_params,
537
+ outputs=[current_params_display]
538
+ )
539
+
540
+ # Original tab logic
541
+ switch_btn.click(
542
+ fn=switch_model_func,
543
+ inputs=[model_dropdown],
544
+ outputs=[model_status]
545
+ )
546
+
547
+ ask_btn.click(
548
+ fn=answer_question_func,
549
+ inputs=[question_input],
550
+ outputs=[answer_output, sources_output, chunks_output]
551
+ )
552
+
553
+ question_input.submit(
554
+ fn=answer_question_func,
555
+ inputs=[question_input],
556
+ outputs=[answer_output, sources_output, chunks_output]
557
+ )
558
+ return demo
559
+
560
+
561
+ query_engine = None
562
+ chunks_df = None
563
+ reranker = None
564
+ vector_index = None
565
+ current_model = DEFAULT_MODEL
566
+
567
+ def main_switch_model(model_name):
568
+ global query_engine, vector_index, current_model
569
+
570
+ new_query_engine, status_message = switch_model(model_name, vector_index)
571
+ if new_query_engine:
572
+ query_engine = new_query_engine
573
+ current_model = model_name
574
+
575
+ return status_message
576
+
577
+
578
+
579
+
580
+ def main():
581
+ global query_engine, chunks_df, reranker, vector_index, current_model
582
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "")
583
+ if GOOGLE_API_KEY:
584
+ log_message("Использование Google API для модели генерации текста")
585
+ else:
586
+ log_message("Google API ключ не найден, использование локальной модели")
587
+ log_message("Запуск AIEXP - AI Expert для нормативной документации")
588
+ query_engine, chunks_df, reranker, vector_index, chunk_info = initialize_system(
589
+ repo_id=HF_REPO_ID,
590
+ hf_token=HF_TOKEN,
591
+ download_dir=DOWNLOAD_DIR,
592
+ json_files_dir=JSON_FILES_DIR,
593
+ table_data_dir=TABLE_DATA_DIR,
594
+ image_data_dir=IMAGE_DATA_DIR,
595
+ use_json_instead_csv=True,
596
+ )
597
+
598
+ if query_engine:
599
+ log_message("Запуск веб-интерфейса")
600
+ demo = create_demo_interface(
601
+ answer_question_func=main_answer_question,
602
+ switch_model_func=main_switch_model,
603
+ current_model=current_model,
604
+ chunk_info=chunk_info
605
+ )
606
+ demo.api = "retrieve_chunks"
607
+ demo.queue()
608
+
609
+ demo.launch(
610
+ server_name="0.0.0.0",
611
+ server_port=7860,
612
+ share=True,
613
+ debug=False
614
+ )
615
+ else:
616
+ log_message("Невозможно запустить приложение из-за ошибки инициализации")
617
+ sys.exit(1)
618
+
619
+ if __name__ == "__main__":
620
  main()