Милана
Первый релиз: интеллектуальный помощник инженера-строителя
96aaef2
import numpy as np
import faiss
from sentence_transformers import SentenceTransformer
from typing import List, Dict
import pickle
from pathlib import Path
import re
class QASystem:
"""Система вопрос-ответ на основе документов"""
def __init__(self, use_llm: bool = False):
print("🔄 Загрузка модели эмбеддингов...")
self.model = SentenceTransformer('intfloat/multilingual-e5-small')
self.index = None
self.chunks = []
self.dimension = 384
self.is_ready = False
self.use_llm = use_llm
self.llm_engine = None
if use_llm:
try:
from core.llm_engine import LLMEngine
self.llm_engine = LLMEngine()
print("✅ LLM Engine готов")
except Exception as e:
print(f"⚠️ LLM Engine не загружен: {e}")
self.use_llm = False
def chunk_text(self, text: str, doc_name: str, chunk_size=500, overlap=100):
"""Разбивает текст на чанки"""
chunks = []
sentences = re.split(r'[.!?]\s+', text)
current_chunk = ""
chunk_id = 0
for sent in sentences:
if len(current_chunk) + len(sent) < chunk_size:
current_chunk += sent + ". "
else:
if current_chunk:
chunks.append({
'id': f"{doc_name}_{chunk_id}",
'text': current_chunk.strip(),
'doc_name': doc_name,
'chunk_id': chunk_id
})
chunk_id += 1
if overlap > 0:
words = current_chunk.split()
current_chunk = " ".join(words[-overlap//10:]) + " "
current_chunk += sent + ". "
if current_chunk:
chunks.append({
'id': f"{doc_name}_{chunk_id}",
'text': current_chunk.strip(),
'doc_name': doc_name,
'chunk_id': chunk_id
})
return chunks
def index_documents(self, documents_dir: Path):
"""Индексирует все документы в папке"""
print(f"📁 Индексация документов из {documents_dir}")
all_chunks = []
# Читаем все файлы
try:
from core.parser import read_docx
except ImportError:
from docx import Document
def read_docx(file_path):
doc = Document(file_path)
return '\n'.join([p.text for p in doc.paragraphs])
for file_path in documents_dir.glob("*"):
if file_path.suffix in ['.docx', '.txt']:
try:
if file_path.suffix == '.docx':
text = read_docx(file_path)
else:
with open(file_path, 'r', encoding='utf-8') as f:
text = f.read()
chunks = self.chunk_text(text, file_path.stem)
all_chunks.extend(chunks)
print(f" ✅ {file_path.name}: {len(chunks)} чанков")
except Exception as e:
print(f" ❌ {file_path.name}: {e}")
if not all_chunks:
print("❌ Нет документов для индексации")
return False
# Генерируем эмбеддинги
print(f"📊 Генерация эмбеддингов для {len(all_chunks)} чанков...")
texts = [chunk['text'] for chunk in all_chunks]
embeddings = self.model.encode(texts, show_progress_bar=True)
# Нормализуем для косинусного сходства
embeddings = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True)
# Создаем FAISS индекс
self.index = faiss.IndexFlatIP(self.dimension)
self.index.add(embeddings.astype(np.float32))
self.chunks = all_chunks
print(f"✅ Индекс создан: {self.index.ntotal} векторов")
self.is_ready = True
return True
def search(self, query: str, top_k: int = 5) -> List[Dict]:
"""Поиск релевантных чанков по вопросу"""
if not self.is_ready:
return []
# Кодируем вопрос
query_emb = self.model.encode([query])
query_emb = query_emb / np.linalg.norm(query_emb)
# Поиск
scores, indices = self.index.search(query_emb.astype(np.float32), top_k)
results = []
for score, idx in zip(scores[0], indices[0]):
if idx >= 0 and score > 0.5:
results.append({
'text': self.chunks[idx]['text'],
'doc_name': self.chunks[idx]['doc_name'],
'score': float(score)
})
return results
def answer(self, question: str, top_k: int = 5) -> Dict:
"""Ответ на вопрос на основе найденных чанков"""
relevant = self.search(question, top_k)
if not relevant:
return {
'question': question,
'answer': "Извините, не нашел информацию по вашему вопросу в документации.",
'sources': []
}
# Если используем LLM, генерируем умный ответ
if self.use_llm and self.llm_engine:
# Формируем контекст
context = ""
for chunk in relevant[:3]:
context += f"\n--- {chunk['doc_name']} ---\n"
context += chunk['text'][:500] + "\n"
# Генерируем ответ через LLM
try:
result = self.llm_engine.answer_with_context(question, context, relevant[:3])
return result
except Exception as e:
print(f"❌ Ошибка LLM: {e}")
# Fallback к простому ответу
# Иначе простой ответ
answer = f"**По вашему вопросу найдена информация:**\n\n"
for i, chunk in enumerate(relevant[:3], 1):
answer += f"**{i}. {chunk['doc_name']}** (релевантность: {chunk['score']:.2f})\n"
answer += f"{chunk['text'][:400]}\n\n"
return {
'question': question,
'answer': answer,
'sources': relevant
}
def set_top_k(self, top_k: int):
"""Установить количество возвращаемых фрагментов"""
self.top_k = top_k
def save_index(self, path: Path):
"""Сохраняет индекс"""
if not self.is_ready:
return
faiss.write_index(self.index, str(path / "index.faiss"))
with open(path / "chunks.pkl", 'wb') as f:
pickle.dump(self.chunks, f)
print(f"✅ Индекс сохранен в {path}")
def load_index(self, path: Path):
"""Загружает индекс"""
index_path = path / "index.faiss"
chunks_path = path / "chunks.pkl"
if not index_path.exists() or not chunks_path.exists():
return False
self.index = faiss.read_index(str(index_path))
with open(chunks_path, 'rb') as f:
self.chunks = pickle.load(f)
self.is_ready = True
print(f"✅ Индекс загружен: {self.index.ntotal} векторов")
return True