202 lines
6.1 KiB
Python
202 lines
6.1 KiB
Python
"""FastAPI 入口。挂载路由,放行前端跨域,启动时同步接口定义。
|
|
|
|
启动: uv run --with-requirements requirements.txt uvicorn app:app --reload --port 8000
|
|
|
|
路由分组(对齐 dograh 的 routes/ 结构):
|
|
/health 健康检查
|
|
/api/assistants 助手 CRUD
|
|
/api/interface-definitions 接口定义
|
|
/api/model-resources 模型资源 CRUD
|
|
/ws/voice WebRTC 输出(浏览器)
|
|
/ws/stream WS 输出(裸音频流)
|
|
/api/webrtc/ice-servers WebRTC STUN/TURN 配置
|
|
"""
|
|
|
|
import asyncio
|
|
import os
|
|
|
|
# NLTK 3.9+ blocks dependency imports when .venv lives under cwd (local dev layout).
|
|
# pipecat -> nltk -> regex hits this false positive; disable before any nltk import.
|
|
os.environ.setdefault("NLTK_DISABLE_IMPORT_SECURITY", "1")
|
|
|
|
from contextlib import asynccontextmanager
|
|
|
|
import settings
|
|
import uvicorn
|
|
from db.session import sync_default_tools, sync_interface_definitions
|
|
from loguru import logger
|
|
from services.openai_api import (
|
|
PUBLIC_API_NAME,
|
|
RequestIDMiddleware,
|
|
openai_error_response,
|
|
)
|
|
from services.knowledge import recover_interrupted_documents
|
|
from services.openai_responses.state import recover_interrupted_responses
|
|
from services.post_call.worker import recover_interrupted_analyses, run_analysis_worker
|
|
from services.webhooks.worker import recover_interrupted_webhooks, run_webhook_worker
|
|
from services.test_runs.orchestrator import (
|
|
recover_interrupted_test_runs,
|
|
test_run_orchestrator,
|
|
)
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.exception_handlers import (
|
|
http_exception_handler,
|
|
request_validation_exception_handler,
|
|
)
|
|
from fastapi.exceptions import RequestValidationError
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
|
|
from routes import (
|
|
assistants,
|
|
auth,
|
|
conversations,
|
|
health,
|
|
input_assets,
|
|
knowledge_bases,
|
|
mcp_servers,
|
|
model_registry,
|
|
node_types,
|
|
openai_models,
|
|
openai_realtime,
|
|
openai_responses,
|
|
realtime_api_keys,
|
|
site_settings,
|
|
test_cases,
|
|
test_assets,
|
|
test_runs,
|
|
tools,
|
|
voice_webrtc,
|
|
voice_ws,
|
|
)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(_app: FastAPI):
|
|
await sync_interface_definitions()
|
|
await sync_default_tools()
|
|
await recover_interrupted_documents()
|
|
await recover_interrupted_analyses()
|
|
await recover_interrupted_webhooks()
|
|
await recover_interrupted_test_runs()
|
|
await recover_interrupted_responses()
|
|
analysis_worker = asyncio.create_task(
|
|
run_analysis_worker(), name="post-call-analysis-worker"
|
|
)
|
|
webhook_worker = asyncio.create_task(
|
|
run_webhook_worker(), name="analysis-webhook-worker"
|
|
)
|
|
try:
|
|
yield
|
|
finally:
|
|
analysis_worker.cancel()
|
|
webhook_worker.cancel()
|
|
await asyncio.gather(
|
|
analysis_worker, webhook_worker, return_exceptions=True
|
|
)
|
|
await test_run_orchestrator.shutdown()
|
|
await voice_webrtc.shutdown_active_sessions()
|
|
|
|
|
|
app = FastAPI(
|
|
title="AI Video Assistant 平台 - 后端",
|
|
description=f"Public protocol: {PUBLIC_API_NAME}.",
|
|
version="1.0.0",
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.CORS_ORIGINS,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
expose_headers=["X-Request-ID", "X-Idempotent-Replayed", "Location"],
|
|
)
|
|
app.add_middleware(RequestIDMiddleware)
|
|
|
|
|
|
def _validation_param(error: dict) -> str | None:
|
|
location = list(error.get("loc") or [])
|
|
if location and location[0] in {"body", "query", "path", "header"}:
|
|
location.pop(0)
|
|
if not location or location == ["json"]:
|
|
return None
|
|
value = ""
|
|
for part in location:
|
|
if isinstance(part, int):
|
|
value += f"[{part}]"
|
|
else:
|
|
value += ("." if value else "") + str(part)
|
|
return value or None
|
|
|
|
|
|
@app.exception_handler(RequestValidationError)
|
|
async def validation_exception_handler(
|
|
request: Request,
|
|
exc: RequestValidationError,
|
|
):
|
|
if not request.url.path.startswith("/v1/"):
|
|
return await request_validation_exception_handler(request, exc)
|
|
first = (exc.errors() or [{}])[0]
|
|
return openai_error_response(
|
|
str(first.get("msg") or "Request does not match the API schema"),
|
|
code="invalid_request_error",
|
|
param=_validation_param(first),
|
|
status_code=422,
|
|
)
|
|
|
|
|
|
@app.exception_handler(StarletteHTTPException)
|
|
async def public_http_exception_handler(
|
|
request: Request,
|
|
exc: StarletteHTTPException,
|
|
):
|
|
if not request.url.path.startswith("/v1/"):
|
|
return await http_exception_handler(request, exc)
|
|
code = "not_found" if exc.status_code == 404 else "http_error"
|
|
return openai_error_response(
|
|
str(exc.detail),
|
|
code=code,
|
|
status_code=exc.status_code,
|
|
)
|
|
|
|
|
|
@app.exception_handler(Exception)
|
|
async def public_unhandled_exception_handler(request: Request, exc: Exception):
|
|
if not request.url.path.startswith("/v1/"):
|
|
raise exc
|
|
logger.exception(
|
|
f"Unhandled public API error: {type(exc).__name__}"
|
|
)
|
|
return openai_error_response(
|
|
"The server encountered an internal error",
|
|
code="internal_error",
|
|
status_code=500,
|
|
)
|
|
|
|
app.include_router(health.router)
|
|
app.include_router(auth.router)
|
|
app.include_router(conversations.router)
|
|
app.include_router(input_assets.router)
|
|
app.include_router(assistants.router)
|
|
app.include_router(knowledge_bases.router)
|
|
app.include_router(mcp_servers.router)
|
|
app.include_router(model_registry.router)
|
|
app.include_router(node_types.router)
|
|
app.include_router(openai_models.router)
|
|
app.include_router(openai_realtime.router)
|
|
app.include_router(openai_responses.router)
|
|
app.include_router(realtime_api_keys.router)
|
|
app.include_router(site_settings.router)
|
|
app.include_router(test_cases.router)
|
|
app.include_router(test_assets.router)
|
|
app.include_router(test_runs.router)
|
|
app.include_router(tools.router)
|
|
app.include_router(voice_webrtc.router)
|
|
app.include_router(voice_ws.router)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
uvicorn.run("app:app", host=settings.HOST, port=settings.PORT, reload=True)
|