MrSimple01 commited on
Commit
602e06e
·
verified ·
1 Parent(s): b716c6d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +575 -575
app.py CHANGED
@@ -1,576 +1,576 @@
1
- import gradio as gr
2
- from huggingface_hub import hf_hub_download, list_repo_files
3
- import faiss
4
- import pandas as pd
5
- import os
6
- import json
7
- from llama_index.core import Document, VectorStoreIndex, Settings
8
- from llama_index.embeddings.huggingface import HuggingFaceEmbedding
9
- from llama_index.llms.google_genai import GoogleGenAI
10
- from llama_index.llms.openai import OpenAI
11
- from llama_index.core.query_engine import RetrieverQueryEngine
12
- from llama_index.core.retrievers import VectorIndexRetriever
13
- from llama_index.core.response_synthesizers import get_response_synthesizer, ResponseMode
14
- from llama_index.core.prompts import PromptTemplate
15
- from llama_index.retrievers.bm25 import BM25Retriever
16
- from sentence_transformers import CrossEncoder
17
- from llama_index.core.retrievers import QueryFusionRetriever
18
- import time
19
- import sys
20
- import logging
21
- from config import *
22
-
23
- REPO_ID = "MrSimple01/AIEXP_RAG_FILES"
24
- faiss_index_filename = "cleaned_faiss_index.index"
25
- chunks_filename = "processed_chunks.csv"
26
- table_data_dir = "Табличные данные_JSON"
27
- image_data_dir = "Изображения"
28
- download_dir = "rag_files"
29
- HF_TOKEN = os.getenv('HF_TOKEN')
30
-
31
- # Global variables
32
- query_engine = None
33
- chunks_df = None
34
- reranker = None
35
- vector_index = None
36
- current_model = DEFAULT_MODEL
37
-
38
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
39
- logger = logging.getLogger(__name__)
40
-
41
- def log_message(message):
42
- logger.info(message)
43
- print(message, flush=True)
44
- sys.stdout.flush()
45
-
46
- def get_llm_model(model_name):
47
- """Get LLM model instance based on model name"""
48
- try:
49
- model_config = AVAILABLE_MODELS.get(model_name)
50
- if not model_config:
51
- log_message(f"Модель {model_name} не найдена, использую модель по умолчанию")
52
- model_config = AVAILABLE_MODELS[DEFAULT_MODEL]
53
-
54
- if not model_config.get("api_key"):
55
- raise Exception(f"API ключ не найден для модели {model_name}")
56
-
57
- if model_config["provider"] == "google":
58
- return GoogleGenAI(
59
- model=model_config["model_name"],
60
- api_key=model_config["api_key"]
61
- )
62
- elif model_config["provider"] == "openai":
63
- return OpenAI(
64
- model=model_config["model_name"],
65
- api_key=model_config["api_key"]
66
- )
67
- else:
68
- raise Exception(f"Неподдерживаемый провайдер: {model_config['provider']}")
69
-
70
- except Exception as e:
71
- log_message(f"Ошибка создания модели {model_name}: {str(e)}")
72
- # Fallback to default Google model
73
- return GoogleGenAI(model="gemini-2.0-flash", api_key=GOOGLE_API_KEY)
74
-
75
- def switch_model(model_name):
76
- """Switch to a different LLM model"""
77
- global query_engine, current_model
78
-
79
- try:
80
- log_message(f"Переключение на модель: {model_name}")
81
-
82
- # Create new LLM instance
83
- new_llm = get_llm_model(model_name)
84
- Settings.llm = new_llm
85
-
86
- # Recreate query engine with new model
87
- if vector_index is not None:
88
- recreate_query_engine()
89
- current_model = model_name
90
- log_message(f"Модель успешно переключена на: {model_name}")
91
- return f"✅ Модель переключена на: {model_name}"
92
- else:
93
- return "❌ Ошибка: система не инициализирована"
94
-
95
- except Exception as e:
96
- error_msg = f"Ошибка переключения модели: {str(e)}"
97
- log_message(error_msg)
98
- return f"❌ {error_msg}"
99
-
100
- def recreate_query_engine():
101
- """Recreate query engine with current settings"""
102
- global query_engine
103
-
104
- try:
105
- # Create BM25 retriever
106
- bm25_retriever = BM25Retriever.from_defaults(
107
- docstore=vector_index.docstore,
108
- similarity_top_k=10
109
- )
110
-
111
- # Create vector retriever
112
- vector_retriever = VectorIndexRetriever(
113
- index=vector_index,
114
- similarity_top_k=10,
115
- similarity_cutoff=0.5
116
- )
117
-
118
- # Create hybrid retriever
119
- hybrid_retriever = QueryFusionRetriever(
120
- [vector_retriever, bm25_retriever],
121
- similarity_top_k=25,
122
- num_queries=1
123
- )
124
-
125
- # Create response synthesizer
126
- custom_prompt_template = PromptTemplate(CUSTOM_PROMPT)
127
- response_synthesizer = get_response_synthesizer(
128
- response_mode=ResponseMode.TREE_SUMMARIZE,
129
- text_qa_template=custom_prompt_template
130
- )
131
-
132
- # Create new query engine
133
- query_engine = RetrieverQueryEngine(
134
- retriever=hybrid_retriever,
135
- response_synthesizer=response_synthesizer
136
- )
137
-
138
- log_message("Query engine успешно пересоздан")
139
-
140
- except Exception as e:
141
- log_message(f"Ошибка пересоздания query engine: {str(e)}")
142
- raise
143
-
144
- def table_to_document(table_data, document_id=None):
145
- content = ""
146
- if isinstance(table_data, dict):
147
- doc_id = document_id or table_data.get('document_id', table_data.get('document', 'Неизвестно'))
148
-
149
- table_num = table_data.get('table_number', 'Неизвестно')
150
- table_title = table_data.get('table_title', 'Неизвестно')
151
- section = table_data.get('section', 'Неизвестно')
152
-
153
- content += f"Таблица: {table_num}\n"
154
- content += f"Название: {table_title}\n"
155
- content += f"Документ: {doc_id}\n"
156
- content += f"Раздел: {section}\n"
157
-
158
- if 'data' in table_data and isinstance(table_data['data'], list):
159
- for row in table_data['data']:
160
- if isinstance(row, dict):
161
- row_text = " | ".join([f"{k}: {v}" for k, v in row.items()])
162
- content += f"{row_text}\n"
163
-
164
- return Document(
165
- text=content,
166
- metadata={
167
- "type": "table",
168
- "table_number": table_data.get('table_number', 'unknown'),
169
- "table_title": table_data.get('table_title', 'unknown'),
170
- "document_id": doc_id or table_data.get('document_id', table_data.get('document', 'unknown')),
171
- "section": table_data.get('section', 'unknown')
172
- }
173
- )
174
-
175
- def download_table_data():
176
- log_message("Начинаю загрузку табличных данных")
177
-
178
- table_files = []
179
- try:
180
- files = list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
181
- for file in files:
182
- if file.startswith(table_data_dir) and file.endswith('.json'):
183
- table_files.append(file)
184
-
185
- log_message(f"Найдено {len(table_files)} JSON файлов с таблицами")
186
-
187
- table_documents = []
188
- for file_path in table_files:
189
- try:
190
- log_message(f"Обрабатываю файл: {file_path}")
191
- local_path = hf_hub_download(
192
- repo_id=REPO_ID,
193
- filename=file_path,
194
- local_dir='',
195
- repo_type="dataset",
196
- token=HF_TOKEN
197
- )
198
-
199
- with open(local_path, 'r', encoding='utf-8') as f:
200
- table_data = json.load(f)
201
-
202
- if isinstance(table_data, dict):
203
- document_id = table_data.get('document', 'unknown')
204
-
205
- if 'sheets' in table_data:
206
- for sheet in table_data['sheets']:
207
- sheet['document'] = document_id
208
- doc = table_to_document(sheet, document_id)
209
- table_documents.append(doc)
210
- else:
211
- doc = table_to_document(table_data, document_id)
212
- table_documents.append(doc)
213
- elif isinstance(table_data, list):
214
- for table_json in table_data:
215
- doc = table_to_document(table_json)
216
- table_documents.append(doc)
217
-
218
- except Exception as e:
219
- log_message(f"Ошибка обработки файла {file_path}: {str(e)}")
220
- continue
221
-
222
- log_message(f"Создано {len(table_documents)} документов из таблиц")
223
- return table_documents
224
-
225
- except Exception as e:
226
- log_message(f"Ошибка загрузки табличных данных: {str(e)}")
227
- return []
228
-
229
- def download_image_data():
230
- log_message("Начинаю загрузку данных изображений")
231
-
232
- image_files = []
233
- try:
234
- files = list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
235
- for file in files:
236
- if file.startswith(image_data_dir) and file.endswith('.csv'):
237
- image_files.append(file)
238
-
239
- log_message(f"Найдено {len(image_files)} CSV файлов с изображениями")
240
-
241
- image_documents = []
242
- for file_path in image_files:
243
- try:
244
- log_message(f"Обрабатываю файл изображений: {file_path}")
245
- local_path = hf_hub_download(
246
- repo_id=REPO_ID,
247
- filename=file_path,
248
- local_dir='',
249
- repo_type="dataset",
250
- token=HF_TOKEN
251
- )
252
-
253
- df = pd.read_csv(local_path)
254
- log_message(f"Загружено {len(df)} записей изображений из файла {file_path}")
255
-
256
- for _, row in df.iterrows():
257
- content = f"Изображение: {row.get('№ Изображения', 'Неизвестно')}\n"
258
- content += f"Название: {row.get('Название изображения', 'Неизвестно')}\n"
259
- content += f"Описание: {row.get('Описание изображение', 'Неизвестно')}\n"
260
- content += f"Документ: {row.get('Обозначение документа', 'Неизвестно')}\n"
261
- content += f"Раздел: {row.get('Раздел документа', 'Неизвестно')}\n"
262
- content += f"Файл: {row.get('Файл изображения', 'Неизвестно')}\n"
263
-
264
- doc = Document(
265
- text=content,
266
- metadata={
267
- "type": "image",
268
- "image_number": row.get('№ Изображения', 'unknown'),
269
- "document_id": row.get('Обозначение документа', 'unknown'),
270
- "file_path": row.get('Файл изображения', 'unknown'),
271
- "section": row.get('Раздел документа', 'unknown')
272
- }
273
- )
274
- image_documents.append(doc)
275
-
276
- except Exception as e:
277
- log_message(f"Ошибка обработки файла {file_path}: {str(e)}")
278
- continue
279
-
280
- log_message(f"Создано {len(image_documents)} документов из изображений")
281
- return image_documents
282
-
283
- except Exception as e:
284
- log_message(f"Ошибка загрузки данных изображений: {str(e)}")
285
- return []
286
-
287
- def initialize_models():
288
- global query_engine, chunks_df, reranker, vector_index, current_model
289
-
290
- try:
291
- log_message("Инициализация системы")
292
- os.makedirs(download_dir, exist_ok=True)
293
-
294
- log_message("Загружаю основные файлы")
295
- chunks_csv_path = hf_hub_download(
296
- repo_id=REPO_ID,
297
- filename=chunks_filename,
298
- local_dir=download_dir,
299
- repo_type="dataset",
300
- token=HF_TOKEN
301
- )
302
-
303
- log_message("Загружаю данные чанков")
304
- chunks_df = pd.read_csv(chunks_csv_path)
305
- log_message(f"Загружено {len(chunks_df)} чанков")
306
-
307
- log_message("Инициализирую модели")
308
- embed_model = HuggingFaceEmbedding(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
309
- llm = get_llm_model(current_model)
310
-
311
- log_message("Инициализирую переранкер")
312
- reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-12-v2')
313
-
314
- Settings.embed_model = embed_model
315
- Settings.llm = llm
316
-
317
- text_column = None
318
- for col in chunks_df.columns:
319
- if 'text' in col.lower() or 'content' in col.lower() or 'chunk' in col.lower():
320
- text_column = col
321
- break
322
-
323
- if text_column is None:
324
- text_column = chunks_df.columns[0]
325
-
326
- log_message(f"Использую колонку: {text_column}")
327
-
328
- log_message("Создаю документы из чанков")
329
- documents = []
330
- for i, (_, row) in enumerate(chunks_df.iterrows()):
331
- doc = Document(
332
- text=str(row[text_column]),
333
- metadata={
334
- "chunk_id": row.get('chunk_id', i),
335
- "document_id": row.get('document_id', 'unknown'),
336
- "type": "text"
337
- }
338
- )
339
- documents.append(doc)
340
-
341
- log_message(f"Создано {len(documents)} текстовых документов")
342
-
343
- log_message("Добавляю табличные данные")
344
- table_documents = download_table_data()
345
- documents.extend(table_documents)
346
-
347
- log_message("Добавляю данные изображений")
348
- image_documents = download_image_data()
349
- documents.extend(image_documents)
350
-
351
- log_message(f"Всего документов: {len(documents)}")
352
-
353
- log_message("Строю векторный индекс")
354
- vector_index = VectorStoreIndex.from_documents(documents)
355
-
356
- # Create query engine
357
- recreate_query_engine()
358
-
359
- log_message(f"Система успешно инициализирована с моделью: {current_model}")
360
- return True
361
-
362
- except Exception as e:
363
- log_message(f"Ошибка инициализации: {str(e)}")
364
- return False
365
-
366
- def rerank_nodes(query, nodes, top_k=10):
367
- if not nodes or not reranker:
368
- return nodes[:top_k]
369
-
370
- try:
371
- log_message(f"Переранжирую {len(nodes)} узлов")
372
-
373
- pairs = []
374
- for node in nodes:
375
- pairs.append([query, node.text])
376
-
377
- scores = reranker.predict(pairs)
378
-
379
- scored_nodes = list(zip(nodes, scores))
380
- scored_nodes.sort(key=lambda x: x[1], reverse=True)
381
-
382
- reranked_nodes = [node for node, score in scored_nodes[:top_k]]
383
- log_message(f"Возвращаю топ-{len(reranked_nodes)} переранжированных узлов")
384
-
385
- return reranked_nodes
386
- except Exception as e:
387
- log_message(f"Ошибка переранжировки: {str(e)}")
388
- return nodes[:top_k]
389
-
390
- def answer_question(question):
391
- global query_engine, chunks_df, current_model
392
-
393
- if query_engine is None:
394
- return "<div style='background-color: #e53e3e; color: white; padding: 20px; border-radius: 10px;'>Система не инициализирована</div>", ""
395
-
396
- try:
397
- log_message(f"Получен вопрос: {question}")
398
- log_message(f"Используется модель: {current_model}")
399
- start_time = time.time()
400
-
401
- log_message("Извлекаю релевантные узлы")
402
- retrieved_nodes = query_engine.retriever.retrieve(question)
403
- log_message(f"Извлечено {len(retrieved_nodes)} узлов")
404
-
405
- log_message("Применяю переранжировку")
406
- reranked_nodes = rerank_nodes(question, retrieved_nodes, top_k=10)
407
-
408
- log_message(f"Отправляю запрос в LLM с {len(reranked_nodes)} узлами")
409
- response = query_engine.query(question)
410
-
411
- end_time = time.time()
412
- processing_time = end_time - start_time
413
-
414
- log_message(f"Обработка завершена за {processing_time:.2f} секунд")
415
-
416
- sources_html = generate_sources_html(reranked_nodes)
417
-
418
- answer_with_time = f"""<div style='background-color: #2d3748; color: white; padding: 20px; border-radius: 10px; margin-bottom: 10px;'>
419
- <h3 style='color: #63b3ed; margin-top: 0;'>Ответ (Модель: {current_model}):</h3>
420
- <div style='line-height: 1.6; font-size: 16px;'>{response.response}</div>
421
- <div style='margin-top: 15px; padding-top: 10px; border-top: 1px solid #4a5568; font-size: 14px; color: #a0aec0;'>
422
- Время обработки: {processing_time:.2f} секунд
423
- </div>
424
- </div>"""
425
-
426
- return answer_with_time, sources_html
427
-
428
- except Exception as e:
429
- log_message(f"Ошибка обработки вопроса: {str(e)}")
430
- error_msg = f"<div style='background-color: #e53e3e; color: white; padding: 20px; border-radius: 10px;'>Ошибка обработки вопроса: {str(e)}</div>"
431
- return error_msg, ""
432
-
433
- def generate_sources_html(nodes):
434
- html = "<div style='background-color: #2d3748; color: white; padding: 20px; border-radius: 10px; max-height: 400px; overflow-y: auto;'>"
435
- html += "<h3 style='color: #63b3ed; margin-top: 0;'>Источники:</h3>"
436
-
437
- for i, node in enumerate(nodes):
438
- metadata = node.metadata if hasattr(node, 'metadata') else {}
439
- doc_type = metadata.get('type', 'text')
440
- doc_id = metadata.get('document_id', 'unknown')
441
-
442
- html += f"<div style='margin-bottom: 15px; padding: 15px; border: 1px solid #4a5568; border-radius: 8px; background-color: #1a202c;'>"
443
-
444
- if doc_type == 'text':
445
- html += f"<h4 style='margin: 0 0 10px 0; color: #63b3ed;'>📄 {doc_id}</h4>"
446
- elif doc_type == 'table':
447
- table_num = metadata.get('table_number', 'unknown')
448
- if table_num and table_num != 'unknown':
449
- if not table_num.startswith('№'):
450
- table_num = f"№{table_num}"
451
- html += f"<h4 style='margin: 0 0 10px 0; color: #68d391;'>📊 Таблица {table_num} - {doc_id}</h4>"
452
- else:
453
- html += f"<h4 style='margin: 0 0 10px 0; color: #68d391;'>📊 Таблица - {doc_id}</h4>"
454
- elif doc_type == 'image':
455
- image_num = metadata.get('image_number', 'unknown')
456
- section = metadata.get('section', '')
457
- if image_num and image_num != 'unknown':
458
- if not str(image_num).startswith('№'):
459
- image_num = f"№{image_num}"
460
- html += f"<h4 style='margin: 0 0 10px 0; color: #fbb6ce;'>🖼️ Изображение {image_num} - {doc_id} ({section})</h4>"
461
- else:
462
- html += f"<h4 style='margin: 0 0 10px 0; color: #fbb6ce;'>🖼️ Изображение - {doc_id} ({section})</h4>"
463
-
464
- if chunks_df is not None and 'file_link' in chunks_df.columns and doc_type == 'text':
465
- doc_rows = chunks_df[chunks_df['document_id'] == doc_id]
466
- if not doc_rows.empty:
467
- file_link = doc_rows.iloc[0]['file_link']
468
- html += f"<a href='{file_link}' target='_blank' style='color: #68d391; text-decoration: none; font-size: 14px; display: inline-block; margin-top: 10px;'>🔗 Ссылка на документ</a><br>"
469
-
470
- html += "</div>"
471
-
472
- html += "</div>"
473
- return html
474
-
475
- def create_demo_interface():
476
- with gr.Blocks(title="AIEXP - AI Expert для нормативной документации", theme=gr.themes.Soft()) as demo:
477
-
478
- gr.Markdown("""
479
- # AIEXP - Artificial Intelligence Expert
480
-
481
- ## Инструмент для работы с нормативной документацией
482
- """)
483
-
484
- with gr.Tab("🏠 Поиск по нормативным документам"):
485
- gr.Markdown("### Задайте вопрос по нормативной документации")
486
-
487
- # Model selection section
488
- with gr.Row():
489
- with gr.Column(scale=2):
490
- model_dropdown = gr.Dropdown(
491
- choices=list(AVAILABLE_MODELS.keys()),
492
- value=current_model,
493
- label="🤖 Выберите языковую модель",
494
- info="Выберите модель для генерации ответов"
495
- )
496
- with gr.Column(scale=1):
497
- switch_btn = gr.Button("🔄 Переключить модель", variant="secondary")
498
- model_status = gr.Textbox(
499
- value=f"Текущая модель: {current_model}",
500
- label="Статус модели",
501
- interactive=False
502
- )
503
-
504
- with gr.Row():
505
- with gr.Column(scale=3):
506
- question_input = gr.Textbox(
507
- label="Ваш вопрос к базе знаний",
508
- placeholder="Введите вопрос по нормативным документам...",
509
- lines=3
510
- )
511
- ask_btn = gr.Button("🔍 Найти ответ", variant="primary", size="lg")
512
-
513
- gr.Examples(
514
- examples=[
515
- "О чем этот рисунок: ГОСТ Р 50.04.07-2022 Приложение Л. Л.1.5 Рисунок Л.5",
516
- "Л.9 Формула в ГОСТ Р 50.04.07 - 2022 что и о чем там?",
517
- "Какой стандарт устанавливает порядок признания протоколов испытаний продукции в области использования атомной энергии?",
518
- "Кто несет ответственность за организацию и проведение признания протоколов испытаний продукции?",
519
- "В каких случаях могут быть признаны протоколы испытаний, проведенные лабораториями?",
520
- ],
521
- inputs=question_input
522
- )
523
-
524
- with gr.Row():
525
- with gr.Column(scale=2):
526
- answer_output = gr.HTML(
527
- label="",
528
- value=f"<div style='background-color: #2d3748; color: white; padding: 20px; border-radius: 10px; text-align: center;'>Здесь появится ответ на ваш вопрос...<br><small>Текущая модель: {current_model}</small></div>",
529
- )
530
-
531
- with gr.Column(scale=1):
532
- sources_output = gr.HTML(
533
- label="",
534
- value="<div style='background-color: #2d3748; color: white; padding: 20px; border-radius: 10px; text-align: center;'>Здесь появятся источники...</div>",
535
- )
536
-
537
- # Event handlers
538
- def update_model_status(new_model):
539
- result = switch_model(new_model)
540
- return result
541
-
542
- switch_btn.click(
543
- fn=update_model_status,
544
- inputs=[model_dropdown],
545
- outputs=[model_status]
546
- )
547
-
548
- ask_btn.click(
549
- fn=answer_question,
550
- inputs=[question_input],
551
- outputs=[answer_output, sources_output]
552
- )
553
-
554
- question_input.submit(
555
- fn=answer_question,
556
- inputs=[question_input],
557
- outputs=[answer_output, sources_output]
558
- )
559
-
560
- return demo
561
-
562
- if __name__ == "__main__":
563
- log_message("Запуск AIEXP - AI Expert для нормативной документации")
564
-
565
- if initialize_models():
566
- log_message("Запуск веб-интерфейса")
567
- demo = create_demo_interface()
568
- demo.launch(
569
- server_name="0.0.0.0",
570
- server_port=7860,
571
- share=True,
572
- debug=False
573
- )
574
- else:
575
- log_message("Невозможно запустить приложение из-за ошибки инициализации")
576
  sys.exit(1)
 
1
+ import gradio as gr
2
+ from huggingface_hub import hf_hub_download, list_repo_files
3
+ import faiss
4
+ import pandas as pd
5
+ import os
6
+ import json
7
+ from llama_index.core import Document, VectorStoreIndex, Settings
8
+ from llama_index.embeddings.huggingface import HuggingFaceEmbedding
9
+ from llama_index.llms.google_genai import GoogleGenAI
10
+ from llama_index.llms.openai import OpenAI
11
+ from llama_index.core.query_engine import RetrieverQueryEngine
12
+ from llama_index.core.retrievers import VectorIndexRetriever
13
+ from llama_index.core.response_synthesizers import get_response_synthesizer, ResponseMode
14
+ from llama_index.core.prompts import PromptTemplate
15
+ from llama_index.retrievers.bm25 import BM25Retriever
16
+ from sentence_transformers import CrossEncoder
17
+ from llama_index.core.retrievers import QueryFusionRetriever
18
+ import time
19
+ import sys
20
+ import logging
21
+ from config import *
22
+
23
+ REPO_ID = "MrSimple01/AIEXP_RAG_FILES"
24
+ faiss_index_filename = "cleaned_faiss_index.index"
25
+ chunks_filename = "processed_chunks.csv"
26
+ table_data_dir = "Табличные данные_JSON"
27
+ image_data_dir = "Изображения"
28
+ download_dir = "rag_files"
29
+ HF_TOKEN = os.getenv('HF_TOKEN')
30
+
31
+ # Global variables
32
+ query_engine = None
33
+ chunks_df = None
34
+ reranker = None
35
+ vector_index = None
36
+ current_model = DEFAULT_MODEL
37
+
38
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
39
+ logger = logging.getLogger(__name__)
40
+
41
+ def log_message(message):
42
+ logger.info(message)
43
+ print(message, flush=True)
44
+ sys.stdout.flush()
45
+
46
+ def get_llm_model(model_name):
47
+ """Get LLM model instance based on model name"""
48
+ try:
49
+ model_config = AVAILABLE_MODELS.get(model_name)
50
+ if not model_config:
51
+ log_message(f"Модель {model_name} не найдена, использую модель по умолчанию")
52
+ model_config = AVAILABLE_MODELS[DEFAULT_MODEL]
53
+
54
+ if not model_config.get("api_key"):
55
+ raise Exception(f"API ключ не найден для модели {model_name}")
56
+
57
+ if model_config["provider"] == "google":
58
+ return GoogleGenAI(
59
+ model=model_config["model_name"],
60
+ api_key=model_config["api_key"]
61
+ )
62
+ elif model_config["provider"] == "openai":
63
+ return OpenAI(
64
+ model=model_config["model_name"],
65
+ api_key=model_config["api_key"]
66
+ )
67
+ else:
68
+ raise Exception(f"Неподдерживаемый провайдер: {model_config['provider']}")
69
+
70
+ except Exception as e:
71
+ log_message(f"Ошибка создания модели {model_name}: {str(e)}")
72
+ # Fallback to default Google model
73
+ return GoogleGenAI(model="gemini-2.0-flash", api_key=GOOGLE_API_KEY)
74
+
75
+ def switch_model(model_name):
76
+ """Switch to a different LLM model"""
77
+ global query_engine, current_model
78
+
79
+ try:
80
+ log_message(f"Переключение на модель: {model_name}")
81
+
82
+ # Create new LLM instance
83
+ new_llm = get_llm_model(model_name)
84
+ Settings.llm = new_llm
85
+
86
+ # Recreate query engine with new model
87
+ if vector_index is not None:
88
+ recreate_query_engine()
89
+ current_model = model_name
90
+ log_message(f"Модель успешно переключена на: {model_name}")
91
+ return f"✅ Модель переключена на: {model_name}"
92
+ else:
93
+ return "❌ Ошибка: система не инициализирована"
94
+
95
+ except Exception as e:
96
+ error_msg = f"Ошибка переключения модели: {str(e)}"
97
+ log_message(error_msg)
98
+ return f"❌ {error_msg}"
99
+
100
+ def recreate_query_engine():
101
+ """Recreate query engine with current settings"""
102
+ global query_engine
103
+
104
+ try:
105
+ # Create BM25 retriever
106
+ bm25_retriever = BM25Retriever.from_defaults(
107
+ docstore=vector_index.docstore,
108
+ similarity_top_k=10
109
+ )
110
+
111
+ # Create vector retriever
112
+ vector_retriever = VectorIndexRetriever(
113
+ index=vector_index,
114
+ similarity_top_k=10,
115
+ similarity_cutoff=0.5
116
+ )
117
+
118
+ # Create hybrid retriever
119
+ hybrid_retriever = QueryFusionRetriever(
120
+ [vector_retriever, bm25_retriever],
121
+ similarity_top_k=25,
122
+ num_queries=1
123
+ )
124
+
125
+ # Create response synthesizer
126
+ custom_prompt_template = PromptTemplate(CUSTOM_PROMPT)
127
+ response_synthesizer = get_response_synthesizer(
128
+ response_mode=ResponseMode.TREE_SUMMARIZE,
129
+ text_qa_template=custom_prompt_template
130
+ )
131
+
132
+ # Create new query engine
133
+ query_engine = RetrieverQueryEngine(
134
+ retriever=hybrid_retriever,
135
+ response_synthesizer=response_synthesizer
136
+ )
137
+
138
+ log_message("Query engine успешно пересоздан")
139
+
140
+ except Exception as e:
141
+ log_message(f"Ошибка пересоздания query engine: {str(e)}")
142
+ raise
143
+
144
+ def table_to_document(table_data, document_id=None):
145
+ content = ""
146
+ if isinstance(table_data, dict):
147
+ doc_id = document_id or table_data.get('document_id', table_data.get('document', 'Неизвестно'))
148
+
149
+ table_num = table_data.get('table_number', 'Неизвестно')
150
+ table_title = table_data.get('table_title', 'Неизвестно')
151
+ section = table_data.get('section', 'Неизвестно')
152
+
153
+ content += f"Таблица: {table_num}\n"
154
+ content += f"Название: {table_title}\n"
155
+ content += f"Документ: {doc_id}\n"
156
+ content += f"Раздел: {section}\n"
157
+
158
+ if 'data' in table_data and isinstance(table_data['data'], list):
159
+ for row in table_data['data']:
160
+ if isinstance(row, dict):
161
+ row_text = " | ".join([f"{k}: {v}" for k, v in row.items()])
162
+ content += f"{row_text}\n"
163
+
164
+ return Document(
165
+ text=content,
166
+ metadata={
167
+ "type": "table",
168
+ "table_number": table_data.get('table_number', 'unknown'),
169
+ "table_title": table_data.get('table_title', 'unknown'),
170
+ "document_id": doc_id or table_data.get('document_id', table_data.get('document', 'unknown')),
171
+ "section": table_data.get('section', 'unknown')
172
+ }
173
+ )
174
+
175
+ def download_table_data():
176
+ log_message("Начинаю загрузку табличных данных")
177
+
178
+ table_files = []
179
+ try:
180
+ files = list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
181
+ for file in files:
182
+ if file.startswith(table_data_dir) and file.endswith('.json'):
183
+ table_files.append(file)
184
+
185
+ log_message(f"Найдено {len(table_files)} JSON файлов с таблицами")
186
+
187
+ table_documents = []
188
+ for file_path in table_files:
189
+ try:
190
+ log_message(f"Обрабатываю файл: {file_path}")
191
+ local_path = hf_hub_download(
192
+ repo_id=REPO_ID,
193
+ filename=file_path,
194
+ local_dir='',
195
+ repo_type="dataset",
196
+ token=HF_TOKEN
197
+ )
198
+
199
+ with open(local_path, 'r', encoding='utf-8') as f:
200
+ table_data = json.load(f)
201
+
202
+ if isinstance(table_data, dict):
203
+ document_id = table_data.get('document', 'unknown')
204
+
205
+ if 'sheets' in table_data:
206
+ for sheet in table_data['sheets']:
207
+ sheet['document'] = document_id
208
+ doc = table_to_document(sheet, document_id)
209
+ table_documents.append(doc)
210
+ else:
211
+ doc = table_to_document(table_data, document_id)
212
+ table_documents.append(doc)
213
+ elif isinstance(table_data, list):
214
+ for table_json in table_data:
215
+ doc = table_to_document(table_json)
216
+ table_documents.append(doc)
217
+
218
+ except Exception as e:
219
+ log_message(f"Ошибка обработки файла {file_path}: {str(e)}")
220
+ continue
221
+
222
+ log_message(f"Создано {len(table_documents)} документов из таблиц")
223
+ return table_documents
224
+
225
+ except Exception as e:
226
+ log_message(f"Ошибка загрузки табличных данных: {str(e)}")
227
+ return []
228
+
229
+ def download_image_data():
230
+ log_message("Начинаю загрузку данных изображений")
231
+
232
+ image_files = []
233
+ try:
234
+ files = list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
235
+ for file in files:
236
+ if file.startswith(image_data_dir) and file.endswith('.csv'):
237
+ image_files.append(file)
238
+
239
+ log_message(f"Найдено {len(image_files)} CSV файлов с изображениями")
240
+
241
+ image_documents = []
242
+ for file_path in image_files:
243
+ try:
244
+ log_message(f"Обрабатываю файл изображений: {file_path}")
245
+ local_path = hf_hub_download(
246
+ repo_id=REPO_ID,
247
+ filename=file_path,
248
+ local_dir='',
249
+ repo_type="dataset",
250
+ token=HF_TOKEN
251
+ )
252
+
253
+ df = pd.read_csv(local_path)
254
+ log_message(f"Загружено {len(df)} записей изображений из файла {file_path}")
255
+
256
+ for _, row in df.iterrows():
257
+ content = f"Изображение: {row.get('№ Изображения', 'Неизвестно')}\n"
258
+ content += f"Название: {row.get('Название изображения', 'Неизвестно')}\n"
259
+ content += f"Описание: {row.get('Описание изображение', 'Неизвестно')}\n"
260
+ content += f"Документ: {row.get('Обозначение документа', 'Неизвестно')}\n"
261
+ content += f"Раздел: {row.get('Раздел документа', 'Неизвестно')}\n"
262
+ content += f"Файл: {row.get('Файл изображения', 'Неизвестно')}\n"
263
+
264
+ doc = Document(
265
+ text=content,
266
+ metadata={
267
+ "type": "image",
268
+ "image_number": row.get('№ Изображения', 'unknown'),
269
+ "document_id": row.get('Обозначение документа', 'unknown'),
270
+ "file_path": row.get('Файл изображения', 'unknown'),
271
+ "section": row.get('Раздел документа', 'unknown')
272
+ }
273
+ )
274
+ image_documents.append(doc)
275
+
276
+ except Exception as e:
277
+ log_message(f"Ошибка обработки файла {file_path}: {str(e)}")
278
+ continue
279
+
280
+ log_message(f"Создано {len(image_documents)} документов из изображений")
281
+ return image_documents
282
+
283
+ except Exception as e:
284
+ log_message(f"Ошибка загрузки данных изображений: {str(e)}")
285
+ return []
286
+
287
+ def initialize_models():
288
+ global query_engine, chunks_df, reranker, vector_index, current_model
289
+
290
+ try:
291
+ log_message("Инициализация системы")
292
+ os.makedirs(download_dir, exist_ok=True)
293
+
294
+ log_message("Загружаю основные файлы")
295
+ chunks_csv_path = hf_hub_download(
296
+ repo_id=REPO_ID,
297
+ filename=chunks_filename,
298
+ local_dir=download_dir,
299
+ repo_type="dataset",
300
+ token=HF_TOKEN
301
+ )
302
+
303
+ log_message("Загружаю данные чанков")
304
+ chunks_df = pd.read_csv(chunks_csv_path)
305
+ log_message(f"Загружено {len(chunks_df)} чанков")
306
+
307
+ log_message("Инициализирую модели")
308
+ embed_model = HuggingFaceEmbedding(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
309
+ llm = get_llm_model(current_model)
310
+
311
+ log_message("Инициализирую переранкер")
312
+ reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-12-v2')
313
+
314
+ Settings.embed_model = embed_model
315
+ Settings.llm = llm
316
+
317
+ text_column = None
318
+ for col in chunks_df.columns:
319
+ if 'text' in col.lower() or 'content' in col.lower() or 'chunk' in col.lower():
320
+ text_column = col
321
+ break
322
+
323
+ if text_column is None:
324
+ text_column = chunks_df.columns[0]
325
+
326
+ log_message(f"Использую колонку: {text_column}")
327
+
328
+ log_message("Создаю документы из чанков")
329
+ documents = []
330
+ for i, (_, row) in enumerate(chunks_df.iterrows()):
331
+ doc = Document(
332
+ text=str(row[text_column]),
333
+ metadata={
334
+ "chunk_id": row.get('chunk_id', i),
335
+ "document_id": row.get('document_id', 'unknown'),
336
+ "type": "text"
337
+ }
338
+ )
339
+ documents.append(doc)
340
+
341
+ log_message(f"Создано {len(documents)} текстовых документов")
342
+
343
+ log_message("Добавляю табличные данные")
344
+ table_documents = download_table_data()
345
+ documents.extend(table_documents)
346
+
347
+ log_message("Добавляю данные изображений")
348
+ image_documents = download_image_data()
349
+ documents.extend(image_documents)
350
+
351
+ log_message(f"Всего документов: {len(documents)}")
352
+
353
+ log_message("Строю векторный индекс")
354
+ vector_index = VectorStoreIndex.from_documents(documents)
355
+
356
+ # Create query engine
357
+ recreate_query_engine()
358
+
359
+ log_message(f"Система успешно инициализирована с моделью: {current_model}")
360
+ return True
361
+
362
+ except Exception as e:
363
+ log_message(f"Ошибка инициализации: {str(e)}")
364
+ return False
365
+
366
+ def rerank_nodes(query, nodes, top_k=10):
367
+ if not nodes or not reranker:
368
+ return nodes[:top_k]
369
+
370
+ try:
371
+ log_message(f"Переранжирую {len(nodes)} узлов")
372
+
373
+ pairs = []
374
+ for node in nodes:
375
+ pairs.append([query, node.text])
376
+
377
+ scores = reranker.predict(pairs)
378
+
379
+ scored_nodes = list(zip(nodes, scores))
380
+ scored_nodes.sort(key=lambda x: x[1], reverse=True)
381
+
382
+ reranked_nodes = [node for node, score in scored_nodes[:top_k]]
383
+ log_message(f"Возвращаю топ-{len(reranked_nodes)} переранжированных узлов")
384
+
385
+ return reranked_nodes
386
+ except Exception as e:
387
+ log_message(f"Ошибка переранжировки: {str(e)}")
388
+ return nodes[:top_k]
389
+
390
+ def answer_question(question):
391
+ global query_engine, chunks_df, current_model
392
+
393
+ if query_engine is None:
394
+ return "<div style='background-color: #e53e3e; color: white; padding: 20px; border-radius: 10px;'>Система не инициализирована</div>", ""
395
+
396
+ try:
397
+ log_message(f"Получен вопрос: {question}")
398
+ log_message(f"Используется модель: {current_model}")
399
+ start_time = time.time()
400
+
401
+ log_message("Извлекаю релевантные узлы")
402
+ retrieved_nodes = query_engine.retriever.retrieve(question)
403
+ log_message(f"Извлечено {len(retrieved_nodes)} узлов")
404
+
405
+ log_message("Применяю переранжировку")
406
+ reranked_nodes = rerank_nodes(question, retrieved_nodes, top_k=10)
407
+
408
+ log_message(f"Отправляю запрос в LLM с {len(reranked_nodes)} узлами")
409
+ response = query_engine.query(question)
410
+
411
+ end_time = time.time()
412
+ processing_time = end_time - start_time
413
+
414
+ log_message(f"Обработка завершена за {processing_time:.2f} секунд")
415
+
416
+ sources_html = generate_sources_html(reranked_nodes)
417
+
418
+ answer_with_time = f"""<div style='background-color: #2d3748; color: white; padding: 20px; border-radius: 10px; margin-bottom: 10px;'>
419
+ <h3 style='color: #63b3ed; margin-top: 0;'>Ответ (Модель: {current_model}):</h3>
420
+ <div style='line-height: 1.6; font-size: 16px;'>{response.response}</div>
421
+ <div style='margin-top: 15px; padding-top: 10px; border-top: 1px solid #4a5568; font-size: 14px; color: #a0aec0;'>
422
+ Время обработки: {processing_time:.2f} секунд
423
+ </div>
424
+ </div>"""
425
+
426
+ return answer_with_time, sources_html
427
+
428
+ except Exception as e:
429
+ log_message(f"Ошибка обработки вопроса: {str(e)}")
430
+ error_msg = f"<div style='background-color: #e53e3e; color: white; padding: 20px; border-radius: 10px;'>Ошибка обработки вопроса: {str(e)}</div>"
431
+ return error_msg, ""
432
+
433
+ def generate_sources_html(nodes):
434
+ html = "<div style='background-color: #2d3748; color: white; padding: 20px; border-radius: 10px; max-height: 400px; overflow-y: auto;'>"
435
+ html += "<h3 style='color: #63b3ed; margin-top: 0;'>Источники:</h3>"
436
+
437
+ for i, node in enumerate(nodes):
438
+ metadata = node.metadata if hasattr(node, 'metadata') else {}
439
+ doc_type = metadata.get('type', 'text')
440
+ doc_id = metadata.get('document_id', 'unknown')
441
+
442
+ html += f"<div style='margin-bottom: 15px; padding: 15px; border: 1px solid #4a5568; border-radius: 8px; background-color: #1a202c;'>"
443
+
444
+ if doc_type == 'text':
445
+ html += f"<h4 style='margin: 0 0 10px 0; color: #63b3ed;'>📄 {doc_id}</h4>"
446
+ elif doc_type == 'table':
447
+ table_num = metadata.get('table_number', 'unknown')
448
+ if table_num and table_num != 'unknown':
449
+ if not table_num.startswith('№'):
450
+ table_num = f"№{table_num}"
451
+ html += f"<h4 style='margin: 0 0 10px 0; color: #68d391;'>📊 Таблица {table_num} - {doc_id}</h4>"
452
+ else:
453
+ html += f"<h4 style='margin: 0 0 10px 0; color: #68d391;'>📊 Таблица - {doc_id}</h4>"
454
+ elif doc_type == 'image':
455
+ image_num = metadata.get('image_number', 'unknown')
456
+ section = metadata.get('section', '')
457
+ if image_num and image_num != 'unknown':
458
+ if not str(image_num).startswith('№'):
459
+ image_num = f"№{image_num}"
460
+ html += f"<h4 style='margin: 0 0 10px 0; color: #fbb6ce;'>🖼️ Изображение {image_num} - {doc_id} ({section})</h4>"
461
+ else:
462
+ html += f"<h4 style='margin: 0 0 10px 0; color: #fbb6ce;'>🖼️ Изображение - {doc_id} ({section})</h4>"
463
+
464
+ if chunks_df is not None and 'file_link' in chunks_df.columns and doc_type == 'text':
465
+ doc_rows = chunks_df[chunks_df['document_id'] == doc_id]
466
+ if not doc_rows.empty:
467
+ file_link = doc_rows.iloc[0]['file_link']
468
+ html += f"<a href='{file_link}' target='_blank' style='color: #68d391; text-decoration: none; font-size: 14px; display: inline-block; margin-top: 10px;'>🔗 Ссылка на документ</a><br>"
469
+
470
+ html += "</div>"
471
+
472
+ html += "</div>"
473
+ return html
474
+
475
+ def create_demo_interface():
476
+ with gr.Blocks(title="AIEXP - AI Expert для нормативной документации", theme=gr.themes.Soft()) as demo:
477
+
478
+ gr.Markdown("""
479
+ # AIEXP - Artificial Intelligence Expert
480
+
481
+ ## Инструмент для работы с нормативной документацией
482
+ """)
483
+
484
+ with gr.Tab("🏠 Поиск по нормативным документам"):
485
+ gr.Markdown("### Задайте вопрос по нормативной документации")
486
+
487
+ # Model selection section
488
+ with gr.Row():
489
+ with gr.Column(scale=2):
490
+ model_dropdown = gr.Dropdown(
491
+ choices=list(AVAILABLE_MODELS.keys()),
492
+ value=current_model,
493
+ label="🤖 Выберите языковую модель",
494
+ info="Выберите модель для генерации ответов"
495
+ )
496
+ with gr.Column(scale=1):
497
+ switch_btn = gr.Button("🔄 Переключить модель", variant="secondary")
498
+ model_status = gr.Textbox(
499
+ value=f"Текущая модель: {current_model}",
500
+ label="Статус модели",
501
+ interactive=False
502
+ )
503
+
504
+ with gr.Row():
505
+ with gr.Column(scale=3):
506
+ question_input = gr.Textbox(
507
+ label="Ваш вопрос к базе знаний",
508
+ placeholder="Введите вопрос по нормативным документам...",
509
+ lines=3
510
+ )
511
+ ask_btn = gr.Button("🔍 Найти ответ", variant="primary", size="lg")
512
+
513
+ gr.Examples(
514
+ examples=[
515
+ "О чем этот рисунок: ГОСТ Р 50.04.07-2022 Приложение Л. Л.1.5 Рисунок Л.2",
516
+ "Л.9 Формула в ГОСТ Р 50.04.07 - 2022 что и о чем там?",
517
+ "Какой стандарт устанавливает порядок признания протоколов испытаний продукции в области использования атомной энергии?",
518
+ "Кто несет ответственность за организацию и проведение признания протоколов испытаний продукции?",
519
+ "В каких случаях могут быть признаны протоколы испытаний, проведенные лабораториями?",
520
+ ],
521
+ inputs=question_input
522
+ )
523
+
524
+ with gr.Row():
525
+ with gr.Column(scale=2):
526
+ answer_output = gr.HTML(
527
+ label="",
528
+ value=f"<div style='background-color: #2d3748; color: white; padding: 20px; border-radius: 10px; text-align: center;'>Здесь появится ответ на ваш вопрос...<br><small>Текущая модель: {current_model}</small></div>",
529
+ )
530
+
531
+ with gr.Column(scale=1):
532
+ sources_output = gr.HTML(
533
+ label="",
534
+ value="<div style='background-color: #2d3748; color: white; padding: 20px; border-radius: 10px; text-align: center;'>Здесь появятся источники...</div>",
535
+ )
536
+
537
+ # Event handlers
538
+ def update_model_status(new_model):
539
+ result = switch_model(new_model)
540
+ return result
541
+
542
+ switch_btn.click(
543
+ fn=update_model_status,
544
+ inputs=[model_dropdown],
545
+ outputs=[model_status]
546
+ )
547
+
548
+ ask_btn.click(
549
+ fn=answer_question,
550
+ inputs=[question_input],
551
+ outputs=[answer_output, sources_output]
552
+ )
553
+
554
+ question_input.submit(
555
+ fn=answer_question,
556
+ inputs=[question_input],
557
+ outputs=[answer_output, sources_output]
558
+ )
559
+
560
+ return demo
561
+
562
+ if __name__ == "__main__":
563
+ log_message("Запуск AIEXP - AI Expert для нормативной документации")
564
+
565
+ if initialize_models():
566
+ log_message("Запуск веб-интерфейса")
567
+ demo = create_demo_interface()
568
+ demo.launch(
569
+ server_name="0.0.0.0",
570
+ server_port=7860,
571
+ share=True,
572
+ debug=False
573
+ )
574
+ else:
575
+ log_message("Невозможно запустить приложение из-за ошибки инициализации")
576
  sys.exit(1)