Przeglądaj źródła

feat: update toolhub.py

elksmmx 17 godzin temu
rodzic
commit
95a892d9aa
2 zmienionych plików z 99 dodań i 175 usunięć
  1. 1 2
      agent/tools/builtin/__init__.py
  2. 98 173
      agent/tools/builtin/toolhub.py

+ 1 - 2
agent/tools/builtin/__init__.py

@@ -21,7 +21,7 @@ from agent.tools.builtin.sandbox import (sandbox_create_environment, sandbox_run
 from agent.tools.builtin.knowledge import(knowledge_search,knowledge_save,knowledge_list,knowledge_update,knowledge_batch_update,knowledge_slim)
 from agent.tools.builtin.knowledge_manager import ask_knowledge, upload_knowledge
 from agent.tools.builtin.context import get_current_context
-from agent.tools.builtin.toolhub import toolhub_health, toolhub_search, toolhub_call, toolhub_create
+from agent.tools.builtin.toolhub import toolhub_health, toolhub_search, toolhub_call
 from agent.tools.builtin.resource import resource_list_tools, resource_get_tool
 from agent.tools.builtin.crawler import youtube_search, youtube_detail, x_search, import_content, extract_video_clip
 from agent.trace.goal_tool import goal
@@ -67,7 +67,6 @@ __all__ = [
     "toolhub_health",
     "toolhub_search",
     "toolhub_call",
-    "toolhub_create",
     # 资源查询
     "resource_list_tools",
     "resource_get_tool",

+ 98 - 173
agent/tools/builtin/toolhub.py

@@ -2,20 +2,19 @@
 ToolHub - 远程工具库集成模块
 
 将 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 time
 from typing import Any, Dict, Optional
 
 import httpx
@@ -27,8 +26,7 @@ from agent.tools import tool, ToolResult
 
 TOOLHUB_BASE_URL = "http://43.106.118.91:8001"
 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 包含服务健康状态信息
     """
     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.raise_for_status()
             data = resp.json()
@@ -82,47 +80,65 @@ async def toolhub_health() -> ToolResult:
 async def toolhub_search(keyword: Optional[str] = None) -> ToolResult:
     """搜索 ToolHub 远程工具库中可用的工具
 
-    从 ToolHub 工具库中搜索可用工具,返回每个工具的完整信息,包括:
-    tool_id、名称、分类、状态、参数列表(含类型、是否必填、默认值、枚举值)、输出 schema 等。
+    从 ToolHub 工具库中获取可用工具列表,返回每个工具的完整信息,包括:
+    tool_id、名称、分类、状态、参数列表(含类型、是否必填、默认值)、输出 schema、
+    分组信息(如 RunComfy 生命周期组)等。
 
     调用 toolhub_call 之前,应先使用此工具了解目标工具的 tool_id 和所需参数。
+    不填 keyword 则返回所有工具。
 
     Args:
-        keyword: 搜索关键词,为空则返回所有工具
+        keyword: 搜索关键词,用于过滤工具名称或描述(客户端过滤);为空则返回所有工具
 
     Returns:
         ToolResult 包含匹配的工具列表及其参数说明
     """
     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()
             data = resp.json()
 
-        total = data.get("total", 0)
         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 的结构化摘要
         summaries = []
         for t in tools:
+            input_props = t.get("input_schema", {}).get("properties", {})
+            required_fields = t.get("input_schema", {}).get("required", [])
             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(
-                    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 = (
                 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:
                 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)
 
-        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(
-            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=(
-                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:
@@ -175,6 +199,9 @@ async def toolhub_call(
     通过 tool_id 调用 ToolHub 工具库中的某个工具,传入该工具所需的参数。
     不同工具的参数不同,请先用 toolhub_search 查询目标工具的参数说明。
 
+    注意:部分工具为异步生命周期(如 RunComfy、即梦、FLUX),需要按分组顺序
+    依次调用多个工具(如先 launch → 再 executor → 再 stop)。
+
     参数通过 params 字典传入,键名和类型需与工具定义一致。
     例如调用图片拼接工具:
         tool_id="image_stitcher"
@@ -192,9 +219,9 @@ async def toolhub_call(
             "tool_id": tool_id,
             "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(
-                f"{TOOLHUB_BASE_URL}/select_tool", json=payload
+                f"{TOOLHUB_BASE_URL}/run_tool", json=payload
             )
             resp.raise_for_status()
             data = resp.json()
@@ -204,18 +231,34 @@ async def toolhub_call(
             result = data.get("result", {})
             result_str = json.dumps(result, ensure_ascii=False, indent=2)
 
-            # 如果结果中有 base64 图片,提取为 images 附件
+            # 提取 base64 图片附件(单张 image 字段)
             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(
                 title=f"ToolHub [{tool_id}] 执行成功",
@@ -234,7 +277,7 @@ async def toolhub_call(
         return ToolResult(
             title=f"ToolHub [{tool_id}] 调用超时",
             output="",
-            error=f"调用工具 {tool_id} 超时(60s),工具可能需要更长处理时间。",
+            error=f"调用工具 {tool_id} 超时({CALL_TIMEOUT:.0f}s),图像生成类工具可能需要更长时间。",
         )
     except Exception as e:
         return ToolResult(
@@ -242,121 +285,3 @@ async def toolhub_call(
             output="",
             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)