From 87c59bb3e29cd7931d8bcc4c78359a49008b0bac Mon Sep 17 00:00:00 2001 From: chiguyong Date: Tue, 16 Jun 2026 07:52:04 +0800 Subject: [PATCH] feat(tools): add SkillSearchTool and improve skill_install workflow Add skill_search tool so agent can search for skills before installing. Update skill_install description to guide LLM to search first. Update system prompt to use skill_search -> skill_install flow. This fixes the issue where agent returns empty when asked to find a skill. --- src/agentkit/server/app.py | 7 +- src/agentkit/tools/skill_install.py | 6 +- src/agentkit/tools/skill_search.py | 141 ++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 src/agentkit/tools/skill_search.py diff --git a/src/agentkit/server/app.py b/src/agentkit/server/app.py index d84577b..d3b161d 100644 --- a/src/agentkit/server/app.py +++ b/src/agentkit/server/app.py @@ -21,6 +21,7 @@ from agentkit.skills.base import Skill from agentkit.skills.registry import SkillRegistry from agentkit.tools.registry import ToolRegistry from agentkit.tools.skill_install import SkillInstallTool +from agentkit.tools.skill_search import SkillSearchTool from agentkit.server.config import ServerConfig, load_dotenv from agentkit.server.routes import ( agents, @@ -174,9 +175,8 @@ async def lifespan(app: FastAPI): "中文内容优先使用 baidu_search 工具,英文/国际内容使用 web_search。" "在能够搜索到真相的情况下,绝不猜测或编造答案。" "始终优先搜索而不是给出可能不正确的信息。\n\n" - "技能安装:当需要安装技能时,使用 skill_install 工具,不要用 shell 执行 npm install。" - "skill_install 的 source 参数格式为 owner/repo@skill,例如 vercel-labs/skills@find-skills。" - "如果不知道完整 source,先用 shell 执行 `npx skills search ` 搜索。" + "技能安装:当需要查找或安装技能时,先用 skill_search 搜索确认技能名称和来源," + "再用 skill_install 安装。不要用 shell 执行 npm install 或 npx skills install。" ) effective_system_prompt = memory_store.build_system_prompt(memory_snapshot, base_prompt) @@ -227,6 +227,7 @@ async def lifespan(app: FastAPI): tool_registry=app.state.tool_registry, ) ) + agent._tool_registry.register(SkillSearchTool()) agent._tool_registry.register(BaiduSearchTool()) agent._tool_registry.register(WebSearchTool(**search_api_keys)) agent._tool_registry.register(WebCrawlTool()) diff --git a/src/agentkit/tools/skill_install.py b/src/agentkit/tools/skill_install.py index 284f8eb..0fb50a4 100644 --- a/src/agentkit/tools/skill_install.py +++ b/src/agentkit/tools/skill_install.py @@ -24,7 +24,11 @@ class SkillInstallTool(Tool): def __init__( self, name: str = "skill_install", - description: str = "安装 Agent 技能包。使用 npx skills install 安装指定技能,不要用 npm install。", + description: str = ( + "安装 Agent 技能包。使用 npx skills install 安装指定技能,不要用 npm install。" + "重要:安装前应先用 skill_search 工具搜索确认技能名称和来源(source)。" + "如果用户只提供了模糊名称,先用 skill_search 搜索,再根据搜索结果安装。" + ), input_schema: dict[str, Any] | None = None, output_schema: dict[str, Any] | None = None, version: str = "1.0.0", diff --git a/src/agentkit/tools/skill_search.py b/src/agentkit/tools/skill_search.py new file mode 100644 index 0000000..b072ae0 --- /dev/null +++ b/src/agentkit/tools/skill_search.py @@ -0,0 +1,141 @@ +"""SkillSearchTool - Agent 可调用的技能搜索工具""" + +import asyncio +import logging +from typing import Any + +from agentkit.tools.base import Tool + +logger = logging.getLogger(__name__) + + +class SkillSearchTool(Tool): + """技能搜索工具 + + 让 Agent 可以搜索可用的技能包,在安装之前先确认技能是否存在。 + 使用 `npx skills search ` 搜索可用技能。 + + Usage: + tool = SkillSearchTool() + result = await tool.execute(keyword="ppt") + """ + + def __init__( + self, + name: str = "skill_search", + description: str = ( + "搜索可用的 Agent 技能包。在安装技能之前,应先使用此工具搜索确认技能名称和来源。" + "返回匹配的技能列表,包含名称、描述和安装来源。" + ), + input_schema: dict[str, Any] | None = None, + output_schema: dict[str, Any] | None = None, + version: str = "1.0.0", + tags: list[str] | None = None, + ): + schema = input_schema or { + "type": "object", + "properties": { + "keyword": { + "type": "string", + "description": "搜索关键词,如 ppt、code-review、find-skills", + }, + }, + "required": ["keyword"], + } + super().__init__( + name=name, + description=description, + input_schema=schema, + output_schema=output_schema, + version=version, + tags=tags or ["skill", "search"], + ) + + async def execute(self, **kwargs) -> dict: + keyword = kwargs.get("keyword", "").strip() + + if not keyword: + return { + "output": "错误: 必须提供 keyword 参数", + "exit_code": 1, + "is_error": True, + } + + try: + proc = await asyncio.create_subprocess_exec( + "npx", "skills@latest", "search", keyword, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=60) + output = stdout.decode("utf-8", errors="replace") + error_output = stderr.decode("utf-8", errors="replace") + + if output.strip(): + # Parse and format the search results + results = self._format_search_results(output, keyword) + return { + "output": results, + "exit_code": 0, + "is_error": False, + } + elif error_output.strip(): + # Some versions output to stderr + results = self._format_search_results(error_output, keyword) + return { + "output": results, + "exit_code": 0, + "is_error": False, + } + else: + return { + "output": ( + f"未找到与 '{keyword}' 匹配的技能。\n\n" + "建议:\n" + "1. 尝试不同的关键词搜索\n" + "2. 检查技能名称拼写\n" + "3. 访问 https://github.com/topics/agent-skills 浏览可用技能" + ), + "exit_code": 0, + "is_error": False, + } + except asyncio.TimeoutError: + return { + "output": f"搜索技能 '{keyword}' 超时(60s),请稍后重试", + "exit_code": -1, + "is_error": True, + } + except FileNotFoundError: + return { + "output": ( + "npx 命令未找到,请确保 Node.js 已安装。\n" + "安装方式: https://nodejs.org/" + ), + "exit_code": -1, + "is_error": True, + } + except Exception as e: + return { + "output": f"搜索技能 '{keyword}' 时发生异常: {e}", + "exit_code": -1, + "is_error": True, + } + + @staticmethod + def _format_search_results(raw_output: str, keyword: str) -> str: + """Format raw npx skills search output into a readable result.""" + # Clean up ANSI escape codes + import re + clean = re.sub(r"\x1b\[[0-9;]*m", "", raw_output) + + if not clean.strip(): + return ( + f"未找到与 '{keyword}' 匹配的技能。\n\n" + "建议:\n" + "1. 尝试不同的关键词搜索\n" + "2. 检查技能名称拼写\n" + "3. 访问 https://github.com/topics/agent-skills 浏览可用技能" + ) + + # Return the cleaned output with a helpful header + return f"搜索 '{keyword}' 的结果:\n\n{clean.strip()}"