# План: 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 <api_key>
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 <api_key>
{
  "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 как опция и хранение в РФ.
