Compare commits

..

No commits in common. "74cdc196c64f34b54d7bf72429c975b53d3650f5" and "dbbd366953f8ef250a8db478d5e9046b283de4ac" have entirely different histories.

27 changed files with 382 additions and 1555 deletions

View File

@ -15,14 +15,7 @@
"Bash(.venv/Scripts/python.exe -m pytest tests/ -v --tb=line)", "Bash(.venv/Scripts/python.exe -m pytest tests/ -v --tb=line)",
"Bash(.venv/Scripts/python.exe -m pytest:*)", "Bash(.venv/Scripts/python.exe -m pytest:*)",
"Bash(.venvScriptspython.exe -m pytest tests/unit/ -v --cov=app --cov-report=term-missing)", "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": [], "deny": [],
"ask": [] "ask": []

View File

@ -1,6 +1,6 @@
# Developer Guide # CLAUDE.md
This file provides detailed technical documentation for developers working with this codebase. This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview ## Project Overview
@ -293,6 +293,6 @@ See [TESTING.md](TESTING.md) for comprehensive testing guide including:
See [PROJECT_STATUS.md](PROJECT_STATUS.md) for detailed implementation status and TODOs. Key points: See [PROJECT_STATUS.md](PROJECT_STATUS.md) for detailed implementation status and TODOs. Key points:
- Core infrastructure is complete (auth, DB API client, RAG service) - Core infrastructure is complete (auth, DB API client, RAG service)
- All main API endpoints are implemented - All main API endpoints are implemented
- TgBackendInterface is fully implemented - TgBackendInterface is fully implemented (not a stub)
- 99% test coverage (unit + integration + E2E tests) - Frontend integration pending (static/ directory is empty)
- Frontend integration complete - No tests yet (tests/ directory is empty)

View File

@ -1,710 +0,0 @@
```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-инъекций
---

View File

@ -7,7 +7,7 @@
## Текущее состояние ## Текущее состояние
### Готово (Backend) ### Готово (Backend)
- Структура FastAPI приложения - Структура FastAPI приложения
- JWT авторизация (8-значный логин) - JWT авторизация (8-значный логин)
- TgBackendInterface (полная реализация с httpx) - TgBackendInterface (полная реализация с httpx)
@ -20,9 +20,9 @@
- `POST /api/v1/query/backend` - последовательные запросы - `POST /api/v1/query/backend` - последовательные запросы
- `POST/GET/DELETE /api/v1/analysis/sessions` - сессии анализа - `POST/GET/DELETE /api/v1/analysis/sessions` - сессии анализа
- Docker setup (Dockerfile, docker-compose.yml) - Docker setup (Dockerfile, docker-compose.yml)
- Документация (README.md, DB_API_CONTRACT.md) - Документация (README.md, DB_API_CONTRACT.md, CLAUDE.md)
### Требуется доделать ### Требуется доделать
- Frontend файлы (перенос из rag-bench-old-version) - Frontend файлы (перенос из rag-bench-old-version)
- API client для frontend - API client для frontend
- Интеграция frontend с новым API - Интеграция frontend с новым API
@ -645,20 +645,20 @@ app.middleware("http")(log_requests)
## Приоритезация задач ## Приоритезация задач
### Критично (сделать в первую очередь) ### 🔴 Критично (сделать в первую очередь)
1. Перенос статических файлов из rag-bench-old-version → `static/` 1. Перенос статических файлов из rag-bench-old-version → `static/`
2. Создание `api-client.js` 2. Создание `api-client.js`
3. Добавление login screen в `index.html` 3. Добавление login screen в `index.html`
4. Переписывание вызовов API в `app.js` 4. Переписывание вызовов API в `app.js`
5. Тестирование auth flow 5. Тестирование auth flow
### Важно (сделать после критичного) ### 🟡 Важно (сделать после критичного)
6. Интеграция Settings UI 6. Интеграция Settings UI
7. Environment selector 7. Environment selector
8. Сохранение и загрузка сессий анализа 8. Сохранение и загрузка сессий анализа
9. Ручное тестирование всех сценариев 9. Ручное тестирование всех сценариев
### Желательно (если есть время) ### 🟢 Желательно (если есть время)
10. Автоматические тесты (pytest) 10. Автоматические тесты (pytest)
11. Production deployment настройка 11. Production deployment настройка
12. Logging middleware 12. Logging middleware

View File

@ -44,7 +44,7 @@ cp rag-bench-old-version/settings.js static/
## Шаг 2: Создание api-client.js ## Шаг 2: Создание api-client.js
См. полную реализацию в DEVELOPMENT_PLAN.md (раздел "Создать API client для frontend"). См. полную реализацию в [DEVELOPMENT_PLAN.md](DEVELOPMENT_PLAN.md#12-создать-api-client-для-frontend).
Создать файл `static/api-client.js` с классом `BriefBenchAPI`. Создать файл `static/api-client.js` с классом `BriefBenchAPI`.

View File

@ -1,60 +1,60 @@
# Production Readiness Checklist # 🚀 Production Readiness Checklist
Полная проверка готовности Brief Bench FastAPI к развертыванию в продакшн. Полная проверка готовности Brief Bench FastAPI к развертыванию в продакшн.
## Backend (FastAPI) ## Backend (FastAPI)
### Код и архитектура ### Код и архитектура
- [x] **Все API endpoints реализованы** - [x] **Все API endpoints реализованы**
- Auth: `/api/v1/auth/login` - Auth: `/api/v1/auth/login`
- Settings: GET/PUT `/api/v1/settings` - Settings: GET/PUT `/api/v1/settings`
- Query: POST `/api/v1/query/bench`, `/api/v1/query/backend` - Query: POST `/api/v1/query/bench`, `/api/v1/query/backend`
- Analysis: CRUD `/api/v1/analysis/sessions` - Analysis: CRUD `/api/v1/analysis/sessions`
- Health: `/health` - Health: `/health`
- [x] **Бизнес-логика покрыта тестами: 99%** - [x] **Бизнес-логика покрыта тестами: 99%**
- 119 unit tests (99% coverage) - 119 unit tests (99% coverage)
- Integration tests (DB API) - Integration tests (DB API)
- E2E tests (полный стек) - E2E tests (полный стек)
- [x] **Services реализованы** - [x] **Services реализованы**
- AuthService (JWT токены) - AuthService (JWT токены)
- RagService (RAG backends: IFT, PSI, PROD) - RagService (RAG backends: IFT, PSI, PROD)
- [x] **Interfaces реализованы** - [x] **Interfaces реализованы**
- TgBackendInterface (базовый HTTP клиент) - TgBackendInterface (базовый HTTP клиент)
- DBApiClient (DB API integration) - DBApiClient (DB API integration)
- [x] **Models валидация** - [x] **Models валидация**
- Все Pydantic models для request/response - Все Pydantic models для request/response
- Валидация входных данных - Валидация входных данных
## Frontend (Static Files) ## Frontend (Static Files)
- [x] **HTML/CSS/JS файлы** - [x] **HTML/CSS/JS файлы**
- index.html - index.html
- styles.css (Material Design) - styles.css (Material Design)
- app.js (основная логика) - app.js (основная логика)
- api-client.js (API клиент) - api-client.js (API клиент)
- settings.js (настройки) - settings.js (настройки)
- [x] **Интеграция с backend** - [x] **Интеграция с backend**
- API client использует `/api/v1` endpoints - API client использует `/api/v1` endpoints
- JWT токены в localStorage - JWT токены в localStorage
- Правильная обработка ошибок (401, 502, etc.) - Правильная обработка ошибок (401, 502, etc.)
- StaticFiles монтированы в main.py - StaticFiles монтированы в main.py
- [x] **UI функциональность** - [x] **UI функциональность**
- Login screen - Login screen
- Multi-environment tabs (IFT, PSI, PROD) - Multi-environment tabs (IFT, PSI, PROD)
- Settings panel - Settings panel
- Query interface - Query interface
- Results display - Results display
- Session management - Session management
## Конфигурация (ТРЕБУЕТ ВНИМАНИЯ!) ## ⚠️ Конфигурация (ТРЕБУЕТ ВНИМАНИЯ!)
### КРИТИЧНО - Сделать перед деплоем: ### 🔴 КРИТИЧНО - Сделать перед деплоем:
- [ ] **1. Создать `.env` файл** - [ ] **1. Создать `.env` файл**
```bash ```bash
@ -121,54 +121,55 @@
DEBUG=false DEBUG=false
``` ```
## Docker & Deployment ## Docker & Deployment
- [x] **Dockerfile готов** - [x] **Dockerfile готов**
- Multi-stage build - Multi-stage build
- Копирует static/ файлы - Копирует static/ файлы
- Expose 8000 - Expose 8000
- Uvicorn с правильными параметрами - Uvicorn с правильными параметрами
- [x] **docker-compose.yml готов** - [x] **docker-compose.yml готов**
- Порты пробрасываются (8000:8000) - Порты пробрасываются (8000:8000)
- Volume для certs (read-only) - Volume для certs (read-only)
- Volume для static файлов - Volume для static файлов
- .env подключается - .env подключается
- restart: unless-stopped - restart: unless-stopped
## Безопасность ## Безопасность
- [x] **Authentication** - [x] **Authentication**
- JWT токены (30 дней expiration) - JWT токены (30 дней expiration)
- Bearer token authentication - Bearer token authentication
- Middleware для проверки токенов - Middleware для проверки токенов
- [x] **Secrets management** - [x] **Secrets management**
- .env не в git (.gitignore) - .env не в git (.gitignore)
- .env.integration не в git - .env.integration не в git
- .env.e2e не в git - .env.e2e не в git
- ВАЖНО: Сменить JWT_SECRET_KEY в продакшн! - ⚠️ ВАЖНО: Сменить JWT_SECRET_KEY в продакшн!
- [x] **mTLS сертификаты** - [x] **mTLS сертификаты**
- Хранятся только на сервере - Хранятся только на сервере
- Read-only volume в Docker - Read-only volume в Docker
- Не коммитятся в git - Не коммитятся в git
- [ ] **HTTPS (рекомендуется)** - [ ] **HTTPS (рекомендуется)**
- Настроить reverse proxy (nginx/traefik) - Настроить reverse proxy (nginx/traefik)
- Let's Encrypt сертификаты - Let's Encrypt сертификаты
- Редирект HTTP → HTTPS - Редирект HTTP → HTTPS
## Документация ## Документация
- [x] **README.md** - основная документация - [x] **README.md** - основная документация
- [x] **CLAUDE.md** - архитектура и гайд для Claude
- [x] **DB_API_CONTRACT.md** - контракт с DB API - [x] **DB_API_CONTRACT.md** - контракт с DB API
- [x] **TESTING.md** - полное руководство по тестированию - [x] **TESTING.md** - полное руководство по тестированию
- [x] **PROJECT_STATUS.md** - статус реализации - [x] **PROJECT_STATUS.md** - статус реализации
- [x] **tests/integration/README.md** - интеграционные тесты - [x] **tests/integration/README.md** - интеграционные тесты
- [x] **tests/e2e/README.md** - E2E тесты - [x] **tests/e2e/README.md** - E2E тесты
## Pre-Deployment Testing ## 🔍 Pre-Deployment Testing
### Локальное тестирование ### Локальное тестирование
@ -237,7 +238,7 @@
docker-compose down docker-compose down
``` ```
## Deployment Steps ## 🚀 Deployment Steps
### 1. Подготовка сервера ### 1. Подготовка сервера
@ -307,7 +308,7 @@ server {
} }
``` ```
## Post-Deployment Verification ## 📊 Post-Deployment Verification
После деплоя проверить: После деплоя проверить:
@ -319,7 +320,7 @@ server {
- [ ] Session save/load работает - [ ] Session save/load работает
- [ ] Логи не содержат ошибок: `docker-compose logs -f` - [ ] Логи не содержат ошибок: `docker-compose logs -f`
## Мониторинг и обслуживание ## 🔧 Мониторинг и обслуживание
### Логи ### Логи
@ -356,19 +357,19 @@ docker-compose up -d --build
### Backup ### Backup
Критичные данные: Критичные данные:
- `.env` - секреты и конфигурация - `.env` - секреты и конфигурация
- `certs/` - mTLS сертификаты - `certs/` - mTLS сертификаты
- Пользовательские данные хранятся в DB API (не в FastAPI) - Пользовательские данные хранятся в DB API (не в FastAPI)
## Performance Considerations ## Performance Considerations
- RAG запросы могут занимать до 30 минут (настроено) - RAG запросы могут занимать до 30 минут (настроено)
- Async/await для всех I/O операций - Async/await для всех I/O операций
- Connection pooling в httpx clients - Connection pooling в httpx clients
- Рассмотреть rate limiting для production - Рассмотреть rate limiting для production
- Рассмотреть caching для settings (опционально) - Рассмотреть caching для settings (опционально)
## Troubleshooting ## 🐛 Troubleshooting
### Проблема: Контейнер не запускается ### Проблема: Контейнер не запускается
@ -399,30 +400,30 @@ docker-compose up -d --build
2. JWT_SECRET_KEY одинаковый между запусками 2. JWT_SECRET_KEY одинаковый между запусками
3. Токен не истек (30 дней по умолчанию) 3. Токен не истек (30 дней по умолчанию)
## Final Checklist Summary ## Final Checklist Summary
Перед деплоем в продакшн: Перед деплоем в продакшн:
1. Backend код готов (99% coverage) 1. Backend код готов (99% coverage)
2. Frontend интегрирован 2. Frontend интегрирован
3. Docker конфигурация готова 3. Docker конфигурация готова
4. **`.env` создан и заполнен** 4. ⚠️ **`.env` создан и заполнен**
5. **`JWT_SECRET_KEY` сгенерирован новый** 5. ⚠️ **`JWT_SECRET_KEY` сгенерирован новый**
6. **RAG hosts настроены** 6. ⚠️ **RAG hosts настроены**
7. **DB_API_URL настроен** 7. ⚠️ **DB_API_URL настроен**
8. **mTLS сертификаты размещены** (если используются) 8. ⚠️ **mTLS сертификаты размещены** (если используются)
9. **CORS настроен** (при необходимости) 9. ⚠️ **CORS настроен** (при необходимости)
10. **DEBUG=false** 10. ⚠️ **DEBUG=false**
11. Unit тесты passed 11. Unit тесты passed
12. Integration тесты passed (опционально) 12. Integration тесты passed (опционально)
13. Локальное тестирование пройдено 13. Локальное тестирование пройдено
14. Docker build успешен 14. Docker build успешен
--- ---
**Статус готовности: ПОЧТИ ГОТОВ** **Статус готовности: 🟡 ПОЧТИ ГОТОВ**
**Готово:** Код, тесты, Docker, документация **Готово:** Код, тесты, Docker, документация
**Требуется:** Конфигурация окружения (.env, сертификаты, финальная настройка) ⚠️ **Требуется:** Конфигурация окружения (.env, сертификаты, финальная настройка)
После выполнения пунктов из раздела "КРИТИЧНО" → **ГОТОВ К ПРОДАКШН** После выполнения пунктов из раздела "КРИТИЧНО" → **🟢 ГОТОВ К ПРОДАКШН**

View File

@ -5,7 +5,7 @@
--- ---
## Что реализовано ## 📋 Что реализовано
### 1. Структура проекта ### 1. Структура проекта
@ -15,45 +15,45 @@ brief-bench-fastapi/
│ ├── api/ │ ├── api/
│ │ └── v1/ │ │ └── v1/
│ │ ├── __init__.py │ │ ├── __init__.py
│ │ └── auth.py POST /api/v1/auth/login │ │ └── auth.py POST /api/v1/auth/login
│ ├── models/ │ ├── models/
│ │ ├── __init__.py │ │ ├── __init__.py
│ │ ├── auth.py LoginRequest, LoginResponse, UserResponse │ │ ├── auth.py LoginRequest, LoginResponse, UserResponse
│ │ ├── settings.py EnvironmentSettings, UserSettings │ │ ├── settings.py EnvironmentSettings, UserSettings
│ │ ├── analysis.py SessionCreate, SessionResponse, SessionList │ │ ├── analysis.py SessionCreate, SessionResponse, SessionList
│ │ └── query.py BenchQueryRequest, BackendQueryRequest │ │ └── query.py BenchQueryRequest, BackendQueryRequest
│ ├── services/ │ ├── services/
│ │ ├── __init__.py │ │ ├── __init__.py
│ │ └── auth_service.py AuthService (login logic) │ │ └── auth_service.py AuthService (login logic)
│ ├── interfaces/ │ ├── interfaces/
│ │ ├── __init__.py │ │ ├── __init__.py
│ │ ├── base.py TgBackendInterface (ЗАГЛУШКА - нужна реализация) │ │ ├── base.py ⚠️ TgBackendInterface (ЗАГЛУШКА - нужна реализация)
│ │ └── db_api_client.py DBApiClient (методы для DB API) │ │ └── db_api_client.py DBApiClient (методы для DB API)
│ ├── middleware/ │ ├── middleware/
│ │ └── __init__.py │ │ └── __init__.py
│ ├── utils/ │ ├── utils/
│ │ ├── __init__.py │ │ ├── __init__.py
│ │ └── security.py JWT encode/decode │ │ └── security.py JWT encode/decode
│ ├── __init__.py │ ├── __init__.py
│ ├── config.py Settings из .env │ ├── config.py Settings из .env
│ ├── dependencies.py DI: get_db_client, get_current_user │ ├── dependencies.py DI: get_db_client, get_current_user
│ └── main.py FastAPI app с CORS │ └── main.py FastAPI app с CORS
├── static/ Пусто (нужно скопировать из rag-bench) ├── static/ Пусто (нужно скопировать из rag-bench)
├── tests/ Полный набор тестов (unit/integration/e2e) ├── tests/ Полный набор тестов (unit/integration/e2e)
├── certs/ Не создана (для mTLS) ├── certs/ Не создана (для mTLS)
├── .env.example ├── .env.example
├── .gitignore ├── .gitignore
├── requirements.txt ├── requirements.txt
├── Dockerfile ├── Dockerfile
├── docker-compose.yml ├── docker-compose.yml
├── DB_API_CONTRACT.md Полный контракт для DB API ├── DB_API_CONTRACT.md Полный контракт для DB API
├── README.md ├── README.md
└── PROJECT_STATUS.md Этот файл └── PROJECT_STATUS.md Этот файл
``` ```
--- ---
## Реализованные компоненты ## Реализованные компоненты
### 1. Configuration (app/config.py) ### 1. Configuration (app/config.py)
- Загрузка из .env через pydantic-settings - Загрузка из .env через pydantic-settings
@ -88,7 +88,7 @@ brief-bench-fastapi/
### 3. Interfaces (app/interfaces/) ### 3. Interfaces (app/interfaces/)
**base.py (ЗАГЛУШКА!):** **base.py (⚠️ ЗАГЛУШКА!):**
```python ```python
class TgBackendInterface: class TgBackendInterface:
def __init__(self, api_prefix: str, **kwargs) def __init__(self, api_prefix: str, **kwargs)
@ -152,7 +152,7 @@ class TgBackendInterface:
--- ---
## Что НЕ реализовано (TODO) ## Что НЕ реализовано (TODO)
### 1. TgBackendInterface реализация (КРИТИЧНО!) ### 1. TgBackendInterface реализация (КРИТИЧНО!)
Файл: `app/interfaces/base.py` Файл: `app/interfaces/base.py`
@ -259,28 +259,28 @@ class RagService:
- `app/middleware/logging.py` - логирование запросов - `app/middleware/logging.py` - логирование запросов
- `app/middleware/error_handler.py` - глобальная обработка ошибок - `app/middleware/error_handler.py` - глобальная обработка ошибок
### 9. Tests COMPLETED ### 9. Tests COMPLETED
- **Unit Tests** (119 tests, 99% coverage) - `tests/unit/` - **Unit Tests** (119 tests, 99% coverage) - `tests/unit/`
- All services, models, utilities tested in isolation - All services, models, utilities tested in isolation
- All external dependencies mocked - All external dependencies mocked
- Run: `.\run_unit_tests.bat` - Run: `.\run_unit_tests.bat`
- **Integration Tests** (DB API integration) - `tests/integration/` - **Integration Tests** (DB API integration) - `tests/integration/`
- FastAPI endpoints with real DB API - FastAPI endpoints with real DB API
- Requires DB API service running - Requires DB API service running
- Run: `.\run_integration_tests.bat` - Run: `.\run_integration_tests.bat`
- **End-to-End Tests** (Full stack) - `tests/e2e/` - **End-to-End Tests** (Full stack) - `tests/e2e/`
- Complete workflows: auth → query → save → retrieve - Complete workflows: auth → query → save → retrieve
- Requires all services (FastAPI + DB API + RAG backends) - Requires all services (FastAPI + DB API + RAG backends)
- Real network calls to RAG backends - Real network calls to RAG backends
- Run: `.\run_e2e_tests.bat` - Run: `.\run_e2e_tests.bat`
- **Test Documentation** - `TESTING.md` - **Test Documentation** - `TESTING.md`
- Comprehensive testing guide - Comprehensive testing guide
- Setup instructions for each test level - Setup instructions for each test level
- Troubleshooting and best practices - Troubleshooting and best practices
--- ---
## Важные детали для продолжения ## 🔑 Важные детали для продолжения
### Архитектура авторизации ### Архитектура авторизации
1. Пользователь отправляет POST /api/v1/auth/login?login=12345678 1. Пользователь отправляет POST /api/v1/auth/login?login=12345678
@ -346,7 +346,7 @@ Body: {
--- ---
## План дальнейшей работы ## 🚀 План дальнейшей работы
### Этап 1: Реализовать TgBackendInterface ### Этап 1: Реализовать TgBackendInterface
**Приоритет:** ВЫСОКИЙ **Приоритет:** ВЫСОКИЙ
@ -398,7 +398,7 @@ app.include_router(analysis.router, prefix="/api/v1")
--- ---
## Dependencies (requirements.txt) ## 📦 Dependencies (requirements.txt)
``` ```
fastapi==0.104.1 fastapi==0.104.1
@ -416,7 +416,7 @@ fastapi-cors==0.0.6
--- ---
## Команды для разработки ## 🔧 Команды для разработки
```bash ```bash
# Установить зависимости # Установить зависимости
@ -436,7 +436,7 @@ curl http://localhost:8000/health
--- ---
## Примечания ## 📝 Примечания
1. **TgBackendInterface** - это ваша реализация, которая будет использоваться во всех клиентах (DBApiClient, возможно RagClient в будущем) 1. **TgBackendInterface** - это ваша реализация, которая будет использоваться во всех клиентах (DBApiClient, возможно RagClient в будущем)
@ -454,7 +454,7 @@ curl http://localhost:8000/health
--- ---
## Готово к продолжению! ## 🎯 Готово к продолжению!
Вся базовая структура создана. Следующий шаг - реализация TgBackendInterface и остальных endpoints. Вся базовая структура создана. Следующий шаг - реализация TgBackendInterface и остальных endpoints.

View File

@ -4,13 +4,13 @@ FastAPI backend для системы тестирования RAG с multi-user
## Возможности ## Возможности
- JWT авторизация (8-значный логин) - 🔐 JWT авторизация (8-значный логин)
- Multi-environment: ИФТ, ПСИ, ПРОМ - 🌐 Multi-environment: ИФТ, ПСИ, ПРОМ
- Bench mode: batch тестирование - 📊 Bench mode: batch тестирование
- Backend mode: имитация бота (вопросы по одному) - 🤖 Backend mode: имитация бота (вопросы по одному)
- Сохранение сессий анализа - 💾 Сохранение сессий анализа
- mTLS для RAG backend - 🔒 mTLS для RAG backend
- Аннотации и экспорт - 📝 Аннотации и экспорт
## Требования ## Требования
@ -186,6 +186,7 @@ docker-compose down
## Документация ## Документация
- [CLAUDE.md](CLAUDE.md) - архитектура и гайд для разработки
- [TESTING.md](TESTING.md) - руководство по тестированию - [TESTING.md](TESTING.md) - руководство по тестированию
- [PRODUCTION_CHECKLIST.md](PRODUCTION_CHECKLIST.md) - чек-лист для продакшн - [PRODUCTION_CHECKLIST.md](PRODUCTION_CHECKLIST.md) - чек-лист для продакшн
- [DB_API_CONTRACT.md](DB_API_CONTRACT.md) - контракт с DB API - [DB_API_CONTRACT.md](DB_API_CONTRACT.md) - контракт с DB API
@ -193,13 +194,13 @@ docker-compose down
## Status ## Status
**Проект готов к продакшн** **Проект готов к продакшн**
- Backend полностью реализован (все endpoints, services, interfaces) - Backend полностью реализован (все endpoints, services, interfaces)
- Frontend интегрирован (HTML/CSS/JS) - Frontend интегрирован (HTML/CSS/JS)
- 99% test coverage (unit + integration + E2E) - 99% test coverage (unit + integration + E2E)
- Docker ready - Docker ready
- Требуется: настройка `.env` и сертификатов - ⚠️ Требуется: настройка `.env` и сертификатов
**Перед деплоем:** см. [PRODUCTION_CHECKLIST.md](PRODUCTION_CHECKLIST.md) **Перед деплоем:** см. [PRODUCTION_CHECKLIST.md](PRODUCTION_CHECKLIST.md)

View File

@ -543,6 +543,7 @@ Current coverage:
- [Integration Tests](tests/integration/README.md) - DB API integration - [Integration Tests](tests/integration/README.md) - DB API integration
- [E2E Tests](tests/e2e/README.md) - Full stack testing - [E2E Tests](tests/e2e/README.md) - Full stack testing
- [DB API Contract](DB_API_CONTRACT.md) - External API spec - [DB API Contract](DB_API_CONTRACT.md) - External API spec
- [CLAUDE.md](CLAUDE.md) - Architecture overview
- [PROJECT_STATUS.md](PROJECT_STATUS.md) - Implementation status - [PROJECT_STATUS.md](PROJECT_STATUS.md) - Implementation status
## Summary ## Summary

View File

@ -6,7 +6,7 @@ Analysis Sessions API endpoints.
from fastapi import APIRouter, Depends, HTTPException, status, Query from fastapi import APIRouter, Depends, HTTPException, status, Query
from typing import Optional from typing import Optional
from app.models.analysis import SessionCreate, SessionResponse, SessionList, SessionUpdate from app.models.analysis import SessionCreate, SessionResponse, SessionList
from app.interfaces.db_api_client import DBApiClient from app.interfaces.db_api_client import DBApiClient
from app.dependencies import get_db_client, get_current_user from app.dependencies import get_db_client, get_current_user
import httpx import httpx
@ -143,54 +143,6 @@ 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) @router.delete("/sessions/{session_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_session( async def delete_session(
session_id: str, session_id: str,

View File

@ -51,22 +51,20 @@ async def get_settings(
) )
@router.patch("", response_model=UserSettings) @router.put("", response_model=UserSettings)
async def update_settings( async def update_settings(
settings_update: UserSettingsUpdate, settings_update: UserSettingsUpdate,
current_user: dict = Depends(get_current_user), current_user: dict = Depends(get_current_user),
db_client: DBApiClient = Depends(get_db_client) db_client: DBApiClient = Depends(get_db_client)
): ):
""" """
Частично обновить настройки пользователя. Обновить настройки пользователя.
Обновляются только переданные поля. Непереданные поля остаются без изменений.
Args: Args:
settings_update: Частичные настройки для одного или нескольких окружений settings_update: Новые настройки для одного или нескольких окружений
Returns: Returns:
UserSettings: Обновленные настройки со всеми полями UserSettings: Обновленные настройки
""" """
user_id = current_user["user_id"] user_id = current_user["user_id"]

View File

@ -252,36 +252,6 @@ class TgBackendInterface:
response = await self.client.put(url, json=json_body, **kwargs) response = await self.client.put(url, json=json_body, **kwargs)
return await self._handle_response(response, response_model) 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( async def delete(
self, self,
path: str, path: str,

View File

@ -3,7 +3,7 @@
from app.interfaces.base import TgBackendInterface from app.interfaces.base import TgBackendInterface
from app.models.auth import LoginRequest, UserResponse from app.models.auth import LoginRequest, UserResponse
from app.models.settings import UserSettings, UserSettingsUpdate from app.models.settings import UserSettings, UserSettingsUpdate
from app.models.analysis import SessionCreate, SessionResponse, SessionList, SessionUpdate from app.models.analysis import SessionCreate, SessionResponse, SessionList
class DBApiClient(TgBackendInterface): class DBApiClient(TgBackendInterface):
@ -11,7 +11,7 @@ class DBApiClient(TgBackendInterface):
Клиент для DB API сервиса. Клиент для DB API сервиса.
Использует Pydantic схемы для type-safety. Использует Pydantic схемы для type-safety.
Методы self.get(), self.post(), self.patch(), self.delete() от TgBackendInterface. Методы self.get(), self.post(), self.put(), self.delete() от TgBackendInterface.
""" """
async def login_user(self, request: LoginRequest) -> UserResponse: async def login_user(self, request: LoginRequest) -> UserResponse:
@ -36,12 +36,11 @@ class DBApiClient(TgBackendInterface):
settings: UserSettingsUpdate settings: UserSettingsUpdate
) -> UserSettings: ) -> UserSettings:
""" """
PATCH {api_prefix}/users/{user_id}/settings PUT {api_prefix}/users/{user_id}/settings
Частично обновить настройки пользователя. Обновить настройки пользователя.
Обновляются только переданные поля.
""" """
return await self.patch( return await self.put(
f"/users/{user_id}/settings", f"/users/{user_id}/settings",
body=settings, body=settings,
response_model=UserSettings response_model=UserSettings
@ -95,23 +94,6 @@ class DBApiClient(TgBackendInterface):
response_model=SessionResponse 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: async def delete_session(self, user_id: str, session_id: str) -> dict:
""" """
DELETE {api_prefix}/users/{user_id}/sessions/{session_id} DELETE {api_prefix}/users/{user_id}/sessions/{session_id}

View File

@ -1,29 +1,17 @@
"""Analysis session Pydantic models.""" """Analysis session Pydantic models."""
from typing import Any, Optional from typing import Any
from pydantic import BaseModel, Field from pydantic import BaseModel
class SessionCreate(BaseModel): class SessionCreate(BaseModel):
"""Create new analysis session.""" """Create new analysis session."""
environment: str = Field(..., description="Environment: ift, psi, or prod") environment: str
api_mode: str = Field(..., description="API mode: bench or backend") api_mode: str
request: list[Any] = Field(..., description="Array of request objects") request: list[Any]
response: dict = Field(..., description="Response object (arbitrary structure)") response: dict
annotations: Optional[dict] = Field(default={}, description="Annotations by question index") annotations: dict
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): class SessionResponse(BaseModel):

View File

@ -1,7 +1,7 @@
"""Query request/response Pydantic models.""" """Query request/response Pydantic models."""
from typing import Any from typing import Any
from pydantic import BaseModel, Field from pydantic import BaseModel
class QuestionRequest(BaseModel): class QuestionRequest(BaseModel):
@ -26,42 +26,10 @@ class BackendQueryRequest(BaseModel):
reset_session: bool = True 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): class QueryResponse(BaseModel):
"""Query response with metadata.""" """Query response with metadata."""
request_id: str request_id: str
timestamp: str timestamp: str
environment: str environment: str
response: RagResponseBenchList | dict | list # RagResponseBenchList для bench, dict/list для backend response: dict

View File

@ -1,43 +1,21 @@
"""User settings Pydantic models.""" """User settings Pydantic models."""
from typing import Optional from pydantic import BaseModel
from pydantic import BaseModel, Field
class EnvironmentSettings(BaseModel): 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" apiMode: str = "bench"
bearerToken: Optional[str] = None bearerToken: str = ""
systemPlatform: Optional[str] = None systemPlatform: str = ""
systemPlatformUser: Optional[str] = None systemPlatformUser: str = ""
platformUserId: Optional[str] = None platformUserId: str = ""
platformId: Optional[str] = None platformId: str = ""
withClassify: bool = False withClassify: bool = False
resetSessionMode: bool = True 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): class UserSettings(BaseModel):
"""User settings for all environments.""" """User settings for all environments."""
@ -47,10 +25,6 @@ class UserSettings(BaseModel):
class UserSettingsUpdate(BaseModel): class UserSettingsUpdate(BaseModel):
"""Partial update user settings request (PATCH). """Update user settings request."""
Only the environments/fields provided will be updated. settings: dict[str, EnvironmentSettings]
Unprovided fields remain unchanged.
"""
settings: dict[str, EnvironmentSettingsUpdate]

View File

@ -12,7 +12,7 @@ import uuid
from typing import List, Dict, Optional, Any from typing import List, Dict, Optional, Any
from datetime import datetime from datetime import datetime
from app.config import settings from app.config import settings
from app.models.query import QuestionRequest, RagResponseBenchList from app.models.query import QuestionRequest
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -200,7 +200,7 @@ class RagService:
questions: List[QuestionRequest], questions: List[QuestionRequest],
user_settings: Dict, user_settings: Dict,
request_id: Optional[str] = None request_id: Optional[str] = None
) -> RagResponseBenchList: ) -> Dict[str, Any]:
""" """
Отправить batch запрос к RAG backend (bench mode). Отправить batch запрос к RAG backend (bench mode).
@ -211,7 +211,7 @@ class RagService:
request_id: Request ID (опционально) request_id: Request ID (опционально)
Returns: Returns:
RagResponseBenchList с ответом от RAG backend Dict с ответом от RAG backend
Raises: Raises:
httpx.HTTPStatusError: При HTTP ошибках httpx.HTTPStatusError: При HTTP ошибках
@ -229,10 +229,7 @@ class RagService:
try: try:
response = await client.post(url, json=body, headers=headers) response = await client.post(url, json=body, headers=headers)
response.raise_for_status() response.raise_for_status()
response_data = response.json() return response.json()
# Валидация ответа через Pydantic модель
return RagResponseBenchList(**response_data)
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
logger.error(f"Bench query failed for {environment}: {e.response.status_code} - {e.response.text}") logger.error(f"Bench query failed for {environment}: {e.response.status_code} - {e.response.text}")
raise raise

View File

@ -1,9 +1,9 @@
<?xml version="1.0" ?> <?xml version="1.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"> <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">
<!-- Generated by coverage.py: https://coverage.readthedocs.io/en/7.13.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 --> <!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd -->
<sources> <sources>
<source>C:\Users\itqop\Documents\code\brief-rags-bench\app</source> <source>C:\Users\leonk\Documents\code\brief-bench-fastapi\app</source>
</sources> </sources>
<packages> <packages>
<package name="." line-rate="0.971" branch-rate="0" complexity="0"> <package name="." line-rate="0.971" branch-rate="0" complexity="0">
@ -18,33 +18,33 @@
<line number="3" hits="1"/> <line number="3" hits="1"/>
<line number="6" hits="1"/> <line number="6" hits="1"/>
<line number="9" hits="1"/> <line number="9" hits="1"/>
<line number="15" hits="1"/>
<line number="16" hits="1"/> <line number="16" hits="1"/>
<line number="18" hits="1"/> <line number="17" hits="1"/>
<line number="19" hits="1"/>
<line number="20" hits="1"/> <line number="20" hits="1"/>
<line number="21" hits="1"/>
<line number="22" hits="1"/> <line number="22" hits="1"/>
<line number="23" hits="1"/>
<line number="25" hits="1"/> <line number="25" hits="1"/>
<line number="26" hits="1"/> <line number="26" hits="1"/>
<line number="27" hits="1"/>
<line number="28" hits="1"/>
<line number="29" hits="1"/> <line number="29" hits="1"/>
<line number="30" hits="1"/> <line number="30" hits="1"/>
<line number="31" hits="1"/>
<line number="32" hits="1"/> <line number="32" hits="1"/>
<line number="33" hits="1"/> <line number="33" hits="1"/>
<line number="34" hits="1"/> <line number="34" hits="1"/>
<line number="35" hits="1"/>
<line number="36" hits="1"/>
<line number="37" hits="1"/> <line number="37" hits="1"/>
<line number="38" hits="1"/>
<line number="39" hits="1"/> <line number="39" hits="1"/>
<line number="40" hits="1"/> <line number="40" hits="1"/>
<line number="41" hits="1"/> <line number="41" hits="1"/>
<line number="42" hits="1"/> <line number="42" hits="1"/>
<line number="43" hits="1"/> <line number="45" hits="1"/>
<line number="44" hits="1"/>
<line number="46" hits="1"/> <line number="46" hits="1"/>
<line number="47" hits="1"/>
<line number="48" hits="1"/>
<line number="49" hits="1"/> <line number="49" hits="1"/>
<line number="50" hits="1"/>
<line number="53" hits="1"/>
<line number="57" hits="1"/>
</lines> </lines>
</class> </class>
<class name="dependencies.py" filename="dependencies.py" complexity="0" line-rate="1" branch-rate="0"> <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="6" hits="1"/>
<line number="8" hits="1"/> <line number="8" hits="1"/>
<line number="9" hits="1"/> <line number="9" hits="1"/>
<line number="11" hits="1"/> <line number="12" hits="1"/>
<line number="17" hits="1"/> <line number="19" hits="1"/>
<line number="25" hits="1"/>
<line number="26" hits="1"/>
<line number="27" hits="1"/>
<line number="28" hits="1"/> <line number="28" hits="1"/>
<line number="29" hits="1"/>
<line number="30" hits="1"/> <line number="30" hits="1"/>
<line number="33" hits="1"/> <line number="31" hits="1"/>
<line number="34" hits="1"/> <line number="34" hits="1"/>
<line number="36" hits="1"/> <line number="37" hits="1"/>
<line number="39" hits="1"/> <line number="38" hits="1"/>
<line number="40" hits="1"/> <line number="40" hits="1"/>
<line number="42" hits="1"/> <line number="43" hits="1"/>
<line number="45" hits="1"/> <line number="44" hits="1"/>
<line number="46" hits="1"/> <line number="46" hits="1"/>
<line number="48" hits="1"/> <line number="49" hits="1"/>
<line number="51" hits="1"/> <line number="50" hits="1"/>
<line number="52" hits="0"/> <line number="52" hits="1"/>
<line number="53" hits="0"/> <line number="55" hits="1"/>
<line number="56" hits="0"/>
<line number="57" hits="0"/>
</lines> </lines>
</class> </class>
</classes> </classes>
@ -106,7 +106,7 @@
</class> </class>
</classes> </classes>
</package> </package>
<package name="api.v1" line-rate="0.9216" branch-rate="0" complexity="0"> <package name="api.v1" line-rate="0.9894" branch-rate="0" complexity="0">
<classes> <classes>
<class name="__init__.py" filename="api/v1/__init__.py" complexity="0" line-rate="1" branch-rate="0"> <class name="__init__.py" filename="api/v1/__init__.py" complexity="0" line-rate="1" branch-rate="0">
<methods/> <methods/>
@ -115,7 +115,7 @@
<line number="7" hits="1"/> <line number="7" hits="1"/>
</lines> </lines>
</class> </class>
<class name="analysis.py" filename="api/v1/analysis.py" complexity="0" line-rate="0.8313" branch-rate="0"> <class name="analysis.py" filename="api/v1/analysis.py" complexity="0" line-rate="1" branch-rate="0">
<methods/> <methods/>
<lines> <lines>
<line number="7" hits="1"/> <line number="7" hits="1"/>
@ -173,34 +173,18 @@
<line number="140" hits="1"/> <line number="140" hits="1"/>
<line number="146" hits="1"/> <line number="146" hits="1"/>
<line number="147" hits="1"/> <line number="147" hits="1"/>
<line number="165" hits="0"/> <line number="161" hits="1"/>
<line number="167" hits="0"/> <line number="163" hits="1"/>
<line number="168" hits="0"/> <line number="164" hits="1"/>
<line number="169" hits="0"/> <line number="165" hits="1"/>
<line number="170" hits="0"/> <line number="166" hits="1"/>
<line number="171" hits="0"/> <line number="167" hits="1"/>
<line number="172" hits="0"/> <line number="168" hits="1"/>
<line number="176" hits="0"/> <line number="172" hits="1"/>
<line number="177" hits="0"/> <line number="173" hits="1"/>
<line number="181" hits="0"/> <line number="177" hits="1"/>
<line number="182" hits="0"/> <line number="178" hits="1"/>
<line number="186" hits="0"/> <line number="179" hits="1"/>
<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> </lines>
</class> </class>
<class name="auth.py" filename="api/v1/auth.py" complexity="0" line-rate="1" branch-rate="0"> <class name="auth.py" filename="api/v1/auth.py" complexity="0" line-rate="1" branch-rate="0">
@ -321,31 +305,31 @@
<line number="48" hits="1"/> <line number="48" hits="1"/>
<line number="54" hits="1"/> <line number="54" hits="1"/>
<line number="55" hits="1"/> <line number="55" hits="1"/>
<line number="69" hits="1"/>
<line number="71" hits="1"/> <line number="71" hits="1"/>
<line number="72" hits="1"/>
<line number="73" hits="1"/> <line number="73" hits="1"/>
<line number="74" hits="1"/> <line number="74" hits="1"/>
<line number="75" hits="1"/> <line number="75" hits="1"/>
<line number="76" hits="1"/> <line number="76" hits="1"/>
<line number="77" hits="1"/> <line number="80" hits="1"/>
<line number="78" hits="1"/> <line number="81" hits="1"/>
<line number="82" hits="1"/> <line number="85" hits="1"/>
<line number="83" hits="1"/> <line number="86" hits="1"/>
<line number="87" hits="1"/> <line number="90" hits="1"/>
<line number="88" hits="1"/> <line number="91" hits="1"/>
<line number="92" hits="1"/> <line number="92" hits="1"/>
<line number="93" hits="1"/>
<line number="94" hits="1"/>
</lines> </lines>
</class> </class>
</classes> </classes>
</package> </package>
<package name="interfaces" line-rate="0.9505" branch-rate="0" complexity="0"> <package name="interfaces" line-rate="1" branch-rate="0" complexity="0">
<classes> <classes>
<class name="__init__.py" filename="interfaces/__init__.py" complexity="0" line-rate="1" branch-rate="0"> <class name="__init__.py" filename="interfaces/__init__.py" complexity="0" line-rate="1" branch-rate="0">
<methods/> <methods/>
<lines/> <lines/>
</class> </class>
<class name="base.py" filename="interfaces/base.py" complexity="0" line-rate="0.9351" branch-rate="0"> <class name="base.py" filename="interfaces/base.py" complexity="0" line-rate="1" branch-rate="0">
<methods/> <methods/>
<lines> <lines>
<line number="9" hits="1"/> <line number="9" hits="1"/>
@ -415,16 +399,10 @@
<line number="252" hits="1"/> <line number="252" hits="1"/>
<line number="253" hits="1"/> <line number="253" hits="1"/>
<line number="255" hits="1"/> <line number="255" hits="1"/>
<line number="278" hits="0"/> <line number="273" hits="1"/>
<line number="279" hits="0"/> <line number="274" hits="1"/>
<line number="280" hits="0"/> <line number="276" hits="1"/>
<line number="282" hits="0"/> <line number="277" hits="1"/>
<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> </lines>
</class> </class>
<class name="db_api_client.py" filename="interfaces/db_api_client.py" complexity="0" line-rate="1" branch-rate="0"> <class name="db_api_client.py" filename="interfaces/db_api_client.py" complexity="0" line-rate="1" branch-rate="0">
@ -440,20 +418,18 @@
<line number="25" hits="1"/> <line number="25" hits="1"/>
<line number="31" hits="1"/> <line number="31" hits="1"/>
<line number="33" hits="1"/> <line number="33" hits="1"/>
<line number="44" hits="1"/> <line number="43" hits="1"/>
<line number="50" hits="1"/> <line number="49" hits="1"/>
<line number="60" hits="1"/> <line number="59" hits="1"/>
<line number="66" hits="1"/> <line number="65" hits="1"/>
<line number="77" hits="1"/>
<line number="78" hits="1"/> <line number="78" hits="1"/>
<line number="79" hits="1"/> <line number="79" hits="1"/>
<line number="80" hits="1"/> <line number="80" hits="1"/>
<line number="81" hits="1"/> <line number="86" hits="1"/>
<line number="87" hits="1"/> <line number="92" hits="1"/>
<line number="93" hits="1"/> <line number="97" hits="1"/>
<line number="98" hits="1"/> <line number="103" hits="1"/>
<line number="109" hits="1"/>
<line number="115" hits="1"/>
<line number="121" hits="1"/>
</lines> </lines>
</class> </class>
</classes> </classes>
@ -484,24 +460,22 @@
<line number="13" hits="1"/> <line number="13" hits="1"/>
<line number="14" hits="1"/> <line number="14" hits="1"/>
<line number="17" 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="26" hits="1"/>
<line number="29" hits="1"/> <line number="27" hits="1"/>
<line number="32" hits="1"/> <line number="28" hits="1"/>
<line number="33" hits="1"/> <line number="31" hits="1"/>
<line number="34" hits="1"/> <line number="34" hits="1"/>
<line number="35" hits="1"/> <line number="35" hits="1"/>
<line number="36" hits="1"/> <line number="36" hits="1"/>
<line number="37" hits="1"/>
<line number="38" hits="1"/>
<line number="39" hits="1"/> <line number="39" hits="1"/>
<line number="40" hits="1"/> <line number="42" hits="1"/>
<line number="43" 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> </lines>
</class> </class>
<class name="auth.py" filename="models/auth.py" complexity="0" line-rate="1" branch-rate="0"> <class name="auth.py" filename="models/auth.py" complexity="0" line-rate="1" branch-rate="0">
@ -544,52 +518,29 @@
<line number="29" hits="1"/> <line number="29" hits="1"/>
<line number="32" hits="1"/> <line number="32" hits="1"/>
<line number="33" hits="1"/> <line number="33" hits="1"/>
<line number="36" hits="1"/> <line number="34" hits="1"/>
<line number="39" hits="1"/> <line number="35" 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> </lines>
</class> </class>
<class name="settings.py" filename="models/settings.py" complexity="0" line-rate="1" branch-rate="0"> <class name="settings.py" filename="models/settings.py" complexity="0" line-rate="1" branch-rate="0">
<methods/> <methods/>
<lines> <lines>
<line number="3" hits="1"/> <line number="3" hits="1"/>
<line number="4" hits="1"/> <line number="6" hits="1"/>
<line number="7" 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="15" hits="1"/> <line number="15" hits="1"/>
<line number="16" hits="1"/> <line number="16" hits="1"/>
<line number="17" hits="1"/>
<line number="18" hits="1"/>
<line number="19" hits="1"/> <line number="19" hits="1"/>
<line number="20" hits="1"/>
<line number="21" hits="1"/>
<line number="22" hits="1"/> <line number="22" hits="1"/>
<line number="25" hits="1"/> <line number="23" hits="1"/>
<line number="31" hits="1"/> <line number="24" hits="1"/>
<line number="32" hits="1"/> <line number="27" hits="1"/>
<line number="33" hits="1"/> <line number="30" 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> </lines>
</class> </class>
</classes> </classes>
@ -698,41 +649,40 @@
<line number="230" hits="1"/> <line number="230" hits="1"/>
<line number="231" hits="1"/> <line number="231" hits="1"/>
<line number="232" hits="1"/> <line number="232" hits="1"/>
<line number="233" hits="1"/>
<line number="234" hits="1"/>
<line number="235" hits="1"/> <line number="235" hits="1"/>
<line number="236" hits="1"/> <line number="236" hits="1"/>
<line number="237" hits="1"/> <line number="237" hits="1"/>
<line number="238" hits="1"/> <line number="238" hits="1"/>
<line number="239" hits="1"/>
<line number="240" hits="1"/> <line number="240" hits="1"/>
<line number="241" hits="1"/> <line number="264" hits="1"/>
<line number="243" hits="1"/> <line number="265" hits="1"/>
<line number="267" hits="1"/> <line number="266" hits="1"/>
<line number="268" hits="1"/> <line number="268" hits="1"/>
<line number="269" hits="1"/> <line number="273" hits="1"/>
<line number="271" hits="1"/> <line number="275" hits="1"/>
<line number="276" hits="1"/> <line number="277" hits="1"/>
<line number="278" hits="1"/> <line number="278" hits="1"/>
<line number="280" hits="1"/> <line number="285" hits="1"/>
<line number="281" hits="1"/> <line number="287" hits="1"/>
<line number="288" hits="1"/> <line number="289" hits="1"/>
<line number="290" hits="1"/> <line number="290" hits="1"/>
<line number="291" hits="1"/>
<line number="292" hits="1"/> <line number="292" hits="1"/>
<line number="293" hits="1"/>
<line number="294" hits="1"/>
<line number="295" hits="1"/> <line number="295" hits="1"/>
<line number="296" hits="1"/>
<line number="297" hits="1"/>
<line number="298" hits="1"/> <line number="298" hits="1"/>
<line number="299" hits="1"/> <line number="303" hits="1"/>
<line number="300" hits="1"/> <line number="305" hits="1"/>
<line number="301" hits="1"/>
<line number="306" hits="1"/> <line number="306" hits="1"/>
<line number="308" hits="1"/> <line number="310" hits="1"/>
<line number="309" hits="1"/> <line number="311" hits="1"/>
<line number="312" hits="1"/>
<line number="313" hits="1"/> <line number="313" hits="1"/>
<line number="314" hits="1"/>
<line number="315" hits="1"/> <line number="315" hits="1"/>
<line number="316" hits="1"/> <line number="316" hits="1"/>
<line number="318" hits="1"/>
<line number="319" hits="1"/>
</lines> </lines>
</class> </class>
</classes> </classes>

View File

@ -1,27 +0,0 @@
from pydantic import BaseModel, Field
class Docs(BaseModel):
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):
answers: list[RagResponseBench]

View File

@ -107,7 +107,7 @@ pytest -x
pytest -s pytest -s
``` ```
## Покрытие тестами ## Покрытие (Coverage)
### Unit Tests: **99%** (567 строк, 4 непокрыто) ### Unit Tests: **99%** (567 строк, 4 непокрыто)
@ -133,51 +133,51 @@ pytest -s
## Что тестируется ## Что тестируется
### 1. Authentication (test_auth.py) ### 1. Authentication (test_auth.py)
- Успешная авторизация с валидным 8-значным логином - Успешная авторизация с валидным 8-значным логином
- Отклонение невалидных форматов логина - Отклонение невалидных форматов логина
- Обработка ошибок DB API - Обработка ошибок DB API
- Генерация JWT токенов - Генерация JWT токенов
- Валидация токенов - Валидация токенов
### 2. Settings (test_settings.py) ### 2. Settings (test_settings.py)
- Получение настроек пользователя - Получение настроек пользователя
- Обновление настроек - Обновление настроек
- Обработка несуществующих пользователей - Обработка несуществующих пользователей
- Валидация формата настроек - Валидация формата настроек
- Требование авторизации - Требование авторизации
### 3. Query (test_query.py) ### 3. Query (test_query.py)
- Bench mode запросы - Bench mode запросы
- Backend mode запросы - Backend mode запросы
- Валидация окружений (ift/psi/prod) - Валидация окружений (ift/psi/prod)
- Проверка соответствия apiMode - Проверка соответствия apiMode
- Обработка ошибок RAG backend - Обработка ошибок RAG backend
- Построение headers для RAG - Построение headers для RAG
- Session reset в Backend mode - Session reset в Backend mode
### 4. Analysis (test_analysis.py) ### 4. Analysis (test_analysis.py)
- Создание сессий анализа - Создание сессий анализа
- Получение списка сессий - Получение списка сессий
- Фильтрация по окружению - Фильтрация по окружению
- Пагинация - Пагинация
- Получение конкретной сессии - Получение конкретной сессии
- Удаление сессии - Удаление сессии
- Требование авторизации - Требование авторизации
### 5. Security (test_security.py) ### 5. Security (test_security.py)
- Создание JWT токенов - Создание JWT токенов
- Декодирование токенов - Декодирование токенов
- Обработка невалидных токенов - Обработка невалидных токенов
- Обработка истекших токенов - Обработка истекших токенов
- Кастомное время жизни токенов - Кастомное время жизни токенов
### 6. Models (test_models.py) ### 6. Models (test_models.py)
- Валидация LoginRequest (8 цифр) - Валидация LoginRequest (8 цифр)
- Валидация QuestionRequest - Валидация QuestionRequest
- Валидация BenchQueryRequest - Валидация BenchQueryRequest
- Валидация BackendQueryRequest - Валидация BackendQueryRequest
- Валидация EnvironmentSettings - Валидация EnvironmentSettings
- Дефолтные значения - Дефолтные значения
## Моки ## Моки

View File

@ -144,36 +144,20 @@ def unauthenticated_client():
@pytest.fixture @pytest.fixture
def mock_bench_response(): def mock_bench_response():
"""Mock RAG backend bench response (matches format.py structure).""" """Mock RAG backend bench response."""
return { return {
"answers": [ "answers": [
{ {
"body_research": "Test research answer 1", "question_id": 1,
"body_analytical_hub": "Test analytical hub answer 1", "answer": "Test answer 1",
"docs_from_vectorstore": { "confidence": 0.95,
"research": [], "docs": []
"analytical_hub": []
},
"docs_to_llm": {
"research": [],
"analytical_hub": []
},
"processing_time_sec": 1.5,
"question": "Test question 1"
}, },
{ {
"body_research": "Test research answer 2", "question_id": 2,
"body_analytical_hub": "Test analytical hub answer 2", "answer": "Test answer 2",
"docs_from_vectorstore": { "confidence": 0.87,
"research": [], "docs": []
"analytical_hub": []
},
"docs_to_llm": {
"research": [],
"analytical_hub": []
},
"processing_time_sec": 2.3,
"question": "Test question 2"
} }
] ]
} }

View File

@ -435,3 +435,4 @@ When adding new E2E tests:
- [Integration Tests](../integration/README.md) - Tests for DB API integration only - [Integration Tests](../integration/README.md) - Tests for DB API integration only
- [Unit Tests](../unit/) - Fast isolated tests - [Unit Tests](../unit/) - Fast isolated tests
- [DB API Contract](../../DB_API_CONTRACT.md) - External DB API specification - [DB API Contract](../../DB_API_CONTRACT.md) - External DB API specification
- [CLAUDE.md](../../CLAUDE.md) - Project architecture overview

View File

@ -88,20 +88,20 @@ tests/integration/
## Что тестируется ## Что тестируется
### Auth Integration (`test_auth_integration.py`) ### Auth Integration (`test_auth_integration.py`)
- Успешная авторизация с реальным DB API - Успешная авторизация с реальным DB API
- Генерация и валидация JWT токенов - Генерация и валидация JWT токенов
- Защита endpoint-ов с использованием JWT - Защита endpoint-ов с использованием JWT
- Обработка ошибок аутентификации - Обработка ошибок аутентификации
### Settings Integration (`test_settings_integration.py`) ### Settings Integration (`test_settings_integration.py`)
- Получение настроек пользователя из DB API - Получение настроек пользователя из DB API
- Обновление настроек для всех окружений (IFT, PSI, PROD) - Обновление настроек для всех окружений (IFT, PSI, PROD)
- Частичное обновление настроек - Частичное обновление настроек
- Персистентность настроек - Персистентность настроек
- Проверка структуры данных настроек - Проверка структуры данных настроек
### Analysis Integration (`test_analysis_integration.py`) ### Analysis Integration (`test_analysis_integration.py`)
- Создание сессий анализа в DB API - Создание сессий анализа в DB API
- Получение списка сессий с фильтрацией - Получение списка сессий с фильтрацией
- Пагинация сессий - Пагинация сессий
@ -109,7 +109,7 @@ tests/integration/
- Удаление сессий - Удаление сессий
- Целостность данных (включая Unicode, вложенные структуры) - Целостность данных (включая Unicode, вложенные структуры)
### Query Integration (`test_query_integration.py`) ### Query Integration (`test_query_integration.py`)
- Получение настроек пользователя для запросов - Получение настроек пользователя для запросов
- Проверка соответствия apiMode (bench/backend) - Проверка соответствия apiMode (bench/backend)
- Обновление настроек между запросами - Обновление настроек между запросами
@ -117,10 +117,10 @@ tests/integration/
## Что НЕ тестируется ## Что НЕ тестируется
**RAG Backend взаимодействие** - требует запущенные RAG сервисы (IFT/PSI/PROD) **RAG Backend взаимодействие** - требует запущенные RAG сервисы (IFT/PSI/PROD)
**mTLS сертификаты** - требует реальные сертификаты **mTLS сертификаты** - требует реальные сертификаты
**Производительность** - используйте отдельные performance тесты **Производительность** - используйте отдельные performance тесты
**Нагрузочное тестирование** - используйте инструменты типа Locust/K6 **Нагрузочное тестирование** - используйте инструменты типа Locust/K6
## Troubleshooting ## Troubleshooting

View File

@ -4,8 +4,8 @@ import pytest
from unittest.mock import AsyncMock, patch, MagicMock from unittest.mock import AsyncMock, patch, MagicMock
from app.interfaces.db_api_client import DBApiClient from app.interfaces.db_api_client import DBApiClient
from app.models.auth import LoginRequest, UserResponse from app.models.auth import LoginRequest, UserResponse
from app.models.settings import UserSettings, UserSettingsUpdate, EnvironmentSettings, EnvironmentSettingsUpdate from app.models.settings import UserSettings, UserSettingsUpdate, EnvironmentSettings
from app.models.analysis import SessionCreate, SessionResponse, SessionList, SessionListItem, SessionUpdate from app.models.analysis import SessionCreate, SessionResponse, SessionList, SessionListItem
class TestDBApiClient: class TestDBApiClient:
@ -70,14 +70,19 @@ class TestDBApiClient:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_update_user_settings(self): async def test_update_user_settings(self):
"""Test update_user_settings calls patch correctly.""" """Test update_user_settings calls put correctly."""
with patch('app.interfaces.base.httpx.AsyncClient'): with patch('app.interfaces.base.httpx.AsyncClient'):
client = DBApiClient(api_prefix="http://db-api:8080/api/v1") client = DBApiClient(api_prefix="http://db-api:8080/api/v1")
settings_update = UserSettingsUpdate( settings_update = UserSettingsUpdate(
settings={ settings={
"ift": EnvironmentSettingsUpdate( "ift": EnvironmentSettings(
apiMode="backend", apiMode="backend",
bearerToken="",
systemPlatform="",
systemPlatformUser="",
platformUserId="",
platformId="",
withClassify=True, withClassify=True,
resetSessionMode=False resetSessionMode=False
) )
@ -85,26 +90,15 @@ class TestDBApiClient:
) )
mock_updated_settings = UserSettings( mock_updated_settings = UserSettings(
user_id="user-123", user_id="user-123",
settings={ settings=settings_update.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" updated_at="2024-01-01T01:00:00Z"
) )
client.patch = AsyncMock(return_value=mock_updated_settings) client.put = AsyncMock(return_value=mock_updated_settings)
result = await client.update_user_settings("user-123", settings_update) result = await client.update_user_settings("user-123", settings_update)
assert result == mock_updated_settings assert result == mock_updated_settings
client.patch.assert_called_once_with( client.put.assert_called_once_with(
"/users/user-123/settings", "/users/user-123/settings",
body=settings_update, body=settings_update,
response_model=UserSettings response_model=UserSettings
@ -217,44 +211,6 @@ class TestDBApiClient:
response_model=SessionResponse 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 @pytest.mark.asyncio
async def test_delete_session(self): async def test_delete_session(self):
"""Test delete_session calls delete correctly.""" """Test delete_session calls delete correctly."""

View File

@ -4,8 +4,7 @@ import pytest
from pydantic import ValidationError from pydantic import ValidationError
from app.models.auth import LoginRequest, UserResponse, LoginResponse from app.models.auth import LoginRequest, UserResponse, LoginResponse
from app.models.query import QuestionRequest, BenchQueryRequest, BackendQueryRequest, QueryResponse from app.models.query import QuestionRequest, BenchQueryRequest, BackendQueryRequest, QueryResponse
from app.models.settings import EnvironmentSettings, EnvironmentSettingsUpdate, UserSettingsUpdate from app.models.settings import EnvironmentSettings, UserSettingsUpdate
from app.models.analysis import SessionCreate, SessionUpdate
class TestAuthModels: class TestAuthModels:
@ -121,34 +120,16 @@ class TestQueryModels:
def test_query_response(self): def test_query_response(self):
"""Test QueryResponse model.""" """Test QueryResponse model."""
# Test with RagResponseBenchList (parsed from dict)
response = QueryResponse( response = QueryResponse(
request_id="req-123", request_id="req-123",
timestamp="2024-01-01T00:00:00Z", timestamp="2024-01-01T00:00:00Z",
environment="ift", environment="ift",
response={"answers": []} # Auto-parsed to RagResponseBenchList response={"answers": []}
) )
assert response.request_id == "req-123" assert response.request_id == "req-123"
assert response.environment == "ift" 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 isinstance(response.response, dict)
assert response.response["status"] == "ok"
class TestSettingsModels: class TestSettingsModels:
@ -173,135 +154,30 @@ class TestSettingsModels:
assert settings.resetSessionMode is False assert settings.resetSessionMode is False
def test_environment_settings_defaults(self): def test_environment_settings_defaults(self):
"""Test EnvironmentSettings with default values (nullable fields).""" """Test EnvironmentSettings with default values."""
settings = EnvironmentSettings() settings = EnvironmentSettings(apiMode="backend")
assert settings.apiMode == "bench" assert settings.apiMode == "backend"
assert settings.bearerToken is None assert settings.bearerToken == ""
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.withClassify is False
assert settings.resetSessionMode is True 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): def test_user_settings_update(self):
"""Test UserSettingsUpdate model with partial updates.""" """Test UserSettingsUpdate model."""
update = UserSettingsUpdate( update = UserSettingsUpdate(
settings={ settings={
"ift": EnvironmentSettingsUpdate(apiMode="bench", withClassify=True), "ift": EnvironmentSettings(apiMode="bench"),
"psi": EnvironmentSettingsUpdate(bearerToken="token123") "psi": EnvironmentSettings(apiMode="backend")
} }
) )
assert "ift" in update.settings assert "ift" in update.settings
assert "psi" in update.settings assert "psi" in update.settings
assert update.settings["ift"].apiMode == "bench" assert update.settings["ift"].apiMode == "bench"
assert update.settings["ift"].withClassify is True assert update.settings["psi"].apiMode == "backend"
assert update.settings["psi"].bearerToken == "token123"
assert update.settings["psi"].apiMode is None # Not provided
def test_user_settings_update_empty(self): def test_user_settings_update_empty(self):
"""Test UserSettingsUpdate with empty settings.""" """Test UserSettingsUpdate with empty settings."""
update = UserSettingsUpdate(settings={}) update = UserSettingsUpdate(settings={})
assert update.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 == {}

View File

@ -4,7 +4,7 @@ import pytest
from unittest.mock import AsyncMock, patch, MagicMock from unittest.mock import AsyncMock, patch, MagicMock
import httpx import httpx
from app.services.rag_service import RagService from app.services.rag_service import RagService
from app.models.query import QuestionRequest, RagResponseBenchList from app.models.query import QuestionRequest
class TestBenchQueryEndpoint: class TestBenchQueryEndpoint:
@ -16,9 +16,7 @@ class TestBenchQueryEndpoint:
with patch('app.api.v1.query.RagService') as MockRagService: with patch('app.api.v1.query.RagService') as MockRagService:
mock_rag = AsyncMock() mock_rag = AsyncMock()
# Mock возвращает RagResponseBenchList mock_rag.send_bench_query = AsyncMock(return_value=mock_bench_response)
from app.models.query import RagResponseBenchList
mock_rag.send_bench_query = AsyncMock(return_value=RagResponseBenchList(**mock_bench_response))
mock_rag.close = AsyncMock() mock_rag.close = AsyncMock()
MockRagService.return_value = mock_rag MockRagService.return_value = mock_rag
@ -39,9 +37,7 @@ class TestBenchQueryEndpoint:
assert "timestamp" in data assert "timestamp" in data
assert data["environment"] == "ift" assert data["environment"] == "ift"
assert "response" in data assert "response" in data
# FastAPI автоматически сериализует RagResponseBenchList в dict assert data["response"] == mock_bench_response
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.send_bench_query.assert_called_once()
mock_rag.close.assert_called_once() mock_rag.close.assert_called_once()
@ -272,12 +268,7 @@ class TestRagService:
request_id="test-request-123" request_id="test-request-123"
) )
# Проверяем, что результат - это RagResponseBenchList assert result == mock_bench_response
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() mock_httpx_client.post.assert_called_once()

View File

@ -46,7 +46,7 @@ class TestSettingsEndpoints:
assert response.status_code == 401 assert response.status_code == 401
def test_update_settings_success(self, client, mock_db_client, test_settings): def test_update_settings_success(self, client, mock_db_client, test_settings):
"""Test updating user settings successfully with PATCH (partial update).""" """Test updating user settings successfully."""
mock_db_client.update_user_settings = AsyncMock(return_value=test_settings) mock_db_client.update_user_settings = AsyncMock(return_value=test_settings)
update_data = { update_data = {
@ -59,7 +59,7 @@ class TestSettingsEndpoints:
} }
} }
response = client.patch("/api/v1/settings", json=update_data) response = client.put("/api/v1/settings", json=update_data)
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
@ -80,7 +80,7 @@ class TestSettingsEndpoints:
} }
} }
response = client.patch("/api/v1/settings", json=update_data) response = client.put("/api/v1/settings", json=update_data)
assert response.status_code == 400 assert response.status_code == 400
@ -96,7 +96,7 @@ class TestSettingsEndpoints:
} }
} }
response = client.patch("/api/v1/settings", json=update_data) response = client.put("/api/v1/settings", json=update_data)
assert response.status_code == 500 assert response.status_code == 500
@ -136,7 +136,7 @@ class TestSettingsEndpoints:
} }
} }
response = client.patch("/api/v1/settings", json=update_data) response = client.put("/api/v1/settings", json=update_data)
assert response.status_code == 404 assert response.status_code == 404
assert "user not found" in response.json()["detail"].lower() assert "user not found" in response.json()["detail"].lower()
@ -154,26 +154,7 @@ class TestSettingsEndpoints:
} }
} }
response = client.patch("/api/v1/settings", json=update_data) response = client.put("/api/v1/settings", json=update_data)
assert response.status_code == 502 assert response.status_code == 502
assert "failed to update settings" in response.json()["detail"].lower() 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()