From cca3457f52b3f6ce55f1b1fe637f694682db9721 Mon Sep 17 00:00:00 2001 From: ed0ss Date: Mon, 29 Jun 2026 15:57:30 +0300 Subject: [PATCH] docs, proxy support, k8s fixes, test scripts --- backend/app/config.py | 4 + backend/app/services/llm.py | 18 +- backend/app/services/vector_store.py | 30 +-- docs/API.md | 303 +++++++++++++++++++++++++++ docs/DEPLOY_DOCKER.md | 115 ++++++++++ docs/DEPLOY_KUBERNETES.md | 182 ++++++++++++++++ k8s/01-postgres.yaml | 2 +- k8s/02-redis.yaml | 2 +- k8s/03-qdrant.yaml | 2 +- k8s/04-backend.yaml | 15 ++ k8s/05-frontend.yaml | 2 + k8s/host-fix-pod.yaml | 23 ++ scripts/script.py | 12 ++ scripts/test_api.sh | 56 +++++ 14 files changed, 746 insertions(+), 20 deletions(-) create mode 100644 docs/API.md create mode 100644 docs/DEPLOY_DOCKER.md create mode 100644 docs/DEPLOY_KUBERNETES.md create mode 100644 k8s/host-fix-pod.yaml create mode 100644 scripts/script.py create mode 100644 scripts/test_api.sh diff --git a/backend/app/config.py b/backend/app/config.py index 60b0e7d..0574354 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -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 diff --git a/backend/app/services/llm.py b/backend/app/services/llm.py index f5b54eb..37efeac 100644 --- a/backend/app/services/llm.py +++ b/backend/app/services/llm.py @@ -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 diff --git a/backend/app/services/vector_store.py b/backend/app/services/vector_store.py index bf93956..8facd66 100644 --- a/backend/app/services/vector_store.py +++ b/backend/app/services/vector_store.py @@ -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]]): diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..38920c5 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,303 @@ +# API + +Базовый URL: `http:///api/v1` + +Авторизация: `Authorization: Bearer ` + +> Получить 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 " \ + -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 "}, + 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=` + +Загрузка массива тикетов в коллекцию. Дубликаты по `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=' \ + -H "Authorization: Bearer " \ + -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"} +``` + +Не требует авторизации. diff --git a/docs/DEPLOY_DOCKER.md b/docs/DEPLOY_DOCKER.md new file mode 100644 index 0000000..f00e950 --- /dev/null +++ b/docs/DEPLOY_DOCKER.md @@ -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 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`. diff --git a/docs/DEPLOY_KUBERNETES.md b/docs/DEPLOY_KUBERNETES.md new file mode 100644 index 0000000..4b8b23a --- /dev/null +++ b/docs/DEPLOY_KUBERNETES.md @@ -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= \ + --docker-username= \ + --docker-password= \ + -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:///api/health +# {"status":"ok"} + +curl http:/// +# HTML страница +``` + +## 4. Обновление backend + +После изменения кода или `.env`: + +```bash +# 1. Собрать образ +docker build -t /backend:latest ./backend +docker push /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 +``` diff --git a/k8s/01-postgres.yaml b/k8s/01-postgres.yaml index d4b3175..72c83ad 100644 --- a/k8s/01-postgres.yaml +++ b/k8s/01-postgres.yaml @@ -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 diff --git a/k8s/02-redis.yaml b/k8s/02-redis.yaml index 5082215..878722b 100644 --- a/k8s/02-redis.yaml +++ b/k8s/02-redis.yaml @@ -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 diff --git a/k8s/03-qdrant.yaml b/k8s/03-qdrant.yaml index af17625..271caef 100644 --- a/k8s/03-qdrant.yaml +++ b/k8s/03-qdrant.yaml @@ -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 diff --git a/k8s/04-backend.yaml b/k8s/04-backend.yaml index 4e36e9d..23256b7 100644 --- a/k8s/04-backend.yaml +++ b/k8s/04-backend.yaml @@ -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 diff --git a/k8s/05-frontend.yaml b/k8s/05-frontend.yaml index 81b7b00..7a95753 100644 --- a/k8s/05-frontend.yaml +++ b/k8s/05-frontend.yaml @@ -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 diff --git a/k8s/host-fix-pod.yaml b/k8s/host-fix-pod.yaml new file mode 100644 index 0000000..2065b72 --- /dev/null +++ b/k8s/host-fix-pod.yaml @@ -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 diff --git a/scripts/script.py b/scripts/script.py new file mode 100644 index 0000000..c9b9324 --- /dev/null +++ b/scripts/script.py @@ -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"])) diff --git a/scripts/test_api.sh b/scripts/test_api.sh new file mode 100644 index 0000000..f98ae18 --- /dev/null +++ b/scripts/test_api.sh @@ -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 " + 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