545 lines
16 KiB
Markdown
545 lines
16 KiB
Markdown
# Challenge Service API Documentation
|
||
|
||
Сервис для проверки заданий в реальном времени через LLM (GigaChat).
|
||
|
||
## Описание
|
||
|
||
Система позволяет:
|
||
- Создавать задания с описанием в Markdown
|
||
- Объединять задания в цепочки
|
||
- Пользователям проходить задания и отправлять результаты
|
||
- Автоматически проверять результаты через GigaChat с ограничением потоков
|
||
- Отслеживать статистику по пользователям и системе
|
||
|
||
## Конфигурация
|
||
|
||
В `.env` файле добавьте:
|
||
|
||
```env
|
||
CHALLENGE_LLM_THREADS=2 # Количество одновременных проверок через LLM
|
||
```
|
||
|
||
## API Endpoints
|
||
|
||
Все endpoints начинаются с префикса `/api/challenge`
|
||
|
||
### Аутентификация
|
||
|
||
#### POST `/auth`
|
||
|
||
Регистрация/авторизация пользователя по nickname.
|
||
|
||
**Request:**
|
||
```json
|
||
{
|
||
"nickname": "user123"
|
||
}
|
||
```
|
||
|
||
**Валидация:**
|
||
- `nickname`: обязательное поле, от 3 до 50 символов
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"error": null,
|
||
"data": {
|
||
"ok": true,
|
||
"userId": "507f1f77bcf86cd799439011"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Управление заданиями
|
||
|
||
**Важно:** Все операции создания/обновления/удаления заданий требуют авторизации через Keycloak с ролью `teacher` или `challenge-author`.
|
||
|
||
#### POST `/task`
|
||
|
||
Создание нового задания (требует роль `teacher` или `challenge-author`).
|
||
|
||
**Headers:**
|
||
```
|
||
Authorization: Bearer <keycloak_token>
|
||
```
|
||
|
||
**Request:**
|
||
```json
|
||
{
|
||
"title": "Написать функцию сортировки",
|
||
"description": "# Задание\n\nНапишите функцию сортировки массива чисел...",
|
||
"hiddenInstructions": "Проверь сложность алгоритма. Должна быть O(n log n), не принимай bubble sort"
|
||
}
|
||
```
|
||
|
||
**Валидация:**
|
||
- `title`: обязательное поле, максимум 255 символов
|
||
- `description`: обязательное поле
|
||
- `hiddenInstructions`: опциональное поле
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"error": null,
|
||
"data": {
|
||
"_id": "507f1f77bcf86cd799439011",
|
||
"title": "Написать функцию сортировки",
|
||
"description": "# Задание\n\n...",
|
||
"hiddenInstructions": "Проверь сложность алгоритма...",
|
||
"creator": { "sub": "...", "preferred_username": "teacher1" },
|
||
"createdAt": "2023-10-29T12:00:00.000Z",
|
||
"updatedAt": "2023-10-29T12:00:00.000Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Примечание:** Поле `hiddenInstructions` опционально. Эти инструкции будут переданы в LLM при проверке, но скрыты от студентов.
|
||
|
||
#### GET `/task/:taskId`
|
||
|
||
Получение задания по ID.
|
||
|
||
**Важно:** Поля `hiddenInstructions` и `creator` возвращаются только для пользователей с ролью `teacher` или `challenge-author`. Обычные студенты их не увидят.
|
||
|
||
#### GET `/tasks`
|
||
|
||
Получение всех заданий.
|
||
|
||
**Примечание:** Для не-преподавателей поля `hiddenInstructions` и `creator` скрыты.
|
||
|
||
#### PUT `/task/:taskId`
|
||
|
||
Обновление задания (требует роль `teacher` или `challenge-author`).
|
||
|
||
**Headers:**
|
||
```
|
||
Authorization: Bearer <keycloak_token>
|
||
```
|
||
|
||
**Request:**
|
||
```json
|
||
{
|
||
"title": "Новый заголовок",
|
||
"description": "Новое описание",
|
||
"hiddenInstructions": "Обновленные инструкции для LLM"
|
||
}
|
||
```
|
||
|
||
**Валидация:**
|
||
- `title`: опциональное поле, максимум 255 символов
|
||
- `description`: опциональное поле
|
||
- `hiddenInstructions`: опциональное поле
|
||
|
||
#### DELETE `/task/:taskId`
|
||
|
||
Удаление задания (требует роль `teacher` или `challenge-author`). Также удаляется из всех цепочек.
|
||
|
||
**Headers:**
|
||
```
|
||
Authorization: Bearer <keycloak_token>
|
||
```
|
||
|
||
### Управление цепочками заданий
|
||
|
||
**Важно:** Все операции создания/обновления/удаления цепочек требуют авторизации через Keycloak с ролью `teacher` или `challenge-author`.
|
||
|
||
#### POST `/chain`
|
||
|
||
Создание цепочки заданий (требует роль `teacher` или `challenge-author`).
|
||
|
||
**Request:**
|
||
```json
|
||
{
|
||
"name": "Основы программирования",
|
||
"taskIds": ["507f1f77bcf86cd799439011", "507f1f77bcf86cd799439012"]
|
||
}
|
||
```
|
||
|
||
**Валидация:**
|
||
- `name`: обязательное поле, максимум 255 символов
|
||
- `taskIds`: обязательное поле, массив строк
|
||
|
||
#### GET `/chains`
|
||
|
||
Получение всех цепочек с заданиями.
|
||
|
||
**Примечание:** Для не-преподавателей поля `hiddenInstructions` и `creator` скрыты в заданиях внутри цепочек.
|
||
|
||
#### GET `/chain/:chainId`
|
||
|
||
Получение цепочки по ID.
|
||
|
||
**Примечание:** Для не-преподавателей поля `hiddenInstructions` и `creator` скрыты в заданиях внутри цепочки.
|
||
|
||
#### PUT `/chain/:chainId`
|
||
|
||
Обновление цепочки (требует роль `teacher` или `challenge-author`).
|
||
|
||
**Headers:**
|
||
```
|
||
Authorization: Bearer <keycloak_token>
|
||
```
|
||
|
||
**Request:**
|
||
```json
|
||
{
|
||
"name": "Новое название цепочки",
|
||
"taskIds": ["507f1f77bcf86cd799439011", "507f1f77bcf86cd799439012"]
|
||
}
|
||
```
|
||
|
||
**Валидация:**
|
||
- `name`: опциональное поле, максимум 255 символов
|
||
- `taskIds`: опциональное поле, массив строк
|
||
|
||
#### DELETE `/chain/:chainId`
|
||
|
||
Удаление цепочки (требует роль `teacher` или `challenge-author`).
|
||
|
||
**Headers:**
|
||
```
|
||
Authorization: Bearer <keycloak_token>
|
||
```
|
||
|
||
### Отправка и проверка заданий
|
||
|
||
#### POST `/submit`
|
||
|
||
Отправка результата выполнения задания на проверку.
|
||
|
||
**Request:**
|
||
```json
|
||
{
|
||
"userId": "507f1f77bcf86cd799439011",
|
||
"taskId": "507f1f77bcf86cd799439012",
|
||
"result": "function sort(arr) { return arr.sort((a, b) => a - b); }"
|
||
}
|
||
```
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"error": null,
|
||
"data": {
|
||
"queueId": "550e8400-e29b-41d4-a716-446655440000",
|
||
"submissionId": "507f1f77bcf86cd799439013"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Валидация:**
|
||
- `userId`: обязательное поле
|
||
- `taskId`: обязательное поле
|
||
- `result`: обязательное поле
|
||
|
||
#### GET `/check-status/:queueId`
|
||
|
||
Polling endpoint для проверки статуса проверки.
|
||
|
||
**Response (в процессе):**
|
||
```json
|
||
{
|
||
"error": null,
|
||
"data": {
|
||
"status": "waiting",
|
||
"position": 3
|
||
}
|
||
}
|
||
```
|
||
|
||
**Response (завершено):**
|
||
```json
|
||
{
|
||
"error": null,
|
||
"data": {
|
||
"status": "completed",
|
||
"submission": {
|
||
"_id": "507f1f77bcf86cd799439013",
|
||
"user": {...},
|
||
"task": {...},
|
||
"result": "...",
|
||
"status": "accepted",
|
||
"feedback": "Отлично! Задание выполнено правильно.",
|
||
"attemptNumber": 1,
|
||
"submittedAt": "2023-10-29T12:00:00.000Z",
|
||
"checkedAt": "2023-10-29T12:00:05.000Z"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Статусы проверки:
|
||
- `waiting` - ожидает в очереди
|
||
- `in_progress` - проверяется
|
||
- `completed` - проверка завершена
|
||
- `error` - ошибка при проверке
|
||
- `not_found` - не найдено
|
||
|
||
Статусы submission:
|
||
- `pending` - ожидает проверки
|
||
- `in_progress` - проверяется
|
||
- `accepted` - принято
|
||
- `needs_revision` - требует доработки
|
||
|
||
### Просмотр попыток
|
||
|
||
#### GET `/user/:userId/submissions?taskId=...`
|
||
|
||
Получение всех попыток пользователя (опционально для конкретного задания).
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"error": null,
|
||
"data": [
|
||
{
|
||
"_id": "507f1f77bcf86cd799439013",
|
||
"task": {...},
|
||
"result": "...",
|
||
"status": "accepted",
|
||
"feedback": "...",
|
||
"attemptNumber": 2,
|
||
"submittedAt": "2023-10-29T12:00:00.000Z",
|
||
"checkedAt": "2023-10-29T12:00:05.000Z"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### Статистика
|
||
|
||
#### GET `/user/:userId/stats`
|
||
|
||
Детальная статистика пользователя.
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"error": null,
|
||
"data": {
|
||
"totalTasksAttempted": 5,
|
||
"completedTasks": 3,
|
||
"inProgressTasks": 1,
|
||
"needsRevisionTasks": 1,
|
||
"totalSubmissions": 8,
|
||
"averageCheckTimeMs": 5234,
|
||
"taskStats": [
|
||
{
|
||
"taskId": "507f1f77bcf86cd799439012",
|
||
"taskTitle": "Написать функцию сортировки",
|
||
"attempts": [...],
|
||
"totalAttempts": 2,
|
||
"status": "completed",
|
||
"lastAttemptAt": "2023-10-29T12:00:00.000Z"
|
||
}
|
||
],
|
||
"chainStats": [
|
||
{
|
||
"chainId": "507f1f77bcf86cd799439014",
|
||
"chainName": "Основы программирования",
|
||
"totalTasks": 5,
|
||
"completedTasks": 3,
|
||
"progress": 60
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
**Примечание:** Статусы заданий в `taskStats.status`:
|
||
- `not_attempted` - задание не начато
|
||
- `pending` - ожидает проверки
|
||
- `in_progress` - проверяется
|
||
- `needs_revision` - требует доработки
|
||
- `completed` - выполнено
|
||
|
||
#### GET `/stats`
|
||
|
||
Общая статистика системы.
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"error": null,
|
||
"data": {
|
||
"users": 150,
|
||
"tasks": 25,
|
||
"chains": 5,
|
||
"submissions": {
|
||
"total": 850,
|
||
"accepted": 420,
|
||
"rejected": 380,
|
||
"pending": 30,
|
||
"inProgress": 20
|
||
},
|
||
"averageCheckTimeMs": 5500,
|
||
"queue": {
|
||
"queueLength": 12,
|
||
"waiting": 10,
|
||
"inProgress": 2,
|
||
"maxConcurrency": 2,
|
||
"currentlyProcessing": 2
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### GET `/stats/v2`
|
||
|
||
Расширенная статистика системы с детальными данными для таблиц и прогресс-баров.
|
||
|
||
**Query параметры:**
|
||
- `chainId` (опционально): фильтрация статистики по конкретной цепочке
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"error": null,
|
||
"data": {
|
||
"users": 150,
|
||
"tasks": 25,
|
||
"chains": 5,
|
||
"submissions": {
|
||
"total": 850,
|
||
"accepted": 420,
|
||
"rejected": 380,
|
||
"pending": 30,
|
||
"inProgress": 20
|
||
},
|
||
"averageCheckTimeMs": 5500,
|
||
"queue": {
|
||
"queueLength": 12,
|
||
"waiting": 10,
|
||
"inProgress": 2,
|
||
"maxConcurrency": 2,
|
||
"currentlyProcessing": 2
|
||
},
|
||
"tasksTable": [
|
||
{
|
||
"taskId": "507f1f77bcf86cd799439012",
|
||
"title": "Написать функцию сортировки",
|
||
"totalAttempts": 45,
|
||
"uniqueUsers": 20,
|
||
"acceptedCount": 18,
|
||
"successRate": 90,
|
||
"averageAttemptsToSuccess": 1.5
|
||
}
|
||
],
|
||
"activeParticipants": [
|
||
{
|
||
"userId": "507f1f77bcf86cd799439011",
|
||
"nickname": "user123",
|
||
"totalSubmissions": 10,
|
||
"completedTasks": 5,
|
||
"chainProgress": [
|
||
{
|
||
"chainId": "507f1f77bcf86cd799439014",
|
||
"chainName": "Основы программирования",
|
||
"totalTasks": 10,
|
||
"completedTasks": 5,
|
||
"progressPercent": 50
|
||
}
|
||
]
|
||
}
|
||
],
|
||
"chainsDetailed": [
|
||
{
|
||
"chainId": "507f1f77bcf86cd799439014",
|
||
"name": "Основы программирования",
|
||
"totalTasks": 10,
|
||
"tasks": [
|
||
{
|
||
"taskId": "507f1f77bcf86cd799439012",
|
||
"title": "Написать функцию сортировки",
|
||
"description": "# Задание\n\n..."
|
||
}
|
||
],
|
||
"participantProgress": [
|
||
{
|
||
"userId": "507f1f77bcf86cd799439011",
|
||
"nickname": "user123",
|
||
"taskProgress": [
|
||
{
|
||
"taskId": "507f1f77bcf86cd799439012",
|
||
"taskTitle": "Написать функцию сортировки",
|
||
"status": "completed"
|
||
}
|
||
],
|
||
"completedCount": 5,
|
||
"progressPercent": 50
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
**Примечание:** Статусы задач в `taskProgress`:
|
||
- `not_started` - задание не начато
|
||
- `pending` - ожидает проверки
|
||
- `in_progress` - проверяется
|
||
- `needs_revision` - требует доработки
|
||
- `completed` - выполнено
|
||
|
||
## Архитектура
|
||
|
||
### Модели данных
|
||
|
||
- **ChallengeUser** - пользователи сервиса
|
||
- **ChallengeTask** - отдельные задания
|
||
- **ChallengeChain** - цепочки заданий
|
||
- **ChallengeSubmission** - результаты выполнения заданий
|
||
|
||
### Сервисы
|
||
|
||
- **challenge-checker.ts** - проверка заданий через GigaChat
|
||
- **ChallengeCheckQueue.ts** - управление очередью проверок
|
||
- **challengeQueueInstance.ts** - singleton экземпляр очереди
|
||
|
||
### Очередь проверки
|
||
|
||
Очередь работает in-memory с ограничением на количество одновременных проверок.
|
||
|
||
- Элементы добавляются в очередь при отправке задания
|
||
- Обработка происходит автоматически каждую секунду
|
||
- Количество параллельных проверок ограничено `CHALLENGE_LLM_THREADS`
|
||
- После завершения проверки результаты сохраняются в БД
|
||
- Записи в очереди удаляются через 5 минут после завершения
|
||
|
||
### Проверка через LLM
|
||
|
||
Для проверки используется GigaChat с промптом, который:
|
||
- Получает описание задания
|
||
- Получает результат пользователя
|
||
- Возвращает статус (ПРИНЯТО/ДОРАБОТКА) и feedback
|
||
|
||
Ответ парсится и сохраняется в submission.
|
||
|
||
## Примеры использования
|
||
|
||
### Типичный flow пользователя
|
||
|
||
1. Авторизация: `POST /api/challenge/auth`
|
||
2. Получение цепочек: `GET /api/challenge/chains`
|
||
3. Получение заданий цепочки: `GET /api/challenge/chain/:chainId`
|
||
4. Отправка результата: `POST /api/challenge/submit`
|
||
5. Polling проверки: `GET /api/challenge/check-status/:queueId` (каждые 2-3 секунды)
|
||
6. Просмотр статистики: `GET /api/challenge/user/:userId/stats`
|
||
|
||
### Типичный flow администратора
|
||
|
||
1. Создание заданий: `POST /api/challenge/task`
|
||
2. Создание цепочки: `POST /api/challenge/chain`
|
||
3. Просмотр общей статистики: `GET /api/challenge/stats`
|
||
4. Просмотр расширенной статистики: `GET /api/challenge/stats/v2` (опционально с `?chainId=...`)
|
||
|
||
## Ограничения и особенности
|
||
|
||
- Очередь работает in-memory, при перезапуске сервера незавершенные проверки вернутся в статус `pending`
|
||
- История всех попыток сохраняется полностью
|
||
- Markdown описания заданий хранятся как простой текст в БД
|
||
- Аутентификация простая, без паролей (только nickname)
|
||
- Nickname должен быть уникальным в системе
|
||
|