|
@@ -2,20 +2,19 @@
|
|
|
ToolHub - 远程工具库集成模块
|
|
ToolHub - 远程工具库集成模块
|
|
|
|
|
|
|
|
将 http://43.106.118.91:8001 的工具库 API 包装为 Agent 可调用的工具。
|
|
将 http://43.106.118.91:8001 的工具库 API 包装为 Agent 可调用的工具。
|
|
|
-提供四个工具:
|
|
|
|
|
-1. toolhub_health - 健康检查
|
|
|
|
|
-2. toolhub_search - 搜索/发现远程工具
|
|
|
|
|
-3. toolhub_call - 调用远程工具
|
|
|
|
|
-4. toolhub_create - 创建新远程工具(异步)
|
|
|
|
|
-
|
|
|
|
|
-设计要点:
|
|
|
|
|
-- toolhub_call 的 params 是动态的(取决于 tool_id),用 dict 类型兼容所有工具
|
|
|
|
|
-- toolhub_create 是异步任务,支持轮询等待完成
|
|
|
|
|
-- 所有接口返回 ToolResult,包含结构化输出和 long_term_memory 摘要
|
|
|
|
|
|
|
+提供三个工具:
|
|
|
|
|
+1. toolhub_health - 健康检查
|
|
|
|
|
+2. toolhub_search - 搜索/发现远程工具(GET /tools)
|
|
|
|
|
+3. toolhub_call - 调用远程工具(POST /run_tool)
|
|
|
|
|
+
|
|
|
|
|
+实际 API 端点(通过 /openapi.json 确认):
|
|
|
|
|
+ GET /health → 健康检查
|
|
|
|
|
+ GET /tools → 列出所有工具(含分组、参数 schema)
|
|
|
|
|
+ POST /run_tool → 调用工具 {"tool_id": str, "params": dict}
|
|
|
|
|
+ POST /chat → 对话接口(不在此封装)
|
|
|
"""
|
|
"""
|
|
|
|
|
|
|
|
import json
|
|
import json
|
|
|
-import time
|
|
|
|
|
from typing import Any, Dict, Optional
|
|
from typing import Any, Dict, Optional
|
|
|
|
|
|
|
|
import httpx
|
|
import httpx
|
|
@@ -27,8 +26,7 @@ from agent.tools import tool, ToolResult
|
|
|
|
|
|
|
|
TOOLHUB_BASE_URL = "http://43.106.118.91:8001"
|
|
TOOLHUB_BASE_URL = "http://43.106.118.91:8001"
|
|
|
DEFAULT_TIMEOUT = 30.0
|
|
DEFAULT_TIMEOUT = 30.0
|
|
|
-CREATE_POLL_TIMEOUT = 600.0 # create 最长轮询 10 分钟
|
|
|
|
|
-CREATE_POLL_INTERVAL = 5.0 # 每 5 秒轮询一次
|
|
|
|
|
|
|
+CALL_TIMEOUT = 600.0 # 图像生成类工具耗时较长,云端机器启动可能需要数分钟
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── 工具实现 ──────────────────────────────────────────
|
|
# ── 工具实现 ──────────────────────────────────────────
|
|
@@ -49,7 +47,7 @@ async def toolhub_health() -> ToolResult:
|
|
|
ToolResult 包含服务健康状态信息
|
|
ToolResult 包含服务健康状态信息
|
|
|
"""
|
|
"""
|
|
|
try:
|
|
try:
|
|
|
- async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
|
|
|
|
|
|
|
+ async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT, trust_env=False) as client:
|
|
|
resp = await client.get(f"{TOOLHUB_BASE_URL}/health")
|
|
resp = await client.get(f"{TOOLHUB_BASE_URL}/health")
|
|
|
resp.raise_for_status()
|
|
resp.raise_for_status()
|
|
|
data = resp.json()
|
|
data = resp.json()
|
|
@@ -82,47 +80,65 @@ async def toolhub_health() -> ToolResult:
|
|
|
async def toolhub_search(keyword: Optional[str] = None) -> ToolResult:
|
|
async def toolhub_search(keyword: Optional[str] = None) -> ToolResult:
|
|
|
"""搜索 ToolHub 远程工具库中可用的工具
|
|
"""搜索 ToolHub 远程工具库中可用的工具
|
|
|
|
|
|
|
|
- 从 ToolHub 工具库中搜索可用工具,返回每个工具的完整信息,包括:
|
|
|
|
|
- tool_id、名称、分类、状态、参数列表(含类型、是否必填、默认值、枚举值)、输出 schema 等。
|
|
|
|
|
|
|
+ 从 ToolHub 工具库中获取可用工具列表,返回每个工具的完整信息,包括:
|
|
|
|
|
+ tool_id、名称、分类、状态、参数列表(含类型、是否必填、默认值)、输出 schema、
|
|
|
|
|
+ 分组信息(如 RunComfy 生命周期组)等。
|
|
|
|
|
|
|
|
调用 toolhub_call 之前,应先使用此工具了解目标工具的 tool_id 和所需参数。
|
|
调用 toolhub_call 之前,应先使用此工具了解目标工具的 tool_id 和所需参数。
|
|
|
|
|
+ 不填 keyword 则返回所有工具。
|
|
|
|
|
|
|
|
Args:
|
|
Args:
|
|
|
- keyword: 搜索关键词,为空则返回所有工具
|
|
|
|
|
|
|
+ keyword: 搜索关键词,用于过滤工具名称或描述(客户端过滤);为空则返回所有工具
|
|
|
|
|
|
|
|
Returns:
|
|
Returns:
|
|
|
ToolResult 包含匹配的工具列表及其参数说明
|
|
ToolResult 包含匹配的工具列表及其参数说明
|
|
|
"""
|
|
"""
|
|
|
try:
|
|
try:
|
|
|
- payload = {"keyword": keyword} if keyword else {}
|
|
|
|
|
- async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
|
|
|
|
|
- resp = await client.post(
|
|
|
|
|
- f"{TOOLHUB_BASE_URL}/search_tools", json=payload
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT, trust_env=False) as client:
|
|
|
|
|
+ resp = await client.get(f"{TOOLHUB_BASE_URL}/tools")
|
|
|
resp.raise_for_status()
|
|
resp.raise_for_status()
|
|
|
data = resp.json()
|
|
data = resp.json()
|
|
|
|
|
|
|
|
- total = data.get("total", 0)
|
|
|
|
|
tools = data.get("tools", [])
|
|
tools = data.get("tools", [])
|
|
|
|
|
+ groups = data.get("groups", [])
|
|
|
|
|
+
|
|
|
|
|
+ # 客户端关键词过滤
|
|
|
|
|
+ if keyword:
|
|
|
|
|
+ kw = keyword.lower()
|
|
|
|
|
+ tools = [
|
|
|
|
|
+ t for t in tools
|
|
|
|
|
+ if kw in t.get("name", "").lower()
|
|
|
|
|
+ or kw in t.get("description", "").lower()
|
|
|
|
|
+ or kw in t.get("tool_id", "").lower()
|
|
|
|
|
+ or kw in t.get("category", "").lower()
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ total = len(tools)
|
|
|
|
|
|
|
|
# 构建给 LLM 的结构化摘要
|
|
# 构建给 LLM 的结构化摘要
|
|
|
summaries = []
|
|
summaries = []
|
|
|
for t in tools:
|
|
for t in tools:
|
|
|
|
|
+ input_props = t.get("input_schema", {}).get("properties", {})
|
|
|
|
|
+ required_fields = t.get("input_schema", {}).get("required", [])
|
|
|
params_desc = []
|
|
params_desc = []
|
|
|
- for p in t.get("params", []):
|
|
|
|
|
- req = "必填" if p["required"] else "可选"
|
|
|
|
|
- desc = p.get("description", "")
|
|
|
|
|
- default_str = f", 默认={p['default']}" if p.get("default") is not None else ""
|
|
|
|
|
- enum_str = f", 可选值={p['enum']}" if p.get("enum") else ""
|
|
|
|
|
|
|
+ for name, info in input_props.items():
|
|
|
|
|
+ req = "必填" if name in required_fields else "可选"
|
|
|
|
|
+ desc = info.get("description", "")
|
|
|
|
|
+ default_str = f", 默认={info['default']}" if info.get("default") is not None else ""
|
|
|
|
|
+ enum_str = f", 可选值={info['enum']}" if info.get("enum") else ""
|
|
|
params_desc.append(
|
|
params_desc.append(
|
|
|
- f" - {p['name']} ({p['type']}, {req}): {desc}{default_str}{enum_str}"
|
|
|
|
|
|
|
+ f" - {name} ({info.get('type','any')}, {req}): {desc}{default_str}{enum_str}"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+ group_str = ""
|
|
|
|
|
+ if t.get("group_ids"):
|
|
|
|
|
+ group_str = f"\n 所属分组: {', '.join(t['group_ids'])}"
|
|
|
|
|
+
|
|
|
tool_block = (
|
|
tool_block = (
|
|
|
f"[{t['tool_id']}] {t['name']}\n"
|
|
f"[{t['tool_id']}] {t['name']}\n"
|
|
|
- f" 状态: {t['state']}\n"
|
|
|
|
|
- f" 描述: {t.get('description', '')}\n"
|
|
|
|
|
- f" 流式: {t.get('stream_support', False)}"
|
|
|
|
|
|
|
+ f" 状态: {t['state']} | 运行时: {t['backend_runtime']} | 分类: {t.get('category','')}"
|
|
|
|
|
+ f"{group_str}\n"
|
|
|
|
|
+ f" 描述: {t.get('description', '')}"
|
|
|
)
|
|
)
|
|
|
if params_desc:
|
|
if params_desc:
|
|
|
tool_block += "\n 参数:\n" + "\n".join(params_desc)
|
|
tool_block += "\n 参数:\n" + "\n".join(params_desc)
|
|
@@ -131,19 +147,27 @@ async def toolhub_search(keyword: Optional[str] = None) -> ToolResult:
|
|
|
|
|
|
|
|
summaries.append(tool_block)
|
|
summaries.append(tool_block)
|
|
|
|
|
|
|
|
- output_text = f"共找到 {total} 个工具:\n\n" + "\n\n".join(summaries)
|
|
|
|
|
|
|
+ # 分组使用说明
|
|
|
|
|
+ group_summary = []
|
|
|
|
|
+ for g in groups:
|
|
|
|
|
+ group_summary.append(
|
|
|
|
|
+ f"[组: {g['group_id']}] {g['name']}\n"
|
|
|
|
|
+ f" 调用顺序: {' → '.join(g.get('usage_order', []))}\n"
|
|
|
|
|
+ f" 说明: {g.get('usage_example', '')}"
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
- # 附上完整 JSON 供精确引用
|
|
|
|
|
- full_json = json.dumps(data, ensure_ascii=False, indent=2)
|
|
|
|
|
|
|
+ output_parts = [f"共找到 {total} 个工具({'关键词: ' + keyword if keyword else '全量'}):\n"]
|
|
|
|
|
+ output_parts.append("\n\n".join(summaries))
|
|
|
|
|
+ if group_summary:
|
|
|
|
|
+ output_parts.append("\n\n=== 工具分组(有顺序依赖)===\n" + "\n\n".join(group_summary))
|
|
|
|
|
|
|
|
return ToolResult(
|
|
return ToolResult(
|
|
|
- title=f"ToolHub 搜索{f': {keyword}' if keyword else ''}",
|
|
|
|
|
- output=f"{output_text}\n\n--- 完整数据 ---\n{full_json}",
|
|
|
|
|
|
|
+ title=f"ToolHub 搜索{f': {keyword}' if keyword else '(全量)'}",
|
|
|
|
|
+ output="\n".join(output_parts),
|
|
|
long_term_memory=(
|
|
long_term_memory=(
|
|
|
- f"ToolHub {'搜索 ' + repr(keyword) + ' ' if keyword else ''}"
|
|
|
|
|
- f"共 {total} 个工具: "
|
|
|
|
|
- + ", ".join(t["tool_id"] for t in tools[:10])
|
|
|
|
|
- + ("..." if total > 10 else "")
|
|
|
|
|
|
|
+ f"ToolHub 共 {total} 个工具: "
|
|
|
|
|
+ + ", ".join(t["tool_id"] for t in tools[:15])
|
|
|
|
|
+ + ("..." if total > 15 else "")
|
|
|
),
|
|
),
|
|
|
)
|
|
)
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
@@ -175,6 +199,9 @@ async def toolhub_call(
|
|
|
通过 tool_id 调用 ToolHub 工具库中的某个工具,传入该工具所需的参数。
|
|
通过 tool_id 调用 ToolHub 工具库中的某个工具,传入该工具所需的参数。
|
|
|
不同工具的参数不同,请先用 toolhub_search 查询目标工具的参数说明。
|
|
不同工具的参数不同,请先用 toolhub_search 查询目标工具的参数说明。
|
|
|
|
|
|
|
|
|
|
+ 注意:部分工具为异步生命周期(如 RunComfy、即梦、FLUX),需要按分组顺序
|
|
|
|
|
+ 依次调用多个工具(如先 launch → 再 executor → 再 stop)。
|
|
|
|
|
+
|
|
|
参数通过 params 字典传入,键名和类型需与工具定义一致。
|
|
参数通过 params 字典传入,键名和类型需与工具定义一致。
|
|
|
例如调用图片拼接工具:
|
|
例如调用图片拼接工具:
|
|
|
tool_id="image_stitcher"
|
|
tool_id="image_stitcher"
|
|
@@ -192,9 +219,9 @@ async def toolhub_call(
|
|
|
"tool_id": tool_id,
|
|
"tool_id": tool_id,
|
|
|
"params": params or {},
|
|
"params": params or {},
|
|
|
}
|
|
}
|
|
|
- async with httpx.AsyncClient(timeout=60.0) as client:
|
|
|
|
|
|
|
+ async with httpx.AsyncClient(timeout=CALL_TIMEOUT, trust_env=False) as client:
|
|
|
resp = await client.post(
|
|
resp = await client.post(
|
|
|
- f"{TOOLHUB_BASE_URL}/select_tool", json=payload
|
|
|
|
|
|
|
+ f"{TOOLHUB_BASE_URL}/run_tool", json=payload
|
|
|
)
|
|
)
|
|
|
resp.raise_for_status()
|
|
resp.raise_for_status()
|
|
|
data = resp.json()
|
|
data = resp.json()
|
|
@@ -204,18 +231,34 @@ async def toolhub_call(
|
|
|
result = data.get("result", {})
|
|
result = data.get("result", {})
|
|
|
result_str = json.dumps(result, ensure_ascii=False, indent=2)
|
|
result_str = json.dumps(result, ensure_ascii=False, indent=2)
|
|
|
|
|
|
|
|
- # 如果结果中有 base64 图片,提取为 images 附件
|
|
|
|
|
|
|
+ # 提取 base64 图片附件(单张 image 字段)
|
|
|
images = []
|
|
images = []
|
|
|
- if isinstance(result, dict) and result.get("image"):
|
|
|
|
|
- images.append({
|
|
|
|
|
- "type": "base64",
|
|
|
|
|
- "media_type": "image/png",
|
|
|
|
|
- "data": result["image"],
|
|
|
|
|
- })
|
|
|
|
|
- # 输出中替换超长 base64,避免占满 context
|
|
|
|
|
- result_display = {k: v for k, v in result.items() if k != "image"}
|
|
|
|
|
- result_display["image"] = f"<base64 image, {len(result['image'])} chars>"
|
|
|
|
|
- result_str = json.dumps(result_display, ensure_ascii=False, indent=2)
|
|
|
|
|
|
|
+ if isinstance(result, dict):
|
|
|
|
|
+ for img_key in ("image",):
|
|
|
|
|
+ if result.get(img_key):
|
|
|
|
|
+ images.append({
|
|
|
|
|
+ "type": "base64",
|
|
|
|
|
+ "media_type": "image/png",
|
|
|
|
|
+ "data": result[img_key],
|
|
|
|
|
+ })
|
|
|
|
|
+ result_display = {k: v for k, v in result.items() if k != img_key}
|
|
|
|
|
+ result_display[img_key] = f"<base64 image, {len(result[img_key])} chars>"
|
|
|
|
|
+ result_str = json.dumps(result_display, ensure_ascii=False, indent=2)
|
|
|
|
|
+
|
|
|
|
|
+ # images 列表字段(如 nano_banana 返回 images 数组)
|
|
|
|
|
+ if result.get("images") and isinstance(result["images"], list):
|
|
|
|
|
+ for img in result["images"]:
|
|
|
|
|
+ if isinstance(img, str) and len(img) > 100:
|
|
|
|
|
+ # data URL 或 base64
|
|
|
|
|
+ if img.startswith("data:"):
|
|
|
|
|
+ header, b64 = img.split(",", 1)
|
|
|
|
|
+ mime = header.split(";")[0].replace("data:", "")
|
|
|
|
|
+ images.append({"type": "base64", "media_type": mime, "data": b64})
|
|
|
|
|
+ else:
|
|
|
|
|
+ images.append({"type": "base64", "media_type": "image/png", "data": img})
|
|
|
|
|
+ result_display = {k: v for k, v in result.items() if k != "images"}
|
|
|
|
|
+ result_display["images"] = f"<{len(result['images'])} images>"
|
|
|
|
|
+ result_str = json.dumps(result_display, ensure_ascii=False, indent=2)
|
|
|
|
|
|
|
|
return ToolResult(
|
|
return ToolResult(
|
|
|
title=f"ToolHub [{tool_id}] 执行成功",
|
|
title=f"ToolHub [{tool_id}] 执行成功",
|
|
@@ -234,7 +277,7 @@ async def toolhub_call(
|
|
|
return ToolResult(
|
|
return ToolResult(
|
|
|
title=f"ToolHub [{tool_id}] 调用超时",
|
|
title=f"ToolHub [{tool_id}] 调用超时",
|
|
|
output="",
|
|
output="",
|
|
|
- error=f"调用工具 {tool_id} 超时(60s),工具可能需要更长处理时间。",
|
|
|
|
|
|
|
+ error=f"调用工具 {tool_id} 超时({CALL_TIMEOUT:.0f}s),图像生成类工具可能需要更长时间。",
|
|
|
)
|
|
)
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
return ToolResult(
|
|
return ToolResult(
|
|
@@ -242,121 +285,3 @@ async def toolhub_call(
|
|
|
output="",
|
|
output="",
|
|
|
error=str(e),
|
|
error=str(e),
|
|
|
)
|
|
)
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-@tool(
|
|
|
|
|
- display={
|
|
|
|
|
- "zh": {
|
|
|
|
|
- "name": "创建 ToolHub 工具",
|
|
|
|
|
- "params": {"description": "工具描述", "wait": "是否等待完成"},
|
|
|
|
|
- },
|
|
|
|
|
- "en": {
|
|
|
|
|
- "name": "Create ToolHub Tool",
|
|
|
|
|
- "params": {"description": "Tool description", "wait": "Wait for completion"},
|
|
|
|
|
- },
|
|
|
|
|
- }
|
|
|
|
|
-)
|
|
|
|
|
-async def toolhub_create(
|
|
|
|
|
- description: str,
|
|
|
|
|
- wait: bool = True,
|
|
|
|
|
-) -> ToolResult:
|
|
|
|
|
- """在 ToolHub 远程工具库中创建一个新工具
|
|
|
|
|
-
|
|
|
|
|
- 向 ToolHub 提交创建工具的请求。ToolHub 会根据描述自动生成工具代码、
|
|
|
|
|
- 构建运行环境并注册到工具库中。
|
|
|
|
|
-
|
|
|
|
|
- 创建过程是异步的:提交后返回 task_id,可通过轮询查看进度。
|
|
|
|
|
- 设置 wait=True(默认)会自动轮询直到完成或超时(10分钟)。
|
|
|
|
|
-
|
|
|
|
|
- Args:
|
|
|
|
|
- description: 工具的自然语言描述,说明工具应该做什么。
|
|
|
|
|
- 例如:"创建一个简单的文本计数工具,输入文本,返回字数和字符数"
|
|
|
|
|
- wait: 是否等待任务完成。True=轮询等待结果(最长10分钟),
|
|
|
|
|
- False=仅提交并返回 task_id
|
|
|
|
|
-
|
|
|
|
|
- Returns:
|
|
|
|
|
- ToolResult 包含创建结果或任务进度信息
|
|
|
|
|
- """
|
|
|
|
|
- try:
|
|
|
|
|
- payload = {"description": description}
|
|
|
|
|
-
|
|
|
|
|
- async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
|
|
|
|
|
- resp = await client.post(
|
|
|
|
|
- f"{TOOLHUB_BASE_URL}/create_tool", json=payload
|
|
|
|
|
- )
|
|
|
|
|
- resp.raise_for_status()
|
|
|
|
|
- data = resp.json()
|
|
|
|
|
-
|
|
|
|
|
- task_id = data.get("task_id")
|
|
|
|
|
- status = data.get("status")
|
|
|
|
|
-
|
|
|
|
|
- if not task_id:
|
|
|
|
|
- return ToolResult(
|
|
|
|
|
- title="创建 ToolHub 工具失败",
|
|
|
|
|
- output=json.dumps(data, ensure_ascii=False, indent=2),
|
|
|
|
|
- error="未返回 task_id",
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- # 不等待,直接返回 task_id
|
|
|
|
|
- if not wait:
|
|
|
|
|
- return ToolResult(
|
|
|
|
|
- title="ToolHub 工具创建已提交",
|
|
|
|
|
- output=f"task_id: {task_id}\nstatus: {status}",
|
|
|
|
|
- long_term_memory=f"Submitted ToolHub creation task {task_id}: {description[:80]}",
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- # 轮询等待完成
|
|
|
|
|
- start_time = time.time()
|
|
|
|
|
- async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
|
|
|
|
|
- while time.time() - start_time < CREATE_POLL_TIMEOUT:
|
|
|
|
|
- await _async_sleep(CREATE_POLL_INTERVAL)
|
|
|
|
|
-
|
|
|
|
|
- resp = await client.get(
|
|
|
|
|
- f"{TOOLHUB_BASE_URL}/tasks/{task_id}", timeout=DEFAULT_TIMEOUT
|
|
|
|
|
- )
|
|
|
|
|
- task = resp.json()
|
|
|
|
|
- task_status = task.get("status")
|
|
|
|
|
-
|
|
|
|
|
- if task_status == "completed":
|
|
|
|
|
- result = task.get("result", "")
|
|
|
|
|
- return ToolResult(
|
|
|
|
|
- title="ToolHub 工具创建完成",
|
|
|
|
|
- output=(
|
|
|
|
|
- f"task_id: {task_id}\n"
|
|
|
|
|
- f"result: {str(result)[:500]}\n\n"
|
|
|
|
|
- f"耗时: {time.time() - start_time:.0f}s\n"
|
|
|
|
|
- f"可使用 toolhub_search 查看新工具。"
|
|
|
|
|
- ),
|
|
|
|
|
- long_term_memory=f"ToolHub tool created (task {task_id}): {description[:60]}",
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- if task_status == "failed":
|
|
|
|
|
- error = task.get("error", "unknown")
|
|
|
|
|
- return ToolResult(
|
|
|
|
|
- title="ToolHub 工具创建失败",
|
|
|
|
|
- output=json.dumps(task, ensure_ascii=False, indent=2),
|
|
|
|
|
- error=f"Task {task_id} failed: {error}",
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- # 超时
|
|
|
|
|
- elapsed = time.time() - start_time
|
|
|
|
|
- return ToolResult(
|
|
|
|
|
- title="ToolHub 工具创建超时",
|
|
|
|
|
- output=f"task_id: {task_id}\n轮询 {elapsed:.0f}s 后仍未完成。",
|
|
|
|
|
- error=f"Task {task_id} 未在 {CREATE_POLL_TIMEOUT:.0f}s 内完成,请稍后查询。",
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- except Exception as e:
|
|
|
|
|
- return ToolResult(
|
|
|
|
|
- title="创建 ToolHub 工具失败",
|
|
|
|
|
- output="",
|
|
|
|
|
- error=str(e),
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-# ── 辅助函数 ─────────────────────────────────────────
|
|
|
|
|
-
|
|
|
|
|
-async def _async_sleep(seconds: float):
|
|
|
|
|
- """异步 sleep"""
|
|
|
|
|
- import asyncio
|
|
|
|
|
- await asyncio.sleep(seconds)
|
|
|