"""Reusable tool CRUD. Tool execution is intentionally not wired to pipelines yet.""" import uuid from db.models import AssistantToolBinding, Tool from db.session import get_session from fastapi import APIRouter, Depends, HTTPException from schemas import ToolOut, ToolUpsert from services.auth import require_admin from services.masking import mask_secrets, merge_secrets from sqlalchemy import select from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession router = APIRouter( prefix="/api/tools", tags=["tools"], dependencies=[Depends(require_admin)], ) def _to_out(tool: Tool) -> ToolOut: return ToolOut( id=tool.id, name=tool.name, function_name=tool.function_name, type=tool.type, description=tool.description, definition=tool.definition, secrets=mask_secrets(tool.secrets or {}), status=tool.status, updated_at=tool.updated_at.isoformat() if tool.updated_at else None, ) def _payload(body: ToolUpsert, stored_secrets: dict | None = None) -> dict: definition = body.definition.model_dump() secrets = ( merge_secrets(body.secrets, stored_secrets or {}) if definition["type"] == "http" else {} ) return { "name": body.name.strip(), "function_name": body.function_name, "type": definition["type"], "description": body.description.strip(), "definition": definition, "secrets": secrets, "status": body.status, } async def _commit(session: AsyncSession, tool: Tool) -> ToolOut: try: await session.commit() except IntegrityError as exc: await session.rollback() raise HTTPException(409, "工具函数名已存在") from exc await session.refresh(tool) return _to_out(tool) @router.get("", response_model=list[ToolOut]) async def list_tools(session: AsyncSession = Depends(get_session)): rows = ( await session.execute(select(Tool).order_by(Tool.updated_at.desc())) ).scalars().all() return [_to_out(tool) for tool in rows] @router.post("", response_model=ToolOut) async def create_tool(body: ToolUpsert, session: AsyncSession = Depends(get_session)): tool = Tool(id=f"tool_{uuid.uuid4().hex[:12]}", **_payload(body)) session.add(tool) return await _commit(session, tool) @router.get("/{tool_id}", response_model=ToolOut) async def get_tool(tool_id: str, session: AsyncSession = Depends(get_session)): tool = await session.get(Tool, tool_id) if not tool: raise HTTPException(404, "工具不存在") return _to_out(tool) @router.put("/{tool_id}", response_model=ToolOut) async def update_tool( tool_id: str, body: ToolUpsert, session: AsyncSession = Depends(get_session), ): tool = await session.get(Tool, tool_id) if not tool: raise HTTPException(404, "工具不存在") for key, value in _payload(body, tool.secrets or {}).items(): setattr(tool, key, value) return await _commit(session, tool) @router.delete("/{tool_id}") async def delete_tool(tool_id: str, session: AsyncSession = Depends(get_session)): tool = await session.get(Tool, tool_id) if not tool: raise HTTPException(404, "工具不存在") in_use = ( await session.execute( select(AssistantToolBinding.assistant_id) .where(AssistantToolBinding.tool_id == tool_id) .limit(1) ) ).scalar_one_or_none() if in_use: raise HTTPException(409, "工具正被助手引用,请先解绑") await session.delete(tool) await session.commit() return {"ok": True}