Files
SQLBot/backend/main.py
T
2025-12-11 15:20:49 +08:00

214 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
from typing import Dict, Any
import sqlbot_xpack
from alembic.config import Config
from fastapi import FastAPI, Request
from fastapi.concurrency import asynccontextmanager
from fastapi.openapi.utils import get_openapi
from fastapi.responses import JSONResponse
from fastapi.routing import APIRoute
from fastapi.staticfiles import StaticFiles
from fastapi_mcp import FastApiMCP
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.middleware.cors import CORSMiddleware
from alembic import command
from apps.api import api_router
from apps.swagger.i18n import PLACEHOLDER_PREFIX, tags_metadata, i18n_list
from apps.swagger.i18n import get_translation, DEFAULT_LANG
from apps.system.crud.aimodel_manage import async_model_info
from apps.system.crud.assistant import init_dynamic_cors
from apps.system.middleware.auth import TokenMiddleware
from common.core.config import settings
from common.core.response_middleware import ResponseMiddleware, exception_handler
from common.core.sqlbot_cache import init_sqlbot_cache
from common.utils.embedding_threads import fill_empty_terminology_embeddings, fill_empty_data_training_embeddings, \
fill_empty_table_and_ds_embeddings
from common.utils.utils import SQLBotLogUtil
def run_migrations():
alembic_cfg = Config("alembic.ini")
command.upgrade(alembic_cfg, "head")
def init_terminology_embedding_data():
fill_empty_terminology_embeddings()
def init_data_training_embedding_data():
fill_empty_data_training_embeddings()
def init_table_and_ds_embedding():
fill_empty_table_and_ds_embeddings()
@asynccontextmanager
async def lifespan(app: FastAPI):
run_migrations()
init_sqlbot_cache()
init_dynamic_cors(app)
init_terminology_embedding_data()
init_data_training_embedding_data()
init_table_and_ds_embedding()
SQLBotLogUtil.info("✅ SQLBot 初始化完成")
await sqlbot_xpack.core.clean_xpack_cache()
await async_model_info() # 异步加密已有模型的密钥和地址
await sqlbot_xpack.core.monitor_app(app)
yield
SQLBotLogUtil.info("SQLBot 应用关闭")
def custom_generate_unique_id(route: APIRoute) -> str:
tag = route.tags[0] if route.tags and len(route.tags) > 0 else ""
return f"{tag}-{route.name}"
app = FastAPI(
title=settings.PROJECT_NAME,
openapi_url=f"{settings.API_V1_STR}/openapi.json",
generate_unique_id_function=custom_generate_unique_id,
lifespan=lifespan,
docs_url=None,
redoc_url=None
)
# cache docs for different text
_openapi_cache: Dict[str, Dict[str, Any]] = {}
# replace placeholder
def replace_placeholders_in_schema(schema: Dict[str, Any], trans: Dict[str, str]) -> None:
"""
search OpenAPI schema,replace PLACEHOLDER_xxx to text。
"""
if isinstance(schema, dict):
for key, value in schema.items():
if isinstance(value, str) and value.startswith(PLACEHOLDER_PREFIX):
placeholder_key = value[len(PLACEHOLDER_PREFIX):]
schema[key] = trans.get(placeholder_key, value)
else:
replace_placeholders_in_schema(value, trans)
elif isinstance(schema, list):
for item in schema:
replace_placeholders_in_schema(item, trans)
# OpenAPI build
def get_language_from_request(request: Request) -> str:
# get param from query ?lang=zh
lang = request.query_params.get("lang")
if lang in i18n_list:
return lang
# get lang from Accept-Language Header
accept_lang = request.headers.get("accept-language", "")
if "zh" in accept_lang.lower():
return "zh"
return DEFAULT_LANG
def generate_openapi_for_lang(lang: str) -> Dict[str, Any]:
if lang in _openapi_cache:
return _openapi_cache[lang]
# tags metadata
trans = get_translation(lang)
localized_tags = []
for tag in tags_metadata:
desc = tag["description"]
if desc.startswith(PLACEHOLDER_PREFIX):
key = desc[len(PLACEHOLDER_PREFIX):]
desc = trans.get(key, desc)
localized_tags.append({
"name": tag["name"],
"description": desc
})
# 1. create OpenAPI
openapi_schema = get_openapi(
title="SQLBot API Document" if lang == "en" else "SQLBot API 文档",
version="1.0.0",
routes=app.routes,
tags=localized_tags
)
# openapi version
openapi_schema.setdefault("openapi", "3.1.0")
# 2. get trans for lang
trans = get_translation(lang)
# 3. replace placeholder
replace_placeholders_in_schema(openapi_schema, trans)
# 4. cache
_openapi_cache[lang] = openapi_schema
return openapi_schema
# custom /openapi.json and /docs
@app.get("/openapi.json", include_in_schema=False)
async def custom_openapi(request: Request):
lang = get_language_from_request(request)
schema = generate_openapi_for_lang(lang)
return JSONResponse(schema)
@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui(request: Request):
lang = get_language_from_request(request)
from fastapi.openapi.docs import get_swagger_ui_html
return get_swagger_ui_html(
openapi_url=f"/openapi.json?lang={lang}",
title="SQLBot API Docs",
swagger_favicon_url="https://fastapi.tiangolo.com/img/favicon.png",
)
mcp_app = FastAPI()
# mcp server, images path
images_path = settings.MCP_IMAGE_PATH
os.makedirs(images_path, exist_ok=True)
mcp_app.mount("/images", StaticFiles(directory=images_path), name="images")
mcp = FastApiMCP(
app,
name="SQLBot MCP Server",
description="SQLBot MCP Server",
describe_all_responses=True,
describe_full_response_schema=True,
include_operations=["get_datasource_list", "get_model_list", "mcp_question", "mcp_start", "mcp_assistant"]
)
mcp.mount(mcp_app)
# Set all CORS enabled origins
if settings.all_cors_origins:
app.add_middleware(
CORSMiddleware,
allow_origins=settings.all_cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(TokenMiddleware)
app.add_middleware(ResponseMiddleware)
app.include_router(api_router, prefix=settings.API_V1_STR)
# Register exception handlers
app.add_exception_handler(StarletteHTTPException, exception_handler.http_exception_handler)
app.add_exception_handler(Exception, exception_handler.global_exception_handler)
mcp.setup_server()
sqlbot_xpack.init_fastapi_app(app)
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
# uvicorn.run("main:mcp_app", host="0.0.0.0", port=8001) # mcp server