first version
This commit is contained in:
commit
85dc167449
|
|
@ -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": []
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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. Пустые строки в настройках означают "не задано" (опциональные поля)
|
||||
|
|
@ -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
|
||||
|
||||
**Готовы начать с первого этапа?**
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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 стили
|
||||
|
|
@ -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.
|
||||
|
|
@ -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,0 +1,7 @@
|
|||
"""
|
||||
API v1 routers.
|
||||
"""
|
||||
|
||||
from app.api.v1 import auth, settings, query, analysis
|
||||
|
||||
__all__ = ["auth", "settings", "query", "analysis"]
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -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)}"
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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}")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,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
|
||||
)
|
||||
|
|
@ -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,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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue