Files
PlantAI-rag-new/sdk/app/main.py
T
adminandClaude Fable 5 e783a26be6 feat(priv): A1 实现私有资料问答 P1-P3——vstore chunk 索引 + BFF 私有问答端点 + 前端并行块与管理界面
- P1 vstore: chunks.py 切块(600字/8段/重叠1/上限200,表格原位聚合,outline
  不参与)+ChunkIndex 三文件原子写+无降级档;embed_batch_chunked 批量16x并发4;
  vault add/delete/rebuild_chunks 钩子(chunk 失败不阻断 doc 级);search-chunks/
  rebuild-chunks 端点+health chunk_count;rebuild CLI
- P2 BFF: privateqa.ask_private(同doc≤3去重/12k截断/40s超时/带引用短答);
  POST /api/qa/private 三态;schemas PrivateDocBlock 入 union;client.search_chunks;
  llm timeout 参数;main.py 注册
- P3 前端: _firePrivate 并行检索+竞态防护(pending/flush/防重入/跨轮归属);
  AssistantMessage private_doc 块;i18n/private.js;PrivateLibraryPane 挂 DocPane
  空态;removeLibrary(204 兜底)

自测: vstore pytest 26/27(1 失败为 T1 骨架笔误待修)+74 回归;前端 vitest
373/373;npm run build 0 error;sdk 222 passed(3 failed 为基线预存);红线 diff 空。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-07 22:22:22 +08:00

103 lines
3.8 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.
"""main.py —— BFF 应用入口(ARCHITECTURE.md §2/§3)。
- 挂全部 5 个 router(qa/kg/extraction 为 A2/A3 实现,import 路径不变)。
- CORS:允许 http://localhost:5180 与 http://127.0.0.1:5180(Vite dev)。
- lifespan:init_db(幂等迁移);关闭回收 httpx 客户端与 Neo4j 连接。
- 全局 RequestValidationError → VALIDATION 信封(API.md §1,2026-08-25 PM 集成)。
- docgen(P2):routers/docgen.py 对 services/docgen/*(dev1 归属)按契约 import,
模块缺失时自动回落到内置最小接口桩(生成/编辑返回 503),不阻断 BFF 启动。
- 生产形态(M6):ui/dist 存在时静态托管(ARCHITECTURE.md §3)。
"""
from __future__ import annotations
import logging
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from .config import get_settings
from .db import close_db, init_db
from .routers import capabilities, docgen, extraction, figures, health, kg, legacy, qa, sessions
from .routers import bioflow_proxy
from .routers import private_docs
from .upstream import close_client
logging.basicConfig(level=logging.INFO)
_ALLOWED_ORIGINS = [
"http://localhost:5180",
"http://127.0.0.1:5180",
]
def create_app() -> FastAPI:
settings = get_settings()
app = FastAPI(title=settings.app_name, version=settings.version)
app.add_middleware(
CORSMiddleware,
allow_origins=_ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(health.router)
app.include_router(sessions.router)
app.include_router(qa.router)
app.include_router(capabilities.router)
app.include_router(kg.router)
app.include_router(extraction.router)
app.include_router(figures.router)
app.include_router(legacy.router)
app.include_router(docgen.router) # docgen(P2)
app.include_router(private_docs.router) # 私有资料问答(FR-PRIV-004)
if get_settings().bioflow_enabled:
app.include_router(bioflow_proxy.router) # 生信反代(BIOFLOW_ENABLED 开关,FR-BIO-028)
@app.exception_handler(RequestValidationError)
async def _validation_handler(_: Request, exc: RequestValidationError) -> JSONResponse:
# API.md §1 已裁决:参数校验错误收敛为统一 VALIDATION 信封(422)
return JSONResponse(
status_code=422,
content={
"ok": False,
"error": {"code": "VALIDATION", "message": "请求参数不合法", "degraded": False},
},
)
@asynccontextmanager
async def _lifespan(_app: FastAPI):
await init_db()
try:
yield
finally:
await close_client()
try:
from .services.neo4j import close_neo4j # A3 提供;缺失则跳过
await close_neo4j()
except ImportError:
pass
await close_db()
app.router.lifespan_context = _lifespan
# 生产形态:静态托管 ui/dist(存在时挂载;本地开发期可无)。
# 2026-08-25 PM 修复:dist 路径必须基于项目根解析(BFF 工作目录是 sdk/,
# 相对路径会解析成 sdk/ui/dist);sync.sh 排除 dist,需单独同步。
project_root = Path(__file__).resolve().parent.parent.parent
dist = project_root / settings.frontend_dist_dir if not Path(settings.frontend_dist_dir).is_absolute() else Path(settings.frontend_dist_dir)
if dist.is_dir():
app.mount("/", StaticFiles(directory=str(dist), html=True), name="frontend")
return app
app = create_app()