merge: T6 知识库实体与图谱(T-P2-01/05)

This commit is contained in:
xsl
2026-08-14 20:09:59 +08:00
10 changed files with 1499 additions and 10 deletions
+115 -3
View File
@@ -1,10 +1,122 @@
"""知识库实体路由(骨架,T6 填充实现)。
"""kb 实体路由(FR-KB-01/02/05):实体 CRUD + 状态机 + 1~3 跳图谱查询。
GET /kb/items 等;实现契约见 docs/开发任务包/T6-知识库实体与图谱.md。
写接口暂不挂鉴权以外的前置(后台运营角色属 T-P2-09,先 ``CurrentUserId``,
``updated_by`` 记录操作者)。统一信封见架构文档 §9.2;跨域响应模型在
``packages/kb/schemas.py``(``apps/api/schemas.py`` 本轮禁改)。
"""
from __future__ import annotations
from fastapi import APIRouter
from typing import Annotated
from fastapi import APIRouter, Depends, Query
from apps.api.deps import CurrentUserId, SessionDep
from packages.kb.schemas import (
MAX_GRAPH_HOPS,
EntityCreate,
EntityOut,
EntityStatus,
EntityType,
EntityUpdate,
)
from packages.kb.service import KbService
from packages.shared.models import KbEntity
from packages.shared.response import ok
router = APIRouter(prefix="/kb", tags=["kb"])
def kb_service_dep(session: SessionDep) -> KbService:
"""``apps/api/deps.py`` 本轮禁改,kb 的服务装配放路由文件内就近完成。"""
return KbService(session)
KbServiceDep = Annotated[KbService, Depends(kb_service_dep)]
def _serialize(entity: KbEntity) -> dict[str, object]:
return EntityOut(
entity_id=str(entity.entity_id),
entity_type=entity.entity_type,
name=entity.name,
attrs=dict(entity.attrs),
status=entity.status,
version=entity.version,
source=entity.source,
license=entity.license,
updated_by=entity.updated_by,
created_at=entity.created_at,
updated_at=entity.updated_at,
).model_dump()
@router.get("/items", summary="实体列表:entity_type / status 过滤 + 名称模糊 + 分页")
async def list_items(
user_id: CurrentUserId,
kb: KbServiceDep,
entity_type: EntityType | None = Query(default=None, description="12 类本体之一"),
status: EntityStatus | None = Query(default=None),
name: str | None = Query(default=None, max_length=128, description="名称模糊匹配"),
limit: int = Query(default=20, ge=1, le=100),
offset: int = Query(default=0, ge=0),
) -> dict[str, object]:
rows, total = await kb.search_entities(
entity_type=entity_type,
status=status,
name_ilike=name,
limit=limit,
offset=offset,
)
return ok(
{
"items": [_serialize(row) for row in rows],
"total": total,
"limit": limit,
"offset": offset,
}
)
@router.post("/items", summary="新建实体(draft / version=1 起步)")
async def create_entity(
payload: EntityCreate,
user_id: CurrentUserId,
kb: KbServiceDep,
) -> dict[str, object]:
entity = await kb.create_entity(payload=payload, updated_by=str(user_id))
return ok(_serialize(entity))
@router.get("/items/{entity_id}", summary="实体详情")
async def get_entity(
entity_id: int,
user_id: CurrentUserId,
kb: KbServiceDep,
) -> dict[str, object]:
entity = await kb.get_entity(entity_id=entity_id)
return ok(_serialize(entity))
@router.patch("/items/{entity_id}", summary="修改内容(draft/reviewing)或推进状态(状态机校验)")
async def update_entity(
entity_id: int,
payload: EntityUpdate,
user_id: CurrentUserId,
kb: KbServiceDep,
) -> dict[str, object]:
entity = await kb.update_entity(entity_id=entity_id, payload=payload, updated_by=str(user_id))
return ok(_serialize(entity))
@router.get("/items/{entity_id}/graph", summary="从该实体出发的 1~3 跳可解释路径(T-P2-05)")
async def get_entity_graph(
entity_id: int,
user_id: CurrentUserId,
kb: KbServiceDep,
max_hops: int = Query(
default=MAX_GRAPH_HOPS, ge=1, le=MAX_GRAPH_HOPS, description="遍历深度 1~3,超界 422"
),
) -> dict[str, object]:
result = await kb.traverse_graph(entity_id=entity_id, max_hops=max_hops)
return ok(result.model_dump())