- Бэкенд: FastAPI + SQLAlchemy async + PostgreSQL, JWT-аутентификация, Qdrant multi-tenant векторное хранилище, Celery воркер - Фронтенд: React + Vite + TypeScript SPA (логин, регистрация, дашборд, поиск по коллекциям, настройки) - Инфраструктура: docker-compose, Helm-чарт, GitHub Actions CI, Prometheus/Grafana мониторинг - Embedding через OpenRouter (nvidia/llama-nemotron-embed-vl-1b-v2:free) - LLM: google/gemma-4-31b-it:free через OpenRouter
83 lines
2.4 KiB
Python
83 lines
2.4 KiB
Python
import re
|
||
from typing import Optional
|
||
|
||
NON_SOLUTION = re.compile(
|
||
r"(уточнит|приложите скрин|какая ошибка|с какой проблемой"
|
||
r"|нет обратной связи|запрос завершу|открыть его снова"
|
||
r"|откройте новый|обратиться в службу поддержки"
|
||
r"|свяжитесь с технической"
|
||
r"|напишите нам|позвоните нам)", re.I
|
||
)
|
||
|
||
|
||
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]) -> Optional[str]:
|
||
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) -> Optional[dict]:
|
||
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
|
||
|
||
full_text = (
|
||
f"Категория: {ticket['category']}\n"
|
||
f"Проблема: {description}\n"
|
||
f"Решение: {solution}"
|
||
)
|
||
|
||
return {
|
||
"ticket_id": ticket["ticket_id"],
|
||
"client": ticket["client"],
|
||
"category": ticket["category"],
|
||
"search_text": search_text,
|
||
"full_text": full_text,
|
||
"tenant_id": None,
|
||
}
|