| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- """
- Skill 工具 - 按需加载 Skill 文件
- Agent 可以调用此工具来加载特定的 skill 文档
- """
- import os
- import subprocess
- from pathlib import Path
- from typing import Optional
- from agent.tools import tool, ToolResult
- from agent.memory.skill_loader import SkillLoader
- # 默认 skills 目录(优先级:项目 skills > 框架 skills)
- DEFAULT_SKILLS_DIRS = [
- os.getenv("SKILLS_DIR", "./skills"), # 项目特定 skills(优先)
- "./agent/memory/skills" # 框架内置 skills
- ]
- # 默认单一目录(用于 list_skills)
- DEFAULT_SKILLS_DIR = DEFAULT_SKILLS_DIRS[0]
- def _check_skill_setup(skill_name: str) -> Optional[str]:
- """
- 检查 skill 的环境配置,返回缺失依赖的警告信息
- Args:
- skill_name: Skill 名称
- Returns:
- 警告信息(如果有缺失的依赖),否则返回 None
- """
- # 特殊处理:browser-use skill
- if skill_name in ["browser-use", "browser_use"]:
- try:
- # 动态导入 browser-use skill 的 setup 模块
- from agent.memory.skills.browser_use.setup import (
- _check_browser_use_cli,
- _check_chromium_installed
- )
- cli_installed = _check_browser_use_cli()
- chromium_installed = _check_chromium_installed()
- if not cli_installed or not chromium_installed:
- warning = "\n⚠️ **Setup Required**\n\n"
- warning += "The following dependencies are missing:\n\n"
- if not cli_installed:
- warning += "- `pip install browser-use`\n"
- if not chromium_installed:
- warning += "- `uvx browser-use install`\n"
- warning += "\nYou can also use the setup tools:\n"
- warning += "- `check_browser_use()` - Check dependency status\n"
- warning += "- `install_browser_use_chromium()` - Auto-install Chromium\n\n"
- return warning
- except ImportError:
- # Setup 模块不存在,跳过检查
- pass
- return None
- @tool(
- description="加载指定的 skill 文档。Skills 提供领域知识和最佳实践指导。"
- )
- async def skill(
- skill_name: str,
- skills_dir: Optional[str] = None,
- ) -> ToolResult:
- """
- 加载指定的 skill 文档
- Args:
- skill_name: Skill 名称(如 "browser-use", "error-handling")
- skills_dir: Skills 目录路径(可选,默认按优先级查找)
- Returns:
- ToolResult: 包含 skill 的详细内容
- 加载顺序:
- 1. 如果指定 skills_dir,只在该目录查找
- 2. 否则按优先级查找:./skills/ (项目) -> ./config/skills/ (框架)
- """
- # 确定要搜索的目录列表
- if skills_dir:
- search_paths = [Path(skills_dir)]
- else:
- search_paths = [Path(d) for d in DEFAULT_SKILLS_DIRS]
- # 在目录中查找 skill 文件
- skill_file = None
- found_in_dir = None
- for skills_path in search_paths:
- if not skills_path.exists():
- continue
- # 查找文件(支持 skill-name.md 或 skill_name.md)
- for ext in [".md"]:
- for name_format in [skill_name, skill_name.replace("-", "_"), skill_name.replace("_", "-")]:
- candidate = skills_path / f"{name_format}{ext}"
- if candidate.exists():
- skill_file = candidate
- found_in_dir = skills_path
- break
- if skill_file:
- break
- if skill_file:
- break
- if not skill_file:
- # 列出所有可用的 skills
- available_skills = []
- for skills_path in search_paths:
- if skills_path.exists():
- available_skills.extend([f.stem for f in skills_path.glob("**/*.md")])
- return ToolResult(
- title=f"Skill '{skill_name}' 未找到",
- output=f"可用的 skills: {', '.join(set(available_skills))}\n\n"
- f"查找路径: {', '.join([str(p) for p in search_paths])}",
- error=f"Skill not found: {skill_name}"
- )
- # 加载 skill
- try:
- loader = SkillLoader(str(skills_path))
- skill_obj = loader.load_file(skill_file)
- if not skill_obj:
- return ToolResult(
- title="加载失败",
- output=f"无法解析 skill 文件: {skill_file.name}",
- error="Failed to parse skill file"
- )
- # 格式化输出
- output = f"# {skill_obj.name}\n\n"
- output += f"**Category**: {skill_obj.category}\n\n"
- if skill_obj.description:
- output += f"## Description\n\n{skill_obj.description}\n\n"
- # 检查 skill 的环境配置
- setup_warning = _check_skill_setup(skill_name)
- if setup_warning:
- output += setup_warning
- if skill_obj.guidelines:
- output += f"## Guidelines\n\n"
- for i, guideline in enumerate(skill_obj.guidelines, 1):
- output += f"{i}. {guideline}\n"
- output += "\n"
- return ToolResult(
- title=f"Skill: {skill_obj.name}",
- output=output,
- long_term_memory=f"已加载 skill: {skill_obj.name} ({skill_obj.category}) from {found_in_dir}",
- include_output_only_once=True, # skill 内容只展示一次
- metadata={
- "skill_name": skill_obj.name,
- "category": skill_obj.category,
- "scope": skill_obj.scope,
- "guidelines_count": len(skill_obj.guidelines),
- "loaded_from": str(found_in_dir)
- }
- )
- except Exception as e:
- return ToolResult(
- title="加载错误",
- output=f"加载 skill 时出错: {str(e)}",
- error=str(e)
- )
- @tool(
- description="列出所有可用的 skills"
- )
- async def list_skills(
- skills_dir: Optional[str] = None,
- ) -> ToolResult:
- """
- 列出所有可用的 skills
- Args:
- skills_dir: Skills 目录路径(可选)
- Returns:
- ToolResult: 包含所有 skills 的列表
- """
- skills_path = Path(skills_dir or DEFAULT_SKILLS_DIR)
- if not skills_path.exists():
- return ToolResult(
- title="Skills 目录不存在",
- output=f"找不到 skills 目录: {skills_path}",
- error=f"Directory not found: {skills_path}"
- )
- try:
- loader = SkillLoader(str(skills_path))
- skills = loader.load_all()
- if not skills:
- return ToolResult(
- title="没有可用的 Skills",
- output="skills 目录中没有找到任何 .md 文件",
- metadata={"count": 0}
- )
- # 按 category 分组
- by_category = {}
- for skill_obj in skills:
- category = skill_obj.category or "general"
- if category not in by_category:
- by_category[category] = []
- by_category[category].append(skill_obj)
- # 格式化输出
- output = f"# 可用的 Skills ({len(skills)} 个)\n\n"
- for category in sorted(by_category.keys()):
- output += f"## {category.title()}\n\n"
- for skill_obj in by_category[category]:
- skill_id = skill_obj.skill_id or skill_obj.name.lower().replace(' ', '-')
- output += f"- **{skill_obj.name}** (`{skill_id}`)\n"
- if skill_obj.description:
- desc = skill_obj.description.split('\n')[0] # 第一行
- output += f" {desc[:100]}{'...' if len(desc) > 100 else ''}\n"
- output += "\n"
- output += "\n使用 `skill` 工具加载具体的 skill:`skill(skill_name=\"browser-use\")`"
- return ToolResult(
- title=f"可用 Skills ({len(skills)} 个)",
- output=output,
- long_term_memory=f"找到 {len(skills)} 个可用 skills,分为 {len(by_category)} 个类别",
- include_output_only_once=True,
- metadata={
- "count": len(skills),
- "categories": list(by_category.keys()),
- "skills": [{"name": s.name, "category": s.category} for s in skills]
- }
- )
- except Exception as e:
- return ToolResult(
- title="列出 Skills 错误",
- output=f"列出 skills 时出错: {str(e)}",
- error=str(e)
- )
|