diff --git a/README.md b/README.md index cc98155..c141588 100644 --- a/README.md +++ b/README.md @@ -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 ← фильтрация, очистка, нарезка чанков - │ - ▼ -sentence-transformers ← векторизация (intfloat/multilingual-e5-small, GPU CUDA) - │ - ▼ -chroma_db/ ← векторная база данных (416 чанков) - │ - ▼ -app.py ← Streamlit-интерфейс - │ - ├── вопрос специалиста → embedding → поиск топ-5 - │ │ - │ ▼ - └── контекст → LM Studio (qwen2.5-7b) → ответ +┌─────────────┐ ┌─────────────┐ +│ Traefik │────▶│ Nginx │ (frontend, React SPA) +│ (Ingress) │ └─────────────┘ +│ │ ┌─────────────┐ +│ │────▶│ FastAPI │ (backend) +└─────────────┘ └──────┬──────┘ + │ + ┌────────────┼────────────┐ + ▼ ▼ ▼ + ┌──────────┐ ┌──────────┐ ┌──────────┐ + │PostgreSQL│ │ Qdrant │ │ Redis │ + │(пользова-│ │(векторная│ │(кэш/сес- │ + │ тели, │ │ БД) │ │ сии) │ + │коллекции)│ └──────────┘ └──────────┘ + └──────────┘ + │ + ▼ + ┌──────────────┐ + │ 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-ключ можно посмотреть и сбросить на странице **Настройки** в веб-интерфейсе. + +**Базовый URL:** `http://api.sovet.itoservice.ru/api/v1` + +### Получить информацию об аккаунте + +```bash +curl -H "Authorization: Bearer " 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 " http://api.sovet.itoservice.ru/api/v1/me/api-key +``` + +Ответ: +```json +{ + "api_key": "новый-ключ" +} +``` + +### Список коллекций + +```bash +curl -H "Authorization: Bearer " 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=" \ + -H "Authorization: Bearer " \ + -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 " \ + -H "Content-Type: application/json" \ + -d '{ + "collection_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 ```