fischer-agentkit/src/agentkit/skills/registry.py

302 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""SkillRegistry - Skill 注册中心v2: 版本管理、能力查询、依赖检查)"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from agentkit.core.exceptions import SkillNotFoundError
from agentkit.skills.base import Skill, SkillConfig
from agentkit.skills.schema import DependencyDecl, HealthCheckResult
if TYPE_CHECKING:
from agentkit.skills.pipeline import SkillPipeline
logger = logging.getLogger(__name__)
class SkillRegistry:
"""Skill 注册中心,管理 Skill 的注册、发现、更新
v2 增强:
- 版本管理:同名 Skill 可注册多个版本,默认使用最新版
- 能力查询:按 capability 标签查询匹配的 Skill
- 依赖检查health_check() 验证所有声明依赖是否已注册
"""
def __init__(self):
self._skills: dict[str, Skill] = {}
# 版本历史name → {version → Skill}
self._skill_versions: dict[str, dict[str, Skill]] = {}
self._pipelines: dict[str, SkillPipeline] = {}
def register(self, skill: Skill) -> None:
"""注册 Skill支持多版本共存
同名 Skill 注册时保留版本历史,默认指向最新注册的版本。
"""
name = skill.name
version = skill.version
# 维护版本历史
if name not in self._skill_versions:
self._skill_versions[name] = {}
self._skill_versions[name][version] = skill
# 默认指向最新注册的版本
self._skills[name] = skill
logger.info(f"Skill '{name}' v{version} registered")
def unregister(self, name: str, version: str | None = None) -> None:
"""注销 Skill
Args:
name: Skill 名称
version: 可选版本号。若指定则仅注销该版本;
若不指定则注销所有版本。
"""
if version is not None:
# 仅注销指定版本
if name in self._skill_versions and version in self._skill_versions[name]:
del self._skill_versions[name][version]
logger.info(f"Skill '{name}' v{version} unregistered")
# 如果删除的是当前默认版本,切换到最新版本
if name in self._skills and self._skills[name].version == version:
remaining = self._skill_versions[name]
if remaining:
latest = max(remaining.keys())
self._skills[name] = remaining[latest]
logger.info(
f"Skill '{name}' default switched to v{latest}"
)
else:
del self._skills[name]
del self._skill_versions[name]
else:
# 注销所有版本
if name in self._skills:
del self._skills[name]
if name in self._skill_versions:
del self._skill_versions[name]
logger.info(f"Skill '{name}' unregistered (all versions)")
def get(self, name: str, version: str | None = None) -> Skill:
"""获取 Skill
Args:
name: Skill 名称
version: 可选版本号。若指定则返回特定版本,否则返回默认(最新)版本。
Raises:
SkillNotFoundError: Skill 或指定版本不存在
"""
if version is not None:
if name not in self._skill_versions:
raise SkillNotFoundError(name)
if version not in self._skill_versions[name]:
raise SkillNotFoundError(f"{name}@{version}")
return self._skill_versions[name][version]
if name not in self._skills:
raise SkillNotFoundError(name)
return self._skills[name]
def list_skills(self) -> list[Skill]:
"""列出所有已注册的 Skill每个名称返回默认版本"""
return list(self._skills.values())
def update_skill(self, name: str, config: SkillConfig) -> Skill:
"""更新已注册 Skill 的配置,返回更新后的 Skill"""
if name not in self._skills:
raise SkillNotFoundError(name)
old_skill = self._skills[name]
new_skill = Skill(config, tools=old_skill.tools)
self._skills[name] = new_skill
# 同时更新版本历史
version = config.version
if name not in self._skill_versions:
self._skill_versions[name] = {}
self._skill_versions[name][version] = new_skill
logger.info(f"Skill '{name}' updated to v{version}")
return new_skill
def has_skill(self, name: str, version: str | None = None) -> bool:
"""检查 Skill 是否已注册
Args:
name: Skill 名称
version: 可选版本号
"""
if version is not None:
return (
name in self._skill_versions
and version in self._skill_versions[name]
)
return name in self._skills
# ---- 版本管理 ----
def get_versions(self, name: str) -> list[str]:
"""获取指定 Skill 的所有已注册版本号
Args:
name: Skill 名称
Returns:
版本号列表(按注册顺序)
Raises:
SkillNotFoundError: Skill 不存在
"""
if name not in self._skill_versions:
raise SkillNotFoundError(name)
return list(self._skill_versions[name].keys())
# ---- 能力查询 ----
def query_by_capability(self, tag: str) -> list[Skill]:
"""按能力标签查询 Skill
Args:
tag: 能力标签名(如 "rag", "terminal", "computer_use"
Returns:
匹配的 Skill 列表
"""
result = []
for skill in self._skills.values():
capability_tags = [
cap.tag for cap in skill.capabilities
]
if tag in capability_tags:
result.append(skill)
return result
# ---- 依赖检查 ----
def health_check(self, name: str | None = None) -> list[HealthCheckResult]:
"""依赖健康检查
验证所有声明依赖是否已注册,以及版本约束是否满足。
Args:
name: 可选,指定检查某个 Skill。若不指定则检查所有 Skill。
Returns:
健康检查结果列表
"""
if name is not None:
if name not in self._skills:
raise SkillNotFoundError(name)
skills_to_check = [self._skills[name]]
else:
skills_to_check = list(self._skills.values())
results = []
for skill in skills_to_check:
result = self._check_skill_dependencies(skill)
results.append(result)
return results
def _check_skill_dependencies(self, skill: Skill) -> HealthCheckResult:
"""检查单个 Skill 的依赖是否满足"""
result = HealthCheckResult(
skill_name=skill.name,
skill_version=skill.version,
healthy=True,
)
for dep in skill.dependencies:
if dep.type == "skill":
if not self.has_skill(dep.name):
if dep.required:
result.healthy = False
result.missing_dependencies.append(dep.name)
else:
result.warnings.append(
f"Optional skill dependency '{dep.name}' not registered"
)
elif dep.version_constraint:
# 简化版本约束检查:仅检查已注册版本是否满足
dep_skill = self.get(dep.name)
if not self._check_version_constraint(
dep_skill.version, dep.version_constraint
):
result.version_mismatches.append(
f"{dep.name}: need {dep.version_constraint}, "
f"got {dep_skill.version}"
)
if dep.required:
result.healthy = False
elif dep.type == "tool":
# Tool 依赖检查需要 ToolRegistry此处仅记录
# 实际检查在运行时由 SkillLoader 配合 ToolRegistry 完成
pass
return result
@staticmethod
def _check_version_constraint(
actual_version: str, constraint: str
) -> bool:
"""简化的版本约束检查
支持基本的约束格式:
- ">=x.y.z" — 大于等于
- "<=x.y.z" — 小于等于
- "==x.y.z" — 精确匹配
- ">=x.y.z,<a.b.c" — 范围约束
使用简单的元组比较进行 semver 检查。
"""
try:
actual = tuple(int(x) for x in actual_version.split("."))
except (ValueError, AttributeError):
return True # 无法解析时默认通过
parts = [p.strip() for p in constraint.split(",")]
for part in parts:
if part.startswith(">="):
target = tuple(int(x) for x in part[2:].split("."))
if actual < target:
return False
elif part.startswith("<="):
target = tuple(int(x) for x in part[2:].split("."))
if actual > target:
return False
elif part.startswith("=="):
target = tuple(int(x) for x in part[2:].split("."))
if actual != target:
return False
elif part.startswith(">"):
target = tuple(int(x) for x in part[1:].split("."))
if actual <= target:
return False
elif part.startswith("<"):
target = tuple(int(x) for x in part[1:].split("."))
if actual >= target:
return False
return True
# ---- Pipeline 管理 ----
def register_pipeline(self, pipeline: SkillPipeline) -> None:
"""注册 SkillPipeline同名覆盖"""
self._pipelines[pipeline.name] = pipeline
logger.info(f"SkillPipeline '{pipeline.name}' registered")
def get_pipeline(self, name: str) -> SkillPipeline | None:
"""获取已注册的 SkillPipeline不存在返回 None"""
return self._pipelines.get(name)
def list_pipelines(self) -> list[str]:
"""列出所有已注册的 Pipeline 名称"""
return list(self._pipelines.keys())
def unregister_pipeline(self, name: str) -> None:
"""注销 SkillPipeline"""
if name in self._pipelines:
del self._pipelines[name]
logger.info(f"SkillPipeline '{name}' unregistered")