skill.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. """
  2. Skill 工具 - 按需加载 Skill 文件
  3. Agent 可以调用此工具来加载特定的 skill 文档
  4. """
  5. import os
  6. from pathlib import Path
  7. from typing import Optional
  8. from agent.tools import tool, ToolResult
  9. from agent.storage.skill_loader import SkillLoader
  10. # 默认 skills 目录
  11. DEFAULT_SKILLS_DIR = os.getenv("SKILLS_DIR", "./skills")
  12. @tool(
  13. description="加载指定的 skill 文档。Skills 提供领域知识和最佳实践指导。"
  14. )
  15. async def skill(
  16. skill_name: str,
  17. skills_dir: Optional[str] = None,
  18. uid: str = ""
  19. ) -> ToolResult:
  20. """
  21. 加载指定的 skill 文档
  22. Args:
  23. skill_name: Skill 名称(如 "browser-use", "error-handling")
  24. skills_dir: Skills 目录路径(可选,默认使用环境变量或 ./skills)
  25. uid: 用户 ID(自动注入)
  26. Returns:
  27. ToolResult: 包含 skill 的详细内容
  28. """
  29. # 确定 skills 目录
  30. skills_path = Path(skills_dir or DEFAULT_SKILLS_DIR)
  31. if not skills_path.exists():
  32. return ToolResult(
  33. title="Skills 目录不存在",
  34. output=f"找不到 skills 目录: {skills_path}",
  35. error=f"Directory not found: {skills_path}"
  36. )
  37. # 查找文件(支持 skill-name.md 或 skill_name.md)
  38. skill_file = None
  39. for ext in [".md"]:
  40. for name_format in [skill_name, skill_name.replace("-", "_"), skill_name.replace("_", "-")]:
  41. candidate = skills_path / f"{name_format}{ext}"
  42. if candidate.exists():
  43. skill_file = candidate
  44. break
  45. if skill_file:
  46. break
  47. if not skill_file:
  48. # 列出可用的 skills
  49. available_skills = [f.stem for f in skills_path.glob("*.md")]
  50. return ToolResult(
  51. title=f"Skill '{skill_name}' 未找到",
  52. output=f"可用的 skills: {', '.join(available_skills)}",
  53. error=f"Skill not found: {skill_name}"
  54. )
  55. # 加载 skill
  56. try:
  57. loader = SkillLoader(str(skills_path))
  58. skill_obj = loader.load_file(skill_file)
  59. if not skill_obj:
  60. return ToolResult(
  61. title="加载失败",
  62. output=f"无法解析 skill 文件: {skill_file.name}",
  63. error="Failed to parse skill file"
  64. )
  65. # 格式化输出
  66. output = f"# {skill_obj.name}\n\n"
  67. output += f"**Category**: {skill_obj.category}\n\n"
  68. if skill_obj.description:
  69. output += f"## Description\n\n{skill_obj.description}\n\n"
  70. if skill_obj.guidelines:
  71. output += f"## Guidelines\n\n"
  72. for i, guideline in enumerate(skill_obj.guidelines, 1):
  73. output += f"{i}. {guideline}\n"
  74. output += "\n"
  75. return ToolResult(
  76. title=f"Skill: {skill_obj.name}",
  77. output=output,
  78. long_term_memory=f"已加载 skill: {skill_obj.name} ({skill_obj.category})",
  79. include_output_only_once=True, # skill 内容只展示一次
  80. metadata={
  81. "skill_name": skill_obj.name,
  82. "category": skill_obj.category,
  83. "scope": skill_obj.scope,
  84. "guidelines_count": len(skill_obj.guidelines)
  85. }
  86. )
  87. except Exception as e:
  88. return ToolResult(
  89. title="加载错误",
  90. output=f"加载 skill 时出错: {str(e)}",
  91. error=str(e)
  92. )
  93. @tool(
  94. description="列出所有可用的 skills"
  95. )
  96. async def list_skills(
  97. skills_dir: Optional[str] = None,
  98. uid: str = ""
  99. ) -> ToolResult:
  100. """
  101. 列出所有可用的 skills
  102. Args:
  103. skills_dir: Skills 目录路径(可选)
  104. uid: 用户 ID(自动注入)
  105. Returns:
  106. ToolResult: 包含所有 skills 的列表
  107. """
  108. skills_path = Path(skills_dir or DEFAULT_SKILLS_DIR)
  109. if not skills_path.exists():
  110. return ToolResult(
  111. title="Skills 目录不存在",
  112. output=f"找不到 skills 目录: {skills_path}",
  113. error=f"Directory not found: {skills_path}"
  114. )
  115. try:
  116. loader = SkillLoader(str(skills_path))
  117. skills = loader.load_all()
  118. if not skills:
  119. return ToolResult(
  120. title="没有可用的 Skills",
  121. output="skills 目录中没有找到任何 .md 文件",
  122. metadata={"count": 0}
  123. )
  124. # 按 category 分组
  125. by_category = {}
  126. for skill_obj in skills:
  127. category = skill_obj.category or "general"
  128. if category not in by_category:
  129. by_category[category] = []
  130. by_category[category].append(skill_obj)
  131. # 格式化输出
  132. output = f"# 可用的 Skills ({len(skills)} 个)\n\n"
  133. for category in sorted(by_category.keys()):
  134. output += f"## {category.title()}\n\n"
  135. for skill_obj in by_category[category]:
  136. skill_id = skill_obj.skill_id or skill_obj.name.lower().replace(' ', '-')
  137. output += f"- **{skill_obj.name}** (`{skill_id}`)\n"
  138. if skill_obj.description:
  139. desc = skill_obj.description.split('\n')[0] # 第一行
  140. output += f" {desc[:100]}{'...' if len(desc) > 100 else ''}\n"
  141. output += "\n"
  142. output += "\n使用 `skill` 工具加载具体的 skill:`skill(skill_name=\"browser-use\")`"
  143. return ToolResult(
  144. title=f"可用 Skills ({len(skills)} 个)",
  145. output=output,
  146. long_term_memory=f"找到 {len(skills)} 个可用 skills,分为 {len(by_category)} 个类别",
  147. include_output_only_once=True,
  148. metadata={
  149. "count": len(skills),
  150. "categories": list(by_category.keys()),
  151. "skills": [{"name": s.name, "category": s.category} for s in skills]
  152. }
  153. )
  154. except Exception as e:
  155. return ToolResult(
  156. title="列出 Skills 错误",
  157. output=f"列出 skills 时出错: {str(e)}",
  158. error=str(e)
  159. )