Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
a52ca6c
Style: убрал разделители
ShiWarai Feb 1, 2026
21d6143
Fix: ограничил доступ к классификатору для безопасности и добавил уве…
ShiWarai Feb 3, 2026
1c5f336
Build: добавлено уведомление после успешного прохождения CD
ShiWarai Feb 3, 2026
04c1951
Build: ещё одна попытка обновить CD
ShiWarai Feb 3, 2026
c87fd09
Build: попытка вызвать ошибку
ShiWarai Feb 3, 2026
8920a7a
Build: убрал тестовый тест для вызова ошибки
ShiWarai Feb 3, 2026
890dd82
Feat: добавлен запрос неправльных фраз и весионирование API
ShiWarai Feb 3, 2026
f0711c6
Fix: исправил URL на основе теста
ShiWarai Feb 3, 2026
f75bfb5
Fix: исправлены ручки
ShiWarai Feb 3, 2026
fb53a03
Feat: добавлены синонимы [retrain]
ShiWarai Feb 3, 2026
d297445
Initial plan
Copilot Feb 3, 2026
457ada9
Fix: добавлена новая строка в конце файла server.py
Copilot Feb 3, 2026
d46b9dd
Feat: добавлена поддержка автоматических тестов для PR и документация
Copilot Feb 3, 2026
c586c3f
Revert "Feat: добавлена поддержка автоматических тестов для PR и доку…
ShiWarai Feb 3, 2026
045e5ce
Merge pull request #9 from ShiWarai/copilot/sub-pr-6-another-one
ShiWarai Feb 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,40 @@ jobs:
echo "✗ Пайплайн завершился с ошибкой"
echo " Проверьте логи выше для деталей"
fi

notify-telegram-on-success:
name: Notify Telegram on 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:
- 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: |
*Пайплайн прошёл успешно*
Репо: `${{ 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
if: always() && (needs.test.result == 'failure' || needs.train-and-publish.result == '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: |
*Пайплайн упал*
Репо: `${{ github.repository }}`
Ветка: `${{ github.ref_name }}`
[Открыть run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Thumbs.db

# Hugging Face cache (для Docker)
cache/
.cache/

# Environment variables
.env
58 changes: 24 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@

Мини-сервис для классификации голосовых команд (SetFit). Обучает модель на малом датасете и классифицирует текстовые команды. Создан для использования в проекте навыка для Sber Salute.

---

## Стек технологий

| Категория | Технологии |
Expand All @@ -21,8 +19,6 @@
| Инфраструктура | Docker |
| Разработка | pytest, ruff, httpx |

---

## Оглавление

| Раздел | Содержание |
Expand All @@ -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) — принять условия).
Expand All @@ -52,8 +46,6 @@

Остановка: `docker-compose down`.

---

## Установка и запуск

### Требование: Hugging Face
Expand Down Expand Up @@ -86,8 +78,6 @@ python -m commands_classifier.cli serve

Опции: `--host`, `--port`, `--config`. БД создаётся при первом запуске, данные из `data/` или CSV из `config.yaml`.

---

## Использование

### CLI
Expand All @@ -103,19 +93,20 @@ 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

### config.yaml
Expand All @@ -137,32 +128,38 @@ 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
batch_size: 32
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) определяется при запуске автоматически.

---

## Данные

### Формат датасета
Expand All @@ -177,8 +174,6 @@ training:

`--iterations`, `--epochs`, `--batch-size`, `--learning-rate`. Значения по умолчанию — в `config.yaml` (секция `training`).

---

## Разработка

### Тесты и линт
Expand Down Expand Up @@ -207,23 +202,18 @@ CVC/
└── docs/ # cicd_setup.md и др.
```

---

## CI/CD

Пайплайн [.github/workflows/deploy.yml](.github/workflows/deploy.yml):

- При каждом 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).

---

## Лицензия

MIT

---

*Проект создан с использованием нейросетей.*
2 changes: 2 additions & 0 deletions commands_classifier/api/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,4 +13,5 @@
"examples_router",
"load_from_hf_router",
"health_router",
"command_feedback_router",
]
75 changes: 75 additions & 0 deletions commands_classifier/api/routes/command_feedback.py
Original file line number Diff line number Diff line change
@@ -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("/v1/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")
8 changes: 4 additions & 4 deletions commands_classifier/api/routes/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
"""
Получает все примеры из базы данных.
Expand All @@ -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):
"""
Добавляет новый пример в базу данных.
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions commands_classifier/api/routes/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
router = APIRouter(tags=["health"])


@router.get("/health")
@router.get("/v1/health")
async def health(response: Response):
"""
Проверка работоспособности сервера (TEI совместимый).
Expand Down Expand Up @@ -37,7 +37,7 @@ async def health(response: Response):
}


@router.get("/metrics")
@router.get("/v1/metrics")
async def metrics():
"""
Метрики сервера (TEI совместимый).
Expand Down
4 changes: 2 additions & 2 deletions commands_classifier/api/routes/load_from_hf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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():
"""
Возвращает статус загрузки модели.
Expand Down
6 changes: 3 additions & 3 deletions commands_classifier/api/routes/predict.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 совместимый эндпоинт).
Expand Down Expand Up @@ -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):
"""
Классифицирует один текст в команду.
Expand Down Expand Up @@ -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):
"""
Классифицирует список текстов в команды.
Expand Down
Loading