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.
This commit is contained in:
chiguyong 2026-06-16 07:52:04 +08:00
parent f770d65c7b
commit 87c59bb3e2
3 changed files with 150 additions and 4 deletions

View File

@ -21,6 +21,7 @@ from agentkit.skills.base import Skill
from agentkit.skills.registry import SkillRegistry from agentkit.skills.registry import SkillRegistry
from agentkit.tools.registry import ToolRegistry from agentkit.tools.registry import ToolRegistry
from agentkit.tools.skill_install import SkillInstallTool 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.config import ServerConfig, load_dotenv
from agentkit.server.routes import ( from agentkit.server.routes import (
agents, agents,
@ -174,9 +175,8 @@ async def lifespan(app: FastAPI):
"中文内容优先使用 baidu_search 工具,英文/国际内容使用 web_search。" "中文内容优先使用 baidu_search 工具,英文/国际内容使用 web_search。"
"在能够搜索到真相的情况下,绝不猜测或编造答案。" "在能够搜索到真相的情况下,绝不猜测或编造答案。"
"始终优先搜索而不是给出可能不正确的信息。\n\n" "始终优先搜索而不是给出可能不正确的信息。\n\n"
"技能安装:当需要安装技能时,使用 skill_install 工具,不要用 shell 执行 npm install。" "技能安装:当需要查找或安装技能时,先用 skill_search 搜索确认技能名称和来源,"
"skill_install 的 source 参数格式为 owner/repo@skill例如 vercel-labs/skills@find-skills。" "再用 skill_install 安装。不要用 shell 执行 npm install 或 npx skills install。"
"如果不知道完整 source先用 shell 执行 `npx skills search <name>` 搜索。"
) )
effective_system_prompt = memory_store.build_system_prompt(memory_snapshot, base_prompt) 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, tool_registry=app.state.tool_registry,
) )
) )
agent._tool_registry.register(SkillSearchTool())
agent._tool_registry.register(BaiduSearchTool()) agent._tool_registry.register(BaiduSearchTool())
agent._tool_registry.register(WebSearchTool(**search_api_keys)) agent._tool_registry.register(WebSearchTool(**search_api_keys))
agent._tool_registry.register(WebCrawlTool()) agent._tool_registry.register(WebCrawlTool())

View File

@ -24,7 +24,11 @@ class SkillInstallTool(Tool):
def __init__( def __init__(
self, self,
name: str = "skill_install", 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, input_schema: dict[str, Any] | None = None,
output_schema: dict[str, Any] | None = None, output_schema: dict[str, Any] | None = None,
version: str = "1.0.0", version: str = "1.0.0",

View File

@ -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 <keyword>` 搜索可用技能
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()}"