# План: RAG SaaS для техподдержки ## Статус - [ ] Этап 0 — Форк и структура проекта - [ ] Этап 1 — Бэкенд: регистрация, авторизация, коллекции - [ ] Этап 2 — Бэкенд: приём тикетов (UI + API) - [ ] Этап 3 — Бэкенд: поиск и генерация ответа - [ ] Этап 4 — Фронтенд: SPA - [ ] Этап 5 — Инфраструктура: K8s + CI/CD - [ ] Этап 6 — Мониторинг, биллинг, доки --- ## Этап 0 — Форк и структура Сделать форк текущего коммита `41b84e6`. Новая структура: ``` support-bot-saas/ ├── backend/ │ ├── app/ │ │ ├── main.py # FastAPI приложение │ │ ├── config.py # настройки (env) │ │ ├── auth/ # JWT, регистрация │ │ ├── models/ # Pydantic схемы │ │ ├── routers/ │ │ │ ├── auth.py # /api/register, /api/login │ │ │ ├── collections.py # /api/collections │ │ │ ├── tickets.py # /api/collections/:id/tickets │ │ │ └── search.py # /api/search │ │ ├── services/ │ │ │ ├── embedding.py # E5-large (общий воркер) │ │ │ ├── vector_store.py # Qdrant client │ │ │ ├── chunking.py # подготовка текста │ │ │ └── llm.py # внешний LLM │ │ └── workers/ │ │ └── process_file.py # Celery — обработка файлов │ ├── Dockerfile │ └── requirements.txt ├── frontend/ │ ├── src/ │ └── Dockerfile ├── helm/ # K8s чарты ├── docker-compose.yml # для локальной разработки └── .github/workflows/ # CI/CD ``` **Что берём из текущего кода:** - chunking.py — `build_chunk`, `has_real_solution`, `clean_text` - cross-encoder реранжировщик (на Django-side или отдельный микросервис) --- ## Этап 1 — Бэкенд: регистрация, коллекции, Qdrant ### 1.1 FastAPI + PostgreSQL ```python # models/user.py class User(Base): id: uuid email: str password_hash: str api_key: str # для API-интеграции created_at: datetime plan: str # free, pro, enterprise # models/collection.py class Collection(Base): id: uuid user_id: uuid # FK → user name: str ticket_count: int created_at: datetime ``` ### 1.2 Qdrant multi-tenancy **Одна коллекция Qdrant `tickets`, tenant_id в payload:** ```python # services/vector_store.py async def create_tenant_index(user_id: str): # Qdrant: одна общая коллекция + фильтр по tenant_id # Индекс создаётся один раз глобально pass async def upsert_chunks(user_id: str, chunks: list[dict]): client.upsert( collection_name="tickets", points=[ PointStruct( id=chunk["id"], vector=chunk["vector"], payload={ "tenant_id": user_id, "ticket_id": chunk["ticket_id"], "category": chunk["category"], "search_text": chunk["search_text"], "full_text": chunk["full_text"], } ) ] ) async def search(user_id: str, query_vector: list, k: int = 20): return client.query_points( collection_name="tickets", query=query_vector, query_filter=Filter( must=[FieldCondition(key="tenant_id", match={"value": user_id})] ), limit=k, ) ``` **Почему одна коллекция, а не на пользователя:** - Qdrant сам рекомендует такую схему для SaaS - Тысячи коллекций жрут память (каждая коллекция = свой HNSW-граф в RAM) - Фильтр по `tenant_id` индексируется, поиск не замедляется **Регистрация пользователя:** 1. Создать запись в PostgreSQL 2. Сгенерировать `api_key` 3. Qdrant ничего не делать — фильтрация по `tenant_id` будет работать автоматически ### API endpoints этапа 1 ``` POST /api/register # email + пароль → JWT + api_key POST /api/login # email + пароль → JWT GET /api/me # профиль POST /api/collections # создать базу знаний GET /api/collections # список баз DELETE /api/collections/:id # удалить базу (не рвать Qdrant) ``` --- ## Этап 2 — Приём тикетов ### 2.1 Через UI (загрузка файла) - Поддерживаемые форматы: JSON, CSV, XLSX - После загрузки → Celery task: 1. Парсинг в единую схему 2. apply `has_real_solution` filter 3. `build_chunk` → search_text + full_text 4. E5-large → вектор (через API embedding-воркера) 5. Upsert в Qdrant 6. Обновить `ticket_count` в Collection ### 2.2 Через API ``` POST /api/collections/:id/tickets Authorization: Bearer Content-Type: application/json { "tickets": [ { "ticket_id": "ext-001", "category": "Сбой", "description": "Не могу войти", "messages": [ {"role": "client", "text": "Пишет ошибку"}, {"role": "support", "text": "Проверьте логин"} ] } ] } ``` Ответ: ```json { "processed": 1, "skipped": 0, "collection_ticket_count": 142 } ``` ### 2.3 Embedding — через OpenRouter API GPU в K8s не будет, embedding тоже через OpenRouter. OpenRouter поддерживает несколько embedding-моделей: `text-embedding-3-small`, `text-embedding-3-large` и др. Выбор уточнить при старте — нужно тестировать качество на русском языке. ```python # services/embedding.py class OpenRouterEmbeddings: def __init__(self, api_key: str, model: str = "text-embedding-3-small"): self.client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=api_key) self.model = model async def embed(self, texts: list[str]) -> list[list[float]]: response = await self.client.embeddings.create( model=self.model, input=texts ) return [item.embedding for item in response.data] ``` **Альтернатива:** если качество OpenRouter embedding не устроит — вынести E5-large в отдельный микросервис с CPU (batch inference, ~5-10 текстов/сек) или использовать другой API (Jina, Voyage, Cohere). --- ## Этап 3 — Поиск и генерация ``` POST /api/search Authorization: Bearer { "collection_id": "uuid", "query": "не могу загрузить фото в приложение", "generate_answer": true } ``` Ответ: ```json { "answer": "Проверьте настройки камеры...", "sources": [ { "ticket_id": "ext-001", "score": 3.51, "category": "Сбой" } ] } ``` ### Внутренняя логика search: 1. E5-large: query → вектор 2. Qdrant: search по tenant_id + collection_id → 20 кандидатов 3. Cross-encoder (CPU): rerank query vs full_text всех 20 4. Взять топ-5 5. Если `generate_answer=true` → LLM API (OpenAI/GigaChat) с системным промптом 6. Вернуть ответ + источники ### LLM провайдер — OpenRouter На этапе разработки — бесплатные модели OpenRouter (deepseek, qwen, gemini-free). Потом платные. Клиент — стандартный OpenAI SDK с `base_url="https://openrouter.ai/api/v1"`. ```python # services/llm.py class OpenRouterLLM: def __init__(self, api_key: str, model: str = "deepseek/deepseek-chat"): self.client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=api_key) self.model = model async def generate(self, system_prompt: str, context: str, query: str) -> str: response = await self.client.chat.completions.create( model=self.model, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": f"Контекст:\n{context}\n\nВопрос:\n{query}"} ] ) return response.choices[0].message.content ``` **Модель по умолчанию:** уточнить после тестов. Варианты: `deepseek/deepseek-chat` (дёшево, хорошо для RAG), `qwen/qwen-2.5-7b-instruct` (бесплатно), `google/gemini-2.0-flash-free` (бесплатно). Системный промпт — тот же, что сейчас в app.py. --- ## Этап 4 — Фронтенд SPA на React (или Vue, решать тебе): **Страницы:** - `/login`, `/register` - `/dashboard` — список коллекций, статистика - `/collections/:id` — просмотр тикетов, загрузка файла, поиск - `/settings` — API ключ, профиль **Чат-интерфейс поиска:** ``` ┌─────────────────────┐ │ Вопрос: │ │ [________________] │ │ [Найти] │ ├─────────────────────┤ │ Ответ ассистента │ │ ─────────────────── │ │ Текст... │ │ │ │ 📄 Найденные │ │ Тикет №123 │ │ Тикет №456 │ └─────────────────────┘ ``` **Технологии:** React + Vite + Tailwind или просто Jinja2 + HTMX для MVP. --- ## Этап 5 — Инфраструктура ### docker-compose (локальная разработка) ```yaml services: postgres: redis: qdrant: backend: build: ./backend env: - OPENROUTER_API_KEY=${OPENROUTER_API_KEY} - DATABASE_URL=postgresql://... frontend: build: ./frontend env: ... worker: # Celery — парсинг, векторизация, upsert build: ./backend command: celery -A app.workers worker ``` ### K8s (Helm-чарт `helm/support-bot-saas/`) ``` Deployments: ├── backend — FastAPI (2-3 реплики) ├── worker — Celery (1-2 реплики) ├── frontend — Nginx + SPA └── cronjob — очистка старых данных StatefulSets: ├── postgres ├── redis └── qdrant Ingress: api.domain.com, app.domain.com ``` ### CI/CD (GitHub Actions) ``` push → lint + test → build Docker → push to registry → deploy to k8s ``` --- ## Этап 6 — После запуска 1. **Мониторинг:** Prometheus + Grafana (latency поиска, ошибки LLM, кол-во тикетов) 2. **Биллинг:** Stripe / YooKassa — free (1 коллекция, 1000 тикетов), pro (10 коллекций, 100k тикетов) 3. **Документация:** OpenAPI (Swagger у FastAPI из коробки), README по интеграции 4. **Rate limiting:** slowapi — для free-тарифа 10 запросов/мин, для pro — без лимита --- ## Принятые решения | Вопрос | Решение | |--------|---------| | LLM провайдер | OpenRouter (бесплатные → платные) | | Embedding | OpenRouter API (если качество не устроит — отдельный E5-large микросервис на CPU) | | Фронтенд | React SPA | | Очередь задач | Celery + Redis | | K8s GPU | Нет, всё через API | | Тарифы | Free / Pro / Enterprise | ## Открытые вопросы 1. **Модель для embedding через OpenRouter** — нужно протестировать `text-embedding-3-small/large` на русском, сравнить с E5-large. 2. **LLM модель по умолчанию** — deepseek/qwen/gemini-free, решить после тестов. 3. **Регионы / 152-ФЗ** — если целевая аудитория в РФ, нужен GigaChat как опция и хранение в РФ.