from llama_index.llms.google_genai import GoogleGenAI from llama_index.embeddings.huggingface import HuggingFaceEmbedding from sentence_transformers import CrossEncoder from my_logging import log_message def get_llm_model(api_key, model_name="gemini-2.0-flash"): """Get LLM model""" return GoogleGenAI(model=model_name, api_key=api_key) def get_embedding_model(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"): """Get embedding model""" return HuggingFaceEmbedding(model_name=model_name) def get_reranker_model(model_name='cross-encoder/ms-marco-MiniLM-L-12-v2'): """Get reranker model""" return CrossEncoder(model_name) def format_sources(nodes): """Format retrieved sources for display""" sources = [] for node in nodes: meta = node.metadata doc_type = meta.get('type', 'text') doc_id = meta.get('document_id', 'unknown') if doc_type == 'table': table_num = meta.get('table_number', 'unknown') title = meta.get('table_title', '') sources.append(f"📊 {doc_id} - Таблица {table_num}: {title}") elif doc_type == 'image': img_num = meta.get('image_number', 'unknown') sources.append(f"🖼️ {doc_id} - Рисунок {img_num}") else: section = meta.get('section_id', '') sources.append(f"📄 {doc_id} - Раздел {section}") return "\n".join(set(sources)) def preprocess_query(question): import re question_lower = question.lower() # Extract document ID and normalize doc_match = re.search(r'(гост|нп|му)\s*р?\s*[№-]*\s*([0-9\.-]+)', question_lower) enhanced_query = question if doc_match: doc_type = doc_match.group(1).upper() doc_num = doc_match.group(2) # Add normalized versions enhanced_query += f" {doc_type} Р {doc_num}" return enhanced_query def answer_question(question, query_engine, reranker): try: log_message(f"Query: {question}") enhanced_query = preprocess_query(question) if enhanced_query != question: log_message(f"Enhanced query: {enhanced_query}") retrieved = query_engine.retriever.retrieve(enhanced_query) log_message(f"Retrieved {len(retrieved)} nodes") doc_stats = {} for n in retrieved: doc_id = n.metadata.get('document_id', 'unknown') doc_type = n.metadata.get('type', 'text') if doc_id not in doc_stats: doc_stats[doc_id] = {'tables': set(), 'text': 0, 'images': 0} if doc_type == 'table': table_id = n.metadata.get('table_identifier', n.metadata.get('table_number', '?')) doc_stats[doc_id]['tables'].add(table_id) elif doc_type == 'image': doc_stats[doc_id]['images'] += 1 else: doc_stats[doc_id]['text'] += 1 for doc_id in sorted(doc_stats.keys()): stats = doc_stats[doc_id] parts = [] if stats['tables']: parts.append(f"tables={list(stats['tables'])[:5]}") if stats['text']: parts.append(f"text={stats['text']}") if stats['images']: parts.append(f"images={stats['images']}") log_message(f" {doc_id}: {', '.join(parts)}") doc_ids = [n.metadata.get('document_id', 'unknown') for n in retrieved] table_nums = [n.metadata.get('table_number', '') for n in retrieved if n.metadata.get('type') == 'table'] log_message(f"Retrieved from documents: {set(doc_ids)}") if table_nums: log_message(f"Retrieved tables: {set(table_nums)}") reranked = rerank_nodes(question, retrieved, reranker, top_k=25) log_message(f"Reranked to {len(reranked)} nodes") doc_ids_reranked = [n.metadata.get('document_id', 'unknown') for n in reranked] table_nums_reranked = [n.metadata.get('table_number', '') for n in reranked if n.metadata.get('type') == 'table'] log_message(f"After reranking - documents: {set(doc_ids_reranked)}") if table_nums_reranked: log_message(f"After reranking - tables: {set(table_nums_reranked)}") context_parts = [] for n in reranked: meta = n.metadata doc_id = meta.get('document_id', 'unknown') doc_type = meta.get('type', 'text') if doc_type == 'table': table_num = meta.get('table_number', 'unknown') title = meta.get('table_title', '') source_label = f"[ТАБЛИЦА {table_num} - {doc_id}]" if title: source_label += f" {title}" elif doc_type == 'image': img_num = meta.get('image_number', 'unknown') source_label = f"[РИСУНОК {img_num} - {doc_id}]" else: section = meta.get('section_id', '') source_label = f"[{doc_id} - {section}]" context_parts.append(f"{source_label}\n{n.text}") context = "\n\n" + ("="*70 + "\n\n").join(context_parts) from config import CUSTOM_PROMPT prompt = f""" Вы являетесь высокоспециализированным Ассистентом для анализа нормативных документов (AIEXP). Ваша цель - предоставлять точные, корректные и контекстно релевантные ответы исключительно на основе предоставленного контекста из нормативной документации. ПРАВИЛА АНАЛИЗА ЗАПРОСА: 1. ПРЯМЫЕ ВОПРОСЫ БЕЗ ДОКУМЕНТАЛЬНОГО КОНТЕКСТА: Если пользователь задает вопрос типа "В каких случаях могут быть признаны протоколы испытаний?" без предоставления дополнительных документов, найдите соответствующую информацию в доступном контексте и предоставьте полный ответ с указанием источников. 2. ОПРЕДЕЛЕНИЕ ТИПА ЗАДАЧИ: а) ПОИСК И ОТВЕТ НА ВОПРОС (ключевые слова: "в каких случаях", "когда", "кто", "что", "как", "почему"): - Найдите релевантную информацию в контексте - Предоставьте развернутый ответ - Обязательно укажите конкретные документы и разделы - Процитируйте ключевые положения б) КРАТКОЕ САММАРИ (ключевые слова: "кратко", "суммировать", "резюме", "основные моменты"): - Предоставьте структурированное резюме - Выделите ключевые требования - Используйте нумерованный список в) ПОИСК ДОКУМЕНТА И ПУНКТА (ключевые слова: "найти", "где", "какой документ", "в каком разделе"): - Укажите конкретный документ и структурное расположение - Предоставьте точные номера разделов/пунктов г) ПРОВЕРКА КОРРЕКТНОСТИ (ключевые слова: "правильно ли", "соответствует ли", "проверить"): - Четко укажите: "СООТВЕТСТВУЕТ" или "НЕ СООТВЕТСТВУЕТ" - Перечислите конкретные требования д) ПЛАН ДЕЙСТВИЙ (ключевые слова: "план", "алгоритм", "пошагово"): - Создайте пронумерованный план - Укажите ссылки на соответствующие пункты НД ПРАВИЛА ФОРМИРОВАНИЯ ОТВЕТОВ: Работай исключительно с информацией из предоставленного контекста. Запрещено использовать: - Общие знания - Информацию из интернета - Данные из предыдущих диалогов - Собственные предположения 1. СТРУКТУРА ОТВЕТА: - Начинайте с прямого ответа на вопрос - Затем указывайте нормативные основания - Завершайте ссылками на конкретные документы и разделы 2. РАБОТА С КОНТЕКСТОМ: - Если информация найдена в контексте - предоставьте полный ответ - Если информация не найдена: "Информация по вашему запросу не найдена в доступной нормативной документации" - Не делайте предположений за пределами контекста - Не используйте общие знания 3. ТЕРМИНОЛОГИЯ И ЦИТИРОВАНИЕ: - Сохраняйте официальную терминологию НД - Цитируйте точные формулировки ключевых требований - При множественных источниках - укажите все релевантные 4. ФОРМАТИРОВАНИЕ: - Для перечислений: используйте нумерованные списки - Выделяйте критически важные требования - Структурируйте ответ логически # КАК РАБОТАТЬ С ЗАПРОСОМ **Шаг 1:** Определи, что именно ищет пользователь (термин, требование, процедура, условие) **Шаг 2:** Найди релевантную информацию в контексте **Шаг 3:** Сформируй ответ: - Если нашел: укажи документ и пункт, процитируй нужную часть - Если не нашел: четко сообщи об отсутствии информации **Шаг 4:** При наличии нескольких источников: - Представь их последовательно с указанием источника каждого - Если источников много (>4) — сначала дай их список, потом цитаты КОНТЕКСТ: {context} ВОПРОС: {question} """ response = query_engine.query(prompt) sources = format_sources(reranked) return response.response, sources except Exception as e: log_message(f"Error: {e}") import traceback log_message(traceback.format_exc()) return f"Ошибка: {e}", "" def rerank_nodes(query, nodes, reranker, top_k=20, min_score=0.3): """Rerank nodes with diversity - MORE LENIENT""" if not nodes: return [] # Score all nodes pairs = [[query, n.text] for n in nodes] scores = reranker.predict(pairs) # Sort by score scored = sorted(zip(nodes, scores), key=lambda x: x[1], reverse=True) # More lenient threshold filtered = [(n, s) for n, s in scored if s >= min_score] if not filtered: # Fallback: take top 50% if nothing passes threshold cutoff = max(scores) * 0.5 filtered = [(n, s) for n, s in scored if s >= cutoff][:top_k] # Log top scores for debugging log_message(f"Top 5 reranking scores: {[f'{s:.3f}' for _, s in scored[:5]]}") # Diversity selection - but prioritize tables if query mentions them selected = [] seen_docs = set() table_nodes = [] other_nodes = [] for node, score in filtered: if node.metadata.get('type') == 'table': table_nodes.append((node, score)) else: other_nodes.append((node, score)) # If query mentions "таблица", prioritize table nodes if 'таблиц' in query.lower(): combined = table_nodes + other_nodes else: combined = filtered for node, score in combined[:top_k]: if len(selected) >= top_k: break selected.append(node) seen_docs.add(node.metadata.get('document_id', 'unknown')) log_message(f"Reranked: {len(filtered)} → {len(selected)} (from {len(seen_docs)} docs)") return selected