docs, proxy support, k8s fixes, test scripts
This commit is contained in:
parent
696c555740
commit
cca3457f52
14 changed files with 746 additions and 20 deletions
|
|
@ -14,6 +14,10 @@ class Settings(BaseSettings):
|
|||
openrouter_embedding_model: str = "nvidia/llama-nemotron-embed-vl-1b-v2:free"
|
||||
openrouter_llm_model: str = "openrouter/owl-alpha"
|
||||
|
||||
proxy_url: str = ""
|
||||
proxy_login: str = ""
|
||||
proxy_pass: str = ""
|
||||
|
||||
jwt_secret: str = "change-me-in-production"
|
||||
jwt_algorithm: str = "HS256"
|
||||
jwt_expire_minutes: int = 1440
|
||||
|
|
|
|||
|
|
@ -17,14 +17,22 @@ _client: httpx.AsyncClient | None = None
|
|||
def get_client() -> httpx.AsyncClient:
|
||||
global _client
|
||||
if _client is None:
|
||||
_client = httpx.AsyncClient(
|
||||
base_url=OPENROUTER_BASE,
|
||||
headers={
|
||||
kwargs: dict = {
|
||||
"base_url": OPENROUTER_BASE,
|
||||
"headers": {
|
||||
"Authorization": f"Bearer {settings.openrouter_api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
timeout=60,
|
||||
)
|
||||
"timeout": 60,
|
||||
}
|
||||
if settings.proxy_url:
|
||||
proxy = settings.proxy_url
|
||||
if not proxy.startswith(("http://", "https://")):
|
||||
proxy = "http://" + proxy
|
||||
if settings.proxy_login and settings.proxy_pass:
|
||||
proxy = proxy.replace("://", f"://{settings.proxy_login}:{settings.proxy_pass}@")
|
||||
kwargs["proxy"] = proxy
|
||||
_client = httpx.AsyncClient(**kwargs)
|
||||
return _client
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from qdrant_client.http.models import (
|
|||
VectorParams,
|
||||
Distance,
|
||||
)
|
||||
from qdrant_client.http.exceptions import UnexpectedResponse
|
||||
|
||||
from app.config import settings
|
||||
|
||||
|
|
@ -52,18 +53,23 @@ def _point_id(tenant_id: str, ticket_id: str) -> int:
|
|||
|
||||
|
||||
def get_existing_ticket_ids(tenant_id: str, collection_id: str) -> set[str]:
|
||||
scroll_filter = Filter(must=[
|
||||
FieldCondition(key="tenant_id", match=MatchValue(value=tenant_id)),
|
||||
FieldCondition(key="collection_id", match=MatchValue(value=collection_id)),
|
||||
])
|
||||
points, _ = client.scroll(
|
||||
collection_name=COLLECTION_NAME,
|
||||
limit=10000,
|
||||
scroll_filter=scroll_filter,
|
||||
with_payload=True,
|
||||
with_vectors=False,
|
||||
)
|
||||
return {p.payload["ticket_id"] for p in points if "ticket_id" in (p.payload or {})}
|
||||
try:
|
||||
scroll_filter = Filter(must=[
|
||||
FieldCondition(key="tenant_id", match=MatchValue(value=tenant_id)),
|
||||
FieldCondition(key="collection_id", match=MatchValue(value=collection_id)),
|
||||
])
|
||||
points, _ = client.scroll(
|
||||
collection_name=COLLECTION_NAME,
|
||||
limit=10000,
|
||||
scroll_filter=scroll_filter,
|
||||
with_payload=True,
|
||||
with_vectors=False,
|
||||
)
|
||||
return {p.payload["ticket_id"] for p in points if "ticket_id" in (p.payload or {})}
|
||||
except UnexpectedResponse as e:
|
||||
if b"doesn't exist" in e.content:
|
||||
return set()
|
||||
raise
|
||||
|
||||
|
||||
async def upsert_chunks(tenant_id: str, chunks: list[dict], vectors: list[list[float]]):
|
||||
|
|
|
|||
303
docs/API.md
Normal file
303
docs/API.md
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
# API
|
||||
|
||||
Базовый URL: `http://<host>/api/v1`
|
||||
|
||||
Авторизация: `Authorization: Bearer <api_key>`
|
||||
|
||||
> Получить API-ключ можно при регистрации через UI (отображается в настройках профиля).
|
||||
|
||||
---
|
||||
|
||||
## 1. Поиск по базе знаний
|
||||
|
||||
`POST /api/v1/search`
|
||||
|
||||
Поиск релевантных тикетов в коллекции с опциональной генерацией ответа.
|
||||
|
||||
### Request body
|
||||
|
||||
```json
|
||||
{
|
||||
"collection_id": "83f64913-9cb6-495e-984a-cbae31eacf54",
|
||||
"query": "не могу начать смену в приложении",
|
||||
"generate_answer": true
|
||||
}
|
||||
```
|
||||
|
||||
| Поле | Тип | По умолчанию | Описание |
|
||||
|------------------|---------|--------------|------------------------------------|
|
||||
| `collection_id` | string | — | ID коллекции |
|
||||
| `query` | string | — | Поисковый запрос |
|
||||
| `generate_answer`| boolean | `true` | Сгенерировать ответ LLM или нет |
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"answer": "Проверьте подключение к интернету...",
|
||||
"sources": [
|
||||
{
|
||||
"ticket_id": "12345",
|
||||
"score": 0.5885,
|
||||
"category": "техподдержка",
|
||||
"full_text": "Категория: техподдержка\nПроблема: ...\nРешение: ..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|----------|----------------------------|----------------------------------------|
|
||||
| `answer` | string или null | Сгенерированный ответ LLM или null |
|
||||
| `sources`| array of SourceItem | Найденные тикеты (от 0 до 5) |
|
||||
|
||||
### SourceItem
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------------|---------|-----------------------------|
|
||||
| `ticket_id`| string | ID тикета |
|
||||
| `score` | float | Релевантность (0..1) |
|
||||
| `category` | string | Категория тикета |
|
||||
| `full_text`| string | Полный текст решения |
|
||||
|
||||
### Пример (curl)
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://sovet.itoservice.ru/api/v1/search \
|
||||
-H "Authorization: Bearer <api-key>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"collection_id": "83f64913-9cb6-495e-984a-cbae31eacf54",
|
||||
"query": "не могу начать смену в приложении",
|
||||
"generate_answer": true
|
||||
}'
|
||||
```
|
||||
|
||||
### Пример (Python)
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
resp = requests.post(
|
||||
"http://sovet.itoservice.ru/api/v1/search",
|
||||
headers={"Authorization": "Bearer <api-key>"},
|
||||
json={
|
||||
"collection_id": "83f64913-9cb6-495e-984a-cbae31eacf54",
|
||||
"query": "не могу начать смену в приложении",
|
||||
"generate_answer": True,
|
||||
},
|
||||
)
|
||||
data = resp.json()
|
||||
print(data["answer"])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Загрузка тикетов
|
||||
|
||||
`POST /api/v1/tickets?collection_id=<uuid>`
|
||||
|
||||
Загрузка массива тикетов в коллекцию. Дубликаты по `ticket_id` автоматически пропускаются.
|
||||
|
||||
### Request body
|
||||
|
||||
```json
|
||||
{
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": 12345,
|
||||
"category": "техподдержка",
|
||||
"description": "Не работает принтер",
|
||||
"client": "Иван",
|
||||
"messages": [
|
||||
{"role": "user", "text": "У меня не печатает принтер"},
|
||||
{"role": "assistant", "text": "Проверьте подключение кабеля USB к компьютеру..."}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Поле | Тип | По умолчанию | Описание |
|
||||
|---------------|----------------------|--------------|-----------------------------|
|
||||
| `ticket_id` | int или string | — | Уникальный ID тикета |
|
||||
| `category` | string | `""` | Категория проблемы |
|
||||
| `description` | string | `""` | Краткое описание проблемы |
|
||||
| `client` | string | `""` | Имя клиента |
|
||||
| `messages` | array of Message | `[]` | Переписка (user + assistant)|
|
||||
|
||||
### Message
|
||||
|
||||
```json
|
||||
{"role": "user|assistant", "text": "текст сообщения"}
|
||||
```
|
||||
|
||||
- `role` — `"user"` или `"assistant"`. Сообщения `"assistant"` используются как решение.
|
||||
- `text` — текст сообщения. Решение должно быть длиннее 100 символов и не заканчиваться вопросительным знаком.
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"processed": 1,
|
||||
"skipped": 0,
|
||||
"collection_ticket_count": 100
|
||||
}
|
||||
```
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------------------------|-------|-----------------------------------|
|
||||
| `processed` | int | Количество обработанных тикетов |
|
||||
| `skipped` | int | Пропущено (дубликат или нет решения)|
|
||||
| `collection_ticket_count`| int | Всего тикетов в коллекции |
|
||||
|
||||
### Почему тикет может быть пропущен (skipped)
|
||||
|
||||
1. **Дубликат** — `ticket_id` уже существует в коллекции
|
||||
2. **Нет решения** — в `messages` нет сообщения от `assistant` длиннее 100 символов
|
||||
3. **Решение — вопрос** — последнее сообщение `assistant` заканчивается на `?` и короче 150 символов
|
||||
4. **Решение — заглушка** — текст содержит фразы вроде «уточните», «приложите скрин», «позвоните нам» и т.п.
|
||||
|
||||
### Пример (curl)
|
||||
|
||||
```bash
|
||||
curl -s -X POST 'http://sovet.itoservice.ru/api/v1/tickets?collection_id=<uuid>' \
|
||||
-H "Authorization: Bearer <api-key>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"tickets": [{"ticket_id": 1, "description": "Проблема", "messages": [{"role": "user", "text": "вопрос"}, {"role": "assistant", "text": "Проверьте соединение и перезагрузите устройство, затем повторите попытку. Если не помогает – обратитесь к администратору системы."}]}]}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Список коллекций
|
||||
|
||||
`GET /api/v1/collections`
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "83f64913-9cb6-495e-984a-cbae31eacf54",
|
||||
"user_id": "30939310-e3a3-4993-b1ac-10c81b751843",
|
||||
"name": "OFRS",
|
||||
"ticket_count": 2898,
|
||||
"created_at": "2026-06-29T11:45:27.866413Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Профиль пользователя
|
||||
|
||||
`GET /api/v1/me`
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "30939310-e3a3-4993-b1ac-10c81b751843",
|
||||
"email": "user@example.com",
|
||||
"api_key": "abc123...",
|
||||
"plan": "pro",
|
||||
"created_at": "2026-06-29T11:45:27.866413Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Сброс API-ключа
|
||||
|
||||
`PATCH /api/v1/me/api-key`
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"api_key": "новый-ключ-32-символа"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. JWT-эндпоинты (для UI)
|
||||
|
||||
Эти эндпоинты требуют JWT-токен (получается через `/api/login`), а не API-ключ.
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|-------|-------------------------------|------------------------------|
|
||||
| POST | `/api/register` | Регистрация |
|
||||
| POST | `/api/login` | Вход (возвращает JWT) |
|
||||
| GET | `/api/me` | Профиль |
|
||||
| POST | `/api/collections` | Создать коллекцию |
|
||||
| GET | `/api/collections` | Список коллекций |
|
||||
| GET | `/api/collections/{id}` | Информация о коллекции |
|
||||
| DELETE| `/api/collections/{id}` | Удалить коллекцию |
|
||||
| POST | `/api/collections/{id}/tickets` | Загрузить тикеты (JSON)|
|
||||
| POST | `/api/collections/{id}/tickets/upload`| Загрузить тикеты (файл)|
|
||||
| POST | `/api/search` | Поиск |
|
||||
|
||||
### Регистрация
|
||||
|
||||
```json
|
||||
POST /api/register
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "secret123",
|
||||
"plan": "free"
|
||||
}
|
||||
→ 201 {"access_token": "jwt...", "token_type": "bearer"}
|
||||
```
|
||||
|
||||
### Логин
|
||||
|
||||
```json
|
||||
POST /api/login
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "secret123"
|
||||
}
|
||||
→ {"access_token": "jwt...", "token_type": "bearer"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Ошибки
|
||||
|
||||
Все ошибки возвращают стандартный формат FastAPI:
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "Collection not found"
|
||||
}
|
||||
```
|
||||
|
||||
HTTP-статусы:
|
||||
- `200` — успех
|
||||
- `201` — создано
|
||||
- `400` — неверный запрос
|
||||
- `401` — неавторизован
|
||||
- `404` — не найдено
|
||||
- `429` — превышен лимит запросов (30 req/min)
|
||||
- `500` — внутренняя ошибка сервера
|
||||
|
||||
---
|
||||
|
||||
## 8. Rate limiting
|
||||
|
||||
30 запросов в минуту на один IP-адрес. При превышении — `429 Too Many Requests`.
|
||||
|
||||
Лимит действует на все эндпоинты, кроме `/api/health`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Health check
|
||||
|
||||
`GET /api/health`
|
||||
|
||||
```json
|
||||
{"status": "ok"}
|
||||
```
|
||||
|
||||
Не требует авторизации.
|
||||
115
docs/DEPLOY_DOCKER.md
Normal file
115
docs/DEPLOY_DOCKER.md
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# Развёртывание через Docker
|
||||
|
||||
## Требования
|
||||
|
||||
- Docker Engine 24+
|
||||
- Docker Compose v2+
|
||||
- Git
|
||||
|
||||
## Состав
|
||||
|
||||
Сервис состоит из 5 компонентов:
|
||||
|
||||
| Компонент | Образ | Назначение |
|
||||
|-------------|------------------------------------|----------------------|
|
||||
| postgres | `postgres:16` | База данных |
|
||||
| redis | `redis:7-alpine` | Кэш / Celery broker |
|
||||
| qdrant | `qdrant/qdrant` | Векторная БД |
|
||||
| backend | сборка из `./backend` | FastAPI приложение |
|
||||
| frontend | сборка из `./frontend` | React SPA (nginx) |
|
||||
|
||||
Опционально — мониторинг (Prometheus + Grafana) из `docker-compose.monitoring.yml`.
|
||||
|
||||
## 1. Клонирование и настройка
|
||||
|
||||
```bash
|
||||
git clone <repo-url> support-bot-saas
|
||||
cd support-bot-saas
|
||||
```
|
||||
|
||||
Создайте файл `.env` в корне проекта:
|
||||
|
||||
```ini
|
||||
OPENROUTER_API_KEY=sk-or-v1-...
|
||||
OPENROUTER_EMBEDDING_MODEL=nvidia/llama-nemotron-embed-vl-1b-v2:free
|
||||
OPENROUTER_LLM_MODEL=openai/gpt-oss-120b:free
|
||||
JWT_SECRET=<случайная строка 32+ символов>
|
||||
|
||||
# Опционально: HTTP-прокси для OpenRouter
|
||||
PROXY_URL=192.168.1.1:3128
|
||||
PROXY_LOGIN=
|
||||
PROXY_PASS=
|
||||
|
||||
# Эти переменные переопределяются в docker-compose.yml для работы по container name:
|
||||
# DATABASE_URL, REDIS_URL, QDRANT_URL — можно не указывать
|
||||
```
|
||||
|
||||
## 2. Запуск
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Проверка:
|
||||
|
||||
```bash
|
||||
docker compose ps
|
||||
curl http://localhost:8080/api/health
|
||||
# {"status":"ok"}
|
||||
```
|
||||
|
||||
Фронтенд будет доступен на `http://localhost:8080`.
|
||||
|
||||
## 3. Мониторинг (опционально)
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.monitoring.yml up -d
|
||||
```
|
||||
|
||||
- Prometheus: `http://localhost:9090`
|
||||
- Grafana: `http://localhost:3001` (admin / admin)
|
||||
|
||||
## 4. Остановка
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
# С удалением томов БД:
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
## 5. Переменные окружения
|
||||
|
||||
| Переменная | По умолчанию | Обязательная |
|
||||
|-------------------------------|---------------------------------------------------|--------------|
|
||||
| `OPENROUTER_API_KEY` | `""` | Да |
|
||||
| `OPENROUTER_EMBEDDING_MODEL` | `nvidia/llama-nemotron-embed-vl-1b-v2:free` | Нет |
|
||||
| `OPENROUTER_LLM_MODEL` | `openrouter/owl-alpha` | Нет |
|
||||
| `JWT_SECRET` | `change-me-in-production` | Да (в проде) |
|
||||
| `DATABASE_URL` | `postgresql+asyncpg://postgres:postgres@localhost:5432/support_bot` | Нет (переопределяется в compose) |
|
||||
| `REDIS_URL` | `redis://localhost:6379/0` | Нет (переопределяется в compose) |
|
||||
| `QDRANT_URL` | `http://localhost:6333` | Нет (переопределяется в compose) |
|
||||
| `PROXY_URL` | `""` | Нет |
|
||||
| `PROXY_LOGIN` | `""` | Нет |
|
||||
| `PROXY_PASS` | `""` | Нет |
|
||||
| `JWT_EXPIRE_MINUTES` | `1440` | Нет |
|
||||
|
||||
## 6. Сборка образов вручную
|
||||
|
||||
```bash
|
||||
# Бэкенд
|
||||
docker build -t support-bot-backend ./backend
|
||||
|
||||
# Фронтенд
|
||||
docker build -t support-bot-frontend ./frontend
|
||||
```
|
||||
|
||||
После сборки замените `build: ./backend` и `build: ./frontend` в `docker-compose.yml` на `image: support-bot-backend` и `image: support-bot-frontend`, либо удалите `build` секции и пересоберите compose.
|
||||
|
||||
## 7. Тома (volumes)
|
||||
|
||||
| Том | Назначение | Размер по умолчанию |
|
||||
|-----------------|----------------------|---------------------|
|
||||
| `pgdata` | Данные PostgreSQL | без ограничения |
|
||||
| `qdrant_storage`| Данные Qdrant | без ограничения |
|
||||
|
||||
Тома создаются автоматически в `docker compose up`.
|
||||
182
docs/DEPLOY_KUBERNETES.md
Normal file
182
docs/DEPLOY_KUBERNETES.md
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
# Развёртывание в Kubernetes
|
||||
|
||||
## Требования
|
||||
|
||||
- Kubernetes 1.28+ (проверено на k0s 1.36.1)
|
||||
- kubectl 1.28+
|
||||
- Доступ к registry с образами backend и frontend
|
||||
|
||||
## Состав manifests
|
||||
|
||||
Все манифесты в `k8s/`. Порядок применения — по префиксу номера.
|
||||
|
||||
| Файл | Ресурсы |
|
||||
|-------------------------|-------------------------------------|
|
||||
| `00-namespace.yaml` | Namespace `support-bot` |
|
||||
| `01-postgres.yaml` | PV, PVC, Deployment, Service (postgres:16-alpine) |
|
||||
| `02-redis.yaml` | PV, PVC, Deployment, Service (redis:7-alpine) |
|
||||
| `03-qdrant.yaml` | PV, PVC, Deployment, Service (qdrant/qdrant) |
|
||||
| `04-backend.yaml` | Deployment, Service (FastAPI) |
|
||||
| `05-frontend.yaml` | Deployment, Service (nginx SPA) |
|
||||
| `06-ingress.yaml` | IngressRoute (Traefik CRD) |
|
||||
| `host-fix-pod.yaml` | Debug pod для отладки узла |
|
||||
|
||||
## 1. Подготовка
|
||||
|
||||
### 1.1 Создание namespace
|
||||
|
||||
```bash
|
||||
kubectl apply -f k8s/00-namespace.yaml
|
||||
```
|
||||
|
||||
### 1.2 Настройка Traefik (Ingress Controller)
|
||||
|
||||
```bash
|
||||
# CRDs и RBAC (однократно)
|
||||
kubectl apply -f https://raw.githubusercontent.com/traefik/traefik/v3.3/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml
|
||||
kubectl apply -f https://raw.githubusercontent.com/traefik/traefik/v3.3/docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml
|
||||
|
||||
# Сам Traefik
|
||||
kubectl apply -f k8s/traefik/01-core.yaml
|
||||
```
|
||||
|
||||
Traefik запускается с `hostNetwork: true` на портах 80/443.
|
||||
|
||||
### 1.3 Настройка Private Registry (если необходимо)
|
||||
|
||||
```bash
|
||||
kubectl create secret docker-registry regcred \
|
||||
--docker-server=<registry> \
|
||||
--docker-username=<user> \
|
||||
--docker-password=<pass> \
|
||||
-n support-bot
|
||||
```
|
||||
|
||||
Secret `regcred` уже указан в `imagePullSecrets` у backend и frontend.
|
||||
|
||||
### 1.4 Secrets
|
||||
|
||||
Секрет `app-env` содержит переменные окружения для backend. Создаётся из `.env`:
|
||||
|
||||
```bash
|
||||
kubectl create secret generic app-env \
|
||||
--from-env-file=.env \
|
||||
-n support-bot
|
||||
```
|
||||
|
||||
Состав ключей в `app-env`:
|
||||
|
||||
| Ключ | Назначение |
|
||||
|------------------------------|--------------------------------------|
|
||||
| `OPENROUTER_API_KEY` | API-ключ OpenRouter |
|
||||
| `OPENROUTER_EMBEDDING_MODEL` | Модель эмбеддингов |
|
||||
| `OPENROUTER_LLM_MODEL` | Модель LLM |
|
||||
| `JWT_SECRET` | Секрет для JWT |
|
||||
| `PROXY_URL` | Адрес HTTP-прокси (опционально) |
|
||||
| `PROXY_LOGIN` | Логин прокси (опционально) |
|
||||
| `PROXY_PASS` | Пароль прокси (опционально) |
|
||||
|
||||
Переменные `DATABASE_URL`, `REDIS_URL`, `QDRANT_URL` заданы в Deployment статически (через container_name).
|
||||
|
||||
## 2. Развёртывание
|
||||
|
||||
### 2.1 Базы данных
|
||||
|
||||
```bash
|
||||
kubectl apply -f k8s/01-postgres.yaml
|
||||
kubectl apply -f k8s/02-redis.yaml
|
||||
kubectl apply -f k8s/03-qdrant.yaml
|
||||
```
|
||||
|
||||
Дождаться запуска:
|
||||
|
||||
```bash
|
||||
kubectl wait --for=condition=ready pod -l app=postgres -n support-bot --timeout=120s
|
||||
kubectl wait --for=condition=ready pod -l app=redis -n support-bot --timeout=60s
|
||||
kubectl wait --for=condition=ready pod -l app=qdrant -n support-bot --timeout=60s
|
||||
```
|
||||
|
||||
### 2.2 Backend
|
||||
|
||||
```bash
|
||||
kubectl apply -f k8s/04-backend.yaml
|
||||
kubectl rollout status deployment/backend -n support-bot
|
||||
```
|
||||
|
||||
Проверка:
|
||||
|
||||
```bash
|
||||
kubectl exec deploy/backend -n support-bot -- curl -s http://localhost:8000/api/health
|
||||
# {"status":"ok"}
|
||||
```
|
||||
|
||||
### 2.3 Frontend
|
||||
|
||||
```bash
|
||||
kubectl apply -f k8s/05-frontend.yaml
|
||||
kubectl rollout status deployment/frontend -n support-bot
|
||||
```
|
||||
|
||||
### 2.4 Ingress
|
||||
|
||||
```bash
|
||||
kubectl apply -f k8s/06-ingress.yaml
|
||||
```
|
||||
|
||||
IngressRoute слушает порт 80 (entrypoint `web`), хост задаётся в манифесте.
|
||||
|
||||
## 3. Проверка
|
||||
|
||||
```bash
|
||||
curl http://<node-ip>/api/health
|
||||
# {"status":"ok"}
|
||||
|
||||
curl http://<node-ip>/
|
||||
# HTML страница
|
||||
```
|
||||
|
||||
## 4. Обновление backend
|
||||
|
||||
После изменения кода или `.env`:
|
||||
|
||||
```bash
|
||||
# 1. Собрать образ
|
||||
docker build -t <registry>/backend:latest ./backend
|
||||
docker push <registry>/backend:latest
|
||||
|
||||
# 2. Обновить секрет (если менялся .env)
|
||||
kubectl delete secret app-env -n support-bot
|
||||
kubectl create secret generic app-env --from-env-file=.env -n support-bot
|
||||
|
||||
# 3. Перекатить под
|
||||
kubectl rollout restart deployment/backend -n support-bot
|
||||
kubectl rollout status deployment/backend -n support-bot
|
||||
```
|
||||
|
||||
## 5. Persistent Volumes
|
||||
|
||||
| Компонент | Размер | Путь на ноде |
|
||||
|-----------|--------|-----------------------------|
|
||||
| postgres | 10Gi | `/mnt/data/postgres` |
|
||||
| redis | 5Gi | `/mnt/data/redis` |
|
||||
| qdrant | 20Gi | `/mnt/data/qdrant` |
|
||||
|
||||
PV привязаны к ноде через `nodeAffinity`. При смене воркера — заменить значение `hostnames` в манифестах.
|
||||
|
||||
## 6. Удаление
|
||||
|
||||
```bash
|
||||
# Удалить всё, кроме PV
|
||||
kubectl delete -f k8s/06-ingress.yaml
|
||||
kubectl delete -f k8s/05-frontend.yaml
|
||||
kubectl delete -f k8s/04-backend.yaml
|
||||
kubectl delete -f k8s/03-qdrant.yaml
|
||||
kubectl delete -f k8s/02-redis.yaml
|
||||
kubectl delete -f k8s/01-postgres.yaml
|
||||
|
||||
# Секреты
|
||||
kubectl delete secret app-env regcred -n support-bot
|
||||
|
||||
# Namespace (удалит всё, включая PV/PVC)
|
||||
kubectl delete namespace support-bot
|
||||
```
|
||||
|
|
@ -17,7 +17,7 @@ spec:
|
|||
- key: kubernetes.io/hostname
|
||||
operator: In
|
||||
values:
|
||||
- worker-172.16.0.5
|
||||
- worker-192.168.123.5
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ spec:
|
|||
- key: kubernetes.io/hostname
|
||||
operator: In
|
||||
values:
|
||||
- worker-172.16.0.5
|
||||
- worker-192.168.123.5
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ spec:
|
|||
- key: kubernetes.io/hostname
|
||||
operator: In
|
||||
values:
|
||||
- worker-172.16.0.5
|
||||
- worker-192.168.123.5
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
|
|
|
|||
|
|
@ -49,6 +49,21 @@ spec:
|
|||
secretKeyRef:
|
||||
name: app-env
|
||||
key: JWT_SECRET
|
||||
- name: PROXY_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: app-env
|
||||
key: PROXY_URL
|
||||
- name: PROXY_LOGIN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: app-env
|
||||
key: PROXY_LOGIN
|
||||
- name: PROXY_PASS
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: app-env
|
||||
key: PROXY_PASS
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ spec:
|
|||
labels:
|
||||
app: frontend
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: regcred
|
||||
containers:
|
||||
- name: frontend
|
||||
image: git.itoservice.ru/dosnav/support-bot-saas/frontend:latest
|
||||
|
|
|
|||
23
k8s/host-fix-pod.yaml
Normal file
23
k8s/host-fix-pod.yaml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: host-fix
|
||||
namespace: support-bot
|
||||
spec:
|
||||
hostPID: true
|
||||
hostNetwork: true
|
||||
containers:
|
||||
- name: host-fix
|
||||
image: cr-internal.twcstorage.ru/k0sproject/calico-node:v3.32.0-0
|
||||
command: ["sleep", "3600"]
|
||||
securityContext:
|
||||
privileged: true
|
||||
runAsUser: 0
|
||||
volumeMounts:
|
||||
- name: host-root
|
||||
mountPath: /host
|
||||
volumes:
|
||||
- name: host-root
|
||||
hostPath:
|
||||
path: /
|
||||
type: Directory
|
||||
12
scripts/script.py
Normal file
12
scripts/script.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import requests, sys
|
||||
|
||||
r = requests.post(
|
||||
"http://sovet.itoservice.ru/api/v1/search",
|
||||
headers={"Authorization": "Bearer 5fd81e60ba17418fa930cdf70504ebb2"},
|
||||
json={"collection_id": "83f64913-9cb6-495e-984a-cbae31eacf54", "query": sys.argv[1], "generate_answer": True},
|
||||
)
|
||||
data = r.json()
|
||||
if data["answer"]:
|
||||
print(data["answer"])
|
||||
else:
|
||||
print("Ошибка генерации ответа (LLM недоступен). Найдено источников:", len(data["sources"]))
|
||||
56
scripts/test_api.sh
Normal file
56
scripts/test_api.sh
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${BASE_URL:-http://sovet.itoservice.ru}"
|
||||
API_KEY="${1:-5fd81e60ba17418fa930cdf70504ebb2}"
|
||||
COLLECTION_ID="${2:-}"
|
||||
|
||||
echo "=== Коллекции ==="
|
||||
COLLECTIONS=$(curl -s -X GET "$BASE_URL/api/v1/collections" \
|
||||
-H "Authorization: Bearer $API_KEY")
|
||||
echo "$COLLECTIONS" | python3 -m json.tool 2>/dev/null || echo "$COLLECTIONS"
|
||||
|
||||
if [ -z "$COLLECTION_ID" ]; then
|
||||
COLLECTION_ID=$(echo "$COLLECTIONS" | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
if data: print(data[0]['id'])
|
||||
else: print('')
|
||||
" 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
if [ -z "$COLLECTION_ID" ]; then
|
||||
echo "Нет коллекций. Создайте через админку, затем: $0 <api_key> <collection_id>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "collection_id=$COLLECTION_ID"
|
||||
|
||||
echo ""
|
||||
echo "=== Загрузка тикета ==="
|
||||
curl -s -X POST "$BASE_URL/api/v1/tickets?collection_id=$COLLECTION_ID" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"tickets": [{
|
||||
"ticket_id": 999,
|
||||
"category": "мобильное приложение",
|
||||
"description": "Не могу начать смену в приложении",
|
||||
"client": "Иван",
|
||||
"messages": [
|
||||
{"role": "user", "text": "Не могу начать смену в приложении, кнопка не активна"},
|
||||
{"role": "assistant", "text": "Проверьте подключение к интернету и перезапустите приложение. Если не помогает, очистите кеш в настройках телефона. Убедитесь, что установлена последняя версия приложения из магазина."}
|
||||
]
|
||||
}]
|
||||
}' | python3 -m json.tool 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "=== Поиск ==="
|
||||
curl -s -X POST "$BASE_URL/api/v1/search" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"collection_id\": \"$COLLECTION_ID\",
|
||||
\"query\": \"не могу начать смену в приложении\",
|
||||
\"generate_answer\": true
|
||||
}" | python3 -m json.tool 2>/dev/null || true
|
||||
Loading…
Add table
Reference in a new issue