"""受信任 Python Skill 的发现、匹配与 Run 级激活。""" from __future__ import annotations import importlib import inspect import pkgutil from collections.abc import Awaitable, Callable, Iterable from dataclasses import dataclass from types import ModuleType from typing import Any from app.application.agent.extensions import ExtensionRegistry SkillCleanup = Callable[[], Awaitable[None] | None] SkillSetup = Callable[[ExtensionRegistry], SkillCleanup | None] SkillMatcher = Callable[[str, dict[str, Any]], bool] @dataclass(frozen=True) class PythonSkillDefinition: """部署期 Python Skill 契约。""" name: str description: str triggers: tuple[str, ...] instructions: str setup: SkillSetup version: str = "1" enabled: bool = True match: SkillMatcher | None = None @dataclass class PythonSkillActivation: """单个 Run 的 Skill 激活结果。""" skills: tuple[PythonSkillDefinition, ...] cleanups: list[SkillCleanup] async def close(self) -> None: """按激活逆序释放 Skill 资源。""" while self.cleanups: result = self.cleanups.pop()() if inspect.isawaitable(result): await result class PythonSkillCatalog: """只保存 Skill 描述与 setup,不持有 Run 状态。""" def __init__(self, skills: Iterable[PythonSkillDefinition] = ()) -> None: self._skills: dict[str, PythonSkillDefinition] = {} for skill in skills: if skill.name in self._skills: raise ValueError(f"Python Skill 名称重复:{skill.name}") self._skills[skill.name] = skill @property def skills(self) -> tuple[PythonSkillDefinition, ...]: return tuple(self._skills.values()) def match( self, context_text: str, *, skill_refs: tuple[str, ...] = (), metadata: dict[str, Any] | None = None, ) -> tuple[PythonSkillDefinition, ...]: """返回显式引用、关键词或自定义匹配器命中的 enabled Skills。""" refs = set(skill_refs) text = context_text.casefold() match_metadata = metadata or {} return tuple( skill for skill in self._skills.values() if skill.enabled and ( skill.name in refs or any(trigger.casefold() in text for trigger in skill.triggers if trigger) or (skill.match is not None and skill.match(context_text, match_metadata)) ) ) async def activate_python_skills( registry: ExtensionRegistry, skills: Iterable[PythonSkillDefinition], ) -> PythonSkillActivation: """把命中的 Skills 注册到当前 Run,并在部分失败时清理已激活项。""" activation = PythonSkillActivation(tuple(skills), []) try: for skill in activation.skills: cleanup = skill.setup(registry) if cleanup is not None: activation.cleanups.append(cleanup) except Exception: await activation.close() raise return activation def discover_python_skills(packages: Iterable[str] = ()) -> PythonSkillCatalog: """从受信任 packages 发现定义,但不执行 Skill setup。""" modules: list[ModuleType] = [] for package_name in dict.fromkeys(packages): package = importlib.import_module(package_name) package_path = getattr(package, "__path__", None) if package_path is None: raise TypeError(f"Python Skill 包必须是 package:{package_name}") modules.extend( importlib.import_module(module.name) for module in pkgutil.iter_modules(package_path, f"{package.__name__}.") if not module.name.rsplit(".", 1)[-1].startswith("_") ) definitions: list[PythonSkillDefinition] = [] for module in sorted(modules, key=lambda item: item.__name__): skill = getattr(module, "SKILL", None) if not isinstance(skill, PythonSkillDefinition): raise TypeError(f"Python Skill 缺少 SKILL 定义:{module.__name__}") definitions.append(skill) return PythonSkillCatalog(definitions) __all__ = [ "PythonSkillActivation", "PythonSkillCatalog", "PythonSkillDefinition", "activate_python_skills", "discover_python_skills", ]