683 lines
22 KiB
Markdown
683 lines
22 KiB
Markdown
# План развития 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)
|
||
|
||
### Требуется доделать
|
||
- 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
|
||
|
||
**Готовы начать с первого этапа?**
|