Files
ai-video-fullstack/backend/app.py
T

209 lines
6.3 KiB
Python
Raw Normal View History

"""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 配置
"""
2026-08-07 10:59:47 +08:00
import asyncio
2026-08-05 21:28:51 +08:00
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
2026-09-07 14:25:48 +08:00
from loguru import logger
from services.openai_api import (
PUBLIC_API_NAME,
RequestIDMiddleware,
openai_error_response,
)
from services.knowledge import recover_interrupted_documents
2026-09-07 00:15:32 +08:00
from services.openai_responses.state import recover_interrupted_responses
2026-08-07 10:59:47 +08:00
from services.post_call.worker import recover_interrupted_analyses, run_analysis_worker
2026-08-07 14:08:49 +08:00
from services.webhooks.worker import recover_interrupted_webhooks, run_webhook_worker
2026-08-10 13:49:24 +08:00
from services.test_runs.orchestrator import (
recover_interrupted_test_runs,
test_run_orchestrator,
)
2026-09-07 14:25:48 +08:00
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
2026-09-07 14:25:48 +08:00
from starlette.exceptions import HTTPException as StarletteHTTPException
from routes import (
assistants,
background_tasks,
task_backends,
auth,
conversations,
health,
input_assets,
knowledge_bases,
2026-07-18 00:00:06 +08:00
mcp_servers,
model_registry,
node_types,
2026-09-07 14:25:48 +08:00
openai_models,
openai_realtime,
2026-09-07 00:15:32 +08:00
openai_responses,
realtime_api_keys,
2026-08-07 14:08:49 +08:00
site_settings,
2026-08-10 13:49:24 +08:00
test_cases,
test_assets,
2026-08-10 13:49:24 +08:00
test_runs,
2026-07-10 10:05:41 +08:00
tools,
voice_webrtc,
voice_ws,
)
@asynccontextmanager
async def lifespan(_app: FastAPI):
await sync_interface_definitions()
await sync_default_tools()
await recover_interrupted_documents()
2026-08-07 10:59:47 +08:00
await recover_interrupted_analyses()
2026-08-07 14:08:49 +08:00
await recover_interrupted_webhooks()
2026-08-10 13:49:24 +08:00
await recover_interrupted_test_runs()
2026-09-07 00:15:32 +08:00
await recover_interrupted_responses()
from services.background_tasks.service import task_service
await task_service.start()
2026-08-07 10:59:47 +08:00
analysis_worker = asyncio.create_task(
run_analysis_worker(), name="post-call-analysis-worker"
)
2026-08-07 14:08:49 +08:00
webhook_worker = asyncio.create_task(
run_webhook_worker(), name="analysis-webhook-worker"
)
2026-08-05 13:48:29 +08:00
try:
yield
finally:
await task_service.close()
2026-08-07 10:59:47 +08:00
analysis_worker.cancel()
2026-08-07 14:08:49 +08:00
webhook_worker.cancel()
await asyncio.gather(
analysis_worker, webhook_worker, return_exceptions=True
)
2026-08-10 13:49:24 +08:00
await test_run_orchestrator.shutdown()
2026-08-05 13:48:29 +08:00
await voice_webrtc.shutdown_active_sessions()
2026-09-07 14:25:48 +08:00
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=["*"],
2026-09-07 14:25:48 +08:00
expose_headers=["X-Request-ID", "X-Idempotent-Replayed", "Location"],
)
2026-09-07 14:25:48 +08:00
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)
2026-07-18 00:00:06 +08:00
app.include_router(mcp_servers.router)
app.include_router(model_registry.router)
app.include_router(node_types.router)
2026-09-07 14:25:48 +08:00
app.include_router(openai_models.router)
app.include_router(openai_realtime.router)
2026-09-07 00:15:32 +08:00
app.include_router(openai_responses.router)
app.include_router(realtime_api_keys.router)
2026-08-07 14:08:49 +08:00
app.include_router(site_settings.router)
2026-08-10 13:49:24 +08:00
app.include_router(test_cases.router)
app.include_router(test_assets.router)
2026-08-10 13:49:24 +08:00
app.include_router(test_runs.router)
2026-07-10 10:05:41 +08:00
app.include_router(tools.router)
app.include_router(background_tasks.router)
app.include_router(task_backends.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)