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:
parent
f770d65c7b
commit
87c59bb3e2
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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()}"
|
||||||
Loading…
Reference in New Issue