RAG_AIEXP_01 / utils.py
MrSimple07's picture
simplest version
57e4dbd
Raw
History Blame
13.2 kB
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