This commit is contained in:
itqop 2025-12-17 18:29:53 +03:00
parent 85dc167449
commit cf86a9378c
7 changed files with 5508 additions and 3 deletions

View File

@ -3,6 +3,7 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from app.api.v1 import auth, settings as settings_router, query, analysis
from app.config import settings
@ -30,13 +31,19 @@ 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.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/app")
async def serve_frontend():
"""Serve the main frontend application."""
return FileResponse("static/index.html")
@app.get("/")
async def root():
"""Root endpoint."""
return {"message": "Brief Bench API", "version": "1.0.0"}
"""Root endpoint - redirect to app."""
return FileResponse("static/index.html")
@app.get("/health")

246
static/api-client.js Normal file
View File

@ -0,0 +1,246 @@
/**
* 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()

1671
static/app.js Normal file

File diff suppressed because it is too large Load Diff

1894
static/app.js.bak Normal file

File diff suppressed because it is too large Load Diff

451
static/index.html Normal file
View File

@ -0,0 +1,451 @@
<!DOCTYPE html>
<html lang="ru">
<!-- LOCAL -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Brief Bench - RAG Testing Interface</title>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&family=Roboto+Mono:wght@400;500&display=swap" rel="stylesheet">
<!-- Material Icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- Styles -->
<link rel="stylesheet" href="styles.css">
</head>
<body>
<!-- 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>
<div id="app">
<!-- App Bar -->
<header class="app-bar">
<div class="app-bar-title">
<button class="btn-icon" id="menu-btn" title="Toggle menu">
<span class="material-icons">menu</span>
</button>
<span>Brief Bench</span>
</div>
<div class="app-bar-actions">
<button class="btn-icon" id="new-query-btn" title="New Query">
<span class="material-icons">add_circle</span>
</button>
<button class="btn-icon" id="import-btn" title="Импорт анализа">
<span class="material-icons">folder_open</span>
</button>
<button class="btn-icon" id="export-btn" title="Экспорт анализа">
<span class="material-icons">save</span>
</button>
<button class="btn-icon" id="settings-btn" title="Settings">
<span class="material-icons">settings</span>
</button>
<button class="btn-icon" id="logout-btn" title="Выход">
<span class="material-icons">logout</span>
</button>
</div>
</header>
<!-- Environment Tabs -->
<div class="environment-tabs">
<button class="env-tab active" data-env="ift">ИФТ</button>
<button class="env-tab" data-env="psi">ПСИ</button>
<button class="env-tab" data-env="prod">ПРОМ</button>
</div>
<!-- Main Content Area -->
<div class="app-content">
<!-- Drawer / Sidebar -->
<aside class="drawer" id="drawer">
<div class="drawer-header">
<div style="flex: 1;">
<h6 style="margin-bottom: 4px;">Вопросы и ответы</h6>
<div class="text-caption" id="questions-count">0 вопросов</div>
</div>
<button class="btn-icon" id="clear-all-btn" title="Очистить всё (обновить страницу)" style="align-self: flex-start;">
<span class="material-icons">refresh</span>
</button>
</div>
<div class="drawer-content" id="questions-list">
<!-- Questions will be populated here -->
<div class="empty-state">
<div class="empty-state-icon">
<span class="material-icons" style="font-size: inherit;">question_answer</span>
</div>
<div class="empty-state-text">Нет данных</div>
<div class="empty-state-subtext">Отправьте запрос к RAG бэкенду</div>
</div>
</div>
</aside>
<!-- Main Area -->
<main class="main-area" id="main-area">
<!-- Query Builder (shown when no data) -->
<div id="query-builder" class="hidden">
<div class="card">
<div class="card-header">
<h5 class="card-title">Создание запроса</h5>
<div class="toggle-group">
<button class="toggle-option active" data-mode="questions">Вопросы</button>
<button class="toggle-option" data-mode="raw-json">Raw JSON</button>
</div>
</div>
<div class="card-content">
<!-- Questions Mode -->
<div id="questions-mode" class="query-mode">
<div class="form-group">
<label class="form-label">Введите вопросы (каждый с новой строки)</label>
<textarea
id="questions-textarea"
class="form-textarea"
rows="10"
placeholder="Какая текущая ВВП в России?&#10;Какие показатели у газпрома"></textarea>
<div class="form-helper-text">Каждая строка будет отправлена как отдельный вопрос с флагом with_docs: true</div>
</div>
</div>
<!-- Raw JSON Mode -->
<div id="raw-json-mode" class="query-mode hidden">
<div class="form-group">
<label class="form-label">JSON запрос</label>
<textarea
id="json-textarea"
class="form-textarea"
rows="10"
placeholder='[&#10; { "body": "Какая текущая ВВП в России?", "with_docs": true }&#10;]'></textarea>
<div class="form-helper-text" id="json-validation-message"></div>
</div>
<div class="flex gap-sm">
<button class="btn btn-outlined" id="load-request-btn">
<span class="material-icons">upload_file</span>
Загрузить из файла
</button>
<button class="btn btn-text" id="validate-json-btn">
<span class="material-icons">check_circle</span>
Проверить JSON
</button>
</div>
</div>
</div>
<div class="card-actions">
<button class="btn btn-outlined" id="load-response-btn">
<span class="material-icons">folder_open</span>
Загрузить ответ (Response)
</button>
<button class="btn btn-filled" id="send-query-btn">
<span class="material-icons">send</span>
Отправить запрос
</button>
</div>
</div>
</div>
<!-- Answer Viewer (shown when data is available) -->
<div id="answer-viewer" class="hidden">
<!-- Question Header -->
<div class="card mb-md">
<div class="card-content">
<div class="text-overline mb-sm">Вопрос #<span id="current-question-number">1</span></div>
<h4 id="current-question-text"></h4>
</div>
</div>
<!-- Metadata Card -->
<div class="card mb-md">
<div class="card-header">
<h6 class="card-title">Метаданные</h6>
</div>
<div class="card-content">
<div class="metadata-grid">
<div class="metadata-item">
<div class="metadata-label">Время обработки</div>
<div class="metadata-value" id="processing-time">-</div>
</div>
<div class="metadata-item">
<div class="metadata-label">Request ID</div>
<div class="metadata-value" id="request-id">-</div>
</div>
<div class="metadata-item">
<div class="metadata-label">Timestamp</div>
<div class="metadata-value" id="request-timestamp">-</div>
</div>
</div>
</div>
</div>
<!-- Answer Bodies -->
<div class="card mb-md">
<div class="card-header">
<h6 class="card-title">Тексты ответов</h6>
</div>
<div class="tabs">
<button class="tab active" data-tab="body-research">Research</button>
<button class="tab" data-tab="body-analytical">Analytical Hub</button>
</div>
<div id="body-research" class="tab-content active">
<div id="body-research-text"></div>
<!-- Annotation Section -->
<div class="annotation-section">
<h6 class="mb-sm">Пометки</h6>
<div class="annotation-issues mb-md">
<label class="issue-checkbox">
<input type="checkbox" class="checkbox" data-section="body_research" data-issue="factual_error">
<span>Факт. ошибка</span>
</label>
<label class="issue-checkbox">
<input type="checkbox" class="checkbox" data-section="body_research" data-issue="inaccurate_wording">
<span>Неточность формулировки</span>
</label>
<label class="issue-checkbox">
<input type="checkbox" class="checkbox" data-section="body_research" data-issue="insufficient_context">
<span>Недостаточно контекста</span>
</label>
<label class="issue-checkbox">
<input type="checkbox" class="checkbox" data-section="body_research" data-issue="offtopic">
<span>Не по вопросу</span>
</label>
<label class="issue-checkbox">
<input type="checkbox" class="checkbox" data-section="body_research" data-issue="technical_answer">
<span>Технический ответ</span>
</label>
</div>
<div class="form-group">
<label class="form-label">Комментарий</label>
<textarea class="form-textarea" rows="3" data-section="body_research" placeholder="Ваш комментарий..."></textarea>
</div>
</div>
</div>
<div id="body-analytical" class="tab-content">
<div id="body-analytical-text"></div>
<!-- Annotation Section -->
<div class="annotation-section">
<h6 class="mb-sm">Пометки</h6>
<div class="annotation-issues mb-md">
<label class="issue-checkbox">
<input type="checkbox" class="checkbox" data-section="body_analytical_hub" data-issue="factual_error">
<span>Факт. ошибка</span>
</label>
<label class="issue-checkbox">
<input type="checkbox" class="checkbox" data-section="body_analytical_hub" data-issue="inaccurate_wording">
<span>Неточность формулировки</span>
</label>
<label class="issue-checkbox">
<input type="checkbox" class="checkbox" data-section="body_analytical_hub" data-issue="insufficient_context">
<span>Недостаточно контекста</span>
</label>
<label class="issue-checkbox">
<input type="checkbox" class="checkbox" data-section="body_analytical_hub" data-issue="offtopic">
<span>Не по вопросу</span>
</label>
<label class="issue-checkbox">
<input type="checkbox" class="checkbox" data-section="body_analytical_hub" data-issue="technical_answer">
<span>Технический ответ</span>
</label>
</div>
<div class="form-group">
<label class="form-label">Комментарий</label>
<textarea class="form-textarea" rows="3" data-section="body_analytical_hub" placeholder="Ваш комментарий..."></textarea>
</div>
</div>
</div>
</div>
<!-- Overall Rating -->
<div class="card mb-md">
<div class="card-header">
<h6 class="card-title">Общая оценка ответа</h6>
</div>
<div class="card-content">
<div class="form-group">
<label class="form-label" for="overall-rating">Оценка</label>
<select class="form-select" id="overall-rating" aria-label="Общая оценка ответа">
<option value="">Не оценено</option>
<option value="correct">Корректно</option>
<option value="partial">Частично корректно</option>
<option value="incorrect">Некорректно</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Общий комментарий</label>
<textarea class="form-textarea" rows="3" id="overall-comment" placeholder="Общий комментарий по ответу..."></textarea>
</div>
</div>
</div>
<!-- Documents Section -->
<div class="card">
<div class="card-header">
<h6 class="card-title">Документы</h6>
</div>
<div class="tabs">
<button class="tab active" data-tab="docs-vectorstore">Docs from Vectorstore</button>
<button class="tab" data-tab="docs-llm">Docs to LLM</button>
</div>
<!-- Docs from Vectorstore -->
<div id="docs-vectorstore" class="tab-content active">
<div class="tabs">
<button class="tab active" data-tab="vectorstore-research">Research</button>
<button class="tab" data-tab="vectorstore-analytical">Analytical Hub</button>
</div>
<div id="vectorstore-research" class="tab-content active">
<div id="vectorstore-research-docs"></div>
</div>
<div id="vectorstore-analytical" class="tab-content">
<div id="vectorstore-analytical-docs"></div>
</div>
</div>
<!-- Docs to LLM -->
<div id="docs-llm" class="tab-content">
<div class="tabs">
<button class="tab active" data-tab="llm-research">Research</button>
<button class="tab" data-tab="llm-analytical">Analytical Hub</button>
</div>
<div id="llm-research" class="tab-content active">
<div id="llm-research-docs"></div>
</div>
<div id="llm-analytical" class="tab-content">
<div id="llm-analytical-docs"></div>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- Settings Dialog -->
<div class="dialog-overlay" id="settings-dialog">
<div class="dialog">
<div class="dialog-header">
<h5 class="dialog-title">Настройки</h5>
<button class="btn-icon" id="close-settings-btn">
<span class="material-icons">close</span>
</button>
</div>
<div class="dialog-content">
<!-- Environment Selector -->
<div class="form-group">
<label class="form-label">Редактируемое окружение</label>
<select class="form-select" id="settings-env-selector">
<option value="ift">ИФТ</option>
<option value="psi">ПСИ</option>
<option value="prod">ПРОМ</option>
</select>
</div>
<hr style="margin: var(--md-spacing-lg) 0; border: none; border-top: 1px solid var(--md-divider);">
<!-- Environment-specific settings -->
<div id="settings-env-fields">
<div class="form-group">
<label class="form-label">Режим API</label>
<select class="form-select" id="setting-api-mode">
<option value="bench">Bench (батч-тестирование)</option>
<option value="backend">Backend (имитация бота)</option>
</select>
<div class="form-helper-text">Bench - отправка массива вопросов; Backend - вопросы по одному</div>
</div>
<div class="form-group">
<label class="form-label">Bearer Token (необязательно)</label>
<input type="password" class="form-input" id="setting-bearer-token" placeholder="your-bearer-token">
<div class="form-helper-text">Токен для авторизации запросов к RAG API</div>
</div>
<div class="form-group">
<label class="form-label">System-Platform (необязательно)</label>
<input type="text" class="form-input" id="setting-system-platform" placeholder="platform-name">
<div class="form-helper-text">Заголовок System-Platform для запросов</div>
</div>
<div class="form-group">
<label class="form-label">System-Platform-User (необязательно)</label>
<input type="text" class="form-input" id="setting-system-platform-user" placeholder="user-name">
<div class="form-helper-text">Заголовок System-Platform-User для запросов</div>
</div>
<h6 class="mt-lg mb-md" id="backend-settings-header" style="display: none;">Настройки Backend Mode</h6>
<div id="backend-settings" style="display: none;">
<div class="form-group">
<label class="form-label">Platform User ID</label>
<input type="text" class="form-input" id="setting-platform-user-id" placeholder="user-123">
<div class="form-helper-text">Идентификатор пользователя платформы</div>
</div>
<div class="form-group">
<label class="form-label">Platform ID</label>
<input type="text" class="form-input" id="setting-platform-id" placeholder="platform-1">
<div class="form-helper-text">Идентификатор платформы</div>
</div>
<div class="form-group">
<label class="form-label" style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="setting-with-classify" class="checkbox">
Включить классификацию вопросов
</label>
<div class="form-helper-text">Запрашивать question_type в ответе</div>
</div>
<div class="form-group">
<label class="form-label" style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="setting-reset-session-mode" class="checkbox" checked>
Сбрасывать сессию после каждого вопроса
</label>
<div class="form-helper-text">Если выключено, вопросы будут задаваться в рамках одной сессии</div>
</div>
</div>
</div>
</div>
<div class="dialog-actions">
<button class="btn btn-text" id="import-settings-btn">
<span class="material-icons">upload</span>
Импорт настроек
</button>
<button class="btn btn-text" id="export-settings-btn">
<span class="material-icons">download</span>
Экспорт настроек
</button>
<button class="btn btn-outlined" id="reset-settings-btn">Сброс</button>
<button class="btn btn-filled" id="save-settings-btn">Сохранить</button>
</div>
</div>
</div>
<!-- Loading Overlay -->
<div class="loading-overlay" id="loading-overlay">
<div class="spinner"></div>
<div class="loading-message" id="loading-message">Отправка запроса...</div>
<div class="loading-submessage">Это может занять до 30 минут</div>
</div>
<!-- Scripts -->
<script src="settings.js"></script>
<script src="api-client.js"></script>
<script src="app.js"></script>
</body>
</html>

70
static/settings.js Normal file
View File

@ -0,0 +1,70 @@
// Default settings for Brief Bench (FastAPI Version)
// User-editable fields only - server configuration managed by FastAPI backend
const defaultSettings = {
// Active environment
activeEnvironment: 'ift', // 'ift', 'psi', or 'prod'
// Environment-specific settings
environments: {
ift: {
name: 'ИФТ',
apiMode: 'bench', // 'bench' or 'backend'
// Optional headers for RAG backend
bearerToken: '', // Bearer token for authorization (optional)
systemPlatform: '', // System-Platform header (optional)
systemPlatformUser: '', // System-Platform-User header (optional)
// Backend mode settings
platformUserId: '',
platformId: '',
withClassify: false,
resetSessionMode: true // Reset session after each question
},
psi: {
name: 'ПСИ',
apiMode: 'bench', // 'bench' or 'backend'
// Optional headers for RAG backend
bearerToken: '',
systemPlatform: '',
systemPlatformUser: '',
// Backend mode settings
platformUserId: '',
platformId: '',
withClassify: false,
resetSessionMode: true
},
prod: {
name: 'ПРОМ',
apiMode: 'bench', // 'bench' or 'backend'
// Optional headers for RAG backend
bearerToken: '',
systemPlatform: '',
systemPlatformUser: '',
// Backend mode settings
platformUserId: '',
platformId: '',
withClassify: false,
resetSessionMode: true
}
},
// UI settings
theme: 'light',
autoSaveDrafts: true,
requestTimeout: 1800000, // 30 minutes in milliseconds
// Query settings
defaultWithDocs: true,
defaultQueryMode: 'questions' // 'questions' or 'raw-json'
};
// Export for use in app.js
if (typeof module !== 'undefined' && module.exports) {
module.exports = defaultSettings;
}

1166
static/styles.css Normal file

File diff suppressed because it is too large Load Diff