""" Skill 工具 - 按需加载 Skill 文件 Agent 可以调用此工具来加载特定的 skill 文档 """ import os import subprocess from pathlib import Path from typing import Optional from agent.tools import tool, ToolResult from agent.skill.skill_loader import SkillLoader # 默认 skills 目录(优先级:项目 skills > 框架 skills) DEFAULT_SKILLS_DIRS = [ os.getenv("SKILLS_DIR", "./skills"), # 项目特定 skills(优先) "./agent/skill/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.skill.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) )