173 lines
8.7 KiB
Python
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) |