fischer-agentkit/src/agentkit/tools/skill_install.py

174 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""SkillInstallTool - Agent 可调用的技能安装工具"""
import asyncio
import logging
import os
from typing import Any, Callable, Awaitable
from agentkit.tools.base import Tool
logger = logging.getLogger(__name__)
class SkillInstallTool(Tool):
"""技能安装工具
让 Agent 可以通过正确的命令安装技能,而不是用 shell 执行 npm install。
使用 `npx skills install <owner/repo@skill>` 安装技能,然后注册到 skill_registry。
Usage:
tool = SkillInstallTool()
result = await tool.execute(name="find-skills", source="vercel-labs/skills@find-skills")
"""
def __init__(
self,
name: str = "skill_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",
tags: list[str] | None = None,
confirm_callback: Callable[[str], Awaitable[bool]] | None = None,
skill_registry=None,
tool_registry=None,
):
schema = input_schema or {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "技能名称,如 find-skills",
},
"source": {
"type": "string",
"description": "技能来源,格式为 owner/repo@skill如 vercel-labs/skills@find-skills。如果不提供则用 name 搜索",
},
},
"required": ["name"],
}
super().__init__(
name=name,
description=description,
input_schema=schema,
output_schema=output_schema,
version=version,
tags=tags,
)
self._confirm_callback = confirm_callback
self._skill_registry = skill_registry
self._tool_registry = tool_registry
async def execute(self, **kwargs) -> dict:
name = kwargs.get("name", "").strip()
source = kwargs.get("source", "").strip()
if not name:
return {
"output": "错误: 必须提供 name 参数",
"exit_code": 1,
"is_error": True,
}
# Build the install command
if source:
install_target = source
else:
install_target = name
# Request confirmation before installing
if self._confirm_callback:
confirmed = await self._confirm_callback(f"npx skills install {install_target}")
if not confirmed:
return {
"output": f"技能安装被用户拒绝: {install_target}",
"exit_code": 126,
"is_error": True,
}
try:
proc = await asyncio.create_subprocess_exec(
"npx", "skills@latest", "install", install_target, "-y",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=120)
output = stdout.decode("utf-8", errors="replace")
error_output = stderr.decode("utf-8", errors="replace")
if proc.returncode == 0:
# Try to register the skill into skill_registry
registration_msg = ""
if self._skill_registry:
registration_msg = self._try_register_skill(name)
return {
"output": f"技能 {name} 安装成功\n{output}\n{registration_msg}",
"exit_code": 0,
"is_error": False,
}
else:
return {
"output": f"技能 {name} 安装失败 (exit={proc.returncode})\n{error_output}\n{output}",
"exit_code": proc.returncode,
"is_error": True,
"suggestions": [
"检查技能名称是否正确",
"使用 source 参数指定完整路径,如 vercel-labs/skills@find-skills",
"运行 npx skills search <name> 搜索可用技能",
],
}
except asyncio.TimeoutError:
return {
"output": f"技能 {name} 安装超时120s",
"exit_code": -1,
"is_error": True,
}
except Exception as e:
return {
"output": f"技能 {name} 安装异常: {e}",
"exit_code": -1,
"is_error": True,
}
def _try_register_skill(self, name: str) -> str:
"""Try to find and register the installed skill YAML into skill_registry."""
try:
from agentkit.skills.loader import SkillLoader
for search_dir in [os.path.join(os.getcwd(), ".agents", "skills"),
os.path.join(os.path.expanduser("~"), ".agents", "skills"),
os.path.join(os.getcwd(), "configs", "skills")]:
yaml_path = os.path.join(search_dir, f"{name}.yaml")
if os.path.exists(yaml_path):
loader = SkillLoader(
skill_registry=self._skill_registry,
tool_registry=self._tool_registry,
)
loader.load_from_file(yaml_path)
return f"技能已注册到系统(来源: {yaml_path}"
# Also check for directory-based skills
for search_dir in [os.path.join(os.getcwd(), ".agents", "skills"),
os.path.join(os.path.expanduser("~"), ".agents", "skills")]:
skill_dir = os.path.join(search_dir, name)
if os.path.isdir(skill_dir):
for fname in os.listdir(skill_dir):
if fname.endswith((".yaml", ".yml")):
yaml_path = os.path.join(skill_dir, fname)
loader = SkillLoader(
skill_registry=self._skill_registry,
tool_registry=self._tool_registry,
)
loader.load_from_file(yaml_path)
return f"技能已注册到系统(来源: {yaml_path}"
return "技能文件已下载,但未找到 YAML 配置文件进行注册。可能需要重启服务。"
except Exception as e:
logger.warning(f"Failed to register skill {name}: {e}")
return f"技能文件已下载,但注册失败: {e}"