- git mv 保留历史:ui/(Vue3+Vite)、sdk/(BFF 中间层)
- 路径语义修正:db_path/figures_cache 改相对 sdk/(BFF cwd),
历史库 sdk/backend/data 迁移至 sdk/data/(会话/图片缓存无损)
- 引用修复 ~15 处:config.py、deploy/*.sh、vitest/playwright 配置、
单测导入路径、docs、README、gitignore、.env.example
- 骨架:agent/{qa,methodology,legacy-rag,vendor}、knowledge/manifest.yaml、
tooling/{compile,kb,crypto,assemble,audit,tools,scripts} + README
- 门禁验证:sdk pytest 125 通过、ui vitest 158 通过、
生产 BFF 脚本重启四上游全绿 + 静态托管 + 会话库无损
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
106 lines
2.8 KiB
Python
106 lines
2.8 KiB
Python
"""routers/kg.py —— 知识图谱浏览(BFF Bolt 直连 Neo4j,API.md §6)。
|
||
|
||
Neo4j 不可用 → 503 UPSTREAM_DOWN 降级信封;未命中实体 → 404 NOT_FOUND。
|
||
错误信封与 mock_server 一致:直接返回 JSONResponse,信封在顶层
|
||
(不用 HTTPException,避免被 FastAPI 包成 {"detail": {...}})。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from typing import Any
|
||
|
||
from fastapi import APIRouter, Query
|
||
from fastapi.responses import JSONResponse
|
||
|
||
from ..services import neo4j as _neo4j
|
||
from ..services.neo4j import Neo4jUnavailable
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = APIRouter(prefix="/api/kg", tags=["kg"])
|
||
|
||
_UPSTREAM_DOWN_MSG = "知识图谱服务当前不可用,请稍后重试"
|
||
|
||
|
||
def _down(exc: BaseException | None = None) -> JSONResponse:
|
||
logger.warning("kg: Neo4j 不可用:%s", exc)
|
||
return JSONResponse(
|
||
status_code=503,
|
||
content={
|
||
"ok": False,
|
||
"error": {
|
||
"code": "UPSTREAM_DOWN",
|
||
"message": _UPSTREAM_DOWN_MSG,
|
||
"degraded": True,
|
||
},
|
||
},
|
||
)
|
||
|
||
|
||
def _not_found() -> JSONResponse:
|
||
return JSONResponse(
|
||
status_code=404,
|
||
content={
|
||
"ok": False,
|
||
"error": {
|
||
"code": "NOT_FOUND",
|
||
"message": "未找到该实体",
|
||
"degraded": False,
|
||
},
|
||
},
|
||
)
|
||
|
||
|
||
@router.get("/status")
|
||
async def kg_status():
|
||
try:
|
||
return await _neo4j.service.kg_status()
|
||
except Neo4jUnavailable as exc:
|
||
return _down(exc)
|
||
|
||
|
||
@router.get("/overview")
|
||
async def kg_overview():
|
||
try:
|
||
return await _neo4j.service.kg_overview()
|
||
except Neo4jUnavailable as exc:
|
||
return _down(exc)
|
||
|
||
|
||
@router.get("/entities")
|
||
async def kg_entities(
|
||
q: str = Query("", description="实体名(name_norm 精确或前缀匹配)"),
|
||
limit: int = Query(20, ge=1, le=20),
|
||
) -> dict[str, Any]:
|
||
try:
|
||
results = await _neo4j.service.search_entities(q, limit=limit)
|
||
except Neo4jUnavailable as exc:
|
||
return _down(exc)
|
||
return {"query": q, "results": results}
|
||
|
||
|
||
@router.get("/entities/{entity_id}")
|
||
async def kg_entity(entity_id: str) -> dict[str, Any]:
|
||
try:
|
||
entity = await _neo4j.service.entity(entity_id)
|
||
except Neo4jUnavailable as exc:
|
||
return _down(exc)
|
||
if entity is None:
|
||
return _not_found()
|
||
return entity
|
||
|
||
|
||
@router.get("/entities/{entity_id}/neighborhood")
|
||
async def kg_neighborhood(
|
||
entity_id: str,
|
||
depth: int = Query(1, ge=1, le=2),
|
||
limit: int = Query(50, ge=1, le=200),
|
||
) -> dict[str, Any]:
|
||
try:
|
||
result = await _neo4j.service.neighborhood(entity_id, depth=depth, limit=limit)
|
||
except Neo4jUnavailable as exc:
|
||
return _down(exc)
|
||
if result.get("center") is None:
|
||
return _not_found()
|
||
return result
|