Бэкенд:
- TicketInput: ticket_id принимает int | str, приводится к str (было 500 на числовых ID)
- LLM: добавлен импорт logger, обработка 429 и не-200 ответов от OpenRouter
- Смена модели на poolside/laguna-m.1:free (обход лимитов OpenRouter)
- Добавлен GET /api/collections/{id} — получение коллекции по ID
- Дедикат заявок: hash() заменён на детерминированный hashlib.md5
для ID точек в Qdrant (раньше после рестарта пода создавались дубликаты)
- Добавлена get_existing_ticket_ids() — проверка дубликатов до эмбеддинга,
пропуск уже загруженных заявок
- Мультитенантная изоляция: tenant_id + collection_id во всех фильтрах Qdrant
- Эмбеддинги: заменён несовместимый openai SDK на прямой httpx
- Реренкинг: исправлен доступ к ScoredPoint.payload через .get()
- Чанкинг: расширены роли (support/operator/agent + client/user/customer)
- bcrypt: понижен до 4.0.1 (5.x несовместим с passlib)
Фронтенд:
- Регистрация: добавлен выбор тарифа (free/starter/pro)
- CollectionView: ticket_count загружается с бэкенда при монтировании
(раньше жил только в React-состоянии — сбрасывался после обновления страницы)
- uploadFile: используется getToken() вместо устаревшей переменной token,
в ошибке показывается реальный HTTP-статус
- Nginx: увеличен client_max_body_size до 50M для загрузки файлов
- k8s: добавлены манифесты namespace, postgres, redis, qdrant, backend,
frontend, ingress, traefik
79 lines
1.5 KiB
Python
79 lines
1.5 KiB
Python
import uuid
|
|
from datetime import datetime
|
|
|
|
from pydantic import BaseModel, EmailStr, field_validator
|
|
|
|
|
|
class RegisterRequest(BaseModel):
|
|
email: EmailStr
|
|
password: str
|
|
plan: str = "free"
|
|
|
|
@field_validator("plan")
|
|
@classmethod
|
|
def validate_plan(cls, v: str) -> str:
|
|
if v not in ("free", "starter", "pro"):
|
|
raise ValueError("Invalid plan. Choose: free, starter, pro")
|
|
return v
|
|
|
|
|
|
class LoginRequest(BaseModel):
|
|
email: EmailStr
|
|
password: str
|
|
|
|
|
|
class TokenResponse(BaseModel):
|
|
access_token: str
|
|
token_type: str = "bearer"
|
|
|
|
|
|
class UserResponse(BaseModel):
|
|
id: uuid.UUID
|
|
email: str
|
|
api_key: str
|
|
plan: str
|
|
created_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class CollectionCreate(BaseModel):
|
|
name: str
|
|
|
|
|
|
class CollectionResponse(BaseModel):
|
|
id: uuid.UUID
|
|
user_id: uuid.UUID
|
|
name: str
|
|
ticket_count: int
|
|
created_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class TicketMessage(BaseModel):
|
|
role: str
|
|
text: str
|
|
|
|
|
|
class TicketInput(BaseModel):
|
|
ticket_id: int | str
|
|
category: str = ""
|
|
description: str = ""
|
|
client: str = ""
|
|
messages: list[TicketMessage] = []
|
|
|
|
@field_validator("ticket_id", mode="before")
|
|
@classmethod
|
|
def coerce_ticket_id(cls, v: int | str) -> str:
|
|
return str(v)
|
|
|
|
|
|
class TicketsUpload(BaseModel):
|
|
tickets: list[TicketInput]
|
|
|
|
|
|
class TicketsUploadResponse(BaseModel):
|
|
processed: int
|
|
skipped: int
|
|
collection_ticket_count: int
|