133 lines
4.3 KiB
Python
133 lines
4.3 KiB
Python
"""受信任 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",
|
||
]
|