first version

This commit is contained in:
itqop 2025-12-17 17:37:32 +03:00
commit 85dc167449
36 changed files with 4370 additions and 0 deletions

View File

@ -0,0 +1,15 @@
{
"permissions": {
"allow": [
"Bash(python -m py_compile:*)",
"Bash(test:*)",
"Bash(python -c:*)",
"Bash(dir:*)",
"Bash(findstr:*)",
"Bash(grep:*)",
"Bash(cat:*)"
],
"deny": [],
"ask": []
}
}

39
.env.example Normal file
View File

@ -0,0 +1,39 @@
# Application
DEBUG=false
APP_NAME=Brief Bench API
# JWT Authentication
JWT_SECRET_KEY=your-super-secret-key-change-in-production
JWT_ALGORITHM=HS256
JWT_EXPIRE_MINUTES=43200 # 30 days
# DB API Service (external)
DB_API_URL=http://db-api-service:8080/api/v1
DB_API_TIMEOUT=30
# RAG Backend - IFT Environment
IFT_RAG_HOST=ift-rag.example.com
IFT_RAG_PORT=8443
IFT_RAG_ENDPOINT=api/rag/bench
IFT_RAG_CERT_CA=/app/certs/ift/ca.crt
IFT_RAG_CERT_KEY=/app/certs/ift/client.key
IFT_RAG_CERT_CERT=/app/certs/ift/client.crt
# RAG Backend - PSI Environment
PSI_RAG_HOST=psi-rag.example.com
PSI_RAG_PORT=8443
PSI_RAG_ENDPOINT=api/rag/bench
PSI_RAG_CERT_CA=/app/certs/psi/ca.crt
PSI_RAG_CERT_KEY=/app/certs/psi/client.key
PSI_RAG_CERT_CERT=/app/certs/psi/client.crt
# RAG Backend - PROD Environment
PROD_RAG_HOST=prod-rag.example.com
PROD_RAG_PORT=8443
PROD_RAG_ENDPOINT=api/rag/bench
PROD_RAG_CERT_CA=/app/certs/prod/ca.crt
PROD_RAG_CERT_KEY=/app/certs/prod/client.key
PROD_RAG_CERT_CERT=/app/certs/prod/client.crt
# Request Timeouts (seconds)
RAG_REQUEST_TIMEOUT=1800 # 30 minutes

62
.gitignore vendored Normal file
View File

@ -0,0 +1,62 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
venv/
ENV/
env/
.venv
# Environment variables
.env
.env.local
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Certificates (sensitive!)
certs/
*.pem
*.key
*.crt
*.cert
# Logs
*.log
logs/
# Testing
.pytest_cache/
.coverage
htmlcov/
.tox/
# Docker
docker-compose.override.yml

244
CLAUDE.md Normal file
View File

@ -0,0 +1,244 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Brief Bench FastAPI is a FastAPI backend for a RAG (Retrieval-Augmented Generation) testing system with multi-user support. The system allows users to test RAG backends across three environments (IFT, PSI, PROD) in two modes: Bench mode (batch testing) and Backend mode (bot simulation).
## Key Concepts
### Multi-Environment Architecture
- **Three environments**: IFT, PSI, PROD (each with separate RAG backend hosts)
- **Per-user settings**: Each user has individual settings for each environment (stored in external DB API)
- **Server configuration**: RAG hosts, ports, endpoints, and mTLS cert paths are defined in `.env` (not user-editable)
### Two Query Modes
1. **Bench Mode**: Send all questions in a single batch request to RAG backend
- Endpoint: `POST /api/v1/query/bench`
- Uses headers: `Request-Id`, `System-Id`, `Authorization`, `System-Platform`
2. **Backend Mode**: Send questions one-by-one, simulating bot behavior
- Endpoint: `POST /api/v1/query/backend`
- Can reset session after each question (controlled by `resetSessionMode` setting)
- Uses headers: `Platform-User-Id`, `Platform-Id`, `Authorization`
### Authentication Flow
1. User sends `POST /api/v1/auth/login?login=12345678` (8-digit login)
2. FastAPI forwards to DB API: `POST /users/login`
3. DB API returns user info (user_id, login, timestamps)
4. FastAPI generates JWT token (30-day expiration) with {user_id, login, exp}
5. Returns {access_token, user} to client
6. All subsequent requests use `Authorization: Bearer {token}`
7. Middleware `get_current_user` validates JWT on protected endpoints
## Development Commands
### Local Development
```bash
# Create virtual environment
python -m venv venv
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
# Install dependencies
pip install -r requirements.txt
# Run development server (with auto-reload)
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
```
### Docker
```bash
# Build and run
docker-compose up -d
# View logs
docker-compose logs -f fastapi
# Stop
docker-compose down
```
### Environment Setup
```bash
# Copy example and edit
cp .env.example .env
# Required variables:
# - JWT_SECRET_KEY (generate a new secret!)
# - DB_API_URL (external DB API service URL)
# - IFT_RAG_HOST, PSI_RAG_HOST, PROD_RAG_HOST
# - Port and endpoint configs for each environment
# - Optional: mTLS certificate paths (certs/ directory)
```
## Architecture Details
### Layered Structure
```
app/
├── api/v1/ # API endpoints (FastAPI routers)
├── models/ # Pydantic models for validation
├── services/ # Business logic layer
├── interfaces/ # External API clients
├── middleware/ # Request/response middleware
├── utils/ # Utilities (JWT, security)
├── config.py # Settings from .env (pydantic-settings)
├── dependencies.py # Dependency injection
└── main.py # FastAPI app entry point
```
### TgBackendInterface Pattern
The `TgBackendInterface` class ([app/interfaces/base.py](app/interfaces/base.py)) is the base for all HTTP API clients:
- Wraps `httpx.AsyncClient` with Pydantic model serialization/deserialization
- Provides `get()`, `post()`, `put()`, `delete()` methods
- Handles error logging and HTTP status errors
- Auto-serializes Pydantic models to JSON and deserializes JSON to Pydantic models
- Initialized with `api_prefix` (base URL)
**DBApiClient** ([app/interfaces/db_api_client.py](app/interfaces/db_api_client.py)) extends this for DB API integration:
- `login_user()` - authenticate user
- `get_user_settings()`, `update_user_settings()` - manage per-user settings
- `save_session()`, `get_sessions()`, `get_session()`, `delete_session()` - analysis sessions
See [DB_API_CONTRACT.md](DB_API_CONTRACT.md) for full DB API specification.
### RagService Pattern
The `RagService` class ([app/services/rag_service.py](app/services/rag_service.py)) manages RAG backend communication:
- Creates separate `httpx.AsyncClient` for each environment (IFT, PSI, PROD)
- Configures mTLS certificates from `.env` settings
- `send_bench_query()` - batch mode requests
- `send_backend_query()` - sequential requests with optional session reset
- Builds environment-specific headers from user settings
### Dependency Injection
Located in [app/dependencies.py](app/dependencies.py):
- `get_db_client()` - Returns DBApiClient instance
- `get_current_user()` - JWT auth middleware, validates Bearer token, returns {user_id, login}
Use in endpoints:
```python
@router.get("/protected")
async def protected_endpoint(
current_user: dict = Depends(get_current_user),
db_client: DBApiClient = Depends(get_db_client)
):
user_id = current_user["user_id"]
# ...
```
### User Settings vs Server Configuration
**Server Config (`.env`)**: RAG hosts, ports, endpoints, mTLS cert paths - applies to all users
**User Settings (DB API)**: Per-environment settings for each user:
- `apiMode`: "bench" or "backend"
- `bearerToken`: Authorization token for RAG backend
- `systemPlatform`, `systemPlatformUser`: Platform identifiers
- `platformUserId`, `platformId`: Platform-specific IDs
- `withClassify`: Enable classification in backend mode
- `resetSessionMode`: Reset session after each question in backend mode
## Important Implementation Notes
### mTLS Certificate Handling
- Certificate paths are configured per-environment in `.env`
- Format: `/app/certs/{env}/ca.crt`, `/app/certs/{env}/client.key`, `/app/certs/{env}/client.crt`
- RagService loads certificates from these paths when creating httpx clients
- Certificates are mounted read-only in Docker: `./certs:/app/certs:ro`
### Error Handling
- All API client methods can raise `httpx.HTTPStatusError` for HTTP errors
- Endpoints should catch these and convert to FastAPI `HTTPException`
- Use `status.HTTP_502_BAD_GATEWAY` for external service errors
- Use `status.HTTP_500_INTERNAL_SERVER_ERROR` for unexpected errors
- Always log errors with context (user_id, environment, request details)
### Async Context Managers
Both TgBackendInterface and RagService support async context managers:
```python
async with RagService() as rag_service:
response = await rag_service.send_bench_query(...)
# Client automatically closed
```
Or manually close:
```python
rag_service = RagService()
try:
response = await rag_service.send_bench_query(...)
finally:
await rag_service.close()
```
### Long-Running Requests
- RAG requests can take up to 30 minutes (`RAG_REQUEST_TIMEOUT=1800`)
- RagService uses 30-minute timeout for httpx clients
- DB API has separate timeout (`DB_API_TIMEOUT=30`, default 30s)
## API Endpoints
### Authentication
- `POST /api/v1/auth/login?login={8-digit}` - Login with 8-digit code, returns JWT token
### Settings
- `GET /api/v1/settings` - Get current user's settings for all environments
- `PUT /api/v1/settings` - Update user settings
### Query
- `POST /api/v1/query/bench` - Send batch query (bench mode)
- `POST /api/v1/query/backend` - Send sequential queries (backend mode)
### Analysis Sessions
- `POST /api/v1/analysis/sessions` - Save analysis session
- `GET /api/v1/analysis/sessions` - List sessions (filterable by environment)
- `GET /api/v1/analysis/sessions/{session_id}` - Get specific session
- `DELETE /api/v1/analysis/sessions/{session_id}` - Delete session
### Health
- `GET /health` - Health check
- `GET /` - API info
## Testing Strategy
When adding tests:
1. Unit test services in isolation (mock httpx responses)
2. Integration test API endpoints (use TestClient from FastAPI)
3. Mock external dependencies (DB API, RAG backends)
4. Test error paths (network failures, invalid tokens, missing settings)
## Common Development Scenarios
### Adding a New API Endpoint
1. Define Pydantic models in `app/models/`
2. Create endpoint in appropriate `app/api/v1/` router
3. Add business logic to `app/services/` if needed
4. Register router in `app/main.py` with `app.include_router()`
### Modifying DB API Integration
1. Update contract in [DB_API_CONTRACT.md](DB_API_CONTRACT.md)
2. Update Pydantic models in `app/models/`
3. Add/modify methods in [app/interfaces/db_api_client.py](app/interfaces/db_api_client.py)
### Adding RAG Backend Endpoints
1. Add endpoint config to `.env.example` and `app/config.py`
2. Update user settings model if needed (`app/models/settings.py`)
3. Modify `RagService` methods to use new endpoints
4. Update query endpoint logic in `app/api/v1/query.py`
## Security Considerations
- JWT tokens have 30-day expiration (configurable via `JWT_EXPIRE_MINUTES`)
- mTLS certificates are stored server-side only (never sent to client)
- CORS is currently set to `allow_origins=["*"]` - configure properly for production
- Never commit `.env` file (use `.env.example` as template)
- Secrets (JWT_SECRET_KEY, bearer tokens) must be kept secure
- User settings may contain sensitive tokens - handle with care
## Project Status
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)
- All main API endpoints are implemented
- TgBackendInterface is fully implemented (not a stub)
- Frontend integration pending (static/ directory is empty)
- No tests yet (tests/ directory is empty)

308
DB_API_CONTRACT.md Normal file
View File

@ -0,0 +1,308 @@
# DB API Contract
Контракт для взаимодействия FastAPI приложения Brief Bench с внешним DB API сервисом.
## Base URL
```
{DB_API_URL} # Из .env, например: http://db-api-service:8080/api/v1
```
---
## Authentication & Users
### POST /users/login
Авторизация пользователя и запись логина.
**Request:**
```json
{
"login": "12345678", // 8-значный логин (строка)
"client_ip": "192.168.1.100"
}
```
**Response 200:**
```json
{
"user_id": "uuid-string",
"login": "12345678",
"last_login_at": "2025-12-17T12:00:00Z",
"created_at": "2025-12-01T10:00:00Z"
}
```
**Errors:**
- 400: Неверный формат логина
- 500: Ошибка сервера
---
## User Settings
### GET /users/{user_id}/settings
Получить настройки пользователя для всех окружений (IFT, PSI, PROD).
**Path Parameters:**
- `user_id`: UUID пользователя
**Response 200:**
```json
{
"user_id": "uuid-string",
"settings": {
"ift": {
"apiMode": "bench",
"bearerToken": "...",
"systemPlatform": "...",
"systemPlatformUser": "...",
"platformUserId": "...",
"platformId": "...",
"withClassify": false,
"resetSessionMode": true
},
"psi": { ... },
"prod": { ... }
},
"updated_at": "2025-12-17T12:00:00Z"
}
```
**Errors:**
- 404: Пользователь не найден
- 500: Ошибка сервера
---
### PUT /users/{user_id}/settings
Обновить настройки пользователя.
**Path Parameters:**
- `user_id`: UUID пользователя
**Request:**
```json
{
"settings": {
"ift": {
"apiMode": "bench",
"bearerToken": "...",
"systemPlatform": "...",
"systemPlatformUser": "...",
"platformUserId": "...",
"platformId": "...",
"withClassify": false,
"resetSessionMode": true
},
"psi": { ... },
"prod": { ... }
}
}
```
**Response 200:**
```json
{
"user_id": "uuid-string",
"settings": { ... },
"updated_at": "2025-12-17T12:05:00Z"
}
```
**Errors:**
- 404: Пользователь не найден
- 400: Неверный формат настроек
- 500: Ошибка сервера
---
## Analysis Sessions
### POST /users/{user_id}/sessions
Создать новую сессию анализа.
**Path Parameters:**
- `user_id`: UUID пользователя
**Request:**
```json
{
"environment": "ift", // ift | psi | prod
"api_mode": "bench", // bench | backend
"request": [
{ "body": "Вопрос 1", "with_docs": true },
{ "body": "Вопрос 2", "with_docs": true }
],
"response": {
"answers": [ ... ] // RagResponseBenchList format
},
"annotations": {
"0": {
"overall": { "rating": "correct", "comment": "..." },
"body_research": { "issues": [], "comment": "" },
...
}
}
}
```
**Response 201:**
```json
{
"session_id": "uuid-string",
"user_id": "uuid-string",
"environment": "ift",
"api_mode": "bench",
"request": [ ... ],
"response": { ... },
"annotations": { ... },
"created_at": "2025-12-17T12:00:00Z",
"updated_at": "2025-12-17T12:00:00Z"
}
```
**Errors:**
- 404: Пользователь не найден
- 400: Неверный формат данных
- 500: Ошибка сервера
---
### GET /users/{user_id}/sessions
Получить список сессий пользователя.
**Path Parameters:**
- `user_id`: UUID пользователя
**Query Parameters:**
- `environment` (optional): Фильтр по окружению (ift/psi/prod)
- `limit` (optional): Лимит результатов (default: 50)
- `offset` (optional): Смещение для пагинации (default: 0)
**Response 200:**
```json
{
"sessions": [
{
"session_id": "uuid-string",
"environment": "ift",
"created_at": "2025-12-17T12:00:00Z"
},
...
],
"total": 123
}
```
**Errors:**
- 404: Пользователь не найден
- 500: Ошибка сервера
---
### GET /users/{user_id}/sessions/{session_id}
Получить конкретную сессию по ID.
**Path Parameters:**
- `user_id`: UUID пользователя
- `session_id`: UUID сессии
**Response 200:**
```json
{
"session_id": "uuid-string",
"user_id": "uuid-string",
"environment": "ift",
"api_mode": "bench",
"request": [ ... ],
"response": { ... },
"annotations": { ... },
"created_at": "2025-12-17T12:00:00Z",
"updated_at": "2025-12-17T12:00:00Z"
}
```
**Errors:**
- 404: Сессия или пользователь не найдены
- 500: Ошибка сервера
---
### PUT /users/{user_id}/sessions/{session_id}
Обновить сессию (например, аннотации).
**Path Parameters:**
- `user_id`: UUID пользователя
- `session_id`: UUID сессии
**Request:**
```json
{
"annotations": {
"0": { ... },
"1": { ... }
}
}
```
**Response 200:**
```json
{
"session_id": "uuid-string",
"user_id": "uuid-string",
...
"updated_at": "2025-12-17T12:05:00Z"
}
```
**Errors:**
- 404: Сессия или пользователь не найдены
- 400: Неверный формат данных
- 500: Ошибка сервера
---
### DELETE /users/{user_id}/sessions/{session_id}
Удалить сессию.
**Path Parameters:**
- `user_id`: UUID пользователя
- `session_id`: UUID сессии
**Response 204:**
Нет тела ответа.
**Errors:**
- 404: Сессия или пользователь не найдены
- 500: Ошибка сервера
---
## Common Error Format
Все ошибки возвращаются в едином формате:
```json
{
"detail": "Описание ошибки",
"error_code": "OPTIONAL_ERROR_CODE"
}
```
---
## Notes
1. Все даты в формате ISO 8601: `YYYY-MM-DDTHH:MM:SSZ`
2. UUID используется для идентификации пользователей и сессий
3. Все запросы должны включать `Content-Type: application/json`
4. Пустые строки в настройках означают "не задано" (опциональные поля)

682
DEVELOPMENT_PLAN.md Normal file
View File

@ -0,0 +1,682 @@
# План развития Brief Bench FastAPI
**Дата создания:** 17 декабря 2025
**Статус:** Backend готов, требуется frontend интеграция и тестирование
---
## Текущее состояние
### ✅ Готово (Backend)
- Структура FastAPI приложения
- JWT авторизация (8-значный логин)
- TgBackendInterface (полная реализация с httpx)
- DBApiClient (интеграция с DB API)
- RagService (интеграция с RAG backends, mTLS)
- API endpoints:
- `POST /api/v1/auth/login` - авторизация
- `GET/PUT /api/v1/settings` - настройки пользователя
- `POST /api/v1/query/bench` - batch запросы
- `POST /api/v1/query/backend` - последовательные запросы
- `POST/GET/DELETE /api/v1/analysis/sessions` - сессии анализа
- Docker setup (Dockerfile, docker-compose.yml)
- Документация (README.md, DB_API_CONTRACT.md, CLAUDE.md)
### ❌ Требуется доделать
- Frontend файлы (перенос из rag-bench-old-version)
- API client для frontend
- Интеграция frontend с новым API
- Тестирование
- Production deployment конфигурация
---
## Этап 1: Подготовка Frontend
### 1.1 Перенос статических файлов из rag-bench-old-version
**Старая архитектура:**
- Статический SPA напрямую делал fetch запросы к RAG backends
- Настройки хранились в localStorage (отдельно для каждого окружения)
- Встроенная поддержка 3 окружений (IFT, PSI, PROD)
- Два режима: Bench (batch) и Backend (sequential)
- Аннотации, экспорт/импорт, детальный UI для анализа ответов
**Файлы для переноса:**
```bash
# Скопировать из rag-bench-old-version/ в static/
static/
├── index.html # Основная страница (24KB, 495 строк)
├── styles.css # Material Design стили (23KB)
├── app.js # Основная логика (61KB, ~1500+ строк)
├── settings.js # Дефолтные настройки (3KB)
└── api-client.js # НОВЫЙ ФАЙЛ - будет создан
```
**Ключевые изменения в архитектуре:**
1. **Было**: `Browser → RAG Backend` (прямой fetch)
2. **Стало**: `Browser → FastAPI → RAG Backend`
3. **Было**: Настройки в localStorage
4. **Стало**: Настройки в DB API (персональные для каждого пользователя)
5. **Добавлено**: JWT авторизация с 8-значным логином
### 1.2 Создать API client для frontend
**Файл:** `static/api-client.js`
**Полная реализация:**
```javascript
/**
* Brief Bench API Client
* Взаимодействие с FastAPI backend
*/
class BriefBenchAPI {
constructor() {
this.baseURL = '/api/v1'
}
// ============================================
// Internal Helpers
// ============================================
_getToken() {
return localStorage.getItem('access_token')
}
_setToken(token) {
localStorage.setItem('access_token', token)
}
_clearToken() {
localStorage.removeItem('access_token')
}
_getHeaders(includeAuth = true) {
const headers = {
'Content-Type': 'application/json'
}
if (includeAuth) {
const token = this._getToken()
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
}
return headers
}
async _handleResponse(response) {
// Handle 401 Unauthorized
if (response.status === 401) {
this._clearToken()
throw new Error('Сессия истекла. Пожалуйста, войдите снова.')
}
// Handle 502 Bad Gateway (RAG backend error)
if (response.status === 502) {
throw new Error('RAG backend недоступен или вернул ошибку')
}
// Handle other errors
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(errorData.detail || `HTTP ${response.status}: ${response.statusText}`)
}
// Handle 204 No Content
if (response.status === 204) {
return null
}
return await response.json()
}
async _request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`
try {
const response = await fetch(url, options)
return await this._handleResponse(response)
} catch (error) {
console.error(`API request failed: ${endpoint}`, error)
throw error
}
}
// ============================================
// Auth API
// ============================================
/**
* Авторизация с 8-значным логином
* @param {string} login - 8-значный логин
* @returns {Promise<{access_token: string, user: object}>}
*/
async login(login) {
const response = await this._request(`/auth/login?login=${login}`, {
method: 'POST',
headers: this._getHeaders(false)
})
// Сохранить токен
this._setToken(response.access_token)
return response
}
/**
* Выход (очистка токена)
*/
logout() {
this._clearToken()
window.location.reload()
}
/**
* Проверка авторизации
* @returns {boolean}
*/
isAuthenticated() {
return !!this._getToken()
}
// ============================================
// Settings API
// ============================================
/**
* Получить настройки пользователя для всех окружений
* @returns {Promise<{user_id: string, settings: object, updated_at: string}>}
*/
async getSettings() {
return await this._request('/settings', {
method: 'GET',
headers: this._getHeaders()
})
}
/**
* Обновить настройки пользователя
* @param {object} settings - Объект с настройками для окружений
* @returns {Promise<{user_id: string, settings: object, updated_at: string}>}
*/
async updateSettings(settings) {
return await this._request('/settings', {
method: 'PUT',
headers: this._getHeaders(),
body: JSON.stringify({ settings })
})
}
// ============================================
// Query API
// ============================================
/**
* Отправить batch запрос (Bench mode)
* @param {string} environment - Окружение (ift/psi/prod)
* @param {Array<{body: string, with_docs: boolean}>} questions - Массив вопросов
* @returns {Promise<{request_id: string, timestamp: string, environment: string, response: object}>}
*/
async benchQuery(environment, questions) {
return await this._request('/query/bench', {
method: 'POST',
headers: this._getHeaders(),
body: JSON.stringify({
environment,
questions
})
})
}
/**
* Отправить последовательные запросы (Backend mode)
* @param {string} environment - Окружение (ift/psi/prod)
* @param {Array<{body: string, with_docs: boolean}>} questions - Массив вопросов
* @param {boolean} resetSession - Сбрасывать ли сессию после каждого вопроса
* @returns {Promise<{request_id: string, timestamp: string, environment: string, response: object}>}
*/
async backendQuery(environment, questions, resetSession = true) {
return await this._request('/query/backend', {
method: 'POST',
headers: this._getHeaders(),
body: JSON.stringify({
environment,
questions,
reset_session: resetSession
})
})
}
// ============================================
// Analysis Sessions API
// ============================================
/**
* Сохранить сессию анализа
* @param {object} sessionData - Данные сессии
* @returns {Promise<object>}
*/
async saveSession(sessionData) {
return await this._request('/analysis/sessions', {
method: 'POST',
headers: this._getHeaders(),
body: JSON.stringify(sessionData)
})
}
/**
* Получить список сессий
* @param {string|null} environment - Фильтр по окружению (опционально)
* @param {number} limit - Лимит результатов
* @param {number} offset - Смещение для пагинации
* @returns {Promise<{sessions: Array, total: number}>}
*/
async getSessions(environment = null, limit = 50, offset = 0) {
const params = new URLSearchParams({ limit, offset })
if (environment) {
params.append('environment', environment)
}
return await this._request(`/analysis/sessions?${params}`, {
method: 'GET',
headers: this._getHeaders()
})
}
/**
* Получить конкретную сессию
* @param {string} sessionId - ID сессии
* @returns {Promise<object>}
*/
async getSession(sessionId) {
return await this._request(`/analysis/sessions/${sessionId}`, {
method: 'GET',
headers: this._getHeaders()
})
}
/**
* Удалить сессию
* @param {string} sessionId - ID сессии
* @returns {Promise<null>}
*/
async deleteSession(sessionId) {
return await this._request(`/analysis/sessions/${sessionId}`, {
method: 'DELETE',
headers: this._getHeaders()
})
}
}
// Export API client instance
const api = new BriefBenchAPI()
```
**Особенности реализации:**
- Автоматическое добавление `Authorization: Bearer {token}` из localStorage
- Обработка всех HTTP ошибок (401 → logout + reload, 502 → RAG error)
- Singleton instance (`const api = new BriefBenchAPI()`)
- Методы соответствуют FastAPI endpoints один-к-одному
---
## Этап 2: Адаптация Frontend под новую архитектуру
### 2.1 Добавить Login Screen
**Изменения в `index.html`:**
```html
<!-- Login screen (показывается если нет токена) -->
<div id="login-screen">
<h1>Brief Bench</h1>
<input type="text" id="login-input" placeholder="8-значный логин" maxlength="8">
<button id="login-btn">Войти</button>
<div id="login-error"></div>
</div>
<!-- Main app (показывается после авторизации) -->
<div id="app" style="display: none;">
<!-- Существующий интерфейс -->
</div>
```
**Логика в `app.js`:**
- При загрузке проверить наличие токена в localStorage
- Если нет → показать login screen
- Если есть → валидировать токен (попробовать загрузить настройки)
- Если токен невалидный → показать login screen
### 2.2 Переписать вызовы API
**Старый код (прямые fetch):**
```javascript
// Было
const response = await fetch('https://rag-backend/api/bench', {
method: 'POST',
headers: { ... },
body: JSON.stringify(questions)
})
```
**Новый код (через API client):**
```javascript
// Стало
const api = new BriefBenchAPI()
const response = await api.benchQuery('ift', questions)
```
**Что нужно переписать:**
- Загрузка настроек пользователя (при старте приложения)
- Отправка bench/backend запросов
- Сохранение сессий анализа
- Загрузка истории сессий
- Экспорт данных
### 2.3 Управление Settings через UI
**Новая логика:**
1. При загрузке приложения: `GET /api/v1/settings` → загрузить настройки для всех 3 окружений
2. Отобразить в UI (вкладки для IFT/PSI/PROD)
3. При изменении: `PUT /api/v1/settings` → сохранить в DB API
**Поля настроек (для каждого окружения):**
- API Mode: bench / backend (radio buttons)
- Bearer Token (input, password type)
- System Platform (input)
- System Platform User (input)
- Platform User ID (input)
- Platform ID (input)
- With Classify (checkbox, только для backend mode)
- Reset Session Mode (checkbox, только для backend mode)
### 2.4 Интеграция History (сессии анализа)
**Функционал:**
1. После выполнения анализа → кнопка "Сохранить сессию"
2. При сохранении: `POST /api/v1/analysis/sessions`
- Отправить environment, api_mode, request, response, annotations
3. Вкладка "История":
- Загрузить список: `GET /api/v1/analysis/sessions?environment=ift`
- Отобразить в виде таблицы (дата, окружение, режим, кол-во вопросов)
- При клике → загрузить полную сессию: `GET /api/v1/analysis/sessions/{id}`
- Кнопка удаления: `DELETE /api/v1/analysis/sessions/{id}`
---
## Этап 3: Environment Selector
### 3.1 UI для выбора окружения
**Добавить в `index.html`:**
```html
<div id="environment-selector">
<label>Окружение:</label>
<select id="env-select">
<option value="ift">ИФТ</option>
<option value="psi">ПСИ</option>
<option value="prod">ПРОД</option>
</select>
</div>
```
**Логика:**
- При выборе окружения → загрузить настройки для этого окружения
- Отобразить текущий apiMode (bench/backend)
- При отправке запроса → использовать выбранное окружение
### 3.2 Валидация перед отправкой
**Проверки:**
1. Выбрано окружение
2. Настройки для окружения существуют
3. apiMode соответствует выбранному режиму (bench/backend)
4. Если требуется bearerToken → он заполнен
**Показывать ошибки:**
- "Настройки для окружения ИФТ не найдены. Перейдите в Settings."
- "Окружение ПСИ настроено на Backend mode, но вы выбрали Bench режим."
---
## Этап 4: Тестирование
### 4.1 Ручное тестирование
**Сценарии:**
1. **Авторизация:**
- Успешный вход (8 цифр)
- Ошибка (невалидный логин)
- Истечение токена (через 30 дней или вручную испортить токен)
2. **Настройки:**
- Загрузка настроек для всех окружений
- Изменение настроек для одного окружения
- Сохранение → перезагрузка страницы → настройки сохранились
3. **Bench Query:**
- Выбрать IFT, bench mode
- Отправить несколько вопросов
- Проверить что ответ отображается корректно
- Проверить headers в Network tab (Request-Id, System-Id, Bearer token)
4. **Backend Query:**
- Выбрать PSI, backend mode
- Отправить вопросы последовательно
- Проверить что между вопросами происходит reset session
5. **Сессии анализа:**
- Сохранить сессию после анализа
- Открыть историю → найти сессию
- Загрузить сессию → проверить что данные корректны
- Удалить сессию
6. **Ошибки:**
- DB API недоступен → показать ошибку
- RAG backend недоступен → показать ошибку 502
- Невалидный токен → редирект на login
### 4.2 Автоматическое тестирование (опционально)
**Backend tests (pytest):**
```bash
tests/
├── test_auth.py # Тесты авторизации
├── test_settings.py # Тесты settings endpoints
├── test_query.py # Тесты query endpoints
├── test_analysis.py # Тесты analysis endpoints
└── conftest.py # Fixtures (mock DB API, mock RAG)
```
**Установка:**
```bash
pip install pytest pytest-asyncio httpx
```
**Запуск:**
```bash
pytest tests/ -v
```
---
## Этап 5: Production Deployment
### 5.1 Настройка .env для production
**Критичные изменения:**
```bash
# Application
DEBUG=false
# JWT (сгенерировать новый секретный ключ!)
JWT_SECRET_KEY=<сгенерированный-секрет-256-бит>
# DB API (production URL)
DB_API_URL=https://db-api.production.com/api/v1
# RAG Backends (production hosts)
IFT_RAG_HOST=ift-rag.production.com
PSI_RAG_HOST=psi-rag.production.com
PROD_RAG_HOST=prod-rag.production.com
# mTLS сертификаты (положить в certs/)
IFT_RAG_CERT_CA=/app/certs/ift/ca.crt
IFT_RAG_CERT_KEY=/app/certs/ift/client.key
IFT_RAG_CERT_CERT=/app/certs/ift/client.crt
# (аналогично для PSI и PROD)
```
### 5.2 CORS configuration
**Изменить `app/main.py`:**
```python
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://brief-bench.production.com", # Production domain
"http://localhost:8000" # Для локальной разработки
],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)
```
### 5.3 Размещение mTLS сертификатов
```bash
certs/
├── ift/
│ ├── ca.crt
│ ├── client.key
│ ├── client.crt
├── psi/
│ ├── ca.crt
│ ├── client.key
│ ├── client.crt
└── prod/
├── ca.crt
├── client.key
└── client.crt
```
**Важно:** Убедиться что права доступа `600` или `400` (только чтение владельцем)
### 5.4 Docker deployment
**Запуск:**
```bash
# Создать .env
cp .env.example .env
nano .env # Заполнить production значения
# Разместить сертификаты
mkdir -p certs/ift certs/psi certs/prod
# Скопировать сертификаты в соответствующие папки
# Запустить
docker-compose up -d
# Проверить логи
docker-compose logs -f fastapi
# Проверить здоровье
curl http://localhost:8000/health
```
### 5.5 Logging и Monitoring
**Добавить middleware для логирования (опционально):**
```python
# app/middleware/logging_middleware.py
import logging
import time
from fastapi import Request
logger = logging.getLogger(__name__)
async def log_requests(request: Request, call_next):
start_time = time.time()
logger.info(f"Request: {request.method} {request.url}")
response = await call_next(request)
process_time = time.time() - start_time
logger.info(
f"Response: {response.status_code} "
f"({process_time:.2f}s)"
)
return response
```
**Зарегистрировать в `app/main.py`:**
```python
from app.middleware.logging_middleware import log_requests
app.middleware("http")(log_requests)
```
---
## Этап 6: Дополнительные улучшения (опционально)
### 6.1 Rate Limiting
- Ограничить количество запросов от одного пользователя
- Использовать `slowapi` или redis-based rate limiter
### 6.2 WebSocket для real-time updates
- Показывать прогресс для backend mode (вопрос N из M)
- Использовать FastAPI WebSocket
### 6.3 Export функционал
- CSV экспорт сессий анализа
- JSON экспорт для интеграции с другими системами
### 6.4 Advanced Analytics
- Dashboard с метриками (кол-во запросов, успешность, время ответа)
- Графики по окружениям
---
## Приоритезация задач
### 🔴 Критично (сделать в первую очередь)
1. Перенос статических файлов из rag-bench-old-version → `static/`
2. Создание `api-client.js`
3. Добавление login screen в `index.html`
4. Переписывание вызовов API в `app.js`
5. Тестирование auth flow
### 🟡 Важно (сделать после критичного)
6. Интеграция Settings UI
7. Environment selector
8. Сохранение и загрузка сессий анализа
9. Ручное тестирование всех сценариев
### 🟢 Желательно (если есть время)
10. Автоматические тесты (pytest)
11. Production deployment настройка
12. Logging middleware
13. Rate limiting
14. Export функционал
---
## Следующие шаги
**Рекомендуемый порядок:**
1. Посмотреть структуру старого проекта `rag-bench-old-version`
2. Скопировать статические файлы в `static/`
3. Создать `api-client.js` с методами для всех endpoints
4. Модифицировать `index.html` (добавить login screen)
5. Переписать `app.js` для работы с новым API
6. Протестировать локально с `uvicorn app.main:app --reload`
7. Исправить найденные проблемы
8. Подготовить production deployment
**Готовы начать с первого этапа?**

27
Dockerfile Normal file
View File

@ -0,0 +1,27 @@
# Multi-stage build for Brief Bench FastAPI
FROM python:3.11-slim as base
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY app /app/app
COPY static /app/static
# Expose port
EXPOSE 8000
# Run uvicorn
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

677
MIGRATION_GUIDE.md Normal file
View File

@ -0,0 +1,677 @@
# Руководство по миграции с rag-bench-old-version на FastAPI
Этот документ содержит детальные инструкции по переносу статического SPA на новую архитектуру с FastAPI backend.
---
## Основные изменения в архитектуре
### Старая версия (rag-bench-old-version)
```
Browser (index.html + app.js)
↓ fetch (HTTPS + mTLS)
RAG Backend (IFT/PSI/PROD)
Настройки: localStorage
Авторизация: нет
```
### Новая версия (brief-bench-fastapi)
```
Browser (index.html + app.js + api-client.js)
↓ fetch (HTTP, JWT token)
FastAPI Backend
↓ httpx (HTTPS + mTLS)
RAG Backend (IFT/PSI/PROD)
Настройки: DB API (per-user)
Авторизация: JWT (8-значный логин)
```
---
## Шаг 1: Копирование статических файлов
```bash
# Из корня проекта brief-bench-fastapi
cp rag-bench-old-version/index.html static/
cp rag-bench-old-version/styles.css static/
cp rag-bench-old-version/app.js static/
cp rag-bench-old-version/settings.js static/
```
---
## Шаг 2: Создание api-client.js
См. полную реализацию в [DEVELOPMENT_PLAN.md](DEVELOPMENT_PLAN.md#12-создать-api-client-для-frontend).
Создать файл `static/api-client.js` с классом `BriefBenchAPI`.
---
## Шаг 3: Модификация index.html
### 3.1 Добавить подключение api-client.js
**Изменить:**
```html
<!-- Scripts -->
<script src="settings.js"></script>
<script src="app.js"></script>
```
**На:**
```html
<!-- Scripts -->
<script src="settings.js"></script>
<script src="api-client.js"></script>
<script src="app.js"></script>
```
### 3.2 Добавить Login Screen
**Добавить ПЕРЕД `<div id="app">`:**
```html
<!-- Login Screen -->
<div id="login-screen" class="login-container" style="display: none;">
<div class="login-card">
<h2 style="text-align: center; margin-bottom: var(--md-spacing-xl);">Brief Bench</h2>
<div class="form-group">
<label class="form-label" for="login-input">8-значный логин</label>
<input
type="text"
class="form-input"
id="login-input"
placeholder="12345678"
maxlength="8"
pattern="[0-9]{8}"
autocomplete="off"
>
<div class="form-helper-text" id="login-error" style="color: var(--md-error); display: none;"></div>
</div>
<button class="btn btn-filled" id="login-submit-btn" style="width: 100%;">
Войти
</button>
</div>
</div>
```
### 3.3 Добавить стили для Login Screen
**Добавить в конец `styles.css`:**
```css
/* Login Screen */
.login-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.login-card {
background: white;
padding: var(--md-spacing-xxl);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
max-width: 400px;
width: 90%;
}
.login-card h2 {
color: #667eea;
font-weight: 500;
}
.login-card .form-input:focus {
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
.login-card .btn-filled {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
margin-top: var(--md-spacing-md);
}
.login-card .btn-filled:hover {
opacity: 0.9;
}
```
### 3.4 Удалить настройки сертификатов из UI
Сертификаты теперь настраиваются в `.env` на сервере, поэтому пользователь не должен их редактировать.
**Удалить из Settings Dialog:**
```html
<h6 class="mt-lg mb-md">Сертификаты (для прокси)</h6>
<div class="form-helper-text mb-md">...</div>
<div class="form-group">
<label class="form-label">CA Certificate Path</label>
...
</div>
<!-- И все остальные поля сертификатов -->
```
### 3.5 Убрать поля host/port/endpoint из Settings Dialog
Эти поля теперь тоже настраиваются на сервере. Пользователь редактирует только:
- API Mode (bench/backend)
- Bearer Token
- System Platform / System Platform User
- Platform User ID / Platform ID
- With Classify / Reset Session Mode
**Удалить:**
```html
<div class="form-group">
<label class="form-label">Полный URL (переопределяет host/port/endpoint)</label>
<input type="text" class="form-input" id="setting-full-url" ...>
</div>
<div class="form-group">
<label class="form-label">Host</label>
<input type="text" class="form-input" id="setting-host" ...>
</div>
<div class="form-group">
<label class="form-label">Port</label>
<input type="text" class="form-input" id="setting-port" ...>
</div>
<div class="form-group">
<label class="form-label">Endpoint</label>
<input type="text" class="form-input" id="setting-endpoint" ...>
</div>
<div class="form-group">
<label class="form-label">System ID</label>
<input type="text" class="form-input" id="setting-system-id" ...>
</div>
<div class="form-group">
<label class="form-label">Request ID Template</label>
<input type="text" class="form-input" id="setting-request-id" ...>
</div>
```
### 3.6 Добавить кнопку Logout
**Добавить в App Bar Actions:**
```html
<button class="btn-icon" id="logout-btn" title="Выход">
<span class="material-icons">logout</span>
</button>
```
---
## Шаг 4: Модификация app.js
### 4.1 Удалить старые API функции
**Удалить функции:**
- `buildApiUrl()`
- `sendQuery()`
- `buildBackendApiUrl()`
- `buildBackendHeaders()`
- `sendBackendAsk()`
- `sendBackendReset()`
- `sendBackendSequential()`
### 4.2 Добавить Login Flow
**Добавить в начало файла (после объявления AppState):**
```javascript
// ============================================
// Authentication
// ============================================
/**
* Проверить авторизацию при загрузке страницы
*/
async function checkAuth() {
if (!api.isAuthenticated()) {
showLoginScreen()
return false
}
// Попробовать загрузить настройки (валидация токена)
try {
await loadSettingsFromServer()
return true
} catch (error) {
console.error('Token validation failed:', error)
showLoginScreen()
return false
}
}
/**
* Показать экран авторизации
*/
function showLoginScreen() {
document.getElementById('login-screen').style.display = 'flex'
document.getElementById('app').style.display = 'none'
}
/**
* Скрыть экран авторизации и показать приложение
*/
function hideLoginScreen() {
document.getElementById('login-screen').style.display = 'none'
document.getElementById('app').style.display = 'block'
}
/**
* Обработка авторизации
*/
async function handleLogin() {
const loginInput = document.getElementById('login-input')
const loginError = document.getElementById('login-error')
const loginBtn = document.getElementById('login-submit-btn')
const login = loginInput.value.trim()
// Валидация
if (!/^[0-9]{8}$/.test(login)) {
loginError.textContent = 'Логин должен состоять из 8 цифр'
loginError.style.display = 'block'
return
}
loginError.style.display = 'none'
loginBtn.disabled = true
loginBtn.textContent = 'Вход...'
try {
const response = await api.login(login)
console.log('Login successful:', response.user)
// Загрузить настройки с сервера
await loadSettingsFromServer()
// Скрыть login screen, показать приложение
hideLoginScreen()
loginInput.value = ''
} catch (error) {
console.error('Login failed:', error)
loginError.textContent = error.message || 'Ошибка авторизации'
loginError.style.display = 'block'
} finally {
loginBtn.disabled = false
loginBtn.textContent = 'Войти'
}
}
/**
* Выход из системы
*/
function handleLogout() {
if (confirm('Вы уверены, что хотите выйти?')) {
api.logout()
}
}
```
### 4.3 Изменить управление настройками
**Заменить функции:**
```javascript
/**
* Загрузить настройки с сервера (DB API)
*/
async function loadSettingsFromServer() {
try {
const response = await api.getSettings()
// Преобразовать в формат AppState.settings
AppState.settings = {
activeEnvironment: AppState.currentEnvironment,
environments: {
ift: {
name: 'ИФТ',
...response.settings.ift
},
psi: {
name: 'ПСИ',
...response.settings.psi
},
prod: {
name: 'ПРОМ',
...response.settings.prod
}
},
requestTimeout: 1800000, // 30 минут (фиксировано)
}
console.log('Settings loaded from server:', AppState.settings)
} catch (error) {
console.error('Failed to load settings from server:', error)
throw error
}
}
/**
* Сохранить настройки на сервер (DB API)
*/
async function saveSettingsToServer(settings) {
try {
// Извлечь только поля, которые сервер ожидает
const settingsToSave = {
ift: extractEnvironmentSettings(settings.environments.ift),
psi: extractEnvironmentSettings(settings.environments.psi),
prod: extractEnvironmentSettings(settings.environments.prod)
}
await api.updateSettings(settingsToSave)
AppState.settings = settings
console.log('Settings saved to server')
} catch (error) {
console.error('Failed to save settings to server:', error)
throw error
}
}
/**
* Извлечь только нужные поля для сервера
*/
function extractEnvironmentSettings(envSettings) {
return {
apiMode: envSettings.apiMode,
bearerToken: envSettings.bearerToken || '',
systemPlatform: envSettings.systemPlatform || '',
systemPlatformUser: envSettings.systemPlatformUser || '',
platformUserId: envSettings.platformUserId || '',
platformId: envSettings.platformId || '',
withClassify: envSettings.withClassify || false,
resetSessionMode: envSettings.resetSessionMode !== false
}
}
// УДАЛИТЬ старые функции loadSettings() и saveSettings()
```
### 4.4 Заменить вызовы API для Query
**Было:**
```javascript
// Старый код
async function handleSendQuery() {
// ...
const response = await sendQuery(requestBody) // Прямой fetch к RAG
// ...
}
```
**Стало:**
```javascript
async function handleSendQuery() {
// Получить текущие настройки окружения
const envSettings = getCurrentEnvSettings()
const env = AppState.currentEnvironment
// Проверить режим API
if (envSettings.apiMode === 'bench') {
// Batch mode
const response = await api.benchQuery(env, requestBody)
processResponse(response.response)
} else if (envSettings.apiMode === 'backend') {
// Sequential mode
const resetSession = envSettings.resetSessionMode !== false
const response = await api.backendQuery(env, requestBody, resetSession)
processResponse(response.response)
}
}
```
### 4.5 Изменить Save Settings в Settings Dialog
**Заменить обработчик:**
```javascript
// Было
document.getElementById('save-settings-btn').addEventListener('click', () => {
const updatedSettings = readSettingsFromDialog()
saveSettings(updatedSettings) // localStorage
showToast('Настройки сохранены')
closeSettingsDialog()
})
// Стало
document.getElementById('save-settings-btn').addEventListener('click', async () => {
const saveBtn = document.getElementById('save-settings-btn')
saveBtn.disabled = true
saveBtn.textContent = 'Сохранение...'
try {
const updatedSettings = readSettingsFromDialog()
await saveSettingsToServer(updatedSettings)
showToast('Настройки сохранены на сервере')
closeSettingsDialog()
} catch (error) {
console.error('Failed to save settings:', error)
showToast(`Ошибка сохранения: ${error.message}`, 'error')
} finally {
saveBtn.disabled = false
saveBtn.textContent = 'Сохранить'
}
})
```
### 4.6 Обновить populateSettingsDialog()
**Удалить строки для полей, которые больше не редактируются:**
```javascript
function populateSettingsDialog() {
const env = AppState.currentEnvironment
const envSettings = AppState.settings.environments[env]
// Set environment selector
document.getElementById('settings-env-selector').value = env
// API Mode
const apiMode = envSettings.apiMode || 'bench'
document.getElementById('setting-api-mode').value = apiMode
toggleBackendSettings(apiMode === 'backend')
// Populate environment-specific fields (только те что остались!)
document.getElementById('setting-bearer-token').value = envSettings.bearerToken || ''
document.getElementById('setting-system-platform').value = envSettings.systemPlatform || ''
document.getElementById('setting-system-platform-user').value = envSettings.systemPlatformUser || ''
// Backend mode fields
document.getElementById('setting-platform-user-id').value = envSettings.platformUserId || ''
document.getElementById('setting-platform-id').value = envSettings.platformId || ''
document.getElementById('setting-with-classify').checked = envSettings.withClassify || false
document.getElementById('setting-reset-session-mode').checked = envSettings.resetSessionMode !== false
// Убрать global timeout (теперь не редактируется)
}
```
### 4.7 Обновить readSettingsFromDialog()
**Изменить:**
```javascript
function readSettingsFromDialog() {
const env = document.getElementById('settings-env-selector').value
// Update environment-specific settings
const updatedSettings = JSON.parse(JSON.stringify(AppState.settings)) // Deep copy
updatedSettings.environments[env] = {
name: updatedSettings.environments[env].name,
apiMode: document.getElementById('setting-api-mode').value,
bearerToken: document.getElementById('setting-bearer-token').value.trim(),
systemPlatform: document.getElementById('setting-system-platform').value.trim(),
systemPlatformUser: document.getElementById('setting-system-platform-user').value.trim(),
platformUserId: document.getElementById('setting-platform-user-id').value.trim(),
platformId: document.getElementById('setting-platform-id').value.trim(),
withClassify: document.getElementById('setting-with-classify').checked,
resetSessionMode: document.getElementById('setting-reset-session-mode').checked,
}
return updatedSettings
}
```
### 4.8 Добавить event listeners
**Добавить в секцию Event Listeners:**
```javascript
// Login
document.getElementById('login-submit-btn').addEventListener('click', handleLogin)
document.getElementById('login-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
handleLogin()
}
})
// Logout
document.getElementById('logout-btn').addEventListener('click', handleLogout)
```
### 4.9 Изменить инициализацию приложения
**Было:**
```javascript
// Initialize app on load
document.addEventListener('DOMContentLoaded', () => {
AppState.settings = loadSettings()
// ...
initApp()
})
```
**Стало:**
```javascript
// Initialize app on load
document.addEventListener('DOMContentLoaded', async () => {
// Проверить авторизацию
const isAuthenticated = await checkAuth()
if (isAuthenticated) {
// Пользователь авторизован, инициализировать приложение
initApp()
}
})
```
---
## Шаг 5: Изменения в settings.js
**Упростить defaultSettings:**
Удалить поля, которые теперь настраиваются на сервере (host, port, endpoint, certPaths, systemId, requestIdTemplate).
Оставить только дефолты для тех полей, которые пользователь может редактировать:
```javascript
const defaultSettings = {
activeEnvironment: 'ift',
environments: {
ift: {
name: 'ИФТ',
apiMode: 'bench',
bearerToken: '',
systemPlatform: '',
systemPlatformUser: '',
platformUserId: '',
platformId: '',
withClassify: false,
resetSessionMode: true,
},
psi: {
name: 'ПСИ',
apiMode: 'bench',
bearerToken: '',
systemPlatform: '',
systemPlatformUser: '',
platformUserId: '',
platformId: '',
withClassify: false,
resetSessionMode: true,
},
prod: {
name: 'ПРОМ',
apiMode: 'bench',
bearerToken: '',
systemPlatform: '',
systemPlatformUser: '',
platformUserId: '',
platformId: '',
withClassify: false,
resetSessionMode: true,
}
},
requestTimeout: 1800000, // 30 minutes (не редактируется)
}
```
---
## Шаг 6: Раскомментировать static files в main.py
**В `app/main.py`:**
```python
# Serve static files (frontend)
app.mount("/static", StaticFiles(directory="static"), name="static")
```
Раскомментировать эту строку.
---
## Шаг 7: Тестирование
### 7.1 Запустить FastAPI локально
```bash
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
```
### 7.2 Открыть в браузере
```
http://localhost:8000/static/index.html
```
### 7.3 Тестовые сценарии
1. Login screen должен показаться
2. Ввести 8-значный логин → успешный вход
3. Настройки должны загрузиться с сервера
4. Изменить настройки → сохранить → перезагрузить → настройки сохранились
5. Отправить bench query → получить ответ
6. Отправить backend query → получить ответ
7. Нажать Logout → вернуться на login screen
---
## Ключевые отличия для разработчика
| Аспект | Старая версия | Новая версия |
|--------|--------------|--------------|
| Авторизация | Нет | JWT (8 цифр) |
| Настройки | localStorage | DB API (per-user) |
| API вызовы | fetch → RAG напрямую | fetch → FastAPI → RAG |
| mTLS | В браузере (невозможно) | На FastAPI сервере |
| Endpoints | RAG endpoints | FastAPI endpoints |
| Пользователи | Один (все настройки общие) | Множество (настройки персональные) |
---
## Что НЕ нужно менять
- Весь UI (остаётся как есть)
- Логика отображения ответов
- Аннотации
- Экспорт/импорт анализа (работает через сохранение сессий на сервер)
- Material Design стили

446
PROJECT_STATUS.md Normal file
View File

@ -0,0 +1,446 @@
# Brief Bench FastAPI - Текущий статус проекта
**Дата:** 17 декабря 2025
**Статус:** Базовая структура реализована, готова к продолжению
---
## 📋 Что реализовано
### 1. Структура проекта
```
brief-bench-fastapi/
├── app/
│ ├── api/
│ │ └── v1/
│ │ ├── __init__.py
│ │ └── auth.py ✅ POST /api/v1/auth/login
│ ├── models/
│ │ ├── __init__.py
│ │ ├── auth.py ✅ LoginRequest, LoginResponse, UserResponse
│ │ ├── settings.py ✅ EnvironmentSettings, UserSettings
│ │ ├── analysis.py ✅ SessionCreate, SessionResponse, SessionList
│ │ └── query.py ✅ BenchQueryRequest, BackendQueryRequest
│ ├── services/
│ │ ├── __init__.py
│ │ └── auth_service.py ✅ AuthService (login logic)
│ ├── interfaces/
│ │ ├── __init__.py
│ │ ├── base.py ⚠️ TgBackendInterface (ЗАГЛУШКА - нужна реализация)
│ │ └── db_api_client.py ✅ DBApiClient (методы для DB API)
│ ├── middleware/
│ │ └── __init__.py
│ ├── utils/
│ │ ├── __init__.py
│ │ └── security.py ✅ JWT encode/decode
│ ├── __init__.py
│ ├── config.py ✅ Settings из .env
│ ├── dependencies.py ✅ DI: get_db_client, get_current_user
│ └── main.py ✅ FastAPI app с CORS
├── static/ ❌ Пусто (нужно скопировать из rag-bench)
├── tests/ ❌ Пусто
├── certs/ ❌ Не создана (для mTLS)
├── .env.example ✅
├── .gitignore ✅
├── requirements.txt ✅
├── Dockerfile ✅
├── docker-compose.yml ✅
├── DB_API_CONTRACT.md ✅ Полный контракт для DB API
├── README.md ✅
└── PROJECT_STATUS.md ✅ Этот файл
```
---
## ✅ Реализованные компоненты
### 1. Configuration (app/config.py)
- Загрузка из .env через pydantic-settings
- Настройки для 3 окружений RAG (IFT, PSI, PROD)
- JWT конфигурация
- DB API URL
### 2. Pydantic Models (app/models/)
**auth.py:**
- `LoginRequest` - login (8 цифр) + client_ip
- `UserResponse` - user_id, login, last_login_at, created_at
- `LoginResponse` - access_token + user
- `TokenPayload` - user_id, login, exp
**settings.py:**
- `EnvironmentSettings` - настройки для одного окружения (apiMode, bearerToken, platformUserId, etc.)
- `UserSettings` - настройки пользователя для всех 3 окружений
- `UserSettingsUpdate` - обновление настроек
**analysis.py:**
- `SessionCreate` - создание сессии анализа
- `SessionResponse` - полная сессия
- `SessionListItem` - элемент списка сессий
- `SessionList` - список с total
**query.py:**
- `QuestionRequest` - body + with_docs
- `BenchQueryRequest` - environment + questions[]
- `BackendQueryRequest` - environment + questions[] + reset_session
- `QueryResponse` - request_id, timestamp, environment, response
### 3. Interfaces (app/interfaces/)
**base.py (⚠️ ЗАГЛУШКА!):**
```python
class TgBackendInterface:
def __init__(self, api_prefix: str, **kwargs)
async def get(path, params, response_model)
async def post(path, body, response_model)
async def put(path, body, response_model)
async def delete(path)
```
**Требует реализации пользователем!** Должна включать:
- httpx.AsyncClient для HTTP запросов
- Автоматическую сериализацию/десериализацию Pydantic моделей
- Error handling и retry logic
- Логирование
**db_api_client.py:**
Наследуется от TgBackendInterface, методы:
- `login_user(LoginRequest) -> UserResponse`
- `get_user_settings(user_id) -> UserSettings`
- `update_user_settings(user_id, settings) -> UserSettings`
- `save_session(user_id, session_data) -> SessionResponse`
- `get_sessions(user_id, environment, limit, offset) -> SessionList`
- `get_session(user_id, session_id) -> SessionResponse`
- `delete_session(user_id, session_id) -> dict`
### 4. Services (app/services/)
**auth_service.py:**
- `AuthService.login(login, client_ip)` - авторизация через DB API + генерация JWT
### 5. Security & Auth (app/utils/security.py)
- `create_access_token(data, expires_delta)` - создание JWT
- `decode_access_token(token)` - валидация JWT
### 6. Dependencies (app/dependencies.py)
- `get_db_client()` - DI для DBApiClient
- `get_current_user(credentials)` - middleware для auth
### 7. API Endpoints (app/api/v1/)
**auth.py:**
- `POST /api/v1/auth/login?login=12345678` - авторизация
- Возвращает JWT token + user info
- Фиксирует IP клиента
### 8. Main App (app/main.py)
- FastAPI app с CORS
- Include router для auth
- Health endpoint: GET /health
- Root endpoint: GET /
### 9. Docker (Dockerfile, docker-compose.yml)
- Multi-stage build
- Порт 8000
- Volumes: certs/ (ro), static/
- Network: brief-bench-network
### 10. Documentation
- **DB_API_CONTRACT.md** - полный контракт для DB API сервиса
- **README.md** - инструкции по запуску и разработке
- **.env.example** - пример конфигурации
---
## ❌ Что НЕ реализовано (TODO)
### 1. TgBackendInterface реализация (КРИТИЧНО!)
Файл: `app/interfaces/base.py`
Нужно реализовать:
```python
class TgBackendInterface:
def __init__(self, api_prefix: str, **kwargs):
self.api_prefix = api_prefix
self.client = httpx.AsyncClient(
timeout=kwargs.get('timeout', 30),
# ... другие настройки
)
async def get(self, path, params=None, response_model=None, **kwargs):
url = f"{self.api_prefix}{path}"
response = await self.client.get(url, params=params)
response.raise_for_status()
data = response.json()
if response_model:
return response_model(**data)
return data
# аналогично post, put, delete
```
### 2. API Endpoints для Settings
Файл: `app/api/v1/settings.py` (создать!)
```python
@router.get("/settings")
async def get_settings(user: dict = Depends(get_current_user)):
# Получить настройки пользователя из DB API
@router.put("/settings")
async def update_settings(settings: UserSettingsUpdate, user: dict = Depends(get_current_user)):
# Обновить настройки через DB API
```
### 3. API Endpoints для Query
Файл: `app/api/v1/query.py` (создать!)
```python
@router.post("/query/bench")
async def bench_query(request: BenchQueryRequest, user: dict = Depends(get_current_user)):
# Отправить batch запрос к RAG backend через RagService
@router.post("/query/backend")
async def backend_query(request: BackendQueryRequest, user: dict = Depends(get_current_user)):
# Отправить вопросы по одному через RagService
```
### 4. API Endpoints для Analysis Sessions
Файл: `app/api/v1/analysis.py` (создать!)
```python
@router.post("/analysis/sessions")
async def create_session(session: SessionCreate, user: dict = Depends(get_current_user)):
# Сохранить сессию через DB API
@router.get("/analysis/sessions")
async def get_sessions(environment: str = None, user: dict = Depends(get_current_user)):
# Получить список сессий
@router.get("/analysis/sessions/{session_id}")
async def get_session(session_id: str, user: dict = Depends(get_current_user)):
# Получить конкретную сессию
@router.delete("/analysis/sessions/{session_id}")
async def delete_session(session_id: str, user: dict = Depends(get_current_user)):
# Удалить сессию
```
### 5. RAG Service
Файл: `app/services/rag_service.py` (создать!)
```python
class RagService:
def __init__(self):
# Создать 3 httpx.AsyncClient для IFT, PSI, PROD
# Настроить mTLS из config
async def send_bench_query(self, environment: str, questions: list, user_settings: dict):
# Отправить batch запрос к RAG backend
# Headers: Request-Id, System-Id, Bearer token (из user_settings)
async def send_backend_query(self, environment: str, questions: list, user_settings: dict):
# Отправить вопросы по одному
# После каждого вопроса: reset session если resetSessionMode=true
```
### 6. Services для Settings и Analysis
Файлы:
- `app/services/settings_service.py` (создать!)
- `app/services/analysis_service.py` (создать!)
### 7. Frontend Integration
- Скопировать `index.html`, `app.js`, `styles.css` из rag-bench в `static/`
- Создать `static/api-client.js` для работы с FastAPI API
- Добавить login screen в index.html
- Обновить app.js для использования api-client.js
### 8. Middleware (опционально)
- `app/middleware/logging.py` - логирование запросов
- `app/middleware/error_handler.py` - глобальная обработка ошибок
### 9. Tests
- Unit tests для services
- Integration tests для API endpoints
---
## 🔑 Важные детали для продолжения
### Архитектура авторизации
1. Пользователь отправляет POST /api/v1/auth/login?login=12345678
2. FastAPI вызывает DB API: POST /users/login
3. DB API возвращает UserResponse (user_id, login, ...)
4. FastAPI генерирует JWT token с {user_id, login, exp}
5. Возвращает {access_token, user}
6. Клиент сохраняет token в localStorage
7. Все последующие запросы: Authorization: Bearer {token}
8. Middleware get_current_user проверяет token
### Настройки пользователя
- **В .env** (не редактируются из UI): хосты RAG, порты, endpoint'ы, пути к сертификатам
- **В БД** (индивидуальны для каждого пользователя): bearerToken, systemPlatform, platformUserId, withClassify, resetSessionMode
- Каждый пользователь имеет свои настройки для 3 окружений (IFT, PSI, PROD)
### RAG Backend Integration
**Bench mode:**
```python
POST https://{IFT_RAG_HOST}:{IFT_RAG_PORT}/{IFT_RAG_ENDPOINT}
Headers:
Content-Type: application/json
Request-Id: {uuid}
System-Id: brief-bench-ift
Authorization: Bearer {user_settings.bearerToken} # если задан
System-Platform: {user_settings.systemPlatform} # если задан
Body: [
{"body": "вопрос", "with_docs": true},
...
]
```
**Backend mode:**
```python
# Для каждого вопроса:
POST https://{host}:{port}/{backendAskEndpoint}
Headers:
Platform-User-Id: {user_settings.platformUserId}
Platform-Id: {user_settings.platformId}
Authorization: Bearer {user_settings.bearerToken} # если задан
Body: {
"question": "вопрос",
"user_message_id": 1,
"user_message_datetime": "2025-12-17T12:00:00Z",
"with_classify": false
}
# Если resetSessionMode=true:
POST https://{host}:{port}/{backendResetEndpoint}
Body: {
"user_message_datetime": "2025-12-17T12:00:00Z"
}
```
### DB API Endpoints (см. DB_API_CONTRACT.md)
- POST /users/login
- GET /users/{user_id}/settings
- PUT /users/{user_id}/settings
- POST /users/{user_id}/sessions
- GET /users/{user_id}/sessions
- GET /users/{user_id}/sessions/{session_id}
- DELETE /users/{user_id}/sessions/{session_id}
---
## 🚀 План дальнейшей работы
### Этап 1: Реализовать TgBackendInterface
**Приоритет:** ВЫСОКИЙ
**Файл:** app/interfaces/base.py
Без этого DB API клиент не будет работать.
### Этап 2: Добавить недостающие API endpoints
**Приоритет:** ВЫСОКИЙ
**Файлы:**
- app/api/v1/settings.py
- app/api/v1/query.py
- app/api/v1/analysis.py
Зарегистрировать в app/main.py:
```python
from app.api.v1 import auth, settings, query, analysis
app.include_router(settings.router, prefix="/api/v1")
app.include_router(query.router, prefix="/api/v1")
app.include_router(analysis.router, prefix="/api/v1")
```
### Этап 3: Реализовать RAG Service
**Приоритет:** ВЫСОКИЙ
**Файл:** app/services/rag_service.py
Нужно для query endpoints.
### Этап 4: Реализовать Settings & Analysis Services
**Приоритет:** СРЕДНИЙ
**Файлы:**
- app/services/settings_service.py
- app/services/analysis_service.py
### Этап 5: Frontend Integration
**Приоритет:** СРЕДНИЙ
- Скопировать static/ файлы из rag-bench
- Создать api-client.js
- Добавить login screen
- Обновить app.js
### Этап 6: Testing & Deployment
**Приоритет:** НИЗКИЙ
- Создать .env из .env.example
- Добавить mTLS сертификаты в certs/
- docker-compose up
- Тестирование
---
## 📦 Dependencies (requirements.txt)
```
fastapi==0.104.1
uvicorn[standard]==0.24.0
python-multipart==0.0.6
httpx==0.25.2
pydantic==2.5.0
pydantic-settings==2.1.0
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
anyio==4.1.0
python-dotenv==1.0.0
fastapi-cors==0.0.6
```
---
## 🔧 Команды для разработки
```bash
# Установить зависимости
pip install -r requirements.txt
# Запустить локально
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
# Docker
docker-compose up -d
docker-compose logs -f fastapi
docker-compose down
# Проверить здоровье
curl http://localhost:8000/health
```
---
## 📝 Примечания
1. **TgBackendInterface** - это ваша реализация, которая будет использоваться во всех клиентах (DBApiClient, возможно RagClient в будущем)
2. **api_prefix** - каждый клиент инициализируется с базовым URL (например, http://db-api:8080/api/v1)
3. **Pydantic схемы** - используются везде для type-safety и валидации
4. **JWT токены** - 30-дневная экспирация, содержат user_id и login
5. **mTLS** - настраивается в .env, пути к сертификатам для каждого окружения
6. **CORS** - сейчас allow_origins=["*"], нужно настроить для production
7. **Ошибки** - возвращаются в формате FastAPI HTTPException с detail
---
## 🎯 Готово к продолжению!
Вся базовая структура создана. Следующий шаг - реализация TgBackendInterface и остальных endpoints.
**Следующий чат:** начните с реализации TgBackendInterface или создания недостающих API endpoints.

181
README.md Normal file
View File

@ -0,0 +1,181 @@
# Brief Bench FastAPI
FastAPI backend для системы тестирования RAG с multi-user поддержкой.
## Возможности
- 🔐 JWT авторизация (8-значный логин)
- 🌐 Multi-environment: ИФТ, ПСИ, ПРОМ
- 📊 Bench mode: batch тестирование
- 🤖 Backend mode: имитация бота (вопросы по одному)
- 💾 Сохранение сессий анализа
- 🔒 mTLS для RAG backend
- 📝 Аннотации и экспорт
## Требования
- Python 3.11+
- Docker & Docker Compose (для деплоя)
- Доступ к DB API сервису
- mTLS сертификаты для RAG backend (опционально)
## Быстрый старт
### 1. Клонировать репозиторий
```bash
cd /path/to/brief-bench-fastapi
```
### 2. Настроить окружение
Скопируйте `.env.example` в `.env` и заполните:
```bash
cp .env.example .env
nano .env
```
**Обязательные переменные:**
- `JWT_SECRET_KEY` - секретный ключ для JWT (сгенерируйте новый!)
- `DB_API_URL` - URL DB API сервиса
- `IFT_RAG_HOST`, `PSI_RAG_HOST`, `PROD_RAG_HOST` - хосты RAG backend
### 3. Разместить сертификаты (если используется mTLS)
```
certs/
ift/
ca.crt
client.key
client.crt
psi/
...
prod/
...
```
### 4. Запустить с Docker Compose
```bash
docker-compose up -d
```
Приложение доступно на `http://localhost:8000`
### 5. Проверить здоровье
```bash
curl http://localhost:8000/health
```
## Разработка
### Локальный запуск (без Docker)
```bash
# Создать виртуальное окружение
python -m venv venv
source venv/bin/activate # Linux/Mac
# или
venv\Scripts\activate # Windows
# Установить зависимости
pip install -r requirements.txt
# Запустить
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
```
### Структура проекта
```
brief-bench-fastapi/
├── app/
│ ├── api/v1/ # API endpoints
│ ├── models/ # Pydantic models
│ ├── services/ # Business logic
│ ├── interfaces/ # API clients (DB, RAG)
│ ├── middleware/ # Middleware
│ ├── utils/ # Utilities (JWT, etc.)
│ ├── config.py # Configuration
│ ├── dependencies.py # DI
│ └── main.py # FastAPI app
├── static/ # Frontend files
├── tests/ # Tests
├── certs/ # mTLS certificates
├── .env # Environment variables
├── requirements.txt # Python dependencies
├── Dockerfile # Docker image
├── docker-compose.yml # Docker Compose
└── DB_API_CONTRACT.md # DB API contract
```
## API Endpoints
### Authentication
- `POST /api/v1/auth/login` - Авторизация (8-значный логин)
### Health
- `GET /health` - Health check
- `GET /` - Root info
## DB API Contract
См. [DB_API_CONTRACT.md](DB_API_CONTRACT.md) для полного описания контракта с DB API сервисом.
## Интерфейсы (Interfaces)
### TgBackendInterface
Базовый класс для всех API клиентов. **Требует реализации пользователем!**
См. `app/interfaces/base.py` для заглушки и требований.
### DBApiClient
Наследуется от `TgBackendInterface`, предоставляет методы для работы с DB API:
- `login_user()`
- `get_user_settings()`, `update_user_settings()`
- `save_session()`, `get_sessions()`, `get_session()`, `delete_session()`
## Deployment
### Docker Compose
```bash
docker-compose up -d
```
### Logs
```bash
docker-compose logs -f fastapi
```
### Остановить
```bash
docker-compose down
```
## Security
- JWT токены с 30-дневной экспирацией
- mTLS сертификаты только на сервере
- Secrets в .env (не коммитить в git!)
- CORS настроен (обновить в production)
## TODO
- [ ] Реализовать `TgBackendInterface` (пользователь)
- [ ] Добавить endpoints для settings, query, analysis
- [ ] Добавить RAG service
- [ ] Добавить frontend интеграцию
- [ ] Добавить тесты
- [ ] Rate limiting
- [ ] Logging middleware
## License
Proprietary

0
app/__init__.py Normal file
View File

0
app/api/__init__.py Normal file
View File

7
app/api/v1/__init__.py Normal file
View File

@ -0,0 +1,7 @@
"""
API v1 routers.
"""
from app.api.v1 import auth, settings, query, analysis
__all__ = ["auth", "settings", "query", "analysis"]

182
app/api/v1/analysis.py Normal file
View File

@ -0,0 +1,182 @@
"""
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.interfaces.db_api_client import DBApiClient
from app.dependencies import get_db_client, get_current_user
import httpx
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/analysis", tags=["analysis"])
@router.post("/sessions", response_model=SessionResponse, status_code=status.HTTP_201_CREATED)
async def create_session(
session: SessionCreate,
current_user: dict = Depends(get_current_user),
db_client: DBApiClient = Depends(get_db_client)
):
"""
Создать новую сессию анализа.
Args:
session: Данные сессии (environment, api_mode, request, response, annotations)
Returns:
SessionResponse: Созданная сессия с session_id
"""
user_id = current_user["user_id"]
try:
created_session = await db_client.save_session(user_id, session)
return created_session
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
elif e.response.status_code == 400:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid session data format"
)
logger.error(f"Failed to create session: {e}")
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail="Failed to create session in DB API"
)
except Exception as e:
logger.error(f"Unexpected error creating session: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal server error"
)
@router.get("/sessions", response_model=SessionList)
async def get_sessions(
environment: Optional[str] = Query(None, description="Фильтр по окружению (ift/psi/prod)"),
limit: int = Query(50, ge=1, le=200, description="Лимит результатов"),
offset: int = Query(0, ge=0, description="Смещение для пагинации"),
current_user: dict = Depends(get_current_user),
db_client: DBApiClient = Depends(get_db_client)
):
"""
Получить список сессий пользователя.
Args:
environment: Фильтр по окружению (опционально)
limit: Лимит результатов (default: 50, max: 200)
offset: Смещение для пагинации (default: 0)
Returns:
SessionList: Список сессий с total count
"""
user_id = current_user["user_id"]
try:
sessions = await db_client.get_sessions(user_id, environment, limit, offset)
return sessions
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
logger.error(f"Failed to get sessions: {e}")
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail="Failed to retrieve sessions from DB API"
)
except Exception as e:
logger.error(f"Unexpected error getting sessions: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal server error"
)
@router.get("/sessions/{session_id}", response_model=SessionResponse)
async def get_session(
session_id: str,
current_user: dict = Depends(get_current_user),
db_client: DBApiClient = Depends(get_db_client)
):
"""
Получить конкретную сессию по ID.
Args:
session_id: UUID сессии
Returns:
SessionResponse: Полная информация о сессии
"""
user_id = current_user["user_id"]
try:
session = await db_client.get_session(user_id, session_id)
return 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"
)
logger.error(f"Failed to get session {session_id}: {e}")
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail="Failed to retrieve session from DB API"
)
except Exception as e:
logger.error(f"Unexpected error getting 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,
current_user: dict = Depends(get_current_user),
db_client: DBApiClient = Depends(get_db_client)
):
"""
Удалить сессию.
Args:
session_id: UUID сессии
Returns:
204 No Content при успехе
"""
user_id = current_user["user_id"]
try:
await db_client.delete_session(user_id, session_id)
return None
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Session not found"
)
logger.error(f"Failed to delete session {session_id}: {e}")
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail="Failed to delete session in DB API"
)
except Exception as e:
logger.error(f"Unexpected error deleting session {session_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal server error"
)

53
app/api/v1/auth.py Normal file
View File

@ -0,0 +1,53 @@
"""Authentication API endpoints."""
from fastapi import APIRouter, Depends, HTTPException, Request, status
from app.dependencies import get_db_client
from app.interfaces.db_api_client import DBApiClient
from app.models.auth import LoginResponse
from app.services.auth_service import AuthService
router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/login", response_model=LoginResponse)
async def login(
request: Request,
login: str,
db_client: DBApiClient = Depends(get_db_client)
):
"""
Authenticate user with 8-digit login.
Args:
request: FastAPI request object (to get client IP)
login: 8-digit login string
db_client: DB API client instance
Returns:
LoginResponse with JWT token and user info
Raises:
HTTPException 400: If login format is invalid
HTTPException 500: If DB API call fails
"""
# Get client IP
client_ip = request.client.host
# Create auth service
auth_service = AuthService(db_client)
try:
# Authenticate user
response = await auth_service.login(login, client_ip)
return response
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Authentication failed: {str(e)}"
)

202
app/api/v1/query.py Normal file
View File

@ -0,0 +1,202 @@
"""
Query API endpoints.
Отправка запросов к RAG backends в двух режимах:
1. Bench mode - batch запросы
2. Backend mode - последовательные запросы
"""
from fastapi import APIRouter, Depends, HTTPException, status
from typing import Dict, Any
from app.models.query import BenchQueryRequest, BackendQueryRequest, QueryResponse
from app.interfaces.db_api_client import DBApiClient
from app.services.rag_service import RagService
from app.dependencies import get_db_client, get_current_user
import httpx
import logging
from datetime import datetime
import uuid
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/query", tags=["query"])
def get_rag_service() -> RagService:
"""Dependency для получения RAG service."""
return RagService()
@router.post("/bench", response_model=QueryResponse)
async def bench_query(
request: BenchQueryRequest,
current_user: dict = Depends(get_current_user),
db_client: DBApiClient = Depends(get_db_client),
rag_service: RagService = Depends(get_rag_service)
):
"""
Отправить batch запрос к RAG backend (bench mode).
Отправляет все вопросы одним запросом.
Args:
request: Запрос с окружением и списком вопросов
Returns:
QueryResponse: Ответ от RAG backend с metadata
"""
user_id = current_user["user_id"]
environment = request.environment.lower()
# Валидация окружения
if environment not in ['ift', 'psi', 'prod']:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid environment. Must be one of: ift, psi, prod"
)
try:
# Получить настройки пользователя из DB API
user_settings_response = await db_client.get_user_settings(user_id)
env_settings = user_settings_response.settings.get(environment)
if not env_settings:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Settings not found for environment: {environment}"
)
# Проверить что apiMode = bench
if env_settings.apiMode != 'bench':
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Environment {environment} is not configured for bench mode"
)
# Сгенерировать request_id
request_id = str(uuid.uuid4())
logger.info(
f"User {user_id} sending bench query to {environment}: "
f"{len(request.questions)} questions, request_id={request_id}"
)
# Отправить запрос к RAG backend
env_settings_dict = env_settings.model_dump()
response_data = await rag_service.send_bench_query(
environment=environment,
questions=request.questions,
user_settings=env_settings_dict,
request_id=request_id
)
# Формируем ответ
return QueryResponse(
request_id=request_id,
timestamp=datetime.utcnow().isoformat() + "Z",
environment=environment,
response=response_data
)
except httpx.HTTPStatusError as e:
logger.error(f"RAG backend error for bench query: {e}")
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=f"RAG backend returned error: {e.response.status_code}"
)
except Exception as e:
logger.error(f"Unexpected error in bench query: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal server error"
)
finally:
await rag_service.close()
@router.post("/backend", response_model=QueryResponse)
async def backend_query(
request: BackendQueryRequest,
current_user: dict = Depends(get_current_user),
db_client: DBApiClient = Depends(get_db_client),
rag_service: RagService = Depends(get_rag_service)
):
"""
Отправить запросы к RAG backend (backend mode).
Отправляет вопросы по одному с возможностью сброса сессии.
Args:
request: Запрос с окружением, вопросами и флагом reset_session
Returns:
QueryResponse: Список ответов от RAG backend с metadata
"""
user_id = current_user["user_id"]
environment = request.environment.lower()
# Валидация окружения
if environment not in ['ift', 'psi', 'prod']:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid environment. Must be one of: ift, psi, prod"
)
try:
# Получить настройки пользователя из DB API
user_settings_response = await db_client.get_user_settings(user_id)
env_settings = user_settings_response.settings.get(environment)
if not env_settings:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Settings not found for environment: {environment}"
)
# Проверить что apiMode = backend
if env_settings.apiMode != 'backend':
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Environment {environment} is not configured for backend mode"
)
# Сгенерировать request_id
request_id = str(uuid.uuid4())
logger.info(
f"User {user_id} sending backend query to {environment}: "
f"{len(request.questions)} questions, request_id={request_id}, "
f"reset_session={request.reset_session}"
)
# Отправить запросы к RAG backend
env_settings_dict = env_settings.model_dump()
response_data = await rag_service.send_backend_query(
environment=environment,
questions=request.questions,
user_settings=env_settings_dict,
reset_session=request.reset_session
)
# Формируем ответ
return QueryResponse(
request_id=request_id,
timestamp=datetime.utcnow().isoformat() + "Z",
environment=environment,
response={"answers": response_data} # Оборачиваем в объект
)
except httpx.HTTPStatusError as e:
logger.error(f"RAG backend error for backend query: {e}")
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=f"RAG backend returned error: {e.response.status_code}"
)
except Exception as e:
logger.error(f"Unexpected error in backend query: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal server error"
)
finally:
await rag_service.close()

95
app/api/v1/settings.py Normal file
View File

@ -0,0 +1,95 @@
"""
Settings API endpoints.
Управление настройками пользователя для всех трех окружений (IFT, PSI, PROD).
"""
from fastapi import APIRouter, Depends, HTTPException, status
from app.models.settings import UserSettings, UserSettingsUpdate
from app.interfaces.db_api_client import DBApiClient
from app.dependencies import get_db_client, get_current_user
import httpx
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/settings", tags=["settings"])
@router.get("", response_model=UserSettings)
async def get_settings(
current_user: dict = Depends(get_current_user),
db_client: DBApiClient = Depends(get_db_client)
):
"""
Получить настройки пользователя для всех окружений.
Returns:
UserSettings: Настройки пользователя для IFT, PSI, PROD
"""
user_id = current_user["user_id"]
try:
settings = await db_client.get_user_settings(user_id)
return settings
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User settings not found"
)
logger.error(f"Failed to get user settings: {e}")
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail="Failed to retrieve settings from DB API"
)
except Exception as e:
logger.error(f"Unexpected error getting user settings: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal server error"
)
@router.put("", 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: Новые настройки для одного или нескольких окружений
Returns:
UserSettings: Обновленные настройки
"""
user_id = current_user["user_id"]
try:
updated_settings = await db_client.update_user_settings(user_id, settings_update)
return updated_settings
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
elif e.response.status_code == 400:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid settings format"
)
logger.error(f"Failed to update user settings: {e}")
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail="Failed to update settings in DB API"
)
except Exception as e:
logger.error(f"Unexpected error updating user settings: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal server error"
)

57
app/config.py Normal file
View File

@ -0,0 +1,57 @@
"""Application configuration loaded from environment variables."""
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""Application settings loaded from .env file."""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
)
# Application settings
APP_NAME: str = "Brief Bench API"
DEBUG: bool = False
# JWT Authentication
JWT_SECRET_KEY: str
JWT_ALGORITHM: str = "HS256"
JWT_EXPIRE_MINUTES: int = 43200 # 30 days
# DB API Service (external)
DB_API_URL: str
DB_API_TIMEOUT: int = 30
# RAG Backend - IFT Environment
IFT_RAG_HOST: str
IFT_RAG_PORT: int
IFT_RAG_ENDPOINT: str
IFT_RAG_CERT_CA: str = ""
IFT_RAG_CERT_KEY: str = ""
IFT_RAG_CERT_CERT: str = ""
# RAG Backend - PSI Environment
PSI_RAG_HOST: str
PSI_RAG_PORT: int
PSI_RAG_ENDPOINT: str
PSI_RAG_CERT_CA: str = ""
PSI_RAG_CERT_KEY: str = ""
PSI_RAG_CERT_CERT: str = ""
# RAG Backend - PROD Environment
PROD_RAG_HOST: str
PROD_RAG_PORT: int
PROD_RAG_ENDPOINT: str
PROD_RAG_CERT_CA: str = ""
PROD_RAG_CERT_KEY: str = ""
PROD_RAG_CERT_CERT: str = ""
# Request Timeouts
RAG_REQUEST_TIMEOUT: int = 1800 # 30 minutes in seconds
# Global settings instance
settings = Settings()

48
app/dependencies.py Normal file
View File

@ -0,0 +1,48 @@
"""Dependency injection for FastAPI."""
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from app.config import settings
from app.interfaces.db_api_client import DBApiClient
from app.utils.security import decode_access_token
security = HTTPBearer()
def get_db_client() -> DBApiClient:
"""
Get DB API client instance.
Returns:
DBApiClient configured with api_prefix from settings
"""
return DBApiClient(api_prefix=settings.DB_API_URL)
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security)
) -> dict:
"""
Get current user from JWT token.
Args:
credentials: HTTP Bearer credentials from request
Returns:
Token payload with user_id and login
Raises:
HTTPException: If token is invalid or expired
"""
token = credentials.credentials
payload = decode_access_token(token)
if payload is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
headers={"WWW-Authenticate": "Bearer"},
)
return payload

View File

277
app/interfaces/base.py Normal file
View File

@ -0,0 +1,277 @@
"""
TgBackendInterface base class implementation.
Базовый интерфейс для всех HTTP API клиентов с использованием httpx.
Предоставляет автоматическую сериализацию/десериализацию Pydantic моделей,
error handling и логирование.
"""
import logging
from typing import Optional, Type, TypeVar, Any, Dict
from pydantic import BaseModel, ValidationError
import httpx
T = TypeVar('T', bound=BaseModel)
logger = logging.getLogger(__name__)
class TgBackendInterface:
"""
Базовый интерфейс для HTTP API клиентов.
Возможности:
- httpx.AsyncClient для асинхронных HTTP запросов
- Автоматическая сериализация Pydantic моделей в JSON
- Автоматическая десериализация JSON в Pydantic модели
- Error handling с детальными логами
- Настраиваемые timeout и retry параметры
"""
def __init__(
self,
api_prefix: str,
timeout: float = 30.0,
max_retries: int = 3,
**kwargs
):
"""
Инициализация клиента.
Args:
api_prefix: Базовый URL API (например, http://db-api:8080/api/v1)
timeout: Таймаут запроса в секундах (default: 30)
max_retries: Максимальное количество повторных попыток (default: 3)
**kwargs: Дополнительные параметры для httpx.AsyncClient
"""
self.api_prefix = api_prefix.rstrip('/')
# Настройка httpx.AsyncClient
self.client = httpx.AsyncClient(
timeout=httpx.Timeout(timeout),
transport=httpx.AsyncHTTPTransport(retries=max_retries),
follow_redirects=True,
**kwargs
)
logger.info(f"TgBackendInterface initialized with api_prefix: {self.api_prefix}")
async def close(self):
"""Закрыть HTTP клиент."""
await self.client.aclose()
logger.info("TgBackendInterface client closed")
async def __aenter__(self):
"""Async context manager entry."""
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Async context manager exit."""
await self.close()
def _build_url(self, path: str) -> str:
"""
Построить полный URL из api_prefix и path.
Args:
path: Путь эндпоинта (может начинаться с / или без)
Returns:
Полный URL
"""
if not path.startswith('/'):
path = '/' + path
return f"{self.api_prefix}{path}"
def _serialize_body(self, body: Optional[BaseModel]) -> Optional[Dict]:
"""
Сериализовать Pydantic модель в dict для JSON.
Args:
body: Pydantic модель
Returns:
Dict с данными или None
"""
if body is None:
return None
return body.model_dump(mode='json', exclude_none=False)
def _deserialize_response(
self,
data: Any,
response_model: Optional[Type[T]]
) -> Any:
"""
Десериализовать JSON ответ в Pydantic модель.
Args:
data: Данные из JSON ответа
response_model: Pydantic модель для валидации
Returns:
Экземпляр Pydantic модели или исходные данные
"""
if response_model is None:
return data
try:
return response_model(**data) if isinstance(data, dict) else response_model(data)
except ValidationError as e:
logger.error(f"Validation error for {response_model.__name__}: {e}")
raise
async def _handle_response(
self,
response: httpx.Response,
response_model: Optional[Type[T]] = None
) -> Any:
"""
Обработать HTTP ответ.
Args:
response: HTTP ответ от httpx
response_model: Pydantic модель для валидации
Returns:
Десериализованные данные
Raises:
httpx.HTTPStatusError: При HTTP ошибках
"""
try:
response.raise_for_status()
except httpx.HTTPStatusError as e:
logger.error(
f"HTTP error {e.response.status_code} for {e.request.url}: "
f"{e.response.text}"
)
raise
# Если ответ пустой (204 No Content), вернуть пустой dict
if response.status_code == 204 or len(response.content) == 0:
return {}
# Парсим JSON
try:
data = response.json()
except Exception as e:
logger.error(f"Failed to parse JSON response: {e}")
logger.debug(f"Response content: {response.text}")
raise
# Десериализуем в Pydantic модель если нужно
return self._deserialize_response(data, response_model)
async def get(
self,
path: str,
params: Optional[dict] = None,
response_model: Optional[Type[T]] = None,
**kwargs
) -> Any:
"""
HTTP GET запрос к {api_prefix}{path}.
Args:
path: Путь эндпоинта
params: Query параметры
response_model: Pydantic модель для валидации ответа
**kwargs: Дополнительные параметры для httpx
Returns:
Десериализованный ответ (Pydantic модель или dict)
Raises:
httpx.HTTPStatusError: При HTTP ошибках
ValidationError: При ошибках валидации Pydantic
"""
url = self._build_url(path)
logger.debug(f"GET {url} with params={params}")
response = await self.client.get(url, params=params, **kwargs)
return await self._handle_response(response, response_model)
async def post(
self,
path: str,
body: Optional[BaseModel] = None,
response_model: Optional[Type[T]] = None,
**kwargs
) -> Any:
"""
HTTP POST запрос к {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"POST {url} with body={json_body}")
response = await self.client.post(url, json=json_body, **kwargs)
return await self._handle_response(response, response_model)
async def put(
self,
path: str,
body: Optional[BaseModel] = None,
response_model: Optional[Type[T]] = None,
**kwargs
) -> Any:
"""
HTTP PUT запрос к {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"PUT {url} with body={json_body}")
response = await self.client.put(url, json=json_body, **kwargs)
return await self._handle_response(response, response_model)
async def delete(
self,
path: str,
**kwargs
) -> Any:
"""
HTTP DELETE запрос к {api_prefix}{path}.
Args:
path: Путь эндпоинта
**kwargs: Дополнительные параметры для httpx
Returns:
Ответ от сервера (обычно пустой dict для 204)
Raises:
httpx.HTTPStatusError: При HTTP ошибках
"""
url = self._build_url(path)
logger.debug(f"DELETE {url}")
response = await self.client.delete(url, **kwargs)
return await self._handle_response(response)

View File

@ -0,0 +1,103 @@
"""DB API client using TgBackendInterface."""
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
class DBApiClient(TgBackendInterface):
"""
Клиент для DB API сервиса.
Использует Pydantic схемы для type-safety.
Методы self.get(), self.post(), self.put(), self.delete() от TgBackendInterface.
"""
async def login_user(self, request: LoginRequest) -> UserResponse:
"""
POST {api_prefix}/users/login
Авторизация пользователя и запись логина.
"""
return await self.post("/users/login", body=request, response_model=UserResponse)
async def get_user_settings(self, user_id: str) -> UserSettings:
"""
GET {api_prefix}/users/{user_id}/settings
Получить настройки пользователя для всех окружений.
"""
return await self.get(f"/users/{user_id}/settings", response_model=UserSettings)
async def update_user_settings(
self,
user_id: str,
settings: UserSettingsUpdate
) -> UserSettings:
"""
PUT {api_prefix}/users/{user_id}/settings
Обновить настройки пользователя.
"""
return await self.put(
f"/users/{user_id}/settings",
body=settings,
response_model=UserSettings
)
async def save_session(
self,
user_id: str,
session_data: SessionCreate
) -> SessionResponse:
"""
POST {api_prefix}/users/{user_id}/sessions
Сохранить сессию анализа.
"""
return await self.post(
f"/users/{user_id}/sessions",
body=session_data,
response_model=SessionResponse
)
async def get_sessions(
self,
user_id: str,
environment: str = None,
limit: int = 50,
offset: int = 0
) -> SessionList:
"""
GET {api_prefix}/users/{user_id}/sessions
Получить список сессий пользователя.
"""
params = {"limit": limit, "offset": offset}
if environment:
params["environment"] = environment
return await self.get(
f"/users/{user_id}/sessions",
params=params,
response_model=SessionList
)
async def get_session(self, user_id: str, session_id: str) -> SessionResponse:
"""
GET {api_prefix}/users/{user_id}/sessions/{session_id}
Получить конкретную сессию.
"""
return await self.get(
f"/users/{user_id}/sessions/{session_id}",
response_model=SessionResponse
)
async def delete_session(self, user_id: str, session_id: str) -> dict:
"""
DELETE {api_prefix}/users/{user_id}/sessions/{session_id}
Удалить сессию.
"""
return await self.delete(f"/users/{user_id}/sessions/{session_id}")

50
app/main.py Normal file
View File

@ -0,0 +1,50 @@
"""FastAPI application entry point."""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from app.api.v1 import auth, settings as settings_router, query, analysis
from app.config import settings
# Create FastAPI app
app = FastAPI(
title=settings.APP_NAME,
debug=settings.DEBUG,
version="1.0.0"
)
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # TODO: Configure properly in production
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# API v1 routes
app.include_router(auth.router, prefix="/api/v1")
app.include_router(settings_router.router, prefix="/api/v1")
app.include_router(query.router, prefix="/api/v1")
app.include_router(analysis.router, prefix="/api/v1")
# Serve static files (frontend)
# app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/")
async def root():
"""Root endpoint."""
return {"message": "Brief Bench API", "version": "1.0.0"}
@app.get("/health")
async def health():
"""Health check endpoint."""
return {"status": "ok"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

View File

0
app/models/__init__.py Normal file
View File

43
app/models/analysis.py Normal file
View File

@ -0,0 +1,43 @@
"""Analysis session Pydantic models."""
from typing import Any
from pydantic import BaseModel
class SessionCreate(BaseModel):
"""Create new analysis session."""
environment: str
api_mode: str
request: list[Any]
response: dict
annotations: dict
class SessionResponse(BaseModel):
"""Analysis session response."""
session_id: str
user_id: str
environment: str
api_mode: str
request: list[Any]
response: dict
annotations: dict
created_at: str
updated_at: str
class SessionListItem(BaseModel):
"""Session list item for listing sessions."""
session_id: str
environment: str
created_at: str
class SessionList(BaseModel):
"""List of sessions with total count."""
sessions: list[SessionListItem]
total: int

35
app/models/auth.py Normal file
View File

@ -0,0 +1,35 @@
"""Authentication Pydantic models."""
from pydantic import BaseModel, Field
class LoginRequest(BaseModel):
"""Login request model."""
login: str = Field(..., pattern=r'^\d{8}$', description="8-значный логин")
client_ip: str = Field(..., description="IP адрес клиента")
class UserResponse(BaseModel):
"""User response model."""
user_id: str
login: str
last_login_at: str
created_at: str
class LoginResponse(BaseModel):
"""Login response with JWT token."""
access_token: str
token_type: str = "bearer"
user: UserResponse
class TokenPayload(BaseModel):
"""JWT token payload."""
user_id: str
login: str
exp: int # Expiration timestamp

35
app/models/query.py Normal file
View File

@ -0,0 +1,35 @@
"""Query request/response Pydantic models."""
from typing import Any
from pydantic import BaseModel
class QuestionRequest(BaseModel):
"""Single question for batch query."""
body: str
with_docs: bool = True
class BenchQueryRequest(BaseModel):
"""Bench mode query request."""
environment: str # ift, psi, prod
questions: list[QuestionRequest]
class BackendQueryRequest(BaseModel):
"""Backend mode query request (one-by-one)."""
environment: str # ift, psi, prod
questions: list[str]
reset_session: bool = True
class QueryResponse(BaseModel):
"""Query response with metadata."""
request_id: str
timestamp: str
environment: str
response: dict # RagResponseBenchList or converted format

30
app/models/settings.py Normal file
View File

@ -0,0 +1,30 @@
"""User settings Pydantic models."""
from pydantic import BaseModel
class EnvironmentSettings(BaseModel):
"""Settings for a specific environment (IFT/PSI/PROD)."""
apiMode: str = "bench"
bearerToken: str = ""
systemPlatform: str = ""
systemPlatformUser: str = ""
platformUserId: str = ""
platformId: str = ""
withClassify: bool = False
resetSessionMode: bool = True
class UserSettings(BaseModel):
"""User settings for all environments."""
user_id: str
settings: dict[str, EnvironmentSettings] # ift, psi, prod
updated_at: str
class UserSettingsUpdate(BaseModel):
"""Update user settings request."""
settings: dict[str, EnvironmentSettings]

0
app/services/__init__.py Normal file
View File

View File

@ -0,0 +1,54 @@
"""Authentication service."""
from app.interfaces.db_api_client import DBApiClient
from app.models.auth import LoginRequest, LoginResponse, UserResponse
from app.utils.security import create_access_token
class AuthService:
"""Service for user authentication."""
def __init__(self, db_client: DBApiClient):
"""
Initialize auth service.
Args:
db_client: DB API client instance
"""
self.db_client = db_client
async def login(self, login: str, client_ip: str) -> LoginResponse:
"""
Authenticate user and generate JWT token.
Args:
login: 8-digit login
client_ip: Client IP address
Returns:
LoginResponse with JWT token and user info
Raises:
ValueError: If login format is invalid
Exception: If DB API call fails
"""
# Validate login format
if not (login.isdigit() and len(login) == 8):
raise ValueError("Login must be 8 digits")
# Call DB API to validate and record login
request = LoginRequest(login=login, client_ip=client_ip)
user: UserResponse = await self.db_client.login_user(request)
# Generate JWT token
token_data = {
"user_id": user.user_id,
"login": user.login
}
access_token = create_access_token(token_data)
return LoginResponse(
access_token=access_token,
token_type="bearer",
user=user
)

316
app/services/rag_service.py Normal file
View File

@ -0,0 +1,316 @@
"""
RAG Service for interacting with RAG backends.
Поддерживает два режима:
1. Bench mode - batch запросы (все вопросы сразу)
2. Backend mode - вопросы по одному с возможностью сброса сессии
"""
import logging
import httpx
import uuid
from typing import List, Dict, Optional, Any
from datetime import datetime
from app.config import settings
from app.models.query import QuestionRequest
logger = logging.getLogger(__name__)
class RagService:
"""
Сервис для взаимодействия с RAG backend для трех окружений (IFT, PSI, PROD).
Поддерживает mTLS, настраиваемые headers и два режима работы:
- bench: batch запросы
- backend: последовательные запросы с reset session
"""
def __init__(self):
"""Инициализация клиентов для всех трех окружений."""
self.clients = {
'ift': self._create_client('ift'),
'psi': self._create_client('psi'),
'prod': self._create_client('prod')
}
logger.info("RagService initialized for all environments")
def _create_client(self, environment: str) -> httpx.AsyncClient:
"""
Создать HTTP клиент для указанного окружения с mTLS поддержкой.
Args:
environment: Окружение (ift/psi/prod)
Returns:
Настроенный httpx.AsyncClient
"""
env_upper = environment.upper()
# Получить пути к сертификатам из config
cert_cert_path = getattr(settings, f"{env_upper}_RAG_CERT_CERT", "")
cert_key_path = getattr(settings, f"{env_upper}_RAG_CERT_KEY", "")
cert_ca_path = getattr(settings, f"{env_upper}_RAG_CERT_CA", "")
# Настройка mTLS
cert = None
verify = True
if cert_cert_path and cert_key_path:
cert = (cert_cert_path, cert_key_path)
logger.info(f"mTLS enabled for {environment} with cert: {cert_cert_path}")
if cert_ca_path:
verify = cert_ca_path
logger.info(f"Custom CA for {environment}: {cert_ca_path}")
return httpx.AsyncClient(
timeout=httpx.Timeout(1800.0), # 30 minutes for long-running requests
cert=cert,
verify=verify,
follow_redirects=True
)
async def close(self):
"""Закрыть все HTTP клиенты."""
for env, client in self.clients.items():
await client.aclose()
logger.info(f"Client closed for {env}")
async def __aenter__(self):
"""Async context manager entry."""
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Async context manager exit."""
await self.close()
def _get_base_url(self, environment: str) -> str:
"""
Получить базовый URL для окружения.
Args:
environment: Окружение (ift/psi/prod)
Returns:
Базовый URL (https://host:port)
"""
env_upper = environment.upper()
host = getattr(settings, f"{env_upper}_RAG_HOST")
port = getattr(settings, f"{env_upper}_RAG_PORT")
return f"https://{host}:{port}"
def _get_bench_endpoint(self, environment: str) -> str:
"""
Получить endpoint для bench mode.
Args:
environment: Окружение (ift/psi/prod)
Returns:
Полный URL для bench запросов
"""
env_upper = environment.upper()
base_url = self._get_base_url(environment)
endpoint = getattr(settings, f"{env_upper}_RAG_ENDPOINT")
return f"{base_url}/{endpoint.lstrip('/')}"
def _get_backend_endpoints(self, environment: str, user_settings: Dict) -> Dict[str, str]:
"""
Получить endpoints для backend mode (ask + reset).
Args:
environment: Окружение (ift/psi/prod)
user_settings: Настройки пользователя для окружения
Returns:
Dict с ask_endpoint и reset_endpoint
"""
base_url = self._get_base_url(environment)
# Endpoints из настроек пользователя или дефолтные
ask_endpoint = user_settings.get('backendAskEndpoint', 'ask')
reset_endpoint = user_settings.get('backendResetEndpoint', 'reset')
return {
'ask': f"{base_url}/{ask_endpoint.lstrip('/')}",
'reset': f"{base_url}/{reset_endpoint.lstrip('/')}"
}
def _build_bench_headers(
self,
environment: str,
user_settings: Dict,
request_id: Optional[str] = None
) -> Dict[str, str]:
"""
Построить headers для bench mode запроса.
Args:
environment: Окружение (ift/psi/prod)
user_settings: Настройки пользователя
request_id: Request ID (генерируется если не задан)
Returns:
Dict с headers
"""
headers = {
"Content-Type": "application/json",
"Request-Id": request_id or str(uuid.uuid4()),
"System-Id": f"brief-bench-{environment}"
}
# Добавить опциональные headers из настроек пользователя
if user_settings.get('bearerToken'):
headers["Authorization"] = f"Bearer {user_settings['bearerToken']}"
if user_settings.get('systemPlatform'):
headers["System-Platform"] = user_settings['systemPlatform']
return headers
def _build_backend_headers(self, user_settings: Dict) -> Dict[str, str]:
"""
Построить headers для backend mode запроса.
Args:
user_settings: Настройки пользователя
Returns:
Dict с headers
"""
headers = {
"Content-Type": "application/json"
}
if user_settings.get('bearerToken'):
headers["Authorization"] = f"Bearer {user_settings['bearerToken']}"
if user_settings.get('platformUserId'):
headers["Platform-User-Id"] = user_settings['platformUserId']
if user_settings.get('platformId'):
headers["Platform-Id"] = user_settings['platformId']
return headers
async def send_bench_query(
self,
environment: str,
questions: List[QuestionRequest],
user_settings: Dict,
request_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Отправить batch запрос к RAG backend (bench mode).
Args:
environment: Окружение (ift/psi/prod)
questions: Список вопросов
user_settings: Настройки пользователя для окружения
request_id: Request ID (опционально)
Returns:
Dict с ответом от RAG backend
Raises:
httpx.HTTPStatusError: При HTTP ошибках
"""
client = self.clients[environment.lower()]
url = self._get_bench_endpoint(environment)
headers = self._build_bench_headers(environment, user_settings, request_id)
# Сериализуем вопросы в JSON
body = [q.model_dump() for q in questions]
logger.info(f"Sending bench query to {environment}: {len(questions)} questions")
logger.debug(f"URL: {url}, Headers: {headers}")
try:
response = await client.post(url, json=body, headers=headers)
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as e:
logger.error(f"Bench query failed for {environment}: {e.response.status_code} - {e.response.text}")
raise
except Exception as e:
logger.error(f"Unexpected error in bench query for {environment}: {e}")
raise
async def send_backend_query(
self,
environment: str,
questions: List[QuestionRequest],
user_settings: Dict,
reset_session: bool = True
) -> List[Dict[str, Any]]:
"""
Отправить вопросы по одному к RAG backend (backend mode).
После каждого вопроса может сбросить сессию (если resetSessionMode=true).
Args:
environment: Окружение (ift/psi/prod)
questions: Список вопросов
user_settings: Настройки пользователя для окружения
reset_session: Сбрасывать ли сессию после каждого вопроса
Returns:
List[Dict] с ответами от RAG backend
Raises:
httpx.HTTPStatusError: При HTTP ошибках
"""
client = self.clients[environment.lower()]
endpoints = self._get_backend_endpoints(environment, user_settings)
headers = self._build_backend_headers(user_settings)
logger.info(
f"Sending backend query to {environment}: {len(questions)} questions "
f"(reset_session={reset_session})"
)
responses = []
for idx, question in enumerate(questions, start=1):
# Формируем body для запроса
now = datetime.utcnow().isoformat() + "Z"
body = {
"question": question.body,
"user_message_id": idx,
"user_message_datetime": now,
"with_classify": user_settings.get('withClassify', False)
}
logger.debug(f"Sending question {idx}/{len(questions)}: {question.body[:50]}...")
try:
# Отправляем вопрос
response = await client.post(endpoints['ask'], json=body, headers=headers)
response.raise_for_status()
response_data = response.json()
responses.append(response_data)
# Сбрасываем сессию если нужно
if reset_session and user_settings.get('resetSessionMode', True):
reset_body = {"user_message_datetime": now}
logger.debug(f"Resetting session after question {idx}")
reset_response = await client.post(
endpoints['reset'],
json=reset_body,
headers=headers
)
reset_response.raise_for_status()
except httpx.HTTPStatusError as e:
logger.error(
f"Backend query failed for {environment} (question {idx}): "
f"{e.response.status_code} - {e.response.text}"
)
raise
except Exception as e:
logger.error(f"Unexpected error in backend query for {environment} (question {idx}): {e}")
raise
logger.info(f"Backend query completed for {environment}: {len(responses)} responses")
return responses

0
app/utils/__init__.py Normal file
View File

58
app/utils/security.py Normal file
View File

@ -0,0 +1,58 @@
"""JWT token encoding/decoding utilities."""
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from app.config import settings
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
"""
Create JWT access token.
Args:
data: Data to encode in token (user_id, login, etc.)
expires_delta: Token expiration time (default: from settings)
Returns:
Encoded JWT token
"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=settings.JWT_EXPIRE_MINUTES)
to_encode.update({"exp": int(expire.timestamp())})
encoded_jwt = jwt.encode(
to_encode,
settings.JWT_SECRET_KEY,
algorithm=settings.JWT_ALGORITHM
)
return encoded_jwt
def decode_access_token(token: str) -> Optional[dict]:
"""
Decode and validate JWT token.
Args:
token: JWT token to decode
Returns:
Decoded token payload or None if invalid
"""
try:
payload = jwt.decode(
token,
settings.JWT_SECRET_KEY,
algorithms=[settings.JWT_ALGORITHM]
)
return payload
except JWTError:
return None

20
docker-compose.yml Normal file
View File

@ -0,0 +1,20 @@
version: '3.8'
services:
fastapi:
build: .
container_name: brief-bench-fastapi
ports:
- "8000:8000"
env_file:
- .env
volumes:
- ./certs:/app/certs:ro # mTLS сертификаты (read-only)
- ./static:/app/static # Frontend files
restart: unless-stopped
networks:
- brief-bench-network
networks:
brief-bench-network:
driver: bridge

24
requirements.txt Normal file
View File

@ -0,0 +1,24 @@
# FastAPI & web server
fastapi==0.104.1
uvicorn[standard]==0.24.0
python-multipart==0.0.6
# HTTP клиенты
httpx==0.25.2
# Pydantic для валидации
pydantic==2.5.0
pydantic-settings==2.1.0
# JWT & Security
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
# Async
anyio==4.1.0
# Environment variables
python-dotenv==1.0.0
# CORS
fastapi-cors==0.0.6