mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-30 04:27:40 +08:00
225 lines
7.6 KiB
Python
225 lines
7.6 KiB
Python
"""Agent canonical entrypoint 的 Capability Runtime 适配器。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import importlib
|
|
import inspect
|
|
from pathlib import Path
|
|
from typing import Any, Iterable, Mapping
|
|
|
|
from app.agent.capabilities import AGENT_ENTRYPOINT_KIND, AGENT_SERVICE_KIND
|
|
from app.runtime.capabilities.errors import CapabilityAdapterContractError
|
|
from app.runtime.capabilities.model import (
|
|
ActivationPolicy,
|
|
AdapterExecutionMode,
|
|
CapabilitySpec,
|
|
SelectorSchema,
|
|
)
|
|
from app.runtime.capabilities.registry import CapabilityRegistry
|
|
from app.runtime.settings import RuntimeSettingsCompat
|
|
|
|
settings = RuntimeSettingsCompat()
|
|
|
|
|
|
_DEFAULT_CAPABILITY_ROOT = Path(__file__).resolve().parent
|
|
_SETTING_SELECTOR = "setting_truthy"
|
|
|
|
|
|
def _validate_setting_selector(config: Mapping[str, Any]) -> None:
|
|
"""限制 selector 只能读取已声明的应用设置。"""
|
|
key = config["key"]
|
|
if not isinstance(key, str) or not key or not hasattr(settings, key):
|
|
raise ValueError(f"未知应用设置:{key!r}")
|
|
|
|
|
|
AGENT_SELECTOR_SCHEMAS = {
|
|
_SETTING_SELECTOR: SelectorSchema(
|
|
required_fields=frozenset({"key"}),
|
|
validator=_validate_setting_selector,
|
|
)
|
|
}
|
|
|
|
|
|
def _load_entrypoint(spec: CapabilitySpec) -> Any:
|
|
"""按 manifest 解析 canonical 符号,不创建额外业务对象。"""
|
|
module_name, symbol_name = spec.entrypoint.split(":", maxsplit=1)
|
|
module = importlib.import_module(module_name)
|
|
try:
|
|
return getattr(module, symbol_name)
|
|
except AttributeError as error:
|
|
raise CapabilityAdapterContractError(
|
|
f"{spec.entrypoint} 未公开 Agent entrypoint"
|
|
) from error
|
|
|
|
|
|
def _lifecycle_method(spec: CapabilitySpec, candidate: Any, name: str) -> Any:
|
|
"""读取 Agent Service 必需的异步生命周期方法。"""
|
|
callback = getattr(candidate, name, None)
|
|
if not callable(callback):
|
|
raise CapabilityAdapterContractError(
|
|
f"{spec.entrypoint} 的 Agent Service 缺少 {name}()"
|
|
)
|
|
return callback
|
|
|
|
|
|
class AgentEntrypointAdapter:
|
|
"""把 canonical Python 符号作为无资源副作用的同步能力发布。"""
|
|
|
|
execution_mode = AdapterExecutionMode.SYNC
|
|
|
|
@staticmethod
|
|
def materialize(spec: CapabilitySpec) -> Any:
|
|
"""按 manifest entrypoint 导入 canonical 符号。"""
|
|
return _load_entrypoint(spec)
|
|
|
|
@staticmethod
|
|
def create(
|
|
_spec: CapabilitySpec,
|
|
implementation: Any,
|
|
_generation: int,
|
|
_previous: Any = None,
|
|
) -> Any:
|
|
"""发布 canonical 符号本身,不创建第二份业务对象。"""
|
|
return implementation
|
|
|
|
@staticmethod
|
|
def start(
|
|
_spec: CapabilitySpec,
|
|
_candidate: Any,
|
|
_generation: int,
|
|
) -> None:
|
|
"""entrypoint 不拥有业务资源,初始化由独立 service 能力负责。"""
|
|
|
|
@staticmethod
|
|
def stop(
|
|
_spec: CapabilitySpec,
|
|
_instance: Any,
|
|
_generation: int,
|
|
) -> None:
|
|
"""撤销入口可见性;业务资源由独立 service 能力关闭。"""
|
|
|
|
@staticmethod
|
|
def cleanup(
|
|
_spec: CapabilitySpec,
|
|
_candidate: Any,
|
|
_generation: int,
|
|
_error: BaseException,
|
|
) -> None:
|
|
"""entrypoint 启动无副作用,因此失败候选无需额外释放。"""
|
|
|
|
|
|
class AgentServiceAdapter:
|
|
"""把具备 initialize/close 的 canonical 对象接入异步资源生命周期。"""
|
|
|
|
execution_mode = AdapterExecutionMode.ASYNC
|
|
|
|
@staticmethod
|
|
async def materialize(spec: CapabilitySpec) -> Any:
|
|
"""在线程中导入 canonical service,避免阻塞应用事件循环。"""
|
|
return await asyncio.to_thread(_load_entrypoint, spec)
|
|
|
|
@staticmethod
|
|
async def create(
|
|
_spec: CapabilitySpec,
|
|
implementation: Any,
|
|
_generation: int,
|
|
_previous: Any = None,
|
|
) -> Any:
|
|
"""复用 canonical service,不复制其内部队列和后台任务所有权。"""
|
|
return implementation
|
|
|
|
@staticmethod
|
|
async def start(
|
|
spec: CapabilitySpec,
|
|
candidate: Any,
|
|
_generation: int,
|
|
) -> None:
|
|
"""等待 service 在当前应用事件循环完成初始化。"""
|
|
result = _lifecycle_method(spec, candidate, "initialize")()
|
|
if not inspect.isawaitable(result):
|
|
raise CapabilityAdapterContractError(
|
|
f"{spec.entrypoint}.initialize() 必须返回 awaitable"
|
|
)
|
|
await result
|
|
|
|
@staticmethod
|
|
async def stop(
|
|
spec: CapabilitySpec,
|
|
instance: Any,
|
|
_generation: int,
|
|
) -> None:
|
|
"""等待 service 停止后台任务并释放其资源。"""
|
|
result = _lifecycle_method(spec, instance, "close")()
|
|
if not inspect.isawaitable(result):
|
|
raise CapabilityAdapterContractError(
|
|
f"{spec.entrypoint}.close() 必须返回 awaitable"
|
|
)
|
|
converged = await result
|
|
if converged is False:
|
|
raise CapabilityAdapterContractError(
|
|
f"{spec.entrypoint}.close() 返回未收敛,保留 service owner"
|
|
)
|
|
|
|
@staticmethod
|
|
async def cleanup(
|
|
spec: CapabilitySpec,
|
|
candidate: Any,
|
|
generation: int,
|
|
_error: BaseException,
|
|
) -> None:
|
|
"""初始化失败或关闭竞态时按相同 close 合同释放部分资源。"""
|
|
await AgentServiceAdapter.stop(spec, candidate, generation)
|
|
|
|
|
|
def _validate_registry(registry: CapabilityRegistry) -> None:
|
|
"""固定 entrypoint 物化轴与 service 资源轴的声明合同。"""
|
|
for spec in registry.list_specs():
|
|
if set(spec.metadata) != {"name"}:
|
|
raise ValueError(f"{spec.source}: Agent Capability metadata 只能包含 name")
|
|
if spec.kind == AGENT_ENTRYPOINT_KIND:
|
|
if spec.activation is not ActivationPolicy.ON_FIRST_USE:
|
|
raise ValueError(
|
|
f"{spec.source}: Agent entrypoint 必须使用 on_first_use"
|
|
)
|
|
if spec.selector is not None or spec.watch:
|
|
raise ValueError(
|
|
f"{spec.source}: Agent entrypoint 不接受 selector 或 watch"
|
|
)
|
|
continue
|
|
if spec.activation is not ActivationPolicy.WHEN_CONFIGURED:
|
|
raise ValueError(f"{spec.source}: Agent Service 必须使用 when_configured")
|
|
selector = spec.selector
|
|
if selector is None or selector.kind != _SETTING_SELECTOR:
|
|
raise ValueError(f"{spec.source}: Agent Service 必须声明 setting_truthy")
|
|
selector_key = str(selector.config["key"])
|
|
if spec.watch != (selector_key,):
|
|
raise ValueError(
|
|
f"{spec.source}: Agent Service watch 必须只包含 selector key"
|
|
)
|
|
|
|
|
|
def build_agent_capability_registry(
|
|
roots: Iterable[Path | str] | None = None,
|
|
) -> CapabilityRegistry:
|
|
"""发现 data-only Agent manifests,不导入编排器、Provider 或工具实现。"""
|
|
registry = CapabilityRegistry.discover(
|
|
tuple(roots) if roots is not None else (_DEFAULT_CAPABILITY_ROOT,),
|
|
kinds={AGENT_ENTRYPOINT_KIND, AGENT_SERVICE_KIND},
|
|
selector_schemas=AGENT_SELECTOR_SCHEMAS,
|
|
)
|
|
_validate_registry(registry)
|
|
return registry
|
|
|
|
|
|
def should_run_agent_service(spec: CapabilitySpec) -> bool:
|
|
"""依据 manifest selector 判断 service 是否应拥有运行实例。"""
|
|
selector = spec.selector
|
|
if (
|
|
spec.kind != AGENT_SERVICE_KIND
|
|
or selector is None
|
|
or selector.kind != _SETTING_SELECTOR
|
|
):
|
|
raise ValueError(f"{spec.source}: 不是可协调的 Agent Service 声明")
|
|
return bool(getattr(settings, selector.config["key"]))
|