fix: db api
This commit is contained in:
parent
ea94f98e74
commit
74cdc196c6
|
|
@ -15,7 +15,14 @@
|
|||
"Bash(.venv/Scripts/python.exe -m pytest tests/ -v --tb=line)",
|
||||
"Bash(.venv/Scripts/python.exe -m pytest:*)",
|
||||
"Bash(.venvScriptspython.exe -m pytest tests/unit/ -v --cov=app --cov-report=term-missing)",
|
||||
"Bash(.\\.venv\\Scripts\\python.exe -m pytest:*)"
|
||||
"Bash(.\\.venv\\Scripts\\python.exe -m pytest:*)",
|
||||
"Bash(.\\\\.venv\\\\Scripts\\\\python.exe -m pytest:*)",
|
||||
"Bash(.venvScriptspython.exe -m pytest tests/unit/test_query.py::TestBenchQueryEndpoint::test_bench_query_success -v)",
|
||||
"Bash(.venvScriptspython.exe -m pytest tests/unit/ -v --tb=short)",
|
||||
"Bash(.\\\\\\\\.venv\\\\\\\\Scripts\\\\\\\\python.exe -m pytest:*)",
|
||||
"Bash(..venvScriptsactivate)",
|
||||
"Bash(pytest:*)",
|
||||
"Bash(source:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
|
@ -0,0 +1,710 @@
|
|||
```markdown
|
||||
# SBS Bench RAG API Contract
|
||||
|
||||
API для управления пользователями, настройками окружений и сессиями анализа в системе Brief Bench.
|
||||
|
||||
## Содержание
|
||||
|
||||
- [Общая информация](#общая-информация)
|
||||
- [Аутентификация и пользователи](#аутентификация-и-пользователи)
|
||||
- [Настройки пользователя](#настройки-пользователя)
|
||||
- [Сессии анализа](#сессии-анализа)
|
||||
- [Форматы данных](#форматы-данных)
|
||||
- [Обработка ошибок](#обработка-ошибок)
|
||||
|
||||
---
|
||||
|
||||
## Общая информация
|
||||
|
||||
### Base URL
|
||||
|
||||
```
|
||||
/api/v1
|
||||
```
|
||||
|
||||
### Форматы данных
|
||||
|
||||
- **Content-Type**: `application/json; charset=utf-8`
|
||||
- **Даты**: ISO 8601 с таймзоной UTC (`YYYY-MM-DDTHH:MM:SSZ`)
|
||||
- **UUID**: строка в формате `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
|
||||
|
||||
### Окружения
|
||||
|
||||
API поддерживает три окружения (enum `environment`):
|
||||
- `ift` — IFT окружение
|
||||
- `psi` — PSI окружение
|
||||
- `prod` — Production окружение
|
||||
|
||||
### Режимы API
|
||||
|
||||
Два режима работы (enum `api_mode`):
|
||||
- `bench` — режим тестирования
|
||||
- `backend` — backend режим
|
||||
|
||||
---
|
||||
|
||||
## Аутентификация и пользователи
|
||||
|
||||
### POST /users/login
|
||||
|
||||
Авторизация пользователя и запись информации о логине.
|
||||
|
||||
#### Request
|
||||
|
||||
```
|
||||
{
|
||||
"login": "12345678",
|
||||
"client_ip": "MTkyLjE2OC4xLjEwMA=="
|
||||
}
|
||||
```
|
||||
|
||||
**Параметры:**
|
||||
- `login` (string, required): 8-значный логин (строка из цифр)
|
||||
- `client_ip` (string, required): IP-адрес в кодировке base64
|
||||
|
||||
#### Response 200
|
||||
|
||||
```
|
||||
{
|
||||
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"login": "12345678",
|
||||
"last_login_at": "2025-12-24T12:00:00Z",
|
||||
"created_at": "2025-12-01T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Errors
|
||||
|
||||
- **400 Bad Request**: Неверный формат логина или client_ip
|
||||
- **500 Internal Server Error**: Ошибка сервера
|
||||
|
||||
---
|
||||
|
||||
## Настройки пользователя
|
||||
|
||||
### GET /users/{user_id}/settings
|
||||
|
||||
Получить настройки пользователя для всех окружений.
|
||||
|
||||
#### Path Parameters
|
||||
|
||||
- `user_id` (UUID, required): UUID пользователя
|
||||
|
||||
#### Response 200
|
||||
|
||||
```
|
||||
{
|
||||
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"settings": {
|
||||
"ift": {
|
||||
"apiMode": "bench",
|
||||
"bearerToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"systemPlatform": "platform-ift",
|
||||
"systemPlatformUser": "user-123",
|
||||
"platformUserId": "p-user-456",
|
||||
"platformId": "platform-789",
|
||||
"withClassify": false,
|
||||
"resetSessionMode": true
|
||||
},
|
||||
"psi": {
|
||||
"apiMode": "bench",
|
||||
"bearerToken": null,
|
||||
"systemPlatform": null,
|
||||
"systemPlatformUser": null,
|
||||
"platformUserId": null,
|
||||
"platformId": null,
|
||||
"withClassify": false,
|
||||
"resetSessionMode": true
|
||||
},
|
||||
"prod": {
|
||||
"apiMode": "backend",
|
||||
"bearerToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"systemPlatform": "platform-prod",
|
||||
"systemPlatformUser": "user-prod",
|
||||
"platformUserId": "p-user-prod",
|
||||
"platformId": "platform-prod-id",
|
||||
"withClassify": true,
|
||||
"resetSessionMode": false
|
||||
}
|
||||
},
|
||||
"updated_at": "2025-12-24T12:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Errors
|
||||
|
||||
- **404 Not Found**: Пользователь не найден
|
||||
- **500 Internal Server Error**: Ошибка сервера
|
||||
|
||||
---
|
||||
|
||||
### PATCH /users/{user_id}/settings
|
||||
|
||||
Частично обновить настройки пользователя. Обновляются только переданные поля.
|
||||
|
||||
#### Path Parameters
|
||||
|
||||
- `user_id` (UUID, required): UUID пользователя
|
||||
|
||||
#### Request
|
||||
|
||||
```
|
||||
{
|
||||
"settings": {
|
||||
"ift": {
|
||||
"bearerToken": "new-token-value",
|
||||
"withClassify": true
|
||||
},
|
||||
"prod": {
|
||||
"apiMode": "bench"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Примечания:**
|
||||
- Передавайте только те окружения и поля, которые нужно изменить
|
||||
- Непереданные поля остаются без изменений
|
||||
- Для сброса значения в `null` явно передайте `null`
|
||||
|
||||
#### Response 200
|
||||
|
||||
```
|
||||
{
|
||||
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"settings": {
|
||||
"ift": {
|
||||
"apiMode": "bench",
|
||||
"bearerToken": "new-token-value",
|
||||
"systemPlatform": "platform-ift",
|
||||
"systemPlatformUser": "user-123",
|
||||
"platformUserId": "p-user-456",
|
||||
"platformId": "platform-789",
|
||||
"withClassify": true,
|
||||
"resetSessionMode": true
|
||||
},
|
||||
"psi": {
|
||||
"apiMode": "bench",
|
||||
"bearerToken": null,
|
||||
"systemPlatform": null,
|
||||
"systemPlatformUser": null,
|
||||
"platformUserId": null,
|
||||
"platformId": null,
|
||||
"withClassify": false,
|
||||
"resetSessionMode": true
|
||||
},
|
||||
"prod": {
|
||||
"apiMode": "bench",
|
||||
"bearerToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"systemPlatform": "platform-prod",
|
||||
"systemPlatformUser": "user-prod",
|
||||
"platformUserId": "p-user-prod",
|
||||
"platformId": "platform-prod-id",
|
||||
"withClassify": true,
|
||||
"resetSessionMode": false
|
||||
}
|
||||
},
|
||||
"updated_at": "2025-12-24T13:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Errors
|
||||
|
||||
- **400 Bad Request**: Неверный формат настроек
|
||||
- **404 Not Found**: Пользователь не найден
|
||||
- **500 Internal Server Error**: Ошибка сервера
|
||||
|
||||
---
|
||||
|
||||
## Сессии анализа
|
||||
|
||||
### POST /users/{user_id}/sessions
|
||||
|
||||
Создать новую сессию анализа.
|
||||
|
||||
#### Path Parameters
|
||||
|
||||
- `user_id` (UUID, required): UUID пользователя
|
||||
|
||||
#### Request
|
||||
|
||||
```
|
||||
{
|
||||
"environment": "ift",
|
||||
"api_mode": "bench",
|
||||
"request": [
|
||||
{
|
||||
"body": "Как получить кредит на недвижимость?",
|
||||
"with_docs": true
|
||||
},
|
||||
{
|
||||
"body": "Какие документы нужны?",
|
||||
"with_docs": true
|
||||
}
|
||||
],
|
||||
"response": {
|
||||
"answers": [
|
||||
{
|
||||
"question": "Как получить кредит на недвижимость?",
|
||||
"answer": "Для получения кредита...",
|
||||
"sources": ["doc1.pdf", "doc2.pdf"]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"processing_time_ms": 1250
|
||||
}
|
||||
},
|
||||
"annotations": {
|
||||
"0": {
|
||||
"overall": {
|
||||
"rating": "correct",
|
||||
"comment": "Ответ корректный и полный"
|
||||
},
|
||||
"body_research": {
|
||||
"issues": [],
|
||||
"comment": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Параметры:**
|
||||
- `environment` (string, required): Окружение (`ift`, `psi`, `prod`)
|
||||
- `api_mode` (string, required): Режим API (`bench`, `backend`)
|
||||
- `request` (array, required): Массив запросов
|
||||
- `body` (string, required): Текст вопроса
|
||||
- `with_docs` (boolean, optional): Использовать документы (default: `true`)
|
||||
- `response` (object, required): Ответ системы (произвольная структура)
|
||||
- `annotations` (object, optional): Аннотации по индексам вопросов
|
||||
- Ключи должны быть числовыми строками (`"0"`, `"1"`, ...)
|
||||
|
||||
#### Response 201
|
||||
|
||||
```
|
||||
{
|
||||
"session_id": "660e8400-e29b-41d4-a716-446655440000",
|
||||
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"environment": "ift",
|
||||
"api_mode": "bench",
|
||||
"request": [
|
||||
{
|
||||
"body": "Как получить кредит на недвижимость?",
|
||||
"with_docs": true
|
||||
},
|
||||
{
|
||||
"body": "Какие документы нужны?",
|
||||
"with_docs": true
|
||||
}
|
||||
],
|
||||
"response": {
|
||||
"answers": [
|
||||
{
|
||||
"question": "Как получить кредит на недвижимость?",
|
||||
"answer": "Для получения кредита...",
|
||||
"sources": ["doc1.pdf", "doc2.pdf"]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"processing_time_ms": 1250
|
||||
}
|
||||
},
|
||||
"annotations": {
|
||||
"0": {
|
||||
"overall": {
|
||||
"rating": "correct",
|
||||
"comment": "Ответ корректный и полный"
|
||||
},
|
||||
"body_research": {
|
||||
"issues": [],
|
||||
"comment": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"created_at": "2025-12-24T14:00:00Z",
|
||||
"updated_at": "2025-12-24T14:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Errors
|
||||
|
||||
- **400 Bad Request**: Неверный формат данных
|
||||
- **404 Not Found**: Пользователь не найден
|
||||
- **500 Internal Server Error**: Ошибка сервера
|
||||
|
||||
---
|
||||
|
||||
### GET /users/{user_id}/sessions
|
||||
|
||||
Получить список сессий пользователя с пагинацией.
|
||||
|
||||
#### Path Parameters
|
||||
|
||||
- `user_id` (UUID, required): UUID пользователя
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
- `environment` (string, optional): Фильтр по окружению (`ift`, `psi`, `prod`)
|
||||
- `limit` (integer, optional): Лимит результатов (1-200, default: 50)
|
||||
- `offset` (integer, optional): Смещение для пагинации (default: 0)
|
||||
|
||||
#### Response 200
|
||||
|
||||
```
|
||||
{
|
||||
"sessions": [
|
||||
{
|
||||
"session_id": "660e8400-e29b-41d4-a716-446655440000",
|
||||
"environment": "ift",
|
||||
"created_at": "2025-12-24T14:00:00Z"
|
||||
},
|
||||
{
|
||||
"session_id": "770e8400-e29b-41d4-a716-446655440000",
|
||||
"environment": "ift",
|
||||
"created_at": "2025-12-23T10:30:00Z"
|
||||
}
|
||||
],
|
||||
"total": 123
|
||||
}
|
||||
```
|
||||
|
||||
**Сортировка:** По дате создания (от новых к старым)
|
||||
|
||||
#### Errors
|
||||
|
||||
- **404 Not Found**: Пользователь не найден
|
||||
- **500 Internal Server Error**: Ошибка сервера
|
||||
|
||||
---
|
||||
|
||||
### GET /users/{user_id}/sessions/{session_id}
|
||||
|
||||
Получить детальную информацию о сессии.
|
||||
|
||||
#### Path Parameters
|
||||
|
||||
- `user_id` (UUID, required): UUID пользователя
|
||||
- `session_id` (UUID, required): UUID сессии
|
||||
|
||||
#### Response 200
|
||||
|
||||
```
|
||||
{
|
||||
"session_id": "660e8400-e29b-41d4-a716-446655440000",
|
||||
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"environment": "ift",
|
||||
"api_mode": "bench",
|
||||
"request": [
|
||||
{
|
||||
"body": "Как получить кредит на недвижимость?",
|
||||
"with_docs": true
|
||||
}
|
||||
],
|
||||
"response": {
|
||||
"answers": [
|
||||
{
|
||||
"question": "Как получить кредит на недвижимость?",
|
||||
"answer": "Для получения кредита...",
|
||||
"sources": ["doc1.pdf"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"annotations": {
|
||||
"0": {
|
||||
"overall": {
|
||||
"rating": "correct",
|
||||
"comment": "Ответ корректный"
|
||||
}
|
||||
}
|
||||
},
|
||||
"created_at": "2025-12-24T14:00:00Z",
|
||||
"updated_at": "2025-12-24T14:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Errors
|
||||
|
||||
- **404 Not Found**: Сессия или пользователь не найдены
|
||||
- **500 Internal Server Error**: Ошибка сервера
|
||||
|
||||
---
|
||||
|
||||
### PATCH /users/{user_id}/sessions/{session_id}
|
||||
|
||||
Обновить аннотации сессии (например, после ревью).
|
||||
|
||||
#### Path Parameters
|
||||
|
||||
- `user_id` (UUID, required): UUID пользователя
|
||||
- `session_id` (UUID, required): UUID сессии
|
||||
|
||||
#### Request
|
||||
|
||||
```
|
||||
{
|
||||
"annotations": {
|
||||
"0": {
|
||||
"overall": {
|
||||
"rating": "incorrect",
|
||||
"comment": "Ответ неполный, не хватает информации о процентах"
|
||||
},
|
||||
"body_research": {
|
||||
"issues": ["missing_info"],
|
||||
"comment": "Не указаны процентные ставки"
|
||||
}
|
||||
},
|
||||
"1": {
|
||||
"overall": {
|
||||
"rating": "correct",
|
||||
"comment": "Ответ корректный"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Примечания:**
|
||||
- Ключи `annotations` должны быть числовыми строками (`"0"`, `"1"`, ...)
|
||||
- Полностью заменяет существующие аннотации
|
||||
|
||||
#### Response 200
|
||||
|
||||
```
|
||||
{
|
||||
"session_id": "660e8400-e29b-41d4-a716-446655440000",
|
||||
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"environment": "ift",
|
||||
"api_mode": "bench",
|
||||
"request": [...],
|
||||
"response": {...},
|
||||
"annotations": {
|
||||
"0": {
|
||||
"overall": {
|
||||
"rating": "incorrect",
|
||||
"comment": "Ответ неполный, не хватает информации о процентах"
|
||||
},
|
||||
"body_research": {
|
||||
"issues": ["missing_info"],
|
||||
"comment": "Не указаны процентные ставки"
|
||||
}
|
||||
},
|
||||
"1": {
|
||||
"overall": {
|
||||
"rating": "correct",
|
||||
"comment": "Ответ корректный"
|
||||
}
|
||||
}
|
||||
},
|
||||
"created_at": "2025-12-24T14:00:00Z",
|
||||
"updated_at": "2025-12-24T14:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Errors
|
||||
|
||||
- **400 Bad Request**: Неверный формат данных
|
||||
- **404 Not Found**: Сессия или пользователь не найдены
|
||||
- **500 Internal Server Error**: Ошибка сервера
|
||||
|
||||
---
|
||||
|
||||
### DELETE /users/{user_id}/sessions/{session_id}
|
||||
|
||||
Удалить сессию.
|
||||
|
||||
#### Path Parameters
|
||||
|
||||
- `user_id` (UUID, required): UUID пользователя
|
||||
- `session_id` (UUID, required): UUID сессии
|
||||
|
||||
#### Response 204
|
||||
|
||||
Нет тела ответа при успешном удалении.
|
||||
|
||||
#### Errors
|
||||
|
||||
- **404 Not Found**: Сессия или пользователь не найдены
|
||||
- **500 Internal Server Error**: Ошибка сервера
|
||||
|
||||
---
|
||||
|
||||
## Форматы данных
|
||||
|
||||
### Environment Settings Object
|
||||
|
||||
```
|
||||
{
|
||||
"apiMode": "bench",
|
||||
"bearerToken": "token-value",
|
||||
"systemPlatform": "platform-name",
|
||||
"systemPlatformUser": "user-name",
|
||||
"platformUserId": "user-id",
|
||||
"platformId": "platform-id",
|
||||
"withClassify": false,
|
||||
"resetSessionMode": true
|
||||
}
|
||||
```
|
||||
|
||||
**Поля:**
|
||||
- `apiMode` (string): `bench` или `backend`
|
||||
- `bearerToken` (string, nullable): Bearer токен для API
|
||||
- `systemPlatform` (string, nullable): Название платформы
|
||||
- `systemPlatformUser` (string, nullable): Пользователь платформы
|
||||
- `platformUserId` (string, nullable): ID пользователя в платформе
|
||||
- `platformId` (string, nullable): ID платформы
|
||||
- `withClassify` (boolean): Использовать классификацию
|
||||
- `resetSessionMode` (boolean): Режим сброса сессии
|
||||
|
||||
**Default значения:**
|
||||
- `apiMode`: `"bench"`
|
||||
- `withClassify`: `false`
|
||||
- `resetSessionMode`: `true`
|
||||
- Все остальные: `null`
|
||||
|
||||
---
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
Все ошибки возвращаются в едином формате:
|
||||
|
||||
```
|
||||
{
|
||||
"detail": "Описание ошибки",
|
||||
"error_code": "OPTIONAL_ERROR_CODE"
|
||||
}
|
||||
```
|
||||
|
||||
### Коды ошибок
|
||||
|
||||
| HTTP Code | Описание | Когда возникает |
|
||||
|-----------|----------|-----------------|
|
||||
| 400 | Bad Request | Неверный формат данных в запросе |
|
||||
| 404 | Not Found | Пользователь или сессия не найдены |
|
||||
| 422 | Unprocessable Entity | Ошибка валидации Pydantic |
|
||||
| 500 | Internal Server Error | Внутренняя ошибка сервера |
|
||||
|
||||
### Примеры ошибок
|
||||
|
||||
**400 - Неверный формат логина:**
|
||||
```
|
||||
{
|
||||
"detail": "login: String should match pattern '^\\d{8}$'",
|
||||
"error_code": null
|
||||
}
|
||||
```
|
||||
|
||||
**404 - Пользователь не найден:**
|
||||
```
|
||||
{
|
||||
"detail": "User 550e8400-e29b-41d4-a716-446655440000 not found",
|
||||
"error_code": null
|
||||
}
|
||||
```
|
||||
|
||||
**422 - Ошибка валидации annotations:**
|
||||
```
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "annotations"],
|
||||
"msg": "annotations keys must be numeric strings (e.g. '0', '1')",
|
||||
"type": "value_error"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Примеры использования
|
||||
|
||||
### Сценарий 1: Создание пользователя и сохранение настроек
|
||||
|
||||
```
|
||||
# 1. Логин пользователя
|
||||
curl -X POST /api/v1/users/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"login": "12345678",
|
||||
"client_ip": "MTkyLjE2OC4xLjEwMA=="
|
||||
}'
|
||||
|
||||
# Response: {"user_id": "550e8400-...", ...}
|
||||
|
||||
# 2. Настройка окружения IFT
|
||||
curl -X PATCH /api/v1/users/550e8400-e29b-41d4-a716-446655440000/settings \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"settings": {
|
||||
"ift": {
|
||||
"bearerToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"systemPlatform": "platform-ift",
|
||||
"withClassify": true
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Сценарий 2: Создание и аннотация сессии
|
||||
|
||||
```
|
||||
# 1. Создание сессии анализа
|
||||
curl -X POST /api/v1/users/550e8400-e29b-41d4-a716-446655440000/sessions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"environment": "ift",
|
||||
"api_mode": "bench",
|
||||
"request": [
|
||||
{
|
||||
"body": "Как получить кредит?",
|
||||
"with_docs": true
|
||||
}
|
||||
],
|
||||
"response": {
|
||||
"answers": [{
|
||||
"question": "Как получить кредит?",
|
||||
"answer": "Для получения кредита..."
|
||||
}]
|
||||
}
|
||||
}'
|
||||
|
||||
# Response: {"session_id": "660e8400-...", ...}
|
||||
|
||||
# 2. Добавление аннотаций после ревью
|
||||
curl -X PATCH /api/v1/users/550e8400-e29b-41d4-a716-446655440000/sessions/660e8400-e29b-41d4-a716-446655440000 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"annotations": {
|
||||
"0": {
|
||||
"overall": {
|
||||
"rating": "correct",
|
||||
"comment": "Ответ полный и корректный"
|
||||
}
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Сценарий 3: Получение истории сессий
|
||||
|
||||
```
|
||||
# Список всех сессий пользователя
|
||||
curl -X GET /api/v1/users/550e8400-e29b-41d4-a716-446655440000/sessions?limit=10&offset=0
|
||||
|
||||
# Фильтр только по IFT окружению
|
||||
curl -X GET /api/v1/users/550e8400-e29b-41d4-a716-446655440000/sessions?environment=ift&limit=20
|
||||
|
||||
# Получение конкретной сессии
|
||||
curl -X GET /api/v1/users/550e8400-e29b-41d4-a716-446655440000/sessions/660e8400-e29b-41d4-a716-446655440000
|
||||
```
|
||||
|
||||
|
||||
### Особенности реализации
|
||||
|
||||
1. **UUID генерация**: UUID создаются на уровне приложения (Python `uuid.uuid4()`)
|
||||
2. **Timestamps**: Автоматическое проставление `load_dttm` при INSERT, `updated_dttm` при UPDATE через триггеры
|
||||
3. **JSONB**: Все JSON-данные хранятся в PostgreSQL JSONB для эффективного поиска
|
||||
4. **Индексы**: Составные индексы на `(user_id, environment, load_dttm)` для быстрой пагинации
|
||||
5. **Безопасность**: Base64-кодирование client_ip для защиты от SQL-инъекций
|
||||
|
||||
---
|
||||
|
|
@ -6,7 +6,7 @@ Analysis Sessions API endpoints.
|
|||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from typing import Optional
|
||||
from app.models.analysis import SessionCreate, SessionResponse, SessionList
|
||||
from app.models.analysis import SessionCreate, SessionResponse, SessionList, SessionUpdate
|
||||
from app.interfaces.db_api_client import DBApiClient
|
||||
from app.dependencies import get_db_client, get_current_user
|
||||
import httpx
|
||||
|
|
@ -143,6 +143,54 @@ async def get_session(
|
|||
)
|
||||
|
||||
|
||||
@router.patch("/sessions/{session_id}", response_model=SessionResponse)
|
||||
async def update_session(
|
||||
session_id: str,
|
||||
update_data: SessionUpdate,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db_client: DBApiClient = Depends(get_db_client)
|
||||
):
|
||||
"""
|
||||
Обновить аннотации сессии (например, после ревью).
|
||||
|
||||
Полностью заменяет существующие аннотации новыми.
|
||||
|
||||
Args:
|
||||
session_id: UUID сессии
|
||||
update_data: Новые аннотации с ключами в виде числовых строк ('0', '1', ...)
|
||||
|
||||
Returns:
|
||||
SessionResponse: Обновленная сессия
|
||||
"""
|
||||
user_id = current_user["user_id"]
|
||||
|
||||
try:
|
||||
updated_session = await db_client.update_session(user_id, session_id, update_data)
|
||||
return updated_session
|
||||
except httpx.HTTPStatusError as e:
|
||||
if e.response.status_code == 404:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Session not found"
|
||||
)
|
||||
elif e.response.status_code == 400:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid annotations format"
|
||||
)
|
||||
logger.error(f"Failed to update session {session_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail="Failed to update session in DB API"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error updating session {session_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Internal server error"
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/sessions/{session_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_session(
|
||||
session_id: str,
|
||||
|
|
|
|||
|
|
@ -51,20 +51,22 @@ async def get_settings(
|
|||
)
|
||||
|
||||
|
||||
@router.put("", response_model=UserSettings)
|
||||
@router.patch("", response_model=UserSettings)
|
||||
async def update_settings(
|
||||
settings_update: UserSettingsUpdate,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db_client: DBApiClient = Depends(get_db_client)
|
||||
):
|
||||
"""
|
||||
Обновить настройки пользователя.
|
||||
Частично обновить настройки пользователя.
|
||||
|
||||
Обновляются только переданные поля. Непереданные поля остаются без изменений.
|
||||
|
||||
Args:
|
||||
settings_update: Новые настройки для одного или нескольких окружений
|
||||
settings_update: Частичные настройки для одного или нескольких окружений
|
||||
|
||||
Returns:
|
||||
UserSettings: Обновленные настройки
|
||||
UserSettings: Обновленные настройки со всеми полями
|
||||
"""
|
||||
user_id = current_user["user_id"]
|
||||
|
||||
|
|
|
|||
|
|
@ -252,6 +252,36 @@ class TgBackendInterface:
|
|||
response = await self.client.put(url, json=json_body, **kwargs)
|
||||
return await self._handle_response(response, response_model)
|
||||
|
||||
async def patch(
|
||||
self,
|
||||
path: str,
|
||||
body: Optional[BaseModel] = None,
|
||||
response_model: Optional[Type[T]] = None,
|
||||
**kwargs
|
||||
) -> Any:
|
||||
"""
|
||||
HTTP PATCH запрос к {api_prefix}{path}.
|
||||
|
||||
Args:
|
||||
path: Путь эндпоинта
|
||||
body: Pydantic модель для тела запроса
|
||||
response_model: Pydantic модель для валидации ответа
|
||||
**kwargs: Дополнительные параметры для httpx
|
||||
|
||||
Returns:
|
||||
Десериализованный ответ (Pydantic модель или dict)
|
||||
|
||||
Raises:
|
||||
httpx.HTTPStatusError: При HTTP ошибках
|
||||
ValidationError: При ошибках валидации Pydantic
|
||||
"""
|
||||
url = self._build_url(path)
|
||||
json_body = self._serialize_body(body)
|
||||
logger.debug(f"PATCH {url} with body={json_body}")
|
||||
|
||||
response = await self.client.patch(url, json=json_body, **kwargs)
|
||||
return await self._handle_response(response, response_model)
|
||||
|
||||
async def delete(
|
||||
self,
|
||||
path: str,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
from app.interfaces.base import TgBackendInterface
|
||||
from app.models.auth import LoginRequest, UserResponse
|
||||
from app.models.settings import UserSettings, UserSettingsUpdate
|
||||
from app.models.analysis import SessionCreate, SessionResponse, SessionList
|
||||
from app.models.analysis import SessionCreate, SessionResponse, SessionList, SessionUpdate
|
||||
|
||||
|
||||
class DBApiClient(TgBackendInterface):
|
||||
|
|
@ -11,7 +11,7 @@ class DBApiClient(TgBackendInterface):
|
|||
Клиент для DB API сервиса.
|
||||
|
||||
Использует Pydantic схемы для type-safety.
|
||||
Методы self.get(), self.post(), self.put(), self.delete() от TgBackendInterface.
|
||||
Методы self.get(), self.post(), self.patch(), self.delete() от TgBackendInterface.
|
||||
"""
|
||||
|
||||
async def login_user(self, request: LoginRequest) -> UserResponse:
|
||||
|
|
@ -36,11 +36,12 @@ class DBApiClient(TgBackendInterface):
|
|||
settings: UserSettingsUpdate
|
||||
) -> UserSettings:
|
||||
"""
|
||||
PUT {api_prefix}/users/{user_id}/settings
|
||||
PATCH {api_prefix}/users/{user_id}/settings
|
||||
|
||||
Обновить настройки пользователя.
|
||||
Частично обновить настройки пользователя.
|
||||
Обновляются только переданные поля.
|
||||
"""
|
||||
return await self.put(
|
||||
return await self.patch(
|
||||
f"/users/{user_id}/settings",
|
||||
body=settings,
|
||||
response_model=UserSettings
|
||||
|
|
@ -94,6 +95,23 @@ class DBApiClient(TgBackendInterface):
|
|||
response_model=SessionResponse
|
||||
)
|
||||
|
||||
async def update_session(
|
||||
self,
|
||||
user_id: str,
|
||||
session_id: str,
|
||||
update_data: SessionUpdate
|
||||
) -> SessionResponse:
|
||||
"""
|
||||
PATCH {api_prefix}/users/{user_id}/sessions/{session_id}
|
||||
|
||||
Обновить аннотации сессии (например, после ревью).
|
||||
"""
|
||||
return await self.patch(
|
||||
f"/users/{user_id}/sessions/{session_id}",
|
||||
body=update_data,
|
||||
response_model=SessionResponse
|
||||
)
|
||||
|
||||
async def delete_session(self, user_id: str, session_id: str) -> dict:
|
||||
"""
|
||||
DELETE {api_prefix}/users/{user_id}/sessions/{session_id}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,29 @@
|
|||
"""Analysis session Pydantic models."""
|
||||
|
||||
from typing import Any
|
||||
from pydantic import BaseModel
|
||||
from typing import Any, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class SessionCreate(BaseModel):
|
||||
"""Create new analysis session."""
|
||||
|
||||
environment: str
|
||||
api_mode: str
|
||||
request: list[Any]
|
||||
response: dict
|
||||
annotations: dict
|
||||
environment: str = Field(..., description="Environment: ift, psi, or prod")
|
||||
api_mode: str = Field(..., description="API mode: bench or backend")
|
||||
request: list[Any] = Field(..., description="Array of request objects")
|
||||
response: dict = Field(..., description="Response object (arbitrary structure)")
|
||||
annotations: Optional[dict] = Field(default={}, description="Annotations by question index")
|
||||
|
||||
|
||||
class SessionUpdate(BaseModel):
|
||||
"""Update session annotations (PATCH).
|
||||
|
||||
According to DB_API_CONTRACT_V2.md:
|
||||
- PATCH /users/{user_id}/sessions/{session_id}
|
||||
- Used to update annotations after review
|
||||
- Completely replaces existing annotations
|
||||
"""
|
||||
|
||||
annotations: dict = Field(..., description="Annotations with numeric string keys ('0', '1', ...)")
|
||||
|
||||
|
||||
class SessionResponse(BaseModel):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""Query request/response Pydantic models."""
|
||||
|
||||
from typing import Any
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class QuestionRequest(BaseModel):
|
||||
|
|
@ -26,10 +26,42 @@ class BackendQueryRequest(BaseModel):
|
|||
reset_session: bool = True
|
||||
|
||||
|
||||
class Docs(BaseModel):
|
||||
"""Documents from RAG."""
|
||||
|
||||
research: list
|
||||
analytical_hub: list
|
||||
|
||||
|
||||
class RagResponse(BaseModel):
|
||||
"""Ответ от RAG на вопрос пользователя."""
|
||||
|
||||
body_research: str = Field(description="Текст ответа от Research на вопрос")
|
||||
body_analytical_hub: str = Field(description="Текст ответа от Analytical Hub на вопрос")
|
||||
docs_from_vectorstore: Docs | None = None
|
||||
docs_to_llm: Docs | None = None
|
||||
|
||||
|
||||
class RagResponseBench(RagResponse):
|
||||
"""Ответ на вопрос + время обработки именно этого вопроса."""
|
||||
|
||||
processing_time_sec: float = Field(
|
||||
description="Время обработки запроса в секундах",
|
||||
ge=0,
|
||||
)
|
||||
question: str = Field(description="Исходный вопрос")
|
||||
|
||||
|
||||
class RagResponseBenchList(BaseModel):
|
||||
"""Список ответов RAG в bench режиме."""
|
||||
|
||||
answers: list[RagResponseBench]
|
||||
|
||||
|
||||
class QueryResponse(BaseModel):
|
||||
"""Query response with metadata."""
|
||||
|
||||
request_id: str
|
||||
timestamp: str
|
||||
environment: str
|
||||
response: dict
|
||||
response: RagResponseBenchList | dict | list # RagResponseBenchList для bench, dict/list для backend
|
||||
|
|
|
|||
|
|
@ -1,21 +1,43 @@
|
|||
"""User settings Pydantic models."""
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class EnvironmentSettings(BaseModel):
|
||||
"""Settings for a specific environment (IFT/PSI/PROD)."""
|
||||
"""Settings for a specific environment (IFT/PSI/PROD).
|
||||
|
||||
According to DB_API_CONTRACT_V2.md:
|
||||
- apiMode, withClassify, resetSessionMode have defaults
|
||||
- All other fields are nullable (Optional)
|
||||
"""
|
||||
|
||||
apiMode: str = "bench"
|
||||
bearerToken: str = ""
|
||||
systemPlatform: str = ""
|
||||
systemPlatformUser: str = ""
|
||||
platformUserId: str = ""
|
||||
platformId: str = ""
|
||||
bearerToken: Optional[str] = None
|
||||
systemPlatform: Optional[str] = None
|
||||
systemPlatformUser: Optional[str] = None
|
||||
platformUserId: Optional[str] = None
|
||||
platformId: Optional[str] = None
|
||||
withClassify: bool = False
|
||||
resetSessionMode: bool = True
|
||||
|
||||
|
||||
class EnvironmentSettingsUpdate(BaseModel):
|
||||
"""Partial update for environment settings (for PATCH requests).
|
||||
|
||||
All fields are optional to support partial updates.
|
||||
"""
|
||||
|
||||
apiMode: Optional[str] = None
|
||||
bearerToken: Optional[str] = None
|
||||
systemPlatform: Optional[str] = None
|
||||
systemPlatformUser: Optional[str] = None
|
||||
platformUserId: Optional[str] = None
|
||||
platformId: Optional[str] = None
|
||||
withClassify: Optional[bool] = None
|
||||
resetSessionMode: Optional[bool] = None
|
||||
|
||||
|
||||
class UserSettings(BaseModel):
|
||||
"""User settings for all environments."""
|
||||
|
||||
|
|
@ -25,6 +47,10 @@ class UserSettings(BaseModel):
|
|||
|
||||
|
||||
class UserSettingsUpdate(BaseModel):
|
||||
"""Update user settings request."""
|
||||
"""Partial update user settings request (PATCH).
|
||||
|
||||
settings: dict[str, EnvironmentSettings]
|
||||
Only the environments/fields provided will be updated.
|
||||
Unprovided fields remain unchanged.
|
||||
"""
|
||||
|
||||
settings: dict[str, EnvironmentSettingsUpdate]
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import uuid
|
|||
from typing import List, Dict, Optional, Any
|
||||
from datetime import datetime
|
||||
from app.config import settings
|
||||
from app.models.query import QuestionRequest
|
||||
from app.models.query import QuestionRequest, RagResponseBenchList
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -200,7 +200,7 @@ class RagService:
|
|||
questions: List[QuestionRequest],
|
||||
user_settings: Dict,
|
||||
request_id: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
) -> RagResponseBenchList:
|
||||
"""
|
||||
Отправить batch запрос к RAG backend (bench mode).
|
||||
|
||||
|
|
@ -211,7 +211,7 @@ class RagService:
|
|||
request_id: Request ID (опционально)
|
||||
|
||||
Returns:
|
||||
Dict с ответом от RAG backend
|
||||
RagResponseBenchList с ответом от RAG backend
|
||||
|
||||
Raises:
|
||||
httpx.HTTPStatusError: При HTTP ошибках
|
||||
|
|
@ -220,7 +220,7 @@ class RagService:
|
|||
url = self._get_bench_endpoint(environment)
|
||||
headers = self._build_bench_headers(environment, user_settings, request_id)
|
||||
|
||||
|
||||
|
||||
body = [q.model_dump() for q in questions]
|
||||
|
||||
logger.info(f"Sending bench query to {environment}: {len(questions)} questions")
|
||||
|
|
@ -229,7 +229,10 @@ class RagService:
|
|||
try:
|
||||
response = await client.post(url, json=body, headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
response_data = response.json()
|
||||
|
||||
# Валидация ответа через Pydantic модель
|
||||
return RagResponseBenchList(**response_data)
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Bench query failed for {environment}: {e.response.status_code} - {e.response.text}")
|
||||
raise
|
||||
|
|
|
|||
260
coverage.xml
260
coverage.xml
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" ?>
|
||||
<coverage version="7.13.0" timestamp="1766039646938" lines-valid="567" lines-covered="563" line-rate="0.9929" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0">
|
||||
<coverage version="7.13.0" timestamp="1766645017585" lines-valid="617" lines-covered="594" line-rate="0.9627" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0">
|
||||
<!-- Generated by coverage.py: https://coverage.readthedocs.io/en/7.13.0 -->
|
||||
<!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd -->
|
||||
<sources>
|
||||
<source>C:\Users\leonk\Documents\code\brief-bench-fastapi\app</source>
|
||||
<source>C:\Users\itqop\Documents\code\brief-rags-bench\app</source>
|
||||
</sources>
|
||||
<packages>
|
||||
<package name="." line-rate="0.971" branch-rate="0" complexity="0">
|
||||
|
|
@ -18,33 +18,33 @@
|
|||
<line number="3" hits="1"/>
|
||||
<line number="6" hits="1"/>
|
||||
<line number="9" hits="1"/>
|
||||
<line number="15" hits="1"/>
|
||||
<line number="16" hits="1"/>
|
||||
<line number="17" hits="1"/>
|
||||
<line number="18" hits="1"/>
|
||||
<line number="19" hits="1"/>
|
||||
<line number="20" hits="1"/>
|
||||
<line number="21" hits="1"/>
|
||||
<line number="22" hits="1"/>
|
||||
<line number="23" hits="1"/>
|
||||
<line number="25" hits="1"/>
|
||||
<line number="26" hits="1"/>
|
||||
<line number="27" hits="1"/>
|
||||
<line number="28" hits="1"/>
|
||||
<line number="29" hits="1"/>
|
||||
<line number="30" hits="1"/>
|
||||
<line number="31" hits="1"/>
|
||||
<line number="32" hits="1"/>
|
||||
<line number="33" hits="1"/>
|
||||
<line number="34" hits="1"/>
|
||||
<line number="35" hits="1"/>
|
||||
<line number="36" hits="1"/>
|
||||
<line number="37" hits="1"/>
|
||||
<line number="38" hits="1"/>
|
||||
<line number="39" hits="1"/>
|
||||
<line number="40" hits="1"/>
|
||||
<line number="41" hits="1"/>
|
||||
<line number="42" hits="1"/>
|
||||
<line number="45" hits="1"/>
|
||||
<line number="43" hits="1"/>
|
||||
<line number="44" hits="1"/>
|
||||
<line number="46" hits="1"/>
|
||||
<line number="47" hits="1"/>
|
||||
<line number="48" hits="1"/>
|
||||
<line number="49" hits="1"/>
|
||||
<line number="50" hits="1"/>
|
||||
<line number="53" hits="1"/>
|
||||
<line number="57" hits="1"/>
|
||||
</lines>
|
||||
</class>
|
||||
<class name="dependencies.py" filename="dependencies.py" complexity="0" line-rate="1" branch-rate="0">
|
||||
|
|
@ -75,25 +75,25 @@
|
|||
<line number="6" hits="1"/>
|
||||
<line number="8" hits="1"/>
|
||||
<line number="9" hits="1"/>
|
||||
<line number="12" hits="1"/>
|
||||
<line number="19" hits="1"/>
|
||||
<line number="11" hits="1"/>
|
||||
<line number="17" hits="1"/>
|
||||
<line number="25" hits="1"/>
|
||||
<line number="26" hits="1"/>
|
||||
<line number="27" hits="1"/>
|
||||
<line number="28" hits="1"/>
|
||||
<line number="29" hits="1"/>
|
||||
<line number="30" hits="1"/>
|
||||
<line number="31" hits="1"/>
|
||||
<line number="33" hits="1"/>
|
||||
<line number="34" hits="1"/>
|
||||
<line number="37" hits="1"/>
|
||||
<line number="38" hits="1"/>
|
||||
<line number="36" hits="1"/>
|
||||
<line number="39" hits="1"/>
|
||||
<line number="40" hits="1"/>
|
||||
<line number="43" hits="1"/>
|
||||
<line number="44" hits="1"/>
|
||||
<line number="42" hits="1"/>
|
||||
<line number="45" hits="1"/>
|
||||
<line number="46" hits="1"/>
|
||||
<line number="49" hits="1"/>
|
||||
<line number="50" hits="1"/>
|
||||
<line number="52" hits="1"/>
|
||||
<line number="55" hits="1"/>
|
||||
<line number="56" hits="0"/>
|
||||
<line number="57" hits="0"/>
|
||||
<line number="48" hits="1"/>
|
||||
<line number="51" hits="1"/>
|
||||
<line number="52" hits="0"/>
|
||||
<line number="53" hits="0"/>
|
||||
</lines>
|
||||
</class>
|
||||
</classes>
|
||||
|
|
@ -106,7 +106,7 @@
|
|||
</class>
|
||||
</classes>
|
||||
</package>
|
||||
<package name="api.v1" line-rate="0.9894" branch-rate="0" complexity="0">
|
||||
<package name="api.v1" line-rate="0.9216" branch-rate="0" complexity="0">
|
||||
<classes>
|
||||
<class name="__init__.py" filename="api/v1/__init__.py" complexity="0" line-rate="1" branch-rate="0">
|
||||
<methods/>
|
||||
|
|
@ -115,7 +115,7 @@
|
|||
<line number="7" hits="1"/>
|
||||
</lines>
|
||||
</class>
|
||||
<class name="analysis.py" filename="api/v1/analysis.py" complexity="0" line-rate="1" branch-rate="0">
|
||||
<class name="analysis.py" filename="api/v1/analysis.py" complexity="0" line-rate="0.8313" branch-rate="0">
|
||||
<methods/>
|
||||
<lines>
|
||||
<line number="7" hits="1"/>
|
||||
|
|
@ -173,18 +173,34 @@
|
|||
<line number="140" hits="1"/>
|
||||
<line number="146" hits="1"/>
|
||||
<line number="147" hits="1"/>
|
||||
<line number="161" hits="1"/>
|
||||
<line number="163" hits="1"/>
|
||||
<line number="164" hits="1"/>
|
||||
<line number="165" hits="1"/>
|
||||
<line number="166" hits="1"/>
|
||||
<line number="167" hits="1"/>
|
||||
<line number="168" hits="1"/>
|
||||
<line number="172" hits="1"/>
|
||||
<line number="173" hits="1"/>
|
||||
<line number="177" hits="1"/>
|
||||
<line number="178" hits="1"/>
|
||||
<line number="179" hits="1"/>
|
||||
<line number="165" hits="0"/>
|
||||
<line number="167" hits="0"/>
|
||||
<line number="168" hits="0"/>
|
||||
<line number="169" hits="0"/>
|
||||
<line number="170" hits="0"/>
|
||||
<line number="171" hits="0"/>
|
||||
<line number="172" hits="0"/>
|
||||
<line number="176" hits="0"/>
|
||||
<line number="177" hits="0"/>
|
||||
<line number="181" hits="0"/>
|
||||
<line number="182" hits="0"/>
|
||||
<line number="186" hits="0"/>
|
||||
<line number="187" hits="0"/>
|
||||
<line number="188" hits="0"/>
|
||||
<line number="194" hits="1"/>
|
||||
<line number="195" hits="1"/>
|
||||
<line number="209" hits="1"/>
|
||||
<line number="211" hits="1"/>
|
||||
<line number="212" hits="1"/>
|
||||
<line number="213" hits="1"/>
|
||||
<line number="214" hits="1"/>
|
||||
<line number="215" hits="1"/>
|
||||
<line number="216" hits="1"/>
|
||||
<line number="220" hits="1"/>
|
||||
<line number="221" hits="1"/>
|
||||
<line number="225" hits="1"/>
|
||||
<line number="226" hits="1"/>
|
||||
<line number="227" hits="1"/>
|
||||
</lines>
|
||||
</class>
|
||||
<class name="auth.py" filename="api/v1/auth.py" complexity="0" line-rate="1" branch-rate="0">
|
||||
|
|
@ -305,31 +321,31 @@
|
|||
<line number="48" hits="1"/>
|
||||
<line number="54" hits="1"/>
|
||||
<line number="55" hits="1"/>
|
||||
<line number="69" hits="1"/>
|
||||
<line number="71" hits="1"/>
|
||||
<line number="72" hits="1"/>
|
||||
<line number="73" hits="1"/>
|
||||
<line number="74" hits="1"/>
|
||||
<line number="75" hits="1"/>
|
||||
<line number="76" hits="1"/>
|
||||
<line number="80" hits="1"/>
|
||||
<line number="81" hits="1"/>
|
||||
<line number="85" hits="1"/>
|
||||
<line number="86" hits="1"/>
|
||||
<line number="90" hits="1"/>
|
||||
<line number="91" hits="1"/>
|
||||
<line number="77" hits="1"/>
|
||||
<line number="78" hits="1"/>
|
||||
<line number="82" hits="1"/>
|
||||
<line number="83" hits="1"/>
|
||||
<line number="87" hits="1"/>
|
||||
<line number="88" hits="1"/>
|
||||
<line number="92" hits="1"/>
|
||||
<line number="93" hits="1"/>
|
||||
<line number="94" hits="1"/>
|
||||
</lines>
|
||||
</class>
|
||||
</classes>
|
||||
</package>
|
||||
<package name="interfaces" line-rate="1" branch-rate="0" complexity="0">
|
||||
<package name="interfaces" line-rate="0.9505" branch-rate="0" complexity="0">
|
||||
<classes>
|
||||
<class name="__init__.py" filename="interfaces/__init__.py" complexity="0" line-rate="1" branch-rate="0">
|
||||
<methods/>
|
||||
<lines/>
|
||||
</class>
|
||||
<class name="base.py" filename="interfaces/base.py" complexity="0" line-rate="1" branch-rate="0">
|
||||
<class name="base.py" filename="interfaces/base.py" complexity="0" line-rate="0.9351" branch-rate="0">
|
||||
<methods/>
|
||||
<lines>
|
||||
<line number="9" hits="1"/>
|
||||
|
|
@ -399,10 +415,16 @@
|
|||
<line number="252" hits="1"/>
|
||||
<line number="253" hits="1"/>
|
||||
<line number="255" hits="1"/>
|
||||
<line number="273" hits="1"/>
|
||||
<line number="274" hits="1"/>
|
||||
<line number="276" hits="1"/>
|
||||
<line number="277" hits="1"/>
|
||||
<line number="278" hits="0"/>
|
||||
<line number="279" hits="0"/>
|
||||
<line number="280" hits="0"/>
|
||||
<line number="282" hits="0"/>
|
||||
<line number="283" hits="0"/>
|
||||
<line number="285" hits="1"/>
|
||||
<line number="303" hits="1"/>
|
||||
<line number="304" hits="1"/>
|
||||
<line number="306" hits="1"/>
|
||||
<line number="307" hits="1"/>
|
||||
</lines>
|
||||
</class>
|
||||
<class name="db_api_client.py" filename="interfaces/db_api_client.py" complexity="0" line-rate="1" branch-rate="0">
|
||||
|
|
@ -418,18 +440,20 @@
|
|||
<line number="25" hits="1"/>
|
||||
<line number="31" hits="1"/>
|
||||
<line number="33" hits="1"/>
|
||||
<line number="43" hits="1"/>
|
||||
<line number="49" hits="1"/>
|
||||
<line number="59" hits="1"/>
|
||||
<line number="65" hits="1"/>
|
||||
<line number="77" hits="1"/>
|
||||
<line number="44" hits="1"/>
|
||||
<line number="50" hits="1"/>
|
||||
<line number="60" hits="1"/>
|
||||
<line number="66" hits="1"/>
|
||||
<line number="78" hits="1"/>
|
||||
<line number="79" hits="1"/>
|
||||
<line number="80" hits="1"/>
|
||||
<line number="86" hits="1"/>
|
||||
<line number="92" hits="1"/>
|
||||
<line number="97" hits="1"/>
|
||||
<line number="103" hits="1"/>
|
||||
<line number="81" hits="1"/>
|
||||
<line number="87" hits="1"/>
|
||||
<line number="93" hits="1"/>
|
||||
<line number="98" hits="1"/>
|
||||
<line number="109" hits="1"/>
|
||||
<line number="115" hits="1"/>
|
||||
<line number="121" hits="1"/>
|
||||
</lines>
|
||||
</class>
|
||||
</classes>
|
||||
|
|
@ -460,22 +484,24 @@
|
|||
<line number="13" hits="1"/>
|
||||
<line number="14" hits="1"/>
|
||||
<line number="17" hits="1"/>
|
||||
<line number="20" hits="1"/>
|
||||
<line number="21" hits="1"/>
|
||||
<line number="22" hits="1"/>
|
||||
<line number="23" hits="1"/>
|
||||
<line number="24" hits="1"/>
|
||||
<line number="25" hits="1"/>
|
||||
<line number="26" hits="1"/>
|
||||
<line number="27" hits="1"/>
|
||||
<line number="28" hits="1"/>
|
||||
<line number="31" hits="1"/>
|
||||
<line number="29" hits="1"/>
|
||||
<line number="32" hits="1"/>
|
||||
<line number="33" hits="1"/>
|
||||
<line number="34" hits="1"/>
|
||||
<line number="35" hits="1"/>
|
||||
<line number="36" hits="1"/>
|
||||
<line number="37" hits="1"/>
|
||||
<line number="38" hits="1"/>
|
||||
<line number="39" hits="1"/>
|
||||
<line number="42" hits="1"/>
|
||||
<line number="40" hits="1"/>
|
||||
<line number="43" hits="1"/>
|
||||
<line number="46" hits="1"/>
|
||||
<line number="47" hits="1"/>
|
||||
<line number="48" hits="1"/>
|
||||
<line number="51" hits="1"/>
|
||||
<line number="54" hits="1"/>
|
||||
<line number="55" hits="1"/>
|
||||
</lines>
|
||||
</class>
|
||||
<class name="auth.py" filename="models/auth.py" complexity="0" line-rate="1" branch-rate="0">
|
||||
|
|
@ -518,29 +544,52 @@
|
|||
<line number="29" hits="1"/>
|
||||
<line number="32" hits="1"/>
|
||||
<line number="33" hits="1"/>
|
||||
<line number="34" hits="1"/>
|
||||
<line number="35" hits="1"/>
|
||||
<line number="36" hits="1"/>
|
||||
<line number="39" hits="1"/>
|
||||
<line number="40" hits="1"/>
|
||||
<line number="41" hits="1"/>
|
||||
<line number="42" hits="1"/>
|
||||
<line number="45" hits="1"/>
|
||||
<line number="48" hits="1"/>
|
||||
<line number="52" hits="1"/>
|
||||
<line number="55" hits="1"/>
|
||||
<line number="58" hits="1"/>
|
||||
<line number="61" hits="1"/>
|
||||
<line number="64" hits="1"/>
|
||||
<line number="65" hits="1"/>
|
||||
<line number="66" hits="1"/>
|
||||
<line number="67" hits="1"/>
|
||||
</lines>
|
||||
</class>
|
||||
<class name="settings.py" filename="models/settings.py" complexity="0" line-rate="1" branch-rate="0">
|
||||
<methods/>
|
||||
<lines>
|
||||
<line number="3" hits="1"/>
|
||||
<line number="6" hits="1"/>
|
||||
<line number="9" hits="1"/>
|
||||
<line number="10" hits="1"/>
|
||||
<line number="11" hits="1"/>
|
||||
<line number="12" hits="1"/>
|
||||
<line number="13" hits="1"/>
|
||||
<line number="14" hits="1"/>
|
||||
<line number="4" hits="1"/>
|
||||
<line number="7" hits="1"/>
|
||||
<line number="15" hits="1"/>
|
||||
<line number="16" hits="1"/>
|
||||
<line number="17" hits="1"/>
|
||||
<line number="18" hits="1"/>
|
||||
<line number="19" hits="1"/>
|
||||
<line number="20" hits="1"/>
|
||||
<line number="21" hits="1"/>
|
||||
<line number="22" hits="1"/>
|
||||
<line number="23" hits="1"/>
|
||||
<line number="24" hits="1"/>
|
||||
<line number="27" hits="1"/>
|
||||
<line number="30" hits="1"/>
|
||||
<line number="25" hits="1"/>
|
||||
<line number="31" hits="1"/>
|
||||
<line number="32" hits="1"/>
|
||||
<line number="33" hits="1"/>
|
||||
<line number="34" hits="1"/>
|
||||
<line number="35" hits="1"/>
|
||||
<line number="36" hits="1"/>
|
||||
<line number="37" hits="1"/>
|
||||
<line number="38" hits="1"/>
|
||||
<line number="41" hits="1"/>
|
||||
<line number="44" hits="1"/>
|
||||
<line number="45" hits="1"/>
|
||||
<line number="46" hits="1"/>
|
||||
<line number="49" hits="1"/>
|
||||
<line number="56" hits="1"/>
|
||||
</lines>
|
||||
</class>
|
||||
</classes>
|
||||
|
|
@ -649,40 +698,41 @@
|
|||
<line number="230" hits="1"/>
|
||||
<line number="231" hits="1"/>
|
||||
<line number="232" hits="1"/>
|
||||
<line number="233" hits="1"/>
|
||||
<line number="234" hits="1"/>
|
||||
<line number="235" hits="1"/>
|
||||
<line number="236" hits="1"/>
|
||||
<line number="237" hits="1"/>
|
||||
<line number="238" hits="1"/>
|
||||
<line number="239" hits="1"/>
|
||||
<line number="240" hits="1"/>
|
||||
<line number="264" hits="1"/>
|
||||
<line number="265" hits="1"/>
|
||||
<line number="266" hits="1"/>
|
||||
<line number="241" hits="1"/>
|
||||
<line number="243" hits="1"/>
|
||||
<line number="267" hits="1"/>
|
||||
<line number="268" hits="1"/>
|
||||
<line number="273" hits="1"/>
|
||||
<line number="275" hits="1"/>
|
||||
<line number="277" hits="1"/>
|
||||
<line number="269" hits="1"/>
|
||||
<line number="271" hits="1"/>
|
||||
<line number="276" hits="1"/>
|
||||
<line number="278" hits="1"/>
|
||||
<line number="285" hits="1"/>
|
||||
<line number="287" hits="1"/>
|
||||
<line number="289" hits="1"/>
|
||||
<line number="280" hits="1"/>
|
||||
<line number="281" hits="1"/>
|
||||
<line number="288" hits="1"/>
|
||||
<line number="290" hits="1"/>
|
||||
<line number="291" hits="1"/>
|
||||
<line number="292" hits="1"/>
|
||||
<line number="293" hits="1"/>
|
||||
<line number="294" hits="1"/>
|
||||
<line number="295" hits="1"/>
|
||||
<line number="296" hits="1"/>
|
||||
<line number="297" hits="1"/>
|
||||
<line number="298" hits="1"/>
|
||||
<line number="303" hits="1"/>
|
||||
<line number="305" hits="1"/>
|
||||
<line number="299" hits="1"/>
|
||||
<line number="300" hits="1"/>
|
||||
<line number="301" hits="1"/>
|
||||
<line number="306" hits="1"/>
|
||||
<line number="310" hits="1"/>
|
||||
<line number="311" hits="1"/>
|
||||
<line number="312" hits="1"/>
|
||||
<line number="308" hits="1"/>
|
||||
<line number="309" hits="1"/>
|
||||
<line number="313" hits="1"/>
|
||||
<line number="314" hits="1"/>
|
||||
<line number="315" hits="1"/>
|
||||
<line number="316" hits="1"/>
|
||||
<line number="318" hits="1"/>
|
||||
<line number="319" hits="1"/>
|
||||
</lines>
|
||||
</class>
|
||||
</classes>
|
||||
|
|
|
|||
|
|
@ -144,20 +144,36 @@ def unauthenticated_client():
|
|||
|
||||
@pytest.fixture
|
||||
def mock_bench_response():
|
||||
"""Mock RAG backend bench response."""
|
||||
"""Mock RAG backend bench response (matches format.py structure)."""
|
||||
return {
|
||||
"answers": [
|
||||
{
|
||||
"question_id": 1,
|
||||
"answer": "Test answer 1",
|
||||
"confidence": 0.95,
|
||||
"docs": []
|
||||
"body_research": "Test research answer 1",
|
||||
"body_analytical_hub": "Test analytical hub answer 1",
|
||||
"docs_from_vectorstore": {
|
||||
"research": [],
|
||||
"analytical_hub": []
|
||||
},
|
||||
"docs_to_llm": {
|
||||
"research": [],
|
||||
"analytical_hub": []
|
||||
},
|
||||
"processing_time_sec": 1.5,
|
||||
"question": "Test question 1"
|
||||
},
|
||||
{
|
||||
"question_id": 2,
|
||||
"answer": "Test answer 2",
|
||||
"confidence": 0.87,
|
||||
"docs": []
|
||||
"body_research": "Test research answer 2",
|
||||
"body_analytical_hub": "Test analytical hub answer 2",
|
||||
"docs_from_vectorstore": {
|
||||
"research": [],
|
||||
"analytical_hub": []
|
||||
},
|
||||
"docs_to_llm": {
|
||||
"research": [],
|
||||
"analytical_hub": []
|
||||
},
|
||||
"processing_time_sec": 2.3,
|
||||
"question": "Test question 2"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import pytest
|
|||
from unittest.mock import AsyncMock, patch, MagicMock
|
||||
from app.interfaces.db_api_client import DBApiClient
|
||||
from app.models.auth import LoginRequest, UserResponse
|
||||
from app.models.settings import UserSettings, UserSettingsUpdate, EnvironmentSettings
|
||||
from app.models.analysis import SessionCreate, SessionResponse, SessionList, SessionListItem
|
||||
from app.models.settings import UserSettings, UserSettingsUpdate, EnvironmentSettings, EnvironmentSettingsUpdate
|
||||
from app.models.analysis import SessionCreate, SessionResponse, SessionList, SessionListItem, SessionUpdate
|
||||
|
||||
|
||||
class TestDBApiClient:
|
||||
|
|
@ -70,19 +70,14 @@ class TestDBApiClient:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_user_settings(self):
|
||||
"""Test update_user_settings calls put correctly."""
|
||||
"""Test update_user_settings calls patch correctly."""
|
||||
with patch('app.interfaces.base.httpx.AsyncClient'):
|
||||
client = DBApiClient(api_prefix="http://db-api:8080/api/v1")
|
||||
|
||||
settings_update = UserSettingsUpdate(
|
||||
settings={
|
||||
"ift": EnvironmentSettings(
|
||||
"ift": EnvironmentSettingsUpdate(
|
||||
apiMode="backend",
|
||||
bearerToken="",
|
||||
systemPlatform="",
|
||||
systemPlatformUser="",
|
||||
platformUserId="",
|
||||
platformId="",
|
||||
withClassify=True,
|
||||
resetSessionMode=False
|
||||
)
|
||||
|
|
@ -90,15 +85,26 @@ class TestDBApiClient:
|
|||
)
|
||||
mock_updated_settings = UserSettings(
|
||||
user_id="user-123",
|
||||
settings=settings_update.settings,
|
||||
settings={
|
||||
"ift": EnvironmentSettings(
|
||||
apiMode="backend",
|
||||
bearerToken=None,
|
||||
systemPlatform=None,
|
||||
systemPlatformUser=None,
|
||||
platformUserId=None,
|
||||
platformId=None,
|
||||
withClassify=True,
|
||||
resetSessionMode=False
|
||||
)
|
||||
},
|
||||
updated_at="2024-01-01T01:00:00Z"
|
||||
)
|
||||
client.put = AsyncMock(return_value=mock_updated_settings)
|
||||
client.patch = AsyncMock(return_value=mock_updated_settings)
|
||||
|
||||
result = await client.update_user_settings("user-123", settings_update)
|
||||
|
||||
assert result == mock_updated_settings
|
||||
client.put.assert_called_once_with(
|
||||
client.patch.assert_called_once_with(
|
||||
"/users/user-123/settings",
|
||||
body=settings_update,
|
||||
response_model=UserSettings
|
||||
|
|
@ -211,6 +217,44 @@ class TestDBApiClient:
|
|||
response_model=SessionResponse
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_session(self):
|
||||
"""Test update_session calls patch correctly."""
|
||||
with patch('app.interfaces.base.httpx.AsyncClient'):
|
||||
client = DBApiClient(api_prefix="http://db-api:8080/api/v1")
|
||||
|
||||
update_data = SessionUpdate(
|
||||
annotations={
|
||||
"0": {
|
||||
"overall": {
|
||||
"rating": "incorrect",
|
||||
"comment": "Ответ неполный"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
mock_updated_session = SessionResponse(
|
||||
session_id="session-123",
|
||||
user_id="user-123",
|
||||
environment="ift",
|
||||
api_mode="bench",
|
||||
request=[{"question": "test"}],
|
||||
response={"answer": "test"},
|
||||
annotations=update_data.annotations,
|
||||
created_at="2024-01-01T00:00:00Z",
|
||||
updated_at="2024-01-01T01:00:00Z"
|
||||
)
|
||||
client.patch = AsyncMock(return_value=mock_updated_session)
|
||||
|
||||
result = await client.update_session("user-123", "session-123", update_data)
|
||||
|
||||
assert result == mock_updated_session
|
||||
client.patch.assert_called_once_with(
|
||||
"/users/user-123/sessions/session-123",
|
||||
body=update_data,
|
||||
response_model=SessionResponse
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_session(self):
|
||||
"""Test delete_session calls delete correctly."""
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import pytest
|
|||
from pydantic import ValidationError
|
||||
from app.models.auth import LoginRequest, UserResponse, LoginResponse
|
||||
from app.models.query import QuestionRequest, BenchQueryRequest, BackendQueryRequest, QueryResponse
|
||||
from app.models.settings import EnvironmentSettings, UserSettingsUpdate
|
||||
from app.models.settings import EnvironmentSettings, EnvironmentSettingsUpdate, UserSettingsUpdate
|
||||
from app.models.analysis import SessionCreate, SessionUpdate
|
||||
|
||||
|
||||
class TestAuthModels:
|
||||
|
|
@ -120,16 +121,34 @@ class TestQueryModels:
|
|||
|
||||
def test_query_response(self):
|
||||
"""Test QueryResponse model."""
|
||||
# Test with RagResponseBenchList (parsed from dict)
|
||||
response = QueryResponse(
|
||||
request_id="req-123",
|
||||
timestamp="2024-01-01T00:00:00Z",
|
||||
environment="ift",
|
||||
response={"answers": []}
|
||||
response={"answers": []} # Auto-parsed to RagResponseBenchList
|
||||
)
|
||||
|
||||
assert response.request_id == "req-123"
|
||||
assert response.environment == "ift"
|
||||
# When dict with "answers" is provided, it's parsed as RagResponseBenchList
|
||||
from app.models.query import RagResponseBenchList
|
||||
assert isinstance(response.response, RagResponseBenchList)
|
||||
|
||||
def test_query_response_with_dict(self):
|
||||
"""Test QueryResponse model with plain dict (backend mode)."""
|
||||
# Use dict that doesn't match RagResponseBenchList schema
|
||||
response = QueryResponse(
|
||||
request_id="req-456",
|
||||
timestamp="2024-01-01T00:00:00Z",
|
||||
environment="psi",
|
||||
response={"result": "some data", "status": "ok"}
|
||||
)
|
||||
|
||||
assert response.request_id == "req-456"
|
||||
assert response.environment == "psi"
|
||||
assert isinstance(response.response, dict)
|
||||
assert response.response["status"] == "ok"
|
||||
|
||||
|
||||
class TestSettingsModels:
|
||||
|
|
@ -154,30 +173,135 @@ class TestSettingsModels:
|
|||
assert settings.resetSessionMode is False
|
||||
|
||||
def test_environment_settings_defaults(self):
|
||||
"""Test EnvironmentSettings with default values."""
|
||||
settings = EnvironmentSettings(apiMode="backend")
|
||||
"""Test EnvironmentSettings with default values (nullable fields)."""
|
||||
settings = EnvironmentSettings()
|
||||
|
||||
assert settings.apiMode == "backend"
|
||||
assert settings.bearerToken == ""
|
||||
assert settings.apiMode == "bench"
|
||||
assert settings.bearerToken is None
|
||||
assert settings.systemPlatform is None
|
||||
assert settings.systemPlatformUser is None
|
||||
assert settings.platformUserId is None
|
||||
assert settings.platformId is None
|
||||
assert settings.withClassify is False
|
||||
assert settings.resetSessionMode is True
|
||||
|
||||
def test_environment_settings_nullable_fields(self):
|
||||
"""Test EnvironmentSettings with explicit None values."""
|
||||
settings = EnvironmentSettings(
|
||||
apiMode="backend",
|
||||
bearerToken=None,
|
||||
systemPlatform=None,
|
||||
withClassify=True
|
||||
)
|
||||
|
||||
assert settings.apiMode == "backend"
|
||||
assert settings.bearerToken is None
|
||||
assert settings.systemPlatform is None
|
||||
assert settings.withClassify is True
|
||||
|
||||
def test_environment_settings_update_partial(self):
|
||||
"""Test EnvironmentSettingsUpdate for partial updates."""
|
||||
update = EnvironmentSettingsUpdate(
|
||||
bearerToken="new-token",
|
||||
withClassify=True
|
||||
)
|
||||
|
||||
assert update.bearerToken == "new-token"
|
||||
assert update.withClassify is True
|
||||
assert update.apiMode is None # Not provided, should be None
|
||||
assert update.systemPlatform is None
|
||||
|
||||
def test_environment_settings_update_all_none(self):
|
||||
"""Test EnvironmentSettingsUpdate with all fields as None."""
|
||||
update = EnvironmentSettingsUpdate()
|
||||
|
||||
assert update.apiMode is None
|
||||
assert update.bearerToken is None
|
||||
assert update.systemPlatform is None
|
||||
assert update.withClassify is None
|
||||
|
||||
def test_user_settings_update(self):
|
||||
"""Test UserSettingsUpdate model."""
|
||||
"""Test UserSettingsUpdate model with partial updates."""
|
||||
update = UserSettingsUpdate(
|
||||
settings={
|
||||
"ift": EnvironmentSettings(apiMode="bench"),
|
||||
"psi": EnvironmentSettings(apiMode="backend")
|
||||
"ift": EnvironmentSettingsUpdate(apiMode="bench", withClassify=True),
|
||||
"psi": EnvironmentSettingsUpdate(bearerToken="token123")
|
||||
}
|
||||
)
|
||||
|
||||
assert "ift" in update.settings
|
||||
assert "psi" in update.settings
|
||||
assert update.settings["ift"].apiMode == "bench"
|
||||
assert update.settings["psi"].apiMode == "backend"
|
||||
assert update.settings["ift"].withClassify is True
|
||||
assert update.settings["psi"].bearerToken == "token123"
|
||||
assert update.settings["psi"].apiMode is None # Not provided
|
||||
|
||||
def test_user_settings_update_empty(self):
|
||||
"""Test UserSettingsUpdate with empty settings."""
|
||||
update = UserSettingsUpdate(settings={})
|
||||
|
||||
assert update.settings == {}
|
||||
|
||||
|
||||
class TestAnalysisModels:
|
||||
"""Tests for analysis session models."""
|
||||
|
||||
def test_session_create_valid(self):
|
||||
"""Test valid SessionCreate."""
|
||||
session = SessionCreate(
|
||||
environment="ift",
|
||||
api_mode="bench",
|
||||
request=[{"body": "question 1", "with_docs": True}],
|
||||
response={"answers": ["answer 1"]},
|
||||
annotations={"0": {"rating": "correct"}}
|
||||
)
|
||||
|
||||
assert session.environment == "ift"
|
||||
assert session.api_mode == "bench"
|
||||
assert len(session.request) == 1
|
||||
assert session.annotations == {"0": {"rating": "correct"}}
|
||||
|
||||
def test_session_create_default_annotations(self):
|
||||
"""Test SessionCreate with default empty annotations."""
|
||||
session = SessionCreate(
|
||||
environment="psi",
|
||||
api_mode="backend",
|
||||
request=[],
|
||||
response={}
|
||||
)
|
||||
|
||||
assert session.annotations == {}
|
||||
|
||||
def test_session_update_valid(self):
|
||||
"""Test valid SessionUpdate."""
|
||||
update = SessionUpdate(
|
||||
annotations={
|
||||
"0": {
|
||||
"overall": {
|
||||
"rating": "incorrect",
|
||||
"comment": "Ответ неполный"
|
||||
},
|
||||
"body_research": {
|
||||
"issues": ["missing_info"],
|
||||
"comment": "Не указаны процентные ставки"
|
||||
}
|
||||
},
|
||||
"1": {
|
||||
"overall": {
|
||||
"rating": "correct",
|
||||
"comment": "Ответ корректный"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
assert "0" in update.annotations
|
||||
assert "1" in update.annotations
|
||||
assert update.annotations["0"]["overall"]["rating"] == "incorrect"
|
||||
assert update.annotations["1"]["overall"]["rating"] == "correct"
|
||||
|
||||
def test_session_update_empty_annotations(self):
|
||||
"""Test SessionUpdate with empty annotations."""
|
||||
update = SessionUpdate(annotations={})
|
||||
|
||||
assert update.annotations == {}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import pytest
|
|||
from unittest.mock import AsyncMock, patch, MagicMock
|
||||
import httpx
|
||||
from app.services.rag_service import RagService
|
||||
from app.models.query import QuestionRequest
|
||||
from app.models.query import QuestionRequest, RagResponseBenchList
|
||||
|
||||
|
||||
class TestBenchQueryEndpoint:
|
||||
|
|
@ -16,7 +16,9 @@ class TestBenchQueryEndpoint:
|
|||
|
||||
with patch('app.api.v1.query.RagService') as MockRagService:
|
||||
mock_rag = AsyncMock()
|
||||
mock_rag.send_bench_query = AsyncMock(return_value=mock_bench_response)
|
||||
# Mock возвращает RagResponseBenchList
|
||||
from app.models.query import RagResponseBenchList
|
||||
mock_rag.send_bench_query = AsyncMock(return_value=RagResponseBenchList(**mock_bench_response))
|
||||
mock_rag.close = AsyncMock()
|
||||
MockRagService.return_value = mock_rag
|
||||
|
||||
|
|
@ -37,7 +39,9 @@ class TestBenchQueryEndpoint:
|
|||
assert "timestamp" in data
|
||||
assert data["environment"] == "ift"
|
||||
assert "response" in data
|
||||
assert data["response"] == mock_bench_response
|
||||
# FastAPI автоматически сериализует RagResponseBenchList в dict
|
||||
assert data["response"]["answers"][0]["question"] == "Test question 1"
|
||||
assert data["response"]["answers"][0]["body_research"] == "Test research answer 1"
|
||||
|
||||
mock_rag.send_bench_query.assert_called_once()
|
||||
mock_rag.close.assert_called_once()
|
||||
|
|
@ -245,7 +249,7 @@ class TestRagService:
|
|||
@pytest.mark.asyncio
|
||||
async def test_send_bench_query_success(self, mock_httpx_client, mock_bench_response):
|
||||
"""Test successful bench query via RagService."""
|
||||
|
||||
|
||||
mock_httpx_client.post.return_value.json.return_value = mock_bench_response
|
||||
|
||||
with patch('app.services.rag_service.httpx.AsyncClient', return_value=mock_httpx_client):
|
||||
|
|
@ -268,10 +272,15 @@ class TestRagService:
|
|||
request_id="test-request-123"
|
||||
)
|
||||
|
||||
assert result == mock_bench_response
|
||||
# Проверяем, что результат - это RagResponseBenchList
|
||||
assert isinstance(result, RagResponseBenchList)
|
||||
assert len(result.answers) == 2
|
||||
assert result.answers[0].question == "Test question 1"
|
||||
assert result.answers[0].body_research == "Test research answer 1"
|
||||
|
||||
mock_httpx_client.post.assert_called_once()
|
||||
|
||||
|
||||
|
||||
call_kwargs = mock_httpx_client.post.call_args[1]
|
||||
headers = call_kwargs["headers"]
|
||||
assert headers["Request-Id"] == "test-request-123"
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class TestSettingsEndpoints:
|
|||
assert response.status_code == 401
|
||||
|
||||
def test_update_settings_success(self, client, mock_db_client, test_settings):
|
||||
"""Test updating user settings successfully."""
|
||||
"""Test updating user settings successfully with PATCH (partial update)."""
|
||||
mock_db_client.update_user_settings = AsyncMock(return_value=test_settings)
|
||||
|
||||
update_data = {
|
||||
|
|
@ -59,7 +59,7 @@ class TestSettingsEndpoints:
|
|||
}
|
||||
}
|
||||
|
||||
response = client.put("/api/v1/settings", json=update_data)
|
||||
response = client.patch("/api/v1/settings", json=update_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
|
@ -80,7 +80,7 @@ class TestSettingsEndpoints:
|
|||
}
|
||||
}
|
||||
|
||||
response = client.put("/api/v1/settings", json=update_data)
|
||||
response = client.patch("/api/v1/settings", json=update_data)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
|
|
@ -96,7 +96,7 @@ class TestSettingsEndpoints:
|
|||
}
|
||||
}
|
||||
|
||||
response = client.put("/api/v1/settings", json=update_data)
|
||||
response = client.patch("/api/v1/settings", json=update_data)
|
||||
|
||||
assert response.status_code == 500
|
||||
|
||||
|
|
@ -136,7 +136,7 @@ class TestSettingsEndpoints:
|
|||
}
|
||||
}
|
||||
|
||||
response = client.put("/api/v1/settings", json=update_data)
|
||||
response = client.patch("/api/v1/settings", json=update_data)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert "user not found" in response.json()["detail"].lower()
|
||||
|
|
@ -154,7 +154,26 @@ class TestSettingsEndpoints:
|
|||
}
|
||||
}
|
||||
|
||||
response = client.put("/api/v1/settings", json=update_data)
|
||||
response = client.patch("/api/v1/settings", json=update_data)
|
||||
|
||||
assert response.status_code == 502
|
||||
assert "failed to update settings" in response.json()["detail"].lower()
|
||||
|
||||
def test_update_settings_partial_update(self, client, mock_db_client, test_settings):
|
||||
"""Test partial update - only updating specific fields."""
|
||||
mock_db_client.update_user_settings = AsyncMock(return_value=test_settings)
|
||||
|
||||
# Partial update - only bearerToken and withClassify for IFT
|
||||
update_data = {
|
||||
"settings": {
|
||||
"ift": {
|
||||
"bearerToken": "updated-token-123",
|
||||
"withClassify": True
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response = client.patch("/api/v1/settings", json=update_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
mock_db_client.update_user_settings.assert_called_once()
|
||||
|
|
|
|||
Loading…
Reference in New Issue