Backend:
- Fix TicketInput ticket_id type (int|str -> str) — Pydantic 500 on numeric IDs
- Fix LLM module: add missing logger import, handle 429 and non-200 responses
- Switch LLM model to poolside/laguna-m.1:free (avoid OpenRouter rate limits)
- Add GET /api/collections/{id} endpoint to fetch single collection
- Deduplicate tickets: replace randomized hash() with deterministic
hashlib.md5 for Qdrant point IDs
- Add get_existing_ticket_ids() — scroll Qdrant before processing,
skip already-imported tickets, avoid redundant embeddings
- Multi-tenant isolation: tenant_id + collection_id in all Qdrant filters
- Fix embedding: drop incompatible openai SDK, use raw httpx
- Fix reranking: access ScoredPoint.payload via .get(), not []
- Fix chunking: expand role detection (support/operator/agent + client/user/customer)
- Fix bcrypt: downgrade to 4.0.1 (incompatible 5.x with passlib)
Frontend:
- Register: add plan selection (free/starter/pro)
- CollectionView: fetch ticket_count from backend on mount
(not only from upload response — survived page refresh)
- uploadFile: use getToken() instead of stale module-level var,
show real HTTP status in error messages
- Nginx: increase client_max_body_size to 50M for file uploads
87 lines
2.6 KiB
Python
87 lines
2.6 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
|
||
|
||
|
||
SUPPORT_ROLES = {"support", "assistant", "operator", "agent"}
|
||
CLIENT_ROLES = {"client", "user", "customer"}
|
||
|
||
|
||
def has_real_solution(messages: list[dict]) -> Optional[str]:
|
||
support_texts = []
|
||
for m in messages:
|
||
if m["role"] in SUPPORT_ROLES:
|
||
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"] in CLIENT_ROLES:
|
||
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,
|
||
}
|