skill.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  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 agent.tools import tool, ToolResult
  10. from agent.memory.skill_loader import SkillLoader
  11. # 默认 skills 目录(优先级:项目 skills > 框架 skills)
  12. DEFAULT_SKILLS_DIRS = [
  13. os.getenv("SKILLS_DIR", "./skills"), # 项目特定 skills(优先)
  14. "./agent/memory/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 agent.memory.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. )
  54. async def skill(
  55. skill_name: str,
  56. skills_dir: Optional[str] = None,
  57. ) -> ToolResult:
  58. """
  59. 加载指定的 skill 文档
  60. Args:
  61. skill_name: Skill 名称(如 "browser-use", "error-handling")
  62. skills_dir: Skills 目录路径(可选,默认按优先级查找)
  63. Returns:
  64. ToolResult: 包含 skill 的详细内容
  65. 加载顺序:
  66. 1. 如果指定 skills_dir,只在该目录查找
  67. 2. 否则按优先级查找:./skills/ (项目) -> ./config/skills/ (框架)
  68. """
  69. # 确定要搜索的目录列表
  70. if skills_dir:
  71. search_paths = [Path(skills_dir)]
  72. else:
  73. search_paths = [Path(d) for d in DEFAULT_SKILLS_DIRS]
  74. # 在目录中查找 skill 文件
  75. skill_file = None
  76. found_in_dir = None
  77. for skills_path in search_paths:
  78. if not skills_path.exists():
  79. continue
  80. # 查找文件(支持 skill-name.md 或 skill_name.md)
  81. for ext in [".md"]:
  82. for name_format in [skill_name, skill_name.replace("-", "_"), skill_name.replace("_", "-")]:
  83. candidate = skills_path / f"{name_format}{ext}"
  84. if candidate.exists():
  85. skill_file = candidate
  86. found_in_dir = skills_path
  87. break
  88. if skill_file:
  89. break
  90. if skill_file:
  91. break
  92. if not skill_file:
  93. # 列出所有可用的 skills
  94. available_skills = []
  95. for skills_path in search_paths:
  96. if skills_path.exists():
  97. available_skills.extend([f.stem for f in skills_path.glob("**/*.md")])
  98. return ToolResult(
  99. title=f"Skill '{skill_name}' 未找到",
  100. output=f"可用的 skills: {', '.join(set(available_skills))}\n\n"
  101. f"查找路径: {', '.join([str(p) for p in search_paths])}",
  102. error=f"Skill not found: {skill_name}"
  103. )
  104. # 加载 skill
  105. try:
  106. loader = SkillLoader(str(skills_path))
  107. skill_obj = loader.load_file(skill_file)
  108. if not skill_obj:
  109. return ToolResult(
  110. title="加载失败",
  111. output=f"无法解析 skill 文件: {skill_file.name}",
  112. error="Failed to parse skill file"
  113. )
  114. # 格式化输出
  115. output = f"# {skill_obj.name}\n\n"
  116. output += f"**Category**: {skill_obj.category}\n\n"
  117. if skill_obj.description:
  118. output += f"## Description\n\n{skill_obj.description}\n\n"
  119. # 检查 skill 的环境配置
  120. setup_warning = _check_skill_setup(skill_name)
  121. if setup_warning:
  122. output += setup_warning
  123. if skill_obj.guidelines:
  124. output += f"## Guidelines\n\n"
  125. for i, guideline in enumerate(skill_obj.guidelines, 1):
  126. output += f"{i}. {guideline}\n"
  127. output += "\n"
  128. return ToolResult(
  129. title=f"Skill: {skill_obj.name}",
  130. output=output,
  131. long_term_memory=f"已加载 skill: {skill_obj.name} ({skill_obj.category}) from {found_in_dir}",
  132. include_output_only_once=True, # skill 内容只展示一次
  133. metadata={
  134. "skill_name": skill_obj.name,
  135. "category": skill_obj.category,
  136. "scope": skill_obj.scope,
  137. "guidelines_count": len(skill_obj.guidelines),
  138. "loaded_from": str(found_in_dir)
  139. }
  140. )
  141. except Exception as e:
  142. return ToolResult(
  143. title="加载错误",
  144. output=f"加载 skill 时出错: {str(e)}",
  145. error=str(e)
  146. )
  147. @tool(
  148. description="列出所有可用的 skills"
  149. )
  150. async def list_skills(
  151. skills_dir: Optional[str] = None,
  152. ) -> ToolResult:
  153. """
  154. 列出所有可用的 skills
  155. Args:
  156. skills_dir: Skills 目录路径(可选)
  157. Returns:
  158. ToolResult: 包含所有 skills 的列表
  159. """
  160. skills_path = Path(skills_dir or DEFAULT_SKILLS_DIR)
  161. if not skills_path.exists():
  162. return ToolResult(
  163. title="Skills 目录不存在",
  164. output=f"找不到 skills 目录: {skills_path}",
  165. error=f"Directory not found: {skills_path}"
  166. )
  167. try:
  168. loader = SkillLoader(str(skills_path))
  169. skills = loader.load_all()
  170. if not skills:
  171. return ToolResult(
  172. title="没有可用的 Skills",
  173. output="skills 目录中没有找到任何 .md 文件",
  174. metadata={"count": 0}
  175. )
  176. # 按 category 分组
  177. by_category = {}
  178. for skill_obj in skills:
  179. category = skill_obj.category or "general"
  180. if category not in by_category:
  181. by_category[category] = []
  182. by_category[category].append(skill_obj)
  183. # 格式化输出
  184. output = f"# 可用的 Skills ({len(skills)} 个)\n\n"
  185. for category in sorted(by_category.keys()):
  186. output += f"## {category.title()}\n\n"
  187. for skill_obj in by_category[category]:
  188. skill_id = skill_obj.skill_id or skill_obj.name.lower().replace(' ', '-')
  189. output += f"- **{skill_obj.name}** (`{skill_id}`)\n"
  190. if skill_obj.description:
  191. desc = skill_obj.description.split('\n')[0] # 第一行
  192. output += f" {desc[:100]}{'...' if len(desc) > 100 else ''}\n"
  193. output += "\n"
  194. output += "\n使用 `skill` 工具加载具体的 skill:`skill(skill_name=\"browser-use\")`"
  195. return ToolResult(
  196. title=f"可用 Skills ({len(skills)} 个)",
  197. output=output,
  198. long_term_memory=f"找到 {len(skills)} 个可用 skills,分为 {len(by_category)} 个类别",
  199. include_output_only_once=True,
  200. metadata={
  201. "count": len(skills),
  202. "categories": list(by_category.keys()),
  203. "skills": [{"name": s.name, "category": s.category} for s in skills]
  204. }
  205. )
  206. except Exception as e:
  207. return ToolResult(
  208. title="列出 Skills 错误",
  209. output=f"列出 skills 时出错: {str(e)}",
  210. error=str(e)
  211. )