Update file rag_server.py
This commit is contained in:
@@ -10,6 +10,7 @@ RAG-сервер – отдельный HTTP-сервис для обработ
|
||||
- Эндпоинт /rag/transcribe для транскрибации аудио (SaluteSpeech)
|
||||
- Использование Pydantic-модели AppConfig для конфигурации.
|
||||
- Аутентификация через API-ключ (X-API-Key)
|
||||
- Документирование API через OpenAPI (Swagger UI и ReDoc)
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -26,6 +27,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Depends, UploadFile, File as FastAPIFile
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
from pydantic import BaseModel, Field
|
||||
import uvicorn
|
||||
|
||||
@@ -53,14 +55,14 @@ class QueryRequest(BaseModel):
|
||||
Модель запроса к RAG-серверу на выполнение RAG-запроса.
|
||||
ИСТОРИЯ НЕ ПЕРЕДАЁТСЯ – сервер получает её из БД.
|
||||
"""
|
||||
query: str = Field(..., description="Текст запроса пользователя")
|
||||
user_jid: str = Field(..., description="JID пользователя (без ресурса)")
|
||||
room_jid: Optional[str] = Field(None, description="JID комнаты (None для личного чата)")
|
||||
query: str = Field(..., description="Текст запроса пользователя", example="Какой OEE на линии?")
|
||||
user_jid: str = Field(..., description="JID пользователя (без ресурса)", example="user@domain.ru")
|
||||
room_jid: Optional[str] = Field(None, description="JID комнаты (None для личного чата)", example="room@conference.domain.ru")
|
||||
prompts: Dict[str, str] = Field(
|
||||
default_factory=dict,
|
||||
description="Словарь промптов (system, synthesis, intent, expand, critique и т.д.)"
|
||||
)
|
||||
intent_override: Optional[str] = Field(None, description="Принудительное переопределение намерения")
|
||||
intent_override: Optional[str] = Field(None, description="Принудительное переопределение намерения", example="METRICS")
|
||||
last_file_path: Optional[str] = Field(None, description="Путь к последнему загруженному файлу")
|
||||
last_file_text: Optional[str] = Field(None, description="Текст последнего загруженного файла")
|
||||
|
||||
@@ -77,9 +79,9 @@ class QueryResponse(BaseModel):
|
||||
|
||||
class IndexRequest(BaseModel):
|
||||
"""Модель запроса на индексацию документа."""
|
||||
file_name: str = Field(..., description="Исходное имя файла")
|
||||
file_name: str = Field(..., description="Исходное имя файла", example="report.docx")
|
||||
file_text: str = Field(..., description="Извлечённый текст документа")
|
||||
user_jid: str = Field(..., description="JID владельца документа")
|
||||
user_jid: str = Field(..., description="JID владельца документа", example="user@domain.ru")
|
||||
room_jid: Optional[str] = Field(None, description="JID комнаты (если документ комнатный)")
|
||||
is_global: bool = Field(False, description="Глобальный ли документ")
|
||||
title: Optional[str] = Field(None, description="Отображаемое название")
|
||||
@@ -211,16 +213,57 @@ def init_orchestrator(config):
|
||||
|
||||
|
||||
# ============================================================
|
||||
# FASTAPI ПРИЛОЖЕНИЕ
|
||||
# FASTAPI ПРИЛОЖЕНИЕ с документацией
|
||||
# ============================================================
|
||||
|
||||
# Описание тегов для группировки эндпоинтов в документации
|
||||
tags_metadata = [
|
||||
{
|
||||
"name": "RAG",
|
||||
"description": "Основные RAG-операции: выполнение запроса и индексация документов",
|
||||
},
|
||||
{
|
||||
"name": "Media",
|
||||
"description": "Обработка медиафайлов: OCR для изображений и транскрибация аудио",
|
||||
},
|
||||
{
|
||||
"name": "Health",
|
||||
"description": "Проверка состояния сервиса",
|
||||
}
|
||||
]
|
||||
|
||||
app = FastAPI(
|
||||
title="RAG-сервер",
|
||||
description="Единый сервис для RAG-обработки запросов и индексации документов",
|
||||
version="3.0.0"
|
||||
title="RAG-сервер Эфцекабот",
|
||||
description="""
|
||||
Единый сервис для RAG-обработки запросов и индексации документов.
|
||||
|
||||
## Аутентификация
|
||||
Для всех защищённых эндпоинтов (кроме `/health`) требуется передача API-ключа
|
||||
в заголовке `X-API-Key`.
|
||||
|
||||
## История диалога
|
||||
Сервер самостоятельно хранит историю диалогов в PostgreSQL и использует её
|
||||
для контекста при ответах. Клиентам не нужно передавать историю в запросах.
|
||||
|
||||
## Поддерживаемые форматы файлов
|
||||
- Документы: DOCX, PDF, XLSX, PPTX, TXT, CSV, JSON
|
||||
- Изображения: JPEG, PNG, BMP, TIFF (OCR через Tesseract)
|
||||
- Аудио: OGG, WAV, MP3, AMR, M4A (транскрибация через SaluteSpeech)
|
||||
- Архивы: ZIP, 7z (рекурсивная распаковка)
|
||||
""",
|
||||
version="3.0.0",
|
||||
openapi_tags=tags_metadata,
|
||||
contact={
|
||||
"name": "Андрей Марков, Вениамин Кокорин",
|
||||
"email": "support@fckbot.ru",
|
||||
},
|
||||
license_info={
|
||||
"name": "ISC",
|
||||
"url": "https://opensource.org/licenses/ISC",
|
||||
},
|
||||
)
|
||||
|
||||
# Разрешаем CORS для возможности запросов с других доменов (для тестов)
|
||||
# Добавляем CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
@@ -229,6 +272,46 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Настраиваем OpenAPI для отображения схемы авторизации
|
||||
def custom_openapi():
|
||||
if app.openapi_schema:
|
||||
return app.openapi_schema
|
||||
|
||||
openapi_schema = get_openapi(
|
||||
title=app.title,
|
||||
version=app.version,
|
||||
description=app.description,
|
||||
routes=app.routes,
|
||||
tags=app.openapi_tags,
|
||||
contact=app.contact,
|
||||
license_info=app.license_info,
|
||||
)
|
||||
|
||||
# Добавляем схему безопасности для API-ключа
|
||||
openapi_schema["components"]["securitySchemes"] = {
|
||||
"APIKeyHeader": {
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-API-Key",
|
||||
"description": "API-ключ для доступа к защищённым эндпоинтам",
|
||||
}
|
||||
}
|
||||
|
||||
# Применяем security ко всем эндпоинтам кроме /health
|
||||
for path in openapi_schema["paths"]:
|
||||
if path != "/health":
|
||||
for method in openapi_schema["paths"][path]:
|
||||
openapi_schema["paths"][path][method]["security"] = [{"APIKeyHeader": []}]
|
||||
|
||||
app.openapi_schema = openapi_schema
|
||||
return app.openapi_schema
|
||||
|
||||
app.openapi = custom_openapi
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ОБРАБОТЧИКИ СОБЫТИЙ
|
||||
# ============================================================
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
@@ -258,7 +341,19 @@ async def shutdown_event():
|
||||
# ЭНДПОИНТЫ API (все защищены аутентификацией, кроме /health)
|
||||
# ============================================================
|
||||
|
||||
@app.post("/rag/query", response_model=QueryResponse)
|
||||
@app.post(
|
||||
"/rag/query",
|
||||
response_model=QueryResponse,
|
||||
tags=["RAG"],
|
||||
summary="Выполнить RAG-запрос",
|
||||
description="Отправляет запрос пользователя в RAG-пайплайн. "
|
||||
"Сервер автоматически подгружает историю диалога из БД.",
|
||||
responses={
|
||||
200: {"description": "Успешный ответ", "model": QueryResponse},
|
||||
403: {"description": "Неверный или отсутствующий API-ключ"},
|
||||
500: {"description": "Внутренняя ошибка сервера"},
|
||||
}
|
||||
)
|
||||
async def rag_query(request: QueryRequest, _: str = Depends(verify_api_key)):
|
||||
"""
|
||||
Выполняет RAG-запрос.
|
||||
@@ -289,7 +384,20 @@ async def rag_query(request: QueryRequest, _: str = Depends(verify_api_key)):
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/rag/index", response_model=IndexResponse)
|
||||
@app.post(
|
||||
"/rag/index",
|
||||
response_model=IndexResponse,
|
||||
tags=["RAG"],
|
||||
summary="Проиндексировать документ",
|
||||
description="Загружает текст документа в базу знаний. "
|
||||
"Документ разбивается на чанки, для каждого генерируются эмбеддинги "
|
||||
"и сохраняются в Qdrant. Метаданные хранятся в PostgreSQL.",
|
||||
responses={
|
||||
200: {"description": "Индексация выполнена", "model": IndexResponse},
|
||||
403: {"description": "Неверный или отсутствующий API-ключ"},
|
||||
500: {"description": "Внутренняя ошибка сервера"},
|
||||
}
|
||||
)
|
||||
async def index_document(request: IndexRequest, _: str = Depends(verify_api_key)):
|
||||
"""Индексирует документ в базу знаний."""
|
||||
orchestrator = get_orchestrator()
|
||||
@@ -316,7 +424,17 @@ async def index_document(request: IndexRequest, _: str = Depends(verify_api_key)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
@app.get(
|
||||
"/health",
|
||||
tags=["Health"],
|
||||
summary="Проверка состояния сервиса",
|
||||
description="Возвращает статус работоспособности RAG-сервера. "
|
||||
"Не требует аутентификации.",
|
||||
responses={
|
||||
200: {"description": "Сервис работает", "content": {"application/json": {"example": {"status": "healthy"}}}},
|
||||
503: {"description": "Сервис недоступен"},
|
||||
}
|
||||
)
|
||||
async def health_check():
|
||||
"""Проверка здоровья сервиса (без аутентификации)."""
|
||||
if _orchestrator is None:
|
||||
@@ -324,7 +442,20 @@ async def health_check():
|
||||
return {"status": "healthy"}
|
||||
|
||||
|
||||
@app.post("/rag/vision", response_model=VisionResponse)
|
||||
@app.post(
|
||||
"/rag/vision",
|
||||
response_model=VisionResponse,
|
||||
tags=["Media"],
|
||||
summary="Распознать текст на изображении (OCR)",
|
||||
description="Отправляет изображение на распознавание текста через Tesseract OCR. "
|
||||
"Поддерживаются форматы: JPEG, PNG, BMP, TIFF.",
|
||||
responses={
|
||||
200: {"description": "Текст распознан", "model": VisionResponse},
|
||||
400: {"description": "Неподдерживаемый формат файла"},
|
||||
403: {"description": "Неверный или отсутствующий API-ключ"},
|
||||
500: {"description": "Внутренняя ошибка сервера"},
|
||||
}
|
||||
)
|
||||
async def vision_endpoint(
|
||||
file: UploadFile = FastAPIFile(...),
|
||||
_: str = Depends(verify_api_key)
|
||||
@@ -364,7 +495,20 @@ async def vision_endpoint(
|
||||
os.unlink(tmp_path)
|
||||
|
||||
|
||||
@app.post("/rag/transcribe", response_model=TranscribeResponse)
|
||||
@app.post(
|
||||
"/rag/transcribe",
|
||||
response_model=TranscribeResponse,
|
||||
tags=["Media"],
|
||||
summary="Транскрибировать аудиофайл",
|
||||
description="Отправляет аудиофайл на распознавание речи через SaluteSpeech. "
|
||||
"Поддерживаются форматы: OGG, WAV, MP3, AMR, M4A.",
|
||||
responses={
|
||||
200: {"description": "Речь распознана", "model": TranscribeResponse},
|
||||
400: {"description": "Неподдерживаемый формат файла"},
|
||||
403: {"description": "Неверный или отсутствующий API-ключ"},
|
||||
500: {"description": "Внутренняя ошибка сервера"},
|
||||
}
|
||||
)
|
||||
async def transcribe_endpoint(
|
||||
file: UploadFile = FastAPIFile(...),
|
||||
_: str = Depends(verify_api_key)
|
||||
|
||||
Reference in New Issue
Block a user