forked from xiaohei/taiji-AI-PAD
1246 lines
38 KiB
Python
1246 lines
38 KiB
Python
"""
|
||
外部数据工具 API 接口
|
||
|
||
用户可以创建外部数据工具,工具配置发送给 Agent Manager 生成 Pydantic 工具文件,
|
||
MCP-Server 只存储工具基本信息和关联标识(tool_ref_id)。
|
||
|
||
v2 简化设计:
|
||
- 用户只需提供 name, url, auth, example
|
||
- 系统自动推断:HTTP 方法、参数结构、认证头配置
|
||
"""
|
||
|
||
import structlog
|
||
from typing import Optional, Dict, Any, Tuple
|
||
from uuid import UUID as PyUUID
|
||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy import select, and_
|
||
import json
|
||
|
||
from database import get_db
|
||
from models import ExternalDataTool, ExternalToolkit, User
|
||
from app.schemas import (
|
||
SuccessResponse,
|
||
CreateExternalToolRequest,
|
||
UpdateExternalToolRequest,
|
||
TestExternalToolRequest,
|
||
ExternalToolInfo,
|
||
ExternalToolDetail,
|
||
CreateToolkitRequest,
|
||
UpdateToolkitRequest,
|
||
ToolkitInfo,
|
||
ToolkitDetail,
|
||
ExternalToolAuthConfig,
|
||
)
|
||
from app.auth import require_auth
|
||
from app.agent_manager_client import get_agent_manager_client
|
||
|
||
logger = structlog.get_logger(__name__)
|
||
|
||
router = APIRouter(prefix="/api/user/external-tools", tags=["External Data Tools"])
|
||
|
||
|
||
# ============= 辅助函数:自动推断配置 =============
|
||
|
||
def infer_json_schema_from_example(example: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""
|
||
从请求示例推断 JSON Schema
|
||
|
||
示例:{"city": "北京", "count": 10}
|
||
=> {
|
||
"type": "object",
|
||
"properties": {
|
||
"city": {"type": "string", "description": "city 参数"},
|
||
"count": {"type": "integer", "description": "count 参数"}
|
||
},
|
||
"required": ["city", "count"]
|
||
}
|
||
"""
|
||
if not example:
|
||
return None
|
||
|
||
def infer_type(value: Any) -> Dict[str, Any]:
|
||
"""推断单个值的类型"""
|
||
if value is None:
|
||
return {"type": "string", "nullable": True}
|
||
elif isinstance(value, bool):
|
||
return {"type": "boolean"}
|
||
elif isinstance(value, int):
|
||
return {"type": "integer"}
|
||
elif isinstance(value, float):
|
||
return {"type": "number"}
|
||
elif isinstance(value, str):
|
||
return {"type": "string"}
|
||
elif isinstance(value, list):
|
||
if len(value) > 0:
|
||
item_schema = infer_type(value[0])
|
||
return {"type": "array", "items": item_schema}
|
||
return {"type": "array", "items": {"type": "string"}}
|
||
elif isinstance(value, dict):
|
||
return infer_json_schema_from_example(value)
|
||
else:
|
||
return {"type": "string"}
|
||
|
||
properties = {}
|
||
required = []
|
||
|
||
for key, value in example.items():
|
||
prop_schema = infer_type(value)
|
||
prop_schema["description"] = f"{key} 参数"
|
||
properties[key] = prop_schema
|
||
# 所有在 example 中出现的字段都标记为必填
|
||
required.append(key)
|
||
|
||
return {
|
||
"type": "object",
|
||
"properties": properties,
|
||
"required": required
|
||
}
|
||
|
||
|
||
def build_full_auth_config(auth: Optional[ExternalToolAuthConfig]) -> Optional[Dict[str, Any]]:
|
||
"""
|
||
从简化的认证配置构建完整的 Agent Manager 认证配置
|
||
|
||
自动推断规则:
|
||
- api_key: header 位置,名称尝试 X-API-Key, Authorization, api_key
|
||
- bearer: header 位置,名称 Authorization,值前缀 Bearer
|
||
- basic: header 位置,名称 Authorization,Base64 编码
|
||
"""
|
||
if not auth or auth.type == "none":
|
||
return None
|
||
|
||
secret = auth.get_secret_value()
|
||
|
||
if auth.type == "api_key":
|
||
return {
|
||
"type": "api_key",
|
||
"key": secret,
|
||
"in": auth.in_location or "header", # 默认 header
|
||
"name": auth.name or "X-API-Key" # 默认 X-API-Key
|
||
}
|
||
|
||
elif auth.type == "bearer":
|
||
return {
|
||
"type": "bearer",
|
||
"token": secret
|
||
}
|
||
|
||
elif auth.type == "basic":
|
||
return {
|
||
"type": "basic",
|
||
"username": auth.username,
|
||
"password": auth.password
|
||
}
|
||
|
||
return None
|
||
|
||
|
||
def resolve_tool_config(req: CreateExternalToolRequest) -> Tuple[str, Dict[str, str], Dict[str, Any], Dict[str, Any], int, Dict[str, Any]]:
|
||
"""
|
||
从简化请求解析完整的工具配置
|
||
|
||
返回: (method, headers, request_params, request_body, timeout, retry)
|
||
"""
|
||
# 1. 解析 HTTP 方法(优先级:advanced > 兼容字段 > 默认 POST)
|
||
method = "POST"
|
||
if req.advanced and req.advanced.method:
|
||
method = req.advanced.method
|
||
elif req.method:
|
||
method = req.method
|
||
|
||
# 2. 解析 headers(优先级:advanced > 兼容字段 > 默认)
|
||
headers = {"Content-Type": "application/json"}
|
||
if req.advanced and req.advanced.headers:
|
||
headers.update(req.advanced.headers)
|
||
elif req.headers:
|
||
headers.update(req.headers)
|
||
|
||
# 3. 解析请求参数/体
|
||
request_params = None
|
||
request_body = None
|
||
|
||
# 优先使用兼容字段(如果用户提供了 JSON Schema)
|
||
if req.request_params:
|
||
request_params = req.request_params
|
||
if req.request_body:
|
||
request_body = req.request_body
|
||
|
||
# 如果用户提供了 example,从中推断
|
||
if req.example and not request_params and not request_body:
|
||
inferred_schema = infer_json_schema_from_example(req.example)
|
||
if method in ("GET", "DELETE"):
|
||
request_params = inferred_schema
|
||
else:
|
||
request_body = inferred_schema
|
||
|
||
# 4. 解析超时(优先级:advanced > 兼容字段 > 默认 30)
|
||
timeout = 30
|
||
if req.advanced and req.advanced.timeout:
|
||
timeout = req.advanced.timeout
|
||
elif req.timeout:
|
||
timeout = req.timeout
|
||
|
||
# 5. 解析重试配置
|
||
retry = None
|
||
if req.advanced and req.advanced.retry:
|
||
retry = req.advanced.retry.dict()
|
||
elif req.retry:
|
||
retry = req.retry.dict()
|
||
|
||
return method, headers, request_params, request_body, timeout, retry
|
||
|
||
|
||
# ============= 创建外部数据工具 =============
|
||
|
||
@router.post("", response_model=SuccessResponse)
|
||
async def create_external_tool(
|
||
req: CreateExternalToolRequest,
|
||
principal: dict = Depends(require_auth),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""
|
||
创建外部数据工具(简化版 v2)
|
||
|
||
用户只需提供最少信息:
|
||
- name: 工具名称
|
||
- url: API 地址
|
||
- auth: 认证配置(type + secret)
|
||
- example: 请求参数示例(可选但推荐)
|
||
|
||
系统自动推断:
|
||
- HTTP 方法(默认 POST)
|
||
- 请求参数结构(从 example 推断 JSON Schema)
|
||
- 认证头名称和位置
|
||
"""
|
||
user_id = principal.get("user_id")
|
||
tenant_id = principal.get("tenant_id")
|
||
# 从 JWT claims 中获取 channelId(驼峰命名)
|
||
channel_id = principal.get("claims", {}).get("channelId")
|
||
|
||
# 解析完整配置(从简化输入推断)
|
||
method, headers, request_params, request_body, timeout, retry = resolve_tool_config(req)
|
||
auth_config = build_full_auth_config(req.auth)
|
||
|
||
logger.info(
|
||
"creating_external_tool",
|
||
user_id=user_id,
|
||
name=req.name,
|
||
url=req.url,
|
||
method=method,
|
||
has_example=req.example is not None
|
||
)
|
||
|
||
# 检查工具名称是否已存在
|
||
existing = await db.execute(
|
||
select(ExternalDataTool).where(
|
||
and_(
|
||
ExternalDataTool.owner_id == PyUUID(user_id),
|
||
ExternalDataTool.name == req.name
|
||
)
|
||
)
|
||
)
|
||
if existing.scalar_one_or_none():
|
||
raise HTTPException(
|
||
status_code=409,
|
||
detail={"error": "tool_name_exists", "message": f"工具名称 '{req.name}' 已存在"}
|
||
)
|
||
|
||
# 调用 Agent Manager 生成工具
|
||
client = get_agent_manager_client()
|
||
|
||
try:
|
||
am_result = await client.generate_external_tool(
|
||
name=req.name,
|
||
description=req.description or "",
|
||
url=req.url,
|
||
method=method,
|
||
user_id=user_id,
|
||
tenant_id=tenant_id,
|
||
headers=headers,
|
||
auth=auth_config,
|
||
request_params=request_params,
|
||
request_body=request_body,
|
||
response_mapping=req.response_mapping, # 兼容字段
|
||
timeout=timeout,
|
||
retry=retry
|
||
)
|
||
|
||
# Agent Manager 返回格式: {"success": true, "data": {"tool_ref_id": "xxx", ...}}
|
||
am_data = am_result.get("data", {})
|
||
tool_ref_id = am_data.get("tool_ref_id")
|
||
am_status = "active" if am_result.get("success") else "error"
|
||
error_message = am_result.get("message") if not am_result.get("success") else None
|
||
|
||
except Exception as e:
|
||
logger.error("agent_manager_generate_failed", error=str(e), name=req.name)
|
||
# Agent Manager 调用失败,仍然创建工具记录,状态为 error
|
||
tool_ref_id = None
|
||
am_status = "error"
|
||
error_message = f"Agent Manager 生成工具失败: {str(e)}"
|
||
|
||
# 创建工具记录(保存完整配置,用于创建自定义 Agent)
|
||
tool = ExternalDataTool(
|
||
name=req.name,
|
||
description=req.description,
|
||
url=req.url,
|
||
method=method,
|
||
auth_type=req.auth.type if req.auth else "none",
|
||
# 保存完整配置
|
||
headers=headers,
|
||
auth_config=auth_config,
|
||
request_params=request_params,
|
||
request_body=request_body,
|
||
response_mapping=req.response_mapping,
|
||
timeout=timeout,
|
||
retry_config=retry,
|
||
# Agent Manager 关联
|
||
tool_ref_id=tool_ref_id,
|
||
status=am_status,
|
||
error_message=error_message,
|
||
owner_id=PyUUID(user_id),
|
||
tenant_id=PyUUID(tenant_id) if tenant_id else None,
|
||
channel_id=PyUUID(channel_id) if channel_id else None,
|
||
is_active=True,
|
||
usage_count=0
|
||
)
|
||
|
||
db.add(tool)
|
||
await db.commit()
|
||
await db.refresh(tool)
|
||
|
||
logger.info(
|
||
"external_tool_created",
|
||
tool_id=str(tool.id),
|
||
tool_ref_id=tool_ref_id,
|
||
status=am_status
|
||
)
|
||
|
||
return SuccessResponse(
|
||
data={
|
||
"id": str(tool.id),
|
||
"name": tool.name,
|
||
"tool_ref_id": tool_ref_id,
|
||
"status": am_status,
|
||
"created_at": tool.created_at.isoformat() if tool.created_at else None
|
||
},
|
||
message="工具创建成功,可以开始使用了" if am_status == "active" else f"工具创建完成,但生成失败: {error_message}"
|
||
)
|
||
|
||
|
||
@router.post("/upload", response_model=SuccessResponse)
|
||
async def upload_external_tool(
|
||
file: UploadFile = File(...),
|
||
principal: dict = Depends(require_auth),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""
|
||
上传 JSON 文件创建外部数据工具
|
||
|
||
JSON 文件格式参考 CreateExternalToolRequest
|
||
"""
|
||
user_id = principal.get("user_id")
|
||
|
||
# 验证文件类型
|
||
if not file.filename.endswith(".json"):
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={"error": "invalid_file_type", "message": "只支持 .json 文件"}
|
||
)
|
||
|
||
# 读取并解析 JSON
|
||
try:
|
||
content = await file.read()
|
||
config = json.loads(content.decode("utf-8"))
|
||
except json.JSONDecodeError as e:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={"error": "invalid_json", "message": f"JSON 解析失败: {str(e)}"}
|
||
)
|
||
except Exception as e:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={"error": "file_read_error", "message": f"文件读取失败: {str(e)}"}
|
||
)
|
||
|
||
# 验证必填字段
|
||
required_fields = ["name", "url"]
|
||
missing_fields = [f for f in required_fields if f not in config]
|
||
if missing_fields:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={"error": "missing_fields", "message": f"缺少必填字段: {', '.join(missing_fields)}"}
|
||
)
|
||
|
||
# 构造请求对象
|
||
try:
|
||
req = CreateExternalToolRequest(**config)
|
||
except Exception as e:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={"error": "invalid_config", "message": f"配置格式无效: {str(e)}"}
|
||
)
|
||
|
||
logger.info("uploading_external_tool", user_id=user_id, filename=file.filename)
|
||
|
||
# 调用创建接口
|
||
return await create_external_tool(req, principal, db)
|
||
|
||
|
||
# ============= 获取工具列表 =============
|
||
|
||
@router.get("", response_model=SuccessResponse)
|
||
async def list_external_tools(
|
||
status: Optional[str] = Query(None, description="过滤状态: active/pending/error"),
|
||
page: int = Query(1, ge=1, description="页码"),
|
||
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
|
||
principal: dict = Depends(require_auth),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""
|
||
获取用户的外部数据工具列表
|
||
"""
|
||
user_id = principal.get("user_id")
|
||
|
||
# 构建查询
|
||
query = select(ExternalDataTool).where(
|
||
ExternalDataTool.owner_id == PyUUID(user_id)
|
||
)
|
||
|
||
if status:
|
||
query = query.where(ExternalDataTool.status == status)
|
||
|
||
# 获取总数
|
||
count_query = select(ExternalDataTool).where(
|
||
ExternalDataTool.owner_id == PyUUID(user_id)
|
||
)
|
||
if status:
|
||
count_query = count_query.where(ExternalDataTool.status == status)
|
||
|
||
result = await db.execute(count_query)
|
||
total = len(result.scalars().all())
|
||
|
||
# 分页查询
|
||
query = query.order_by(ExternalDataTool.created_at.desc())
|
||
query = query.offset((page - 1) * page_size).limit(page_size)
|
||
|
||
result = await db.execute(query)
|
||
tools = result.scalars().all()
|
||
|
||
# 构建响应
|
||
tools_data = []
|
||
for tool in tools:
|
||
tools_data.append({
|
||
"id": str(tool.id),
|
||
"name": tool.name,
|
||
"description": tool.description,
|
||
"url": tool.url,
|
||
"method": tool.method,
|
||
"auth_type": tool.auth_type,
|
||
"tool_ref_id": tool.tool_ref_id, # Agent Manager 返回的工具标识
|
||
"status": tool.status,
|
||
"usage_count": tool.usage_count,
|
||
"created_at": tool.created_at.isoformat() if tool.created_at else None
|
||
})
|
||
|
||
return SuccessResponse(
|
||
data={
|
||
"tools": tools_data,
|
||
"total": total,
|
||
"page": page,
|
||
"page_size": page_size
|
||
}
|
||
)
|
||
|
||
|
||
# ============= 获取工具详情 =============
|
||
|
||
@router.get("/{tool_id}", response_model=SuccessResponse)
|
||
async def get_external_tool(
|
||
tool_id: str,
|
||
principal: dict = Depends(require_auth),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""
|
||
获取外部数据工具详情
|
||
"""
|
||
user_id = principal.get("user_id")
|
||
|
||
try:
|
||
tool_uuid = PyUUID(tool_id)
|
||
except ValueError:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={"error": "invalid_tool_id", "message": "无效的工具 ID 格式"}
|
||
)
|
||
|
||
result = await db.execute(
|
||
select(ExternalDataTool).where(
|
||
and_(
|
||
ExternalDataTool.id == tool_uuid,
|
||
ExternalDataTool.owner_id == PyUUID(user_id)
|
||
)
|
||
)
|
||
)
|
||
tool = result.scalar_one_or_none()
|
||
|
||
if not tool:
|
||
raise HTTPException(
|
||
status_code=404,
|
||
detail={"error": "tool_not_found", "message": "工具不存在"}
|
||
)
|
||
|
||
return SuccessResponse(
|
||
data={
|
||
"id": str(tool.id),
|
||
"name": tool.name,
|
||
"description": tool.description,
|
||
"url": tool.url,
|
||
"method": tool.method,
|
||
"auth_type": tool.auth_type,
|
||
"tool_ref_id": tool.tool_ref_id,
|
||
"status": tool.status,
|
||
"error_message": tool.error_message,
|
||
"usage_count": tool.usage_count,
|
||
"created_at": tool.created_at.isoformat() if tool.created_at else None,
|
||
"updated_at": tool.updated_at.isoformat() if tool.updated_at else None
|
||
}
|
||
)
|
||
|
||
|
||
# ============= 更新工具 =============
|
||
|
||
@router.put("/{tool_id}", response_model=SuccessResponse)
|
||
async def update_external_tool(
|
||
tool_id: str,
|
||
req: UpdateExternalToolRequest,
|
||
principal: dict = Depends(require_auth),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""
|
||
更新外部数据工具配置
|
||
|
||
需要传递完整配置,会重新调用 Agent Manager 生成工具文件。
|
||
"""
|
||
user_id = principal.get("user_id")
|
||
tenant_id = principal.get("tenant_id")
|
||
|
||
try:
|
||
tool_uuid = PyUUID(tool_id)
|
||
except ValueError:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={"error": "invalid_tool_id", "message": "无效的工具 ID 格式"}
|
||
)
|
||
|
||
result = await db.execute(
|
||
select(ExternalDataTool).where(
|
||
and_(
|
||
ExternalDataTool.id == tool_uuid,
|
||
ExternalDataTool.owner_id == PyUUID(user_id)
|
||
)
|
||
)
|
||
)
|
||
tool = result.scalar_one_or_none()
|
||
|
||
if not tool:
|
||
raise HTTPException(
|
||
status_code=404,
|
||
detail={"error": "tool_not_found", "message": "工具不存在"}
|
||
)
|
||
|
||
# 解析更新配置(支持部分更新)
|
||
# 使用请求中的值,如果没有则保持原值
|
||
new_name = req.name if req.name else tool.name
|
||
new_description = req.description if req.description is not None else tool.description
|
||
new_url = req.url if req.url else tool.url
|
||
|
||
# 解析配置(使用新的辅助函数,兼容旧格式)
|
||
# 创建一个临时的 CreateExternalToolRequest 来复用 resolve_tool_config
|
||
from app.schemas import CreateExternalToolRequest as TempReq
|
||
temp_req = TempReq(
|
||
name=new_name,
|
||
url=new_url,
|
||
auth=req.auth,
|
||
example=req.example,
|
||
advanced=req.advanced,
|
||
method=req.method,
|
||
headers=req.headers,
|
||
request_params=req.request_params,
|
||
request_body=req.request_body,
|
||
response_mapping=req.response_mapping,
|
||
timeout=req.timeout,
|
||
retry=req.retry
|
||
)
|
||
method, headers, request_params, request_body, timeout, retry = resolve_tool_config(temp_req)
|
||
auth_config = build_full_auth_config(req.auth)
|
||
|
||
# 如果没有提供新的 method,使用原来的
|
||
if not req.method and not (req.advanced and req.advanced.method):
|
||
method = tool.method
|
||
|
||
logger.info(
|
||
"updating_external_tool",
|
||
tool_id=tool_id,
|
||
user_id=user_id,
|
||
name=new_name,
|
||
has_example=req.example is not None
|
||
)
|
||
|
||
# 调用 Agent Manager 更新工具
|
||
client = get_agent_manager_client()
|
||
|
||
try:
|
||
if tool.tool_ref_id:
|
||
# 已有 tool_ref_id,调用更新接口
|
||
am_result = await client.update_external_tool(
|
||
tool_ref_id=tool.tool_ref_id,
|
||
name=new_name,
|
||
description=new_description or "",
|
||
url=new_url,
|
||
method=method,
|
||
user_id=user_id,
|
||
tenant_id=tenant_id,
|
||
headers=headers,
|
||
auth=auth_config,
|
||
request_params=request_params,
|
||
request_body=request_body,
|
||
response_mapping=req.response_mapping,
|
||
timeout=timeout,
|
||
retry=retry
|
||
)
|
||
else:
|
||
# 没有 tool_ref_id,调用生成接口
|
||
am_result = await client.generate_external_tool(
|
||
name=new_name,
|
||
description=new_description or "",
|
||
url=new_url,
|
||
method=method,
|
||
user_id=user_id,
|
||
tenant_id=tenant_id,
|
||
headers=headers,
|
||
auth=auth_config,
|
||
request_params=request_params,
|
||
request_body=request_body,
|
||
response_mapping=req.response_mapping,
|
||
timeout=timeout,
|
||
retry=retry
|
||
)
|
||
|
||
# Agent Manager 返回格式: {"success": true, "data": {"tool_ref_id": "xxx", ...}}
|
||
am_data = am_result.get("data", {})
|
||
new_tool_ref_id = am_data.get("tool_ref_id")
|
||
am_status = "active" if am_result.get("success") else "error"
|
||
error_message = am_result.get("message") if not am_result.get("success") else None
|
||
|
||
except Exception as e:
|
||
logger.error("agent_manager_update_failed", error=str(e), tool_id=tool_id)
|
||
new_tool_ref_id = tool.tool_ref_id # 保持原有的
|
||
am_status = "error"
|
||
error_message = f"Agent Manager 更新工具失败: {str(e)}"
|
||
|
||
# 更新数据库记录
|
||
tool.name = new_name
|
||
tool.description = new_description
|
||
tool.url = new_url
|
||
tool.method = method
|
||
tool.auth_type = req.auth.type if req.auth else tool.auth_type
|
||
tool.tool_ref_id = new_tool_ref_id
|
||
tool.status = am_status
|
||
tool.error_message = error_message
|
||
|
||
await db.commit()
|
||
await db.refresh(tool)
|
||
|
||
logger.info(
|
||
"external_tool_updated",
|
||
tool_id=str(tool.id),
|
||
tool_ref_id=new_tool_ref_id,
|
||
status=am_status
|
||
)
|
||
|
||
return SuccessResponse(
|
||
data={
|
||
"id": str(tool.id),
|
||
"name": tool.name,
|
||
"tool_ref_id": new_tool_ref_id,
|
||
"status": am_status,
|
||
"updated_at": tool.updated_at.isoformat() if tool.updated_at else None
|
||
},
|
||
message="外部数据工具更新成功" if am_status == "active" else f"工具更新完成,但生成失败: {error_message}"
|
||
)
|
||
|
||
|
||
# ============= 删除工具 =============
|
||
|
||
@router.delete("/{tool_id}", response_model=SuccessResponse)
|
||
async def delete_external_tool(
|
||
tool_id: str,
|
||
principal: dict = Depends(require_auth),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""
|
||
删除外部数据工具
|
||
|
||
会同时通知 Agent Manager 删除对应的工具文件。
|
||
"""
|
||
user_id = principal.get("user_id")
|
||
|
||
try:
|
||
tool_uuid = PyUUID(tool_id)
|
||
except ValueError:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={"error": "invalid_tool_id", "message": "无效的工具 ID 格式"}
|
||
)
|
||
|
||
result = await db.execute(
|
||
select(ExternalDataTool).where(
|
||
and_(
|
||
ExternalDataTool.id == tool_uuid,
|
||
ExternalDataTool.owner_id == PyUUID(user_id)
|
||
)
|
||
)
|
||
)
|
||
tool = result.scalar_one_or_none()
|
||
|
||
if not tool:
|
||
raise HTTPException(
|
||
status_code=404,
|
||
detail={"error": "tool_not_found", "message": "工具不存在"}
|
||
)
|
||
|
||
logger.info(
|
||
"deleting_external_tool",
|
||
tool_id=tool_id,
|
||
tool_ref_id=tool.tool_ref_id,
|
||
user_id=user_id
|
||
)
|
||
|
||
# 通知 Agent Manager 删除工具文件
|
||
if tool.tool_ref_id:
|
||
client = get_agent_manager_client()
|
||
try:
|
||
await client.delete_external_tool(tool.tool_ref_id)
|
||
except Exception as e:
|
||
logger.warning(
|
||
"agent_manager_delete_failed",
|
||
error=str(e),
|
||
tool_ref_id=tool.tool_ref_id
|
||
)
|
||
# 即使 AM 删除失败,也继续删除本地记录
|
||
|
||
# 删除数据库记录
|
||
await db.delete(tool)
|
||
await db.commit()
|
||
|
||
logger.info("external_tool_deleted", tool_id=tool_id)
|
||
|
||
return SuccessResponse(
|
||
data={"id": tool_id},
|
||
message="外部数据工具删除成功"
|
||
)
|
||
|
||
|
||
# ============= 测试工具连接 =============
|
||
|
||
@router.post("/{tool_id}/test", response_model=SuccessResponse)
|
||
async def test_external_tool(
|
||
tool_id: str,
|
||
req: TestExternalToolRequest = None,
|
||
principal: dict = Depends(require_auth),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""
|
||
测试外部数据工具连接
|
||
|
||
调用 Agent Manager 的测试接口。
|
||
"""
|
||
user_id = principal.get("user_id")
|
||
|
||
try:
|
||
tool_uuid = PyUUID(tool_id)
|
||
except ValueError:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={"error": "invalid_tool_id", "message": "无效的工具 ID 格式"}
|
||
)
|
||
|
||
result = await db.execute(
|
||
select(ExternalDataTool).where(
|
||
and_(
|
||
ExternalDataTool.id == tool_uuid,
|
||
ExternalDataTool.owner_id == PyUUID(user_id)
|
||
)
|
||
)
|
||
)
|
||
tool = result.scalar_one_or_none()
|
||
|
||
if not tool:
|
||
raise HTTPException(
|
||
status_code=404,
|
||
detail={"error": "tool_not_found", "message": "工具不存在"}
|
||
)
|
||
|
||
if not tool.tool_ref_id:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={"error": "tool_not_ready", "message": "工具尚未生成成功,无法测试"}
|
||
)
|
||
|
||
logger.info(
|
||
"testing_external_tool",
|
||
tool_id=tool_id,
|
||
tool_ref_id=tool.tool_ref_id
|
||
)
|
||
|
||
# 调用 Agent Manager 测试接口
|
||
client = get_agent_manager_client()
|
||
|
||
try:
|
||
test_params = req.test_params if req else None
|
||
am_result = await client.test_external_tool(tool.tool_ref_id, test_params)
|
||
|
||
return SuccessResponse(
|
||
data={
|
||
"connected": am_result.get("connected", False),
|
||
"response_time_ms": am_result.get("response_time_ms"),
|
||
"status_code": am_result.get("status_code"),
|
||
"sample_response": am_result.get("sample_response")
|
||
},
|
||
message="工具连接测试成功" if am_result.get("connected") else "工具连接测试失败"
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error("test_external_tool_failed", error=str(e), tool_id=tool_id)
|
||
return SuccessResponse(
|
||
data={
|
||
"connected": False,
|
||
"error_message": str(e)
|
||
},
|
||
message=f"工具连接测试失败: {str(e)}"
|
||
)
|
||
|
||
|
||
# 注意:创建自定义 Agent 请使用 /api/user/custom-agents 接口
|
||
# 在请求体中通过 externalTools 字段传入外部数据工具 ID 列表
|
||
# 或通过 toolkit 字段传入工具集 ID
|
||
|
||
|
||
# ============= 工具集管理 =============
|
||
|
||
toolkit_router = APIRouter(prefix="/api/user/toolkits", tags=["External Toolkits"])
|
||
|
||
|
||
@toolkit_router.post("", response_model=SuccessResponse)
|
||
async def create_toolkit(
|
||
req: CreateToolkitRequest,
|
||
principal: dict = Depends(require_auth),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""
|
||
创建工具集
|
||
|
||
将多个外部数据工具组合成一个工具集,最多 8 个工具。
|
||
"""
|
||
user_id = principal.get("user_id")
|
||
tenant_id = principal.get("tenant_id")
|
||
# 从 JWT claims 中获取 channelId(驼峰命名)
|
||
channel_id = principal.get("claims", {}).get("channelId")
|
||
|
||
logger.info(
|
||
"creating_toolkit",
|
||
user_id=user_id,
|
||
name=req.name,
|
||
tool_count=len(req.tool_ids)
|
||
)
|
||
|
||
# 验证工具数量
|
||
if len(req.tool_ids) > 8:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={"error": "too_many_tools", "message": "工具集最多包含 8 个工具"}
|
||
)
|
||
|
||
# 检查工具集名称是否已存在
|
||
existing = await db.execute(
|
||
select(ExternalToolkit).where(
|
||
and_(
|
||
ExternalToolkit.owner_id == PyUUID(user_id),
|
||
ExternalToolkit.name == req.name
|
||
)
|
||
)
|
||
)
|
||
if existing.scalar_one_or_none():
|
||
raise HTTPException(
|
||
status_code=409,
|
||
detail={"error": "toolkit_name_exists", "message": f"工具集名称 '{req.name}' 已存在"}
|
||
)
|
||
|
||
# 验证所有工具是否存在且属于用户
|
||
validated_tool_ids = []
|
||
for tool_id in req.tool_ids:
|
||
try:
|
||
tool_uuid = PyUUID(tool_id)
|
||
except ValueError:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={"error": "invalid_tool_id", "message": f"无效的工具 ID: {tool_id}"}
|
||
)
|
||
|
||
result = await db.execute(
|
||
select(ExternalDataTool).where(
|
||
and_(
|
||
ExternalDataTool.id == tool_uuid,
|
||
ExternalDataTool.owner_id == PyUUID(user_id)
|
||
)
|
||
)
|
||
)
|
||
tool = result.scalar_one_or_none()
|
||
|
||
if not tool:
|
||
raise HTTPException(
|
||
status_code=404,
|
||
detail={"error": "tool_not_found", "message": f"工具不存在或无权限: {tool_id}"}
|
||
)
|
||
|
||
validated_tool_ids.append(str(tool.id))
|
||
|
||
# 创建工具集
|
||
toolkit = ExternalToolkit(
|
||
name=req.name,
|
||
description=req.description,
|
||
tool_ids=validated_tool_ids,
|
||
owner_id=PyUUID(user_id),
|
||
tenant_id=PyUUID(tenant_id) if tenant_id else None,
|
||
channel_id=PyUUID(channel_id) if channel_id else None,
|
||
is_active=True,
|
||
usage_count=0
|
||
)
|
||
|
||
db.add(toolkit)
|
||
await db.commit()
|
||
await db.refresh(toolkit)
|
||
|
||
logger.info(
|
||
"toolkit_created",
|
||
toolkit_id=str(toolkit.id),
|
||
name=toolkit.name,
|
||
tool_count=len(validated_tool_ids)
|
||
)
|
||
|
||
return SuccessResponse(
|
||
data={
|
||
"id": str(toolkit.id),
|
||
"name": toolkit.name,
|
||
"description": toolkit.description,
|
||
"tool_count": len(validated_tool_ids),
|
||
"created_at": toolkit.created_at.isoformat() if toolkit.created_at else None
|
||
},
|
||
message="工具集创建成功"
|
||
)
|
||
|
||
|
||
@toolkit_router.get("", response_model=SuccessResponse)
|
||
async def list_toolkits(
|
||
page: int = Query(1, ge=1, description="页码"),
|
||
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
|
||
principal: dict = Depends(require_auth),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""
|
||
获取用户的工具集列表
|
||
"""
|
||
user_id = principal.get("user_id")
|
||
|
||
# 获取总数
|
||
count_result = await db.execute(
|
||
select(ExternalToolkit).where(
|
||
ExternalToolkit.owner_id == PyUUID(user_id)
|
||
)
|
||
)
|
||
total = len(count_result.scalars().all())
|
||
|
||
# 分页查询
|
||
query = select(ExternalToolkit).where(
|
||
ExternalToolkit.owner_id == PyUUID(user_id)
|
||
).order_by(ExternalToolkit.created_at.desc())
|
||
query = query.offset((page - 1) * page_size).limit(page_size)
|
||
|
||
result = await db.execute(query)
|
||
toolkits = result.scalars().all()
|
||
|
||
# 构建响应
|
||
toolkits_data = []
|
||
for tk in toolkits:
|
||
tool_ids = tk.tool_ids if tk.tool_ids else []
|
||
toolkits_data.append({
|
||
"id": str(tk.id),
|
||
"name": tk.name,
|
||
"description": tk.description,
|
||
"tool_count": len(tool_ids),
|
||
"usage_count": tk.usage_count or 0,
|
||
"created_at": tk.created_at.isoformat() if tk.created_at else None
|
||
})
|
||
|
||
return SuccessResponse(
|
||
data={
|
||
"toolkits": toolkits_data,
|
||
"total": total,
|
||
"page": page,
|
||
"page_size": page_size
|
||
}
|
||
)
|
||
|
||
|
||
@toolkit_router.get("/{toolkit_id}", response_model=SuccessResponse)
|
||
async def get_toolkit(
|
||
toolkit_id: str,
|
||
principal: dict = Depends(require_auth),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""
|
||
获取工具集详情
|
||
|
||
包含工具集的基本信息和包含的工具详情。
|
||
"""
|
||
user_id = principal.get("user_id")
|
||
|
||
try:
|
||
toolkit_uuid = PyUUID(toolkit_id)
|
||
except ValueError:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={"error": "invalid_toolkit_id", "message": "无效的工具集 ID 格式"}
|
||
)
|
||
|
||
result = await db.execute(
|
||
select(ExternalToolkit).where(
|
||
and_(
|
||
ExternalToolkit.id == toolkit_uuid,
|
||
ExternalToolkit.owner_id == PyUUID(user_id)
|
||
)
|
||
)
|
||
)
|
||
toolkit = result.scalar_one_or_none()
|
||
|
||
if not toolkit:
|
||
raise HTTPException(
|
||
status_code=404,
|
||
detail={"error": "toolkit_not_found", "message": "工具集不存在"}
|
||
)
|
||
|
||
# 获取包含的工具详情
|
||
tool_ids = toolkit.tool_ids if toolkit.tool_ids else []
|
||
tools_data = []
|
||
|
||
for tid in tool_ids:
|
||
try:
|
||
tool_result = await db.execute(
|
||
select(ExternalDataTool).where(ExternalDataTool.id == PyUUID(tid))
|
||
)
|
||
tool = tool_result.scalar_one_or_none()
|
||
if tool:
|
||
tools_data.append({
|
||
"id": str(tool.id),
|
||
"name": tool.name,
|
||
"description": tool.description,
|
||
"url": tool.url,
|
||
"method": tool.method,
|
||
"auth_type": tool.auth_type,
|
||
"status": tool.status,
|
||
"usage_count": tool.usage_count or 0
|
||
})
|
||
except Exception:
|
||
pass # 跳过无效的工具 ID
|
||
|
||
return SuccessResponse(
|
||
data={
|
||
"id": str(toolkit.id),
|
||
"name": toolkit.name,
|
||
"description": toolkit.description,
|
||
"tool_ids": tool_ids,
|
||
"tools": tools_data,
|
||
"usage_count": toolkit.usage_count or 0,
|
||
"created_at": toolkit.created_at.isoformat() if toolkit.created_at else None,
|
||
"updated_at": toolkit.updated_at.isoformat() if toolkit.updated_at else None
|
||
}
|
||
)
|
||
|
||
|
||
@toolkit_router.put("/{toolkit_id}", response_model=SuccessResponse)
|
||
async def update_toolkit(
|
||
toolkit_id: str,
|
||
req: UpdateToolkitRequest,
|
||
principal: dict = Depends(require_auth),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""
|
||
更新工具集
|
||
"""
|
||
user_id = principal.get("user_id")
|
||
|
||
try:
|
||
toolkit_uuid = PyUUID(toolkit_id)
|
||
except ValueError:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={"error": "invalid_toolkit_id", "message": "无效的工具集 ID 格式"}
|
||
)
|
||
|
||
result = await db.execute(
|
||
select(ExternalToolkit).where(
|
||
and_(
|
||
ExternalToolkit.id == toolkit_uuid,
|
||
ExternalToolkit.owner_id == PyUUID(user_id)
|
||
)
|
||
)
|
||
)
|
||
toolkit = result.scalar_one_or_none()
|
||
|
||
if not toolkit:
|
||
raise HTTPException(
|
||
status_code=404,
|
||
detail={"error": "toolkit_not_found", "message": "工具集不存在"}
|
||
)
|
||
|
||
logger.info(
|
||
"updating_toolkit",
|
||
toolkit_id=toolkit_id,
|
||
user_id=user_id
|
||
)
|
||
|
||
# 更新名称(检查是否与其他工具集重名)
|
||
if req.name and req.name != toolkit.name:
|
||
existing = await db.execute(
|
||
select(ExternalToolkit).where(
|
||
and_(
|
||
ExternalToolkit.owner_id == PyUUID(user_id),
|
||
ExternalToolkit.name == req.name,
|
||
ExternalToolkit.id != toolkit_uuid
|
||
)
|
||
)
|
||
)
|
||
if existing.scalar_one_or_none():
|
||
raise HTTPException(
|
||
status_code=409,
|
||
detail={"error": "toolkit_name_exists", "message": f"工具集名称 '{req.name}' 已存在"}
|
||
)
|
||
toolkit.name = req.name
|
||
|
||
if req.description is not None:
|
||
toolkit.description = req.description
|
||
|
||
# 更新工具列表
|
||
if req.tool_ids is not None:
|
||
if len(req.tool_ids) > 8:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={"error": "too_many_tools", "message": "工具集最多包含 8 个工具"}
|
||
)
|
||
|
||
# 验证所有工具
|
||
validated_tool_ids = []
|
||
for tool_id in req.tool_ids:
|
||
try:
|
||
tool_uuid = PyUUID(tool_id)
|
||
except ValueError:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={"error": "invalid_tool_id", "message": f"无效的工具 ID: {tool_id}"}
|
||
)
|
||
|
||
tool_result = await db.execute(
|
||
select(ExternalDataTool).where(
|
||
and_(
|
||
ExternalDataTool.id == tool_uuid,
|
||
ExternalDataTool.owner_id == PyUUID(user_id)
|
||
)
|
||
)
|
||
)
|
||
if not tool_result.scalar_one_or_none():
|
||
raise HTTPException(
|
||
status_code=404,
|
||
detail={"error": "tool_not_found", "message": f"工具不存在或无权限: {tool_id}"}
|
||
)
|
||
|
||
validated_tool_ids.append(str(tool_uuid))
|
||
|
||
toolkit.tool_ids = validated_tool_ids
|
||
|
||
await db.commit()
|
||
await db.refresh(toolkit)
|
||
|
||
logger.info(
|
||
"toolkit_updated",
|
||
toolkit_id=str(toolkit.id),
|
||
name=toolkit.name
|
||
)
|
||
|
||
return SuccessResponse(
|
||
data={
|
||
"id": str(toolkit.id),
|
||
"name": toolkit.name,
|
||
"description": toolkit.description,
|
||
"tool_count": len(toolkit.tool_ids) if toolkit.tool_ids else 0,
|
||
"updated_at": toolkit.updated_at.isoformat() if toolkit.updated_at else None
|
||
},
|
||
message="工具集更新成功"
|
||
)
|
||
|
||
|
||
@toolkit_router.delete("/{toolkit_id}", response_model=SuccessResponse)
|
||
async def delete_toolkit(
|
||
toolkit_id: str,
|
||
principal: dict = Depends(require_auth),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""
|
||
删除工具集
|
||
|
||
注意:删除工具集不会删除其中的工具。
|
||
"""
|
||
user_id = principal.get("user_id")
|
||
|
||
try:
|
||
toolkit_uuid = PyUUID(toolkit_id)
|
||
except ValueError:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={"error": "invalid_toolkit_id", "message": "无效的工具集 ID 格式"}
|
||
)
|
||
|
||
result = await db.execute(
|
||
select(ExternalToolkit).where(
|
||
and_(
|
||
ExternalToolkit.id == toolkit_uuid,
|
||
ExternalToolkit.owner_id == PyUUID(user_id)
|
||
)
|
||
)
|
||
)
|
||
toolkit = result.scalar_one_or_none()
|
||
|
||
if not toolkit:
|
||
raise HTTPException(
|
||
status_code=404,
|
||
detail={"error": "toolkit_not_found", "message": "工具集不存在"}
|
||
)
|
||
|
||
logger.info(
|
||
"deleting_toolkit",
|
||
toolkit_id=toolkit_id,
|
||
name=toolkit.name,
|
||
user_id=user_id
|
||
)
|
||
|
||
await db.delete(toolkit)
|
||
await db.commit()
|
||
|
||
logger.info("toolkit_deleted", toolkit_id=toolkit_id)
|
||
|
||
return SuccessResponse(
|
||
data={"id": toolkit_id},
|
||
message="工具集删除成功"
|
||
)
|
||
|