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