dp-zp-agent/app/main.py
2026-10-26 19:23:02 +01:00

192 lines
5.0 KiB
Python

from __future__ import annotations
import asyncio
import logging
import sys
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import (
FastAPI,
Request,
)
from fastapi.middleware.cors import (
CORSMiddleware,
)
from fastapi.exceptions import RequestValidationError
from fastapi.encoders import jsonable_encoder
from fastapi.exception_handlers import http_exception_handler, request_validation_exception_handler
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.responses import JSONResponse
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(
0,
str(PROJECT_ROOT),
)
from app.routes import router
from app.security import (
validate_security_configuration,
)
from tools.rag.embedding_utils import embed_query
from tools.retrieval.retrieval_contract import error_result
from tools.rag.common import DATA_DIR
from tools.sync.sync_jobs import SyncJobs
from tools.rag.rebuild_index import rebuild_index
logger = logging.getLogger(__name__)
OPENWEBUI_ORIGIN = (
"https://ui.tukekemt.xyz"
)
@asynccontextmanager
async def lifespan(
application: FastAPI,
):
validate_security_configuration()
application.state.embedding_ready = False
try:
await asyncio.to_thread(
embed_query,
"warmup",
)
application.state.embedding_ready = True
except Exception:
logger.exception(
"Predhriatie embedding modelu "
"zlyhalo. API pokračuje bez "
"úspešného warm-upu."
)
def run_sync(**kwargs):
result = rebuild_index(**kwargs)
application.state.embedding_ready = True
return result
application.state.sync_jobs = SyncJobs(DATA_DIR, runner=run_sync)
application.state.sync_jobs.start()
try:
yield
finally:
await asyncio.to_thread(application.state.sync_jobs.stop)
app = FastAPI(
title="ZP Agent API",
description=(
"RAG API pre vyhľadávanie "
"a získavanie informácií "
"z repozitára záverečných prác "
"ZP Wiki."
),
version="0.9.0",
lifespan=lifespan,
docs_url=None,
redoc_url=None,
)
@app.exception_handler(StarletteHTTPException)
async def retrieval_http_error(request: Request, error: StarletteHTTPException):
if request.url.path not in {'/rag', '/search'}:
return await http_exception_handler(request, error)
reason = {400: 'invalid_query', 401: 'unauthorized', 403: 'forbidden',
404: 'not_found', 405: 'method_not_allowed', 422: 'invalid_request',
503: 'retrieval_unavailable'}.get(error.status_code, 'internal_error')
return JSONResponse(status_code=error.status_code, headers=error.headers,
content={**error_result(reason=reason), 'detail': error.detail})
@app.exception_handler(RequestValidationError)
async def retrieval_validation_error(request: Request, error: RequestValidationError):
if request.url.path not in {'/rag', '/search'}:
return await request_validation_exception_handler(request, error)
# Retain useful validation locations/types without echoing submitted secrets.
details = [{'loc': row['loc'], 'type': row['type'], 'msg': row['msg']} for row in error.errors()]
return JSONResponse(status_code=422, content=jsonable_encoder({
**error_result(reason='invalid_request'), 'detail': details}))
app.add_middleware(
CORSMiddleware,
allow_origins=[
OPENWEBUI_ORIGIN,
],
allow_credentials=True,
allow_methods=[
"GET",
"POST",
"OPTIONS",
],
allow_headers=[
"Authorization",
"Content-Type",
"X-API-Key",
"X-Session-Id",
"X-OpenWebUI-Chat-Id",
"X-OpenWebUI-Message-Id",
"X-OpenWebUI-User-Id",
"X-OpenWebUI-User-Name",
"X-OpenWebUI-User-Email",
"X-OpenWebUI-User-Role",
"X-OpenWebUI-User-Jwt",
"X-OpenWebUI-User-Message-Id",
"X-OpenWebUI-User-Message-Parent-Id",
"X-OpenWebUI-Task",
],
allow_private_network=True,
)
@app.middleware(
"http"
)
async def add_security_headers(
request: Request,
call_next,
):
try:
response = await call_next(request)
except Exception:
if request.url.path not in {'/rag', '/search'}:
raise
# Serialization failures must obey the same contract as backend errors.
logger.exception('Neočakávaná chyba výstupu retrieval API.')
response = JSONResponse(status_code=500, content={
**error_result(reason='internal_error'),
'detail': 'Nastala interná chyba servera.'})
response.headers[
"Cache-Control"
] = "no-store"
response.headers[
"X-Content-Type-Options"
] = "nosniff"
response.headers[
"Referrer-Policy"
] = "no-referrer"
response.headers[
"X-Frame-Options"
] = "DENY"
return response
app.include_router(
router
)