From a52ca6c672c14605830255686ae5afa25d4b180b Mon Sep 17 00:00:00 2001 From: ShiWarai Date: Sun, 1 Feb 2026 18:59:32 +0300 Subject: [PATCH 01/14] =?UTF-8?q?Style:=20=D1=83=D0=B1=D1=80=D0=B0=D0=BB?= =?UTF-8?q?=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=D0=B8=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/README.md b/README.md index 05d72ba..5975226 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,6 @@ Мини-сервис для классификации голосовых команд (SetFit). Обучает модель на малом датасете и классифицирует текстовые команды. Создан для использования в проекте навыка для Sber Salute. ---- - ## Стек технологий | Категория | Технологии | @@ -21,8 +19,6 @@ | Инфраструктура | Docker | | Разработка | pytest, ruff, httpx | ---- - ## Оглавление | Раздел | Содержание | @@ -37,8 +33,6 @@ | [CI/CD](#cicd) | Пайплайн и ссылка на настройку | | [Лицензия](#лицензия) | MIT | ---- - ## Быстрый старт 1. Создайте `.env` с `HF_TOKEN` и `HF_REPO_ID` ([токен](https://huggingface.co/settings/tokens), [модель](https://huggingface.co/google/embeddinggemma-300M) — принять условия). @@ -52,8 +46,6 @@ Остановка: `docker-compose down`. ---- - ## Установка и запуск ### Требование: Hugging Face @@ -86,8 +78,6 @@ python -m commands_classifier.cli serve Опции: `--host`, `--port`, `--config`. БД создаётся при первом запуске, данные из `data/` или CSV из `config.yaml`. ---- - ## Использование ### CLI @@ -114,8 +104,6 @@ python -m commands_classifier.client load-from-hf-status - **API-клиент:** `CVCApiClient(base_url)` — методы `predict`, `predict_batch`, `embed`, `train`, `get_training_status`, `get_examples`, `add_example`, `delete_example`. - **Библиотека (без сервера):** `CommandsClassifier()` + `load_dataset(path)` → `train(texts, labels)`, `predict(text)`, `save(path)`, `load(path)`. ---- - ## Конфигурация и API ### config.yaml @@ -161,8 +149,6 @@ training: Интерактивная документация: **http://localhost:20001/docs**. Устройство (CPU/CUDA/ROCm) определяется при запуске автоматически. ---- - ## Данные ### Формат датасета @@ -177,8 +163,6 @@ training: `--iterations`, `--epochs`, `--batch-size`, `--learning-rate`. Значения по умолчанию — в `config.yaml` (секция `training`). ---- - ## Разработка ### Тесты и линт @@ -207,8 +191,6 @@ CVC/ └── docs/ # cicd_setup.md и др. ``` ---- - ## CI/CD Пайплайн [.github/workflows/deploy.yml](.github/workflows/deploy.yml): @@ -218,12 +200,8 @@ CVC/ Подробная настройка (self-hosted runner, GPU, секреты): [docs/cicd_setup.md](docs/cicd_setup.md). ---- - ## Лицензия MIT ---- - *Проект создан с использованием нейросетей.* From 21d614366334912d61b2402341aa4077006561cb Mon Sep 17 00:00:00 2001 From: ShiWarai Date: Tue, 3 Feb 2026 16:53:54 +0300 Subject: [PATCH 02/14] =?UTF-8?q?Fix:=20=D0=BE=D0=B3=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=87=D0=B8=D0=BB=20=D0=B4=D0=BE=D1=81=D1=82=D1=83=D0=BF?= =?UTF-8?q?=20=D0=BA=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=D0=B8=D1=84=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=D1=82=D0=BE=D1=80=D1=83=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B1=D0=B5=D0=B7=D0=BE=D0=BF=D0=B0=D1=81=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=83=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B2=20=D0=A2=D0=93=20[retrain]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 18 +++++++++++ data/commands_special_commands.csv | 50 ++++++++++++++++++++++++++++++ data/unknown_base.csv | 4 ++- docker-compose.yml | 2 +- docs/cicd_setup.md | 6 ++++ 5 files changed, 78 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2cb00a2..48e25f9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -246,3 +246,21 @@ jobs: echo "✗ Пайплайн завершился с ошибкой" echo " Проверьте логи выше для деталей" fi + + notify-telegram-on-failure: + name: Notify Telegram on failure + if: failure() + needs: [test, train-and-publish] + runs-on: ubuntu-latest + steps: + - name: Send Telegram notification + uses: appleboy/telegram-action@v1.0.1 + with: + to: ${{ secrets.TELEGRAM_TO }} + token: ${{ secrets.TELEGRAM_TOKEN }} + format: markdown + message: | + *Pipeline failed* + Repo: `${{ github.repository }}` + Branch: `${{ github.ref_name }}` + [View run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) diff --git a/data/commands_special_commands.csv b/data/commands_special_commands.csv index 5397b01..7828fc1 100644 --- a/data/commands_special_commands.csv +++ b/data/commands_special_commands.csv @@ -140,3 +140,53 @@ text,command отвяжи панду панда,unbind пожалуйста отвяжи робота,unbind пожалуйста отвяжи панду,unbind +исправить команду,report_command +исправь команду,report_command +исправьте команду,report_command +исправлять команду,report_command +исправляй команду,report_command +исправляйте команду,report_command +исправишь команду,report_command +исправил бы команду,report_command +исправила бы команду,report_command +исправили бы команду,report_command +надо исправить команду,report_command +нужно исправить команду,report_command +пора исправить команду,report_command +можно исправить команду,report_command +давай исправь команду,report_command +поправить команду,report_command +поправь команду,report_command +поправьте команду,report_command +поправлять команду,report_command +поправляй команду,report_command +поправляйте команду,report_command +поправишь команду,report_command +поправил бы команду,report_command +надо поправить команду,report_command +нужно поправить команду,report_command +давай поправь команду,report_command +пожаловаться на команду,report_command +пожаловаться на неправильную команду,report_command +пожалуюсь на команду,report_command +пожаловался бы на команду,report_command +сообщить об ошибке в команде,report_command +сообщи об ошибке в команде,report_command +сообщите об ошибке в команде,report_command +сообщить об ошибке команды,report_command +сообщи об ошибке команды,report_command +ошибка в команде,report_command +неправильная команда,report_command +команда неправильная,report_command +команда работает неправильно,report_command +команда не та,report_command +не та команда,report_command +изменить команду,report_command +измени команду,report_command +измените команду,report_command +изменять команду,report_command +надо изменить команду,report_command +нужно изменить команду,report_command +давай измени команду,report_command +пожалуйста исправь команду,report_command +пожалуйста поправь команду,report_command diff --git a/data/unknown_base.csv b/data/unknown_base.csv index e533994..385705e 100644 --- a/data/unknown_base.csv +++ b/data/unknown_base.csv @@ -475,4 +475,6 @@ text,command олежи робота,unknown запусти робота,unknown робот запустись,unknown -опусти руку,unknown \ No newline at end of file +опусти руку,unknown +включи колонку,unknown +выключи колонку,unknown \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 32893ff..9bbdd22 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ services: user: "${RUNNER_UID:-0}:${RUNNER_GID:-0}" container_name: cvc-api ports: - - "20001:20001" + - "127.0.0.1:20001:20001" # только localhost + robot-services-network (контейнеры) volumes: - ./commands_classifier:/app/commands_classifier:ro - ./tests:/app/tests:ro diff --git a/docs/cicd_setup.md b/docs/cicd_setup.md index 45d5a78..26b3214 100644 --- a/docs/cicd_setup.md +++ b/docs/cicd_setup.md @@ -62,6 +62,12 @@ cat ~/.cvc_ssh_keys/deploy_key.pub Для переопределения параметров обучения из config.yaml ``` +4. **TELEGRAM_TOKEN**, **TELEGRAM_TO** (опционально) + ``` + Для уведомлений в Telegram при падении пайплайна. + Подробная настройка: см. docs/telegram_notifications.md + ``` + ## Шаг 4: Настройка self-hosted runner на GPU-машине Если вы используете GitHub Actions с self-hosted runner: From 1c5f336e5ac40b1904608a9e58bde9139f821a3c Mon Sep 17 00:00:00 2001 From: ShiWarai Date: Tue, 3 Feb 2026 17:04:06 +0300 Subject: [PATCH 03/14] =?UTF-8?q?Build:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=BE=20=D1=83=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5?= =?UTF-8?q?=20=D1=83=D1=81=D0=BF=D0=B5=D1=88=D0=BD=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=85=D0=BE=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20CD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 48e25f9..43b3353 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -247,6 +247,25 @@ jobs: echo " Проверьте логи выше для деталей" fi + notify-telegram-on-success: + name: Notify Telegram on success + if: success() + needs: [test, train-and-publish] + runs-on: ubuntu-latest + steps: + - name: Send Telegram notification (silent) + uses: appleboy/telegram-action@v1.0.1 + with: + to: ${{ secrets.TELEGRAM_TO }} + token: ${{ secrets.TELEGRAM_TOKEN }} + format: markdown + disable_notification: true + message: | + *Pipeline succeeded* + Repo: `${{ github.repository }}` + Branch: `${{ github.ref_name }}` + [View run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + notify-telegram-on-failure: name: Notify Telegram on failure if: failure() From 04c19510359b0ed71a4b0f23c35a10521c02339f Mon Sep 17 00:00:00 2001 From: ShiWarai Date: Tue, 3 Feb 2026 17:07:35 +0300 Subject: [PATCH 04/14] =?UTF-8?q?Build:=20=D0=B5=D1=89=D1=91=20=D0=BE?= =?UTF-8?q?=D0=B4=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=BF=D1=8B=D1=82=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82=D1=8C=20CD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 43b3353..3fda661 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -249,7 +249,7 @@ jobs: notify-telegram-on-success: name: Notify Telegram on success - if: success() + if: always() && needs.test.result == 'success' && (needs.train-and-publish.result == 'success' || needs.train-and-publish.result == 'skipped') needs: [test, train-and-publish] runs-on: ubuntu-latest steps: @@ -268,7 +268,7 @@ jobs: notify-telegram-on-failure: name: Notify Telegram on failure - if: failure() + if: always() && (needs.test.result == 'failure' || needs.train-and-publish.result == 'failure') needs: [test, train-and-publish] runs-on: ubuntu-latest steps: From c87fd093741c520bd0acc339c96ae0b138919deb Mon Sep 17 00:00:00 2001 From: ShiWarai Date: Tue, 3 Feb 2026 17:11:09 +0300 Subject: [PATCH 05/14] =?UTF-8?q?Build:=20=D0=BF=D0=BE=D0=BF=D1=8B=D1=82?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B2=D1=8B=D0=B7=D0=B2=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 16 ++++++++-------- tests/api/test_health.py | 5 +++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3fda661..c9df105 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -261,10 +261,10 @@ jobs: format: markdown disable_notification: true message: | - *Pipeline succeeded* - Repo: `${{ github.repository }}` - Branch: `${{ github.ref_name }}` - [View run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + *Пайплайн прошёл успешно* + Репо: `${{ github.repository }}` + Ветка: `${{ github.ref_name }}` + [Открыть run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) notify-telegram-on-failure: name: Notify Telegram on failure @@ -279,7 +279,7 @@ jobs: token: ${{ secrets.TELEGRAM_TOKEN }} format: markdown message: | - *Pipeline failed* - Repo: `${{ github.repository }}` - Branch: `${{ github.ref_name }}` - [View run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + *Пайплайн упал* + Репо: `${{ github.repository }}` + Ветка: `${{ github.ref_name }}` + [Открыть run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) diff --git a/tests/api/test_health.py b/tests/api/test_health.py index 116749d..0136b7c 100644 --- a/tests/api/test_health.py +++ b/tests/api/test_health.py @@ -35,3 +35,8 @@ def test_metrics_empty_db(client): response = client.get("/metrics") assert response.status_code == 200 assert response.json()["total_examples"] == 0 + + +def test_ci_fail_notification_remove_me(): + """Временный тест для проверки Telegram-уведомления при падении. Удалить после проверки.""" + assert False, "Проверка fail-уведомления в Telegram — удалить этот тест после проверки" From 8920a7a97365ce26503f01007d5507d3bfbe5b04 Mon Sep 17 00:00:00 2001 From: ShiWarai Date: Tue, 3 Feb 2026 17:15:54 +0300 Subject: [PATCH 06/14] =?UTF-8?q?Build:=20=D1=83=D0=B1=D1=80=D0=B0=D0=BB?= =?UTF-8?q?=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2=D1=8B=D0=B9=20=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D1=8B=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/api/test_health.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/api/test_health.py b/tests/api/test_health.py index 0136b7c..116749d 100644 --- a/tests/api/test_health.py +++ b/tests/api/test_health.py @@ -35,8 +35,3 @@ def test_metrics_empty_db(client): response = client.get("/metrics") assert response.status_code == 200 assert response.json()["total_examples"] == 0 - - -def test_ci_fail_notification_remove_me(): - """Временный тест для проверки Telegram-уведомления при падении. Удалить после проверки.""" - assert False, "Проверка fail-уведомления в Telegram — удалить этот тест после проверки" From 890dd82529e47adc28b60deb827a8055c0aadcec Mon Sep 17 00:00:00 2001 From: ShiWarai Date: Tue, 3 Feb 2026 20:50:25 +0300 Subject: [PATCH 07/14] =?UTF-8?q?Feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=20?= =?UTF-8?q?=D0=BD=D0=B5=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D1=8C=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D1=84=D1=80=D0=B0=D0=B7=20=D0=B8=20=D0=B2=D0=B5=D1=81?= =?UTF-8?q?=D0=B8=D0=BE=D0=BD=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 36 ++++++--- commands_classifier/api/routes/__init__.py | 2 + .../api/routes/command_feedback.py | 75 +++++++++++++++++++ commands_classifier/api/server.py | 15 ++-- commands_classifier/client.py | 67 +++++++++++++---- config.yaml | 5 ++ docker-compose.yml | 4 +- docs/cicd_setup.md | 4 +- tests/api/test_examples.py | 36 ++++----- tests/api/test_health.py | 14 ++-- tests/api/test_predict.py | 30 ++++---- tests/api/test_training.py | 18 ++--- tests/conftest.py | 25 ++++--- tests/integration/test_e2e_docker.py | 27 ++++--- 14 files changed, 250 insertions(+), 108 deletions(-) create mode 100644 commands_classifier/api/routes/command_feedback.py diff --git a/README.md b/README.md index 5975226..8322bc6 100644 --- a/README.md +++ b/README.md @@ -93,15 +93,18 @@ python -m commands_classifier.client examples list python -m commands_classifier.client examples add --text "команда" --command "label" python -m commands_classifier.client examples delete --id 1 python -m commands_classifier.client health +python -m commands_classifier.client metrics +python -m commands_classifier.client reset python -m commands_classifier.client load-from-hf [--repo-id "username/model-name"] python -m commands_classifier.client load-from-hf-status +python -m commands_classifier.client command-feedback # репорт «исправить команду» из RDS-2P-Salute ``` По умолчанию клиент подключается к `http://localhost:20001` (флаг `--url` для другого адреса). ### Python -- **API-клиент:** `CVCApiClient(base_url)` — методы `predict`, `predict_batch`, `embed`, `train`, `get_training_status`, `get_examples`, `add_example`, `delete_example`. +- **API-клиент:** `CVCApiClient(base_url)` — методы `predict`, `predict_batch`, `embed`, `train`, `get_training_status`, `get_examples`, `add_example`, `delete_example`, `health`, `metrics`, `reset`, `load_from_hf`, `get_load_from_hf_status`, `get_command_feedback`. - **Библиотека (без сервера):** `CommandsClassifier()` + `load_dataset(path)` → `train(texts, labels)`, `predict(text)`, `save(path)`, `load(path)`. ## Конфигурация и API @@ -125,6 +128,10 @@ database: path: "db/training_data.db" csv_migration_path: "data" +# Опционально: URL репорта «исправить команду» из RDS-2P-Salute (по умолчанию: rds-2p-salute-app:8000) +# command_feedback: +# url: "http://rds-2p-salute-app:8000/v1/admin/command-feedback" + training: iterations: 20 epochs: 1 @@ -132,20 +139,24 @@ training: learning_rate: 2e-5 ``` -### Эндпоинты +### Эндпоинты (API v1) + +Все ручки версионированы префиксом `/v1`. | Метод | Путь | Описание | |-------|------|----------| -| POST | /embed | Эмбеддинги (TEI) | -| GET | /health | Проверка работоспособности | -| GET | /metrics | Счётчики примеров и статус обучения | -| POST | /predict | Классификация одного текста | -| POST | /predict/batch | Batch классификация | -| POST | /train | Запуск обучения (фоновый) | -| GET | /train/status | Статус обучения | -| GET, POST, DELETE | /examples, /examples/{id} | Обучающие примеры | -| POST | /load_from_hf | Загрузка модели с Hugging Face | -| GET | /load_from_hf/status | Статус загрузки | +| POST | /v1/embed | Эмбеддинги (TEI) | +| GET | /v1/health | Проверка работоспособности | +| GET | /v1/metrics | Счётчики примеров и статус обучения | +| POST | /v1/predict | Классификация одного текста | +| POST | /v1/predict/batch | Batch классификация | +| POST | /v1/train | Запуск обучения (фоновый) | +| GET | /v1/train/status | Статус обучения | +| GET, POST, DELETE | /v1/examples, /v1/examples/{id} | Обучающие примеры | +| POST | /v1/reset | Сброс обучения (удаление модели, пометка примеров как необученных) | +| POST | /v1/load_from_hf | Загрузка модели с Hugging Face | +| GET | /v1/load_from_hf/status | Статус загрузки | +| GET | /v1/command-feedback | Репорт «исправить команду» из RDS-2P-Salute (прокси) | Интерактивная документация: **http://localhost:20001/docs**. Устройство (CPU/CUDA/ROCm) определяется при запуске автоматически. @@ -197,6 +208,7 @@ CVC/ - При каждом push — **тесты** (линт + pytest в Docker). - Job **Train and Publish** — при метке `[retrain]` в сообщении коммита или при ручном запуске (Actions → Run workflow). Секреты: `HF_TOKEN`, `HF_REPO_ID`. +- **Уведомления в Telegram** при успешной и неуспешной сборке (опционально: секреты `TELEGRAM_TOKEN`, `TELEGRAM_TO`). Подробнее: [docs/telegram_notifications.md](docs/telegram_notifications.md). Подробная настройка (self-hosted runner, GPU, секреты): [docs/cicd_setup.md](docs/cicd_setup.md). diff --git a/commands_classifier/api/routes/__init__.py b/commands_classifier/api/routes/__init__.py index d02dfd5..ed6884e 100644 --- a/commands_classifier/api/routes/__init__.py +++ b/commands_classifier/api/routes/__init__.py @@ -1,5 +1,6 @@ """Routes для API сервера CVC.""" +from commands_classifier.api.routes.command_feedback import router as command_feedback_router from commands_classifier.api.routes.examples import router as examples_router from commands_classifier.api.routes.health import router as health_router from commands_classifier.api.routes.load_from_hf import router as load_from_hf_router @@ -12,4 +13,5 @@ "examples_router", "load_from_hf_router", "health_router", + "command_feedback_router", ] diff --git a/commands_classifier/api/routes/command_feedback.py b/commands_classifier/api/routes/command_feedback.py new file mode 100644 index 0000000..5968212 --- /dev/null +++ b/commands_classifier/api/routes/command_feedback.py @@ -0,0 +1,75 @@ +"""Эндпоинт для загрузки репорта «исправить команду» из RDS-2P-Salute.""" + +import logging +from typing import Any, List + +import requests +from fastapi import APIRouter, HTTPException + +from commands_classifier.api.state import get_config + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=["command-feedback"]) + +# URL по умолчанию (внутри Docker-сети robot-services-network) +DEFAULT_COMMAND_FEEDBACK_URL = "http://rds-2p-salute-app:8000/v1/admin/command-feedback" +REQUEST_TIMEOUT = 30 + + +@router.get("/command-feedback") +def get_command_feedback() -> List[dict]: + """ + Загружает записи обратной связи по командам из приложения RDS-2P-Salute. + + Эндпоинт доступен только из локальной/внутренней сети на стороне RDS; + при запросе с публичного IP RDS вернёт 403. CVC проксирует ответ как есть. + """ + config = get_config() + url = ( + config.get("command_feedback", {}).get("url") + or config.get("rds", {}).get("command_feedback_url") + or DEFAULT_COMMAND_FEEDBACK_URL + ).strip() + + if not url: + raise HTTPException( + status_code=503, + detail="command_feedback.url не задан в конфигурации", + ) + + try: + resp = requests.get(url, timeout=REQUEST_TIMEOUT) + resp.raise_for_status() + data: Any = resp.json() + if not isinstance(data, list): + raise HTTPException( + status_code=502, + detail="Ответ RDS не является массивом", + ) + return data + except requests.exceptions.Timeout: + logger.warning("Timeout при запросе command-feedback: %s", url) + raise HTTPException( + status_code=504, + detail="Таймаут при обращении к сервису RDS", + ) + except requests.exceptions.ConnectionError as e: + logger.warning("Ошибка соединения с RDS: %s", e) + raise HTTPException( + status_code=503, + detail="Сервис RDS недоступен (проверьте сеть и имя хоста)", + ) + except requests.exceptions.HTTPError as e: + if e.response is not None and e.response.status_code == 403: + raise HTTPException( + status_code=502, + detail="RDS вернул 403 Forbidden (доступ только из внутренней сети)", + ) + raise HTTPException( + status_code=502, + detail=f"Ошибка RDS: {e.response.status_code if e.response else str(e)}", + ) + except ValueError as e: + logger.warning("Невалидный JSON от RDS: %s", e) + raise HTTPException(status_code=502, detail="Невалидный JSON в ответе RDS") diff --git a/commands_classifier/api/server.py b/commands_classifier/api/server.py index 773169f..71b23c6 100644 --- a/commands_classifier/api/server.py +++ b/commands_classifier/api/server.py @@ -10,6 +10,7 @@ from commands_classifier import db from commands_classifier.api.routes import ( + command_feedback_router, examples_router, health_router, load_from_hf_router, @@ -133,9 +134,11 @@ async def lifespan(app: FastAPI): title="CVC API", description="API для классификации голосовых команд", lifespan=lifespan ) -# Подключаем роутеры -app.include_router(predict_router) -app.include_router(training_router) -app.include_router(examples_router) -app.include_router(load_from_hf_router) -app.include_router(health_router) +# Подключаем роутеры с версионированием API +API_V1_PREFIX = "/v1" +app.include_router(predict_router, prefix=API_V1_PREFIX) +app.include_router(training_router, prefix=API_V1_PREFIX) +app.include_router(examples_router, prefix=API_V1_PREFIX) +app.include_router(load_from_hf_router, prefix=API_V1_PREFIX) +app.include_router(health_router, prefix=API_V1_PREFIX) +app.include_router(command_feedback_router, prefix=API_V1_PREFIX) diff --git a/commands_classifier/client.py b/commands_classifier/client.py index f664e1c..02ea419 100644 --- a/commands_classifier/client.py +++ b/commands_classifier/client.py @@ -16,7 +16,7 @@ def __init__(self, base_url: str = "http://localhost:20001", use_proxy: bool = F Инициализирует клиент. Args: - base_url: Базовый URL API сервера (по умолчанию: http://localhost:20001) + base_url: Базовый URL сервера (по умолчанию: http://localhost:20001) use_proxy: Использовать ли системный прокси (по умолчанию: False) """ self.base_url = base_url.rstrip("/") @@ -40,7 +40,7 @@ def predict(self, text: str, return_confidence: bool = False) -> dict: Результат классификации """ response = self.session.post( - f"{self.base_url}/predict", json={"text": text, "return_confidence": return_confidence} + f"{self.base_url}/v1/predict", json={"text": text, "return_confidence": return_confidence} ) response.raise_for_status() return response.json() @@ -57,7 +57,7 @@ def predict_batch(self, texts: list, return_confidence: bool = False) -> dict: Результаты классификации """ response = self.session.post( - f"{self.base_url}/predict/batch", + f"{self.base_url}/v1/predict/batch", json={"texts": texts, "return_confidence": return_confidence}, ) response.raise_for_status() @@ -73,7 +73,7 @@ def embed(self, texts: list) -> dict: Returns: Эмбеддинги """ - response = self.session.post(f"{self.base_url}/embed", json={"inputs": texts}) + response = self.session.post(f"{self.base_url}/v1/embed", json={"inputs": texts}) response.raise_for_status() return response.json() @@ -107,19 +107,19 @@ def train( if learning_rate is not None: payload["learning_rate"] = learning_rate - response = self.session.post(f"{self.base_url}/train", json=payload) + response = self.session.post(f"{self.base_url}/v1/train", json=payload) response.raise_for_status() return response.json() def get_training_status(self) -> dict: """Получает статус обучения.""" - response = self.session.get(f"{self.base_url}/train/status") + response = self.session.get(f"{self.base_url}/v1/train/status") response.raise_for_status() return response.json() def get_examples(self) -> list: """Получает все примеры.""" - response = self.session.get(f"{self.base_url}/examples") + response = self.session.get(f"{self.base_url}/v1/examples") response.raise_for_status() return response.json() @@ -135,7 +135,7 @@ def add_example(self, text: str, command: str) -> dict: Созданный пример """ response = self.session.post( - f"{self.base_url}/examples", json={"text": text, "command": command} + f"{self.base_url}/v1/examples", json={"text": text, "command": command} ) response.raise_for_status() return response.json() @@ -150,7 +150,7 @@ def delete_example(self, example_id: int) -> dict: Returns: Результат удаления """ - response = self.session.delete(f"{self.base_url}/examples/{example_id}") + response = self.session.delete(f"{self.base_url}/v1/examples/{example_id}") response.raise_for_status() return response.json() @@ -164,19 +164,19 @@ def get_example(self, example_id: int) -> dict: Returns: Пример """ - response = self.session.get(f"{self.base_url}/examples/{example_id}") + response = self.session.get(f"{self.base_url}/v1/examples/{example_id}") response.raise_for_status() return response.json() def health(self) -> dict: """Проверяет работоспособность сервера.""" - response = self.session.get(f"{self.base_url}/health") + response = self.session.get(f"{self.base_url}/v1/health") response.raise_for_status() return response.json() def metrics(self) -> dict: """Получает метрики сервера.""" - response = self.session.get(f"{self.base_url}/metrics") + response = self.session.get(f"{self.base_url}/v1/metrics") response.raise_for_status() return response.json() @@ -189,7 +189,7 @@ def reset(self) -> dict: Returns: Результат сброса (reset_examples, model_deleted) """ - response = self.session.post(f"{self.base_url}/reset") + response = self.session.post(f"{self.base_url}/v1/reset") response.raise_for_status() return response.json() @@ -211,7 +211,7 @@ def load_from_hf(self, repo_id: Optional[str] = None, local_dir: Optional[str] = if local_dir: payload["local_dir"] = local_dir - response = self.session.post(f"{self.base_url}/load_from_hf", json=payload) + response = self.session.post(f"{self.base_url}/v1/load_from_hf", json=payload) response.raise_for_status() return response.json() @@ -222,7 +222,18 @@ def get_load_from_hf_status(self) -> dict: Returns: Статус загрузки (load_id, status, progress, local_path, error) """ - response = self.session.get(f"{self.base_url}/load_from_hf/status") + response = self.session.get(f"{self.base_url}/v1/load_from_hf/status") + response.raise_for_status() + return response.json() + + def get_command_feedback(self) -> list: + """ + Загружает репорт «исправить команду» из сервиса RDS-2P-Salute через CVC. + + Returns: + Список записей обратной связи (user_utterance, classified_function, created_at, ...) + """ + response = self.session.get(f"{self.base_url}/v1/command-feedback") response.raise_for_status() return response.json() @@ -374,7 +385,7 @@ def main(): "--url", type=str, default="http://localhost:20001", - help="URL API сервера (по умолчанию: http://localhost:20001)", + help="URL сервера (по умолчанию: http://localhost:20001)", ) subparsers = parser.add_subparsers(dest="command", help="Команды") @@ -445,6 +456,12 @@ def main(): # Команда load-from-hf-status subparsers.add_parser("load-from-hf-status", help="Проверить статус загрузки модели") + # Команда command-feedback — выгрузка некорректных выражений из RDS-2P-Salute + subparsers.add_parser( + "command-feedback", + help="Загрузить репорт «исправить команду» из RDS-2P-Salute (некорректные выражения)", + ) + args = parser.parse_args() if not args.command: @@ -571,6 +588,24 @@ def main(): except Exception as e: print(f"Ошибка: {e}", file=sys.stderr) sys.exit(1) + elif args.command == "command-feedback": + try: + client = CVCApiClient(args.url) + items = client.get_command_feedback() + print(json.dumps(items, indent=2, ensure_ascii=False)) + except requests.exceptions.HTTPError as e: + if e.response is not None: + try: + detail = e.response.json().get("detail", e.response.text) + except Exception: + detail = e.response.text + print(f"Ошибка: {detail}", file=sys.stderr) + else: + print(f"Ошибка: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Ошибка: {e}", file=sys.stderr) + sys.exit(1) if __name__ == "__main__": diff --git a/config.yaml b/config.yaml index d6800d4..dbaec57 100644 --- a/config.yaml +++ b/config.yaml @@ -25,6 +25,11 @@ database: # Если указана директория, загружаются все CSV файлы из неё csv_migration_path: "data" +# Репорты «исправить команду» из RDS-2P-Salute (опционально) +# По умолчанию: http://rds-2p-salute-app:8000/v1/admin/command-feedback +# command_feedback: +# url: "http://rds-2p-salute-app:8000/v1/admin/command-feedback" + # Параметры обучения по умолчанию # Можно переопределить через HTTP API /train endpoint training: diff --git a/docker-compose.yml b/docker-compose.yml index 9bbdd22..95dbf64 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ services: user: "${RUNNER_UID:-0}:${RUNNER_GID:-0}" container_name: cvc-api ports: - - "127.0.0.1:20001:20001" # только localhost + robot-services-network (контейнеры) + - "20001:20001" volumes: - ./commands_classifier:/app/commands_classifier:ro - ./tests:/app/tests:ro @@ -28,7 +28,7 @@ services: networks: - robot-services-network healthcheck: - test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:20001/health', timeout=5).raise_for_status()"] + test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:20001/v1/health', timeout=5).raise_for_status()"] interval: 30s timeout: 10s retries: 3 diff --git a/docs/cicd_setup.md b/docs/cicd_setup.md index 26b3214..20cf8c5 100644 --- a/docs/cicd_setup.md +++ b/docs/cicd_setup.md @@ -222,9 +222,11 @@ cat ~/.cvc_ssh_keys/deploy_key.pub Скрипты уже созданы в директории `ci/`: - `ci/generate_ssh_key.sh` - генерация SSH-ключа (опционально, если нужен SSH для других задач) -- `ci/train_via_api.py` - запуск обучения модели через API +- `ci/train_via_api.py` - запуск обучения модели (напрямую, без вызова API) - `ci/upload_to_hf.py` - загрузка модели на Hugging Face Hub +**API:** все эндпоинты CVC версионированы префиксом `/v1` (например, `/v1/health`, `/v1/train`). Полный список — в [README](../README.md#эндпоинты-api-v1). + ## Шаг 6: Проверка workflow Workflow файл находится в `.github/workflows/deploy.yml`. Он автоматически: diff --git a/tests/api/test_examples.py b/tests/api/test_examples.py index b58b7eb..7e9020e 100644 --- a/tests/api/test_examples.py +++ b/tests/api/test_examples.py @@ -2,16 +2,16 @@ def test_get_examples_empty(client): - """GET /examples при пустой БД возвращает [].""" - response = client.get("/examples") + """GET /v1/examples при пустой БД возвращает [].""" + response = client.get("/v1/examples") assert response.status_code == 200 assert response.json() == [] def test_add_example_returns_201(client): - """POST /examples создаёт пример и возвращает 201 с id, text, command.""" + """POST /v1/examples создаёт пример и возвращает 201 с id, text, command.""" response = client.post( - "/examples", + "/v1/examples", json={"text": "лягись", "command": "lie_down"}, ) assert response.status_code == 201 @@ -22,9 +22,9 @@ def test_add_example_returns_201(client): def test_get_examples_after_add(client): - """GET /examples после добавления возвращает список с одним примером.""" - client.post("/examples", json={"text": "отмена", "command": "dismiss"}) - response = client.get("/examples") + """GET /v1/examples после добавления возвращает список с одним примером.""" + client.post("/v1/examples", json={"text": "отмена", "command": "dismiss"}) + response = client.get("/v1/examples") assert response.status_code == 200 examples = response.json() assert len(examples) == 1 @@ -33,32 +33,32 @@ def test_get_examples_after_add(client): def test_get_example_by_id(client): - """GET /examples/{id} возвращает пример по ID.""" - add_resp = client.post("/examples", json={"text": "неизвестно", "command": "unknown"}) + """GET /v1/examples/{id} возвращает пример по ID.""" + add_resp = client.post("/v1/examples", json={"text": "неизвестно", "command": "unknown"}) example_id = add_resp.json()["id"] - response = client.get(f"/examples/{example_id}") + response = client.get(f"/v1/examples/{example_id}") assert response.status_code == 200 assert response.json()["id"] == example_id assert response.json()["text"] == "неизвестно" def test_get_example_by_id_not_found(client): - """GET /examples/999 возвращает 404.""" - response = client.get("/examples/999") + """GET /v1/examples/999 возвращает 404.""" + response = client.get("/v1/examples/999") assert response.status_code == 404 def test_delete_example(client): - """DELETE /examples/{id} удаляет пример и возвращает 200.""" - add_resp = client.post("/examples", json={"text": "x", "command": "y"}) + """DELETE /v1/examples/{id} удаляет пример и возвращает 200.""" + add_resp = client.post("/v1/examples", json={"text": "x", "command": "y"}) example_id = add_resp.json()["id"] - response = client.delete(f"/examples/{example_id}") + response = client.delete(f"/v1/examples/{example_id}") assert response.status_code == 200 - get_resp = client.get(f"/examples/{example_id}") + get_resp = client.get(f"/v1/examples/{example_id}") assert get_resp.status_code == 404 def test_add_example_validation_empty_text(client): - """POST /examples с пустым text возвращает 422.""" - response = client.post("/examples", json={"text": "", "command": "cmd"}) + """POST /v1/examples с пустым text возвращает 422.""" + response = client.post("/v1/examples", json={"text": "", "command": "cmd"}) assert response.status_code == 422 diff --git a/tests/api/test_health.py b/tests/api/test_health.py index 116749d..1ac69bb 100644 --- a/tests/api/test_health.py +++ b/tests/api/test_health.py @@ -1,9 +1,9 @@ -"""API-тесты для эндпоинтов /health и /metrics.""" +"""API-тесты для эндпоинтов /v1/health и /v1/metrics.""" def test_health_returns_200(client): - """GET /health возвращает 200 и структуру status, model_loaded, training_active.""" - response = client.get("/health") + """GET /v1/health возвращает 200 и структуру status, model_loaded, training_active.""" + response = client.get("/v1/health") assert response.status_code == 200 data = response.json() assert data["status"] == "healthy" @@ -13,14 +13,14 @@ def test_health_returns_200(client): def test_health_model_not_loaded(client): """Без загруженной модели model_loaded = False.""" - response = client.get("/health") + response = client.get("/v1/health") assert response.status_code == 200 assert response.json()["model_loaded"] is False def test_metrics_returns_200(client): - """GET /metrics возвращает 200 и счётчики примеров.""" - response = client.get("/metrics") + """GET /v1/metrics возвращает 200 и счётчики примеров.""" + response = client.get("/v1/metrics") assert response.status_code == 200 data = response.json() assert "total_examples" in data @@ -32,6 +32,6 @@ def test_metrics_returns_200(client): def test_metrics_empty_db(client): """При пустой БД total_examples = 0.""" - response = client.get("/metrics") + response = client.get("/v1/metrics") assert response.status_code == 200 assert response.json()["total_examples"] == 0 diff --git a/tests/api/test_predict.py b/tests/api/test_predict.py index e3427eb..8775c79 100644 --- a/tests/api/test_predict.py +++ b/tests/api/test_predict.py @@ -2,8 +2,8 @@ def test_predict_without_model_returns_503(client): - """POST /predict без загруженной модели возвращает 503.""" - response = client.post("/predict", json={"text": "лягись"}) + """POST /v1/predict без загруженной модели возвращает 503.""" + response = client.post("/v1/predict", json={"text": "лягись"}) assert response.status_code == 503 assert ( "модель" in response.json().get("detail", "").lower() @@ -12,8 +12,8 @@ def test_predict_without_model_returns_503(client): def test_predict_with_mock_classifier_returns_200(client_with_mock_classifier): - """POST /predict с мок-классификатором возвращает 200 и command.""" - response = client_with_mock_classifier.post("/predict", json={"text": "лягись"}) + """POST /v1/predict с мок-классификатором возвращает 200 и command.""" + response = client_with_mock_classifier.post("/v1/predict", json={"text": "лягись"}) assert response.status_code == 200 data = response.json() assert "command" in data @@ -21,9 +21,9 @@ def test_predict_with_mock_classifier_returns_200(client_with_mock_classifier): def test_predict_with_confidence(client_with_mock_classifier): - """POST /predict с return_confidence=True возвращает confidence.""" + """POST /v1/predict с return_confidence=True возвращает confidence.""" response = client_with_mock_classifier.post( - "/predict", + "/v1/predict", json={"text": "лягись", "return_confidence": True}, ) assert response.status_code == 200 @@ -34,15 +34,15 @@ def test_predict_with_confidence(client_with_mock_classifier): def test_predict_batch_without_model_returns_503(client): - """POST /predict/batch без модели возвращает 503.""" - response = client.post("/predict/batch", json={"texts": ["a", "b"]}) + """POST /v1/predict/batch без модели возвращает 503.""" + response = client.post("/v1/predict/batch", json={"texts": ["a", "b"]}) assert response.status_code == 503 def test_predict_batch_with_mock_classifier(client_with_mock_classifier): - """POST /predict/batch с моком возвращает список commands.""" + """POST /v1/predict/batch с моком возвращает список commands.""" response = client_with_mock_classifier.post( - "/predict/batch", + "/v1/predict/batch", json={"texts": ["лягись", "отмена"]}, ) assert response.status_code == 200 @@ -52,7 +52,7 @@ def test_predict_batch_with_mock_classifier(client_with_mock_classifier): def test_embed_returns_200(client): """POST /embed без модели создаёт временный классификатор и возвращает эмбеддинги (или 200).""" - response = client.post("/embed", json={"inputs": ["hello"]}) + response = client.post("/v1/embed", json={"inputs": ["hello"]}) # Может быть 200 (если эндпоинт создаёт временную модель по config) или 503/500 при отсутствии модели assert response.status_code in (200, 503, 500) if response.status_code == 200: @@ -62,8 +62,8 @@ def test_embed_returns_200(client): def test_embed_with_mock_classifier(client_with_mock_classifier): - """POST /embed с мок-классификатором возвращает эмбеддинги.""" - response = client_with_mock_classifier.post("/embed", json={"inputs": ["hello", "world"]}) + """POST /v1/embed с мок-классификатором возвращает эмбеддинги.""" + response = client_with_mock_classifier.post("/v1/embed", json={"inputs": ["hello", "world"]}) assert response.status_code == 200 data = response.json() assert "embeddings" in data @@ -71,6 +71,6 @@ def test_embed_with_mock_classifier(client_with_mock_classifier): def test_predict_validation_empty_text(client_with_mock_classifier): - """POST /predict с пустым text возвращает 422.""" - response = client_with_mock_classifier.post("/predict", json={"text": ""}) + """POST /v1/predict с пустым text возвращает 422.""" + response = client_with_mock_classifier.post("/v1/predict", json={"text": ""}) assert response.status_code == 422 diff --git a/tests/api/test_training.py b/tests/api/test_training.py index c102741..8863552 100644 --- a/tests/api/test_training.py +++ b/tests/api/test_training.py @@ -1,9 +1,9 @@ -"""API-тесты для эндпоинтов /train, /train/status, /reset.""" +"""API-тесты для эндпоинтов /v1/train, /v1/train/status, /v1/reset.""" def test_train_returns_200(client): - """POST /train с моком TrainingManager возвращает 200 и training_id.""" - response = client.post("/train", json={}) + """POST /v1/train с моком TrainingManager возвращает 200 и training_id.""" + response = client.post("/v1/train", json={}) assert response.status_code == 200 data = response.json() assert "training_id" in data @@ -12,9 +12,9 @@ def test_train_returns_200(client): def test_train_with_params(client): - """POST /train с параметрами принимает их.""" + """POST /v1/train с параметрами принимает их.""" response = client.post( - "/train", + "/v1/train", json={ "num_iterations": 5, "num_epochs": 1, @@ -28,8 +28,8 @@ def test_train_with_params(client): def test_train_status_returns_200(client): - """GET /train/status возвращает 200 и статус.""" - response = client.get("/train/status") + """GET /v1/train/status возвращает 200 и статус.""" + response = client.get("/v1/train/status") assert response.status_code == 200 data = response.json() assert "status" in data @@ -37,8 +37,8 @@ def test_train_status_returns_200(client): def test_reset_returns_200(client): - """POST /reset возвращает 200 и reset_examples, model_deleted.""" - response = client.post("/reset") + """POST /v1/reset возвращает 200 и reset_examples, model_deleted.""" + response = client.post("/v1/reset") assert response.status_code == 200 data = response.json() assert "message" in data diff --git a/tests/conftest.py b/tests/conftest.py index b588b46..05e74c1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,7 @@ from commands_classifier import db from commands_classifier.api.routes import ( + command_feedback_router, examples_router, health_router, load_from_hf_router, @@ -114,11 +115,13 @@ def app(temp_db_path, temp_model_dir): """Тестовое приложение с подменённым state (без реальной модели и HF).""" _setup_test_state(temp_db_path, temp_model_dir, classifier=None) test_app = FastAPI(title="CVC API Test", lifespan=_noop_lifespan) - test_app.include_router(predict_router) - test_app.include_router(training_router) - test_app.include_router(examples_router) - test_app.include_router(load_from_hf_router) - test_app.include_router(health_router) + api_v1 = "/v1" + test_app.include_router(predict_router, prefix=api_v1) + test_app.include_router(training_router, prefix=api_v1) + test_app.include_router(examples_router, prefix=api_v1) + test_app.include_router(load_from_hf_router, prefix=api_v1) + test_app.include_router(health_router, prefix=api_v1) + test_app.include_router(command_feedback_router, prefix=api_v1) return test_app @@ -128,11 +131,13 @@ def app_with_mock_classifier(temp_db_path, temp_model_dir): mock_clf = _make_mock_classifier() _setup_test_state(temp_db_path, temp_model_dir, classifier=mock_clf) test_app = FastAPI(title="CVC API Test", lifespan=_noop_lifespan) - test_app.include_router(predict_router) - test_app.include_router(training_router) - test_app.include_router(examples_router) - test_app.include_router(load_from_hf_router) - test_app.include_router(health_router) + api_v1 = "/v1" + test_app.include_router(predict_router, prefix=api_v1) + test_app.include_router(training_router, prefix=api_v1) + test_app.include_router(examples_router, prefix=api_v1) + test_app.include_router(load_from_hf_router, prefix=api_v1) + test_app.include_router(health_router, prefix=api_v1) + test_app.include_router(command_feedback_router, prefix=api_v1) return test_app diff --git a/tests/integration/test_e2e_docker.py b/tests/integration/test_e2e_docker.py index f7be31e..3c6eeba 100644 --- a/tests/integration/test_e2e_docker.py +++ b/tests/integration/test_e2e_docker.py @@ -11,6 +11,7 @@ from commands_classifier import db from commands_classifier.api.routes import ( + command_feedback_router, examples_router, health_router, load_from_hf_router, @@ -80,11 +81,13 @@ def e2e_app(temp_db_path, temp_model_dir): """Приложение для E2E с реальной БД и реальным TrainingManager.""" _setup_e2e_state(temp_db_path, temp_model_dir) app = FastAPI(title="CVC E2E Test") - app.include_router(predict_router) - app.include_router(training_router) - app.include_router(examples_router) - app.include_router(load_from_hf_router) - app.include_router(health_router) + api_v1 = "/v1" + app.include_router(predict_router, prefix=api_v1) + app.include_router(training_router, prefix=api_v1) + app.include_router(examples_router, prefix=api_v1) + app.include_router(load_from_hf_router, prefix=api_v1) + app.include_router(health_router, prefix=api_v1) + app.include_router(command_feedback_router, prefix=api_v1) return app @@ -98,8 +101,8 @@ def e2e_client(e2e_app, temp_db_path, temp_model_dir): def test_e2e_health(e2e_client): - """GET /health возвращает 200 и структуру.""" - response = e2e_client.get("/health") + """GET /v1/health возвращает 200 и структуру.""" + response = e2e_client.get("/v1/health") assert response.status_code == 200 data = response.json() assert data["status"] == "healthy" @@ -107,8 +110,8 @@ def test_e2e_health(e2e_client): def test_e2e_predict_without_model_fails(e2e_client): - """POST /predict без обученной модели возвращает ошибку (503).""" - response = e2e_client.post("/predict", json={"text": "лягись"}) + """POST /v1/predict без обученной модели возвращает ошибку (503).""" + response = e2e_client.post("/v1/predict", json={"text": "лягись"}) assert response.status_code == 503 @@ -130,7 +133,7 @@ def test_e2e_train_then_predict_then_reset_then_predict_fails(e2e_client): step = 5 elapsed = 0 while elapsed < max_wait: - status_resp = e2e_client.get("/train/status") + status_resp = e2e_client.get("/v1/train/status") assert status_resp.status_code == 200 status_data = status_resp.json() if status_data.get("status") == "completed": @@ -153,10 +156,10 @@ def test_e2e_train_then_predict_then_reset_then_predict_fails(e2e_client): ) # Сброс модели - reset_resp = e2e_client.post("/reset") + reset_resp = e2e_client.post("/v1/reset") assert reset_resp.status_code == 200 assert "reset_examples" in reset_resp.json() # После reset predict снова должен вернуть ошибку (модель выгружена) - predict_after_reset = e2e_client.post("/predict", json={"text": "лягись"}) + predict_after_reset = e2e_client.post("/v1/predict", json={"text": "лягись"}) assert predict_after_reset.status_code == 503 From f0711c6bb7973d261a78c786976db668152d5406 Mon Sep 17 00:00:00 2001 From: ShiWarai Date: Tue, 3 Feb 2026 20:55:06 +0300 Subject: [PATCH 08/14] =?UTF-8?q?Fix:=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB=20URL=20=D0=BD=D0=B0=20=D0=BE=D1=81=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + tests/integration/test_e2e_docker.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 369f553..54e628f 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ Thumbs.db # Hugging Face cache (для Docker) cache/ +.cache/ # Environment variables .env \ No newline at end of file diff --git a/tests/integration/test_e2e_docker.py b/tests/integration/test_e2e_docker.py index 3c6eeba..dd77709 100644 --- a/tests/integration/test_e2e_docker.py +++ b/tests/integration/test_e2e_docker.py @@ -121,7 +121,7 @@ def test_e2e_train_then_predict_then_reset_then_predict_fails(e2e_client): """ # Запуск обучения (минимальные итерации для скорости) train_resp = e2e_client.post( - "/train", + "/v1/train", json={"num_iterations": 2, "num_epochs": 1, "batch_size": 32}, ) assert train_resp.status_code == 200 @@ -147,7 +147,7 @@ def test_e2e_train_then_predict_then_reset_then_predict_fails(e2e_client): # Predict по трём строкам из датасета (по одной на класс) for text in SAMPLE_TEXTS_BY_CLASS: - pred_resp = e2e_client.post("/predict", json={"text": text}) + pred_resp = e2e_client.post("/v1/predict", json={"text": text}) assert pred_resp.status_code == 200, f"predict для '{text}' вернул {pred_resp.status_code}" data = pred_resp.json() assert "command" in data From f75bfb57fd58dc3789e9dc7923ac1b487865ed3f Mon Sep 17 00:00:00 2001 From: ShiWarai Date: Tue, 3 Feb 2026 21:07:55 +0300 Subject: [PATCH 09/14] =?UTF-8?q?Fix:=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=80=D1=83=D1=87=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/routes/command_feedback.py | 2 +- commands_classifier/api/routes/examples.py | 8 +++--- commands_classifier/api/routes/health.py | 4 +-- .../api/routes/load_from_hf.py | 4 +-- commands_classifier/api/routes/predict.py | 6 ++--- commands_classifier/api/routes/training.py | 6 ++--- commands_classifier/api/server.py | 15 +++++------ tests/conftest.py | 26 +++++++++---------- tests/integration/test_e2e_docker.py | 13 +++++----- 9 files changed, 40 insertions(+), 44 deletions(-) diff --git a/commands_classifier/api/routes/command_feedback.py b/commands_classifier/api/routes/command_feedback.py index 5968212..187a6ae 100644 --- a/commands_classifier/api/routes/command_feedback.py +++ b/commands_classifier/api/routes/command_feedback.py @@ -17,7 +17,7 @@ REQUEST_TIMEOUT = 30 -@router.get("/command-feedback") +@router.get("/v1/command-feedback") def get_command_feedback() -> List[dict]: """ Загружает записи обратной связи по командам из приложения RDS-2P-Salute. diff --git a/commands_classifier/api/routes/examples.py b/commands_classifier/api/routes/examples.py index 2cda780..0e24fce 100644 --- a/commands_classifier/api/routes/examples.py +++ b/commands_classifier/api/routes/examples.py @@ -36,7 +36,7 @@ class ExampleResponse(BaseModel): command: str -@router.get("/examples", response_model=List[ExampleResponse]) +@router.get("/v1/examples", response_model=List[ExampleResponse]) async def get_examples(): """ Получает все примеры из базы данных. @@ -50,7 +50,7 @@ async def get_examples(): return [ExampleResponse(id=ex[0], text=ex[1], command=ex[2]) for ex in examples] -@router.post("/examples", response_model=ExampleResponse, status_code=201) +@router.post("/v1/examples", response_model=ExampleResponse, status_code=201) async def add_example(request: ExampleRequest): """ Добавляет новый пример в базу данных. @@ -79,7 +79,7 @@ async def add_example(request: ExampleRequest): raise HTTPException(status_code=500, detail=f"Ошибка при добавлении примера: {str(e)}") -@router.delete("/examples/{example_id}") +@router.delete("/v1/examples/{example_id}") async def delete_example(example_id: int): """ Удаляет пример по ID. @@ -102,7 +102,7 @@ async def delete_example(example_id: int): return {"message": f"Пример {example_id} успешно удален"} -@router.get("/examples/{example_id}", response_model=ExampleResponse) +@router.get("/v1/examples/{example_id}", response_model=ExampleResponse) async def get_example(example_id: int): """ Получает пример по ID. diff --git a/commands_classifier/api/routes/health.py b/commands_classifier/api/routes/health.py index 7838939..4dd3bf2 100644 --- a/commands_classifier/api/routes/health.py +++ b/commands_classifier/api/routes/health.py @@ -8,7 +8,7 @@ router = APIRouter(tags=["health"]) -@router.get("/health") +@router.get("/v1/health") async def health(response: Response): """ Проверка работоспособности сервера (TEI совместимый). @@ -37,7 +37,7 @@ async def health(response: Response): } -@router.get("/metrics") +@router.get("/v1/metrics") async def metrics(): """ Метрики сервера (TEI совместимый). diff --git a/commands_classifier/api/routes/load_from_hf.py b/commands_classifier/api/routes/load_from_hf.py index 24d1330..1d0a60e 100644 --- a/commands_classifier/api/routes/load_from_hf.py +++ b/commands_classifier/api/routes/load_from_hf.py @@ -158,7 +158,7 @@ def _run_load_from_hf_task(repo_id: str, local_dir: str, load_id: str): _load_from_hf_status["completed_at"] = datetime.now().isoformat() -@router.post("/load_from_hf", response_model=LoadFromHfResponse) +@router.post("/v1/load_from_hf", response_model=LoadFromHfResponse) async def load_from_hf(request: LoadFromHfRequest): """ Загружает модель с Hugging Face Hub. @@ -239,7 +239,7 @@ async def load_from_hf(request: LoadFromHfRequest): return LoadFromHfResponse(message="Загрузка модели запущена в фоновом режиме", load_id=load_id) -@router.get("/load_from_hf/status", response_model=LoadFromHfStatusResponse) +@router.get("/v1/load_from_hf/status", response_model=LoadFromHfStatusResponse) async def get_load_from_hf_status(): """ Возвращает статус загрузки модели. diff --git a/commands_classifier/api/routes/predict.py b/commands_classifier/api/routes/predict.py index b5b2ed9..c9ceea5 100644 --- a/commands_classifier/api/routes/predict.py +++ b/commands_classifier/api/routes/predict.py @@ -75,7 +75,7 @@ class PredictBatchResponse(BaseModel): confidences: Optional[List[float]] = None -@router.post("/embed", response_model=EmbedResponse) +@router.post("/v1/embed", response_model=EmbedResponse) async def embed(request: EmbedRequest): """ Получает эмбеддинги для текстов (TEI совместимый эндпоинт). @@ -106,7 +106,7 @@ async def embed(request: EmbedRequest): return EmbedResponse(embeddings=embeddings) -@router.post("/predict", response_model=PredictResponse) +@router.post("/v1/predict", response_model=PredictResponse) async def predict(request: PredictRequest): """ Классифицирует один текст в команду. @@ -136,7 +136,7 @@ async def predict(request: PredictRequest): raise HTTPException(status_code=500, detail=f"Ошибка при предсказании: {str(e)}") -@router.post("/predict/batch", response_model=PredictBatchResponse) +@router.post("/v1/predict/batch", response_model=PredictBatchResponse) async def predict_batch(request: PredictBatchRequest): """ Классифицирует список текстов в команды. diff --git a/commands_classifier/api/routes/training.py b/commands_classifier/api/routes/training.py index b42fb2d..1e96613 100644 --- a/commands_classifier/api/routes/training.py +++ b/commands_classifier/api/routes/training.py @@ -38,7 +38,7 @@ class ResetResponse(BaseModel): model_deleted: bool -@router.post("/train", response_model=TrainResponse) +@router.post("/v1/train", response_model=TrainResponse) async def train(request: TrainRequest): """ Запускает обучение модели в фоновом режиме. @@ -89,7 +89,7 @@ async def train(request: TrainRequest): raise HTTPException(status_code=500, detail=f"Ошибка при запуске обучения: {str(e)}") -@router.get("/train/status") +@router.get("/v1/train/status") async def get_training_status(): """ Возвращает статус текущего обучения. @@ -105,7 +105,7 @@ async def get_training_status(): return training_manager.get_status() -@router.post("/reset", response_model=ResetResponse) +@router.post("/v1/reset", response_model=ResetResponse) async def reset_training(): """ Полностью сбрасывает обучение: diff --git a/commands_classifier/api/server.py b/commands_classifier/api/server.py index 71b23c6..6e163a1 100644 --- a/commands_classifier/api/server.py +++ b/commands_classifier/api/server.py @@ -134,11 +134,10 @@ async def lifespan(app: FastAPI): title="CVC API", description="API для классификации голосовых команд", lifespan=lifespan ) -# Подключаем роутеры с версионированием API -API_V1_PREFIX = "/v1" -app.include_router(predict_router, prefix=API_V1_PREFIX) -app.include_router(training_router, prefix=API_V1_PREFIX) -app.include_router(examples_router, prefix=API_V1_PREFIX) -app.include_router(load_from_hf_router, prefix=API_V1_PREFIX) -app.include_router(health_router, prefix=API_V1_PREFIX) -app.include_router(command_feedback_router, prefix=API_V1_PREFIX) +# Подключаем роутеры (версионирование указано в самих роутах) +app.include_router(predict_router) +app.include_router(training_router) +app.include_router(examples_router) +app.include_router(load_from_hf_router) +app.include_router(health_router) +app.include_router(command_feedback_router) diff --git a/tests/conftest.py b/tests/conftest.py index 05e74c1..2b3c198 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -115,13 +115,12 @@ def app(temp_db_path, temp_model_dir): """Тестовое приложение с подменённым state (без реальной модели и HF).""" _setup_test_state(temp_db_path, temp_model_dir, classifier=None) test_app = FastAPI(title="CVC API Test", lifespan=_noop_lifespan) - api_v1 = "/v1" - test_app.include_router(predict_router, prefix=api_v1) - test_app.include_router(training_router, prefix=api_v1) - test_app.include_router(examples_router, prefix=api_v1) - test_app.include_router(load_from_hf_router, prefix=api_v1) - test_app.include_router(health_router, prefix=api_v1) - test_app.include_router(command_feedback_router, prefix=api_v1) + test_app.include_router(predict_router) + test_app.include_router(training_router) + test_app.include_router(examples_router) + test_app.include_router(load_from_hf_router) + test_app.include_router(health_router) + test_app.include_router(command_feedback_router) return test_app @@ -131,13 +130,12 @@ def app_with_mock_classifier(temp_db_path, temp_model_dir): mock_clf = _make_mock_classifier() _setup_test_state(temp_db_path, temp_model_dir, classifier=mock_clf) test_app = FastAPI(title="CVC API Test", lifespan=_noop_lifespan) - api_v1 = "/v1" - test_app.include_router(predict_router, prefix=api_v1) - test_app.include_router(training_router, prefix=api_v1) - test_app.include_router(examples_router, prefix=api_v1) - test_app.include_router(load_from_hf_router, prefix=api_v1) - test_app.include_router(health_router, prefix=api_v1) - test_app.include_router(command_feedback_router, prefix=api_v1) + test_app.include_router(predict_router) + test_app.include_router(training_router) + test_app.include_router(examples_router) + test_app.include_router(load_from_hf_router) + test_app.include_router(health_router) + test_app.include_router(command_feedback_router) return test_app diff --git a/tests/integration/test_e2e_docker.py b/tests/integration/test_e2e_docker.py index dd77709..95f9a02 100644 --- a/tests/integration/test_e2e_docker.py +++ b/tests/integration/test_e2e_docker.py @@ -81,13 +81,12 @@ def e2e_app(temp_db_path, temp_model_dir): """Приложение для E2E с реальной БД и реальным TrainingManager.""" _setup_e2e_state(temp_db_path, temp_model_dir) app = FastAPI(title="CVC E2E Test") - api_v1 = "/v1" - app.include_router(predict_router, prefix=api_v1) - app.include_router(training_router, prefix=api_v1) - app.include_router(examples_router, prefix=api_v1) - app.include_router(load_from_hf_router, prefix=api_v1) - app.include_router(health_router, prefix=api_v1) - app.include_router(command_feedback_router, prefix=api_v1) + app.include_router(predict_router) + app.include_router(training_router) + app.include_router(examples_router) + app.include_router(load_from_hf_router) + app.include_router(health_router) + app.include_router(command_feedback_router) return app From fb53a03aba7abcf3af3f37242228aad68af1bf73 Mon Sep 17 00:00:00 2001 From: ShiWarai Date: Tue, 3 Feb 2026 21:16:46 +0300 Subject: [PATCH 10/14] =?UTF-8?q?Feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=81=D0=B8=D0=BD=D0=BE=D0=BD=D0=B8?= =?UTF-8?q?=D0=BC=D1=8B=20[retrain]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- commands_classifier/api/server.py | 2 +- data/commands_extended.csv | 63 +++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/commands_classifier/api/server.py b/commands_classifier/api/server.py index 6e163a1..da65bc0 100644 --- a/commands_classifier/api/server.py +++ b/commands_classifier/api/server.py @@ -140,4 +140,4 @@ async def lifespan(app: FastAPI): app.include_router(examples_router) app.include_router(load_from_hf_router) app.include_router(health_router) -app.include_router(command_feedback_router) +app.include_router(command_feedback_router) \ No newline at end of file diff --git a/data/commands_extended.csv b/data/commands_extended.csv index c159553..aa4ba86 100644 --- a/data/commands_extended.csv +++ b/data/commands_extended.csv @@ -383,6 +383,69 @@ text,command попроси лечь,lie_down вели лечь,lie_down прикажи лечь,lie_down +положи робота,lie_down +положи панду,lie_down +положите робота,lie_down +положите панду,lie_down +положить робота,lie_down +положить панду,lie_down +положь робота,lie_down +положь панду,lie_down +положишь робота,lie_down +положишь панду,lie_down +положил бы робота,lie_down +положила бы панду,lie_down +положили бы робота,lie_down +надо положить робота,lie_down +нужно положить робота,lie_down +пора положить робота,lie_down +надо положить панду,lie_down +нужно положить панду,lie_down +пора положить панду,lie_down +можно положить робота,lie_down +можно положить панду,lie_down +давай положи робота,lie_down +давай положи панду,lie_down +уложить робота,lie_down +уложить панду,lie_down +уложи робота,lie_down +уложи панду,lie_down +уложите робота,lie_down +уложите панду,lie_down +уложишь робота,lie_down +уложил бы робота,lie_down +уложила бы панду,lie_down +надо уложить робота,lie_down +нужно уложить панду,lie_down +пора уложить робота,lie_down +давай уложи робота,lie_down +опустить робота,lie_down +опустить панду,lie_down +опусти робота,lie_down +опусти панду,lie_down +опустите робота,lie_down +опустите панду,lie_down +опустишь робота,lie_down +опустил бы робота,lie_down +надо опустить робота,lie_down +нужно опустить панду,lie_down +давай опусти робота,lie_down +скажи роботу положить робота,lie_down +скажи панде положить панду,lie_down +робот положи робота,lie_down +панда положи панду,lie_down +положи робота робот,lie_down +положи панду панда,lie_down +пожалуйста положи робота,lie_down +пожалуйста положи панду,lie_down +положи-ка робота,lie_down +уложи-ка робота,lie_down +ну-ка положи робота,lie_down +а ну положи робота,lie_down +эй положи робота,lie_down +быстро положи робота,lie_down +робот пожалуйста положи робота,lie_down +панда пожалуйста положи панду,lie_down кувыркнуться,rotate кувыркнись,rotate кувыркаться,rotate From d2974457926f0b9b2e4d2663f4a10bace86b0987 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:20:53 +0000 Subject: [PATCH 11/14] Initial plan From 457ada9c92fa3801c5281a7b54143f2dde2f905c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:22:43 +0000 Subject: [PATCH 12/14] =?UTF-8?q?Fix:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BD=D0=BE=D0=B2=D0=B0=D1=8F=20?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0=20=D0=B2=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=86=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0=20server.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ShiWarai <40498648+ShiWarai@users.noreply.github.com> --- commands_classifier/api/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands_classifier/api/server.py b/commands_classifier/api/server.py index da65bc0..6e163a1 100644 --- a/commands_classifier/api/server.py +++ b/commands_classifier/api/server.py @@ -140,4 +140,4 @@ async def lifespan(app: FastAPI): app.include_router(examples_router) app.include_router(load_from_hf_router) app.include_router(health_router) -app.include_router(command_feedback_router) \ No newline at end of file +app.include_router(command_feedback_router) From d46b9ddbdcc0bcb4c83b16b54e9c117d8bcfbf4d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:26:21 +0000 Subject: [PATCH 13/14] =?UTF-8?q?Feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B6=D0=BA=D0=B0=20=D0=B0=D0=B2=D1=82=D0=BE=D0=BC=D0=B0=D1=82?= =?UTF-8?q?=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8=D1=85=20=D1=82=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=20=D0=B4=D0=BB=D1=8F=20PR=20=D0=B8=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ShiWarai <40498648+ShiWarai@users.noreply.github.com> --- .github/workflows/deploy.yml | 2 + README.md | 4 +- docs/running_tests_manually.md | 90 ++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 docs/running_tests_manually.md diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c9df105..fedf171 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,6 +3,8 @@ name: ML Pipeline - Train and Publish on: push: branches: [ main, master, dev ] # Триггер на push в main/master/dev (новый датасет) + pull_request: + branches: [ main, master, dev ] # Триггер на PR к main/master/dev workflow_dispatch: # Позволяет запускать вручную через GitHub UI inputs: run_training: diff --git a/README.md b/README.md index 8322bc6..e0b83b8 100644 --- a/README.md +++ b/README.md @@ -206,10 +206,12 @@ CVC/ Пайплайн [.github/workflows/deploy.yml](.github/workflows/deploy.yml): -- При каждом push — **тесты** (линт + pytest в Docker). +- При каждом push в `main`, `master`, `dev` и при **Pull Request** к этим веткам — **тесты** (линт + pytest в Docker). - Job **Train and Publish** — при метке `[retrain]` в сообщении коммита или при ручном запуске (Actions → Run workflow). Секреты: `HF_TOKEN`, `HF_REPO_ID`. - **Уведомления в Telegram** при успешной и неуспешной сборке (опционально: секреты `TELEGRAM_TOKEN`, `TELEGRAM_TO`). Подробнее: [docs/telegram_notifications.md](docs/telegram_notifications.md). +**Как запустить тесты вручную для PR:** [docs/running_tests_manually.md](docs/running_tests_manually.md) + Подробная настройка (self-hosted runner, GPU, секреты): [docs/cicd_setup.md](docs/cicd_setup.md). ## Лицензия diff --git a/docs/running_tests_manually.md b/docs/running_tests_manually.md new file mode 100644 index 0000000..bb7baac --- /dev/null +++ b/docs/running_tests_manually.md @@ -0,0 +1,90 @@ +# Как запустить тесты вручную для Pull Request + +## Автоматический запуск тестов + +После изменений в workflow файле, тесты теперь запускаются автоматически: + +1. **При создании Pull Request** к веткам `main`, `master` или `dev` +2. **При каждом push** в ветку с открытым PR +3. **При push** напрямую в ветки `main`, `master` или `dev` + +## Ручной запуск тестов (workflow_dispatch) + +Если вам нужно запустить тесты вручную (например, для перезапуска после временного сбоя), выполните следующие шаги: + +### Шаг 1: Перейдите в Actions + +1. Откройте репозиторий на GitHub: https://github.com/ShiWarai/CVC +2. Перейдите на вкладку **Actions** (в верхнем меню репозитория) + +### Шаг 2: Выберите workflow + +1. В левой боковой панели найдите workflow **"ML Pipeline - Train and Publish"** +2. Кликните на него + +### Шаг 3: Запустите workflow + +1. Справа вверху найдите кнопку **"Run workflow"** (серая кнопка) +2. Кликните на неё - откроется выпадающее меню +3. Выберите ветку, на которой хотите запустить тесты (например, вашу PR ветку) +4. Настройте параметры (опционально): + - **Run training**: снимите галочку, если НЕ хотите запускать обучение модели (только тесты) + - **db_path**: оставьте по умолчанию или укажите путь к другой базе данных +5. Нажмите зелёную кнопку **"Run workflow"** + +### Что будет выполнено + +При ручном запуске: + +1. **Job "test"** (всегда): + - Сборка Docker образов + - Линтинг кода с помощью ruff + - Запуск pytest тестов + +2. **Job "train-and-publish"** (только если включен параметр "Run training"): + - Обучение модели + - Загрузка модели на Hugging Face Hub + +3. **Уведомления в Telegram** (если настроены) + +## Просмотр результатов + +После запуска: + +1. Вы увидите новый workflow run в списке +2. Кликните на него для просмотра деталей +3. В каждом job вы можете развернуть шаги и посмотреть логи +4. Статус отобразится: + - ✅ Зелёная галочка - всё прошло успешно + - ❌ Красный крестик - есть ошибки + - 🟡 Жёлтый кружок - в процессе выполнения + +## Просмотр статуса тестов в PR + +В самом Pull Request: + +1. Внизу страницы PR вы увидите секцию **"Checks"** +2. Там отображается статус всех workflow runs +3. Кликните на **"Details"** рядом с любым check, чтобы посмотреть логи + +## Локальный запуск тестов + +Если вы хотите запустить тесты локально перед push: + +```bash +# Сборка образов +docker compose -f docker-compose.yml build cvc-api +docker compose -f docker-compose.yml -f docker-compose.dev.yml build cvc-dev + +# Линтинг +docker compose -f docker-compose.yml -f docker-compose.dev.yml run --rm cvc-dev ruff check . + +# Тесты +docker compose -f docker-compose.yml -f docker-compose.dev.yml run --rm cvc-dev pytest tests/ -v --tb=short --cov=commands_classifier --cov-report=term-missing +``` + +## Полезные ссылки + +- [GitHub Actions документация](https://docs.github.com/en/actions/using-workflows/manually-running-a-workflow) +- [Настройка CI/CD](cicd_setup.md) +- [README - раздел CI/CD](../README.md#cicd) From c586c3fdd2a89b21eb8c260538e723c1b68d9cf0 Mon Sep 17 00:00:00 2001 From: ShiWarai Date: Tue, 3 Feb 2026 21:35:50 +0300 Subject: [PATCH 14/14] =?UTF-8?q?Revert=20"Feat:=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4=D0=B4?= =?UTF-8?q?=D0=B5=D1=80=D0=B6=D0=BA=D0=B0=20=D0=B0=D0=B2=D1=82=D0=BE=D0=BC?= =?UTF-8?q?=D0=B0=D1=82=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8=D1=85=20=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=BE=D0=B2=20=D0=B4=D0=BB=D1=8F=20PR=20?= =?UTF-8?q?=D0=B8=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit d46b9ddbdcc0bcb4c83b16b54e9c117d8bcfbf4d. --- .github/workflows/deploy.yml | 2 - README.md | 4 +- docs/running_tests_manually.md | 90 ---------------------------------- 3 files changed, 1 insertion(+), 95 deletions(-) delete mode 100644 docs/running_tests_manually.md diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index fedf171..c9df105 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,8 +3,6 @@ name: ML Pipeline - Train and Publish on: push: branches: [ main, master, dev ] # Триггер на push в main/master/dev (новый датасет) - pull_request: - branches: [ main, master, dev ] # Триггер на PR к main/master/dev workflow_dispatch: # Позволяет запускать вручную через GitHub UI inputs: run_training: diff --git a/README.md b/README.md index e0b83b8..8322bc6 100644 --- a/README.md +++ b/README.md @@ -206,12 +206,10 @@ CVC/ Пайплайн [.github/workflows/deploy.yml](.github/workflows/deploy.yml): -- При каждом push в `main`, `master`, `dev` и при **Pull Request** к этим веткам — **тесты** (линт + pytest в Docker). +- При каждом push — **тесты** (линт + pytest в Docker). - Job **Train and Publish** — при метке `[retrain]` в сообщении коммита или при ручном запуске (Actions → Run workflow). Секреты: `HF_TOKEN`, `HF_REPO_ID`. - **Уведомления в Telegram** при успешной и неуспешной сборке (опционально: секреты `TELEGRAM_TOKEN`, `TELEGRAM_TO`). Подробнее: [docs/telegram_notifications.md](docs/telegram_notifications.md). -**Как запустить тесты вручную для PR:** [docs/running_tests_manually.md](docs/running_tests_manually.md) - Подробная настройка (self-hosted runner, GPU, секреты): [docs/cicd_setup.md](docs/cicd_setup.md). ## Лицензия diff --git a/docs/running_tests_manually.md b/docs/running_tests_manually.md deleted file mode 100644 index bb7baac..0000000 --- a/docs/running_tests_manually.md +++ /dev/null @@ -1,90 +0,0 @@ -# Как запустить тесты вручную для Pull Request - -## Автоматический запуск тестов - -После изменений в workflow файле, тесты теперь запускаются автоматически: - -1. **При создании Pull Request** к веткам `main`, `master` или `dev` -2. **При каждом push** в ветку с открытым PR -3. **При push** напрямую в ветки `main`, `master` или `dev` - -## Ручной запуск тестов (workflow_dispatch) - -Если вам нужно запустить тесты вручную (например, для перезапуска после временного сбоя), выполните следующие шаги: - -### Шаг 1: Перейдите в Actions - -1. Откройте репозиторий на GitHub: https://github.com/ShiWarai/CVC -2. Перейдите на вкладку **Actions** (в верхнем меню репозитория) - -### Шаг 2: Выберите workflow - -1. В левой боковой панели найдите workflow **"ML Pipeline - Train and Publish"** -2. Кликните на него - -### Шаг 3: Запустите workflow - -1. Справа вверху найдите кнопку **"Run workflow"** (серая кнопка) -2. Кликните на неё - откроется выпадающее меню -3. Выберите ветку, на которой хотите запустить тесты (например, вашу PR ветку) -4. Настройте параметры (опционально): - - **Run training**: снимите галочку, если НЕ хотите запускать обучение модели (только тесты) - - **db_path**: оставьте по умолчанию или укажите путь к другой базе данных -5. Нажмите зелёную кнопку **"Run workflow"** - -### Что будет выполнено - -При ручном запуске: - -1. **Job "test"** (всегда): - - Сборка Docker образов - - Линтинг кода с помощью ruff - - Запуск pytest тестов - -2. **Job "train-and-publish"** (только если включен параметр "Run training"): - - Обучение модели - - Загрузка модели на Hugging Face Hub - -3. **Уведомления в Telegram** (если настроены) - -## Просмотр результатов - -После запуска: - -1. Вы увидите новый workflow run в списке -2. Кликните на него для просмотра деталей -3. В каждом job вы можете развернуть шаги и посмотреть логи -4. Статус отобразится: - - ✅ Зелёная галочка - всё прошло успешно - - ❌ Красный крестик - есть ошибки - - 🟡 Жёлтый кружок - в процессе выполнения - -## Просмотр статуса тестов в PR - -В самом Pull Request: - -1. Внизу страницы PR вы увидите секцию **"Checks"** -2. Там отображается статус всех workflow runs -3. Кликните на **"Details"** рядом с любым check, чтобы посмотреть логи - -## Локальный запуск тестов - -Если вы хотите запустить тесты локально перед push: - -```bash -# Сборка образов -docker compose -f docker-compose.yml build cvc-api -docker compose -f docker-compose.yml -f docker-compose.dev.yml build cvc-dev - -# Линтинг -docker compose -f docker-compose.yml -f docker-compose.dev.yml run --rm cvc-dev ruff check . - -# Тесты -docker compose -f docker-compose.yml -f docker-compose.dev.yml run --rm cvc-dev pytest tests/ -v --tb=short --cov=commands_classifier --cov-report=term-missing -``` - -## Полезные ссылки - -- [GitHub Actions документация](https://docs.github.com/en/actions/using-workflows/manually-running-a-workflow) -- [Настройка CI/CD](cicd_setup.md) -- [README - раздел CI/CD](../README.md#cicd)