Initial commit of integrated agent system

This commit is contained in:
cillin
2026-03-30 17:46:44 +08:00
commit 0fa413380c
337 changed files with 75268 additions and 0 deletions

View File

@@ -0,0 +1,388 @@
# -*- coding: utf-8 -*-
"""Skill loader for loading and validating skills from directories.
提供从目录加载技能、解析SKILL.md frontmatter、获取工具列表等功能。
"""
import logging
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Set
import yaml
from backend.agents.skill_metadata import SkillMetadata, parse_skill_metadata
logger = logging.getLogger(__name__)
@dataclass
class SkillInfo:
"""完整的技能信息"""
name: str
description: str
version: str
source: str
path: Path
metadata: SkillMetadata
tools: List[str] = field(default_factory=list)
scripts: List[str] = field(default_factory=list)
references: List[str] = field(default_factory=list)
content: str = ""
def load_skill_from_dir(skill_dir: Path, source: str = "unknown") -> Optional[Dict[str, Any]]:
"""从目录加载技能
Args:
skill_dir: 技能目录路径
source: 技能来源 (builtin/customized/local/installed/active)
Returns:
技能信息字典加载失败返回None
"""
if not skill_dir.exists() or not skill_dir.is_dir():
logger.warning(f"Skill directory does not exist: {skill_dir}")
return None
skill_md = skill_dir / "SKILL.md"
if not skill_md.exists():
logger.warning(f"SKILL.md not found in: {skill_dir}")
return None
try:
# 解析元数据
metadata = parse_skill_metadata(skill_dir, source=source)
# 读取完整内容
content = skill_md.read_text(encoding="utf-8")
# 提取body (去掉frontmatter)
body = content
if content.startswith("---"):
parts = content.split("---", 2)
if len(parts) >= 3:
body = parts[2].strip()
# 获取工具列表
tools = get_skill_tools(skill_dir)
# 获取脚本列表
scripts = _get_skill_scripts(skill_dir)
# 获取参考资料列表
references = _get_skill_references(skill_dir)
return {
"name": metadata.name,
"skill_name": metadata.skill_name,
"description": metadata.description,
"version": metadata.version,
"source": source,
"path": str(skill_dir),
"content": body,
"tools": tools,
"scripts": scripts,
"references": references,
"metadata": metadata,
}
except Exception as e:
logger.error(f"Failed to load skill from {skill_dir}: {e}")
return None
def parse_skill_metadata(skill_dir: Path, source: str = "unknown") -> SkillMetadata:
"""解析技能元数据 (兼容已有函数)
Args:
skill_dir: 技能目录路径
source: 技能来源
Returns:
SkillMetadata对象
"""
from backend.agents.skill_metadata import parse_skill_metadata as _parse
return _parse(skill_dir, source=source)
def get_skill_tools(skill_dir: Path) -> List[str]:
"""获取技能提供的工具列表
从SKILL.md frontmatter的tools字段和scripts目录解析工具。
Args:
skill_dir: 技能目录路径
Returns:
工具名称列表
"""
tools: Set[str] = set()
# 1. 从SKILL.md frontmatter读取tools字段
skill_md = skill_dir / "SKILL.md"
if skill_md.exists():
try:
raw = skill_md.read_text(encoding="utf-8").strip()
if raw.startswith("---"):
parts = raw.split("---", 2)
if len(parts) >= 3:
try:
frontmatter = yaml.safe_load(parts[1].strip()) or {}
if isinstance(frontmatter, dict):
tools_list = frontmatter.get("tools", [])
if isinstance(tools_list, str):
tools.add(tools_list.strip())
elif isinstance(tools_list, list):
for tool in tools_list:
if isinstance(tool, str):
tools.add(tool.strip())
except yaml.YAMLError:
pass
except Exception as e:
logger.warning(f"Failed to parse tools from SKILL.md: {e}")
# 2. 从scripts目录推断工具
scripts_dir = skill_dir / "scripts"
if scripts_dir.exists() and scripts_dir.is_dir():
for script in scripts_dir.iterdir():
if script.is_file() and not script.name.startswith("_"):
# 去掉扩展名作为工具名
tool_name = script.stem
tools.add(tool_name)
return sorted(list(tools))
def _get_skill_scripts(skill_dir: Path) -> List[str]:
"""获取技能脚本列表
Args:
skill_dir: 技能目录路径
Returns:
脚本相对路径列表 (相对于scripts目录)
"""
scripts: List[str] = []
scripts_dir = skill_dir / "scripts"
if not scripts_dir.exists():
return scripts
try:
for item in scripts_dir.rglob("*"):
if item.is_file() and not item.name.startswith("_"):
rel_path = item.relative_to(scripts_dir)
scripts.append(str(rel_path))
except Exception as e:
logger.warning(f"Failed to list scripts in {skill_dir}: {e}")
return sorted(scripts)
def _get_skill_references(skill_dir: Path) -> List[str]:
"""获取技能参考资料列表
Args:
skill_dir: 技能目录路径
Returns:
参考资料相对路径列表 (相对于references目录)
"""
refs: List[str] = []
refs_dir = skill_dir / "references"
if not refs_dir.exists():
return refs
try:
for item in refs_dir.rglob("*"):
if item.is_file():
rel_path = item.relative_to(refs_dir)
refs.append(str(rel_path))
except Exception as e:
logger.warning(f"Failed to list references in {skill_dir}: {e}")
return sorted(refs)
def validate_skill(skill_dir: Path) -> Dict[str, Any]:
"""验证技能格式
检查技能目录结构是否符合规范。
Args:
skill_dir: 技能目录路径
Returns:
验证结果字典,包含:
- valid: 是否有效
- errors: 错误列表
- warnings: 警告列表
"""
errors: List[str] = []
warnings: List[str] = []
# 检查目录存在
if not skill_dir.exists():
errors.append(f"Skill directory does not exist: {skill_dir}")
return {"valid": False, "errors": errors, "warnings": warnings}
if not skill_dir.is_dir():
errors.append(f"Path is not a directory: {skill_dir}")
return {"valid": False, "errors": errors, "warnings": warnings}
# 检查SKILL.md
skill_md = skill_dir / "SKILL.md"
if not skill_md.exists():
errors.append("SKILL.md is required but not found")
return {"valid": False, "errors": errors, "warnings": warnings}
# 解析frontmatter
try:
content = skill_md.read_text(encoding="utf-8").strip()
if not content.startswith("---"):
warnings.append("SKILL.md should have YAML frontmatter (starts with ---)")
else:
parts = content.split("---", 2)
if len(parts) < 3:
errors.append("Invalid YAML frontmatter format")
else:
try:
frontmatter = yaml.safe_load(parts[1].strip()) or {}
if not isinstance(frontmatter, dict):
errors.append("YAML frontmatter must be a dictionary")
else:
# 检查必需字段
if "name" not in frontmatter:
warnings.append("Frontmatter should have 'name' field")
if "description" not in frontmatter:
warnings.append("Frontmatter should have 'description' field")
# 检查version字段
version = frontmatter.get("version")
if version and not isinstance(version, str):
warnings.append("'version' should be a string")
# 检查tools字段
tools = frontmatter.get("tools")
if tools and not isinstance(tools, (str, list)):
warnings.append("'tools' should be a string or list")
except yaml.YAMLError as e:
errors.append(f"Invalid YAML in frontmatter: {e}")
except Exception as e:
errors.append(f"Failed to read SKILL.md: {e}")
# 检查body内容
try:
content = skill_md.read_text(encoding="utf-8")
body = content
if content.startswith("---"):
parts = content.split("---", 2)
if len(parts) >= 3:
body = parts[2].strip()
if not body:
warnings.append("SKILL.md body is empty")
elif len(body) < 50:
warnings.append("SKILL.md body is very short, consider adding more details")
except Exception as e:
errors.append(f"Failed to validate body: {e}")
# 检查scripts目录
scripts_dir = skill_dir / "scripts"
if scripts_dir.exists():
if not scripts_dir.is_dir():
errors.append("'scripts' exists but is not a directory")
else:
# 检查是否有可执行脚本
has_scripts = any(
f.is_file() and not f.name.startswith("_")
for f in scripts_dir.iterdir()
)
if not has_scripts:
warnings.append("scripts directory exists but contains no valid scripts")
# 检查references目录
refs_dir = skill_dir / "references"
if refs_dir.exists() and not refs_dir.is_dir():
errors.append("'references' exists but is not a directory")
return {
"valid": len(errors) == 0,
"errors": errors,
"warnings": warnings,
}
def load_skills_from_directory(
directory: Path,
source: str = "unknown",
recursive: bool = False,
) -> List[Dict[str, Any]]:
"""从目录加载所有技能
Args:
directory: 包含技能目录的父目录
source: 技能来源标识
recursive: 是否递归搜索子目录
Returns:
技能信息列表
"""
skills: List[Dict[str, Any]] = []
if not directory.exists() or not directory.is_dir():
logger.warning(f"Directory does not exist: {directory}")
return skills
try:
for item in directory.iterdir():
if not item.is_dir():
continue
# 检查是否是技能目录 (包含SKILL.md)
if (item / "SKILL.md").exists():
skill_info = load_skill_from_dir(item, source=source)
if skill_info:
skills.append(skill_info)
elif recursive:
# 递归搜索子目录
sub_skills = load_skills_from_directory(item, source, recursive)
skills.extend(sub_skills)
except Exception as e:
logger.error(f"Failed to load skills from {directory}: {e}")
return skills
def get_skill_manifest(skill_dir: Path) -> Dict[str, Any]:
"""获取技能清单
生成技能的详细清单,用于调试和展示。
Args:
skill_dir: 技能目录路径
Returns:
技能清单字典
"""
info = load_skill_from_dir(skill_dir)
if not info:
return {"error": "Failed to load skill"}
validation = validate_skill(skill_dir)
return {
"name": info["name"],
"skill_name": info["skill_name"],
"version": info["version"],
"description": info["description"],
"source": info["source"],
"path": info["path"],
"tools": info["tools"],
"scripts": info["scripts"],
"references": info["references"],
"validation": validation,
"content_preview": info["content"][:500] + "..." if len(info["content"]) > 500 else info["content"],
}