Update 2 files

- /requirements.txt
- /rag/rag_server.py
This commit is contained in:
Markov Andrey
2026-06-30 16:03:15 +00:00
parent eea2bbb62d
commit 41f32964fe
2 changed files with 85 additions and 62 deletions

View File

@@ -6,9 +6,8 @@ RAG-сервер отдельный HTTP-сервис для обработ
ИСТОРИЯ ХРАНИТСЯ НА СЕРВЕРЕ (в PostgreSQL). ИСТОРИЯ ХРАНИТСЯ НА СЕРВЕРЕ (в PostgreSQL).
ДОБАВЛЕНО: ДОБАВЛЕНО:
- Эндпоинт /rag/vision для распознавания текста на изображениях (OCR) - Rate limiting через slowapi (защита от перегрузки)
- Эндпоинт /rag/transcribe для транскрибации аудио (SaluteSpeech) - Централизованное логирование всех запросов (middleware)
- Использование Pydantic-модели AppConfig для конфигурации.
- Аутентификация через API-ключ (X-API-Key) - Аутентификация через API-ключ (X-API-Key)
- Полная OpenAPI-документация с примерами и тегами - Полная OpenAPI-документация с примерами и тегами
""" """
@@ -19,18 +18,24 @@ import sys
import tempfile import tempfile
import shutil import shutil
import asyncio import asyncio
import time
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, List, Any from typing import Optional, Dict, List, Any
# Добавляем путь к rag, если запускаем из корня проекта # Добавляем путь к rag, если запускаем из корня проекта
sys.path.insert(0, str(Path(__file__).parent.parent)) sys.path.insert(0, str(Path(__file__).parent.parent))
from fastapi import FastAPI, HTTPException, Depends, UploadFile, File as FastAPIFile, status from fastapi import FastAPI, HTTPException, Depends, UploadFile, File as FastAPIFile, Request, status
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.openapi.utils import get_openapi from fastapi.openapi.utils import get_openapi
from pydantic import BaseModel, Field, HttpUrl, constr from pydantic import BaseModel, Field
import uvicorn import uvicorn
# Rate limiting
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
# Импорты наших модулей # Импорты наших модулей
from .utils.config_loader import load_config from .utils.config_loader import load_config
from .utils.logger import setup_logging from .utils.logger import setup_logging
@@ -336,10 +341,13 @@ def init_orchestrator(config):
# ============================================================ # ============================================================
# FASTAPI ПРИЛОЖЕНИЕ С КАСТОМНОЙ ОPENAPI # FASTAPI ПРИЛОЖЕНИЕ С КАСТОМНОЙ OPENAPI, RATE LIMITING И ЛОГИРОВАНИЕМ
# ============================================================ # ============================================================
# Создаём приложение с базовыми метаданными # 1. Настройка rate limiter
limiter = Limiter(key_func=get_remote_address)
# 2. Создание приложения
app = FastAPI( app = FastAPI(
title="Эфцекабот RAG-сервер", title="Эфцекабот RAG-сервер",
description=""" description="""
@@ -357,6 +365,12 @@ app = FastAPI(
### Аутентификация ### Аутентификация
Все эндпоинты (кроме `/health`) требуют передачи API-ключа в заголовке `X-API-Key`. Все эндпоинты (кроме `/health`) требуют передачи API-ключа в заголовке `X-API-Key`.
### Rate Limiting
- `/rag/query`: 10 запросов в минуту
- `/rag/index`: 5 запросов в минуту
- `/rag/vision`: 15 запросов в минуту
- `/rag/transcribe`: 10 запросов в минуту
""", """,
version="3.0.0", version="3.0.0",
contact={ contact={
@@ -372,7 +386,11 @@ app = FastAPI(
openapi_url="/openapi.json", openapi_url="/openapi.json",
) )
# Разрешаем CORS для возможности запросов с других доменов (для тестов) # 3. Подключаем обработчик ошибок rate limiting
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
# 4. CORS
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=["*"],
@@ -381,8 +399,22 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
# 5. Централизованное логирование запросов (middleware)
@app.middleware("http")
async def log_requests(request: Request, call_next):
"""Логирует все входящие запросы с временем выполнения и IP-адресом."""
start_time = time.time()
response = await call_next(request)
duration = time.time() - start_time
client_host = request.client.host if request.client else "unknown"
logger.info(
f"{request.method} {request.url.path}{response.status_code} "
f"({duration:.3f}s) from {client_host}"
)
return response
# Кастомизируем OpenAPI-схему для добавления примеров и кодов ошибок
# 6. Кастомизация OpenAPI
def custom_openapi(): def custom_openapi():
if app.openapi_schema: if app.openapi_schema:
return app.openapi_schema return app.openapi_schema
@@ -405,17 +437,10 @@ def custom_openapi():
"description": "API-ключ для доступа к сервису. Получите у администратора.", "description": "API-ключ для доступа к сервису. Получите у администратора.",
} }
} }
# Добавляем глобальную security requirement
openapi_schema["security"] = [{"APIKeyHeader": []}] openapi_schema["security"] = [{"APIKeyHeader": []}]
# Исключаем /health из требований безопасности
# (это делается через параметр `include_in_schema=False` в эндпоинте)
app.openapi_schema = openapi_schema app.openapi_schema = openapi_schema
return app.openapi_schema return app.openapi_schema
app.openapi = custom_openapi app.openapi = custom_openapi
@@ -448,30 +473,9 @@ async def shutdown_event():
# ============================================================ # ============================================================
# ЭНДПОИНТЫ API С ТЕГАМИ И ПОДРОБНЫМИ ОПИСАНИЯМИ # ЭНДПОИНТЫ API С ТЕГАМИ, ПОДРОБНЫМИ ОПИСАНИЯМИ И RATE LIMITING
# ============================================================ # ============================================================
# Теги для группировки в документации
tags_metadata = [
{
"name": "RAG",
"description": "Основные RAG-операции: запросы и индексация документов",
},
{
"name": "Vision",
"description": "Распознавание текста на изображениях (OCR)",
},
{
"name": "Audio",
"description": "Транскрибация аудиофайлов (SaluteSpeech)",
},
{
"name": "Health",
"description": "Проверка состояния сервиса",
},
]
@app.post( @app.post(
"/rag/query", "/rag/query",
response_model=QueryResponse, response_model=QueryResponse,
@@ -490,24 +494,27 @@ tags_metadata = [
**Требования**: заголовок `X-API-Key` с валидным ключом. **Требования**: заголовок `X-API-Key` с валидным ключом.
**История**: сервер автоматически получает историю диалога из БД. **История**: сервер автоматически получает историю диалога из БД.
**Rate limit**: 10 запросов в минуту.
""", """,
response_description="Ответ содержит сгенерированный текст, намерение, источники и уверенность" response_description="Ответ содержит сгенерированный текст, намерение, источники и уверенность"
) )
async def rag_query(request: QueryRequest, _: str = Depends(verify_api_key)): @limiter.limit("10/minute")
""" async def rag_query(
Выполняет RAG-запрос. request: Request, # обязательный параметр для slowapi
""" query_req: QueryRequest,
_: str = Depends(verify_api_key)
):
orchestrator = get_orchestrator() orchestrator = get_orchestrator()
try: try:
result = await orchestrator.process_query( result = await orchestrator.process_query(
query=request.query, query=query_req.query,
user_jid=request.user_jid, user_jid=query_req.user_jid,
room_jid=request.room_jid, room_jid=query_req.room_jid,
prompts=request.prompts, prompts=query_req.prompts,
intent_override=request.intent_override, intent_override=query_req.intent_override,
last_file_path=request.last_file_path, last_file_path=query_req.last_file_path,
last_file_text=request.last_file_text last_file_text=query_req.last_file_text
) )
return QueryResponse( return QueryResponse(
answer=result.get('answer', ''), answer=result.get('answer', ''),
@@ -546,24 +553,30 @@ async def rag_query(request: QueryRequest, _: str = Depends(verify_api_key)):
**Обновление:** если документ с таким именем уже существует в комнате, **Обновление:** если документ с таким именем уже существует в комнате,
он будет заменён новой версией (по хешу). он будет заменён новой версией (по хешу).
**Rate limit**: 5 запросов в минуту.
""", """,
response_description="Возвращает идентификатор документа и количество чанков" response_description="Возвращает идентификатор документа и количество чанков"
) )
async def index_document(request: IndexRequest, _: str = Depends(verify_api_key)): @limiter.limit("5/minute")
"""Индексирует документ в базу знаний.""" async def index_document(
request: Request,
index_req: IndexRequest,
_: str = Depends(verify_api_key)
):
orchestrator = get_orchestrator() orchestrator = get_orchestrator()
try: try:
result = await orchestrator.index_document( result = await orchestrator.index_document(
file_name=request.file_name, file_name=index_req.file_name,
file_text=request.file_text, file_text=index_req.file_text,
user_jid=request.user_jid, user_jid=index_req.user_jid,
room_jid=request.room_jid, room_jid=index_req.room_jid,
is_global=request.is_global, is_global=index_req.is_global,
title=request.title, title=index_req.title,
metadata=request.metadata, metadata=index_req.metadata,
file_hash=request.file_hash, file_hash=index_req.file_hash,
update_if_exists=request.update_if_exists update_if_exists=index_req.update_if_exists
) )
return IndexResponse( return IndexResponse(
doc_id=result.get('doc_id'), doc_id=result.get('doc_id'),
@@ -594,10 +607,13 @@ async def index_document(request: IndexRequest, _: str = Depends(verify_api_key)
- Статус: `ok`, `no_text_found`, `error` - Статус: `ok`, `no_text_found`, `error`
**Требования:** заголовок `X-API-Key`. **Требования:** заголовок `X-API-Key`.
**Rate limit**: 15 запросов в минуту.
""", """,
response_description="Текст, распознанный на изображении" response_description="Текст, распознанный на изображении"
) )
@limiter.limit("15/minute")
async def vision_endpoint( async def vision_endpoint(
request: Request,
file: UploadFile = FastAPIFile( file: UploadFile = FastAPIFile(
..., ...,
description="Файл изображения (JPEG, PNG, BMP, TIFF)" description="Файл изображения (JPEG, PNG, BMP, TIFF)"
@@ -656,10 +672,14 @@ async def vision_endpoint(
**Возвращает:** **Возвращает:**
- Распознанный текст - Распознанный текст
- Статус: `ok`, `no_speech_detected`, `error` - Статус: `ok`, `no_speech_detected`, `error`
**Rate limit**: 10 запросов в минуту.
""", """,
response_description="Текст, распознанный из аудио" response_description="Текст, распознанный из аудио"
) )
@limiter.limit("10/minute")
async def transcribe_endpoint( async def transcribe_endpoint(
request: Request,
file: UploadFile = FastAPIFile( file: UploadFile = FastAPIFile(
..., ...,
description="Аудиофайл (OGG, WAV, MP3, AMR, M4A)" description="Аудиофайл (OGG, WAV, MP3, AMR, M4A)"
@@ -705,7 +725,7 @@ async def transcribe_endpoint(
- `healthy` сервер работает - `healthy` сервер работает
- `unhealthy` оркестратор не инициализирован - `unhealthy` оркестратор не инициализирован
**Примечание:** этот эндпоинт **не требует** аутентификации. **Примечание:** этот эндпоинт **не требует** аутентификации и **не имеет** rate limiting.
""", """,
response_description="Статус сервиса" response_description="Статус сервиса"
) )

View File

@@ -65,3 +65,6 @@ uvicorn >= 0.30.0
# Для CORS поддержки # Для CORS поддержки
python-multipart >= 0.0.9 python-multipart >= 0.0.9
# Защита от перегрузки эндпоинтов
slowapi>=0.1.0