- BFF 薄适配层(FastAPI,8090):QA 代理/SSE 中继/Neo4j 直连/抽取代理/会话持久化 - Vue3 前端:五相位检索流水线、文献来源与证据链、知识图谱、提取任务四 tab - 四档响应式(桌面/笔记本/平板/手机)、深浅主题 - 81 个 FR 全部实现,测试追溯矩阵 81/81,后端 121 测/前端 156 测 - 真实凭据已脱敏,运行时配置在 backend/.env(不入 git) 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
|