support-bot-saas/backend/app/services/embedding.py
ed0ss 41af532e7b
Some checks are pending
CI / lint-backend (push) Waiting to run
CI / lint-frontend (push) Waiting to run
CI / build-backend (push) Blocked by required conditions
CI / build-frontend (push) Blocked by required conditions
fix: масштабные исправления и улучшения бэкенда и фронтенда
Бэкенд:
- 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
2026-06-20 19:51:58 +03:00

61 lines
1.7 KiB
Python

import logging
import httpx
from app.config import settings
logger = logging.getLogger(__name__)
OPENROUTER_BASE = "https://openrouter.ai/api/v1"
EMBEDDING_MODEL = settings.openrouter_embedding_model
IS_NVIDIA = "nvidia" in EMBEDDING_MODEL
_client: httpx.AsyncClient | None = None
def get_client() -> httpx.AsyncClient:
global _client
if _client is None:
_client = httpx.AsyncClient(
base_url=OPENROUTER_BASE,
headers={
"Authorization": f"Bearer {settings.openrouter_api_key}",
"Content-Type": "application/json",
},
timeout=30,
)
return _client
def _prefix_text(text: str, prefix: str) -> str:
if IS_NVIDIA:
return f"{prefix}: {text}"
return text
async def embed_texts(texts: list[str]) -> list[list[float]]:
prefixed = [_prefix_text(t, "passage") for t in texts]
client = get_client()
response = await client.post(
"/embeddings",
json={"model": EMBEDDING_MODEL, "input": prefixed},
)
data = response.json()
if not data.get("data"):
logger.error("Empty embedding response: %s", data)
return [[0.0] * 2048 for _ in texts]
return [item["embedding"] for item in data["data"]]
async def embed_query(text: str) -> list[float]:
prefixed = _prefix_text(text, "query")
client = get_client()
response = await client.post(
"/embeddings",
json={"model": EMBEDDING_MODEL, "input": [prefixed]},
)
data = response.json()
if not data.get("data"):
logger.error("Empty embedding response for query: %s", data)
return [0.0] * 2048
return data["data"][0]["embedding"]