Files
trueconf_bot/search_bot/indexer.py

173 lines
8.7 KiB
Python

import os
import sys
import string
import pickle # Библиотека для сохранения фрагментов текста на диск
# Загрузчики и разбивка текста
from langchain_community.document_loaders import DirectoryLoader, PyMuPDFLoader, Docx2txtLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Эмбеддинги и векторная база
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
# Ретриверы
from langchain_community.retrievers import BM25Retriever
from langchain_classic.retrievers import EnsembleRetriever
# Цепочки
from langchain_classic.chains.combine_documents import create_stuff_documents_chain
from langchain_classic.chains import create_retrieval_chain
# Базовые модули и LLM
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
# ==========================================
# НАСТРОЙКА ПУТЕЙ
# ==========================================
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BASE_REGULATIONS_PATH = os.path.join(BASE_DIR, "search_bot", "Регламенты", "АОХКСибцем", "07 Служба Вице-президента по экономике и финансам", "ДИТ")
MODEL_PATH = os.path.join(BASE_DIR, "models", "mpnet_model")
# Пути для сохранения базы данных
FAISS_DB_PATH = os.path.join(BASE_DIR, "search_bot", "faiss_index")
CHUNKS_PATH = os.path.join(BASE_DIR, "search_bot", "chunks.pkl")
# ==========================================
# 1. ИНИЦИАЛИЗАЦИЯ ЭМБЕДДИНГОВ
# ==========================================
print("Загрузка модели эмбеддингов (оффлайн)...")
embedding = HuggingFaceEmbeddings(
model_name=MODEL_PATH,
model_kwargs={'device': 'cpu'},
encode_kwargs={'normalize_embeddings': False}
)
# ==========================================
# 2. ЗАГРУЗКА ИЛИ СОЗДАНИЕ БАЗЫ ДАННЫХ
# ==========================================
# Проверяем, существует ли уже сохраненная база
if os.path.exists(FAISS_DB_PATH) and os.path.exists(CHUNKS_PATH):
print("✅ Найдена сохраненная база данных! Загружаю с диска (это быстро)...")
# Загружаем фрагменты текста для BM25
with open(CHUNKS_PATH, 'rb') as f:
split_docs = pickle.load(f)
# Загружаем векторы FAISS
vector_store = FAISS.load_local(
FAISS_DB_PATH,
embedding,
allow_dangerous_deserialization=True # Обязательно для локальных файлов
)
else:
print("⚠️ Сохраненная база не найдена. Начинаю чтение и индексацию документов...\n")
if not os.path.exists(BASE_REGULATIONS_PATH):
print("❌ ОШИБКА: Указанный путь к документам не существует.")
sys.exit(1)
# Загружаем PDF и DOCX
pdf_loader = DirectoryLoader(BASE_REGULATIONS_PATH, glob="**/*.pdf", loader_cls=PyMuPDFLoader, show_progress=True)
docx_loader = DirectoryLoader(BASE_REGULATIONS_PATH, glob="**/*.docx", loader_cls=Docx2txtLoader, show_progress=True)
docs = pdf_loader.load() + docx_loader.load()
if len(docs) == 0:
print("❌ В указанной папке нет файлов .pdf или .docx. Завершение.")
sys.exit(1)
print(f"✅ Загружено страниц/файлов: {len(docs)}. Разбивка на фрагменты...")
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1200, chunk_overlap=200, separators=["\n\n", "\n", ".", " "])
split_docs = text_splitter.split_documents(docs)
print("Создаю векторную базу FAISS...")
vector_store = FAISS.from_documents(split_docs, embedding=embedding)
# СОХРАНЕНИЕ НА ДИСК ДЛЯ БУДУЩИХ ЗАПУСКОВ
print("💾 Сохраняю базу данных на диск...")
vector_store.save_local(FAISS_DB_PATH)
with open(CHUNKS_PATH, 'wb') as f:
pickle.dump(split_docs, f)
print("✅ База успешно сохранена!")
# ==========================================
# 3. НАСТРОЙКА РЕТРИВЕРОВ (ПОИСКА)
# ==========================================
# 3.1 Семантический поиск
embedding_retriever = vector_store.as_retriever(search_kwargs={"k": 4})
# 3.2 Лексический поиск
def tokenize(s):
return s.lower().translate(str.maketrans("", "", string.punctuation)).split(" ")
bm25_retriever = BM25Retriever.from_documents(
documents=split_docs,
preprocess_func=tokenize,
k=5
)
# 3.3 Гибридный поиск (Ансамбль)
ensemble_retriever = EnsembleRetriever(
retrievers=[embedding_retriever, bm25_retriever],
weights=[0.4, 0.6]
)
# ==========================================
# 4. ПОДКЛЮЧЕНИЕ К LLAMA.CPP
# ==========================================
print("Подключение к серверу llama.cpp...")
llm = ChatOpenAI(
base_url="http://127.0.0.1:8080/v1",
api_key="not-needed",
temperature=0.0,
max_tokens=1024
)
prompt = ChatPromptTemplate.from_template('''Ты — строгий корпоративный ИИ-помощник.
Твоя задача — отвечать на вопросы строго на основании текста предоставленных регламентов.
Внимательно изучи контекст. Если там есть ответ, сформулируй его четко и по делу. Обязательно указывай номер пункта или название документа, откуда взята информация.
Если в контексте НЕТ ответа на вопрос, не придумывай информацию, а выведи фразу: "В предоставленных регламентах нет информации по данному вопросу."
Контекст:
{context}
Вопрос пользователя: {input}
Ответ:'''
)
document_chain = create_stuff_documents_chain(llm=llm, prompt=prompt)
rag_chain = create_retrieval_chain(ensemble_retriever, document_chain)
# ==========================================
# 5. ТЕСТИРОВАНИЕ СИСТЕМЫ
# ==========================================
print("\n🚀 Система готова к работе! Выполняем запросы...\n")
# Тестовые вопросы по Положению № ПОЛ-177
questions = [
"Где работник обязан хранить электронные документы, связанные с производственной деятельностью?",
"Через какой срок удаляются электронные документы, если их не открывали, и можно ли их восстановить?",
"Кому разрешен доступ к сетевым ресурсам Soft/Distr?",
"Какие правила хранения и удаления установлены для медиафайлов в сетевой папке 'СВК'?",
"Разрешено ли самостоятельно предоставлять общий доступ к папкам на своем рабочем компьютере?",
"Что имеют право сделать сотрудники ДИТ или СИБ, если файл несет угрозу ресурсам?"
]
for q in questions:
print(f"❓ Вопрос: {q}")
try:
response = rag_chain.invoke({'input': q})
print(f"🤖 Ответ: {response['answer']}\n")
# Распечатка источников (полезно для отладки)
print("🔍 Найденные источники:")
for i, doc in enumerate(response['context']):
source_name = doc.metadata.get('source', 'Неизвестный источник').split('/')[-1] # Берем только имя файла
print(f" [{i+1}] Файл: {source_name}")
print("\n" + "-" * 50 + "\n")
except Exception as e:
print(f"❌ Ошибка при запросе к серверу llama.cpp: {e}")
print("-" * 50)