from __future__ import annotations from typing import Annotated from fastapi import APIRouter, Depends, Header, HTTPException, Query, status from sqlalchemy.orm import Session from app.api.deps import get_db from app.schemas.agent_asset import ( AgentAssetCreate, AgentAssetListItem, AgentAssetRead, AgentAssetReviewCreate, AgentAssetReviewRead, AgentAssetUpdate, AgentAssetVersionCreate, AgentAssetVersionRead, ) from app.schemas.common import ErrorResponse from app.services.agent_assets import AgentAssetService router = APIRouter(prefix="/agent-assets") DbSession = Annotated[Session, Depends(get_db)] ActorHeader = Annotated[ str | None, Header(description="审计操作者。未传时回退到请求体中的 owner / reviewer 或 `system`。"), ] RequestIdHeader = Annotated[ str | None, Header(description="外部请求 ID,用于串联审计日志和上游调用链。"), ] def _handle_asset_error(exc: Exception) -> None: if isinstance(exc, LookupError): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc if isinstance(exc, PermissionError): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc if isinstance(exc, ValueError): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc raise exc @router.get( "", response_model=list[AgentAssetListItem], summary="查询 Agent 资产列表", description="按资产类型、状态、领域和关键字筛选规则、技能、MCP 与任务资产。", ) def list_agent_assets( db: DbSession, asset_type: Annotated[ str | None, Query(description="资产类型:`rule`、`skill`、`mcp`、`task`。"), ] = None, status_value: Annotated[ str | None, Query(alias="status", description="资产状态筛选。"), ] = None, domain: Annotated[ str | None, Query(description="业务领域筛选,例如 `expense`、`ar`、`ap`。"), ] = None, keyword: Annotated[ str | None, Query(description="资产编码、名称关键字模糊查询。"), ] = None, ) -> list[AgentAssetListItem]: return AgentAssetService(db).list_assets( asset_type=asset_type, status=status_value, domain=domain, keyword=keyword, ) @router.get( "/{asset_id}", response_model=AgentAssetRead, summary="读取 Agent 资产详情", description="返回资产当前版本正文、最近版本列表和最近一次审核信息。", responses={ status.HTTP_404_NOT_FOUND: { "model": ErrorResponse, "description": "资产不存在。", } }, ) def get_agent_asset(asset_id: str, db: DbSession) -> AgentAssetRead: asset = AgentAssetService(db).get_asset(asset_id) if asset is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Asset not found") return asset @router.post( "", response_model=AgentAssetRead, status_code=status.HTTP_201_CREATED, summary="创建 Agent 资产", description="创建新的规则、技能、MCP 或任务资产,并自动记录审计日志。", responses={ status.HTTP_400_BAD_REQUEST: { "model": ErrorResponse, "description": "资产编码冲突或请求字段不合法。", } }, ) def create_agent_asset( payload: AgentAssetCreate, db: DbSession, x_actor: ActorHeader = None, x_request_id: RequestIdHeader = None, ) -> AgentAssetRead: try: return AgentAssetService(db).create_asset( payload, actor=(x_actor or payload.owner).strip() or "system", request_id=x_request_id, ) except Exception as exc: _handle_asset_error(exc) @router.patch( "/{asset_id}", response_model=AgentAssetRead, summary="更新 Agent 资产", description="更新资产基础信息、当前版本、状态和配置,并写入审计日志。", responses={ status.HTTP_400_BAD_REQUEST: { "model": ErrorResponse, "description": "状态更新非法或请求字段不合法。", }, status.HTTP_404_NOT_FOUND: { "model": ErrorResponse, "description": "资产或指定版本不存在。", }, }, ) def update_agent_asset( asset_id: str, payload: AgentAssetUpdate, db: DbSession, x_actor: ActorHeader = None, x_request_id: RequestIdHeader = None, ) -> AgentAssetRead: try: return AgentAssetService(db).update_asset( asset_id, payload, actor=(x_actor or "system").strip() or "system", request_id=x_request_id, ) except Exception as exc: _handle_asset_error(exc) @router.get( "/{asset_id}/versions", response_model=list[AgentAssetVersionRead], summary="查询资产版本列表", description="返回指定资产的版本历史,默认按最近版本优先排序。", responses={ status.HTTP_404_NOT_FOUND: { "model": ErrorResponse, "description": "资产不存在。", } }, ) def list_agent_asset_versions( asset_id: str, db: DbSession, limit: Annotated[ int, Query(ge=1, le=100, description="返回版本数量上限。"), ] = 20, ) -> list[AgentAssetVersionRead]: try: return AgentAssetService(db).list_versions(asset_id, limit=limit) except Exception as exc: _handle_asset_error(exc) @router.post( "/{asset_id}/versions", response_model=AgentAssetVersionRead, status_code=status.HTTP_201_CREATED, summary="创建资产版本", description="为指定资产创建新版本;规则使用 Markdown,其他资产使用 JSON 快照。", responses={ status.HTTP_400_BAD_REQUEST: { "model": ErrorResponse, "description": "版本号重复或内容类型不匹配。", }, status.HTTP_404_NOT_FOUND: { "model": ErrorResponse, "description": "资产不存在。", }, }, ) def create_agent_asset_version( asset_id: str, payload: AgentAssetVersionCreate, db: DbSession, x_actor: ActorHeader = None, x_request_id: RequestIdHeader = None, ) -> AgentAssetVersionRead: try: return AgentAssetService(db).create_version( asset_id, payload, actor=(x_actor or payload.created_by).strip() or "system", request_id=x_request_id, ) except Exception as exc: _handle_asset_error(exc) @router.post( "/{asset_id}/reviews", response_model=AgentAssetReviewRead, status_code=status.HTTP_201_CREATED, summary="创建资产审核记录", description="为指定资产版本写入审核结果,并联动更新资产状态。", responses={ status.HTTP_400_BAD_REQUEST: { "model": ErrorResponse, "description": "审核参数不合法。", }, status.HTTP_404_NOT_FOUND: { "model": ErrorResponse, "description": "资产或版本不存在。", }, }, ) def create_agent_asset_review( asset_id: str, payload: AgentAssetReviewCreate, db: DbSession, x_actor: ActorHeader = None, x_request_id: RequestIdHeader = None, ) -> AgentAssetReviewRead: try: return AgentAssetService(db).create_review( asset_id, payload, actor=(x_actor or payload.reviewer).strip() or "system", request_id=x_request_id, ) except Exception as exc: _handle_asset_error(exc) @router.post( "/{asset_id}/activate", response_model=AgentAssetRead, summary="激活资产当前版本", description="将资产当前版本切换为上线状态;规则资产必须已有 `approved` 审核记录。", responses={ status.HTTP_400_BAD_REQUEST: { "model": ErrorResponse, "description": "审核未通过或当前版本未设置。", }, status.HTTP_404_NOT_FOUND: { "model": ErrorResponse, "description": "资产不存在。", }, }, ) def activate_agent_asset( asset_id: str, db: DbSession, x_actor: ActorHeader = None, x_request_id: RequestIdHeader = None, ) -> AgentAssetRead: try: return AgentAssetService(db).activate_asset( asset_id, actor=(x_actor or "system").strip() or "system", request_id=x_request_id, ) except Exception as exc: _handle_asset_error(exc)