Files
ai-video-fullstack/backend/routes/tools.py
Xin Wang 8c0358ad91 Add tool duplication functionality and enhance ComponentsToolsPage
- Implement a new API endpoint for duplicating tools in the backend.
- Add a duplicate_tool function in ComponentsToolsPage to handle tool duplication requests.
- Update the UI to include a dropdown menu option for duplicating tools, improving user experience.
- Refactor the remove function to accept a resource object instead of an ID for better clarity and maintainability.
2026-07-10 16:45:51 +08:00

153 lines
4.8 KiB
Python

"""Reusable tool CRUD. Runtime execution is implemented per supported tool type."""
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,
}
def _duplicate_function_name(source: str) -> str:
candidate = f"{source}_copy"
if len(candidate) <= 64:
return candidate
return f"{source[:57]}_copy"
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.post("/{tool_id}/duplicate", response_model=ToolOut)
async def duplicate_tool(tool_id: str, session: AsyncSession = Depends(get_session)):
source = await session.get(Tool, tool_id)
if not source:
raise HTTPException(404, "工具不存在")
function_name = _duplicate_function_name(source.function_name)
existing = (
await session.execute(select(Tool).where(Tool.function_name == function_name))
).scalar_one_or_none()
if existing:
suffix = uuid.uuid4().hex[:6]
max_base = 64 - len(suffix) - 1
function_name = f"{source.function_name[:max_base]}_{suffix}"
tool = Tool(
id=f"tool_{uuid.uuid4().hex[:12]}",
name=f"{source.name} 副本",
function_name=function_name,
type=source.type,
description=source.description,
definition=dict(source.definition or {}),
secrets=dict(source.secrets or {}),
status=source.status,
)
session.add(tool)
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}