toolhub.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. """
  2. ToolHub - 远程工具库集成模块
  3. 将 http://43.106.118.91:8001 的工具库 API 包装为 Agent 可调用的工具。
  4. 提供四个工具:
  5. 1. toolhub_health - 健康检查
  6. 2. toolhub_search - 搜索/发现远程工具
  7. 3. toolhub_call - 调用远程工具
  8. 4. toolhub_create - 创建新远程工具(异步)
  9. 设计要点:
  10. - toolhub_call 的 params 是动态的(取决于 tool_id),用 dict 类型兼容所有工具
  11. - toolhub_create 是异步任务,支持轮询等待完成
  12. - 所有接口返回 ToolResult,包含结构化输出和 long_term_memory 摘要
  13. """
  14. import json
  15. import time
  16. from typing import Any, Dict, Optional
  17. import httpx
  18. from agent.tools import tool, ToolResult
  19. # ── 配置 ─────────────────────────────────────────────
  20. TOOLHUB_BASE_URL = "http://43.106.118.91:8001"
  21. DEFAULT_TIMEOUT = 30.0
  22. CREATE_POLL_TIMEOUT = 600.0 # create 最长轮询 10 分钟
  23. CREATE_POLL_INTERVAL = 5.0 # 每 5 秒轮询一次
  24. # ── 工具实现 ──────────────────────────────────────────
  25. @tool(
  26. display={
  27. "zh": {"name": "ToolHub 健康检查", "params": {}},
  28. "en": {"name": "ToolHub Health Check", "params": {}},
  29. }
  30. )
  31. async def toolhub_health() -> ToolResult:
  32. """检查 ToolHub 远程工具库服务是否可用
  33. 检查 ToolHub 服务的健康状态,确认服务是否正常运行。
  34. 建议在调用其他 toolhub 工具之前先检查。
  35. Returns:
  36. ToolResult 包含服务健康状态信息
  37. """
  38. try:
  39. async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
  40. resp = await client.get(f"{TOOLHUB_BASE_URL}/health")
  41. resp.raise_for_status()
  42. data = resp.json()
  43. return ToolResult(
  44. title="ToolHub 健康检查",
  45. output=json.dumps(data, ensure_ascii=False, indent=2),
  46. long_term_memory=f"ToolHub service at {TOOLHUB_BASE_URL} is healthy.",
  47. )
  48. except httpx.ConnectError:
  49. return ToolResult(
  50. title="ToolHub 健康检查",
  51. output="",
  52. error=f"无法连接到 ToolHub 服务 {TOOLHUB_BASE_URL},请确认服务已启动。",
  53. )
  54. except Exception as e:
  55. return ToolResult(
  56. title="ToolHub 健康检查",
  57. output="",
  58. error=str(e),
  59. )
  60. @tool(
  61. display={
  62. "zh": {"name": "搜索 ToolHub 工具", "params": {"keyword": "搜索关键词"}},
  63. "en": {"name": "Search ToolHub", "params": {"keyword": "Search keyword"}},
  64. }
  65. )
  66. async def toolhub_search(keyword: Optional[str] = None) -> ToolResult:
  67. """搜索 ToolHub 远程工具库中可用的工具
  68. 从 ToolHub 工具库中搜索可用工具,返回每个工具的完整信息,包括:
  69. tool_id、名称、分类、状态、参数列表(含类型、是否必填、默认值、枚举值)、输出 schema 等。
  70. 调用 toolhub_call 之前,应先使用此工具了解目标工具的 tool_id 和所需参数。
  71. Args:
  72. keyword: 搜索关键词,为空则返回所有工具
  73. Returns:
  74. ToolResult 包含匹配的工具列表及其参数说明
  75. """
  76. try:
  77. payload = {"keyword": keyword} if keyword else {}
  78. async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
  79. resp = await client.post(
  80. f"{TOOLHUB_BASE_URL}/search_tools", json=payload
  81. )
  82. resp.raise_for_status()
  83. data = resp.json()
  84. total = data.get("total", 0)
  85. tools = data.get("tools", [])
  86. # 构建给 LLM 的结构化摘要
  87. summaries = []
  88. for t in tools:
  89. params_desc = []
  90. for p in t.get("params", []):
  91. req = "必填" if p["required"] else "可选"
  92. desc = p.get("description", "")
  93. default_str = f", 默认={p['default']}" if p.get("default") is not None else ""
  94. enum_str = f", 可选值={p['enum']}" if p.get("enum") else ""
  95. params_desc.append(
  96. f" - {p['name']} ({p['type']}, {req}): {desc}{default_str}{enum_str}"
  97. )
  98. tool_block = (
  99. f"[{t['tool_id']}] {t['name']}\n"
  100. f" 状态: {t['state']}\n"
  101. f" 描述: {t.get('description', '')}\n"
  102. f" 流式: {t.get('stream_support', False)}"
  103. )
  104. if params_desc:
  105. tool_block += "\n 参数:\n" + "\n".join(params_desc)
  106. else:
  107. tool_block += "\n 参数: 无"
  108. summaries.append(tool_block)
  109. output_text = f"共找到 {total} 个工具:\n\n" + "\n\n".join(summaries)
  110. # 附上完整 JSON 供精确引用
  111. full_json = json.dumps(data, ensure_ascii=False, indent=2)
  112. return ToolResult(
  113. title=f"ToolHub 搜索{f': {keyword}' if keyword else ''}",
  114. output=f"{output_text}\n\n--- 完整数据 ---\n{full_json}",
  115. long_term_memory=(
  116. f"ToolHub {'搜索 ' + repr(keyword) + ' ' if keyword else ''}"
  117. f"共 {total} 个工具: "
  118. + ", ".join(t["tool_id"] for t in tools[:10])
  119. + ("..." if total > 10 else "")
  120. ),
  121. )
  122. except Exception as e:
  123. return ToolResult(
  124. title="搜索 ToolHub 工具失败",
  125. output="",
  126. error=str(e),
  127. )
  128. @tool(
  129. display={
  130. "zh": {
  131. "name": "调用 ToolHub 工具",
  132. "params": {"tool_id": "工具ID", "params": "工具参数"},
  133. },
  134. "en": {
  135. "name": "Call ToolHub Tool",
  136. "params": {"tool_id": "Tool ID", "params": "Tool parameters"},
  137. },
  138. }
  139. )
  140. async def toolhub_call(
  141. tool_id: str,
  142. params: Optional[Dict[str, Any]] = None,
  143. ) -> ToolResult:
  144. """调用 ToolHub 远程工具库中的指定工具
  145. 通过 tool_id 调用 ToolHub 工具库中的某个工具,传入该工具所需的参数。
  146. 不同工具的参数不同,请先用 toolhub_search 查询目标工具的参数说明。
  147. 参数通过 params 字典传入,键名和类型需与工具定义一致。
  148. 例如调用图片拼接工具:
  149. tool_id="image_stitcher"
  150. params={"images": [...], "direction": "grid", "columns": 2}
  151. Args:
  152. tool_id: 要调用的工具 ID(从 toolhub_search 获取)
  153. params: 工具参数字典,键值对根据目标工具的参数定义决定
  154. Returns:
  155. ToolResult 包含工具执行结果
  156. """
  157. try:
  158. payload = {
  159. "tool_id": tool_id,
  160. "params": params or {},
  161. }
  162. async with httpx.AsyncClient(timeout=60.0) as client:
  163. resp = await client.post(
  164. f"{TOOLHUB_BASE_URL}/select_tool", json=payload
  165. )
  166. resp.raise_for_status()
  167. data = resp.json()
  168. status = data.get("status")
  169. if status == "success":
  170. result = data.get("result", {})
  171. result_str = json.dumps(result, ensure_ascii=False, indent=2)
  172. # 如果结果中有 base64 图片,提取为 images 附件
  173. images = []
  174. if isinstance(result, dict) and result.get("image"):
  175. images.append({
  176. "type": "base64",
  177. "media_type": "image/png",
  178. "data": result["image"],
  179. })
  180. # 输出中替换超长 base64,避免占满 context
  181. result_display = {k: v for k, v in result.items() if k != "image"}
  182. result_display["image"] = f"<base64 image, {len(result['image'])} chars>"
  183. result_str = json.dumps(result_display, ensure_ascii=False, indent=2)
  184. return ToolResult(
  185. title=f"ToolHub [{tool_id}] 执行成功",
  186. output=result_str,
  187. long_term_memory=f"Called ToolHub tool '{tool_id}' → success",
  188. images=images,
  189. )
  190. else:
  191. error_msg = data.get("error", "未知错误")
  192. return ToolResult(
  193. title=f"ToolHub [{tool_id}] 执行失败",
  194. output=json.dumps(data, ensure_ascii=False, indent=2),
  195. error=error_msg,
  196. )
  197. except httpx.TimeoutException:
  198. return ToolResult(
  199. title=f"ToolHub [{tool_id}] 调用超时",
  200. output="",
  201. error=f"调用工具 {tool_id} 超时(60s),工具可能需要更长处理时间。",
  202. )
  203. except Exception as e:
  204. return ToolResult(
  205. title=f"ToolHub [{tool_id}] 调用失败",
  206. output="",
  207. error=str(e),
  208. )
  209. @tool(
  210. display={
  211. "zh": {
  212. "name": "创建 ToolHub 工具",
  213. "params": {"description": "工具描述", "wait": "是否等待完成"},
  214. },
  215. "en": {
  216. "name": "Create ToolHub Tool",
  217. "params": {"description": "Tool description", "wait": "Wait for completion"},
  218. },
  219. }
  220. )
  221. async def toolhub_create(
  222. description: str,
  223. wait: bool = True,
  224. ) -> ToolResult:
  225. """在 ToolHub 远程工具库中创建一个新工具
  226. 向 ToolHub 提交创建工具的请求。ToolHub 会根据描述自动生成工具代码、
  227. 构建运行环境并注册到工具库中。
  228. 创建过程是异步的:提交后返回 task_id,可通过轮询查看进度。
  229. 设置 wait=True(默认)会自动轮询直到完成或超时(10分钟)。
  230. Args:
  231. description: 工具的自然语言描述,说明工具应该做什么。
  232. 例如:"创建一个简单的文本计数工具,输入文本,返回字数和字符数"
  233. wait: 是否等待任务完成。True=轮询等待结果(最长10分钟),
  234. False=仅提交并返回 task_id
  235. Returns:
  236. ToolResult 包含创建结果或任务进度信息
  237. """
  238. try:
  239. payload = {"description": description}
  240. async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
  241. resp = await client.post(
  242. f"{TOOLHUB_BASE_URL}/create_tool", json=payload
  243. )
  244. resp.raise_for_status()
  245. data = resp.json()
  246. task_id = data.get("task_id")
  247. status = data.get("status")
  248. if not task_id:
  249. return ToolResult(
  250. title="创建 ToolHub 工具失败",
  251. output=json.dumps(data, ensure_ascii=False, indent=2),
  252. error="未返回 task_id",
  253. )
  254. # 不等待,直接返回 task_id
  255. if not wait:
  256. return ToolResult(
  257. title="ToolHub 工具创建已提交",
  258. output=f"task_id: {task_id}\nstatus: {status}",
  259. long_term_memory=f"Submitted ToolHub creation task {task_id}: {description[:80]}",
  260. )
  261. # 轮询等待完成
  262. start_time = time.time()
  263. async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
  264. while time.time() - start_time < CREATE_POLL_TIMEOUT:
  265. await _async_sleep(CREATE_POLL_INTERVAL)
  266. resp = await client.get(
  267. f"{TOOLHUB_BASE_URL}/tasks/{task_id}", timeout=DEFAULT_TIMEOUT
  268. )
  269. task = resp.json()
  270. task_status = task.get("status")
  271. if task_status == "completed":
  272. result = task.get("result", "")
  273. return ToolResult(
  274. title="ToolHub 工具创建完成",
  275. output=(
  276. f"task_id: {task_id}\n"
  277. f"result: {str(result)[:500]}\n\n"
  278. f"耗时: {time.time() - start_time:.0f}s\n"
  279. f"可使用 toolhub_search 查看新工具。"
  280. ),
  281. long_term_memory=f"ToolHub tool created (task {task_id}): {description[:60]}",
  282. )
  283. if task_status == "failed":
  284. error = task.get("error", "unknown")
  285. return ToolResult(
  286. title="ToolHub 工具创建失败",
  287. output=json.dumps(task, ensure_ascii=False, indent=2),
  288. error=f"Task {task_id} failed: {error}",
  289. )
  290. # 超时
  291. elapsed = time.time() - start_time
  292. return ToolResult(
  293. title="ToolHub 工具创建超时",
  294. output=f"task_id: {task_id}\n轮询 {elapsed:.0f}s 后仍未完成。",
  295. error=f"Task {task_id} 未在 {CREATE_POLL_TIMEOUT:.0f}s 内完成,请稍后查询。",
  296. )
  297. except Exception as e:
  298. return ToolResult(
  299. title="创建 ToolHub 工具失败",
  300. output="",
  301. error=str(e),
  302. )
  303. # ── 辅助函数 ─────────────────────────────────────────
  304. async def _async_sleep(seconds: float):
  305. """异步 sleep"""
  306. import asyncio
  307. await asyncio.sleep(seconds)