"""kb 实体路由(FR-KB-01/02/05):实体 CRUD + 状态机 + 1~3 跳图谱查询。 写接口暂不挂鉴权以外的前置(后台运营角色属 T-P2-09,先 ``CurrentUserId``, ``updated_by`` 记录操作者)。统一信封见架构文档 §9.2;跨域响应模型在 ``packages/kb/schemas.py``(``apps/api/schemas.py`` 本轮禁改)。 """ from __future__ import annotations 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())