docs: обновление README, мёрж feat/rest-api в master
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

- README.md полностью переписан под текущую архитектуру: K8s + FastAPI +
  React + Qdrant + OpenRouter
- Добавлена документация REST API v1 с примерами curl
- Добавлена информация об очистке ПД, дедупликации, архитектуре
- Мёрдж ветки feat/rest-api: новый роутер /api/v1/ с API-ключом
This commit is contained in:
ed0ss 2026-06-25 11:55:55 +03:00
parent 69c2bc114b
commit 5f6ad253de

387
README.md
View file

@ -1,172 +1,301 @@
# RAG-ассистент техподдержки
# Support Bot SaaS — RAG-ассистент техподдержки
Система поиска решений по истории обращений в техподдержку. Основана на RAG (Retrieval-Augmented Generation) — находит похожие решённые тикеты и формирует ответ на их основе.
Система поиска решений по истории обращений в техподдержку. Основана на RAG (Retrieval-Augmented Generation) — находит похожие решённые тикеты в векторной БД и формирует ответ на их основе через LLM.
Развёрнута на Kubernetes (k0s), используются OpenRouter для эмбеддингов и LLM.
---
## Архитектура
```
tickets.json (792 тикета)
Пользователь (браузер)
prepare_data.py ← фильтрация, очистка, нарезка чанков
┌─────────────┐ ┌─────────────┐
│ Traefik │────▶│ Nginx │ (frontend, React SPA)
│ (Ingress) │ └─────────────┘
│ │ ┌─────────────┐
│ │────▶│ FastAPI │ (backend)
└─────────────┘ └──────┬──────┘
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│PostgreSQL│ │ Qdrant │ │ Redis │
│(пользова-│ │(векторная│ │(кэш/сес- │
│ тели, │ │ БД) │ │ сии) │
│коллекции)│ └──────────┘ └──────────┘
└──────────┘
sentence-transformers ← векторизация (intfloat/multilingual-e5-small, GPU CUDA)
chroma_db/ ← векторная база данных (416 чанков)
app.py ← Streamlit-интерфейс
├── вопрос специалиста → embedding → поиск топ-5
│ │
│ ▼
└── контекст → LM Studio (qwen2.5-7b) → ответ
┌──────────────┐
│ OpenRouter │
│(эмбеддинги + │
│ LLM) │
└──────────────┘
```
### Компоненты
| Компонент | Технология |
|-----------|-----------|
| **Фронтенд** | React + Vite + TypeScript, Nginx |
| **Бэкенд** | FastAPI (Python 3.12) + SQLAlchemy async |
| **База данных** | PostgreSQL (пользователи, коллекции) |
| **Векторная БД** | Qdrant (точки с эмбеддингами) |
| **Кэш** | Redis |
| **Эмбеддинги** | `nvidia/llama-nemotron-embed-vl-1b-v2:free` через OpenRouter |
| **LLM** | `poolside/laguna-m.1:free` через OpenRouter |
| **Ingress** | Traefik v3.7 |
| **Оркестрация** | Kubernetes (k0s v1.36.1) |
| **CI** | GitLab CI (Forgejo) |
---
## REST API v1
Аутентификация: `Authorization: Bearer <API-ключ>`
API-ключ можно посмотреть и сбросить на странице **Настройки** в веб-интерфейсе.
**Базовый URL:** `http://api.sovet.itoservice.ru/api/v1`
### Получить информацию об аккаунте
```bash
curl -H "Authorization: Bearer <API_KEY>" http://api.sovet.itoservice.ru/api/v1/me
```
Ответ:
```json
{
"id": "uuid",
"email": "user@example.com",
"api_key": "d469cee785e444cf9abd24a3e709108c",
"plan": "free",
"created_at": "2026-06-20T12:00:00Z"
}
```
### Сбросить API-ключ
```bash
curl -X PATCH -H "Authorization: Bearer <API_KEY>" http://api.sovet.itoservice.ru/api/v1/me/api-key
```
Ответ:
```json
{
"api_key": "новый-ключ"
}
```
### Список коллекций
```bash
curl -H "Authorization: Bearer <API_KEY>" http://api.sovet.itoservice.ru/api/v1/collections
```
Ответ:
```json
[
{
"id": "49a3f88e-...",
"user_id": "uuid",
"name": "OFRS",
"ticket_count": 2901,
"created_at": "2026-06-20T..."
}
]
```
### Загрузить тикеты в коллекцию
```bash
curl -X POST "http://api.sovet.itoservice.ru/api/v1/tickets?collection_id=<ID_КОЛЛЕКЦИИ>" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"tickets": [
{
"ticket_id": "12345",
"category": "Сбой работы",
"description": "Описание проблемы",
"client": "Имя клиента",
"messages": [
{"role": "client", "text": "У меня проблема"},
{"role": "support", "text": "Решение проблемы"}
]
}
]
}'
```
Ответ:
```json
{
"processed": 1,
"skipped": 0,
"collection_ticket_count": 2902
}
```
### Поиск с AI-ответом
```bash
curl -X POST http://api.sovet.itoservice.ru/api/v1/search \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"collection_id": "<ID_КОЛЛЕКЦИИ>",
"query": "не могу начать смену в приложении",
"generate_answer": true
}'
```
Параметры:
- `collection_id` — ID коллекции (обязательно)
- `query` — текст проблемы (обязательно)
- `generate_answer` — true/false, генерировать ответ LLM (опционально, по умолчанию true)
Ответ:
```json
{
"answer": "Добрый день! Для решения проблемы...",
"sources": [
{
"ticket_id": "38446",
"score": 0.514,
"category": "Вопрос по работе мобильного приложения",
"full_text": "Категория: ...\nПроблема: ...\nРешение: ..."
}
]
}
```
---
## Как это работает
### 1. Подготовка данных (`prepare_data.py`)
### 1. Загрузка тикетов
Что делает скрипт:
Пользователь загружает JSON-файл с тикетами через веб-интерфейс или API.
Бэкенд:
- Проверяет дубликаты (по `tenant_id:ticket_id` через md5)
- Очищает персональные данные: телефоны → `[ТЕЛЕФОН]`, email → `[EMAIL]`, имена → `[ИМЯ]`
- Строит чанк: выделяет сообщения поддержки (роли `support/assistant/operator/agent`) как решение
- Фильтрует бесполезные тикеты:
- последнее сообщение поддержки — просьба уточнить / прислать скрин
- слишком короткие (< 100 символов)
- нет описания и нет сообщений клиента
- Векторизует `search_text` (описание + сообщения клиента) через OpenRouter
- Сохраняет точку в Qdrant с полями: `tenant_id`, `collection_id`, `ticket_id`, `category`, `client`, `search_text`, `full_text`
- Читает все файлы `tickets*.json` в папке проекта (сейчас 6 файлов, ~3300 тикетов)
- **Чистит текст**: удаляет цитаты `[q]...[/q]`, ссылки, лишние пробелы
- **Фильтрует**: оставляет только тикеты с реальным решением. Отбрасываются:
- тикеты, где последнее сообщение support — просьба уточнить / прислать скрин
- тикеты без ответа / с закрытием "нет обратной связи"
- слишком короткие сообщения (< 100 символов)
- **Формирует чанки**: каждый тикет = один чанк вида:
```
Категория: ...
Проблема: <описание>
Решение: <все сообщения support>
```
- **Векторизует**: превращает текст в вектор (384 числа) через `intfloat/multilingual-e5-small` на GPU
- **Сохраняет** в ChromaDB (папка `chroma_db/`)
### 2. Поиск и генерация
Результат: 416 чанков с реальными решениями (376 тикетов отфильтровано как бесполезные).
Пользователь вводит вопрос. Система:
1. Векторизует вопрос той же моделью эмбеддингов
2. Ищет 20 похожих точек в Qdrant (фильтр по `tenant_id` + `collection_id`)
3. Сортирует по скорингу (косинусная близость), берёт топ-5
4. Формирует контекст из `full_text` найденных тикетов
5. Отправляет в LLM с системным промптом
6. Возвращает ответ + исходные тикеты
### 2. Поиск и генерация (`app.py`)
### Очистка персональных данных
Специалист вводит проблему клиента. Приложение:
Перед сохранением в Qdrant из текста удаляются:
- Номера телефонов: `+7xxxxxxxxxx`, `8xxxxxxxxxx``[ТЕЛЕФОН]`
- Email-адреса → `[EMAIL]`
- Русские имена в обращениях → `[ИМЯ]`
1. **Векторизует вопрос** той же моделью эмбеддингов
2. **Ищет 5 похожих чанков** в ChromaDB по косинусной близости
3. **Формирует контекст** из найденных тикетов
4. **Отправляет в LM Studio** (чат-модель Qwen2.5-7B) с промптом:
> "Ответь на основе ТОЛЬКО переданного контекста. Не придумывай, не отсылай в другую поддержку"
5. **Показывает ответ** и исходные тикеты для проверки
---
## Установка и запуск
## Разработка
### Требования
- Python 3.11+
- NVIDIA GPU с 8+ GB VRAM (для эмбеддингов и LLM)
- [LM Studio](https://lmstudio.ai/) (для чат-модели)
### Установка
### Локальный запуск бэкенда
```bash
cd backend
python -m venv venv
venv\Scripts\activate # Windows
pip install -r requirements.txt
# Настройки в .env:
# openrouter_api_key=...
# database_url=postgresql+asyncpg://postgres:postgres@localhost:5432/support_bot
# qdrant_url=http://localhost:6333
uvicorn app.main:app --reload
```
Установить PyTorch с CUDA (если ещё не):
```bash
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu124
```
### Подготовка базы знаний
### Локальный запуск фронтенда
```bash
python prepare_data.py
cd frontend
npm install
npm run dev
```
Скрипт создаст папку `chroma_db/` с векторной базой.
### Запуск
### Сборка Docker-образов
```bash
streamlit run app.py --server.headless true
docker build -t git.itoservice.ru/dosnav/support-bot-saas/backend:latest ./backend
docker build -t git.itoservice.ru/dosnav/support-bot-saas/frontend:latest ./frontend
docker push git.itoservice.ru/dosnav/support-bot-saas/backend:latest
docker push git.itoservice.ru/dosnav/support-bot-saas/frontend:latest
```
Открыть в браузере: http://localhost:8501
### Запуск чат-модели
### Деплой в K8s
Для генерации ответов нужна чат-модель в LM Studio:
1. Открыть LM Studio
2. Вкладка Developer → загрузить модель (например, `Qwen2.5-7B-Instruct`)
3. Нажать Start Server
4. API будет доступен на `http://localhost:1234/v1`
```bash
kubectl apply -f k8s/00-namespace.yaml
kubectl apply -f k8s/01-postgres.yaml
kubectl apply -f k8s/02-redis.yaml
kubectl apply -f k8s/03-qdrant.yaml
kubectl apply -f k8s/04-backend.yaml
kubectl apply -f k8s/05-frontend.yaml
kubectl apply -f k8s/traefik/
kubectl apply -f k8s/06-ingress.yaml
```
Без чат-модели приложение работает в режиме поиска — показывает похожие тикеты без генерации ответа.
---
## Структура проекта
```
support-bot/
├── tickets*.json # исходные данные (несколько файлов)
├── prepare_data.py # подготовка и векторизация
├── app.py # Streamlit-интерфейс + RAG
├── requirements.txt # зависимости
├── chroma_db/ # векторная БД (создаётся prepare_data.py)
└── .gitignore
```
## Системные промпты
Главный системный промпт — в файле **`app.py:15`**, переменная `SYSTEM_PROMPT`:
```python
SYSTEM_PROMPT = """Ты — специалист техподдержки. Отвечай клиенту, используя ТОЛЬКО информацию из переданных тикетов (контекст).
Правила:
- Если контекст содержит подходящее решение — напиши ответ своими словами, адаптируя под вопрос
- Если контекст не относится к вопросу — напиши: «Недостаточно информации в истории обращений»
- НЕ придумывай ответы, НЕ используй общие знания
- НЕ говори «обратитесь в службу поддержки» — ты сам и есть поддержка
- Укажи в конце: «Основано на тикете №...»"""
```
Именно этот промпт управляет тем, **как LLM отвечает** на вопрос специалиста. Если нужно изменить стиль ответа, тон, правила или требования к формату — редактировать здесь.
Промпт отправляется в LM Studio как `system`-сообщение при каждом запросе генерации ответа (строка `app.py:120-127`).
### Фильтрация тикетов без решения
Паттерны для отбраковки пустых тикетов — в **`prepare_data.py:19-22`**, переменная `NON_SOLUTION`:
```python
NON_SOLUTION = re.compile(
r"(уточнит|приложите скрин|какая ошибка|с какой проблемой"
r"|нет обратной связи|запрос завершу|открыть его снова"
r"|откройте новый|обратиться в службу поддержки"
r"|свяжитесь с технической"
r"|напишите нам|позвоните нам)", re.I
)
```
Если в последнем сообщении support встречается одно из этих слов — тикет считается бесполезным и не попадает в базу знаний. Можно расширять или уточнять список.
---
## Технологии
| Компонент | Технология |
|-----------|-----------|
| Эмбеддинги | `intfloat/multilingual-e5-small` (384d, GPU) через `sentence-transformers` |
| Векторная БД | ChromaDB |
| Чат-модель | Qwen2.5-7B-Instruct через LM Studio (OpenAI-совместимый API) |
| Фреймворк | LangChain + Streamlit |
| Язык | Python 3.11 |
| GPU | CUDA 12.4+ |
## Git
```bash
git remote add origin https://git.itoservice.ru/dosnav/support-bot.git
git push -u origin master
support-bot-saas/
├── backend/
│ └── app/
│ ├── auth/ # JWT + API-ключ аутентификация
│ ├── models/ # SQLAlchemy модели + Pydantic схемы
│ ├── routers/ # FastAPI роутеры
│ │ ├── auth.py # регистрация, логин, коллекции
│ │ ├── tickets.py # загрузка тикетов
│ │ ├── search.py # поиск (для фронтенда)
│ │ └── api_v1.py # внешний REST API v1
│ ├── services/ # бизнес-логика
│ │ ├── chunking.py # нарезка чанков, очистка ПД
│ │ ├── embedding.py # эмбеддинги через OpenRouter
│ │ ├── vector_store.py# Qdrant: upsert, scroll, search
│ │ ├── reranking.py # сортировка результатов
│ │ └── llm.py # генерация ответа через OpenRouter
│ ├── config.py # настройки из .env
│ ├── database.py # подключение к БД
│ └── main.py # точка входа FastAPI
├── frontend/
│ └── src/
│ ├── api/client.ts # API-клиент
│ ├── pages/ # страницы React
│ │ ├── Login.tsx / Register.tsx
│ │ ├── Dashboard.tsx
│ │ ├── CollectionView.tsx
│ │ └── Settings.tsx
│ └── nginx.conf # конфиг Nginx
├── k8s/ # манифесты Kubernetes
│ ├── 00-namespace.yaml
│ ├── 01-postgres.yaml ... 06-ingress.yaml
│ └── traefik/ # Traefik CRDs + Deployment
├── tickets*.json # тестовые данные
└── README.md
```