119 lines
3.6 KiB
Python
119 lines
3.6 KiB
Python
"""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}
|