support-bot-saas/prepare_data.py
2026-06-20 16:07:21 +03:00

171 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import json
import pickle
import re
from pathlib import Path
from typing import List
import faiss
import numpy as np
from langchain_core.embeddings import Embeddings
from sentence_transformers import SentenceTransformer
EMBED_MODEL = "intfloat/multilingual-e5-large"
DATA_DIR = Path(__file__).parent
TICKETS_PATTERN = "tickets*.json"
VECTOR_DIR = DATA_DIR / "vector_store"
VECTOR_INDEX = VECTOR_DIR / "index.faiss.bin"
VECTOR_META = VECTOR_DIR / "metadata.pkl"
NON_SOLUTION = re.compile(
r"(уточнит|приложите скрин|какая ошибка|с какой проблемой"
r"|нет обратной связи|запрос завершу|открыть его снова"
r"|откройте новый|обратиться в службу поддержки"
r"|свяжитесь с технической"
r"|напишите нам|позвоните нам)", re.I
)
class LocalEmbeddings(Embeddings):
def __init__(self, model_name: str):
self.model = SentenceTransformer(model_name, device="cuda")
def embed_documents(self, texts: List[str]) -> List[List[float]]:
emb = self.model.encode(texts, normalize_embeddings=True, show_progress_bar=True)
return emb.tolist()
def embed_query(self, text: str) -> List[float]:
emb = self.model.encode([text], normalize_embeddings=True)
return emb[0].tolist()
def clean_text(text: str) -> str:
text = re.sub(r'\[/?q\].*?\[/q\]', '', text, flags=re.DOTALL | re.IGNORECASE)
text = re.sub(r'https?://\S+', '', text)
text = re.sub(r'\s+', ' ', text).strip()
return text
def has_real_solution(messages: list[dict]) -> str | None:
support_texts = []
for m in messages:
if m["role"] == "support":
cleaned = clean_text(m["text"])
if cleaned:
support_texts.append(cleaned)
if not support_texts:
return None
combined = "\n".join(support_texts)
if len(combined) < 100:
return None
last = support_texts[-1]
if NON_SOLUTION.search(last):
return None
if last.strip().endswith("?") and len(last) < 150:
return None
return combined
def get_client_dialogue(messages: list[dict]) -> str:
texts = []
for m in messages:
if m["role"] == "client":
cleaned = clean_text(m["text"])
if cleaned:
texts.append(cleaned)
return "\n".join(texts)
def build_chunk(ticket: dict) -> dict | None:
msgs = ticket.get("messages") or []
if not msgs:
return None
description = clean_text(ticket.get("description", ""))
solution = has_real_solution(msgs)
if not solution:
return None
client_msgs = get_client_dialogue(msgs)
# Текст для поиска — только описание + переписка клиента
search_parts = []
if description:
search_parts.append(description)
if client_msgs:
search_parts.append(client_msgs)
search_text = "\n".join(search_parts).strip()
if not search_text:
return None
# Полный текст для показа и LLM
full_text = (
f"Категория: {ticket['category']}\n"
f"Проблема: {description}\n"
f"Решение: {solution}"
)
metadata = {
"ticket_id": ticket["ticket_id"],
"client": ticket["client"],
"category": ticket["category"],
"full_text": full_text,
}
return {"search_text": search_text, "metadata": metadata}
def load_all_tickets() -> list:
all_tickets = []
for f in sorted(DATA_DIR.glob(TICKETS_PATTERN)):
tickets = json.loads(f.read_text(encoding="utf-8"))
print(f" {f.name}: {len(tickets)} тикетов")
all_tickets.extend(tickets)
return all_tickets
def save_faiss_index(index, path: Path):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(faiss.serialize_index(index))
def load_faiss_index(path: Path):
return faiss.deserialize_index(np.frombuffer(path.read_bytes(), dtype=np.uint8))
def main():
print("Загрузка файлов tickets*.json ...")
tickets = load_all_tickets()
print(f"Всего тикетов: {len(tickets)}")
chunks = []
skipped = 0
for t in tickets:
chunk = build_chunk(t)
if chunk is not None:
chunks.append(chunk)
else:
skipped += 1
print(f"Сформировано чанков: {len(chunks)}")
print(f"Пропущено (нет решения): {skipped}")
print(f"Загрузка модели эмбеддингов: {EMBED_MODEL} ...")
embeddings = LocalEmbeddings(EMBED_MODEL)
search_texts = [c["search_text"] for c in chunks]
metadatas = [c["metadata"] for c in chunks]
print("Создание эмбеддингов и FAISS индекса...")
vectors = embeddings.embed_documents(search_texts)
dim = len(vectors[0])
index = faiss.IndexFlatIP(dim)
index.add(np.array(vectors, dtype=np.float32))
save_faiss_index(index, VECTOR_INDEX)
with open(VECTOR_META, "wb") as f:
pickle.dump({"metadatas": metadatas}, f)
print(f"FAISS индекс сохранён: {VECTOR_DIR}")
print(f"Размерность: {dim}")
print(f"Векторов в индексе: {index.ntotal}")
if __name__ == "__main__":
main()