skill.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. """
  2. Skill 工具 - 按需加载 Skill 文件
  3. Agent 可以调用此工具来加载特定的 skill 文档
  4. """
  5. import os
  6. import subprocess
  7. from pathlib import Path
  8. from typing import Optional
  9. from cyber_agent.tools import tool, ToolResult
  10. from cyber_agent.skill.skill_loader import SkillLoader
  11. # 默认 skills 目录(优先级:项目 skills > 框架 skills)
  12. DEFAULT_SKILLS_DIRS = [
  13. os.getenv("SKILLS_DIR", "./skills"), # 项目特定 skills(优先)
  14. "./cyber_agent/skill/skills" # 框架内置 skills
  15. ]
  16. # 默认单一目录(用于 list_skills)
  17. DEFAULT_SKILLS_DIR = DEFAULT_SKILLS_DIRS[0]
  18. def _check_skill_setup(skill_name: str) -> Optional[str]:
  19. """
  20. 检查 skill 的环境配置,返回缺失依赖的警告信息
  21. Args:
  22. skill_name: Skill 名称
  23. Returns:
  24. 警告信息(如果有缺失的依赖),否则返回 None
  25. """
  26. # 特殊处理:browser-use skill
  27. if skill_name in ["browser-use", "browser_use"]:
  28. try:
  29. # 动态导入 browser-use skill 的 setup 模块
  30. from cyber_agent.skill.skills.browser_use.setup import (
  31. _check_browser_use_cli,
  32. _check_chromium_installed
  33. )
  34. cli_installed = _check_browser_use_cli()
  35. chromium_installed = _check_chromium_installed()
  36. if not cli_installed or not chromium_installed:
  37. warning = "\n⚠️ **Setup Required**\n\n"
  38. warning += "The following dependencies are missing:\n\n"
  39. if not cli_installed:
  40. warning += "- `pip install browser-use`\n"
  41. if not chromium_installed:
  42. warning += "- `uvx browser-use install`\n"
  43. warning += "\nYou can also use the setup tools:\n"
  44. warning += "- `check_browser_use()` - Check dependency status\n"
  45. warning += "- `install_browser_use_chromium()` - Auto-install Chromium\n\n"
  46. return warning
  47. except ImportError:
  48. # Setup 模块不存在,跳过检查
  49. pass
  50. return None
  51. @tool(
  52. description="加载指定的 skill 文档。Skills 提供领域知识和最佳实践指导。",
  53. groups=["core"],
  54. )
  55. async def skill(
  56. skill_name: str,
  57. skills_dir: Optional[str] = None,
  58. ) -> ToolResult:
  59. """
  60. 加载指定的 skill 文档
  61. Args:
  62. skill_name: Skill 名称(如 "browser-use", "error-handling")
  63. skills_dir: Skills 目录路径(可选,默认按优先级查找)
  64. Returns:
  65. ToolResult: 包含 skill 的详细内容
  66. 加载顺序:
  67. 1. 如果指定 skills_dir,只在该目录查找
  68. 2. 否则按优先级查找:./skills/ (项目) -> ./config/skills/ (框架)
  69. """
  70. # 确定要搜索的目录列表
  71. if skills_dir:
  72. search_paths = [Path(skills_dir)]
  73. else:
  74. search_paths = [Path(d) for d in DEFAULT_SKILLS_DIRS]
  75. # 在目录中查找 skill 文件
  76. skill_file = None
  77. found_in_dir = None
  78. for skills_path in search_paths:
  79. if not skills_path.exists():
  80. continue
  81. # 查找文件(支持 skill-name.md 或 skill_name.md)
  82. for ext in [".md"]:
  83. for name_format in [skill_name, skill_name.replace("-", "_"), skill_name.replace("_", "-")]:
  84. candidate = skills_path / f"{name_format}{ext}"
  85. if candidate.exists():
  86. skill_file = candidate
  87. found_in_dir = skills_path
  88. break
  89. if skill_file:
  90. break
  91. if skill_file:
  92. break
  93. if not skill_file:
  94. # 列出所有可用的 skills
  95. available_skills = []
  96. for skills_path in search_paths:
  97. if skills_path.exists():
  98. available_skills.extend([f.stem for f in skills_path.glob("**/*.md")])
  99. return ToolResult(
  100. title=f"Skill '{skill_name}' 未找到",
  101. output=f"可用的 skills: {', '.join(set(available_skills))}\n\n"
  102. f"查找路径: {', '.join([str(p) for p in search_paths])}",
  103. error=f"Skill not found: {skill_name}"
  104. )
  105. # 加载 skill
  106. try:
  107. loader = SkillLoader(str(skills_path))
  108. skill_obj = loader.load_file(skill_file)
  109. if not skill_obj:
  110. return ToolResult(
  111. title="加载失败",
  112. output=f"无法解析 skill 文件: {skill_file.name}",
  113. error="Failed to parse skill file"
  114. )
  115. # 格式化输出
  116. output = f"# {skill_obj.name}\n\n"
  117. output += f"**Category**: {skill_obj.category}\n\n"
  118. if skill_obj.description:
  119. output += f"## Description\n\n{skill_obj.description}\n\n"
  120. # 检查 skill 的环境配置
  121. setup_warning = _check_skill_setup(skill_name)
  122. if setup_warning:
  123. output += setup_warning
  124. if skill_obj.guidelines:
  125. output += f"## Guidelines\n\n"
  126. for i, guideline in enumerate(skill_obj.guidelines, 1):
  127. output += f"{i}. {guideline}\n"
  128. output += "\n"
  129. return ToolResult(
  130. title=f"Skill: {skill_obj.name}",
  131. output=output,
  132. long_term_memory=f"已加载 skill: {skill_obj.name} ({skill_obj.category}) from {found_in_dir}",
  133. include_output_only_once=True, # skill 内容只展示一次
  134. metadata={
  135. "skill_name": skill_obj.name,
  136. "category": skill_obj.category,
  137. "scope": skill_obj.scope,
  138. "guidelines_count": len(skill_obj.guidelines),
  139. "loaded_from": str(found_in_dir)
  140. }
  141. )
  142. except Exception as e:
  143. return ToolResult(
  144. title="加载错误",
  145. output=f"加载 skill 时出错: {str(e)}",
  146. error=str(e)
  147. )
  148. @tool(
  149. description="列出所有可用的 skills",
  150. groups=["core"],
  151. )
  152. async def list_skills(
  153. skills_dir: Optional[str] = None,
  154. ) -> ToolResult:
  155. """
  156. 列出所有可用的 skills
  157. Args:
  158. skills_dir: Skills 目录路径(可选)
  159. Returns:
  160. ToolResult: 包含所有 skills 的列表
  161. """
  162. skills_path = Path(skills_dir or DEFAULT_SKILLS_DIR)
  163. if not skills_path.exists():
  164. return ToolResult(
  165. title="Skills 目录不存在",
  166. output=f"找不到 skills 目录: {skills_path}",
  167. error=f"Directory not found: {skills_path}"
  168. )
  169. try:
  170. loader = SkillLoader(str(skills_path))
  171. skills = loader.load_all()
  172. if not skills:
  173. return ToolResult(
  174. title="没有可用的 Skills",
  175. output="skills 目录中没有找到任何 .md 文件",
  176. metadata={"count": 0}
  177. )
  178. # 按 category 分组
  179. by_category = {}
  180. for skill_obj in skills:
  181. category = skill_obj.category or "general"
  182. if category not in by_category:
  183. by_category[category] = []
  184. by_category[category].append(skill_obj)
  185. # 格式化输出
  186. output = f"# 可用的 Skills ({len(skills)} 个)\n\n"
  187. for category in sorted(by_category.keys()):
  188. output += f"## {category.title()}\n\n"
  189. for skill_obj in by_category[category]:
  190. skill_id = skill_obj.skill_id or skill_obj.name.lower().replace(' ', '-')
  191. output += f"- **{skill_obj.name}** (`{skill_id}`)\n"
  192. if skill_obj.description:
  193. desc = skill_obj.description.split('\n')[0] # 第一行
  194. output += f" {desc[:100]}{'...' if len(desc) > 100 else ''}\n"
  195. output += "\n"
  196. output += "\n使用 `skill` 工具加载具体的 skill:`skill(skill_name=\"browser-use\")`"
  197. return ToolResult(
  198. title=f"可用 Skills ({len(skills)} 个)",
  199. output=output,
  200. long_term_memory=f"找到 {len(skills)} 个可用 skills,分为 {len(by_category)} 个类别",
  201. include_output_only_once=True,
  202. metadata={
  203. "count": len(skills),
  204. "categories": list(by_category.keys()),
  205. "skills": [{"name": s.name, "category": s.category} for s in skills]
  206. }
  207. )
  208. except Exception as e:
  209. return ToolResult(
  210. title="列出 Skills 错误",
  211. output=f"列出 skills 时出错: {str(e)}",
  212. error=str(e)
  213. )