forked from zhanggangyong/agent_management
- 修改 template_manager.py、k8s_manager.py 中的端口映射 - 更新 jina_search_agent、azure_blob_agent 系列、a2a_litellm_agent 的代码和 Dockerfile 为 8000 - 添加端口修改脚本和测试脚本 Made-with: Cursor
659 lines
22 KiB
Python
659 lines
22 KiB
Python
"""
|
||
工具生成 API 模块
|
||
独立的 FastAPI Router,用于动态生成 Agent
|
||
不影响现有的 Agent Manager 规范
|
||
"""
|
||
|
||
import os
|
||
import uuid
|
||
import logging
|
||
import requests
|
||
from datetime import datetime
|
||
from typing import Dict, List, Optional, Any
|
||
from fastapi import APIRouter, HTTPException, BackgroundTasks
|
||
from pydantic import BaseModel, Field
|
||
from k8s_manager import sanitize_k8s_name
|
||
|
||
from gitee_manager import gitee_manager
|
||
from agent_code_generator import agent_code_generator
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# 创建独立的 Router
|
||
router = APIRouter(prefix="/tools", tags=["Dynamic Tool Generator"])
|
||
|
||
# 内存存储工具引用(生产环境应使用数据库)
|
||
TOOL_REFS: Dict[str, Dict] = {}
|
||
|
||
|
||
# ==================== 请求/响应模型 ====================
|
||
|
||
class AuthConfig(BaseModel):
|
||
"""认证配置"""
|
||
type: str = Field(..., description="认证类型: api_key, bearer, basic")
|
||
key: Optional[str] = Field(None, description="API Key 或 Bearer Token")
|
||
username: Optional[str] = Field(None, description="Basic Auth 用户名")
|
||
password: Optional[str] = Field(None, description="Basic Auth 密码")
|
||
in_location: Optional[str] = Field("header", alias="in", description="API Key 位置: header, query")
|
||
name: Optional[str] = Field("X-API-Key", description="API Key 参数名")
|
||
|
||
|
||
class RetryConfig(BaseModel):
|
||
"""重试配置"""
|
||
max_retries: int = Field(3, description="最大重试次数")
|
||
retry_delay: int = Field(1, description="重试间隔(秒)")
|
||
|
||
|
||
class GenerateToolRequest(BaseModel):
|
||
"""生成工具请求 - 符合接口规范"""
|
||
name: str = Field(..., min_length=1, max_length=100, description="工具名称")
|
||
description: str = Field(..., description="工具描述")
|
||
url: str = Field(..., description="API 端点 URL")
|
||
method: str = Field(..., description="HTTP 方法: GET/POST/PUT/DELETE/PATCH")
|
||
user_id: str = Field(..., description="用户 ID")
|
||
tenant_id: Optional[str] = Field(None, description="租户 ID")
|
||
headers: Optional[Dict[str, str]] = Field(None, description="自定义请求头")
|
||
auth: Optional[AuthConfig] = Field(None, description="认证配置")
|
||
request_params: Optional[Dict] = Field(None, description="URL 查询参数定义 (JSON Schema)")
|
||
request_body: Optional[Dict] = Field(None, description="请求体定义 (JSON Schema)")
|
||
response_mapping: Optional[Dict] = Field(None, description="响应字段映射")
|
||
timeout: int = Field(30, description="超时时间(秒)")
|
||
retry: Optional[RetryConfig] = Field(None, description="重试配置")
|
||
|
||
|
||
class GenerateAgentRequest(BaseModel):
|
||
"""生成完整 Agent 请求"""
|
||
agent_name: str = Field(..., min_length=1, max_length=63, description="Agent 名称")
|
||
description: str = Field(..., description="Agent 描述")
|
||
tools: List[GenerateToolRequest] = Field(..., description="工具配置列表")
|
||
user_id: str = Field(..., description="用户 ID")
|
||
tenant_id: Optional[str] = Field(None, description="租户 ID")
|
||
auto_deploy: bool = Field(False, description="是否自动部署(构建完成后)")
|
||
|
||
|
||
class UpdateToolRequest(BaseModel):
|
||
"""更新工具请求"""
|
||
description: Optional[str] = None
|
||
url: Optional[str] = None
|
||
method: Optional[str] = None
|
||
headers: Optional[Dict[str, str]] = None
|
||
auth: Optional[AuthConfig] = None
|
||
request_params: Optional[Dict] = None
|
||
request_body: Optional[Dict] = None
|
||
timeout: Optional[int] = None
|
||
|
||
|
||
class TestToolRequest(BaseModel):
|
||
"""测试工具请求"""
|
||
test_params: Optional[Dict] = Field(None, description="测试参数")
|
||
|
||
|
||
# ==================== API 端点 ====================
|
||
|
||
@router.post("/generate")
|
||
async def generate_tool(request: GenerateToolRequest):
|
||
"""
|
||
生成外部数据工具
|
||
|
||
1. 验证配置格式
|
||
2. 生成 Pydantic AI 工具代码
|
||
3. 返回 tool_ref_id
|
||
|
||
注意:此端点仅生成单个工具代码,不创建仓库
|
||
"""
|
||
try:
|
||
# 生成唯一 ID
|
||
tool_ref_id = f"tool-{request.name}-{uuid.uuid4().hex[:8]}"
|
||
|
||
# 转换请求为工具配置
|
||
tool_config = {
|
||
"name": request.name,
|
||
"description": request.description,
|
||
"url": request.url,
|
||
"method": request.method,
|
||
"auth": request.auth.dict() if request.auth else None,
|
||
"request_params": request.request_params,
|
||
"request_body": request.request_body,
|
||
"timeout": request.timeout
|
||
}
|
||
|
||
# 生成工具代码
|
||
tool_code = agent_code_generator.generate_tool_code(tool_config)
|
||
|
||
# 存储工具引用
|
||
TOOL_REFS[tool_ref_id] = {
|
||
"id": tool_ref_id,
|
||
"name": request.name,
|
||
"description": request.description,
|
||
"config": tool_config,
|
||
"code": tool_code,
|
||
"user_id": request.user_id,
|
||
"tenant_id": request.tenant_id,
|
||
"created_at": datetime.utcnow().isoformat(),
|
||
"status": "created"
|
||
}
|
||
|
||
logger.info(f"✅ 工具生成成功: {tool_ref_id}")
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"tool_ref_id": tool_ref_id,
|
||
"name": request.name,
|
||
"description": request.description,
|
||
"created_at": TOOL_REFS[tool_ref_id]["created_at"]
|
||
},
|
||
"message": "工具生成成功"
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"工具生成失败: {e}")
|
||
return {
|
||
"success": False,
|
||
"error": "generation_failed",
|
||
"message": str(e)
|
||
}
|
||
|
||
|
||
@router.post("/generate-agent")
|
||
async def generate_agent(request: GenerateAgentRequest, background_tasks: BackgroundTasks):
|
||
"""
|
||
生成完整 Agent 并推送到 Gitee
|
||
|
||
1. 生成所有工具代码
|
||
2. 生成 Agent 主文件
|
||
3. 生成 Dockerfile 和 CI/CD 配置
|
||
4. 创建 Gitee 仓库
|
||
5. 推送代码(触发 CI/CD 构建)
|
||
6. 返回状态查询信息
|
||
"""
|
||
try:
|
||
# 生成唯一的仓库名
|
||
repo_name = f"agent-{request.agent_name.lower().replace('_', '-')}-{uuid.uuid4().hex[:6]}"
|
||
agent_ref_id = f"agent-{repo_name}"
|
||
|
||
# 转换工具配置
|
||
tools_config = []
|
||
for tool in request.tools:
|
||
tools_config.append({
|
||
"name": tool.name,
|
||
"description": tool.description,
|
||
"url": tool.url,
|
||
"method": tool.method,
|
||
"auth": tool.auth.dict() if tool.auth else None,
|
||
"request_params": tool.request_params,
|
||
"request_body": tool.request_body,
|
||
"timeout": tool.timeout
|
||
})
|
||
|
||
# 生成完整项目文件(k8s_name 必须合规,否则 Service 创建会报 DNS-1035)
|
||
project_files = agent_code_generator.generate_full_project(
|
||
agent_name=k8s_name_for_cicd,
|
||
description=request.description,
|
||
tools_config=tools_config,
|
||
auto_deploy=request.auto_deploy
|
||
)
|
||
|
||
# 创建 Gitee 仓库
|
||
repo_result = gitee_manager.create_repository(
|
||
repo_name=repo_name,
|
||
description=f"{request.agent_name} - {request.description}",
|
||
private=False
|
||
)
|
||
|
||
if not repo_result.get("success"):
|
||
return {
|
||
"success": False,
|
||
"error": "repo_creation_failed",
|
||
"message": repo_result.get("error", "仓库创建失败")
|
||
}
|
||
|
||
# 获取仓库所有者
|
||
repo_owner = repo_result.get("owner", gitee_manager.gitee_username)
|
||
|
||
# 推送文件到仓库
|
||
push_result = gitee_manager.push_files(
|
||
repo_name=repo_name,
|
||
files=project_files,
|
||
commit_message=f"Initial commit: {request.agent_name}",
|
||
owner=repo_owner
|
||
)
|
||
|
||
logger.info(f"文件推送结果: {push_result}")
|
||
|
||
# 设置 CI/CD Secrets(包含 DNS 配置)
|
||
cicd_secrets = {
|
||
"ACR_LOGIN_SERVER": "agnettaiji.azurecr.io",
|
||
"ACR_USERNAME": "agnettaiji",
|
||
"ACR_PASSWORD": "hDpX5t34N5ZmnKdtqyjYL5co/SnXJrmD20CRpGpWaG+ACRCw2wGM",
|
||
"AZ_CLIENT_ID": "f2dd1cb2-02f6-4efb-bc72-d148f6e01545",
|
||
"AZ_CLIENT_SECRET": "UVU8Q~Hcrf5KeLi2RvUXB2rcuKFEjRCCrf_JrbwA",
|
||
"AZ_TENANT_ID": "263c3ff6-1be5-4141-8308-b188464fb297",
|
||
"AZ_SUBSCRIPTION_ID": "45d7a360-af09-40fc-9afc-56dc475245ec",
|
||
"AZ_RG": "taiji-ai-pda",
|
||
"AZ_AKS": "taiji-ai-pda",
|
||
"AZURE_DNS_ZONE": "taijiagnet.com"
|
||
}
|
||
|
||
secrets_result = gitee_manager.set_repo_secrets(
|
||
repo_name=repo_name,
|
||
secrets=cicd_secrets,
|
||
owner=repo_owner
|
||
)
|
||
|
||
logger.info(f"Secrets 设置结果: {secrets_result}")
|
||
|
||
# 计算 K8s 相关名称
|
||
k8s_name = repo_name.lower().replace("_", "-").replace(" ", "-")
|
||
|
||
# 存储 Agent 引用
|
||
TOOL_REFS[agent_ref_id] = {
|
||
"id": agent_ref_id,
|
||
"type": "agent",
|
||
"name": request.agent_name,
|
||
"owner": repo_owner,
|
||
"description": request.description,
|
||
"repo_name": repo_name,
|
||
"repo_url": repo_result.get("html_url"),
|
||
"clone_url": repo_result.get("clone_url"),
|
||
"tools": [t.name for t in request.tools],
|
||
"user_id": request.user_id,
|
||
"tenant_id": request.tenant_id,
|
||
"created_at": datetime.utcnow().isoformat(),
|
||
"status": "building",
|
||
"auto_deploy": request.auto_deploy,
|
||
"image_name": f"agnettaiji.azurecr.io/ai-agents/{k8s_name_for_cicd}:latest",
|
||
"expected_domain": f"{k8s_name_for_cicd}.taijiagnet.com",
|
||
"expected_namespace": f"agent-{k8s_name_for_cicd}"
|
||
}
|
||
|
||
logger.info(f"✅ Agent 项目创建成功: {repo_name}")
|
||
|
||
# 计算预期的 DNS 域名
|
||
k8s_name = repo_name.lower().replace("_", "-").replace(" ", "-")
|
||
expected_domain = f"{k8s_name}.taijiagnet.com"
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"agent_ref_id": agent_ref_id,
|
||
"repo_name": repo_name,
|
||
"repo_url": repo_result.get("html_url"),
|
||
"image_name": f"agnettaiji.azurecr.io/ai-agents/{k8s_name_for_cicd}:latest",
|
||
"status": "building",
|
||
"files_pushed": len(project_files),
|
||
"tools_count": len(request.tools),
|
||
"expected_domain": expected_domain,
|
||
"expected_namespace": f"agent-{k8s_name_for_cicd}"
|
||
},
|
||
"message": f"Agent 项目已创建并推送到 Gitee,CI/CD 正在构建中。部署后访问: http://{expected_domain}"
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Agent 生成失败: {e}")
|
||
import traceback
|
||
logger.error(traceback.format_exc())
|
||
return {
|
||
"success": False,
|
||
"error": "generation_failed",
|
||
"message": str(e)
|
||
}
|
||
|
||
|
||
@router.get("/{tool_ref_id}")
|
||
async def get_tool(tool_ref_id: str):
|
||
"""获取工具信息"""
|
||
if tool_ref_id not in TOOL_REFS:
|
||
raise HTTPException(status_code=404, detail="工具不存在")
|
||
|
||
tool = TOOL_REFS[tool_ref_id]
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"id": tool["id"],
|
||
"name": tool["name"],
|
||
"description": tool["description"],
|
||
"status": tool["status"],
|
||
"created_at": tool["created_at"],
|
||
"repo_url": tool.get("repo_url"),
|
||
"image_name": tool.get("image_name")
|
||
}
|
||
}
|
||
|
||
|
||
@router.put("/{tool_ref_id}")
|
||
async def update_tool(tool_ref_id: str, request: UpdateToolRequest):
|
||
"""更新工具配置"""
|
||
if tool_ref_id not in TOOL_REFS:
|
||
raise HTTPException(status_code=404, detail="工具不存在")
|
||
|
||
tool = TOOL_REFS[tool_ref_id]
|
||
|
||
# 更新配置
|
||
if request.description:
|
||
tool["description"] = request.description
|
||
tool["config"]["description"] = request.description
|
||
if request.url:
|
||
tool["config"]["url"] = request.url
|
||
if request.method:
|
||
tool["config"]["method"] = request.method
|
||
if request.auth:
|
||
tool["config"]["auth"] = request.auth.dict()
|
||
if request.request_params:
|
||
tool["config"]["request_params"] = request.request_params
|
||
if request.request_body:
|
||
tool["config"]["request_body"] = request.request_body
|
||
if request.timeout:
|
||
tool["config"]["timeout"] = request.timeout
|
||
|
||
# 重新生成代码
|
||
tool["code"] = agent_code_generator.generate_tool_code(tool["config"])
|
||
tool["updated_at"] = datetime.utcnow().isoformat()
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"id": tool["id"],
|
||
"name": tool["name"],
|
||
"updated_at": tool["updated_at"]
|
||
},
|
||
"message": "工具更新成功"
|
||
}
|
||
|
||
|
||
@router.delete("/{tool_ref_id}")
|
||
async def delete_tool(tool_ref_id: str, delete_repo: bool = False):
|
||
"""删除工具"""
|
||
if tool_ref_id not in TOOL_REFS:
|
||
raise HTTPException(status_code=404, detail="工具不存在")
|
||
|
||
tool = TOOL_REFS[tool_ref_id]
|
||
|
||
# 如果需要删除仓库
|
||
if delete_repo and tool.get("repo_name"):
|
||
gitee_manager.delete_repository(tool["repo_name"])
|
||
|
||
del TOOL_REFS[tool_ref_id]
|
||
|
||
return {
|
||
"success": True,
|
||
"message": "工具删除成功"
|
||
}
|
||
|
||
|
||
@router.post("/{tool_ref_id}/test")
|
||
async def test_tool(tool_ref_id: str, request: TestToolRequest):
|
||
"""测试工具连接"""
|
||
if tool_ref_id not in TOOL_REFS:
|
||
raise HTTPException(status_code=404, detail="工具不存在")
|
||
|
||
tool = TOOL_REFS[tool_ref_id]
|
||
config = tool.get("config", {})
|
||
|
||
try:
|
||
# 构建请求
|
||
url = config.get("url", "")
|
||
method = config.get("method", "GET")
|
||
timeout = config.get("timeout", 30)
|
||
|
||
headers = {}
|
||
params = request.test_params or {}
|
||
|
||
# 处理认证
|
||
auth = config.get("auth", {})
|
||
if auth:
|
||
if auth.get("type") == "api_key":
|
||
if auth.get("in") == "header":
|
||
headers[auth.get("name", "X-API-Key")] = auth.get("key", "")
|
||
else:
|
||
params[auth.get("name", "apikey")] = auth.get("key", "")
|
||
elif auth.get("type") == "bearer":
|
||
headers["Authorization"] = f"Bearer {auth.get('key', '')}"
|
||
|
||
# 发送测试请求
|
||
response = requests.request(
|
||
method=method,
|
||
url=url,
|
||
headers=headers,
|
||
params=params,
|
||
timeout=timeout
|
||
)
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"status_code": response.status_code,
|
||
"response_time_ms": response.elapsed.total_seconds() * 1000,
|
||
"content_type": response.headers.get("Content-Type"),
|
||
"response_preview": response.text[:500] if response.text else None
|
||
},
|
||
"message": "工具连接测试成功"
|
||
}
|
||
|
||
except requests.exceptions.Timeout:
|
||
return {
|
||
"success": False,
|
||
"error": "timeout",
|
||
"message": "连接超时"
|
||
}
|
||
except requests.exceptions.ConnectionError:
|
||
return {
|
||
"success": False,
|
||
"error": "connection_error",
|
||
"message": "无法连接到目标服务"
|
||
}
|
||
except Exception as e:
|
||
return {
|
||
"success": False,
|
||
"error": "test_failed",
|
||
"message": str(e)
|
||
}
|
||
|
||
|
||
@router.get("/{tool_ref_id}/build-status")
|
||
async def get_build_status(tool_ref_id: str):
|
||
"""
|
||
查询构建状态
|
||
|
||
1. 查询 Gitee Action 状态
|
||
2. 查询 ACR 镜像状态
|
||
"""
|
||
if tool_ref_id not in TOOL_REFS:
|
||
raise HTTPException(status_code=404, detail="工具/Agent 不存在")
|
||
|
||
tool = TOOL_REFS[tool_ref_id]
|
||
repo_name = tool.get("repo_name")
|
||
|
||
if not repo_name:
|
||
return {
|
||
"success": False,
|
||
"error": "no_repo",
|
||
"message": "此工具未关联 Gitee 仓库"
|
||
}
|
||
|
||
# 查询 Gitee Action 状态
|
||
action_status = gitee_manager.get_action_status(repo_name)
|
||
|
||
# 查询 ACR 镜像状态
|
||
image_name = tool.get("image_name", "")
|
||
acr_status = _check_acr_image(image_name)
|
||
|
||
# 更新状态
|
||
if acr_status.get("exists"):
|
||
tool["status"] = "ready"
|
||
elif action_status.get("conclusion") == "failure":
|
||
tool["status"] = "build_failed"
|
||
elif action_status.get("status") == "in_progress":
|
||
tool["status"] = "building"
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"tool_ref_id": tool_ref_id,
|
||
"repo_name": repo_name,
|
||
"overall_status": tool["status"],
|
||
"gitee_action": {
|
||
"status": action_status.get("status"),
|
||
"conclusion": action_status.get("conclusion"),
|
||
"run_id": action_status.get("run_id"),
|
||
"html_url": action_status.get("html_url")
|
||
},
|
||
"acr_image": {
|
||
"image_name": image_name,
|
||
"exists": acr_status.get("exists", False),
|
||
"digest": acr_status.get("digest"),
|
||
"created_at": acr_status.get("created_at")
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
@router.post("/{tool_ref_id}/deploy")
|
||
async def deploy_agent(tool_ref_id: str):
|
||
"""
|
||
部署 Agent
|
||
|
||
前提条件:
|
||
1. ACR 中已存在镜像
|
||
2. 或者手动触发部署
|
||
"""
|
||
if tool_ref_id not in TOOL_REFS:
|
||
raise HTTPException(status_code=404, detail="Agent 不存在")
|
||
|
||
tool = TOOL_REFS[tool_ref_id]
|
||
|
||
if tool.get("type") != "agent":
|
||
return {
|
||
"success": False,
|
||
"error": "not_agent",
|
||
"message": "此引用不是 Agent 类型"
|
||
}
|
||
|
||
# 检查镜像是否存在
|
||
image_name = tool.get("image_name", "")
|
||
acr_status = _check_acr_image(image_name)
|
||
|
||
if not acr_status.get("exists"):
|
||
return {
|
||
"success": False,
|
||
"error": "image_not_found",
|
||
"message": f"镜像 {image_name} 尚未构建完成,请等待 CI/CD 完成或检查构建状态"
|
||
}
|
||
|
||
# 调用 Agent Manager 的部署接口
|
||
try:
|
||
from template_manager import template_manager
|
||
|
||
# 先创建模板
|
||
template_name = tool["repo_name"].replace("-", "_")
|
||
|
||
try:
|
||
template_manager.create_template(
|
||
name=template_name,
|
||
image=image_name,
|
||
display_name=tool["name"],
|
||
description=tool["description"],
|
||
port=8000,
|
||
agent_framework="api",
|
||
created_by="tool_generator"
|
||
)
|
||
except ValueError:
|
||
# 模板已存在,更新镜像
|
||
template_manager.update_template(
|
||
name=template_name,
|
||
image=image_name
|
||
)
|
||
|
||
tool["status"] = "deployed"
|
||
tool["template_name"] = template_name
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"tool_ref_id": tool_ref_id,
|
||
"template_name": template_name,
|
||
"image_name": image_name,
|
||
"status": "deployed",
|
||
"message": f"模板 {template_name} 已创建,可通过 POST /agents 创建实例"
|
||
},
|
||
"message": "Agent 模板已创建,可以开始部署实例"
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"部署失败: {e}")
|
||
return {
|
||
"success": False,
|
||
"error": "deployment_failed",
|
||
"message": str(e)
|
||
}
|
||
|
||
|
||
def _check_acr_image(image_name: str) -> Dict[str, Any]:
|
||
"""检查 ACR 镜像是否存在"""
|
||
try:
|
||
# 解析镜像名称
|
||
# agnettaiji.azurecr.io/ai-agents/repo-name:latest
|
||
parts = image_name.split("/")
|
||
if len(parts) < 3:
|
||
return {"exists": False, "error": "Invalid image name"}
|
||
|
||
registry = parts[0]
|
||
repo = "/".join(parts[1:]).split(":")[0]
|
||
tag = parts[-1].split(":")[-1] if ":" in parts[-1] else "latest"
|
||
|
||
# 使用 az acr 命令检查
|
||
import subprocess
|
||
result = subprocess.run(
|
||
["az", "acr", "repository", "show-tags",
|
||
"--name", registry.replace(".azurecr.io", ""),
|
||
"--repository", repo,
|
||
"--output", "json"],
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=30
|
||
)
|
||
|
||
if result.returncode == 0:
|
||
import json
|
||
tags = json.loads(result.stdout)
|
||
if tag in tags:
|
||
return {
|
||
"exists": True,
|
||
"tag": tag,
|
||
"all_tags": tags
|
||
}
|
||
|
||
return {"exists": False}
|
||
|
||
except Exception as e:
|
||
logger.debug(f"检查 ACR 镜像失败: {e}")
|
||
return {"exists": False, "error": str(e)}
|
||
|
||
|
||
# 获取所有工具/Agent 列表
|
||
@router.get("/")
|
||
async def list_tools(user_id: Optional[str] = None):
|
||
"""列出所有工具和 Agent"""
|
||
tools = list(TOOL_REFS.values())
|
||
|
||
if user_id:
|
||
tools = [t for t in tools if t.get("user_id") == user_id]
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"tools": [
|
||
{
|
||
"id": t["id"],
|
||
"name": t["name"],
|
||
"type": t.get("type", "tool"),
|
||
"status": t["status"],
|
||
"created_at": t["created_at"]
|
||
}
|
||
for t in tools
|
||
],
|
||
"count": len(tools)
|
||
}
|
||
}
|