Services: - api: FastAPI gateway with JWT auth, async endpoints, WebSocket - worker-gpu: CUDA sentence-transformers, FAISS IVFFlat, Ollama LLM - worker-indexer: Winnowing+MinHash plagiarism detection, PDF/DOCX extraction - worker-notifier: SMTP email notifications - worker-gost: GOST 7.1-2003 and GOST R 7.0.5-2008 formatting Infrastructure: - docker-compose.yml (production) + docker-compose.dev.yml (hot reload) - Nginx reverse proxy + WebSocket support - PostgreSQL 16 with Alembic migrations - Elasticsearch 8 with Russian/English analyzers - MinIO, RabbitMQ, Redis, Ollama Frontend: - React 18 + Vite + TypeScript + TailwindCSS + Zustand + React Query v5 - 9 pages: Home, Search, Cabinet, Task, Check, Bibliography, Pricing, Login, Register Scripts: - Parser stubs: OpenAlex, КиберЛенинка, arXiv (Phase 0 - to be filled) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
114 lines
4.8 KiB
Python
114 lines
4.8 KiB
Python
"""Точка входа FastAPI приложения — Академический помощник (anti-plagiarism)."""
|
|
|
|
import logging
|
|
from contextlib import asynccontextmanager
|
|
from typing import AsyncGenerator
|
|
|
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from app.api import auth, documents, reports, search, tasks
|
|
from app.config import settings
|
|
from app.core.websocket_manager import ws_manager
|
|
from app.database import engine
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
"""
|
|
Lifecycle менеджер приложения.
|
|
Выполняет инициализацию при старте и очистку при остановке.
|
|
"""
|
|
logger.info("Запуск Академического помощника...")
|
|
|
|
# Проверка соединений при старте
|
|
try:
|
|
async with engine.connect() as conn:
|
|
await conn.execute(__import__("sqlalchemy").text("SELECT 1"))
|
|
logger.info("PostgreSQL: соединение установлено")
|
|
except Exception as e:
|
|
logger.error(f"PostgreSQL: ошибка соединения: {e}")
|
|
|
|
yield
|
|
|
|
# Очистка при остановке
|
|
logger.info("Остановка приложения...")
|
|
await engine.dispose()
|
|
|
|
|
|
app = FastAPI(
|
|
title="Академический помощник API",
|
|
description=(
|
|
"Сервис антиплагиата и поиска академических источников. "
|
|
"Семантический поиск через FAISS GPU + Elasticsearch, "
|
|
"проверка плагиата в 4 уровня, ГОСТ библиография."
|
|
),
|
|
version="0.1.0",
|
|
lifespan=lifespan,
|
|
docs_url="/api/docs",
|
|
redoc_url="/api/redoc",
|
|
openapi_url="/api/openapi.json",
|
|
)
|
|
|
|
# ─── CORS ─────────────────────────────────────────────────────────────────────
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.CORS_ORIGINS,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# ─── Роутеры ──────────────────────────────────────────────────────────────────
|
|
app.include_router(auth.router, prefix="/api")
|
|
app.include_router(tasks.router, prefix="/api")
|
|
app.include_router(search.router, prefix="/api")
|
|
app.include_router(documents.router, prefix="/api")
|
|
app.include_router(reports.router, prefix="/api")
|
|
|
|
|
|
# ─── WebSocket ────────────────────────────────────────────────────────────────
|
|
@app.websocket("/ws/tasks/{task_id}")
|
|
async def websocket_task_updates(websocket: WebSocket, task_id: str) -> None:
|
|
"""
|
|
WebSocket endpoint для real-time обновлений статуса задачи.
|
|
|
|
Клиент подключается и получает обновления при изменении статуса задачи.
|
|
Соединение закрывается при получении статуса done/failed.
|
|
"""
|
|
await ws_manager.connect(task_id, websocket)
|
|
try:
|
|
while True:
|
|
# Ждём входящих сообщений (ping/pong для поддержания соединения)
|
|
data = await websocket.receive_text()
|
|
if data == "ping":
|
|
await websocket.send_text("pong")
|
|
except WebSocketDisconnect:
|
|
ws_manager.disconnect(task_id, websocket)
|
|
logger.info(f"WebSocket клиент отключился от задачи {task_id!r}")
|
|
|
|
|
|
# ─── Health check ──────────────────────────────────────────────────────────────
|
|
@app.get("/health", tags=["monitoring"])
|
|
async def health_check() -> JSONResponse:
|
|
"""Проверка работоспособности сервиса."""
|
|
return JSONResponse(
|
|
content={
|
|
"status": "ok",
|
|
"service": "api",
|
|
"version": "0.1.0",
|
|
}
|
|
)
|
|
|
|
|
|
@app.get("/", include_in_schema=False)
|
|
async def root() -> JSONResponse:
|
|
"""Корневой endpoint — редирект к документации."""
|
|
return JSONResponse(
|
|
content={"message": "Академический помощник API", "docs": "/api/docs"}
|
|
)
|