51dc8dc4f6
- FastAPI + Qdrant + Redis + Ollama 技术栈 - L1→L2→L3→chunk 四层分层检索(dense + sparse RRF 融合) - 文档三级总结与 2.5 级回退 - query 解析路由与分类 - /admin 管理页面
80 lines
2.6 KiB
Python
80 lines
2.6 KiB
Python
from collections.abc import AsyncIterator
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
|
|
import structlog
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.exceptions import RequestValidationError
|
|
from fastapi.responses import FileResponse, JSONResponse
|
|
|
|
from app.api.response import ApiError, error
|
|
from app.api.v1.document import router as document_router
|
|
from app.api.v1.knowledge import router as knowledge_router
|
|
from app.api.v1.search import router as search_router
|
|
from app.config import settings
|
|
from app.services.qdrant import QdrantService
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
|
|
"""应用生命周期:启动时初始化 Qdrant 集合
|
|
|
|
初始化失败仅记录日志、不阻止启动(本地开发可能无 Qdrant)。
|
|
"""
|
|
try:
|
|
await QdrantService().ensure_collections()
|
|
logger.info("Qdrant 集合初始化完成")
|
|
except Exception:
|
|
logger.error("Qdrant 集合初始化失败,跳过初始化继续启动")
|
|
yield
|
|
|
|
|
|
app = FastAPI(
|
|
title=settings.app_name,
|
|
description="AI Agent 分层信息检索服务",
|
|
version="0.1.0",
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
app.include_router(search_router)
|
|
app.include_router(document_router)
|
|
app.include_router(knowledge_router)
|
|
|
|
|
|
@app.exception_handler(ApiError)
|
|
async def api_error_handler(request: Request, exc: ApiError) -> JSONResponse:
|
|
"""业务异常 → 统一错误响应(code 取自异常)"""
|
|
return JSONResponse(content=error(exc.code, exc.message))
|
|
|
|
|
|
@app.exception_handler(RequestValidationError)
|
|
async def validation_error_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
|
|
"""请求参数校验失败 → code 1001"""
|
|
errors = exc.errors()
|
|
message = f"请求参数校验失败: {errors[0]['msg']}" if errors else "请求参数校验失败"
|
|
return JSONResponse(content=error(1001, message))
|
|
|
|
|
|
@app.exception_handler(Exception)
|
|
async def unhandled_error_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
"""未捕获异常 → code 2000"""
|
|
logger.exception("未捕获异常", path=request.url.path)
|
|
return JSONResponse(content=error(2000, "服务器内部错误"))
|
|
|
|
|
|
@app.get("/api/v1/health")
|
|
async def health() -> dict[str, str]:
|
|
"""健康检查"""
|
|
return {"status": "ok"}
|
|
|
|
|
|
_ADMIN_HTML = Path(__file__).resolve().parent / "static" / "admin.html"
|
|
|
|
|
|
@app.get("/admin", include_in_schema=False)
|
|
async def admin_page() -> FileResponse:
|
|
"""管理后台单页(单文件静态 HTML,零外部依赖)"""
|
|
return FileResponse(_ADMIN_HTML, media_type="text/html")
|