- Introduce a new `Tool` model and `AssistantToolBinding` for managing reusable tools within the application. - Implement CRUD operations for tools in the new `tools` route, allowing for the creation, retrieval, updating, and deletion of tools. - Update the `Assistant` model to include a list of tool IDs, enabling assistants to utilize these tools. - Enhance the backend routes to synchronize tool bindings with assistants, ensuring proper management of tool associations. - Add frontend components for tool management, including a tool picker in the assistant configuration, improving user experience in tool selection. - Create a mobile call page to facilitate video calls, integrating camera and microphone selection for enhanced communication capabilities. - Update API definitions to include tool-related types and operations, ensuring consistency across the application. - Add a migration script to create the necessary database tables for tools and bindings, supporting the new functionality.
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}
|