diff --git a/astrbot/core/agent/runners/registry.py b/astrbot/core/agent/runners/registry.py new file mode 100644 index 0000000000..40e1763d15 --- /dev/null +++ b/astrbot/core/agent/runners/registry.py @@ -0,0 +1,275 @@ +"""Agent Runner Registry. + +Provides a global registry that allows plugins to register custom +third-party Agent Runners at runtime. Built-in runners (Dify, Coze, +DashScope, DeerFlow) are still dispatched via the static if/elif chain +in ``third_party.py``; this registry is the *fallback* path for +plugin-provided runners. + +Dynamic WebUI integration +~~~~~~~~~~~~~~~~~~~~~~~~~ +When a runner is registered the registry injects the corresponding +``options`` / ``labels`` entry into ``CONFIG_METADATA_3`` so that the +dashboard dropdown automatically reflects the new runner type. +""" + +from __future__ import annotations + +import logging +from collections.abc import Awaitable, Callable +from dataclasses import dataclass, field +from typing import Any + +from astrbot.core.agent.runners.base import BaseAgentRunner + +logger = logging.getLogger("astrbot") + + +@dataclass +class AgentRunnerEntry: + """Descriptor for a plugin-provided agent runner.""" + + runner_type: str + """Unique identifier used in ``agent_runner_type`` config, e.g. ``"my_runner"``.""" + + runner_cls: type[BaseAgentRunner] + """Concrete ``BaseAgentRunner`` subclass to instantiate.""" + + provider_id_key: str + """Config key that stores the selected provider ID, + e.g. ``"my_runner_agent_runner_provider_id"``.""" + + display_name: str + """Human-readable label shown in the WebUI dropdown.""" + + on_initialize: Callable[..., Awaitable[None]] | None = None + """Optional async callback invoked once when the pipeline stage initialises + (for pre-connection, tool sync, etc.).""" + + conversation_id_key: str | None = None + """If the runner manages its own conversation state, the sp key used + to store the conversation/thread id. ``None`` means no such state.""" + + provider_config_fields: dict[str, dict[str, Any]] = field(default_factory=dict) + """Extra provider config field definitions to inject into + CONFIG_METADATA_2, keyed by field name. + e.g. ``{"my_api_url": {"description": "API URL", "type": "string", ...}}`` + """ + + +class AgentRunnerRegistry: + """Global singleton that holds all plugin-registered runner entries.""" + + def __init__(self) -> None: + self._entries: dict[str, AgentRunnerEntry] = {} + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + def register(self, entry: AgentRunnerEntry) -> None: + """Register an agent runner entry (and inject into WebUI config).""" + if entry.runner_type in self._entries: + logger.warning( + "Replacing existing agent runner registration: %s", + entry.runner_type, + ) + + self._entries[entry.runner_type] = entry + self._inject_config_metadata(entry) + logger.info( + "Registered agent runner: %s (%s)", + entry.runner_type, + entry.display_name, + ) + + def unregister(self, runner_type: str) -> None: + """Remove an agent runner entry (and clean up WebUI config).""" + entry = self._entries.pop(runner_type, None) + if entry: + self._remove_config_metadata(entry) + logger.info("Unregistered agent runner: %s", runner_type) + + def get(self, runner_type: str) -> AgentRunnerEntry | None: + return self._entries.get(runner_type) + + def get_all(self) -> dict[str, AgentRunnerEntry]: + return dict(self._entries) + + # ------------------------------------------------------------------ + # WebUI config injection helpers + # ------------------------------------------------------------------ + + @staticmethod + def _inject_config_metadata(entry: AgentRunnerEntry) -> None: + """Mutate CONFIG_METADATA_3 to add the runner option.""" + try: + from astrbot.core.config.astrbot_config import AstrBotConfig + from astrbot.core.config.default import ( + CONFIG_METADATA_2, + CONFIG_METADATA_3, + ) + + # --- CONFIG_METADATA_3: agent_runner dropdown --- + agent_runner_section = ( + CONFIG_METADATA_3.get("ai_group", {}) + .get("metadata", {}) + .get("agent_runner", {}) + .get("items", {}) + ) + runner_type_field = agent_runner_section.get( + "provider_settings.agent_runner_type", + ) + if runner_type_field: + options: list = runner_type_field.setdefault("options", []) + labels: list = runner_type_field.setdefault("labels", []) + if entry.runner_type not in options: + options.append(entry.runner_type) + labels.append(entry.display_name) + + # --- CONFIG_METADATA_3: provider_id selector --- + prov_id_config_key = f"provider_settings.{entry.provider_id_key}" + if prov_id_config_key not in agent_runner_section: + agent_runner_section[prov_id_config_key] = { + "description": f"{entry.display_name} Agent 执行器提供商 ID", + "type": "string", + "_special": f"select_agent_runner_provider:{entry.runner_type}", + "condition": { + "provider_settings.agent_runner_type": entry.runner_type, + "provider_settings.enable": True, + }, + } + + # --- CONFIG_METADATA_2: provider_settings schema --- + prov_settings_schema = ( + CONFIG_METADATA_2.get("provider_group", {}) + .get("metadata", {}) + .get("provider_settings", {}) + .get("items", {}) + ) + if ( + prov_settings_schema + and entry.provider_id_key not in prov_settings_schema + ): + prov_settings_schema[entry.provider_id_key] = { + "type": "string", + } + + # --- CONFIG_METADATA_2: extra provider config fields --- + provider_schema = ( + CONFIG_METADATA_2.get("provider_group", {}) + .get("metadata", {}) + .get("provider", {}) + .get("items", {}) + ) + if provider_schema and entry.provider_config_fields: + for field_name, field_def in entry.provider_config_fields.items(): + if field_name not in provider_schema: + provider_schema[field_name] = field_def + + # --- Dynamic key registration --- + # Tell config migration to preserve this key. + AstrBotConfig.register_dynamic_key( + f"provider_settings.{entry.provider_id_key}" + ) + + # --- CONFIG_METADATA_2: provider config_template --- + provider_config_template = ( + CONFIG_METADATA_2.get("provider_group", {}) + .get("metadata", {}) + .get("provider", {}) + .get("config_template", {}) + ) + if entry.display_name not in provider_config_template: + template: dict[str, Any] = { + "id": entry.runner_type, + "provider": entry.runner_type, + "type": entry.runner_type, + "provider_type": "agent_runner", + "enable": True, + } + for field_name, field_def in entry.provider_config_fields.items(): + template[field_name] = field_def.get("default", "") + provider_config_template[entry.display_name] = template + + except Exception: + logger.warning( + "Failed to inject config metadata for runner %s", + entry.runner_type, + exc_info=True, + ) + + @staticmethod + def _remove_config_metadata(entry: AgentRunnerEntry) -> None: + """Reverse the injection when a runner is unregistered.""" + try: + from astrbot.core.config.astrbot_config import AstrBotConfig + from astrbot.core.config.default import ( + CONFIG_METADATA_2, + CONFIG_METADATA_3, + ) + + agent_runner_section = ( + CONFIG_METADATA_3.get("ai_group", {}) + .get("metadata", {}) + .get("agent_runner", {}) + .get("items", {}) + ) + runner_type_field = agent_runner_section.get( + "provider_settings.agent_runner_type", + ) + if runner_type_field: + options: list = runner_type_field.get("options", []) + labels: list = runner_type_field.get("labels", []) + if entry.runner_type in options: + idx = options.index(entry.runner_type) + options.pop(idx) + if idx < len(labels): + labels.pop(idx) + + prov_id_config_key = f"provider_settings.{entry.provider_id_key}" + agent_runner_section.pop(prov_id_config_key, None) + + prov_settings_schema = ( + CONFIG_METADATA_2.get("provider_group", {}) + .get("metadata", {}) + .get("provider_settings", {}) + .get("items", {}) + ) + if prov_settings_schema: + prov_settings_schema.pop(entry.provider_id_key, None) + + provider_schema = ( + CONFIG_METADATA_2.get("provider_group", {}) + .get("metadata", {}) + .get("provider", {}) + .get("items", {}) + ) + if provider_schema and entry.provider_config_fields: + for field_name in entry.provider_config_fields: + provider_schema.pop(field_name, None) + + # --- CONFIG_METADATA_2: config_template cleanup --- + provider_config_template = ( + CONFIG_METADATA_2.get("provider_group", {}) + .get("metadata", {}) + .get("provider", {}) + .get("config_template", {}) + ) + provider_config_template.pop(entry.display_name, None) + + # --- Dynamic key unregister --- + AstrBotConfig.unregister_dynamic_key( + f"provider_settings.{entry.provider_id_key}" + ) + + except Exception: + logger.warning( + "Failed to remove config metadata for runner %s", + entry.runner_type, + exc_info=True, + ) + + +# Module-level singleton +agent_runner_registry = AgentRunnerRegistry() diff --git a/astrbot/core/config/astrbot_config.py b/astrbot/core/config/astrbot_config.py index 77c298cac8..082d94f099 100644 --- a/astrbot/core/config/astrbot_config.py +++ b/astrbot/core/config/astrbot_config.py @@ -92,6 +92,22 @@ def _parse_schema(schema: dict, conf: dict) -> None: return conf + # 插件可通过 AstrBotConfig.register_dynamic_key("provider_settings.xxx") + # 来注册动态配置项,迁移时不会被删除 + _dynamic_config_keys: set[str] = set() + + @classmethod + def register_dynamic_key(cls, path: str) -> None: + """注册一个动态配置项路径,迁移时不会被删除。 + + 路径格式: "provider_settings.maibot_agent_runner_provider_id" + """ + cls._dynamic_config_keys.add(path) + + @classmethod + def unregister_dynamic_key(cls, path: str) -> None: + cls._dynamic_config_keys.discard(path) + def check_config_integrity(self, refer_conf: dict, conf: dict, path=""): """检查配置完整性,如果有新的配置项或顺序不一致则返回 True""" has_new = False @@ -130,12 +146,16 @@ def check_config_integrity(self, refer_conf: dict, conf: dict, path=""): # 直接使用现有配置 new_conf[key] = conf[key] - # 检查是否存在参考配置中没有的配置项 + # 检查不在参考配置中的项:如果在动态白名单中则保留,否则删除 for key in list(conf.keys()): if key not in refer_conf: - path_ = path + "." + key if path else key - logger.info(f"检查到配置项 {path_} 不存在,将从当前配置中删除") - has_new = True + full_path = path + "." + key if path else key + if full_path in self._dynamic_config_keys: + logger.debug(f"配置项 {full_path} 为动态注册项,保留") + new_conf[key] = conf[key] + else: + logger.info(f"检查到配置项 {full_path} 不存在,将从当前配置中删除") + has_new = True # 顺序不一致也算作变更 if list(conf.keys()) != list(new_conf.keys()): diff --git a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py index 070ad7bdee..3e22d3b802 100644 --- a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +++ b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py @@ -16,6 +16,7 @@ DeerFlowAgentRunner, ) from astrbot.core.agent.runners.dify.dify_agent_runner import DifyAgentRunner +from astrbot.core.agent.runners.registry import agent_runner_registry from astrbot.core.astr_agent_hooks import MAIN_AGENT_HOOKS from astrbot.core.message.components import Image from astrbot.core.message.message_event_result import ( @@ -31,6 +32,7 @@ if TYPE_CHECKING: from astrbot.core.agent.runners.base import BaseAgentRunner + from astrbot.core.agent.runners.registry import AgentRunnerEntry from astrbot.core.provider.entities import LLMResponse from astrbot.core.pipeline.stage import Stage from astrbot.core.platform.astr_message_event import AstrMessageEvent @@ -44,6 +46,8 @@ from .....astr_agent_context import AgentContextWrapper, AstrAgentContext from ....context import PipelineContext, call_event_hook +# Built-in runner type -> provider_id config key mapping. +# Plugin-registered runners use agent_runner_registry instead. AGENT_RUNNER_TYPE_KEY = { "dify": "dify_agent_runner_provider_id", "coze": "coze_agent_runner_provider_id", @@ -166,10 +170,15 @@ async def initialize(self, ctx: PipelineContext) -> None: self.ctx = ctx self.conf = ctx.astrbot_config self.runner_type = self.conf["provider_settings"]["agent_runner_type"] - self.prov_id = self.conf["provider_settings"].get( - AGENT_RUNNER_TYPE_KEY.get(self.runner_type, ""), - "", - ) + + # Resolve provider ID config key: check built-in map first, then registry + prov_id_key = AGENT_RUNNER_TYPE_KEY.get(self.runner_type, "") + if not prov_id_key: + registry_entry = agent_runner_registry.get(self.runner_type) + if registry_entry: + prov_id_key = registry_entry.provider_id_key + self.prov_id = self.conf["provider_settings"].get(prov_id_key, "") + settings = ctx.astrbot_config["provider_settings"] self.streaming_response: bool = settings["streaming_response"] self.unsupported_streaming_strategy: str = settings[ @@ -186,6 +195,22 @@ async def initialize(self, ctx: PipelineContext) -> None: source="Third-party runner config", ) + # Invoke on_initialize callback for plugin-registered runners + registry_entry = agent_runner_registry.get(self.runner_type) + if registry_entry and registry_entry.on_initialize and self.prov_id: + asyncio.create_task(self._run_registry_on_initialize(registry_entry)) + + async def _run_registry_on_initialize(self, entry: "AgentRunnerEntry") -> None: + """Run the on_initialize callback for a plugin-registered runner.""" + try: + await entry.on_initialize(self.ctx, self.prov_id) + except Exception as e: + logger.warning( + "[%s] on_initialize failed (will retry on first message): %s", + entry.runner_type, + e, + ) + async def _resolve_persona_custom_error_message( self, event: AstrMessageEvent ) -> str | None: @@ -337,9 +362,14 @@ async def process( elif self.runner_type == DEERFLOW_PROVIDER_TYPE: runner = DeerFlowAgentRunner[AstrAgentContext]() else: - raise ValueError( - f"Unsupported third party agent runner type: {self.runner_type}", - ) + # Fallback to plugin-registered runners + registry_entry = agent_runner_registry.get(self.runner_type) + if registry_entry: + runner = registry_entry.runner_cls[AstrAgentContext]() + else: + raise ValueError( + f"Unsupported third party agent runner type: {self.runner_type}", + ) astr_agent_ctx = AstrAgentContext( context=self.ctx.plugin_manager.context, diff --git a/astrbot/core/star/context.py b/astrbot/core/star/context.py index 606f46dd73..5369db1526 100644 --- a/astrbot/core/star/context.py +++ b/astrbot/core/star/context.py @@ -46,6 +46,7 @@ logger = logging.getLogger("astrbot") if TYPE_CHECKING: + from astrbot.core.agent.runners.registry import AgentRunnerEntry from astrbot.core.cron.manager import CronJobManager @@ -583,6 +584,47 @@ def register_provider(self, provider: Provider) -> None: """ self.provider_manager.provider_insts.append(provider) + def register_agent_runner( + self, + entry: AgentRunnerEntry, + ) -> None: + """注册一个第三方 Agent Runner。 + + 插件可通过此方法注册自定义的 Agent Runner,注册后会自动出现在 WebUI + 的执行器下拉选项中。 + + .. versionadded:: 4.6.0 (sdk) + + Args: + entry: Agent Runner 注册条目。 + + Example:: + + from astrbot.core.agent.runners.registry import AgentRunnerEntry + + context.register_agent_runner(AgentRunnerEntry( + runner_type="my_runner", + runner_cls=MyCustomAgentRunner, + provider_id_key="my_runner_agent_runner_provider_id", + display_name="My Runner", + )) + """ + from astrbot.core.agent.runners.registry import agent_runner_registry + + agent_runner_registry.register(entry) + + def unregister_agent_runner(self, runner_type: str) -> None: + """移除一个已注册的第三方 Agent Runner。 + + .. versionadded:: 4.6.0 (sdk) + + Args: + runner_type: Runner 类型标识符。 + """ + from astrbot.core.agent.runners.registry import agent_runner_registry + + agent_runner_registry.unregister(runner_type) + def register_llm_tool( self, name: str, diff --git a/dashboard/src/components/config/AstrBotCoreConfigWrapper.vue b/dashboard/src/components/config/AstrBotCoreConfigWrapper.vue index 88029a25aa..c0bc8af5f4 100644 --- a/dashboard/src/components/config/AstrBotCoreConfigWrapper.vue +++ b/dashboard/src/components/config/AstrBotCoreConfigWrapper.vue @@ -76,11 +76,18 @@ export default { const { tm: tmMetadata } = useModuleI18n('features/config-metadata'); const tm = (key) => { + if (!key) return ''; const metadataResult = tmMetadata(key); if (!metadataResult.startsWith('[MISSING:') && !metadataResult.startsWith('[INVALID:')) { return metadataResult; } - return tmConfig(key); + const configResult = tmConfig(key); + if (!configResult.startsWith('[MISSING:') && !configResult.startsWith('[INVALID:')) { + return configResult; + } + // Fallback: return the key itself so that dynamically injected + // descriptions (e.g. from AgentRunnerRegistry) display as-is. + return key; }; return { diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index 5b92e97d2f..4d6a12dafe 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -193,6 +193,7 @@ export default defineConfig({ { text: "存储", link: "/guides/storage" }, { text: "文转图", link: "/guides/html-to-pic" }, { text: "会话控制器", link: "/guides/session-control" }, + { text: "注册自定义 Agent 执行器", link: "/guides/agent-runner" }, { text: "杂项", link: "/guides/other" }, { text: "发布插件", link: "/plugin-publish" }, { text: "插件指南(旧)", link: "/plugin" }, @@ -430,6 +431,7 @@ export default defineConfig({ { text: "Storage", link: "/guides/storage" }, { text: "HTML to Image", link: "/guides/html-to-pic" }, { text: "Session Control", link: "/guides/session-control" }, + { text: "Custom Agent Runner", link: "/guides/agent-runner" }, { text: "Publish Plugin", link: "/plugin-publish" }, ], }, diff --git a/docs/zh/dev/star/guides/agent-runner.md b/docs/zh/dev/star/guides/agent-runner.md new file mode 100644 index 0000000000..f1f7db0711 --- /dev/null +++ b/docs/zh/dev/star/guides/agent-runner.md @@ -0,0 +1,186 @@ +# 注册自定义 Agent 执行器 + +> AstrBot >= 4.20.1 + +AstrBot 允许插件通过 `context.register_agent_runner()` 动态注册自定义的 **Agent 执行器(Agent Runner)**,注册后会自动出现在 WebUI 的执行器下拉选项中,用户可以像使用内置执行器一样选择和配置。 + +## 概念回顾 + +- **Agent 执行器**:负责「思考 + 做事」的组件,接收用户意图后执行多轮感知 → 规划 → 执行 → 观察循环。 +- **Chat Provider**:负责「说话」的组件,提供单轮文本补全接口。 + +如果你的插件需要对接一个已有的外部 Agent 服务,就需要注册一个自定义 Agent 执行器。 + +## 快速上手 + +### 1. 实现 Agent Runner 类 + +继承 `BaseAgentRunner`,实现 `reset()`、`step()`、`step_until_done()`、`done()`、`get_final_llm_resp()` 方法。 + +```python +import typing as T +from astrbot.core.agent.runners.base import BaseAgentRunner, AgentState +from astrbot.core.agent.hooks import BaseAgentRunHooks +from astrbot.core.agent.response import AgentResponse, AgentResponseData +from astrbot.core.agent.run_context import ContextWrapper, TContext +from astrbot.core.provider.entities import LLMResponse, ProviderRequest +from astrbot.core.message.message_event_result import MessageChain +import astrbot.core.message.components as Comp + + +class MyAgentRunner(BaseAgentRunner[TContext]): + + async def reset( + self, + request: ProviderRequest, + run_context: ContextWrapper[TContext], + agent_hooks: BaseAgentRunHooks[TContext], + provider_config: dict, + **kwargs: T.Any, + ) -> None: + """初始化 Runner 状态。 + + provider_config 为该提供商的完整配置字典, + 可以从中读取你在 provider_config_fields 中定义的字段。 + """ + self.req = request + self._state = AgentState.IDLE + self.agent_hooks = agent_hooks + self.run_context = run_context + self.final_llm_resp = None + + # 读取自定义配置 + self.api_url = provider_config.get("my_api_url", "http://127.0.0.1:8080") + self.api_key = provider_config.get("my_api_key", "") + + async def step(self): + """执行一步。这是一个 async generator,通过 yield 返回中间 / 最终结果。""" + self._transition_state(AgentState.RUNNING) + + # 在这里调用你的外部 Agent 服务 + result_text = await self._call_my_agent(self.req.prompt) + + chain = MessageChain(chain=[Comp.Plain(result_text)]) + self.final_llm_resp = LLMResponse(role="assistant", result_chain=chain) + self._transition_state(AgentState.DONE) + + yield AgentResponse( + type="llm_result", + data=AgentResponseData(chain=chain), + ) + + async def step_until_done( + self, max_step: int = 30 + ) -> T.AsyncGenerator[AgentResponse, None]: + while not self.done(): + async for resp in self.step(): + yield resp + + def done(self) -> bool: + return self._state in (AgentState.DONE, AgentState.ERROR) + + def get_final_llm_resp(self) -> LLMResponse | None: + return self.final_llm_resp + + async def _call_my_agent(self, prompt: str) -> str: + """调用外部 Agent 服务(示例)。""" + # 这里替换为你的实际逻辑 + return f"来自自定义 Agent 的回复: {prompt}" +``` + +### 2. 在插件入口注册 + +在插件的 `__init__` 方法中调用 `context.register_agent_runner()`: + +```python +from astrbot.api import star +from astrbot.core.agent.runners.registry import AgentRunnerEntry +from .my_agent_runner import MyAgentRunner + + +class Main(star.Star): + def __init__(self, context: star.Context) -> None: + super().__init__(context) + + context.register_agent_runner( + AgentRunnerEntry( + runner_type="my_agent", # 唯一标识符 + runner_cls=MyAgentRunner, # Runner 类 + provider_id_key="my_agent_provider_id", # 配置键名 + display_name="My Agent", # WebUI 显示名称 + provider_config_fields={ # 提供商配置字段 + "my_api_url": { + "description": "API 地址", + "type": "string", + "hint": "你的 Agent 服务地址。默认 http://127.0.0.1:8080", + }, + "my_api_key": { + "description": "API Key", + "type": "string", + "hint": "用于鉴权的 API Key。", + }, + }, + ) + ) +``` + +> [!TIP] +> 注册后,你的 Agent 执行器会自动出现在 WebUI 的「模型提供商」→「新增提供商」→「Agent 执行器」下拉列表中。 + +## AgentRunnerEntry 字段详解 + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `runner_type` | `str` | ✅ | 唯一标识符,用于配置项 `agent_runner_type` 的值 | +| `runner_cls` | `type[BaseAgentRunner]` | ✅ | 实现了 `BaseAgentRunner` 的具体类 | +| `provider_id_key` | `str` | ✅ | 存储所选提供商 ID 的配置键名 | +| `display_name` | `str` | ✅ | 在 WebUI 下拉列表中显示的名称 | +| `on_initialize` | `async callable` | ❌ | Pipeline 初始化时调用的异步回调,可用于预连接、工具同步等 | +| `conversation_id_key` | `str \| None` | ❌ | 如果 Runner 自行管理会话状态,用于存储会话/线程 ID 的键名 | +| `provider_config_fields` | `dict` | ❌ | 注入到提供商配置表单的额外字段定义 | + +### provider_config_fields 格式 + +每个字段定义是一个字典,支持以下属性: + +```python +{ + "field_name": { + "description": "字段显示名", # 必填 + "type": "string", # 字段类型: string, int, bool 等 + "hint": "输入提示文本", # 可选,WebUI 中的占位提示 + "default": "", # 可选,默认值 + } +} +``` + +## 注册生命周期 + +``` +插件加载 (__init__) + │ + ├─ context.register_agent_runner(entry) + │ ├─ 注入 WebUI 下拉选项 + │ ├─ 注入提供商配置字段 + │ └─ 注册配置模板 + │ + ├─ [用户在 WebUI 创建提供商并选择该执行器] + │ + ├─ on_initialize 回调 (如果设置) + │ └─ 适合执行预连接、工具同步等初始化操作 + │ + └─ 消息到达时 + ├─ reset() ← 读取 provider_config, 初始化状态 + ├─ step() ← 执行 Agent 逻辑 + └─ done() ← 检查是否完成 +``` + +## 卸载 / 清理 + +当插件被卸载时,可在 `__del__` 或自定义清理方法中移除注册: + +```python +context.unregister_agent_runner("my_agent") +``` + + diff --git a/docs/zh/providers/agent-runners.md b/docs/zh/providers/agent-runners.md index 34b9510b19..52d675a15e 100644 --- a/docs/zh/providers/agent-runners.md +++ b/docs/zh/providers/agent-runners.md @@ -4,7 +4,7 @@ Agent 执行器是 AstrBot 中用于执行 Agent 的组件,Agent 执行器负责了所有关于 AI 的功能。 -AstrBot 内置了**强大的** Agent 执行器,用户也可以选择接入第三方的 Agent 执行器服务,例如 Dify、Coze、阿里云百炼应用、DeerFlow 等,或者自己开发 Agent 执行器。 +AstrBot 内置了**强大的** Agent 执行器,用户也可以选择接入第三方的 Agent 执行器服务,例如 Dify、Coze、阿里云百炼应用、DeerFlow 等,或者通过插件注册自定义的 Agent 执行器。 想象一下,你现在已经有 AI 模型提供商,它接收你的单次请求,然后返回响应。你需要一个东西来调用这个 AI 模型提供商来实现多轮对话、工具调用等功能,这就是 Agent 执行器 的作用。 @@ -17,3 +17,7 @@ AstrBot 内置了**强大的** Agent 执行器,用户也可以选择接入第 - [扣子 Coze](/providers/agent-runners/coze) - [阿里云百炼应用](/providers/agent-runners/dashscope) - [DeerFlow](/providers/agent-runners/deerflow) + +## 开发自定义 Agent 执行器 + +如果你是插件开发者,可以通过 `context.register_agent_runner()` 注册自定义的 Agent 执行器。详见 [注册自定义 Agent 执行器](/dev/star/guides/agent-runner)。 diff --git a/docs/zh/use/agent-runner.md b/docs/zh/use/agent-runner.md index 95a4d27e01..45c4895f3d 100644 --- a/docs/zh/use/agent-runner.md +++ b/docs/zh/use/agent-runner.md @@ -4,7 +4,7 @@ Agent 执行器是 AstrBot 中用于执行 Agent 的组件。 在 v4.7.0 版本之后,我们将 Dify、Coze、阿里云百炼应用这三个提供商迁移到了 Agent 执行器层面,减少了与 AstrBot 目前功能的一些冲突。请放心,如果您从旧版本升级到 v4.7.0 版本,您无需进行任何操作,AstrBot 会自动为您迁移。此后,AstrBot 也新增了 DeerFlow Agent 执行器支持。 -AstrBot 目前支持五种 Agent 执行器: +AstrBot 目前支持五种内置 Agent 执行器: - AstrBot 内置 Agent 执行器 - Dify Agent 执行器 @@ -12,6 +12,8 @@ AstrBot 目前支持五种 Agent 执行器: - 阿里云百炼应用 Agent 执行器 - DeerFlow Agent 执行器 +此外,插件也可以通过 `context.register_agent_runner()` 注册自定义的 Agent 执行器。 + 默认情况下,AstrBot 内置 Agent 执行器为默认执行器。 ## 为什么需要抽象出 Agent 执行器 @@ -50,3 +52,9 @@ Dify、Coze、百炼应用、DeerFlow 等平台已经内置了这个循环,如 ![image](https://files.astrbot.app/docs/source/images/use/agent-runner/image.png) 在 WebUI 中,点击「配置」->「Agent 执行方式」,将执行器类型更换为你刚刚创建的 Agent 执行器类型,然后选择 `XX Agent 执行器提供商 ID` 为你刚刚创建的 Agent 执行器提供商的 ID,点击保存即可。 + +## 插件自定义 Agent 执行器 + +除了内置的 Agent 执行器,AstrBot 还允许插件动态注册自定义的 Agent 执行器。注册后,新的执行器类型会自动出现在 WebUI 的下拉选项中。 + +如果你是插件开发者,想要开发自定义的 Agent 执行器,请参阅 [开发文档 · 注册自定义 Agent 执行器](/dev/star/guides/agent-runner)。