From 2a8bd3ceb28d256ada27534682819a1c0484fb96 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Wed, 9 Sep 2026 15:36:18 +0800 Subject: [PATCH 01/22] docs(plugins): add EventProcessor component tutorial --- docs.json | 3 + en/plugin/dev/components/add.mdx | 3 + en/plugin/dev/components/event-listener.mdx | 4 + en/plugin/dev/components/event-processor.mdx | 158 ++++++++++++++++++ en/plugin/dev/tutor.mdx | 4 + ja/plugin/dev/components/add.mdx | 3 + ja/plugin/dev/components/event-listener.mdx | 4 + ja/plugin/dev/components/event-processor.mdx | 158 ++++++++++++++++++ ja/plugin/dev/tutor.mdx | 4 + scripts/README-event-processor-validation.md | 79 +++++++++ scripts/verify-event-processor-tutorial.py | 163 +++++++++++++++++++ sitemap-alternates.xml | 21 +++ tests/agent-static-build.test.mjs | 2 +- tests/fumapress-migration.test.mjs | 6 +- tests/fumapress-static-build.test.mjs | 12 +- tests/locale-static-build.test.mjs | 4 +- zh/plugin/dev/components/add.mdx | 3 + zh/plugin/dev/components/event-listener.mdx | 4 + zh/plugin/dev/components/event-processor.mdx | 158 ++++++++++++++++++ zh/plugin/dev/tutor.mdx | 4 + 20 files changed, 787 insertions(+), 10 deletions(-) create mode 100644 en/plugin/dev/components/event-processor.mdx create mode 100644 ja/plugin/dev/components/event-processor.mdx create mode 100644 scripts/README-event-processor-validation.md create mode 100644 scripts/verify-event-processor-tutorial.py create mode 100644 zh/plugin/dev/components/event-processor.mdx diff --git a/docs.json b/docs.json index ec40b98fb..11f375f02 100644 --- a/docs.json +++ b/docs.json @@ -677,6 +677,7 @@ "group": "Component Development", "pages": [ "en/plugin/dev/components/add", + "en/plugin/dev/components/event-processor", "en/plugin/dev/components/event-listener", "en/plugin/dev/components/command", "en/plugin/dev/components/tool", @@ -980,6 +981,7 @@ "group": "组件开发", "pages": [ "zh/plugin/dev/components/add", + "zh/plugin/dev/components/event-processor", "zh/plugin/dev/components/event-listener", "zh/plugin/dev/components/command", "zh/plugin/dev/components/tool", @@ -1278,6 +1280,7 @@ "group": "コンポーネント開発", "pages": [ "ja/plugin/dev/components/add", + "ja/plugin/dev/components/event-processor", "ja/plugin/dev/components/event-listener", "ja/plugin/dev/components/command", "ja/plugin/dev/components/tool", diff --git a/en/plugin/dev/components/add.mdx b/en/plugin/dev/components/add.mdx index 4bc82626a..2e7665b69 100644 --- a/en/plugin/dev/components/add.mdx +++ b/en/plugin/dev/components/add.mdx @@ -4,6 +4,8 @@ title: "Adding Components" Plugins consist of one or more components that provide different functionalities to LangBot. Currently supported component types include: +- Plugin processor (EventProcessor): handles platform events in plugin code after an instance is created and bound. See the [tutorial](/en/plugin/dev/components/event-processor). +- Agent runner (AgentRunner): provides execution logic selected in an Agent configuration; it is distinct from a standalone plugin processor. - Event Listeners (EventListener): Listen to events during pipeline execution and modify context or pipelines. - Commands (Command): Triggered by user command messages starting with `!` (or other configured prefixes). - Tools (Tool): Called by LLMs during execution of LangBot's built-in Local Agent. @@ -70,6 +72,7 @@ To remove components, you can delete the corresponding information manually. For detailed usage of each component, please refer to: +- [EventProcessor](./event-processor) - [EventListener](./event-listener) - [Command](./command) - [Tool](./tool) diff --git a/en/plugin/dev/components/event-listener.mdx b/en/plugin/dev/components/event-listener.mdx index e36937c42..3050c078e 100644 --- a/en/plugin/dev/components/event-listener.mdx +++ b/en/plugin/dev/components/event-listener.mdx @@ -2,6 +2,10 @@ title: "Component: Event Listener" --- + +This component extends Pipeline processing steps. To handle Bot events such as incoming messages and new group members, see [Event Processor](/en/plugin/dev/components/event-processor). + + LangBot's pipeline generates events during runtime for plugins to hook into and process. Each plugin can only have one event listener (components/event_listener/default.yaml), but can register any number of events within it. ## Adding Event Listener Component diff --git a/en/plugin/dev/components/event-processor.mdx b/en/plugin/dev/components/event-processor.mdx new file mode 100644 index 000000000..1dec59c98 --- /dev/null +++ b/en/plugin/dev/components/event-processor.mdx @@ -0,0 +1,158 @@ +--- +title: "Component: Event Processor" +--- + +The EventProcessor component handles Bot events such as incoming messages and new group members. After installing the plugin, users create a **Plugin processor** in LangBot, select the component, and bind Bot events to it. + +For environment setup, see the [plugin development tutorial](/en/plugin/dev/tutor). + +## Adding an Event Processor Component + +A plugin can contain several event processors. Run `lbp comp EventProcessor` in the plugin directory. Enter `welcome` as the name and `Welcome new members` as the description. + +```bash +lbp comp EventProcessor +``` + +The generated `components/event_processor/welcome.yaml` defines the component, and `welcome.py` contains its implementation. + +## Manifest File: Event Processor + +Replace `welcome.yaml` with: + +```yaml +apiVersion: langbot/v1 +kind: EventProcessor +metadata: + name: welcome + label: + en_US: Welcome members + zh_Hans: 欢迎新成员 + ja_JP: 新しいメンバーを歓迎 + description: + en_US: Welcome new members and respond to /hello. + zh_Hans: 欢迎新成员,并响应 /hello 消息。 + ja_JP: 新しいメンバーを歓迎し、/hello に返信します。 +spec: + events: + - group.member_joined + - message.received + capabilities: + tool_calling: true + permissions: + tools: [detail, call] + config: + - name: greeting + type: string + label: + en_US: Greeting + zh_Hans: 欢迎语 + ja_JP: 歓迎メッセージ + required: true + default: Welcome aboard! + - name: reply_enabled + type: boolean + label: + en_US: Send replies + zh_Hans: 发送回复 + ja_JP: 返信する + default: true +execution: + python: + path: ./welcome.py + attr: Welcome +``` + +`spec.events` lists supported events, and `spec.config` defines settings users can edit on the processor detail page. This example handles new members and incoming messages, with a greeting and a reply switch. Sending replies requires the `tool_calling` and `permissions.tools` settings shown above. + +## Plugin Implementation + +Register handlers in the `Welcome` class's `initialize` method. Replace `welcome.py` with: + +```python +from langbot_plugin.api.definition.components.event_processor import ( + EventProcessor, + EventProcessorContext, +) +from langbot_plugin.api.entities.builtin.platform.events import ( + MemberJoinedEvent, + MessageReceivedEvent, +) +from langbot_plugin.api.entities.builtin.platform.message import Plain + + +class Welcome(EventProcessor): + async def initialize(self): + await super().initialize() + + @self.handler(MemberJoinedEvent) + async def on_join(ctx: EventProcessorContext): + name = ctx.event.member.nickname or str(ctx.event.member.id) + await ctx.log(f"Member joined: {name}") + if not ctx.config.get("reply_enabled", True): + await ctx.log("Replies are disabled for this processor") + return + greeting = ctx.config.get("greeting", "Welcome aboard!") + await ctx.reply(f"{name}, {greeting}") + await ctx.log("Welcome action completed") + + @self.handler(MessageReceivedEvent) + async def on_message(ctx: EventProcessorContext): + text = "".join( + item.text for item in ctx.event.message_chain if isinstance(item, Plain) + ).strip() + if text != "/hello": + await ctx.log("Message ignored: expected /hello") + return + if ctx.config.get("reply_enabled", True): + await ctx.reply(ctx.config.get("greeting", "Welcome aboard!")) + else: + await ctx.log("Replies are disabled for this processor") +``` + +`on_join` welcomes new members, and `on_message` replies to `/hello`. With **Send replies** turned off, they only write logs. + +`ctx.event` contains the event data, and `ctx.config` contains this processor's settings. Use `ctx.reply()` to reply and `ctx.log()` to write logs. Processing finishes when the handlers return. + +## Registering Events + +Register a handler with `@self.handler(EventClass)` and add the event identifier to `spec.events`. For example, `MemberJoinedEvent` corresponds to `group.member_joined`. + +| Event | Manifest identifier | SDK event class | +| --- | --- | --- | +| Message received | `message.received` | `MessageReceivedEvent` | +| Message edited | `message.edited` | `MessageEditedEvent` | +| Message deleted | `message.deleted` | `MessageDeletedEvent` | +| Message reaction | `message.reaction` | `MessageReactionEvent` | +| Feedback received | `feedback.received` | `FeedbackReceivedEvent` | +| Member joined | `group.member_joined` | `MemberJoinedEvent` | +| Member left | `group.member_left` | `MemberLeftEvent` | +| Member muted | `group.member_banned` | `MemberBannedEvent` | +| Group information updated | `group.info_updated` | `GroupInfoUpdatedEvent` | +| Friend request received | `friend.request_received` | `FriendRequestReceivedEvent` | +| Friend added | `friend.added` | `FriendAddedEvent` | +| Friend removed | `friend.removed` | `FriendRemovedEvent` | +| Bot invited to group | `bot.invited_to_group` | `BotInvitedToGroupEvent` | +| Bot removed from group | `bot.removed_from_group` | `BotRemovedFromGroupEvent` | +| Bot muted | `bot.muted` | `BotMutedEvent` | +| Bot unmuted | `bot.unmuted` | `BotUnmutedEvent` | +| Platform-specific event | `platform.specific` | `PlatformSpecificEvent` | + +See the SDK's [events.py](https://github.com/langbot-app/langbot-plugin-sdk/blob/main/src/langbot_plugin/api/entities/builtin/platform/events.py) for available events and fields. Event support varies by platform. + +## Testing the Event Processor + +Configure the debug connection following the [plugin development tutorial](/en/plugin/dev/tutor), then run `lbp run` in the plugin directory. In LangBot: + +1. Create a **Plugin processor**, select **Welcome members** on its detail page, configure it, and save. +2. Select **Member joined** on the left, enter the member nickname, member ID, and group ID, then click **Run test**. +3. Inspect the reply and logs. You can also select **Message received** and enter `/hello` to test replies. + +Platform replies use Mock during debugging and send no real messages. Restart `lbp run` after editing component YAML. + +After installing the plugin, bind the Bot's **Member joined** and **Message received** events to this processor to handle actual events. + +## Next Steps + +- Explore more examples in [EventProcessorDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/EventProcessorDemo). +- To extend Pipeline steps such as preprocessing or completion of a model call, use [Event Listener](/en/plugin/dev/components/event-listener). diff --git a/en/plugin/dev/tutor.mdx b/en/plugin/dev/tutor.mdx index 21d3261da..7ab4b43fc 100644 --- a/en/plugin/dev/tutor.mdx +++ b/en/plugin/dev/tutor.mdx @@ -2,6 +2,10 @@ title: "Plugin Development Tutorial" --- + +To build a plugin for incoming messages, new group members, and other Bot events, complete the basic setup here and continue with the [Event Processor tutorial](/en/plugin/dev/components/event-processor). + + ## Architecture Foundation In version 4.0, we introduced a high-security, high-flexibility production-grade plugin system and provided developers with rich APIs and easy-to-use supporting tools. diff --git a/ja/plugin/dev/components/add.mdx b/ja/plugin/dev/components/add.mdx index 48a5902b7..65610aca9 100644 --- a/ja/plugin/dev/components/add.mdx +++ b/ja/plugin/dev/components/add.mdx @@ -4,6 +4,8 @@ title: "コンポーネントの追加" プラグインは、LangBotにさまざまな機能を提供する1つ以上のコンポーネントで構成されています。現在サポートされているコンポーネントタイプには次のものがあります: +- プラグインプロセッサー(EventProcessor):インスタンスを作成してイベントに紐付け、プラグインのコードで処理します。[チュートリアル](/ja/plugin/dev/components/event-processor)を参照してください。 +- Agent ランナー(AgentRunner):Agent の設定で選択する実行コンポーネントです。独立したプラグインプロセッサーとは異なります。 - イベントリスナー(EventListener): パイプライン実行中のイベントをリッスンし、コンテキストやパイプラインを変更します。 - コマンド(Command): `!`(または他の設定されたプレフィックス)で始まるユーザーコマンドメッセージによってトリガーされます。 - ツール(Tool): LangBotの組み込みLocal Agentの実行中にLLMによって呼び出されます。 @@ -70,6 +72,7 @@ components: 各コンポーネントの詳細な使用方法については、次を参照してください: +- [EventProcessor](./event-processor) - [EventListener](./event-listener) - [Command](./command) - [Tool](./tool) diff --git a/ja/plugin/dev/components/event-listener.mdx b/ja/plugin/dev/components/event-listener.mdx index e14f12e54..4c2840606 100644 --- a/ja/plugin/dev/components/event-listener.mdx +++ b/ja/plugin/dev/components/event-listener.mdx @@ -2,6 +2,10 @@ title: "コンポーネント: イベントリスナー" --- + +このコンポーネントはパイプラインの処理を拡張します。メッセージ受信やメンバー参加などの Bot イベントを処理する場合は、[イベントプロセッサー](/ja/plugin/dev/components/event-processor)を参照してください。 + + LangBotのパイプラインは実行時にイベントを生成し、プラグインがフックして処理できるようにします。各プラグインは1つのイベントリスナー(components/event_listener/default.yaml)のみを持つことができますが、その中で任意の数のイベントを登録できます。 ## イベントリスナーコンポーネントの追加 diff --git a/ja/plugin/dev/components/event-processor.mdx b/ja/plugin/dev/components/event-processor.mdx new file mode 100644 index 000000000..b6df1487d --- /dev/null +++ b/ja/plugin/dev/components/event-processor.mdx @@ -0,0 +1,158 @@ +--- +title: "コンポーネント: イベントプロセッサー" +--- + +イベントプロセッサー(EventProcessor)は、メッセージ受信やメンバー参加などの Bot イベントを処理するコンポーネントです。インストール後、ユーザーは LangBot で「プラグインプロセッサー」を作成してコンポーネントを選び、Bot のイベントを紐付けます。 + +環境の準備は[プラグイン開発チュートリアル](/ja/plugin/dev/tutor)を参照してください。 + +## イベントプロセッサーコンポーネントの追加 + +1 つのプラグインに複数のイベントプロセッサーを追加できます。プラグインのディレクトリで `lbp comp EventProcessor` を実行し、名前に `welcome`、説明に `Welcome new members` を入力します。 + +```bash +lbp comp EventProcessor +``` + +生成される `components/event_processor/welcome.yaml` はコンポーネントを定義するファイルで、`welcome.py` に処理を記述します。 + +## マニフェストファイル: イベントプロセッサー + +`welcome.yaml` を次の内容に変更します。 + +```yaml +apiVersion: langbot/v1 +kind: EventProcessor +metadata: + name: welcome + label: + en_US: Welcome members + zh_Hans: 欢迎新成员 + ja_JP: 新しいメンバーを歓迎 + description: + en_US: Welcome new members and respond to /hello. + zh_Hans: 欢迎新成员,并响应 /hello 消息。 + ja_JP: 新しいメンバーを歓迎し、/hello に返信します。 +spec: + events: + - group.member_joined + - message.received + capabilities: + tool_calling: true + permissions: + tools: [detail, call] + config: + - name: greeting + type: string + label: + en_US: Greeting + zh_Hans: 欢迎语 + ja_JP: 歓迎メッセージ + required: true + default: Welcome aboard! + - name: reply_enabled + type: boolean + label: + en_US: Send replies + zh_Hans: 发送回复 + ja_JP: 返信する + default: true +execution: + python: + path: ./welcome.py + attr: Welcome +``` + +`spec.events` は対応するイベント、`spec.config` は詳細画面でユーザーが変更できる設定です。この例ではメンバー参加とメッセージ受信に対応し、歓迎メッセージと返信の有効・無効を設定できます。返信には、上記の `tool_calling` と `permissions.tools` の指定が必要です。 + +## プラグインの実装 + +`Welcome` クラスの `initialize` メソッド内で処理関数を登録します。`welcome.py` を次の内容に変更してください。 + +```python +from langbot_plugin.api.definition.components.event_processor import ( + EventProcessor, + EventProcessorContext, +) +from langbot_plugin.api.entities.builtin.platform.events import ( + MemberJoinedEvent, + MessageReceivedEvent, +) +from langbot_plugin.api.entities.builtin.platform.message import Plain + + +class Welcome(EventProcessor): + async def initialize(self): + await super().initialize() + + @self.handler(MemberJoinedEvent) + async def on_join(ctx: EventProcessorContext): + name = ctx.event.member.nickname or str(ctx.event.member.id) + await ctx.log(f"Member joined: {name}") + if not ctx.config.get("reply_enabled", True): + await ctx.log("Replies are disabled for this processor") + return + greeting = ctx.config.get("greeting", "Welcome aboard!") + await ctx.reply(f"{name}, {greeting}") + await ctx.log("Welcome action completed") + + @self.handler(MessageReceivedEvent) + async def on_message(ctx: EventProcessorContext): + text = "".join( + item.text for item in ctx.event.message_chain if isinstance(item, Plain) + ).strip() + if text != "/hello": + await ctx.log("Message ignored: expected /hello") + return + if ctx.config.get("reply_enabled", True): + await ctx.reply(ctx.config.get("greeting", "Welcome aboard!")) + else: + await ctx.log("Replies are disabled for this processor") +``` + +`on_join` は参加したメンバーに挨拶し、`on_message` は `/hello` に返信します。「返信する」を無効にすると、ログだけを記録します。 + +`ctx.event` は現在のイベント、`ctx.config` はこのプロセッサーの設定です。返信には `ctx.reply()`、ログの記録には `ctx.log()` を使います。関数の実行が終わると、イベントの処理も終了します。 + +## イベントの登録 + +`@self.handler(イベントクラス)` で処理関数を登録し、対応する識別子を `spec.events` に追加します。例えば `MemberJoinedEvent` は `group.member_joined` に対応します。 + +| イベント | マニフェストの識別子 | SDK のイベントクラス | +| --- | --- | --- | +| メッセージ受信 | `message.received` | `MessageReceivedEvent` | +| メッセージ編集 | `message.edited` | `MessageEditedEvent` | +| メッセージ削除 | `message.deleted` | `MessageDeletedEvent` | +| メッセージへのリアクション | `message.reaction` | `MessageReactionEvent` | +| フィードバック受信 | `feedback.received` | `FeedbackReceivedEvent` | +| メンバー参加 | `group.member_joined` | `MemberJoinedEvent` | +| メンバー退出 | `group.member_left` | `MemberLeftEvent` | +| メンバーのミュート | `group.member_banned` | `MemberBannedEvent` | +| グループ情報更新 | `group.info_updated` | `GroupInfoUpdatedEvent` | +| 友達申請受信 | `friend.request_received` | `FriendRequestReceivedEvent` | +| 友達追加 | `friend.added` | `FriendAddedEvent` | +| 友達削除 | `friend.removed` | `FriendRemovedEvent` | +| Bot のグループ招待 | `bot.invited_to_group` | `BotInvitedToGroupEvent` | +| Bot のグループからの削除 | `bot.removed_from_group` | `BotRemovedFromGroupEvent` | +| Bot のミュート | `bot.muted` | `BotMutedEvent` | +| Bot のミュート解除 | `bot.unmuted` | `BotUnmutedEvent` | +| プラットフォーム固有のイベント | `platform.specific` | `PlatformSpecificEvent` | + +利用できるイベントとフィールドは、SDK の [events.py](https://github.com/langbot-app/langbot-plugin-sdk/blob/main/src/langbot_plugin/api/entities/builtin/platform/events.py)を参照してください。対応イベントはプラットフォームによって異なります。 + +## イベントプロセッサーのテスト + +[プラグイン開発チュートリアル](/ja/plugin/dev/tutor)に従ってデバッグ接続を設定し、プラグインのディレクトリで `lbp run` を実行します。LangBot で次の操作を行ってください。 + +1. 「プラグインプロセッサー」を作成し、詳細画面で「新しいメンバーを歓迎」を選び、設定を保存します。 +2. 左側でメンバー参加を選択し、ニックネーム、メンバー ID、グループ ID を入力してテストを実行します。 +3. 返信とログを確認します。メッセージ受信を選んで `/hello` を入力すると、返信のテストもできます。 + +デバッグ中の返信は Mock で再現され、実際のメッセージは送信されません。コンポーネントの YAML を変更したら `lbp run` を再起動してください。 + +インストール後、Bot のメンバー参加とメッセージ受信をこのプロセッサーに紐付けると、実際のイベントを処理できます。 + +## 次のステップ + +- [EventProcessorDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/EventProcessorDemo)で他の実装例を確認できます。 +- パイプラインの前処理やモデル呼び出し完了時などに機能を追加する場合は、[イベントリスナー](/ja/plugin/dev/components/event-listener)を使用してください。 diff --git a/ja/plugin/dev/tutor.mdx b/ja/plugin/dev/tutor.mdx index 66b334a3a..a7911881c 100644 --- a/ja/plugin/dev/tutor.mdx +++ b/ja/plugin/dev/tutor.mdx @@ -2,6 +2,10 @@ title: "プラグイン開発チュートリアル" --- + +メッセージ受信やメンバー参加などを処理するプラグインを作る場合は、基本環境を準備した後、[イベントプロセッサーのチュートリアル](/ja/plugin/dev/components/event-processor)に進んでください。 + + ## アーキテクチャの基礎 バージョン4.0では、高セキュリティで柔軟性の高い本番環境グレードのプラグインシステムを導入し、開発者に豊富なAPIと使いやすいサポートツールを提供しています。 diff --git a/scripts/README-event-processor-validation.md b/scripts/README-event-processor-validation.md new file mode 100644 index 000000000..add5e9900 --- /dev/null +++ b/scripts/README-event-processor-validation.md @@ -0,0 +1,79 @@ +# EventProcessor tutorial verification + +The published tutorial is in `{zh,en,ja}/plugin/dev/components/event-processor.mdx`. +All three translations contain identical executable Python and component YAML. +Run the example checks with the SDK used by your LangBot installation. + +## Repeatable checks + +With the matching SDK installed in the active Python environment: + +```bash +python scripts/verify-event-processor-tutorial.py +``` + +This extracts the actual MDX examples, compares all translations, loads the +component, applies YAML defaults, and executes ten cases. They cover member names +and ID fallback, private/group commands, disabled replies, ordinary/empty/image +messages, and a failed delivery that must retain its logs without completing. +Its tool API is mocked; it does not claim platform delivery. + +With Node.js 24 or later: + +```bash +npm ci +npm test +npm run typecheck +npm run build +``` + +The build also checks static HTML, Markdown exports, navigation, canonical URLs, +alternate languages, and preservation of legacy documentation routes. + +The companion `langbot-plugin-demo/EventProcessorDemo` repository contains +`examples/event-matrix.json` and `scripts/event_matrix.py`. See its README for +authenticated Host checks covering all 17 event types and invalid payloads. +Use a dedicated observer instance with payload logging enabled and no Bot binding. + +## Verified development snapshot: 2026-09-09 + +Documentation work started from the updated `langbot-app/langbot-docs` main commit +`a86ecb4`, on branch `docs/4.11-event-processor`. The local directory still uses +the previous name `langbot-wiki`. + +The tested SDK was `dev/4.11.x` at +`f82b3ce935f9a33afee389fb39fe8dc29a45b615`, both as a freshly built wheel and via +a Git installation from the matching SDK branch in an isolated Python 3.12 environment. +The Host was `dev/4.11.x` at `ea3e32c` with local processor UI refinements. + +| Layer | Observed result | +| --- | --- | +| Documentation | 36 Node tests, 6 Python tests, TypeScript check and full build passed; all 30 post-build static checks passed | +| SDK CLI, packaging and event tests | 246 passed, including 92 new event-matrix cases and packaged CLI execution of generated EventProcessor/AgentRunner components | +| Host processor service and run ledger | 73 passed | +| Platform EBA conversion, routing and run-log authorization | 212 passed | +| EventProcessorDemo unit tests | 8 passed, including scenario subtests, lookup errors and instance isolation | +| Installed observer through HTTP → Host → Runtime | 48 passed: 42 valid cases and 6 rejected inputs; one completed run per valid input | +| Tutorial via developer Runtime and installed package | Both workflows passed; final installed checks cover 9 valid cases plus unsupported-event rejection | +| Python extracted from all three tutorial pages | 10 passed, also using the SDK installed from Git | +| Local OneBot transport → adapter → binding → installed processor | 5 passed: unbound, bound, disabled, higher priority and equal-priority order | +| Authenticated browser tutorial | Selected member joined, entered Alice/alice/tutorial-group, clicked once; one reply trace, one completed run, and automatic switch to Logs | + +The generated AgentRunner was also connected with `lbp run` and invoked through +the real Host, producing its expected echo. The tutorial EventProcessor was built +and uploaded as `tutorial-WelcomeEvents-0.1.0.lbpkg`; it continued to work after +the developer process stopped and the Host restarted. + +The localhost OneBot peer received exactly one `send_group_msg` in the bound and +equal-priority cases. Other cases sent none. It exercised normal routing, not the +debug Mock, but the remote platform was simulated locally: this is not evidence +of delivery through QQ or every other third-party service. Original QA Bot +bindings and the local extension limit were restored after testing. + +Python hot reload worked during development. Component YAML and manifest changes +required restarting `lbp run`; the tutorial explicitly documents that boundary. + +This matrix covers the registered event model and representative data, failure, +concurrency and routing branches. Arbitrary plugin business logic and every real +platform/account combination require their own integration checks. Local passes +are not a CI, publication or deployment result. diff --git a/scripts/verify-event-processor-tutorial.py b/scripts/verify-event-processor-tutorial.py new file mode 100644 index 000000000..43f88b209 --- /dev/null +++ b/scripts/verify-event-processor-tutorial.py @@ -0,0 +1,163 @@ +"""Execute the published tutorial with the matching 4.11 SDK installed. + +Run: python scripts/verify-event-processor-tutorial.py +The tool API is mocked; real Host/runtime checks are a separate validation layer. +""" + +import asyncio +import re +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import AsyncMock, Mock + +import yaml + + +ROOT = Path(__file__).resolve().parents[1] + + +def load_tutorial(): + examples = [] + for locale in ("zh", "en", "ja"): + path = ROOT / locale / "plugin/dev/components/event-processor.mdx" + source = path.read_text() + python = re.findall(r"^```python\n(.*?)^```", source, re.M | re.S) + manifests = [ + block + for block in re.findall(r"^```yaml\n(.*?)^```", source, re.M | re.S) + if "kind: EventProcessor" in block + ] + assert len(python) == len(manifests) == 1, path + examples.append((python[0], manifests[0])) + assert examples[0] == examples[1] == examples[2], "Locale examples differ" + code, manifest = examples[0] + namespace = {} + exec(compile(code, "event-processor.mdx", "exec"), namespace) + manifest = yaml.safe_load(manifest) + assert manifest["spec"]["events"] == ["group.member_joined", "message.received"] + assert manifest["spec"]["capabilities"]["tool_calling"] is True + assert manifest["spec"]["permissions"]["tools"] == ["detail", "call"] + defaults = {field["name"]: field["default"] for field in manifest["spec"]["config"]} + return namespace[manifest["execution"]["python"]["attr"]], defaults + + +async def main(): + component_class, defaults = load_tutorial() + component = component_class() + await component.initialize() + cases = [ + ( + "join", + { + "type": "group.member_joined", + "member": {"id": "alice", "nickname": "Alice"}, + }, + {}, + "Alice, Welcome aboard!", + ), + ( + "nickname-fallback", + {"type": "group.member_joined", "member": {"id": "alice"}}, + {}, + "alice, Welcome aboard!", + ), + ( + "join-disabled", + {"type": "group.member_joined"}, + {"reply_enabled": False}, + None, + ), + ( + "command-disabled", + { + "type": "message.received", + "message_chain": [{"type": "Plain", "text": "/hello"}], + }, + {"reply_enabled": False}, + None, + ), + ( + "ordinary-text", + { + "type": "message.received", + "message_chain": [{"type": "Plain", "text": "hello"}], + }, + {}, + None, + ), + ("empty-message", {"type": "message.received", "message_chain": []}, {}, None), + ( + "image-only", + { + "type": "message.received", + "message_chain": [ + {"type": "Image", "url": "https://example.com/image.png"} + ], + }, + {}, + None, + ), + ] + for chat_type in ("private", "group"): + cases.append( + ( + chat_type, + { + "type": "message.received", + "chat_type": chat_type, + "message_chain": [{"type": "Plain", "text": " /hello "}], + }, + {"greeting": "你好 🧩"}, + "你好 🧩", + ) + ) + + async def execute(name, data, config, reply, *, failure=False): + api = SimpleNamespace( + call_tool=AsyncMock( + side_effect=RuntimeError("Delivery failed") if failure else None, + return_value={"mock": True}, + ) + ) + component.get_run_api = Mock(return_value=api) + context = SimpleNamespace( + run_id=name, config={**defaults, **config}, event=SimpleNamespace(data=data) + ) + results = [] + try: + async for result in component.run(context): + results.append(result) + except RuntimeError as exc: + assert failure and str(exc) == "Delivery failed" + else: + assert not failure, "Delivery failure was swallowed" + assert sum(r.type == "run.completed" for r in results) == int(not failure) + assert all(r.run_id == name for r in results) + if reply is None: + api.call_tool.assert_not_called() + else: + api.call_tool.assert_awaited_once_with("event_reply", {"text": reply}) + started = [ + r.data["tool_call_id"] for r in results if r.type == "tool.call.started" + ] + completed = [ + r.data["tool_call_id"] + for r in results + if r.type == "tool.call.completed" + ] + assert len(started) == 1 and started == completed + if failure: + assert any(r.type == "processor.log" for r in results) + assert any(r.data.get("error") == "Delivery failed" for r in results) + print(f"PASS {name}") + + for case in cases: + await execute(*case) + await execute("delivery-failure", cases[0][1], {}, cases[0][3], failure=True) + print( + "All 10 tutorial cases passed; Chinese, English and Japanese code is identical." + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sitemap-alternates.xml b/sitemap-alternates.xml index 88cd9ce3d..44f351f32 100644 --- a/sitemap-alternates.xml +++ b/sitemap-alternates.xml @@ -2100,6 +2100,27 @@ + + https://langbot.app/docs/en/plugin/dev/components/event-processor + + + + + + + https://langbot.app/docs/zh/plugin/dev/components/event-processor + + + + + + + https://langbot.app/docs/ja/plugin/dev/components/event-processor + + + + + https://langbot.app/docs/en/plugin/dev/components/knowledge-engine diff --git a/tests/agent-static-build.test.mjs b/tests/agent-static-build.test.mjs index 0abc55a5a..9ee1c33f0 100644 --- a/tests/agent-static-build.test.mjs +++ b/tests/agent-static-build.test.mjs @@ -22,7 +22,7 @@ function links(node, result = []) { } test("every sitemap page has meaningful canonical Markdown and HTML discovery", async () => { - assert.equal(urls.length, 482); + assert.equal(urls.length, 485); for (const url of urls) { const route = relative(url); const text = await read(`${route}.md`); diff --git a/tests/fumapress-migration.test.mjs b/tests/fumapress-migration.test.mjs index 7738db42c..a99888ae5 100644 --- a/tests/fumapress-migration.test.mjs +++ b/tests/fumapress-migration.test.mjs @@ -170,8 +170,8 @@ test("MDX compatibility normalization removes internal .html route suffixes", () test("canonical MDX and nested OpenAPI sources are discovered", async () => { const docs = JSON.parse(await readFile(path.join(root, "docs.json"), "utf8")); const pages = await collectMdxDocuments(root); - assert.equal(pages.length, 305); - assert.deepEqual(Object.fromEntries([...pages.reduce((acc, page) => acc.set(page.split("/", 1)[0], (acc.get(page.split("/", 1)[0]) ?? 0) + 1), new Map()).entries()]), { en: 97, zh: 111, ja: 97 }); + assert.equal(pages.length, 308); + assert.deepEqual(Object.fromEntries([...pages.reduce((acc, page) => acc.set(page.split("/", 1)[0], (acc.get(page.split("/", 1)[0]) ?? 0) + 1), new Map()).entries()]), { en: 98, zh: 112, ja: 98 }); assert.deepEqual(collectOpenApiSources(docs), [ { locale: "en", source: "openapi/service-api-en.json", directory: "en/api-reference" }, { locale: "zh", source: "openapi/service-api-zh.json", directory: "zh/api-reference" }, @@ -216,7 +216,7 @@ test("prebuild is deterministic and preserves local assets and SEO files", async const first = await prepareFumapress({ root, outRoot: temp }); const second = await prepareFumapress({ root, outRoot: temp }); assert.deepEqual(first, second); - assert.equal(first.documents, 305); + assert.equal(first.documents, 308); assert.equal(first.fallbackDefaults, 0); assert.equal(first.localeOnlyDocuments, 14); assert.equal(first.redirects, 103); diff --git a/tests/fumapress-static-build.test.mjs b/tests/fumapress-static-build.test.mjs index b2254c36e..ee5bf234c 100644 --- a/tests/fumapress-static-build.test.mjs +++ b/tests/fumapress-static-build.test.mjs @@ -43,9 +43,9 @@ function staticHtmlForUrlPath(urlPath) { return path.join(publicRoot, ...parts, "index.html"); } -test("all 305 canonical localized documents have static HTML", async () => { +test("all canonical localized documents have static HTML", async () => { const documents = await collectMdxDocuments(root); - assert.equal(documents.length, 305); + assert.equal(documents.length, 308); const missing = []; for (const document of documents) { try { @@ -193,7 +193,7 @@ test("all locales emit the OpenAPI surface with endpoint semantics", async () => } }); -test("canonical sitemap routes match the legacy Mintlify directory structure", async () => { +test("canonical sitemap preserves legacy routes and includes current documents", async () => { const legacy = new Set((await readFile(path.join(root, "tests/fixtures/legacy-sitemap-routes.txt"), "utf8")) .trim().split("\n").map((route) => route.replace(/\/$/, ""))); // Mintlify accidentally indexed this repository maintenance note; preserve @@ -206,7 +206,11 @@ test("canonical sitemap routes match the legacy Mintlify directory structure", a const sitemap = await readFile(path.join(publicRoot, "sitemap.xml"), "utf8"); const actual = new Set([...sitemap.matchAll(/https:\/\/langbot\.app\/docs(\/[^<]+)<\/loc>/g)] .map((match) => decodeURIComponent(match[1]).replace(/\/$/, ""))); - assert.deepEqual([...actual].sort(), [...legacy].sort()); + const expected = new Set(legacy); + for (const document of await collectMdxDocuments(root)) { + expected.add(`/${document.replace(/\.mdx$/, "").replace(/\/index$/, "")}`); + } + assert.deepEqual([...actual].sort(), [...expected].sort()); }); const locales = ["en", "zh", "ja"]; diff --git a/tests/locale-static-build.test.mjs b/tests/locale-static-build.test.mjs index 96cb1900d..3cd370884 100644 --- a/tests/locale-static-build.test.mjs +++ b/tests/locale-static-build.test.mjs @@ -35,6 +35,6 @@ test("every localized HTML payload includes the preference client provider", asy } } for (const locale of ["en", "zh", "ja"]) await visit(path.join(root, locale)); - // 482 canonical content pages plus one framework 404 page per locale. - assert.equal(count, 485); + // 485 canonical content pages plus one framework 404 page per locale. + assert.equal(count, 488); }); diff --git a/zh/plugin/dev/components/add.mdx b/zh/plugin/dev/components/add.mdx index 3ad704041..e2a027f5d 100644 --- a/zh/plugin/dev/components/add.mdx +++ b/zh/plugin/dev/components/add.mdx @@ -4,6 +4,8 @@ title: "添加组件" 插件由一种或多种组件构成,为 LangBot 提供不同的功能。目前支持的组件类型有: +- 事件处理器(EventProcessor):处理机器人收到的消息、成员入群等事件。详见[开发教程](/zh/plugin/dev/components/event-processor)。 +- Agent 运行器(AgentRunner):为 Agent 提供模型调用、外部 Agent 服务连接等运行方式。 - 事件监听器(EventListener):监听流水线执行期间的事件,对上下文或流水线进行修改。 - 命令(Command):由用户通过`!`(或其他已设置的前缀)开头的命令消息触发。 - 工具(Tool):供 LangBot 内置的 Local Agent 在执行期间由 LLM 调用。 @@ -71,6 +73,7 @@ components: 各个组件的详细使用方式请参考: +- [EventProcessor](./event-processor) - [EventListener](./event-listener) - [Command](./command) - [Tool](./tool) diff --git a/zh/plugin/dev/components/event-listener.mdx b/zh/plugin/dev/components/event-listener.mdx index 4bd61cec2..b59963727 100644 --- a/zh/plugin/dev/components/event-listener.mdx +++ b/zh/plugin/dev/components/event-listener.mdx @@ -2,6 +2,10 @@ title: "组件:事件监听器" --- + +本组件用于扩展流水线的处理流程。如果您需要处理机器人收到的消息、成员入群等事件,请参阅[事件处理器](/zh/plugin/dev/components/event-processor)。 + + LangBot 的流水线在运行期间会产生一些事件,供插件 Hook 并处理。每个插件中只允许添加一个事件监听器(components/event_listener/default.yaml),但在其中可以注册任意数量的事件。 ## 添加事件监听器组件 diff --git a/zh/plugin/dev/components/event-processor.mdx b/zh/plugin/dev/components/event-processor.mdx new file mode 100644 index 000000000..9c8c51516 --- /dev/null +++ b/zh/plugin/dev/components/event-processor.mdx @@ -0,0 +1,158 @@ +--- +title: "组件:事件处理器" +--- + +事件处理器(EventProcessor)组件用于处理机器人收到的消息、成员入群等事件。用户安装插件后,可在 LangBot 中创建「插件处理器」并选择该组件,再绑定机器人的事件。 + +环境准备见[插件开发教程](/zh/plugin/dev/tutor)。 + +## 添加事件处理器组件 + +单个插件中可以添加多个事件处理器。请在插件目录执行命令 `lbp comp EventProcessor`,名称填写 `welcome`,描述填写 `Welcome new members`。 + +```bash +lbp comp EventProcessor +``` + +生成的 `components/event_processor/welcome.yaml` 定义组件信息,`welcome.py` 是处理程序。 + +## 清单文件:事件处理器 + +将 `welcome.yaml` 修改为: + +```yaml +apiVersion: langbot/v1 +kind: EventProcessor +metadata: + name: welcome + label: + en_US: Welcome members + zh_Hans: 欢迎新成员 + ja_JP: 新しいメンバーを歓迎 + description: + en_US: Welcome new members and respond to /hello. + zh_Hans: 欢迎新成员,并响应 /hello 消息。 + ja_JP: 新しいメンバーを歓迎し、/hello に返信します。 +spec: + events: + - group.member_joined + - message.received + capabilities: + tool_calling: true + permissions: + tools: [detail, call] + config: + - name: greeting + type: string + label: + en_US: Greeting + zh_Hans: 欢迎语 + ja_JP: 歓迎メッセージ + required: true + default: Welcome aboard! + - name: reply_enabled + type: boolean + label: + en_US: Send replies + zh_Hans: 发送回复 + ja_JP: 返信する + default: true +execution: + python: + path: ./welcome.py + attr: Welcome +``` + +`spec.events` 指定支持的事件,`spec.config` 定义用户可在处理器详情页修改的配置。本例支持成员入群和收到消息,并提供欢迎语、回复开关两个配置项。回复消息需要上面的 `tool_calling` 和 `permissions.tools` 配置。 + +## 插件处理 + +在 `Welcome` 类的 `initialize` 方法中注册事件处理函数。将 `welcome.py` 修改为: + +```python +from langbot_plugin.api.definition.components.event_processor import ( + EventProcessor, + EventProcessorContext, +) +from langbot_plugin.api.entities.builtin.platform.events import ( + MemberJoinedEvent, + MessageReceivedEvent, +) +from langbot_plugin.api.entities.builtin.platform.message import Plain + + +class Welcome(EventProcessor): + async def initialize(self): + await super().initialize() + + @self.handler(MemberJoinedEvent) + async def on_join(ctx: EventProcessorContext): + name = ctx.event.member.nickname or str(ctx.event.member.id) + await ctx.log(f"Member joined: {name}") + if not ctx.config.get("reply_enabled", True): + await ctx.log("Replies are disabled for this processor") + return + greeting = ctx.config.get("greeting", "Welcome aboard!") + await ctx.reply(f"{name}, {greeting}") + await ctx.log("Welcome action completed") + + @self.handler(MessageReceivedEvent) + async def on_message(ctx: EventProcessorContext): + text = "".join( + item.text for item in ctx.event.message_chain if isinstance(item, Plain) + ).strip() + if text != "/hello": + await ctx.log("Message ignored: expected /hello") + return + if ctx.config.get("reply_enabled", True): + await ctx.reply(ctx.config.get("greeting", "Welcome aboard!")) + else: + await ctx.log("Replies are disabled for this processor") +``` + +`on_join` 在成员入群时发送欢迎语,`on_message` 回复 `/hello`。关闭「发送回复」后,只记录日志。 + +处理函数中,`ctx.event` 保存当前事件的数据,`ctx.config` 是当前处理器的配置。调用 `ctx.reply()` 回复消息,`ctx.log()` 记录日志。函数执行完毕,本次处理即结束。 + +## 事件注册 + +通过 `@self.handler(事件类)` 注册处理函数,并将对应事件标识加入清单的 `spec.events`。例如,`MemberJoinedEvent` 对应 `group.member_joined`。 + +| 事件 | 清单中的标识 | SDK 事件类 | +| --- | --- | --- | +| 收到消息 | `message.received` | `MessageReceivedEvent` | +| 消息被编辑 | `message.edited` | `MessageEditedEvent` | +| 消息被删除 | `message.deleted` | `MessageDeletedEvent` | +| 消息表情回应 | `message.reaction` | `MessageReactionEvent` | +| 收到反馈 | `feedback.received` | `FeedbackReceivedEvent` | +| 成员加入群组 | `group.member_joined` | `MemberJoinedEvent` | +| 成员离开群组 | `group.member_left` | `MemberLeftEvent` | +| 成员被禁言 | `group.member_banned` | `MemberBannedEvent` | +| 群组信息更新 | `group.info_updated` | `GroupInfoUpdatedEvent` | +| 收到好友申请 | `friend.request_received` | `FriendRequestReceivedEvent` | +| 添加好友 | `friend.added` | `FriendAddedEvent` | +| 删除好友 | `friend.removed` | `FriendRemovedEvent` | +| 机器人被邀请入群 | `bot.invited_to_group` | `BotInvitedToGroupEvent` | +| 机器人被移出群组 | `bot.removed_from_group` | `BotRemovedFromGroupEvent` | +| 机器人被禁言 | `bot.muted` | `BotMutedEvent` | +| 机器人被解除禁言 | `bot.unmuted` | `BotUnmutedEvent` | +| 平台自定义事件 | `platform.specific` | `PlatformSpecificEvent` | + +可用事件及字段见 SDK 的 [events.py](https://github.com/langbot-app/langbot-plugin-sdk/blob/main/src/langbot_plugin/api/entities/builtin/platform/events.py)。各平台支持的事件有所不同。 + +## 测试事件处理器 + +按[插件开发教程](/zh/plugin/dev/tutor)配置调试连接,在插件目录执行 `lbp run`。然后在 LangBot 中: + +1. 创建「插件处理器」,在详情页选择「欢迎新成员」组件,填写配置并保存。 +2. 在左侧选择「成员加入群组」,填写成员昵称、ID 和群组 ID,点击「运行测试」。 +3. 查看回复和日志。也可以选择「收到消息」,输入 `/hello` 测试回复。 + +调试中的平台回复使用 Mock,不发送真实消息。修改组件 YAML 后,请重启 `lbp run`。 + +安装插件后,在机器人中将「成员加入群组」和「收到消息」绑定到该处理器,即可处理实际事件。 + +## 接下来做什么 + +- 查看 [EventProcessorDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/EventProcessorDemo) 中的更多示例。 +- 如果要扩展流水线的预处理、模型调用完成等步骤,请使用[事件监听器](/zh/plugin/dev/components/event-listener)。 diff --git a/zh/plugin/dev/tutor.mdx b/zh/plugin/dev/tutor.mdx index 82d40245e..f5ffd0b95 100644 --- a/zh/plugin/dev/tutor.mdx +++ b/zh/plugin/dev/tutor.mdx @@ -2,6 +2,10 @@ title: "插件开发教程" --- + +要开发处理消息、成员入群等事件的插件,可在完成基础环境准备后,继续阅读[事件处理器教程](/zh/plugin/dev/components/event-processor)。 + + ## 架构基础 在 4.0 版本中,我们引入了一种高安全性、高灵活性的生产级插件系统,并为插件开发者提供了丰富的 API 和易用的配套工具。 From cb260817302f31a535d04a59a066b60e408a389b Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Thu, 10 Sep 2026 17:20:49 +0800 Subject: [PATCH 02/22] docs(plugin): document platform and run context APIs --- docs.json | 6 + en/plugin/dev/apis/agent-run.mdx | 182 ++++++++++++++++ en/plugin/dev/apis/common.mdx | 134 +++++++++++- en/plugin/dev/apis/messages.mdx | 32 +-- en/plugin/dev/apis/platform.mdx | 78 +++++++ en/plugin/dev/components/event-processor.mdx | 16 ++ ja/plugin/dev/apis/agent-run.mdx | 182 ++++++++++++++++ ja/plugin/dev/apis/common.mdx | 136 +++++++++++- ja/plugin/dev/apis/messages.mdx | 32 +-- ja/plugin/dev/apis/platform.mdx | 78 +++++++ ja/plugin/dev/components/event-processor.mdx | 16 ++ scripts/README-event-processor-validation.md | 5 + scripts/verify-event-processor-tutorial.py | 1 + scripts/verify-plugin-api-examples.py | 207 +++++++++++++++++++ sitemap-alternates.xml | 42 ++++ tests/agent-static-build.test.mjs | 2 +- tests/fumapress-migration.test.mjs | 6 +- tests/fumapress-static-build.test.mjs | 2 +- tests/locale-static-build.test.mjs | 4 +- zh/plugin/dev/apis/agent-run.mdx | 182 ++++++++++++++++ zh/plugin/dev/apis/common.mdx | 136 +++++++++++- zh/plugin/dev/apis/messages.mdx | 33 +-- zh/plugin/dev/apis/platform.mdx | 78 +++++++ zh/plugin/dev/components/event-processor.mdx | 16 ++ 24 files changed, 1500 insertions(+), 106 deletions(-) create mode 100644 en/plugin/dev/apis/agent-run.mdx create mode 100644 en/plugin/dev/apis/platform.mdx create mode 100644 ja/plugin/dev/apis/agent-run.mdx create mode 100644 ja/plugin/dev/apis/platform.mdx create mode 100644 scripts/verify-plugin-api-examples.py create mode 100644 zh/plugin/dev/apis/agent-run.mdx create mode 100644 zh/plugin/dev/apis/platform.mdx diff --git a/docs.json b/docs.json index 11f375f02..4ef85f881 100644 --- a/docs.json +++ b/docs.json @@ -701,6 +701,8 @@ "group": "Plugin SDK API", "pages": [ "en/plugin/dev/apis/common", + "en/plugin/dev/apis/agent-run", + "en/plugin/dev/apis/platform", "en/plugin/dev/apis/pipeline-events", "en/plugin/dev/apis/messages", "en/plugin/dev/apis/tech-details" @@ -1005,6 +1007,8 @@ "group": "插件 SDK API", "pages": [ "zh/plugin/dev/apis/common", + "zh/plugin/dev/apis/agent-run", + "zh/plugin/dev/apis/platform", "zh/plugin/dev/apis/pipeline-events", "zh/plugin/dev/apis/messages", "zh/plugin/dev/apis/tech-details" @@ -1304,6 +1308,8 @@ "group": "プラグイン SDK API", "pages": [ "ja/plugin/dev/apis/common", + "ja/plugin/dev/apis/agent-run", + "ja/plugin/dev/apis/platform", "ja/plugin/dev/apis/pipeline-events", "ja/plugin/dev/apis/messages", "ja/plugin/dev/apis/tech-details" diff --git a/en/plugin/dev/apis/agent-run.mdx b/en/plugin/dev/apis/agent-run.mdx new file mode 100644 index 000000000..ea59012e1 --- /dev/null +++ b/en/plugin/dev/apis/agent-run.mdx @@ -0,0 +1,182 @@ +--- +title: "Run Context APIs" +--- + +AgentRunner and EventProcessor use run-scoped APIs to access authorized models, tools, knowledge bases, and context. Neither component exposes `self.plugin`. + +## Access + +```python +# Inside an EventProcessor handler +api = ctx.api + +# Inside AgentRunner.run(ctx) +api = self.get_run_api(ctx) +``` + +In the examples below, `api` refers to the current run API. Use `await` for the methods below, except synchronous `get_allowed_*` methods and streaming context/iterator APIs. Do not retain or reuse it across runs. + +### Configuration and permissions + +`ctx.config` contains the processor or Runner instance configuration; `self.get_plugin_config()` reads plugin-level configuration. Manifest `spec.permissions` declares requested permissions; actual resources also depend on user configuration and the current event. + +```yaml +spec: + capabilities: + tool_calling: true + permissions: + tools: [detail, call] +``` + +This is the minimum declaration for replies and tool calls. Add other entries to `spec.permissions` as needed: + +| `spec.permissions` | Operations | +| --- | --- | +| `models` | `invoke`, `stream`, `rerank`, `count_tokens` | +| `tools` | `detail`, `call` | +| `knowledge_bases` | `list`, `retrieve` | +| `history` | `page`, `search` | +| `events` | `get`, `page` | +| `storage` | `plugin`, `workspace` | +| `interactions` | `request` | + +Inspect the resources actually granted: + +```python +models = api.get_allowed_models() # Each item has model_id and operations +tools = api.get_allowed_tools() # Each item has tool_name and parameters +knowledge_bases = api.get_allowed_knowledge_bases() # Each item has kb_id +``` + +Check context capabilities through `api.ctx.context.available_apis`. Do not call an API whose flag is `False`; a manifest permission alone does not enable it. + +## Replies and logs + +| EventProcessor method | Purpose | +| --- | --- | +| `await ctx.reply(text)` | Send text to the current event conversation; return the action result. | +| `await ctx.log(text, level="info")` | Record a log at `debug`, `info`, `warning`, or `error` level. | +| `ctx.reply_stream()` | Update one reply through an async context manager. | + +`ctx.reply(text)` is equivalent to `await api.call_tool("event_reply", {"text": text})`. The event must have a reply target and grant `event_reply`. Logs and text returned by an independent Agent are not automatically sent to the platform. + +### Streaming replies + +```python +async with api.reply_stream() as reply: + await reply.update("Processing…") + await reply.update("Processing complete.") +result = reply.result +``` + +EventProcessor can use `ctx.reply_stream()` to also record the action start and result. + +`update(text)` takes the **complete text so far**, not a delta, and returns the action result. Normal exit completes the reply. Message events with platform streaming support enabled update the platform message; other replyable events buffer text and send once on successful exit. Errors or cancellation discard buffered partial text; already displayed content is not guaranteed to be retracted. Debug mode simulates platform delivery. + +This requires `available_apis.reply_stream` and the `event_reply` permission. Model streaming and platform streaming are separate APIs; the plugin explicitly connects them. + +## Models, tools, and knowledge bases + +See [Common APIs](/en/plugin/dev/apis/common) for model parameters and usage objects. The run API also provides: + +| Method | Result or purpose | +| --- | --- | +| `invoke_llm(llm_model_uuid, messages, ...)` | `Message` | +| `invoke_llm_with_usage(llm_model_uuid, messages, ...)` | `LLMInvokeResult` | +| `invoke_llm_stream(llm_model_uuid, messages, ...)` | Async iterator of `MessageChunk` | +| `invoke_llm_stream_events(llm_model_uuid, messages, ...)` | Async iterator of `LLMStreamEvent` | +| `count_tokens(llm_model_uuid, messages, funcs=None, extra_args=None, timeout=None)` | Input token count, `int` | +| `invoke_rerank(rerank_model_uuid, query, documents, top_k=None, extra_args=None, timeout=30.0)` | List with `index` and `relevance_score` | +| `get_tool_detail(tool_name)` | Tool description and parameter schema, `dict` | +| `call_tool(tool_name, parameters)` | Tool result, `dict`; no `session` or `query_id` | +| `retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` | Retrieval result list | + +All four model invocation methods also accept `remove_think=None`; non-streaming methods accept `timeout=None`. Timeouts are bounded by the remaining run time. Resource lists may be empty; do not assume a first model or knowledge base exists. + +```python +allowed = {tool.tool_name for tool in api.get_allowed_tools()} +if "event_get_actor" in allowed: + actor = await api.call_tool("event_get_actor", {}) +``` + +LangBot fixes targets for `event_*` tools; `platform_*` tools accept targets according to their schema. Use names and parameters from the granted list; pass `{}` to tools with no parameters. + +## History and events + +The current input does not include full history. Pull records within the current run scope as needed; passing another conversation ID does not expand access. + +| Method | Returns | +| --- | --- | +| `get_prompt()` | Effective prompt list; capability flag: `prompt_get` | +| `history_page(conversation_id=None, before_cursor=None, after_cursor=None, limit=50, direction="backward", include_attachments=False)` | `HistoryPage` | +| `history_search(query, filters=None, top_k=10)` | `HistorySearchResult` | +| `event_get(event_id)` | `AgentEventRecord` | +| `event_page(conversation_id=None, event_types=None, before_cursor=None, limit=50)` | `EventPage` | +| `steering_pull(mode="all", limit=None)` | `SteeringPullResult`; consume queued follow-up inputs | + +```python +if api.ctx.context.available_apis.history_page: + page = await api.history_page(limit=20) + for item in page.items: + print(item.model_dump()) + if page.has_more and page.prev_cursor: + older = await api.history_page(before_cursor=page.prev_cursor, limit=20) +``` + +Page results are SDK objects: use `.items`, `.has_more`, `.prev_cursor`, and `.next_cursor`, not dictionary indexing. Use `before_cursor` for older records, and `after_cursor` with `direction="forward"` for newer records. History search may only provide text matching. `steering_pull` accepts `all`, `one`, or `one-at-a-time` for Runners supporting follow-up inputs. + +## State and storage + +| Method | Returns | +| --- | --- | +| `state_get(scope, key)` | `{"value": ...}` | +| `state_set(scope, key, value)` | `{"success": ...}` | +| `state_delete(scope, key)` | `{"success": ...}` | +| `state_list(scope, prefix=None, limit=100)` | `{"keys": [...], "has_more": ...}` | + +State requires `available_apis.state` and a Host-enabled scope: `conversation`, `actor`, `subject`, or `runner`. Values must be JSON-serializable; use a key prefix such as `external.`. LangBot persists state; these methods return dictionaries. + +```python +if api.ctx.context.available_apis.state: + await api.state_set("conversation", "external.last_topic", "setup") + value = await api.state_get("conversation", "external.last_topic") +``` + +Plugin and workspace storage provide `set_plugin_storage`, `get_plugin_storage`, `get_plugin_storage_keys`, `delete_plugin_storage`, and the corresponding `*_workspace_storage*` methods. Parameters match [Common APIs](/en/plugin/dev/apis/common); values are `bytes`. Access requires `api.ctx.resources.storage.plugin_storage` or `workspace_storage` respectively. + +## Run records + +The following methods require the matching `available_apis` flag and only access runs within the authorized scope. + +| Method | Returns | +| --- | --- | +| `run_get(run_id=None)` | `AgentRun` | +| `run_list(conversation_id=None, statuses=None, before_cursor=None, limit=50)` | `RunPage` | +| `run_events_page(run_id=None, before_cursor=None, after_cursor=None, limit=50, direction="forward")` | `RunEventPage` | +| `run_cancel(run_id=None, reason=None)` | `AgentRun` | +| `run_append_result(result)` | `AgentRunEvent` | +| `run_finalize(run_id=None, status=None, reason=None)` | `AgentRun` | + +`run_id=None` selects the current run. EventProcessor handlers finish automatically when they return and usually do not need to append or finalize records manually. AgentRunner reports results with `yield AgentRunResult`; do not also append the same result through `run_append_result`. + +## Structured interactions (AgentRunner) + +To request confirmation or form input, yield `AgentRunResult.interaction_requested(ctx.run_id, request)`. `request` accepts an `InteractionRequest` object or dictionary. Required fields are `interaction_id`, `title`, and `fallback_text`; optional fields include `kind` (`form`, `confirmation`, `choice`), `fields`, `actions`, and `expires_at`. + +This requires `spec.capabilities.interactions: true`, `spec.permissions.interactions: [request]`, and Host authorization. Inspect `ctx.delivery.interactions` for surface support; submissions return as `interaction.submitted` events. This is a Runner result event, not a method on `ctx.api`, and is not directly exposed by ordinary EventProcessor handlers. + +## Error handling + +```python +from langbot_plugin.api.proxies.agent_run import PermissionDeniedError +from langbot_plugin.api.entities.builtin.agent_runner.errors import AgentAPIException + +try: + result = await api.call_tool("event_get_actor", {}) +except PermissionDeniedError as exc: + await ctx.log(str(exc), level="warning") +except AgentAPIException as exc: + await ctx.log(f"{exc.error.code}: {exc.error.message}", level="error") +``` + +This example runs inside an EventProcessor. `PermissionDeniedError` means a resource, operation, or capability is not granted. `AgentAPIException.error` contains `code`, `message`, `retryable`, and `details`. A timed-out send may already have succeeded; do not resend solely because `retryable` is true. diff --git a/en/plugin/dev/apis/common.mdx b/en/plugin/dev/apis/common.mdx index c2d247131..9df6a2078 100644 --- a/en/plugin/dev/apis/common.mdx +++ b/en/plugin/dev/apis/common.mdx @@ -4,6 +4,8 @@ title: "Plugin Common APIs" LangBot provides a series of APIs for plugins to operate various LangBot modules and control message contexts. +Regular components such as `Command`, `EventListener`, and `Tool` use these APIs through `self.plugin`. `AgentRunner` and `EventProcessor` use [Run Context APIs](/en/plugin/dev/apis/agent-run), without `self.plugin`. See [Platform APIs](/en/plugin/dev/apis/platform) for platform queries and specific actions. + ## Request API Operations for the currently processing user request (message). Only available in `EventListener` and `Command` components. Access methods: @@ -105,12 +107,20 @@ knowledge_bases = await event_context.list_pipeline_knowledge_bases() Only returns knowledge bases bound in the `Local Agent` configuration of the pipeline corresponding to the current request. If the current pipeline has no knowledge bases configured, or does not use `Local Agent`, an empty list is returned. +### Start a new conversation + +`await event_context.create_new_conversation()` clears the conversation currently used by the Pipeline session, so subsequent processing starts a new one. It returns an empty dictionary and does not delete platform chat history. + +### Retrieve from Pipeline knowledge bases + +`await event_context.retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` returns retrieval results. `kb_id` must come from `list_pipeline_knowledge_bases()`; this request API cannot access knowledge bases outside the Pipeline configuration. + ## LangBot API These APIs can be called in any plugin component. Access methods: - In the plugin root directory `main.py`: Internal methods of the `self` object, these APIs are all provided by the plugin class parent class `BasePlugin`. -- In any plugin component class: Internal methods of the `self.plugin` object. +- In regular plugin component classes: Internal methods of the `self.plugin` object. ### Get Plugin Configuration @@ -224,6 +234,7 @@ async def invoke_llm( messages: list[provider_message.Message], funcs: list[resource_tool.LLMTool] = [], extra_args: dict[str, Any] = {}, + timeout: float | None = None, ) -> provider_message.Message: """Invoke an LLM model""" @@ -236,6 +247,45 @@ llm_message = await self.plugin.invoke_llm( ) ``` + +### Model streaming and usage + +| Method | Returns | +| --- | --- | +| `invoke_llm_with_usage(llm_model_uuid, messages, funcs=[], extra_args={}, timeout=None)` | `LLMInvokeResult` | +| `invoke_llm_stream(llm_model_uuid, messages, funcs=[], extra_args={})` | Async iterator of `MessageChunk` | +| `invoke_llm_stream_events(llm_model_uuid, messages, funcs=[], extra_args={})` | Async iterator of `LLMStreamEvent` | + + +Parameters match `invoke_llm`. `timeout` is in seconds; non-streaming calls default to 120 seconds. Use `invoke_llm_with_usage` when token usage is needed: + +```python +from langbot_plugin.api.entities.builtin.provider.message import Message + +result = await self.plugin.invoke_llm_with_usage( + llm_model_uuid=model_uuid, + messages=[Message(role="user", content="Hello!")], +) +print(result.message.content) +if result.usage is not None: + print(result.usage.total_tokens) +``` + +`usage` may be `None`; `prompt_tokens`, `completion_tokens`, and `total_tokens` may also be absent. Missing values do not mean zero usage. Additional provider fields, such as cache usage, are preserved. + +```python +async for event in self.plugin.invoke_llm_stream_events( + llm_model_uuid=model_uuid, + messages=[Message(role="user", content="Hello!")], +): + if event.chunk is not None: + print(event.chunk.content) + if event.usage is not None: + print(event.usage.model_dump()) +``` + +`LLMStreamEvent` has optional `chunk` and `usage`; the final event may contain usage without text. `invoke_llm_stream` yields only message/tool-call chunks. `MessageChunk.content` is the current chunk; `all_content`, when supplied, is accumulated text. Replace with accumulated text and append only deltas; an `is_final` chunk may still carry text. Model streaming does not send platform messages; see [Run Context APIs](/en/plugin/dev/apis/agent-run) for explicit streaming replies. + ### List Available Parsers List Parser plugins currently available on the host, optionally filtered by MIME type. @@ -428,7 +478,7 @@ Use this in conjunction with configuration fields of type [``file or array[file` ## Knowledge Base API -These APIs are accessible via `self.plugin` in any component, allowing you to list and retrieve from all knowledge bases in the LangBot instance without pipeline restrictions. +These APIs are accessible via `self.plugin` in regular components, allowing you to list and retrieve from all knowledge bases in the LangBot instance without pipeline restrictions. ### List All Knowledge Bases @@ -486,7 +536,7 @@ for entry in results: ``` -These APIs do not require a `query_id` and can be used in any component (including Tool components). They can access all knowledge bases without being restricted to the current pipeline's configuration. +These APIs do not require a `query_id` and can be used in regular components (including Tool components). They can access all knowledge bases without being restricted to the current pipeline's configuration. ## RAG API @@ -676,3 +726,81 @@ async def get_knowledge_file_stream(self, storage_path: str) -> bytes: # Usage example file_bytes = await self.plugin.get_knowledge_file_stream(context.file_object.storage_path) ``` + + +## Additional resource APIs + +These methods are also called through `self.plugin`. + +### Invoke a rerank model + +```python +async def invoke_rerank( + self, rerank_model_uuid: str, query: str, documents: list[str], + top_k: int | None = None, extra_args: dict[str, Any] | None = None, + timeout: float = 60.0, +) -> list[dict[str, Any]]: + ... + +scores = await self.plugin.invoke_rerank( + rerank_model_uuid=rerank_model_uuid, + query="How do I configure a bot?", + documents=["Bot configuration guide", "Plugin publishing guide"], + top_k=1, +) +``` + +Results usually contain the original document `index` and `relevance_score`. + +### Invoke a parser + +```python +async def invoke_parser( + self, plugin_author: str, plugin_name: str, storage_path: str, + mime_type: str, filename: str, metadata: dict[str, Any] | None = None, +) -> dict[str, Any]: + ... + +parsers = await self.plugin.list_parsers(mime_type="application/pdf") +if parsers: + parser = parsers[0] + parsed = await self.plugin.invoke_parser( + plugin_author=parser["plugin_author"], + plugin_name=parser["plugin_name"], + storage_path=storage_path, + mime_type="application/pdf", + filename="guide.pdf", + ) +``` + +`storage_path` is a Host storage path, such as `context.file_object.storage_path` during knowledge ingestion, not a plugin-local path. The result dictionary contains `text`, `sections`, and `metadata`. + +### List vector records + +```python +async def vector_list( + self, collection_id: str, filters: dict[str, Any] | None = None, + limit: int = 20, offset: int = 0, +) -> dict[str, Any]: + ... + +page = await self.plugin.vector_list( + collection_id=collection_id, + filters={"file_id": "doc_001"}, + limit=20, + offset=0, +) +for item in page["items"]: + print(item["id"], item.get("metadata")) +``` + +Returns `items` and `total`. Items contain `id`, `document`, and `metadata`; `total` is a best-effort matching count from the storage backend. Filters use the vector API format above. + +### List plugins and commands + +```python +manifests = await self.plugin.list_plugins_manifest() +commands = await self.plugin.list_commands() +``` + +Both methods take no arguments and return the Host-provided plugin manifest list and command list respectively. diff --git a/en/plugin/dev/apis/messages.mdx b/en/plugin/dev/apis/messages.mdx index eadf571c6..eea097b8d 100644 --- a/en/plugin/dev/apis/messages.mdx +++ b/en/plugin/dev/apis/messages.mdx @@ -58,34 +58,8 @@ When creating components, named parameters must be used. For specific usage, you ## Other Entities -In addition to the above entities, there are also entities like `Friend` and `Group`. These entities are defined in `langbot_plugin.api.entities.platform.entities`. +In addition to the above entities, there are also entities like `Friend` and `Group`. These entities are defined in `langbot_plugin.api.entities.builtin.platform.entities`. -## Accessing Underlying Platform APIs +## Platform APIs - - Not applicable for 4.x. - - -To abstract away platform differences, LangBot provides an abstraction layer above messaging platform APIs, which includes the content described above and various platform adapters in the `pkg/platform/sources` directory. However, due to significant platform differences, the entity and API encapsulation in the abstraction layer is not complete. If your plugin needs to access platform-specific functionality, you can access underlying APIs according to the following instructions. - - - ```python - # For example in a plugin - @handler(GroupMessageReceived) - async def _(self, ctx: EventContext): - # Get adapter object - adapter = ctx.query.adapter - ``` - - -| Platform | Adapter | Access Method | Description | -| ------------------------- | --------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| OneBot v11 | aiocqhttp | adapter.bot | bot object corresponds to [aiocqhttp](https://github.com/nonebot/aiocqhttp) CQHttp object, please refer to aiocqhttp documentation for detailed usage | -| QQ Official API | qqofficial | adapter.bot | bot object is the SDK object under libs/qq_official_api, for detailed usage please refer to the `send_group_text_msg` method in `QQOfficialClient` in libs/qq_official_api/api.py for HTTP request methods, please refer to QQ Official API documentation for specific interface documentation | -| WeCom (Enterprise WeChat) | wecom | adapter.bot | Refer to the `send_image` method in `WecomClient` in libs/wecom_api/api.py for HTTP request methods, please refer to WeCom API documentation for specific interface documentation | -| WeChat Official Account | officialaccount | - | Please refer to WeChat Official Account API documentation for interface documentation, credential information can be obtained from adapter.bot | -| Lark | lark | adapter.api_client | Lark SDK's API Client object, please refer to [oapi-sdk-python](https://github.com/larksuite/oapi-sdk-python) for details | -| DingTalk | dingtalk | - | Please refer to DingTalk API documentation, credential information can be obtained from adapter.bot | -| Discord | discord | adapter.bot | bot object is [Rapptz/discord.py](https://github.com/Rapptz/discord.py) Discord object, please refer to documentation for detailed usage | -| Telegram | telegram | adapter.bot | bot object is [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) Telegram object, please refer to documentation for detailed usage | -| Slack | slack | adapter.bot | bot object in libs/slack_api/api.py, please refer to its code and Slack SDK for detailed usage | \ No newline at end of file +Use [Platform APIs](/en/plugin/dev/apis/platform) to query users, manage groups, and call platform-specific actions. EventProcessor and AgentRunner call authorized platform tools through [Run Context APIs](/en/plugin/dev/apis/agent-run). diff --git a/en/plugin/dev/apis/platform.mdx b/en/plugin/dev/apis/platform.mdx new file mode 100644 index 000000000..d9edab3ec --- /dev/null +++ b/en/plugin/dev/apis/platform.mdx @@ -0,0 +1,78 @@ +--- +title: "Platform APIs" +--- + +Call platform adapter APIs using a bot UUID. Continue using `self.plugin.send_message()` for message chains; use the API below for groups, users, and other platform features. + +## Call a platform API + +```python +async def call_platform_api( + self, + bot_uuid: str, + action: str, + params: dict[str, Any] | None = None, +) -> Any: + ... +``` + +Call through `self.plugin` in Command, EventListener, Tool, and other regular components. Results are serialized dictionaries, lists, scalars, or `None`, depending on the action, rather than platform SDK objects. + +```python +group = await self.plugin.call_platform_api( + bot_uuid=bot_uuid, + action="get_group_info", + params={"group_id": "123456"}, +) +``` + +`bot_uuid` comes from `await self.plugin.get_bots()` or `await event_context.get_bot_uuid()` in a Pipeline event. Use platform IDs from real events or platform configuration. + +### Common actions + +| `action` | `params` | +| --- | --- | +| `get_user_info` | `user_id` | +| `get_friend_list` | `{}` | +| `get_group_info` | `group_id` | +| `get_group_list` | `{}` | +| `get_group_member_list` | `group_id` | +| `get_group_member_info` | `group_id`, `user_id` | +| `get_message` / `delete_message` | `chat_type`, `chat_id`, `message_id` | +| `set_group_name` | `group_id`, `name` | +| `mute_member` | `group_id`, `user_id`, `duration` | +| `unmute_member` / `kick_member` | `group_id`, `user_id` | +| `leave_group` | `group_id` | +| `approve_friend_request` | `request_id`, `approve`, `remark` | +| `approve_group_invite` | `request_id`, `approve` | +| `get_file_url` | `file_id` | + + +Supported actions vary by platform and may require bot administrator permissions. `duration` is in seconds; the meaning of `0` is platform-specific, not a universal permanent mute. Queries may depend on platform caches. Calls raise an exception if the bot is not running, the action is unsupported, or parameters are invalid. + +## Platform-specific APIs + +Set the outer `action` to `call_platform_api` and put the platform action and parameters inside `params`. For example, read the bot account using OneBot Omni: + +```python +account = await self.plugin.call_platform_api( + bot_uuid=bot_uuid, + action="call_platform_api", + params={"action": "get_login_info", "params": {}}, +) +``` + +Only actions declared by the adapter are allowed; this is not unrestricted pass-through to every platform endpoint. Plugins call the Host through the runtime, rather than accessing `ctx.query.adapter` or `adapter.bot`. + +## Calling from an event processor + +EventProcessor and AgentRunner use run-authorized tools and do not expose the generic `call_platform_api` method. For example, when the event supports actor lookup and grants it: + +```python +tools = {tool.tool_name for tool in ctx.api.get_allowed_tools()} +if "event_get_actor" in tools: + actor = await ctx.api.call_tool("event_get_actor", {}) + await ctx.log(str(actor)) +``` + +See [Run Context APIs](/en/plugin/dev/apis/agent-run) for access, permissions, and replies. Event debugging mocks platform tools; `self.plugin.call_platform_api()` in regular components performs real calls. diff --git a/en/plugin/dev/components/event-processor.mdx b/en/plugin/dev/components/event-processor.mdx index 1dec59c98..135187dcf 100644 --- a/en/plugin/dev/components/event-processor.mdx +++ b/en/plugin/dev/components/event-processor.mdx @@ -114,6 +114,20 @@ class Welcome(EventProcessor): `ctx.event` contains the event data, and `ctx.config` contains this processor's settings. Use `ctx.reply()` to reply and `ctx.log()` to write logs. Processing finishes when the handlers return. +## Streaming Replies + +Use `ctx.reply_stream()` to update one reply. `update(text)` takes the complete text so far: + +```python +async with ctx.reply_stream() as reply: + await reply.update("Working…") + await reply.update("Done.") +``` + +The reply finishes when the `async with` block exits normally. If streaming is unsupported, disabled, or the event is not a message, the Host sends one complete message at the end. Exceptions discard buffered partial text. Debug runs simulate delivery. + +This API uses the same permission as `ctx.reply()`. AgentRunner components can use `self.get_run_api(ctx).reply_stream()`; model output text is still not sent automatically. + ## Registering Events Register a handler with `@self.handler(EventClass)` and add the event identifier to `spec.events`. For example, `MemberJoinedEvent` corresponds to `group.member_joined`. @@ -156,3 +170,5 @@ After installing the plugin, bind the Bot's **Member joined** and **Message rece - Explore more examples in [EventProcessorDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/EventProcessorDemo). - To extend Pipeline steps such as preprocessing or completion of a model call, use [Event Listener](/en/plugin/dev/components/event-listener). + +See [Run Context APIs](/en/plugin/dev/apis/agent-run) for replies, models, tools, history, and state. diff --git a/ja/plugin/dev/apis/agent-run.mdx b/ja/plugin/dev/apis/agent-run.mdx new file mode 100644 index 000000000..6738a54e6 --- /dev/null +++ b/ja/plugin/dev/apis/agent-run.mdx @@ -0,0 +1,182 @@ +--- +title: "実行コンテキスト API" +--- + +AgentRunner と EventProcessor は、現在の実行に許可されたモデル、ツール、ナレッジベース、コンテキストへアクセスする API を使用します。両コンポーネントでは `self.plugin` を使用できません。 + +## アクセス方法 + +```python +# Inside an EventProcessor handler +api = ctx.api + +# Inside AgentRunner.run(ctx) +api = self.get_run_api(ctx) +``` + +以下の `api` は現在の実行の API です。 同期の `get_allowed_*` とストリーミングのコンテキスト・イテレーター以外は、`await` で呼び出します。別の実行へ持ち越して再利用しないでください。 + +### 設定と権限 + +`ctx.config` はプロセッサーまたは Runner インスタンスの設定、`self.get_plugin_config()` はプラグイン全体の設定です。`spec.permissions` は要求する権限を宣言し、実際に利用できるリソースはユーザー設定と現在のイベントにも依存します。 + +```yaml +spec: + capabilities: + tool_calling: true + permissions: + tools: [detail, call] +``` + +返信とツール呼び出しに必要な最小設定です。他の機能を使う場合は `spec.permissions` に追加します: + +| `spec.permissions` | 操作 | +| --- | --- | +| `models` | `invoke`, `stream`, `rerank`, `count_tokens` | +| `tools` | `detail`, `call` | +| `knowledge_bases` | `list`, `retrieve` | +| `history` | `page`, `search` | +| `events` | `get`, `page` | +| `storage` | `plugin`, `workspace` | +| `interactions` | `request` | + +実際に許可されたリソースを確認します: + +```python +models = api.get_allowed_models() # Each item has model_id and operations +tools = api.get_allowed_tools() # Each item has tool_name and parameters +knowledge_bases = api.get_allowed_knowledge_bases() # Each item has kb_id +``` + +コンテキスト API の利用可否は `api.ctx.context.available_apis` で確認できます。`False` の API は呼び出せません。マニフェストへの権限追加だけで有効になるわけではありません。 + +## 返信とログ + +| EventProcessor メソッド | 用途 | +| --- | --- | +| `await ctx.reply(text)` | 現在のイベントの会話へテキストを送信し、操作結果を返します。 | +| `await ctx.log(text, level="info")` | `debug`、`info`、`warning`、`error` のログを記録します。 | +| `ctx.reply_stream()` | 非同期コンテキストマネージャーで同じ返信を更新します。 | + +`ctx.reply(text)` は `await api.call_tool("event_reply", {"text": text})` と同じです。イベントに返信先があり、`event_reply` が許可されている必要があります。ログや独立した Agent の出力テキストは自動送信されません。 + +### ストリーミング返信 + +```python +async with api.reply_stream() as reply: + await reply.update("Processing…") + await reply.update("Processing complete.") +result = reply.result +``` + +EventProcessor では `ctx.reply_stream()` を使うと、操作の開始と結果も記録されます。 + +`update(text)` には差分ではなく**その時点の全文**を渡します。戻り値は操作結果です。正常終了時に返信を完了します。プラットフォームでストリーミング返信が有効なメッセージイベントでは表示を更新し、それ以外の返信可能なイベントでは正常終了時に全文を一度送信します。例外やキャンセルでは未送信のテキストを破棄しますが、表示済みの内容の撤回は保証されません。デバッグ時の送信は Mock です。 + +`available_apis.reply_stream` と `event_reply` 権限が必要です。モデル出力のストリーミングとプラットフォームへの送信は別の API です。 + +## モデル、ツール、ナレッジベース + +モデルの引数と使用量の形式は[共通 API](/ja/plugin/dev/apis/common)を参照してください。実行 API には次のメソッドもあります: + +| メソッド | 戻り値・用途 | +| --- | --- | +| `invoke_llm(llm_model_uuid, messages, ...)` | `Message` | +| `invoke_llm_with_usage(llm_model_uuid, messages, ...)` | `LLMInvokeResult` | +| `invoke_llm_stream(llm_model_uuid, messages, ...)` | `MessageChunk` の非同期イテレーター | +| `invoke_llm_stream_events(llm_model_uuid, messages, ...)` | `LLMStreamEvent` の非同期イテレーター | +| `count_tokens(llm_model_uuid, messages, funcs=None, extra_args=None, timeout=None)` | 入力トークン数(`int`) | +| `invoke_rerank(rerank_model_uuid, query, documents, top_k=None, extra_args=None, timeout=30.0)` | `index`、`relevance_score` を含むリスト | +| `get_tool_detail(tool_name)` | ツールの説明と引数 schema(`dict`) | +| `call_tool(tool_name, parameters)` | ツール結果(`dict`)。`session`、`query_id` は不要 | +| `retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` | 検索結果のリスト | + +4 種類のモデル呼び出しは `remove_think=None`、非ストリーミング呼び出しは `timeout=None` も受け取ります。タイムアウトは実行の残り時間に制限されます。リソース一覧が空の場合もあります。 + +```python +allowed = {tool.tool_name for tool in api.get_allowed_tools()} +if "event_get_actor" in allowed: + actor = await api.call_tool("event_get_actor", {}) +``` + +`event_*` ツールの対象は LangBot がイベントから固定し、`platform_*` ツールの対象は schema に従って指定します。許可された一覧の名前と引数を使い、引数なしのツールには `{}` を渡します。 + +## 履歴とイベント + +現在の入力には全履歴が含まれません。必要に応じて現在の実行範囲の記録を取得します。別の会話 ID を渡してアクセス範囲を広げることはできません。 + +| メソッド | 戻り値 | +| --- | --- | +| `get_prompt()` | 適用されたプロンプトのリスト。機能フラグは `prompt_get` | +| `history_page(conversation_id=None, before_cursor=None, after_cursor=None, limit=50, direction="backward", include_attachments=False)` | `HistoryPage` | +| `history_search(query, filters=None, top_k=10)` | `HistorySearchResult` | +| `event_get(event_id)` | `AgentEventRecord` | +| `event_page(conversation_id=None, event_types=None, before_cursor=None, limit=50)` | `EventPage` | +| `steering_pull(mode="all", limit=None)` | `SteeringPullResult`。実行中に追加された入力を取得 | + +```python +if api.ctx.context.available_apis.history_page: + page = await api.history_page(limit=20) + for item in page.items: + print(item.model_dump()) + if page.has_more and page.prev_cursor: + older = await api.history_page(before_cursor=page.prev_cursor, limit=20) +``` + +ページ結果は SDK オブジェクトです。辞書ではなく `.items`、`.has_more`、`.prev_cursor`、`.next_cursor` を使います。古い記録は `before_cursor`、新しい記録は `after_cursor` と `direction="forward"` で取得します。履歴検索はテキスト一致のみの場合があります。追加入力対応 Runner の `steering_pull` は `all`、`one`、`one-at-a-time` を受け取ります。 + +## 状態とストレージ + +| メソッド | 戻り値 | +| --- | --- | +| `state_get(scope, key)` | `{"value": ...}` | +| `state_set(scope, key, value)` | `{"success": ...}` | +| `state_delete(scope, key)` | `{"success": ...}` | +| `state_list(scope, prefix=None, limit=100)` | `{"keys": [...], "has_more": ...}` | + +状態には `available_apis.state` とホストが許可するスコープ(`conversation`、`actor`、`subject`、`runner`)が必要です。値は JSON 化可能にし、`external.` などのキープレフィックスを推奨します。LangBot が永続化し、API は辞書を返します。 + +```python +if api.ctx.context.available_apis.state: + await api.state_set("conversation", "external.last_topic", "setup") + value = await api.state_get("conversation", "external.last_topic") +``` + +プラグインおよびワークスペースのストレージは `set_plugin_storage`、`get_plugin_storage`、`get_plugin_storage_keys`、`delete_plugin_storage` と対応する `*_workspace_storage*` を使用します。引数は[共通 API](/ja/plugin/dev/apis/common)と同じで、値は `bytes` です。`api.ctx.resources.storage.plugin_storage` または `workspace_storage` の権限が必要です。 + +## 実行記録 + +以下は同名の `available_apis` フラグが必要で、許可された範囲の実行のみ操作できます。 + +| メソッド | 戻り値 | +| --- | --- | +| `run_get(run_id=None)` | `AgentRun` | +| `run_list(conversation_id=None, statuses=None, before_cursor=None, limit=50)` | `RunPage` | +| `run_events_page(run_id=None, before_cursor=None, after_cursor=None, limit=50, direction="forward")` | `RunEventPage` | +| `run_cancel(run_id=None, reason=None)` | `AgentRun` | +| `run_append_result(result)` | `AgentRunEvent` | +| `run_finalize(run_id=None, status=None, reason=None)` | `AgentRun` | + +`run_id=None` は現在の実行を指します。EventProcessor はハンドラーの完了時に自動終了するため、通常は記録の手動追加・終了は不要です。AgentRunner は `yield AgentRunResult` で結果を報告します。同じ結果を `run_append_result` で重複登録しないでください。 + +## 構造化インタラクション(AgentRunner) + +確認やフォーム入力には `yield AgentRunResult.interaction_requested(ctx.run_id, request)` を使用します。`request` は `InteractionRequest` または辞書で、必須フィールドは `interaction_id`、`title`、`fallback_text` です。`kind`(`form`、`confirmation`、`choice`)、`fields`、`actions`、`expires_at` は任意です。 + +`spec.capabilities.interactions: true` と `spec.permissions.interactions: [request]` の宣言、およびホストの許可が必要です。画面の対応状況は `ctx.delivery.interactions` で確認し、送信結果は `interaction.submitted` イベントとして返ります。これは Runner の結果イベントであり、`ctx.api` のメソッドではありません。通常の EventProcessor ハンドラーには直接公開されていません。 + +## エラー処理 + +```python +from langbot_plugin.api.proxies.agent_run import PermissionDeniedError +from langbot_plugin.api.entities.builtin.agent_runner.errors import AgentAPIException + +try: + result = await api.call_tool("event_get_actor", {}) +except PermissionDeniedError as exc: + await ctx.log(str(exc), level="warning") +except AgentAPIException as exc: + await ctx.log(f"{exc.error.code}: {exc.error.message}", level="error") +``` + +EventProcessor 内の例です。`PermissionDeniedError` はリソース・操作・機能の権限不足を示します。`AgentAPIException.error` は `code`、`message`、`retryable`、`details` を持ちます。送信のタイムアウト時は成功済みの場合もあるため、`retryable` だけで再送しないでください。 diff --git a/ja/plugin/dev/apis/common.mdx b/ja/plugin/dev/apis/common.mdx index 7eb56e3bd..d1e97ce22 100644 --- a/ja/plugin/dev/apis/common.mdx +++ b/ja/plugin/dev/apis/common.mdx @@ -4,6 +4,8 @@ title: "プラグイン共通API" LangBotは、プラグインがさまざまなLangBotモジュールを操作し、メッセージコンテキストを制御するための一連のAPIを提供します。 +`Command`、`EventListener`、`Tool` などの通常のコンポーネントは `self.plugin` を使用します。`AgentRunner` と `EventProcessor` は `self.plugin` ではなく[実行コンテキスト API](/ja/plugin/dev/apis/agent-run)を使用してください。プラットフォームの操作は[プラットフォーム API](/ja/plugin/dev/apis/platform)を参照してください。 + ## リクエストAPI 現在処理中のユーザーリクエスト(メッセージ)に対する操作です。`EventListener`と`Command`コンポーネントでのみ使用可能です。アクセス方法: @@ -105,12 +107,20 @@ knowledge_bases = await event_context.list_pipeline_knowledge_bases() 現在のリクエストに対応するパイプラインの `Local Agent` 設定にバインドされたナレッジベースのみを返します。現在のパイプラインにナレッジベースが設定されていない場合、または `Local Agent` を使用していない場合は、空のリストが返されます。 +### 新しい会話を開始 + +`await event_context.create_new_conversation()` は現在のパイプラインセッションが使用している会話を解除し、後続の処理で新しい会話を開始します。空の辞書を返し、プラットフォームのチャット履歴は削除しません。 + +### パイプラインのナレッジベースを検索 + +`await event_context.retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` は検索結果のリストを返します。`kb_id` は `list_pipeline_knowledge_bases()` で取得します。このリクエスト API ではパイプラインに設定されていないナレッジベースにアクセスできません。 + ## LangBot API -これらのAPIは、任意のプラグインコンポーネントで呼び出すことができます。アクセス方法: +これらのAPIは、通常のプラグインコンポーネントで呼び出すことができます。アクセス方法: - プラグインルートディレクトリの`main.py`内: `self`オブジェクトの内部メソッド。これらのAPIはすべて、プラグインクラスの親クラス`BasePlugin`によって提供されます。 -- 任意のプラグインコンポーネントクラス内: `self.plugin`オブジェクトの内部メソッド。 +- 通常のプラグインコンポーネントクラス内: `self.plugin`オブジェクトの内部メソッド。 ### プラグイン設定の取得 @@ -224,6 +234,7 @@ async def invoke_llm( messages: list[provider_message.Message], funcs: list[resource_tool.LLMTool] = [], extra_args: dict[str, Any] = {}, + timeout: float | None = None, ) -> provider_message.Message: """LLMモデルを呼び出す""" @@ -236,6 +247,45 @@ llm_message = await self.plugin.invoke_llm( ) ``` + +### モデルのストリーミングと使用量 + +| メソッド | 戻り値 | +| --- | --- | +| `invoke_llm_with_usage(llm_model_uuid, messages, funcs=[], extra_args={}, timeout=None)` | `LLMInvokeResult` | +| `invoke_llm_stream(llm_model_uuid, messages, funcs=[], extra_args={})` | `MessageChunk` の非同期イテレーター | +| `invoke_llm_stream_events(llm_model_uuid, messages, funcs=[], extra_args={})` | `LLMStreamEvent` の非同期イテレーター | + + +引数は `invoke_llm` と同じです。`timeout` は秒数で、非ストリーミング呼び出しの既定値は 120 秒です。トークン使用量が必要な場合は `invoke_llm_with_usage` を使用します: + +```python +from langbot_plugin.api.entities.builtin.provider.message import Message + +result = await self.plugin.invoke_llm_with_usage( + llm_model_uuid=model_uuid, + messages=[Message(role="user", content="Hello!")], +) +print(result.message.content) +if result.usage is not None: + print(result.usage.total_tokens) +``` + +`usage` は `None` の場合があり、`prompt_tokens`、`completion_tokens`、`total_tokens` も欠ける場合があります。未取得は使用量ゼロを意味しません。キャッシュなどの追加フィールドは保持されます。 + +```python +async for event in self.plugin.invoke_llm_stream_events( + llm_model_uuid=model_uuid, + messages=[Message(role="user", content="Hello!")], +): + if event.chunk is not None: + print(event.chunk.content) + if event.usage is not None: + print(event.usage.model_dump()) +``` + +`LLMStreamEvent` は任意の `chunk` と `usage` を持ち、最後が使用量のみの場合もあります。`invoke_llm_stream` はメッセージ・ツール呼び出しのチャンクのみ返します。`MessageChunk.content` は現在の断片、`all_content` は提供される場合に累積テキストです。累積テキストは置換し、差分だけ追加します。`is_final` のチャンクにもテキストが含まれる場合があります。モデルのストリーミングはプラットフォームに送信しません。送信方法は[実行コンテキスト API](/ja/plugin/dev/apis/agent-run)を参照してください。 + ### 利用可能な Parser の一覧取得 ホスト上で現在利用可能な Parser プラグインを列挙します。MIME タイプで絞り込むこともできます。 @@ -428,7 +478,7 @@ file_bytes = await self.plugin.get_config_file("key") ## ナレッジベース API -これらのAPIは任意のコンポーネントから`self.plugin`経由でアクセスでき、パイプライン制限なしにLangBotインスタンス内のすべてのナレッジベースの一覧取得と検索が可能です。 +これらのAPIは通常のコンポーネントから`self.plugin`経由でアクセスでき、パイプライン制限なしにLangBotインスタンス内のすべてのナレッジベースの一覧取得と検索が可能です。 ### すべてのナレッジベースの一覧取得 @@ -486,7 +536,7 @@ for entry in results: ``` -これらのAPIは`query_id`を必要とせず、任意のコンポーネント(Toolコンポーネントを含む)で使用できます。現在のパイプライン設定に制限されることなく、すべてのナレッジベースにアクセスできます。 +これらのAPIは`query_id`を必要とせず、通常のコンポーネント(Toolコンポーネントを含む)で使用できます。現在のパイプライン設定に制限されることなく、すべてのナレッジベースにアクセスできます。 ## RAG API @@ -676,3 +726,81 @@ async def get_knowledge_file_stream(self, storage_path: str) -> bytes: # 使用例 file_bytes = await self.plugin.get_knowledge_file_stream(context.file_object.storage_path) ``` + + +## その他のリソース API + +以下も `self.plugin` から呼び出します。 + +### リランクモデルの呼び出し + +```python +async def invoke_rerank( + self, rerank_model_uuid: str, query: str, documents: list[str], + top_k: int | None = None, extra_args: dict[str, Any] | None = None, + timeout: float = 60.0, +) -> list[dict[str, Any]]: + ... + +scores = await self.plugin.invoke_rerank( + rerank_model_uuid=rerank_model_uuid, + query="How do I configure a bot?", + documents=["Bot configuration guide", "Plugin publishing guide"], + top_k=1, +) +``` + +結果には通常、元の文書の `index` と関連度 `relevance_score` が含まれます。 + +### パーサーの呼び出し + +```python +async def invoke_parser( + self, plugin_author: str, plugin_name: str, storage_path: str, + mime_type: str, filename: str, metadata: dict[str, Any] | None = None, +) -> dict[str, Any]: + ... + +parsers = await self.plugin.list_parsers(mime_type="application/pdf") +if parsers: + parser = parsers[0] + parsed = await self.plugin.invoke_parser( + plugin_author=parser["plugin_author"], + plugin_name=parser["plugin_name"], + storage_path=storage_path, + mime_type="application/pdf", + filename="guide.pdf", + ) +``` + +`storage_path` はナレッジ取り込み時の `context.file_object.storage_path` など、ホストの保存先です。プラグインのローカルパスではありません。結果の辞書には `text`、`sections`、`metadata` が含まれます。 + +### ベクトルレコード一覧 + +```python +async def vector_list( + self, collection_id: str, filters: dict[str, Any] | None = None, + limit: int = 20, offset: int = 0, +) -> dict[str, Any]: + ... + +page = await self.plugin.vector_list( + collection_id=collection_id, + filters={"file_id": "doc_001"}, + limit=20, + offset=0, +) +for item in page["items"]: + print(item["id"], item.get("metadata")) +``` + +`items` と `total` を返します。各項目は `id`、`document`、`metadata` を含み、`total` はストレージが取得可能な一致件数です。フィルターは上記のベクトル API と同じ形式です。 + +### プラグインとコマンド一覧 + +```python +manifests = await self.plugin.list_plugins_manifest() +commands = await self.plugin.list_commands() +``` + +いずれも引数なしで、ホストが提供するプラグインのマニフェスト一覧とコマンド一覧を返します。 diff --git a/ja/plugin/dev/apis/messages.mdx b/ja/plugin/dev/apis/messages.mdx index 130ca772d..f390db8b3 100644 --- a/ja/plugin/dev/apis/messages.mdx +++ b/ja/plugin/dev/apis/messages.mdx @@ -58,34 +58,8 @@ msg_chain = MessageChain([ ## その他のエンティティ -上記のエンティティに加えて、`Friend`や`Group`などのエンティティもあります。これらのエンティティは`langbot_plugin.api.entities.platform.entities`で定義されています。 +上記のエンティティに加えて、`Friend`や`Group`などのエンティティもあります。これらのエンティティは`langbot_plugin.api.entities.builtin.platform.entities`で定義されています。 -## 基盤プラットフォームAPIへのアクセス +## プラットフォーム API - - 4.xには適用されません。 - - -プラットフォームの違いを抽象化するために、LangBotはメッセージングプラットフォームAPIの上に抽象化レイヤーを提供しています。これには、上記で説明した内容と、`pkg/platform/sources`ディレクトリ内のさまざまなプラットフォームアダプターが含まれます。 ただし、プラットフォームの違いが大きいため、抽象化レイヤーのエンティティとAPIのカプセル化は完全ではありません。プラグインがプラットフォーム固有の機能にアクセスする必要がある場合は、次の手順に従って基盤APIにアクセスできます。 - - - ```python - # 例えばプラグイン内で - @handler(GroupMessageReceived) - async def _(self, ctx: EventContext): - # アダプターオブジェクトを取得 - adapter = ctx.query.adapter - ``` - - -| プラットフォーム | アダプター | アクセス方法 | 説明 | -| ------------- | --------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| OneBot v11 | aiocqhttp | adapter.bot | botオブジェクトは[aiocqhttp](https://github.com/nonebot/aiocqhttp) CQHttpオブジェクトに対応します。詳細な使用方法についてはaiocqhttpドキュメントを参照してください | -| QQ公式API | qqofficial | adapter.bot | botオブジェクトはlibs/qq_official_api以下のSDKオブジェクトです。詳細な使用方法については、libs/qq_official_api/api.pyの`QQOfficialClient`内の`send_group_text_msg`メソッドを参照してHTTPリクエストメソッドを確認し、具体的なインターフェースドキュメントについてはQQ公式APIドキュメントを参照してください | -| WeCom(企業微信) | wecom | adapter.bot | libs/wecom_api/api.pyの`WecomClient`内の`send_image`メソッドを参照してHTTPリクエストメソッドを確認し、具体的なインターフェースドキュメントについてはWeCom APIドキュメントを参照してください | -| WeChat公式アカウント | officialaccount | - | インターフェースドキュメントについてはWeChat公式アカウントAPIドキュメントを参照してください。認証情報はadapter.botから取得できます | -| Lark | lark | adapter.api_client | Lark SDKのAPI Clientオブジェクトです。詳細については[oapi-sdk-python](https://github.com/larksuite/oapi-sdk-python)を参照してください | -| DingTalk | dingtalk | - | DingTalk APIドキュメントを参照してください。認証情報はadapter.botから取得できます | -| Discord | discord | adapter.bot | botオブジェクトは[Rapptz/discord.py](https://github.com/Rapptz/discord.py) Discordオブジェクトです。詳細な使用方法についてはドキュメントを参照してください | -| Telegram | telegram | adapter.bot | botオブジェクトは[python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) Telegramオブジェクトです。詳細な使用方法についてはドキュメントを参照してください | -| Slack | slack | adapter.bot | botオブジェクトはlibs/slack_api/api.py内にあります。詳細な使用方法についてはそのコードとSlack SDKを参照してください | \ No newline at end of file +ユーザー取得、グループ管理、固有の操作には[プラットフォーム API](/ja/plugin/dev/apis/platform)を使用してください。EventProcessor と AgentRunner は[実行コンテキスト API](/ja/plugin/dev/apis/agent-run)から許可されたツールを呼び出します。 diff --git a/ja/plugin/dev/apis/platform.mdx b/ja/plugin/dev/apis/platform.mdx new file mode 100644 index 000000000..59e8f4d58 --- /dev/null +++ b/ja/plugin/dev/apis/platform.mdx @@ -0,0 +1,78 @@ +--- +title: "プラットフォーム API" +--- + +ボット UUID を指定してプラットフォームアダプターの API を呼び出します。メッセージチェーンの送信は `self.plugin.send_message()`、グループやユーザーの取得などは以下を使用します。 + +## プラットフォーム API の呼び出し + +```python +async def call_platform_api( + self, + bot_uuid: str, + action: str, + params: dict[str, Any] | None = None, +) -> Any: + ... +``` + +Command、EventListener、Tool などのコンポーネントでは `self.plugin` から呼び出します。戻り値は操作に応じた辞書、リスト、スカラー、`None` で、プラットフォーム SDK のオブジェクトではありません。 + +```python +group = await self.plugin.call_platform_api( + bot_uuid=bot_uuid, + action="get_group_info", + params={"group_id": "123456"}, +) +``` + +`bot_uuid` は `await self.plugin.get_bots()`、またはパイプラインイベントの `await event_context.get_bot_uuid()` で取得できます。プラットフォーム ID には実際のイベントや設定の値を使用します。 + +### 主な操作 + +| `action` | `params` | +| --- | --- | +| `get_user_info` | `user_id` | +| `get_friend_list` | `{}` | +| `get_group_info` | `group_id` | +| `get_group_list` | `{}` | +| `get_group_member_list` | `group_id` | +| `get_group_member_info` | `group_id`, `user_id` | +| `get_message` / `delete_message` | `chat_type`, `chat_id`, `message_id` | +| `set_group_name` | `group_id`, `name` | +| `mute_member` | `group_id`, `user_id`, `duration` | +| `unmute_member` / `kick_member` | `group_id`, `user_id` | +| `leave_group` | `group_id` | +| `approve_friend_request` | `request_id`, `approve`, `remark` | +| `approve_group_invite` | `request_id`, `approve` | +| `get_file_url` | `file_id` | + + +対応する操作はプラットフォームごとに異なり、ボットの管理者権限が必要な場合があります。`duration` は秒数で、`0` の意味はプラットフォームに依存します。共通の永久ミュート値ではありません。取得結果がキャッシュに依存することもあります。ボットが未起動、未対応の操作、引数不正の場合は例外になります。 + +## プラットフォーム固有の API + +外側の `action` を `call_platform_api` にし、`params` にプラットフォームの操作と引数を指定します。OneBot Omni でボットのアカウント情報を取得する例です: + +```python +account = await self.plugin.call_platform_api( + bot_uuid=bot_uuid, + action="call_platform_api", + params={"action": "get_login_info", "params": {}}, +) +``` + +アダプターが宣言した操作のみ呼び出せます。すべてのプラットフォーム API を任意に転送できるわけではありません。プラグインはランタイム経由でホストを呼び出し、`ctx.query.adapter` や `adapter.bot` には直接アクセスしません。 + +## イベントプロセッサーからの呼び出し + +EventProcessor と AgentRunner は実行に許可されたツールを使用し、汎用の `call_platform_api` は公開しません。現在のイベントで発起者の取得が許可されている場合の例です: + +```python +tools = {tool.tool_name for tool in ctx.api.get_allowed_tools()} +if "event_get_actor" in tools: + actor = await ctx.api.call_tool("event_get_actor", {}) + await ctx.log(str(actor)) +``` + +アクセス方法、権限、返信の例は[実行コンテキスト API](/ja/plugin/dev/apis/agent-run)を参照してください。イベントデバッグのプラットフォームツールは Mock ですが、通常のコンポーネントの `self.plugin.call_platform_api()` は実際の呼び出しです。 diff --git a/ja/plugin/dev/components/event-processor.mdx b/ja/plugin/dev/components/event-processor.mdx index b6df1487d..3abbd3c26 100644 --- a/ja/plugin/dev/components/event-processor.mdx +++ b/ja/plugin/dev/components/event-processor.mdx @@ -114,6 +114,20 @@ class Welcome(EventProcessor): `ctx.event` は現在のイベント、`ctx.config` はこのプロセッサーの設定です。返信には `ctx.reply()`、ログの記録には `ctx.log()` を使います。関数の実行が終わると、イベントの処理も終了します。 +## ストリーミング返信 + +`ctx.reply_stream()` で同じ返信を更新します。`update(text)` には、その時点の全文を渡します。 + +```python +async with ctx.reply_stream() as reply: + await reply.update("処理中…") + await reply.update("完了しました。") +``` + +`async with` ブロックが正常終了すると返信が完了します。ストリーミングが未対応・無効の場合やメッセージ以外のイベントでは、最後に全文を一度だけ送信します。例外時には、バッファ内の未完成テキストを送信しません。デバッグでは送信をシミュレーションします。 + +権限は `ctx.reply()` と共通です。AgentRunner も `self.get_run_api(ctx).reply_stream()` を利用できます。モデルの出力テキストが自動送信されることはありません。 + ## イベントの登録 `@self.handler(イベントクラス)` で処理関数を登録し、対応する識別子を `spec.events` に追加します。例えば `MemberJoinedEvent` は `group.member_joined` に対応します。 @@ -156,3 +170,5 @@ class Welcome(EventProcessor): - [EventProcessorDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/EventProcessorDemo)で他の実装例を確認できます。 - パイプラインの前処理やモデル呼び出し完了時などに機能を追加する場合は、[イベントリスナー](/ja/plugin/dev/components/event-listener)を使用してください。 + +返信、モデル、ツール、履歴、状態は[実行コンテキスト API](/ja/plugin/dev/apis/agent-run)を参照してください。 diff --git a/scripts/README-event-processor-validation.md b/scripts/README-event-processor-validation.md index add5e9900..678a5dd1e 100644 --- a/scripts/README-event-processor-validation.md +++ b/scripts/README-event-processor-validation.md @@ -6,6 +6,11 @@ Run the example checks with the SDK used by your LangBot installation. ## Repeatable checks +The API references in `{zh,en,ja}/plugin/dev/apis/` are also checked against the +installed SDK. Run `python scripts/verify-plugin-api-examples.py` to execute +the examples, validate method arguments, and check translated code parity. +The Host transport is mocked; this does not verify external delivery. + With the matching SDK installed in the active Python environment: ```bash diff --git a/scripts/verify-event-processor-tutorial.py b/scripts/verify-event-processor-tutorial.py index 43f88b209..6cd10940a 100644 --- a/scripts/verify-event-processor-tutorial.py +++ b/scripts/verify-event-processor-tutorial.py @@ -27,6 +27,7 @@ def load_tutorial(): for block in re.findall(r"^```yaml\n(.*?)^```", source, re.M | re.S) if "kind: EventProcessor" in block ] + python = [block for block in python if "class Welcome(EventProcessor):" in block] assert len(python) == len(manifests) == 1, path examples.append((python[0], manifests[0])) assert examples[0] == examples[1] == examples[2], "Locale examples differ" diff --git a/scripts/verify-plugin-api-examples.py b/scripts/verify-plugin-api-examples.py new file mode 100644 index 000000000..8f6b618b4 --- /dev/null +++ b/scripts/verify-plugin-api-examples.py @@ -0,0 +1,207 @@ +"""Run the API documentation snippets with the installed SDK and a mock Host. + +Usage: python scripts/verify-plugin-api-examples.py +This checks SDK calls and result shapes, not external platform delivery. +""" + +from __future__ import annotations + +import asyncio +import ast +import inspect +import re +from pathlib import Path +from types import SimpleNamespace +from typing import Any +from unittest.mock import AsyncMock + +from langbot_plugin.api.entities.builtin.agent_runner.context import AgentRunContext +from langbot_plugin.api.entities.builtin.agent_runner.errors import ( + AgentAPIError, + AgentAPIException, +) +from langbot_plugin.api.proxies.agent_run import AgentRunAPIProxy +from langbot_plugin.api.proxies.langbot_api import LangBotAPIProxy +from langbot_plugin.api.entities.builtin.provider.message import Message + +ROOT = Path(__file__).resolve().parents[1] + + +class Host: + def __init__(self): + self.calls = [] + + async def call_action(self, action, data, timeout=15): + name = action.value + self.calls.append((name, data)) + responses = { + "invoke_llm": { + "message": {"role": "assistant", "content": "Hello!"}, + "usage": {"total_tokens": 8}, + }, + "invoke_rerank": {"results": [{"index": 0, "relevance_score": 0.9}]}, + "list_parsers": { + "parsers": [{"plugin_author": "demo", "plugin_name": "Parser"}] + }, + "invoke_parser": {"text": "guide", "sections": [], "metadata": {}}, + "vector_list": { + "items": [{"id": "chunk", "document": "guide", "metadata": {}}], + "total": 1, + }, + "list_plugins_manifest": {"plugins": []}, + "list_commands": {"commands": []}, + "call_platform_api": {"result": {"id": "123456", "name": "Example"}}, + "call_tool": {"result": {"id": "alice"}}, + "reply_stream": {"result": {"ok": True, "mock": True}}, + "history_page": { + "items": [], + "has_more": not data.get("before_cursor"), + "prev_cursor": "older", + "next_cursor": None, + }, + "state_set": {"success": True}, + "state_get": {"value": "setup"}, + } + if name not in responses: + raise AssertionError(f"Unexpected Host action: {name}") + return responses[name] + + async def call_action_generator(self, action, data, timeout=15): + assert action.value == "invoke_llm_stream" + yield {"chunk": {"role": "assistant", "content": "Hello", "is_final": False}} + yield {"chunk": {"role": "assistant", "content": "!", "is_final": True}} + yield {"usage": {"total_tokens": 8}} + + +def context(): + return AgentRunContext.model_validate( + { + "run_id": "docs-run", + "trigger": {"type": "user_message"}, + "event": { + "event_id": "docs-event", + "event_type": "message.received", + "source": "test", + }, + "input": {"content": "Hello"}, + "delivery": {"surface": "test"}, + "runtime": {}, + "resources": { + "models": [{"model_id": "model"}], + "tools": [ + {"tool_name": "event_get_actor"}, + {"tool_name": "event_reply"}, + ], + }, + "context": { + "available_apis": { + "history_page": True, + "state": True, + "reply_stream": True, + } + }, + } + ) + + +def snippets(locale, page): + source = (ROOT / locale / f"plugin/dev/apis/{page}.mdx").read_text() + if page == "common": + headings = { + "zh": ("### 模型流式输出与用量", "## 其他资源 API"), + "en": ("### Model streaming and usage", "## Additional resource APIs"), + "ja": ("### モデルのストリーミングと使用量", "## その他のリソース API"), + }[locale] + start = source.index(headings[0]) + end = source.index("\n### ", start + len(headings[0])) + source = source[start:end] + source[source.index(headings[1]) :] + return re.findall(r"^```python\n(.*?)^```", source, re.M | re.S) + + +async def execute(block, namespace): + # Examples are function bodies; signature reference blocks may include stubs. + function = "async def example():\n" + "\n".join( + " " + line for line in block.splitlines() + ) + tree = ast.parse(function) + for node in ast.walk(tree): + if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute): + continue + owner = ast.unparse(node.func.value) + cls = ( + LangBotAPIProxy + if owner == "self.plugin" + else AgentRunAPIProxy + if owner == "api" + else None + ) + if cls is not None: + signature = inspect.signature(getattr(cls, node.func.attr)) + signature.bind( + None, + *([None] * len(node.args)), + **{kw.arg: None for kw in node.keywords}, + ) + exec(compile(tree, "plugin-api-doc-example", "exec"), namespace) + await namespace["example"]() + + +async def main(): + total = 0 + for page in ("common", "agent-run", "platform"): + blocks = snippets("zh", page) + for locale in ("en", "ja"): + assert blocks == snippets(locale, page), f"{page}: {locale} code differs" + for block in blocks: + host = Host() + api = AgentRunAPIProxy(context(), host) + ctx = SimpleNamespace(api=api, log=AsyncMock()) + namespace = { + "Any": Any, + "Message": Message, + "api": api, + "ctx": ctx, + "self": SimpleNamespace( + plugin=LangBotAPIProxy(host), get_run_api=lambda _: api + ), + "model_uuid": "model", + "rerank_model_uuid": "model", + "collection_id": "collection", + "storage_path": "files/guide.pdf", + "bot_uuid": "bot", + "print": lambda *args: None, + } + await execute(block, namespace) + if "reply.update" in block: + ops = [ + data["operation"] + for name, data in host.calls + if name == "reply_stream" + ] + assert ops == ["update", "update", "finish"], ops + if "older =" in block: + pages = [data for name, data in host.calls if name == "history_page"] + assert len(pages) == 2 and pages[1]["before_cursor"] == "older" + if "except PermissionDeniedError" in block: + # Exercise both documented error handlers with the real SDK proxy. + api._allowed_tool_names = frozenset() + await execute(block, namespace) + ctx.log.assert_awaited() + api._allowed_tool_names = frozenset({"event_get_actor"}) + host.call_action = AsyncMock( + side_effect=AgentAPIException( + AgentAPIError( + code="host.action_error", message="Failed", retryable=False + ) + ) + ) + await execute(block, namespace) + assert ctx.log.await_count == 2 + total += 1 + print( + f"{total} API examples passed; all three locales have identical code. Host delivery is mocked." + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sitemap-alternates.xml b/sitemap-alternates.xml index 44f351f32..7a81e086b 100644 --- a/sitemap-alternates.xml +++ b/sitemap-alternates.xml @@ -1932,6 +1932,27 @@ + + https://langbot.app/docs/en/plugin/dev/apis/agent-run + + + + + + + https://langbot.app/docs/zh/plugin/dev/apis/agent-run + + + + + + + https://langbot.app/docs/ja/plugin/dev/apis/agent-run + + + + + https://langbot.app/docs/en/plugin/dev/apis/common @@ -1995,6 +2016,27 @@ + + https://langbot.app/docs/en/plugin/dev/apis/platform + + + + + + + https://langbot.app/docs/zh/plugin/dev/apis/platform + + + + + + + https://langbot.app/docs/ja/plugin/dev/apis/platform + + + + + https://langbot.app/docs/en/plugin/dev/apis/tech-details diff --git a/tests/agent-static-build.test.mjs b/tests/agent-static-build.test.mjs index 9ee1c33f0..fd217fbbd 100644 --- a/tests/agent-static-build.test.mjs +++ b/tests/agent-static-build.test.mjs @@ -22,7 +22,7 @@ function links(node, result = []) { } test("every sitemap page has meaningful canonical Markdown and HTML discovery", async () => { - assert.equal(urls.length, 485); + assert.equal(urls.length, 491); for (const url of urls) { const route = relative(url); const text = await read(`${route}.md`); diff --git a/tests/fumapress-migration.test.mjs b/tests/fumapress-migration.test.mjs index a99888ae5..edc265a74 100644 --- a/tests/fumapress-migration.test.mjs +++ b/tests/fumapress-migration.test.mjs @@ -170,8 +170,8 @@ test("MDX compatibility normalization removes internal .html route suffixes", () test("canonical MDX and nested OpenAPI sources are discovered", async () => { const docs = JSON.parse(await readFile(path.join(root, "docs.json"), "utf8")); const pages = await collectMdxDocuments(root); - assert.equal(pages.length, 308); - assert.deepEqual(Object.fromEntries([...pages.reduce((acc, page) => acc.set(page.split("/", 1)[0], (acc.get(page.split("/", 1)[0]) ?? 0) + 1), new Map()).entries()]), { en: 98, zh: 112, ja: 98 }); + assert.equal(pages.length, 314); + assert.deepEqual(Object.fromEntries([...pages.reduce((acc, page) => acc.set(page.split("/", 1)[0], (acc.get(page.split("/", 1)[0]) ?? 0) + 1), new Map()).entries()]), { en: 100, zh: 114, ja: 100 }); assert.deepEqual(collectOpenApiSources(docs), [ { locale: "en", source: "openapi/service-api-en.json", directory: "en/api-reference" }, { locale: "zh", source: "openapi/service-api-zh.json", directory: "zh/api-reference" }, @@ -216,7 +216,7 @@ test("prebuild is deterministic and preserves local assets and SEO files", async const first = await prepareFumapress({ root, outRoot: temp }); const second = await prepareFumapress({ root, outRoot: temp }); assert.deepEqual(first, second); - assert.equal(first.documents, 308); + assert.equal(first.documents, 314); assert.equal(first.fallbackDefaults, 0); assert.equal(first.localeOnlyDocuments, 14); assert.equal(first.redirects, 103); diff --git a/tests/fumapress-static-build.test.mjs b/tests/fumapress-static-build.test.mjs index ee5bf234c..d7eb4e298 100644 --- a/tests/fumapress-static-build.test.mjs +++ b/tests/fumapress-static-build.test.mjs @@ -45,7 +45,7 @@ function staticHtmlForUrlPath(urlPath) { test("all canonical localized documents have static HTML", async () => { const documents = await collectMdxDocuments(root); - assert.equal(documents.length, 308); + assert.equal(documents.length, 314); const missing = []; for (const document of documents) { try { diff --git a/tests/locale-static-build.test.mjs b/tests/locale-static-build.test.mjs index 3cd370884..6ab2a9cf8 100644 --- a/tests/locale-static-build.test.mjs +++ b/tests/locale-static-build.test.mjs @@ -35,6 +35,6 @@ test("every localized HTML payload includes the preference client provider", asy } } for (const locale of ["en", "zh", "ja"]) await visit(path.join(root, locale)); - // 485 canonical content pages plus one framework 404 page per locale. - assert.equal(count, 488); + // 491 canonical content pages plus one framework 404 page per locale. + assert.equal(count, 494); }); diff --git a/zh/plugin/dev/apis/agent-run.mdx b/zh/plugin/dev/apis/agent-run.mdx new file mode 100644 index 000000000..3bc152a2d --- /dev/null +++ b/zh/plugin/dev/apis/agent-run.mdx @@ -0,0 +1,182 @@ +--- +title: "运行上下文 API" +--- + +AgentRunner 和 EventProcessor 使用当前运行的 API,访问已授权的模型、工具、知识库和上下文。这两个组件不提供 `self.plugin`。 + +## 访问方式 + +```python +# Inside an EventProcessor handler +api = ctx.api + +# Inside AgentRunner.run(ctx) +api = self.get_run_api(ctx) +``` + +以下示例中的 `api` 均指当前运行的 API。 除 `get_allowed_*` 同步方法和流式上下文/迭代接口外,下文方法均使用 `await` 调用。不要跨运行保存或复用这个对象。 + +### 配置与授权 + +`ctx.config` 是处理器或 Runner 实例的配置;`self.get_plugin_config()` 读取插件级配置。清单中的 `spec.permissions` 只声明需要的权限,实际可用资源还取决于用户配置和当前事件。 + +```yaml +spec: + capabilities: + tool_calling: true + permissions: + tools: [detail, call] +``` + +这是回复消息和调用工具所需的最小配置。需要其他能力时,在 `spec.permissions` 中按需添加: + +| `spec.permissions` | 可选操作 | +| --- | --- | +| `models` | `invoke`, `stream`, `rerank`, `count_tokens` | +| `tools` | `detail`, `call` | +| `knowledge_bases` | `list`, `retrieve` | +| `history` | `page`, `search` | +| `events` | `get`, `page` | +| `storage` | `plugin`, `workspace` | +| `interactions` | `request` | + +查询实际授权的资源: + +```python +models = api.get_allowed_models() # Each item has model_id and operations +tools = api.get_allowed_tools() # Each item has tool_name and parameters +knowledge_bases = api.get_allowed_knowledge_bases() # Each item has kb_id +``` + +上下文能力可通过 `api.ctx.context.available_apis` 检查。某项为 `False` 时不要调用对应接口;只在清单中声明权限不会自动启用它。 + +## 回复与日志 + +| EventProcessor 方法 | 用途 | +| --- | --- | +| `await ctx.reply(text)` | 向当前事件的会话发送文本,返回动作结果。 | +| `await ctx.log(text, level="info")` | 记录日志;级别为 `debug`、`info`、`warning`、`error`。 | +| `ctx.reply_stream()` | 以异步上下文管理器更新同一条回复。 | + +`ctx.reply(text)` 等价于 `await api.call_tool("event_reply", {"text": text})`。需要当前事件具有回复目标,并且 `event_reply` 已被授权。日志和独立 Agent 返回的文本不会自动发送到平台。 + +### 流式回复 + +```python +async with api.reply_stream() as reply: + await reply.update("Processing…") + await reply.update("Processing complete.") +result = reply.result +``` + +EventProcessor 也可使用 `ctx.reply_stream()`,它会同时记录动作的开始与结果。 + +`update(text)` 接收**当前完整文本**,不是新增片段;返回本次动作结果。正常退出时完成回复。支持流式且开启流式回复的消息事件会更新平台消息;其他可回复事件会缓存文本,在成功结束时发送一次。异常或取消时丢弃缓存中的未完成文本,已显示的内容不保证能撤回。调试模式仅模拟平台发送。 + +此接口需要 `available_apis.reply_stream` 和 `event_reply` 权限。模型的流式输出与平台的流式发送是两个接口,需要插件主动连接。 + +## 模型、工具与知识库 + +模型调用的参数和用量结构见[通用 API](/zh/plugin/dev/apis/common)。运行 API 还提供以下方法: + +| 方法 | 返回或用途 | +| --- | --- | +| `invoke_llm(llm_model_uuid, messages, ...)` | `Message` | +| `invoke_llm_with_usage(llm_model_uuid, messages, ...)` | `LLMInvokeResult` | +| `invoke_llm_stream(llm_model_uuid, messages, ...)` | 异步迭代 `MessageChunk` | +| `invoke_llm_stream_events(llm_model_uuid, messages, ...)` | 异步迭代 `LLMStreamEvent` | +| `count_tokens(llm_model_uuid, messages, funcs=None, extra_args=None, timeout=None)` | 模型输入 token 数,`int` | +| `invoke_rerank(rerank_model_uuid, query, documents, top_k=None, extra_args=None, timeout=30.0)` | 带 `index`、`relevance_score` 的列表 | +| `get_tool_detail(tool_name)` | 工具说明与参数 schema,`dict` | +| `call_tool(tool_name, parameters)` | 工具结果,`dict`;无需 `session`、`query_id` | +| `retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` | 检索结果列表 | + +四个模型调用方法还支持 `remove_think=None`;非流式调用支持 `timeout=None`。调用超时受本次运行的剩余时间约束。资源列表可能为空,不能直接假定存在第一个模型或知识库。 + +```python +allowed = {tool.tool_name for tool in api.get_allowed_tools()} +if "event_get_actor" in allowed: + actor = await api.call_tool("event_get_actor", {}) +``` + +`event_*` 工具由 LangBot 固定事件中的目标,`platform_*` 工具允许按 schema 指定目标。使用实际授权列表中的名称与参数;无参数工具传 `{}`。 + +## 历史与事件 + +当前输入不包含完整历史。按需读取当前运行范围内的记录;不能通过传入其他会话 ID 扩大访问范围。 + +| 方法 | 返回值 | +| --- | --- | +| `get_prompt()` | 生效的提示词列表;能力标识为 `prompt_get` | +| `history_page(conversation_id=None, before_cursor=None, after_cursor=None, limit=50, direction="backward", include_attachments=False)` | `HistoryPage` | +| `history_search(query, filters=None, top_k=10)` | `HistorySearchResult` | +| `event_get(event_id)` | `AgentEventRecord` | +| `event_page(conversation_id=None, event_types=None, before_cursor=None, limit=50)` | `EventPage` | +| `steering_pull(mode="all", limit=None)` | `SteeringPullResult`;读取运行中排队的追加输入 | + +```python +if api.ctx.context.available_apis.history_page: + page = await api.history_page(limit=20) + for item in page.items: + print(item.model_dump()) + if page.has_more and page.prev_cursor: + older = await api.history_page(before_cursor=page.prev_cursor, limit=20) +``` + +分页结果是 SDK 对象,使用 `.items`、`.has_more`、`.prev_cursor`、`.next_cursor`,不是字典下标。读取旧记录使用 `before_cursor`;读取新记录使用 `after_cursor` 和 `direction="forward"`。历史搜索可能只提供文本匹配。`steering_pull` 的 `mode` 可为 `all`、`one` 或 `one-at-a-time`,用于支持追加输入的 Runner。 + +## 状态与存储 + +| 方法 | 返回值 | +| --- | --- | +| `state_get(scope, key)` | `{"value": ...}` | +| `state_set(scope, key, value)` | `{"success": ...}` | +| `state_delete(scope, key)` | `{"success": ...}` | +| `state_list(scope, prefix=None, limit=100)` | `{"keys": [...], "has_more": ...}` | + +状态需要 `available_apis.state` 和宿主允许的作用域:`conversation`(当前会话)、`actor`(当前用户)、`subject`(当前群组或对象)、`runner`(Runner 实例)。值必须可 JSON 序列化,建议使用 `external.` 等键前缀。状态由 LangBot 持久化,接口返回字典。 + +```python +if api.ctx.context.available_apis.state: + await api.state_set("conversation", "external.last_topic", "setup") + value = await api.state_get("conversation", "external.last_topic") +``` + +插件和工作空间存储使用 `set_plugin_storage`、`get_plugin_storage`、`get_plugin_storage_keys`、`delete_plugin_storage`,以及对应的 `*_workspace_storage*` 方法。参数与[通用 API](/zh/plugin/dev/apis/common)一致,值为 `bytes`,分别受 `api.ctx.resources.storage.plugin_storage` 和 `workspace_storage` 授权限制。 + +## 运行记录 + +以下接口按同名 `available_apis` 标识检查能力,只能操作当前授权范围内的运行。 + +| 方法 | 返回值 | +| --- | --- | +| `run_get(run_id=None)` | `AgentRun` | +| `run_list(conversation_id=None, statuses=None, before_cursor=None, limit=50)` | `RunPage` | +| `run_events_page(run_id=None, before_cursor=None, after_cursor=None, limit=50, direction="forward")` | `RunEventPage` | +| `run_cancel(run_id=None, reason=None)` | `AgentRun` | +| `run_append_result(result)` | `AgentRunEvent` | +| `run_finalize(run_id=None, status=None, reason=None)` | `AgentRun` | + +`run_id=None` 默认当前运行。普通 EventProcessor 处理函数返回后会自动结束,通常不需要手动追加或结束运行记录。AgentRunner 通过 `yield AgentRunResult` 上报结果,避免同时用 `run_append_result` 重复记录相同结果。 + +## 结构化交互(AgentRunner) + +需要用户确认或填写表单时,通过 `yield AgentRunResult.interaction_requested(ctx.run_id, request)` 请求交互。`request` 可以是 `InteractionRequest` 对象或字典,必填字段为 `interaction_id`、`title`、`fallback_text`;可选字段有 `kind`(`form`、`confirmation`、`choice`)、`fields`、`actions`、`expires_at`。 + +清单需声明 `spec.capabilities.interactions: true` 和 `spec.permissions.interactions: [request]`,并获得宿主授权。界面支持情况见 `ctx.delivery.interactions`;提交结果以 `interaction.submitted` 事件返回。这是 Runner 的结果事件,不是 `ctx.api` 方法,EventProcessor 的普通处理函数不直接提供此接口。 + +## 错误处理 + +```python +from langbot_plugin.api.proxies.agent_run import PermissionDeniedError +from langbot_plugin.api.entities.builtin.agent_runner.errors import AgentAPIException + +try: + result = await api.call_tool("event_get_actor", {}) +except PermissionDeniedError as exc: + await ctx.log(str(exc), level="warning") +except AgentAPIException as exc: + await ctx.log(f"{exc.error.code}: {exc.error.message}", level="error") +``` + +这是 EventProcessor 中的示例。`PermissionDeniedError` 表示资源、操作或能力未授权;`AgentAPIException.error` 包含 `code`、`message`、`retryable`、`details`。发送消息等动作超时后,结果可能不确定,不应仅依据 `retryable` 自动重发。 diff --git a/zh/plugin/dev/apis/common.mdx b/zh/plugin/dev/apis/common.mdx index 71dd51fa1..d555c4fb2 100644 --- a/zh/plugin/dev/apis/common.mdx +++ b/zh/plugin/dev/apis/common.mdx @@ -4,6 +4,8 @@ title: "插件通用 API" LangBot 为插件提供了一系列 API ,用于操作 LangBot 各个模块、控制消息上下文等。 +`Command`、`EventListener`、`Tool` 等普通组件通过 `self.plugin` 使用本页 API。`AgentRunner` 和 `EventProcessor` 请使用[运行上下文 API](/zh/plugin/dev/apis/agent-run),不使用 `self.plugin`。平台查询和专有动作见[平台 API](/zh/plugin/dev/apis/platform)。 + ## 请求 API 操作当前正在处理的用户请求(消息)。仅在`EventListener`和`Command`组件中可用。访问方式如下: @@ -105,12 +107,20 @@ knowledge_bases = await event_context.list_pipeline_knowledge_bases() 仅返回当前请求对应流水线在 `Local Agent` 配置中绑定的知识库。若当前流水线未配置知识库,或未使用 `Local Agent`,则返回空列表。 +### 开始新对话 + +`await event_context.create_new_conversation()` 清除当前流水线会话正在使用的对话,后续处理会重新建立对话。返回空字典,不会清除平台聊天记录。 + +### 检索当前流水线的知识库 + +`await event_context.retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` 返回检索结果列表。`kb_id` 必须来自 `list_pipeline_knowledge_bases()`;不能用这个请求接口访问流水线未配置的知识库。 + ## LangBot API -这些 API 可在插件任何组件中调用。访问方式如下: +这些 API 可在提供 `self.plugin` 的普通组件中调用。访问方式如下: - 插件根目录`main.py`中:`self` 对象内部方法,这些 API 均由插件类父类 `BasePlugin` 提供。 -- 插件任何组件类中:`self.plugin` 对象内部方法。 +- 普通组件类中:`self.plugin` 对象内部方法。 ### 获取插件配置信息 @@ -224,6 +234,7 @@ async def invoke_llm( messages: list[provider_message.Message], funcs: list[resource_tool.LLMTool] = [], extra_args: dict[str, Any] = {}, + timeout: float | None = None, ) -> provider_message.Message: """Invoke an LLM model""" @@ -236,6 +247,45 @@ llm_message = await self.plugin.invoke_llm( ) ``` + +### 模型流式输出与用量 + +| 方法 | 返回值 | +| --- | --- | +| `invoke_llm_with_usage(llm_model_uuid, messages, funcs=[], extra_args={}, timeout=None)` | `LLMInvokeResult` | +| `invoke_llm_stream(llm_model_uuid, messages, funcs=[], extra_args={})` | 异步迭代 `MessageChunk` | +| `invoke_llm_stream_events(llm_model_uuid, messages, funcs=[], extra_args={})` | 异步迭代 `LLMStreamEvent` | + + +参数与 `invoke_llm` 相同。`timeout` 单位为秒,非流式调用默认 120 秒。需要 token 用量时使用 `invoke_llm_with_usage`: + +```python +from langbot_plugin.api.entities.builtin.provider.message import Message + +result = await self.plugin.invoke_llm_with_usage( + llm_model_uuid=model_uuid, + messages=[Message(role="user", content="Hello!")], +) +print(result.message.content) +if result.usage is not None: + print(result.usage.total_tokens) +``` + +`usage` 可为 `None`,其中 `prompt_tokens`、`completion_tokens`、`total_tokens` 也可能缺失,不代表用量为零。供应商附加的缓存等用量字段会保留。 + +```python +async for event in self.plugin.invoke_llm_stream_events( + llm_model_uuid=model_uuid, + messages=[Message(role="user", content="Hello!")], +): + if event.chunk is not None: + print(event.chunk.content) + if event.usage is not None: + print(event.usage.model_dump()) +``` + +`LLMStreamEvent` 包含可选的 `chunk` 和 `usage`,末尾可能只有用量,没有文本。`invoke_llm_stream` 只返回文本或工具调用分块。`MessageChunk.content` 是当前片段,`all_content` 在提供时是累计文本;累计文本应用替换,增量文本才追加;`is_final` 为真时也可能仍有文本。模型流式调用不会直接发送平台消息,主动流式回复见[运行上下文 API](/zh/plugin/dev/apis/agent-run)。 + ### 列出可用解析器 列出宿主当前可用的 Parser 插件,可按 MIME 类型过滤。 @@ -428,7 +478,7 @@ file_bytes = await self.plugin.get_config_file("key") ## 知识库 API -这些 API 可在任意组件中通过 `self.plugin` 访问,用于列出和检索 LangBot 实例中的所有知识库,不受流水线配置限制。 +这些 API 可在提供 `self.plugin` 的普通组件中访问,用于列出和检索 LangBot 实例中的所有知识库,不受流水线配置限制。 ### 列出所有知识库 @@ -486,7 +536,7 @@ for entry in results: ``` -这些 API 不需要 `query_id`,可以在任何组件中使用(包括 Tool 组件),且可以访问所有知识库,不受当前流水线配置限制。 +这些 API 不需要 `query_id`,可以在普通组件中使用(包括 Tool 组件),且可以访问所有知识库,不受当前流水线配置限制。 ## RAG API @@ -676,3 +726,81 @@ async def get_knowledge_file_stream(self, storage_path: str) -> bytes: # 调用示例 file_bytes = await self.plugin.get_knowledge_file_stream(context.file_object.storage_path) ``` + + +## 其他资源 API + +以下方法同样通过 `self.plugin` 调用。 + +### 调用重排模型 + +```python +async def invoke_rerank( + self, rerank_model_uuid: str, query: str, documents: list[str], + top_k: int | None = None, extra_args: dict[str, Any] | None = None, + timeout: float = 60.0, +) -> list[dict[str, Any]]: + ... + +scores = await self.plugin.invoke_rerank( + rerank_model_uuid=rerank_model_uuid, + query="How do I configure a bot?", + documents=["Bot configuration guide", "Plugin publishing guide"], + top_k=1, +) +``` + +返回结果通常包含原文档索引 `index` 和相关性分数 `relevance_score`。 + +### 调用解析器 + +```python +async def invoke_parser( + self, plugin_author: str, plugin_name: str, storage_path: str, + mime_type: str, filename: str, metadata: dict[str, Any] | None = None, +) -> dict[str, Any]: + ... + +parsers = await self.plugin.list_parsers(mime_type="application/pdf") +if parsers: + parser = parsers[0] + parsed = await self.plugin.invoke_parser( + plugin_author=parser["plugin_author"], + plugin_name=parser["plugin_name"], + storage_path=storage_path, + mime_type="application/pdf", + filename="guide.pdf", + ) +``` + +`storage_path` 是宿主存储中的文件路径,例如知识库摄取上下文的 `context.file_object.storage_path`,不是插件本地路径。返回字典包含 `text`、`sections`、`metadata`。 + +### 列出向量记录 + +```python +async def vector_list( + self, collection_id: str, filters: dict[str, Any] | None = None, + limit: int = 20, offset: int = 0, +) -> dict[str, Any]: + ... + +page = await self.plugin.vector_list( + collection_id=collection_id, + filters={"file_id": "doc_001"}, + limit=20, + offset=0, +) +for item in page["items"]: + print(item["id"], item.get("metadata")) +``` + +返回 `items` 和 `total`。条目包含 `id`、`document`、`metadata`;`total` 是存储后端尽力提供的匹配数量。过滤条件使用上文的向量 API 格式。 + +### 列出插件和命令 + +```python +manifests = await self.plugin.list_plugins_manifest() +commands = await self.plugin.list_commands() +``` + +两个方法均不需要参数,分别返回宿主提供的插件清单列表和命令列表。 diff --git a/zh/plugin/dev/apis/messages.mdx b/zh/plugin/dev/apis/messages.mdx index a5a5cee67..7ca749934 100644 --- a/zh/plugin/dev/apis/messages.mdx +++ b/zh/plugin/dev/apis/messages.mdx @@ -58,35 +58,8 @@ msg_chain = MessageChain([ ## 其他实体 -除了上述实体之外,还有`好友 Friend`和`群 Group`等实体,这些实体的定义位于`langbot_plugin.api.entities.platform.entities`中。 +除了上述实体之外,还有`好友 Friend`和`群 Group`等实体,这些实体的定义位于`langbot_plugin.api.entities.builtin.platform.entities`中。 -## 访问消息平台底层 API +## 平台 API - - 4.x 暂不适用。 - - -为了抹平各个平台的差异,LangBot 在消息平台API之上,提供了一个抽象层,即上方所述的内容,以及`pkg/platform/sources`目录下的各个平台适配器。\ -但由于各个平台的差异较大,抽象层内的实体和 API 封装并不完整,如果您的插件需要访问特定平台的功能,可以按照以下说明访问底层 API。 - - - ```python - # 例如在插件中 - @handler(GroupMessageReceived) - async def _(self, ctx: EventContext): - # 取得适配器对象 - adapter = ctx.query.adapter - ``` - - -| 平台 | 适配器 | 访问方式 | 说明 | -| ---------- | --------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| OneBot v11 | aiocqhttp | adapter.bot | bot 对象对应 [aiocqhttp](https://github.com/nonebot/aiocqhttp) 的 CQHttp 对象,详细使用方式请参考 aiocqhttp 文档 | -| QQ 官方API | qqofficial | adapter.bot | bot 对象为 libs/qq_official_api 下的 SDK 对象,详细使用方式可以参考 libs/qq_official_api/api.py 中`QQOfficialClient`中的`send_group_text_msg`方法发起 HTTP 请求的方式,具体接口文档请查看 QQ 官方 API 文档 | -| 企业微信 | wecom | adapter.bot | 参考 libs/wecom_api/api.py 中`WecomClient`中的`send_image`方法发起 HTTP 请求的方式,具体接口文档请查看企业微信 API 文档 | -| 微信公众号 | officialaccount | - | 请参考微信公众号的 API 文档中的接口文档信息,相关的凭据信息可以在 adapter.bot 中取得 | -| 飞书 | lark | adapter.api_client | 飞书 SDK 的 API Client 对象,具体请参考[oapi-sdk-python](https://github.com/larksuite/oapi-sdk-python) | -| 钉钉 | dingtalk | - | 请参考钉钉 API 文档,相关的凭据信息可以在 adapter.bot 中取得 | -| Discord | discord | adapter.bot | bot 对象为 [Rapptz/discord.py](https://github.com/Rapptz/discord.py) 的 Discord 对象,详细使用方式请参考文档 | -| Telegram | telegram | adapter.bot | bot 对象为 [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) 的 Telegram 对象,详细使用方式请参考文档 | -| Slack | slack | adapter.bot | bot 对象 libs/slack_api/api.py 中的对象,详细使用方式请参考其代码以及 Slack SDK | \ No newline at end of file +查询用户、管理群组和调用平台专有接口,请使用[平台 API](/zh/plugin/dev/apis/platform)。EventProcessor 和 AgentRunner 通过[运行上下文 API](/zh/plugin/dev/apis/agent-run)调用已授权的平台工具。 diff --git a/zh/plugin/dev/apis/platform.mdx b/zh/plugin/dev/apis/platform.mdx new file mode 100644 index 000000000..07fa809e7 --- /dev/null +++ b/zh/plugin/dev/apis/platform.mdx @@ -0,0 +1,78 @@ +--- +title: "平台 API" +--- + +通过机器人 UUID 调用平台适配器的 API。发送消息链可继续使用 `self.plugin.send_message()`;查询群组、用户及其他平台能力使用下面的接口。 + +## 调用平台 API + +```python +async def call_platform_api( + self, + bot_uuid: str, + action: str, + params: dict[str, Any] | None = None, +) -> Any: + ... +``` + +在 Command、EventListener、Tool 等组件中通过 `self.plugin` 调用。返回值为可序列化的字典、列表、标量或 `None`,具体取决于动作;不是平台 SDK 对象。 + +```python +group = await self.plugin.call_platform_api( + bot_uuid=bot_uuid, + action="get_group_info", + params={"group_id": "123456"}, +) +``` + +`bot_uuid` 可从 `await self.plugin.get_bots()` 获取,流水线事件中也可使用 `await event_context.get_bot_uuid()`。平台 ID 的值应来自实际事件或平台配置。 + +### 常用动作 + +| `action` | `params` | +| --- | --- | +| `get_user_info` | `user_id` | +| `get_friend_list` | `{}` | +| `get_group_info` | `group_id` | +| `get_group_list` | `{}` | +| `get_group_member_list` | `group_id` | +| `get_group_member_info` | `group_id`, `user_id` | +| `get_message` / `delete_message` | `chat_type`, `chat_id`, `message_id` | +| `set_group_name` | `group_id`, `name` | +| `mute_member` | `group_id`, `user_id`, `duration` | +| `unmute_member` / `kick_member` | `group_id`, `user_id` | +| `leave_group` | `group_id` | +| `approve_friend_request` | `request_id`, `approve`, `remark` | +| `approve_group_invite` | `request_id`, `approve` | +| `get_file_url` | `file_id` | + + +每个平台支持的动作不同,也可能要求机器人管理员权限。`duration` 单位为秒,`0` 的含义依平台而异,不应作为通用的永久禁言值。查询结果可能依赖平台缓存。机器人未运行、动作不受支持或参数不正确时,调用会抛出异常。 + +## 平台专有 API + +将外层 `action` 设为 `call_platform_api`,在 `params` 中填写平台动作及其参数。例如,使用 OneBot Omni 读取机器人账号信息: + +```python +account = await self.plugin.call_platform_api( + bot_uuid=bot_uuid, + action="call_platform_api", + params={"action": "get_login_info", "params": {}}, +) +``` + +只能调用适配器已声明的动作,并非任意平台接口都能透传。插件通过运行时调用宿主,不直接访问 `ctx.query.adapter` 或 `adapter.bot`。 + +## 在事件处理器中调用 + +EventProcessor 和 AgentRunner 使用运行授权的工具,不提供通用的 `call_platform_api` 方法。例如,在当前事件支持查询发起者且已授权时: + +```python +tools = {tool.tool_name for tool in ctx.api.get_allowed_tools()} +if "event_get_actor" in tools: + actor = await ctx.api.call_tool("event_get_actor", {}) + await ctx.log(str(actor)) +``` + +访问方式、权限与回复示例见[运行上下文 API](/zh/plugin/dev/apis/agent-run)。事件调试中的平台工具使用 Mock;普通组件的 `self.plugin.call_platform_api()` 是实际调用。 diff --git a/zh/plugin/dev/components/event-processor.mdx b/zh/plugin/dev/components/event-processor.mdx index 9c8c51516..990fc241f 100644 --- a/zh/plugin/dev/components/event-processor.mdx +++ b/zh/plugin/dev/components/event-processor.mdx @@ -114,6 +114,20 @@ class Welcome(EventProcessor): 处理函数中,`ctx.event` 保存当前事件的数据,`ctx.config` 是当前处理器的配置。调用 `ctx.reply()` 回复消息,`ctx.log()` 记录日志。函数执行完毕,本次处理即结束。 +## 流式回复 + +使用 `ctx.reply_stream()` 更新同一条回复,`update(text)` 接收当前的完整文本: + +```python +async with ctx.reply_stream() as reply: + await reply.update("正在处理…") + await reply.update("处理完成。") +``` + +正常退出 `async with` 时完成回复。平台不支持流式、未开启流式回复或当前不是消息事件时,会在结束后发送一条完整消息;发生异常时不发送缓存中的未完成文本。调试模式仅模拟发送。 + +此接口沿用 `ctx.reply()` 的权限。AgentRunner 可通过 `self.get_run_api(ctx).reply_stream()` 使用同一接口,模型返回文本仍不会自动发送。 + ## 事件注册 通过 `@self.handler(事件类)` 注册处理函数,并将对应事件标识加入清单的 `spec.events`。例如,`MemberJoinedEvent` 对应 `group.member_joined`。 @@ -156,3 +170,5 @@ class Welcome(EventProcessor): - 查看 [EventProcessorDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/EventProcessorDemo) 中的更多示例。 - 如果要扩展流水线的预处理、模型调用完成等步骤,请使用[事件监听器](/zh/plugin/dev/components/event-listener)。 + +回复、模型、工具、历史与状态接口见[运行上下文 API](/zh/plugin/dev/apis/agent-run)。 From 5cbe5eee597ab099784af82dff38bc835fe07cb0 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Thu, 10 Sep 2026 18:04:42 +0800 Subject: [PATCH 03/22] docs(plugin): document unified Runner development and APIs --- docs.json | 6 +- en/plugin/dev/apis/agent-run.mdx | 25 +++--- en/plugin/dev/apis/common.mdx | 2 +- en/plugin/dev/apis/messages.mdx | 2 +- en/plugin/dev/apis/platform.mdx | 2 +- en/plugin/dev/components/add.mdx | 6 +- en/plugin/dev/components/event-listener.mdx | 2 +- .../{event-processor.mdx => runner.mdx} | 59 ++++++++----- en/plugin/dev/tutor.mdx | 2 +- ja/plugin/dev/apis/agent-run.mdx | 25 +++--- ja/plugin/dev/apis/common.mdx | 2 +- ja/plugin/dev/apis/messages.mdx | 2 +- ja/plugin/dev/apis/platform.mdx | 2 +- ja/plugin/dev/components/add.mdx | 6 +- ja/plugin/dev/components/event-listener.mdx | 2 +- .../{event-processor.mdx => runner.mdx} | 59 ++++++++----- ja/plugin/dev/tutor.mdx | 2 +- scripts/README-event-processor-validation.md | 84 ------------------- scripts/README-runner-validation.md | 14 ++++ scripts/verify-plugin-api-examples.py | 12 +-- ...-tutorial.py => verify-runner-tutorial.py} | 21 +++-- sitemap-alternates.xml | 42 +++++----- zh/plugin/dev/apis/agent-run.mdx | 25 +++--- zh/plugin/dev/apis/common.mdx | 2 +- zh/plugin/dev/apis/messages.mdx | 2 +- zh/plugin/dev/apis/platform.mdx | 2 +- zh/plugin/dev/components/add.mdx | 6 +- zh/plugin/dev/components/event-listener.mdx | 2 +- .../{event-processor.mdx => runner.mdx} | 59 ++++++++----- zh/plugin/dev/tutor.mdx | 2 +- 30 files changed, 233 insertions(+), 246 deletions(-) rename en/plugin/dev/components/{event-processor.mdx => runner.mdx} (71%) rename ja/plugin/dev/components/{event-processor.mdx => runner.mdx} (72%) delete mode 100644 scripts/README-event-processor-validation.md create mode 100644 scripts/README-runner-validation.md rename scripts/{verify-event-processor-tutorial.py => verify-runner-tutorial.py} (86%) rename zh/plugin/dev/components/{event-processor.mdx => runner.mdx} (72%) diff --git a/docs.json b/docs.json index 4ef85f881..96d117643 100644 --- a/docs.json +++ b/docs.json @@ -677,7 +677,7 @@ "group": "Component Development", "pages": [ "en/plugin/dev/components/add", - "en/plugin/dev/components/event-processor", + "en/plugin/dev/components/runner", "en/plugin/dev/components/event-listener", "en/plugin/dev/components/command", "en/plugin/dev/components/tool", @@ -983,7 +983,7 @@ "group": "组件开发", "pages": [ "zh/plugin/dev/components/add", - "zh/plugin/dev/components/event-processor", + "zh/plugin/dev/components/runner", "zh/plugin/dev/components/event-listener", "zh/plugin/dev/components/command", "zh/plugin/dev/components/tool", @@ -1284,7 +1284,7 @@ "group": "コンポーネント開発", "pages": [ "ja/plugin/dev/components/add", - "ja/plugin/dev/components/event-processor", + "ja/plugin/dev/components/runner", "ja/plugin/dev/components/event-listener", "ja/plugin/dev/components/command", "ja/plugin/dev/components/tool", diff --git a/en/plugin/dev/apis/agent-run.mdx b/en/plugin/dev/apis/agent-run.mdx index ea59012e1..a9dd55425 100644 --- a/en/plugin/dev/apis/agent-run.mdx +++ b/en/plugin/dev/apis/agent-run.mdx @@ -2,16 +2,13 @@ title: "Run Context APIs" --- -AgentRunner and EventProcessor use run-scoped APIs to access authorized models, tools, knowledge bases, and context. Neither component exposes `self.plugin`. +Runner provides `ctx.api` bound to the current run ID, context, and resource grants. Custom `run(ctx)` implementations and registered handlers share `RunnerContext`. `self.plugin` still exposes [Common APIs](/en/plugin/dev/apis/common), but does not automatically represent the current run. ## Access ```python -# Inside an EventProcessor handler +# Inside Runner.run(ctx) or a registered handler api = ctx.api - -# Inside AgentRunner.run(ctx) -api = self.get_run_api(ctx) ``` In the examples below, `api` refers to the current run API. Use `await` for the methods below, except synchronous `get_allowed_*` methods and streaming context/iterator APIs. Do not retain or reuse it across runs. @@ -52,7 +49,7 @@ Check context capabilities through `api.ctx.context.available_apis`. Do not call ## Replies and logs -| EventProcessor method | Purpose | +| Runner method | Purpose | | --- | --- | | `await ctx.reply(text)` | Send text to the current event conversation; return the action result. | | `await ctx.log(text, level="info")` | Record a log at `debug`, `info`, `warning`, or `error` level. | @@ -69,7 +66,7 @@ async with api.reply_stream() as reply: result = reply.result ``` -EventProcessor can use `ctx.reply_stream()` to also record the action start and result. +Runner can use `ctx.reply_stream()` to also record the action start and result. `update(text)` takes the **complete text so far**, not a delta, and returns the action result. Normal exit completes the reply. Message events with platform streaming support enabled update the platform message; other replyable events buffer text and send once on successful exit. Errors or cancellation discard buffered partial text; already displayed content is not guaranteed to be retracted. Debug mode simulates platform delivery. @@ -157,19 +154,19 @@ The following methods require the matching `available_apis` flag and only access | `run_append_result(result)` | `AgentRunEvent` | | `run_finalize(run_id=None, status=None, reason=None)` | `AgentRun` | -`run_id=None` selects the current run. EventProcessor handlers finish automatically when they return and usually do not need to append or finalize records manually. AgentRunner reports results with `yield AgentRunResult`; do not also append the same result through `run_append_result`. +`run_id=None` selects the current run. Registered handlers finish automatically when they return. Custom `run(ctx)` implementations can yield `RunnerResult`; do not append the same result again through `run_append_result`. -## Structured interactions (AgentRunner) +## Structured interactions (Runner) -To request confirmation or form input, yield `AgentRunResult.interaction_requested(ctx.run_id, request)`. `request` accepts an `InteractionRequest` object or dictionary. Required fields are `interaction_id`, `title`, and `fallback_text`; optional fields include `kind` (`form`, `confirmation`, `choice`), `fields`, `actions`, and `expires_at`. +To request confirmation or form input, yield `RunnerResult.interaction_requested(ctx.run_id, request)`. `request` accepts an `InteractionRequest` object or dictionary. Required fields are `interaction_id`, `title`, and `fallback_text`; optional fields include `kind` (`form`, `confirmation`, `choice`), `fields`, `actions`, and `expires_at`. -This requires `spec.capabilities.interactions: true`, `spec.permissions.interactions: [request]`, and Host authorization. Inspect `ctx.delivery.interactions` for surface support; submissions return as `interaction.submitted` events. This is a Runner result event, not a method on `ctx.api`, and is not directly exposed by ordinary EventProcessor handlers. +This requires `spec.capabilities.interactions: true`, `spec.permissions.interactions: [request]`, and Host authorization. Inspect `ctx.delivery.interactions` for surface support; submissions return as `interaction.submitted` events. This is a Runner result event, not a method on `ctx.api`, and is yielded from a custom run(ctx) implementation. ## Error handling ```python -from langbot_plugin.api.proxies.agent_run import PermissionDeniedError -from langbot_plugin.api.entities.builtin.agent_runner.errors import AgentAPIException +from langbot_plugin.api.proxies.runner import PermissionDeniedError +from langbot_plugin.api.entities.builtin.runner.errors import AgentAPIException try: result = await api.call_tool("event_get_actor", {}) @@ -179,4 +176,4 @@ except AgentAPIException as exc: await ctx.log(f"{exc.error.code}: {exc.error.message}", level="error") ``` -This example runs inside an EventProcessor. `PermissionDeniedError` means a resource, operation, or capability is not granted. `AgentAPIException.error` contains `code`, `message`, `retryable`, and `details`. A timed-out send may already have succeeded; do not resend solely because `retryable` is true. +This example runs inside a Runner. `PermissionDeniedError` means a resource, operation, or capability is not granted. `AgentAPIException.error` contains `code`, `message`, `retryable`, and `details`. A timed-out send may already have succeeded; do not resend solely because `retryable` is true. diff --git a/en/plugin/dev/apis/common.mdx b/en/plugin/dev/apis/common.mdx index 9df6a2078..f60294710 100644 --- a/en/plugin/dev/apis/common.mdx +++ b/en/plugin/dev/apis/common.mdx @@ -4,7 +4,7 @@ title: "Plugin Common APIs" LangBot provides a series of APIs for plugins to operate various LangBot modules and control message contexts. -Regular components such as `Command`, `EventListener`, and `Tool` use these APIs through `self.plugin`. `AgentRunner` and `EventProcessor` use [Run Context APIs](/en/plugin/dev/apis/agent-run), without `self.plugin`. See [Platform APIs](/en/plugin/dev/apis/platform) for platform queries and specific actions. +All components can access these LangBot APIs through `self.plugin`. Runner also provides `ctx.api`, bound to the current invocation and its resource grants; see [Run Context APIs](/en/plugin/dev/apis/agent-run). Operations with an explicit bot and target are covered in [Platform APIs](/en/plugin/dev/apis/platform). ## Request API diff --git a/en/plugin/dev/apis/messages.mdx b/en/plugin/dev/apis/messages.mdx index eea097b8d..2bc79edea 100644 --- a/en/plugin/dev/apis/messages.mdx +++ b/en/plugin/dev/apis/messages.mdx @@ -62,4 +62,4 @@ In addition to the above entities, there are also entities like `Friend` and `Gr ## Platform APIs -Use [Platform APIs](/en/plugin/dev/apis/platform) to query users, manage groups, and call platform-specific actions. EventProcessor and AgentRunner call authorized platform tools through [Run Context APIs](/en/plugin/dev/apis/agent-run). +Use [Platform APIs](/en/plugin/dev/apis/platform) to query users, manage groups, and call platform-specific actions. Runner call authorized platform tools through [Run Context APIs](/en/plugin/dev/apis/agent-run). diff --git a/en/plugin/dev/apis/platform.mdx b/en/plugin/dev/apis/platform.mdx index d9edab3ec..c9fdc73c6 100644 --- a/en/plugin/dev/apis/platform.mdx +++ b/en/plugin/dev/apis/platform.mdx @@ -66,7 +66,7 @@ Only actions declared by the adapter are allowed; this is not unrestricted pass- ## Calling from an event processor -EventProcessor and AgentRunner use run-authorized tools and do not expose the generic `call_platform_api` method. For example, when the event supports actor lookup and grants it: +Runner can also call these platform APIs through `self.plugin`, specifying the bot and target explicitly. To use the current event grants and record actions in its run trace, use `ctx.api` or `ctx.reply()`: ```python tools = {tool.tool_name for tool in ctx.api.get_allowed_tools()} diff --git a/en/plugin/dev/components/add.mdx b/en/plugin/dev/components/add.mdx index 2e7665b69..869d1b5f4 100644 --- a/en/plugin/dev/components/add.mdx +++ b/en/plugin/dev/components/add.mdx @@ -4,8 +4,8 @@ title: "Adding Components" Plugins consist of one or more components that provide different functionalities to LangBot. Currently supported component types include: -- Plugin processor (EventProcessor): handles platform events in plugin code after an instance is created and bound. See the [tutorial](/en/plugin/dev/components/event-processor). -- Agent runner (AgentRunner): provides execution logic selected in an Agent configuration; it is distinct from a standalone plugin processor. +- Plugin processor (Runner): handles platform events in plugin code after an instance is created and bound. See the [tutorial](/en/plugin/dev/components/runner). +- Agent runner (Runner): provides execution logic selected in an Agent configuration; it is distinct from a standalone plugin processor. - Event Listeners (EventListener): Listen to events during pipeline execution and modify context or pipelines. - Commands (Command): Triggered by user command messages starting with `!` (or other configured prefixes). - Tools (Tool): Called by LLMs during execution of LangBot's built-in Local Agent. @@ -72,7 +72,7 @@ To remove components, you can delete the corresponding information manually. For detailed usage of each component, please refer to: -- [EventProcessor](./event-processor) +- [Runner](./runner) - [EventListener](./event-listener) - [Command](./command) - [Tool](./tool) diff --git a/en/plugin/dev/components/event-listener.mdx b/en/plugin/dev/components/event-listener.mdx index 3050c078e..47d85830a 100644 --- a/en/plugin/dev/components/event-listener.mdx +++ b/en/plugin/dev/components/event-listener.mdx @@ -3,7 +3,7 @@ title: "Component: Event Listener" --- -This component extends Pipeline processing steps. To handle Bot events such as incoming messages and new group members, see [Event Processor](/en/plugin/dev/components/event-processor). +This component extends Pipeline processing steps. To handle Bot events such as incoming messages and new group members, see [Event Processor](/en/plugin/dev/components/runner). LangBot's pipeline generates events during runtime for plugins to hook into and process. Each plugin can only have one event listener (components/event_listener/default.yaml), but can register any number of events within it. diff --git a/en/plugin/dev/components/event-processor.mdx b/en/plugin/dev/components/runner.mdx similarity index 71% rename from en/plugin/dev/components/event-processor.mdx rename to en/plugin/dev/components/runner.mdx index 135187dcf..df269b769 100644 --- a/en/plugin/dev/components/event-processor.mdx +++ b/en/plugin/dev/components/runner.mdx @@ -1,28 +1,45 @@ --- -title: "Component: Event Processor" +title: "Component: Runner" --- -The EventProcessor component handles Bot events such as incoming messages and new group members. After installing the plugin, users create a **Plugin processor** in LangBot, select the component, and bind Bot events to it. +Runner implements Agent execution or typed event handlers for a **Plugin processor**. Both styles receive `RunnerContext`. After installation, users select and configure the component in LangBot. For environment setup, see the [plugin development tutorial](/en/plugin/dev/tutor). -## Adding an Event Processor Component +## Adding a Runner Component -A plugin can contain several event processors. Run `lbp comp EventProcessor` in the plugin directory. Enter `welcome` as the name and `Welcome new members` as the description. +A plugin can contain several event processors. Run `lbp comp Runner` in the plugin directory. Enter `welcome` as the name and `Welcome new members` as the description. ```bash -lbp comp EventProcessor +lbp comp Runner ``` -The generated `components/event_processor/welcome.yaml` defines the component, and `welcome.py` contains its implementation. +The generated `components/runner/welcome.yaml` defines the component, and `welcome.py` contains its implementation. -## Manifest File: Event Processor +## Custom execution + +To integrate a model or implement an execution loop, declare `spec.usages: [agent]` and yield results from `run(ctx)`: + +```python +from langbot_plugin.api.definition.components.runner import Runner, RunnerContext, RunnerResult +from langbot_plugin.api.entities.builtin.provider.message import Message + + +class Echo(Runner): + async def run(self, ctx: RunnerContext): + yield RunnerResult.run_completed( + ctx.run_id, + message=Message(role="assistant", content=ctx.input.to_text()), + ) +``` + +## Manifest File: Runner Replace `welcome.yaml` with: ```yaml apiVersion: langbot/v1 -kind: EventProcessor +kind: Runner metadata: name: welcome label: @@ -34,6 +51,7 @@ metadata: zh_Hans: 欢迎新成员,并响应 /hello 消息。 ja_JP: 新しいメンバーを歓迎し、/hello に返信します。 spec: + usages: [event] events: - group.member_joined - message.received @@ -65,14 +83,17 @@ execution: `spec.events` lists supported events, and `spec.config` defines settings users can edit on the processor detail page. This example handles new members and incoming messages, with a greeting and a reply switch. Sending replies requires the `tool_calling` and `permissions.tools` settings shown above. + +`spec.usages` controls where the component can be selected: `agent` for Agents and Pipelines, `event` for Plugin processors. Both can be declared as `[agent, event]`. Event usage requires `spec.events`. The example uses typed handlers; a custom `run(ctx)` can also call `await super().run(ctx)` to dispatch them. + ## Plugin Implementation Register handlers in the `Welcome` class's `initialize` method. Replace `welcome.py` with: ```python -from langbot_plugin.api.definition.components.event_processor import ( - EventProcessor, - EventProcessorContext, +from langbot_plugin.api.definition.components.runner import ( + Runner, + RunnerContext, ) from langbot_plugin.api.entities.builtin.platform.events import ( MemberJoinedEvent, @@ -81,13 +102,13 @@ from langbot_plugin.api.entities.builtin.platform.events import ( from langbot_plugin.api.entities.builtin.platform.message import Plain -class Welcome(EventProcessor): +class Welcome(Runner): async def initialize(self): await super().initialize() @self.handler(MemberJoinedEvent) - async def on_join(ctx: EventProcessorContext): - name = ctx.event.member.nickname or str(ctx.event.member.id) + async def on_join(ctx: RunnerContext): + name = ctx.platform_event.member.nickname or str(ctx.platform_event.member.id) await ctx.log(f"Member joined: {name}") if not ctx.config.get("reply_enabled", True): await ctx.log("Replies are disabled for this processor") @@ -97,9 +118,9 @@ class Welcome(EventProcessor): await ctx.log("Welcome action completed") @self.handler(MessageReceivedEvent) - async def on_message(ctx: EventProcessorContext): + async def on_message(ctx: RunnerContext): text = "".join( - item.text for item in ctx.event.message_chain if isinstance(item, Plain) + item.text for item in ctx.platform_event.message_chain if isinstance(item, Plain) ).strip() if text != "/hello": await ctx.log("Message ignored: expected /hello") @@ -112,7 +133,7 @@ class Welcome(EventProcessor): `on_join` welcomes new members, and `on_message` replies to `/hello`. With **Send replies** turned off, they only write logs. -`ctx.event` contains the event data, and `ctx.config` contains this processor's settings. Use `ctx.reply()` to reply and `ctx.log()` to write logs. Processing finishes when the handlers return. +`ctx.platform_event` contains the event data, and `ctx.config` contains this processor's settings. Use `ctx.reply()` to reply and `ctx.log()` to write logs. Processing finishes when the handlers return. ## Streaming Replies @@ -126,7 +147,7 @@ async with ctx.reply_stream() as reply: The reply finishes when the `async with` block exits normally. If streaming is unsupported, disabled, or the event is not a message, the Host sends one complete message at the end. Exceptions discard buffered partial text. Debug runs simulate delivery. -This API uses the same permission as `ctx.reply()`. AgentRunner components can use `self.get_run_api(ctx).reply_stream()`; model output text is still not sent automatically. +This API uses the same permission as `ctx.reply()`. Runner components can use `self.get_run_api(ctx).reply_stream()`; model output text is still not sent automatically. ## Registering Events @@ -168,7 +189,7 @@ After installing the plugin, bind the Bot's **Member joined** and **Message rece ## Next Steps -- Explore more examples in [EventProcessorDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/EventProcessorDemo). +- Explore more examples in [RunnerDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/RunnerDemo). - To extend Pipeline steps such as preprocessing or completion of a model call, use [Event Listener](/en/plugin/dev/components/event-listener). See [Run Context APIs](/en/plugin/dev/apis/agent-run) for replies, models, tools, history, and state. diff --git a/en/plugin/dev/tutor.mdx b/en/plugin/dev/tutor.mdx index 7ab4b43fc..9b8e26926 100644 --- a/en/plugin/dev/tutor.mdx +++ b/en/plugin/dev/tutor.mdx @@ -3,7 +3,7 @@ title: "Plugin Development Tutorial" --- -To build a plugin for incoming messages, new group members, and other Bot events, complete the basic setup here and continue with the [Event Processor tutorial](/en/plugin/dev/components/event-processor). +To build a plugin for incoming messages, new group members, and other Bot events, complete the basic setup here and continue with the [Event Processor tutorial](/en/plugin/dev/components/runner). ## Architecture Foundation diff --git a/ja/plugin/dev/apis/agent-run.mdx b/ja/plugin/dev/apis/agent-run.mdx index 6738a54e6..480c6ddfa 100644 --- a/ja/plugin/dev/apis/agent-run.mdx +++ b/ja/plugin/dev/apis/agent-run.mdx @@ -2,16 +2,13 @@ title: "実行コンテキスト API" --- -AgentRunner と EventProcessor は、現在の実行に許可されたモデル、ツール、ナレッジベース、コンテキストへアクセスする API を使用します。両コンポーネントでは `self.plugin` を使用できません。 +Runner の `ctx.api` は現在の実行 ID、コンテキスト、リソースの許可範囲に結び付いています。独自の `run(ctx)` と登録した処理関数は同じ `RunnerContext` を使用します。`self.plugin` から[共通 API](/ja/plugin/dev/apis/common)も利用できますが、現在の実行を自動的に表すものではありません。 ## アクセス方法 ```python -# Inside an EventProcessor handler +# Inside Runner.run(ctx) or a registered handler api = ctx.api - -# Inside AgentRunner.run(ctx) -api = self.get_run_api(ctx) ``` 以下の `api` は現在の実行の API です。 同期の `get_allowed_*` とストリーミングのコンテキスト・イテレーター以外は、`await` で呼び出します。別の実行へ持ち越して再利用しないでください。 @@ -52,7 +49,7 @@ knowledge_bases = api.get_allowed_knowledge_bases() # Each item has kb_id ## 返信とログ -| EventProcessor メソッド | 用途 | +| Runner メソッド | 用途 | | --- | --- | | `await ctx.reply(text)` | 現在のイベントの会話へテキストを送信し、操作結果を返します。 | | `await ctx.log(text, level="info")` | `debug`、`info`、`warning`、`error` のログを記録します。 | @@ -69,7 +66,7 @@ async with api.reply_stream() as reply: result = reply.result ``` -EventProcessor では `ctx.reply_stream()` を使うと、操作の開始と結果も記録されます。 +Runner では `ctx.reply_stream()` を使うと、操作の開始と結果も記録されます。 `update(text)` には差分ではなく**その時点の全文**を渡します。戻り値は操作結果です。正常終了時に返信を完了します。プラットフォームでストリーミング返信が有効なメッセージイベントでは表示を更新し、それ以外の返信可能なイベントでは正常終了時に全文を一度送信します。例外やキャンセルでは未送信のテキストを破棄しますが、表示済みの内容の撤回は保証されません。デバッグ時の送信は Mock です。 @@ -157,19 +154,19 @@ if api.ctx.context.available_apis.state: | `run_append_result(result)` | `AgentRunEvent` | | `run_finalize(run_id=None, status=None, reason=None)` | `AgentRun` | -`run_id=None` は現在の実行を指します。EventProcessor はハンドラーの完了時に自動終了するため、通常は記録の手動追加・終了は不要です。AgentRunner は `yield AgentRunResult` で結果を報告します。同じ結果を `run_append_result` で重複登録しないでください。 +`run_id=None` は現在の実行を指します。登録した処理関数は戻ると自動終了します。独自の `run(ctx)` は `yield RunnerResult` で結果を報告できます。同じ結果を `run_append_result` で重複登録しないでください。 -## 構造化インタラクション(AgentRunner) +## 構造化インタラクション(Runner) -確認やフォーム入力には `yield AgentRunResult.interaction_requested(ctx.run_id, request)` を使用します。`request` は `InteractionRequest` または辞書で、必須フィールドは `interaction_id`、`title`、`fallback_text` です。`kind`(`form`、`confirmation`、`choice`)、`fields`、`actions`、`expires_at` は任意です。 +確認やフォーム入力には `yield RunnerResult.interaction_requested(ctx.run_id, request)` を使用します。`request` は `InteractionRequest` または辞書で、必須フィールドは `interaction_id`、`title`、`fallback_text` です。`kind`(`form`、`confirmation`、`choice`)、`fields`、`actions`、`expires_at` は任意です。 -`spec.capabilities.interactions: true` と `spec.permissions.interactions: [request]` の宣言、およびホストの許可が必要です。画面の対応状況は `ctx.delivery.interactions` で確認し、送信結果は `interaction.submitted` イベントとして返ります。これは Runner の結果イベントであり、`ctx.api` のメソッドではありません。通常の EventProcessor ハンドラーには直接公開されていません。 +`spec.capabilities.interactions: true` と `spec.permissions.interactions: [request]` の宣言、およびホストの許可が必要です。画面の対応状況は `ctx.delivery.interactions` で確認し、送信結果は `interaction.submitted` イベントとして返ります。これは Runner の結果イベントであり、`ctx.api` のメソッドではありません。独自の run(ctx) から yield します。 ## エラー処理 ```python -from langbot_plugin.api.proxies.agent_run import PermissionDeniedError -from langbot_plugin.api.entities.builtin.agent_runner.errors import AgentAPIException +from langbot_plugin.api.proxies.runner import PermissionDeniedError +from langbot_plugin.api.entities.builtin.runner.errors import AgentAPIException try: result = await api.call_tool("event_get_actor", {}) @@ -179,4 +176,4 @@ except AgentAPIException as exc: await ctx.log(f"{exc.error.code}: {exc.error.message}", level="error") ``` -EventProcessor 内の例です。`PermissionDeniedError` はリソース・操作・機能の権限不足を示します。`AgentAPIException.error` は `code`、`message`、`retryable`、`details` を持ちます。送信のタイムアウト時は成功済みの場合もあるため、`retryable` だけで再送しないでください。 +Runner 内の例です。`PermissionDeniedError` はリソース・操作・機能の権限不足を示します。`AgentAPIException.error` は `code`、`message`、`retryable`、`details` を持ちます。送信のタイムアウト時は成功済みの場合もあるため、`retryable` だけで再送しないでください。 diff --git a/ja/plugin/dev/apis/common.mdx b/ja/plugin/dev/apis/common.mdx index d1e97ce22..2d24166e7 100644 --- a/ja/plugin/dev/apis/common.mdx +++ b/ja/plugin/dev/apis/common.mdx @@ -4,7 +4,7 @@ title: "プラグイン共通API" LangBotは、プラグインがさまざまなLangBotモジュールを操作し、メッセージコンテキストを制御するための一連のAPIを提供します。 -`Command`、`EventListener`、`Tool` などの通常のコンポーネントは `self.plugin` を使用します。`AgentRunner` と `EventProcessor` は `self.plugin` ではなく[実行コンテキスト API](/ja/plugin/dev/apis/agent-run)を使用してください。プラットフォームの操作は[プラットフォーム API](/ja/plugin/dev/apis/platform)を参照してください。 +すべてのコンポーネントは `self.plugin` から本ページの LangBot API を利用できます。Runner の `ctx.api` は現在の実行とリソースの許可範囲に結び付いています。[実行コンテキスト API](/ja/plugin/dev/apis/agent-run)を参照してください。Bot と宛先を指定する操作は[プラットフォーム API](/ja/plugin/dev/apis/platform)に記載しています。 ## リクエストAPI diff --git a/ja/plugin/dev/apis/messages.mdx b/ja/plugin/dev/apis/messages.mdx index f390db8b3..eff0d072d 100644 --- a/ja/plugin/dev/apis/messages.mdx +++ b/ja/plugin/dev/apis/messages.mdx @@ -62,4 +62,4 @@ msg_chain = MessageChain([ ## プラットフォーム API -ユーザー取得、グループ管理、固有の操作には[プラットフォーム API](/ja/plugin/dev/apis/platform)を使用してください。EventProcessor と AgentRunner は[実行コンテキスト API](/ja/plugin/dev/apis/agent-run)から許可されたツールを呼び出します。 +ユーザー取得、グループ管理、固有の操作には[プラットフォーム API](/ja/plugin/dev/apis/platform)を使用してください。Runner は[実行コンテキスト API](/ja/plugin/dev/apis/agent-run)から許可されたツールを呼び出します。 diff --git a/ja/plugin/dev/apis/platform.mdx b/ja/plugin/dev/apis/platform.mdx index 59e8f4d58..7b797d541 100644 --- a/ja/plugin/dev/apis/platform.mdx +++ b/ja/plugin/dev/apis/platform.mdx @@ -66,7 +66,7 @@ account = await self.plugin.call_platform_api( ## イベントプロセッサーからの呼び出し -EventProcessor と AgentRunner は実行に許可されたツールを使用し、汎用の `call_platform_api` は公開しません。現在のイベントで発起者の取得が許可されている場合の例です: +Runner も `self.plugin` から Bot と宛先を指定して本ページの API を利用できます。現在のイベントの許可範囲で操作し、実行記録に残すには `ctx.api` または `ctx.reply()` を使用します: ```python tools = {tool.tool_name for tool in ctx.api.get_allowed_tools()} diff --git a/ja/plugin/dev/components/add.mdx b/ja/plugin/dev/components/add.mdx index 65610aca9..8ca1425c1 100644 --- a/ja/plugin/dev/components/add.mdx +++ b/ja/plugin/dev/components/add.mdx @@ -4,8 +4,8 @@ title: "コンポーネントの追加" プラグインは、LangBotにさまざまな機能を提供する1つ以上のコンポーネントで構成されています。現在サポートされているコンポーネントタイプには次のものがあります: -- プラグインプロセッサー(EventProcessor):インスタンスを作成してイベントに紐付け、プラグインのコードで処理します。[チュートリアル](/ja/plugin/dev/components/event-processor)を参照してください。 -- Agent ランナー(AgentRunner):Agent の設定で選択する実行コンポーネントです。独立したプラグインプロセッサーとは異なります。 +- プラグインプロセッサー(Runner):インスタンスを作成してイベントに紐付け、プラグインのコードで処理します。[チュートリアル](/ja/plugin/dev/components/runner)を参照してください。 +- Agent ランナー(Runner):Agent の設定で選択する実行コンポーネントです。独立したプラグインプロセッサーとは異なります。 - イベントリスナー(EventListener): パイプライン実行中のイベントをリッスンし、コンテキストやパイプラインを変更します。 - コマンド(Command): `!`(または他の設定されたプレフィックス)で始まるユーザーコマンドメッセージによってトリガーされます。 - ツール(Tool): LangBotの組み込みLocal Agentの実行中にLLMによって呼び出されます。 @@ -72,7 +72,7 @@ components: 各コンポーネントの詳細な使用方法については、次を参照してください: -- [EventProcessor](./event-processor) +- [Runner](./runner) - [EventListener](./event-listener) - [Command](./command) - [Tool](./tool) diff --git a/ja/plugin/dev/components/event-listener.mdx b/ja/plugin/dev/components/event-listener.mdx index 4c2840606..2e7fdd561 100644 --- a/ja/plugin/dev/components/event-listener.mdx +++ b/ja/plugin/dev/components/event-listener.mdx @@ -3,7 +3,7 @@ title: "コンポーネント: イベントリスナー" --- -このコンポーネントはパイプラインの処理を拡張します。メッセージ受信やメンバー参加などの Bot イベントを処理する場合は、[イベントプロセッサー](/ja/plugin/dev/components/event-processor)を参照してください。 +このコンポーネントはパイプラインの処理を拡張します。メッセージ受信やメンバー参加などの Bot イベントを処理する場合は、[イベントプロセッサー](/ja/plugin/dev/components/runner)を参照してください。 LangBotのパイプラインは実行時にイベントを生成し、プラグインがフックして処理できるようにします。各プラグインは1つのイベントリスナー(components/event_listener/default.yaml)のみを持つことができますが、その中で任意の数のイベントを登録できます。 diff --git a/ja/plugin/dev/components/event-processor.mdx b/ja/plugin/dev/components/runner.mdx similarity index 72% rename from ja/plugin/dev/components/event-processor.mdx rename to ja/plugin/dev/components/runner.mdx index 3abbd3c26..b744566e5 100644 --- a/ja/plugin/dev/components/event-processor.mdx +++ b/ja/plugin/dev/components/runner.mdx @@ -1,28 +1,45 @@ --- -title: "コンポーネント: イベントプロセッサー" +title: "コンポーネント: Runner" --- -イベントプロセッサー(EventProcessor)は、メッセージ受信やメンバー参加などの Bot イベントを処理するコンポーネントです。インストール後、ユーザーは LangBot で「プラグインプロセッサー」を作成してコンポーネントを選び、Bot のイベントを紐付けます。 +Runner は Agent の実行ロジック、または「プラグインプロセッサー」用のイベント処理関数を実装するコンポーネントです。どちらも `RunnerContext` を受け取り、インストール後にユーザーがコンポーネントを選択・設定します。 環境の準備は[プラグイン開発チュートリアル](/ja/plugin/dev/tutor)を参照してください。 -## イベントプロセッサーコンポーネントの追加 +## Runner コンポーネントの追加 -1 つのプラグインに複数のイベントプロセッサーを追加できます。プラグインのディレクトリで `lbp comp EventProcessor` を実行し、名前に `welcome`、説明に `Welcome new members` を入力します。 +1 つのプラグインに複数のイベントプロセッサーを追加できます。プラグインのディレクトリで `lbp comp Runner` を実行し、名前に `welcome`、説明に `Welcome new members` を入力します。 ```bash -lbp comp EventProcessor +lbp comp Runner ``` -生成される `components/event_processor/welcome.yaml` はコンポーネントを定義するファイルで、`welcome.py` に処理を記述します。 +生成される `components/runner/welcome.yaml` はコンポーネントを定義するファイルで、`welcome.py` に処理を記述します。 -## マニフェストファイル: イベントプロセッサー +## 実行ロジックの実装 + +モデルの接続や独自の実行ループには `spec.usages: [agent]` を宣言し、`run(ctx)` から結果を返します: + +```python +from langbot_plugin.api.definition.components.runner import Runner, RunnerContext, RunnerResult +from langbot_plugin.api.entities.builtin.provider.message import Message + + +class Echo(Runner): + async def run(self, ctx: RunnerContext): + yield RunnerResult.run_completed( + ctx.run_id, + message=Message(role="assistant", content=ctx.input.to_text()), + ) +``` + +## マニフェストファイル: Runner `welcome.yaml` を次の内容に変更します。 ```yaml apiVersion: langbot/v1 -kind: EventProcessor +kind: Runner metadata: name: welcome label: @@ -34,6 +51,7 @@ metadata: zh_Hans: 欢迎新成员,并响应 /hello 消息。 ja_JP: 新しいメンバーを歓迎し、/hello に返信します。 spec: + usages: [event] events: - group.member_joined - message.received @@ -65,14 +83,17 @@ execution: `spec.events` は対応するイベント、`spec.config` は詳細画面でユーザーが変更できる設定です。この例ではメンバー参加とメッセージ受信に対応し、歓迎メッセージと返信の有効・無効を設定できます。返信には、上記の `tool_calling` と `permissions.tools` の指定が必要です。 + +`spec.usages` は選択先を指定します。`agent` は Agent とパイプライン、`event` はプラグインプロセッサーです。`[agent, event]` の併記も可能です。`event` には `spec.events` が必要です。以下はイベント処理関数の例です。独自の `run(ctx)` から `await super().run(ctx)` で呼び出すこともできます。 + ## プラグインの実装 `Welcome` クラスの `initialize` メソッド内で処理関数を登録します。`welcome.py` を次の内容に変更してください。 ```python -from langbot_plugin.api.definition.components.event_processor import ( - EventProcessor, - EventProcessorContext, +from langbot_plugin.api.definition.components.runner import ( + Runner, + RunnerContext, ) from langbot_plugin.api.entities.builtin.platform.events import ( MemberJoinedEvent, @@ -81,13 +102,13 @@ from langbot_plugin.api.entities.builtin.platform.events import ( from langbot_plugin.api.entities.builtin.platform.message import Plain -class Welcome(EventProcessor): +class Welcome(Runner): async def initialize(self): await super().initialize() @self.handler(MemberJoinedEvent) - async def on_join(ctx: EventProcessorContext): - name = ctx.event.member.nickname or str(ctx.event.member.id) + async def on_join(ctx: RunnerContext): + name = ctx.platform_event.member.nickname or str(ctx.platform_event.member.id) await ctx.log(f"Member joined: {name}") if not ctx.config.get("reply_enabled", True): await ctx.log("Replies are disabled for this processor") @@ -97,9 +118,9 @@ class Welcome(EventProcessor): await ctx.log("Welcome action completed") @self.handler(MessageReceivedEvent) - async def on_message(ctx: EventProcessorContext): + async def on_message(ctx: RunnerContext): text = "".join( - item.text for item in ctx.event.message_chain if isinstance(item, Plain) + item.text for item in ctx.platform_event.message_chain if isinstance(item, Plain) ).strip() if text != "/hello": await ctx.log("Message ignored: expected /hello") @@ -112,7 +133,7 @@ class Welcome(EventProcessor): `on_join` は参加したメンバーに挨拶し、`on_message` は `/hello` に返信します。「返信する」を無効にすると、ログだけを記録します。 -`ctx.event` は現在のイベント、`ctx.config` はこのプロセッサーの設定です。返信には `ctx.reply()`、ログの記録には `ctx.log()` を使います。関数の実行が終わると、イベントの処理も終了します。 +`ctx.platform_event` は現在のイベント、`ctx.config` はこのプロセッサーの設定です。返信には `ctx.reply()`、ログの記録には `ctx.log()` を使います。関数の実行が終わると、イベントの処理も終了します。 ## ストリーミング返信 @@ -126,7 +147,7 @@ async with ctx.reply_stream() as reply: `async with` ブロックが正常終了すると返信が完了します。ストリーミングが未対応・無効の場合やメッセージ以外のイベントでは、最後に全文を一度だけ送信します。例外時には、バッファ内の未完成テキストを送信しません。デバッグでは送信をシミュレーションします。 -権限は `ctx.reply()` と共通です。AgentRunner も `self.get_run_api(ctx).reply_stream()` を利用できます。モデルの出力テキストが自動送信されることはありません。 +権限は `ctx.reply()` と共通です。Runner も `self.get_run_api(ctx).reply_stream()` を利用できます。モデルの出力テキストが自動送信されることはありません。 ## イベントの登録 @@ -168,7 +189,7 @@ async with ctx.reply_stream() as reply: ## 次のステップ -- [EventProcessorDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/EventProcessorDemo)で他の実装例を確認できます。 +- [RunnerDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/RunnerDemo)で他の実装例を確認できます。 - パイプラインの前処理やモデル呼び出し完了時などに機能を追加する場合は、[イベントリスナー](/ja/plugin/dev/components/event-listener)を使用してください。 返信、モデル、ツール、履歴、状態は[実行コンテキスト API](/ja/plugin/dev/apis/agent-run)を参照してください。 diff --git a/ja/plugin/dev/tutor.mdx b/ja/plugin/dev/tutor.mdx index a7911881c..1f521fcf0 100644 --- a/ja/plugin/dev/tutor.mdx +++ b/ja/plugin/dev/tutor.mdx @@ -3,7 +3,7 @@ title: "プラグイン開発チュートリアル" --- -メッセージ受信やメンバー参加などを処理するプラグインを作る場合は、基本環境を準備した後、[イベントプロセッサーのチュートリアル](/ja/plugin/dev/components/event-processor)に進んでください。 +メッセージ受信やメンバー参加などを処理するプラグインを作る場合は、基本環境を準備した後、[イベントプロセッサーのチュートリアル](/ja/plugin/dev/components/runner)に進んでください。 ## アーキテクチャの基礎 diff --git a/scripts/README-event-processor-validation.md b/scripts/README-event-processor-validation.md deleted file mode 100644 index 678a5dd1e..000000000 --- a/scripts/README-event-processor-validation.md +++ /dev/null @@ -1,84 +0,0 @@ -# EventProcessor tutorial verification - -The published tutorial is in `{zh,en,ja}/plugin/dev/components/event-processor.mdx`. -All three translations contain identical executable Python and component YAML. -Run the example checks with the SDK used by your LangBot installation. - -## Repeatable checks - -The API references in `{zh,en,ja}/plugin/dev/apis/` are also checked against the -installed SDK. Run `python scripts/verify-plugin-api-examples.py` to execute -the examples, validate method arguments, and check translated code parity. -The Host transport is mocked; this does not verify external delivery. - -With the matching SDK installed in the active Python environment: - -```bash -python scripts/verify-event-processor-tutorial.py -``` - -This extracts the actual MDX examples, compares all translations, loads the -component, applies YAML defaults, and executes ten cases. They cover member names -and ID fallback, private/group commands, disabled replies, ordinary/empty/image -messages, and a failed delivery that must retain its logs without completing. -Its tool API is mocked; it does not claim platform delivery. - -With Node.js 24 or later: - -```bash -npm ci -npm test -npm run typecheck -npm run build -``` - -The build also checks static HTML, Markdown exports, navigation, canonical URLs, -alternate languages, and preservation of legacy documentation routes. - -The companion `langbot-plugin-demo/EventProcessorDemo` repository contains -`examples/event-matrix.json` and `scripts/event_matrix.py`. See its README for -authenticated Host checks covering all 17 event types and invalid payloads. -Use a dedicated observer instance with payload logging enabled and no Bot binding. - -## Verified development snapshot: 2026-09-09 - -Documentation work started from the updated `langbot-app/langbot-docs` main commit -`a86ecb4`, on branch `docs/4.11-event-processor`. The local directory still uses -the previous name `langbot-wiki`. - -The tested SDK was `dev/4.11.x` at -`f82b3ce935f9a33afee389fb39fe8dc29a45b615`, both as a freshly built wheel and via -a Git installation from the matching SDK branch in an isolated Python 3.12 environment. -The Host was `dev/4.11.x` at `ea3e32c` with local processor UI refinements. - -| Layer | Observed result | -| --- | --- | -| Documentation | 36 Node tests, 6 Python tests, TypeScript check and full build passed; all 30 post-build static checks passed | -| SDK CLI, packaging and event tests | 246 passed, including 92 new event-matrix cases and packaged CLI execution of generated EventProcessor/AgentRunner components | -| Host processor service and run ledger | 73 passed | -| Platform EBA conversion, routing and run-log authorization | 212 passed | -| EventProcessorDemo unit tests | 8 passed, including scenario subtests, lookup errors and instance isolation | -| Installed observer through HTTP → Host → Runtime | 48 passed: 42 valid cases and 6 rejected inputs; one completed run per valid input | -| Tutorial via developer Runtime and installed package | Both workflows passed; final installed checks cover 9 valid cases plus unsupported-event rejection | -| Python extracted from all three tutorial pages | 10 passed, also using the SDK installed from Git | -| Local OneBot transport → adapter → binding → installed processor | 5 passed: unbound, bound, disabled, higher priority and equal-priority order | -| Authenticated browser tutorial | Selected member joined, entered Alice/alice/tutorial-group, clicked once; one reply trace, one completed run, and automatic switch to Logs | - -The generated AgentRunner was also connected with `lbp run` and invoked through -the real Host, producing its expected echo. The tutorial EventProcessor was built -and uploaded as `tutorial-WelcomeEvents-0.1.0.lbpkg`; it continued to work after -the developer process stopped and the Host restarted. - -The localhost OneBot peer received exactly one `send_group_msg` in the bound and -equal-priority cases. Other cases sent none. It exercised normal routing, not the -debug Mock, but the remote platform was simulated locally: this is not evidence -of delivery through QQ or every other third-party service. Original QA Bot -bindings and the local extension limit were restored after testing. - -Python hot reload worked during development. Component YAML and manifest changes -required restarting `lbp run`; the tutorial explicitly documents that boundary. - -This matrix covers the registered event model and representative data, failure, -concurrency and routing branches. Arbitrary plugin business logic and every real -platform/account combination require their own integration checks. Local passes -are not a CI, publication or deployment result. diff --git a/scripts/README-runner-validation.md b/scripts/README-runner-validation.md new file mode 100644 index 000000000..06d9b0d0d --- /dev/null +++ b/scripts/README-runner-validation.md @@ -0,0 +1,14 @@ +# Runner tutorial verification + +With the local SDK installed, run: + +```bash +python scripts/verify-runner-tutorial.py +python scripts/verify-plugin-api-examples.py +npm test +npm run build +``` + +The tutorial checker extracts the published Chinese, English and Japanese examples, checks that their code and manifests match, and executes event handlers including disabled replies, empty input, private/group messages and delivery failure. Platform actions use Mock; these tests do not prove external message delivery. + +The official `langbot-plugin-demo/RunnerDemo` plugin adds multi-step actions, configuration isolation and progress/error examples. Its `scripts/smoke.py` installs a built package and runs the scenarios against a local Host. Use the same SDK and Core revision when testing. diff --git a/scripts/verify-plugin-api-examples.py b/scripts/verify-plugin-api-examples.py index 8f6b618b4..550c79282 100644 --- a/scripts/verify-plugin-api-examples.py +++ b/scripts/verify-plugin-api-examples.py @@ -15,12 +15,12 @@ from types import SimpleNamespace from typing import Any from unittest.mock import AsyncMock -from langbot_plugin.api.entities.builtin.agent_runner.context import AgentRunContext -from langbot_plugin.api.entities.builtin.agent_runner.errors import ( +from langbot_plugin.api.entities.builtin.runner.context import RunnerContext +from langbot_plugin.api.entities.builtin.runner.errors import ( AgentAPIError, AgentAPIException, ) -from langbot_plugin.api.proxies.agent_run import AgentRunAPIProxy +from langbot_plugin.api.proxies.runner import RunnerAPIProxy from langbot_plugin.api.proxies.langbot_api import LangBotAPIProxy from langbot_plugin.api.entities.builtin.provider.message import Message @@ -74,7 +74,7 @@ class Host: def context(): - return AgentRunContext.model_validate( + return RunnerContext.model_validate( { "run_id": "docs-run", "trigger": {"type": "user_message"}, @@ -131,7 +131,7 @@ async def execute(block, namespace): cls = ( LangBotAPIProxy if owner == "self.plugin" - else AgentRunAPIProxy + else RunnerAPIProxy if owner == "api" else None ) @@ -154,7 +154,7 @@ async def main(): assert blocks == snippets(locale, page), f"{page}: {locale} code differs" for block in blocks: host = Host() - api = AgentRunAPIProxy(context(), host) + api = RunnerAPIProxy(context(), host) ctx = SimpleNamespace(api=api, log=AsyncMock()) namespace = { "Any": Any, diff --git a/scripts/verify-event-processor-tutorial.py b/scripts/verify-runner-tutorial.py similarity index 86% rename from scripts/verify-event-processor-tutorial.py rename to scripts/verify-runner-tutorial.py index 6cd10940a..dfb27b2df 100644 --- a/scripts/verify-event-processor-tutorial.py +++ b/scripts/verify-runner-tutorial.py @@ -1,6 +1,6 @@ """Execute the published tutorial with the matching 4.11 SDK installed. -Run: python scripts/verify-event-processor-tutorial.py +Run: python scripts/verify-runner-tutorial.py The tool API is mocked; real Host/runtime checks are a separate validation layer. """ @@ -11,6 +11,7 @@ from types import SimpleNamespace from unittest.mock import AsyncMock, Mock import yaml +from langbot_plugin.api.definition.components.runner import RunnerContext ROOT = Path(__file__).resolve().parents[1] @@ -19,21 +20,21 @@ ROOT = Path(__file__).resolve().parents[1] def load_tutorial(): examples = [] for locale in ("zh", "en", "ja"): - path = ROOT / locale / "plugin/dev/components/event-processor.mdx" + path = ROOT / locale / "plugin/dev/components/runner.mdx" source = path.read_text() python = re.findall(r"^```python\n(.*?)^```", source, re.M | re.S) manifests = [ block for block in re.findall(r"^```yaml\n(.*?)^```", source, re.M | re.S) - if "kind: EventProcessor" in block + if "kind: Runner" in block ] - python = [block for block in python if "class Welcome(EventProcessor):" in block] + python = [block for block in python if "class Welcome(Runner):" in block] assert len(python) == len(manifests) == 1, path examples.append((python[0], manifests[0])) assert examples[0] == examples[1] == examples[2], "Locale examples differ" code, manifest = examples[0] namespace = {} - exec(compile(code, "event-processor.mdx", "exec"), namespace) + exec(compile(code, "runner.mdx", "exec"), namespace) manifest = yaml.safe_load(manifest) assert manifest["spec"]["events"] == ["group.member_joined", "message.received"] assert manifest["spec"]["capabilities"]["tool_calling"] is True @@ -121,12 +122,14 @@ async def main(): ) ) component.get_run_api = Mock(return_value=api) - context = SimpleNamespace( - run_id=name, config={**defaults, **config}, event=SimpleNamespace(data=data) - ) + context = RunnerContext.model_validate({ + "run_id": name, "trigger": {"type": data["type"]}, + "event": {"event_id": name, "event_type": data["type"], "source": "test", "data": data}, + "config": {**defaults, **config}, "input": {}, "delivery": {"surface": "test"}, "resources": {}, "runtime": {}, + }) results = [] try: - async for result in component.run(context): + async for result in component.invoke(context): results.append(result) except RuntimeError as exc: assert failure and str(exc) == "Delivery failed" diff --git a/sitemap-alternates.xml b/sitemap-alternates.xml index 7a81e086b..4dffc50ed 100644 --- a/sitemap-alternates.xml +++ b/sitemap-alternates.xml @@ -2142,27 +2142,6 @@ - - https://langbot.app/docs/en/plugin/dev/components/event-processor - - - - - - - https://langbot.app/docs/zh/plugin/dev/components/event-processor - - - - - - - https://langbot.app/docs/ja/plugin/dev/components/event-processor - - - - - https://langbot.app/docs/en/plugin/dev/components/knowledge-engine @@ -2247,6 +2226,27 @@ + + https://langbot.app/docs/en/plugin/dev/components/runner + + + + + + + https://langbot.app/docs/zh/plugin/dev/components/runner + + + + + + + https://langbot.app/docs/ja/plugin/dev/components/runner + + + + + https://langbot.app/docs/en/plugin/dev/components/tool diff --git a/zh/plugin/dev/apis/agent-run.mdx b/zh/plugin/dev/apis/agent-run.mdx index 3bc152a2d..1d983089f 100644 --- a/zh/plugin/dev/apis/agent-run.mdx +++ b/zh/plugin/dev/apis/agent-run.mdx @@ -2,16 +2,13 @@ title: "运行上下文 API" --- -AgentRunner 和 EventProcessor 使用当前运行的 API,访问已授权的模型、工具、知识库和上下文。这两个组件不提供 `self.plugin`。 +Runner 的 `ctx.api` 携带当前运行的 ID、上下文与资源授权;自定义 `run(ctx)` 和事件处理函数使用同一种 `RunnerContext`。`self.plugin` 仍可访问[插件通用 API](/zh/plugin/dev/apis/common),但不会自动代表当前运行。 ## 访问方式 ```python -# Inside an EventProcessor handler +# Inside Runner.run(ctx) or a registered handler api = ctx.api - -# Inside AgentRunner.run(ctx) -api = self.get_run_api(ctx) ``` 以下示例中的 `api` 均指当前运行的 API。 除 `get_allowed_*` 同步方法和流式上下文/迭代接口外,下文方法均使用 `await` 调用。不要跨运行保存或复用这个对象。 @@ -52,7 +49,7 @@ knowledge_bases = api.get_allowed_knowledge_bases() # Each item has kb_id ## 回复与日志 -| EventProcessor 方法 | 用途 | +| Runner 方法 | 用途 | | --- | --- | | `await ctx.reply(text)` | 向当前事件的会话发送文本,返回动作结果。 | | `await ctx.log(text, level="info")` | 记录日志;级别为 `debug`、`info`、`warning`、`error`。 | @@ -69,7 +66,7 @@ async with api.reply_stream() as reply: result = reply.result ``` -EventProcessor 也可使用 `ctx.reply_stream()`,它会同时记录动作的开始与结果。 +Runner 也可使用 `ctx.reply_stream()`,它会同时记录动作的开始与结果。 `update(text)` 接收**当前完整文本**,不是新增片段;返回本次动作结果。正常退出时完成回复。支持流式且开启流式回复的消息事件会更新平台消息;其他可回复事件会缓存文本,在成功结束时发送一次。异常或取消时丢弃缓存中的未完成文本,已显示的内容不保证能撤回。调试模式仅模拟平台发送。 @@ -157,19 +154,19 @@ if api.ctx.context.available_apis.state: | `run_append_result(result)` | `AgentRunEvent` | | `run_finalize(run_id=None, status=None, reason=None)` | `AgentRun` | -`run_id=None` 默认当前运行。普通 EventProcessor 处理函数返回后会自动结束,通常不需要手动追加或结束运行记录。AgentRunner 通过 `yield AgentRunResult` 上报结果,避免同时用 `run_append_result` 重复记录相同结果。 +`run_id=None` 表示当前运行。事件处理函数返回后自动结束;自定义 `run(ctx)` 可通过 `yield RunnerResult` 上报结果,不要再用 `run_append_result` 重复登记。 -## 结构化交互(AgentRunner) +## 结构化交互(Runner) -需要用户确认或填写表单时,通过 `yield AgentRunResult.interaction_requested(ctx.run_id, request)` 请求交互。`request` 可以是 `InteractionRequest` 对象或字典,必填字段为 `interaction_id`、`title`、`fallback_text`;可选字段有 `kind`(`form`、`confirmation`、`choice`)、`fields`、`actions`、`expires_at`。 +需要用户确认或填写表单时,通过 `yield RunnerResult.interaction_requested(ctx.run_id, request)` 请求交互。`request` 可以是 `InteractionRequest` 对象或字典,必填字段为 `interaction_id`、`title`、`fallback_text`;可选字段有 `kind`(`form`、`confirmation`、`choice`)、`fields`、`actions`、`expires_at`。 -清单需声明 `spec.capabilities.interactions: true` 和 `spec.permissions.interactions: [request]`,并获得宿主授权。界面支持情况见 `ctx.delivery.interactions`;提交结果以 `interaction.submitted` 事件返回。这是 Runner 的结果事件,不是 `ctx.api` 方法,EventProcessor 的普通处理函数不直接提供此接口。 +清单需声明 `spec.capabilities.interactions: true` 和 `spec.permissions.interactions: [request]`,并获得宿主授权。界面支持情况见 `ctx.delivery.interactions`;提交结果以 `interaction.submitted` 事件返回。这是 Runner 的结果事件,不是 `ctx.api` 方法,Runner 的普通处理函数不直接提供此接口。 ## 错误处理 ```python -from langbot_plugin.api.proxies.agent_run import PermissionDeniedError -from langbot_plugin.api.entities.builtin.agent_runner.errors import AgentAPIException +from langbot_plugin.api.proxies.runner import PermissionDeniedError +from langbot_plugin.api.entities.builtin.runner.errors import AgentAPIException try: result = await api.call_tool("event_get_actor", {}) @@ -179,4 +176,4 @@ except AgentAPIException as exc: await ctx.log(f"{exc.error.code}: {exc.error.message}", level="error") ``` -这是 EventProcessor 中的示例。`PermissionDeniedError` 表示资源、操作或能力未授权;`AgentAPIException.error` 包含 `code`、`message`、`retryable`、`details`。发送消息等动作超时后,结果可能不确定,不应仅依据 `retryable` 自动重发。 +这是 Runner 中的示例。`PermissionDeniedError` 表示资源、操作或能力未授权;`AgentAPIException.error` 包含 `code`、`message`、`retryable`、`details`。发送消息等动作超时后,结果可能不确定,不应仅依据 `retryable` 自动重发。 diff --git a/zh/plugin/dev/apis/common.mdx b/zh/plugin/dev/apis/common.mdx index d555c4fb2..cd8bd3ca8 100644 --- a/zh/plugin/dev/apis/common.mdx +++ b/zh/plugin/dev/apis/common.mdx @@ -4,7 +4,7 @@ title: "插件通用 API" LangBot 为插件提供了一系列 API ,用于操作 LangBot 各个模块、控制消息上下文等。 -`Command`、`EventListener`、`Tool` 等普通组件通过 `self.plugin` 使用本页 API。`AgentRunner` 和 `EventProcessor` 请使用[运行上下文 API](/zh/plugin/dev/apis/agent-run),不使用 `self.plugin`。平台查询和专有动作见[平台 API](/zh/plugin/dev/apis/platform)。 +所有组件都可通过 `self.plugin` 使用本页的 LangBot API。Runner 的 `ctx.api` 则携带当前运行的上下文和资源授权,见[运行上下文 API](/zh/plugin/dev/apis/agent-run)。指定机器人和目标的平台操作见[平台 API](/zh/plugin/dev/apis/platform)。 ## 请求 API diff --git a/zh/plugin/dev/apis/messages.mdx b/zh/plugin/dev/apis/messages.mdx index 7ca749934..31d053065 100644 --- a/zh/plugin/dev/apis/messages.mdx +++ b/zh/plugin/dev/apis/messages.mdx @@ -62,4 +62,4 @@ msg_chain = MessageChain([ ## 平台 API -查询用户、管理群组和调用平台专有接口,请使用[平台 API](/zh/plugin/dev/apis/platform)。EventProcessor 和 AgentRunner 通过[运行上下文 API](/zh/plugin/dev/apis/agent-run)调用已授权的平台工具。 +查询用户、管理群组和调用平台专有接口,请使用[平台 API](/zh/plugin/dev/apis/platform)。Runner 通过[运行上下文 API](/zh/plugin/dev/apis/agent-run)调用已授权的平台工具。 diff --git a/zh/plugin/dev/apis/platform.mdx b/zh/plugin/dev/apis/platform.mdx index 07fa809e7..98e1c3919 100644 --- a/zh/plugin/dev/apis/platform.mdx +++ b/zh/plugin/dev/apis/platform.mdx @@ -66,7 +66,7 @@ account = await self.plugin.call_platform_api( ## 在事件处理器中调用 -EventProcessor 和 AgentRunner 使用运行授权的工具,不提供通用的 `call_platform_api` 方法。例如,在当前事件支持查询发起者且已授权时: +Runner 也可以通过 `self.plugin` 调用本页的平台 API,显式指定机器人和目标。若要按当前事件的授权查询或回复,并在运行记录中展示动作,请使用 `ctx.api` 或 `ctx.reply()`: ```python tools = {tool.tool_name for tool in ctx.api.get_allowed_tools()} diff --git a/zh/plugin/dev/components/add.mdx b/zh/plugin/dev/components/add.mdx index e2a027f5d..580fd67a0 100644 --- a/zh/plugin/dev/components/add.mdx +++ b/zh/plugin/dev/components/add.mdx @@ -4,8 +4,8 @@ title: "添加组件" 插件由一种或多种组件构成,为 LangBot 提供不同的功能。目前支持的组件类型有: -- 事件处理器(EventProcessor):处理机器人收到的消息、成员入群等事件。详见[开发教程](/zh/plugin/dev/components/event-processor)。 -- Agent 运行器(AgentRunner):为 Agent 提供模型调用、外部 Agent 服务连接等运行方式。 +- 事件处理器(Runner):处理机器人收到的消息、成员入群等事件。详见[开发教程](/zh/plugin/dev/components/runner)。 +- Agent 运行器(Runner):为 Agent 提供模型调用、外部 Agent 服务连接等运行方式。 - 事件监听器(EventListener):监听流水线执行期间的事件,对上下文或流水线进行修改。 - 命令(Command):由用户通过`!`(或其他已设置的前缀)开头的命令消息触发。 - 工具(Tool):供 LangBot 内置的 Local Agent 在执行期间由 LLM 调用。 @@ -73,7 +73,7 @@ components: 各个组件的详细使用方式请参考: -- [EventProcessor](./event-processor) +- [Runner](./runner) - [EventListener](./event-listener) - [Command](./command) - [Tool](./tool) diff --git a/zh/plugin/dev/components/event-listener.mdx b/zh/plugin/dev/components/event-listener.mdx index b59963727..eea6cee58 100644 --- a/zh/plugin/dev/components/event-listener.mdx +++ b/zh/plugin/dev/components/event-listener.mdx @@ -3,7 +3,7 @@ title: "组件:事件监听器" --- -本组件用于扩展流水线的处理流程。如果您需要处理机器人收到的消息、成员入群等事件,请参阅[事件处理器](/zh/plugin/dev/components/event-processor)。 +本组件用于扩展流水线的处理流程。如果您需要处理机器人收到的消息、成员入群等事件,请参阅[事件处理器](/zh/plugin/dev/components/runner)。 LangBot 的流水线在运行期间会产生一些事件,供插件 Hook 并处理。每个插件中只允许添加一个事件监听器(components/event_listener/default.yaml),但在其中可以注册任意数量的事件。 diff --git a/zh/plugin/dev/components/event-processor.mdx b/zh/plugin/dev/components/runner.mdx similarity index 72% rename from zh/plugin/dev/components/event-processor.mdx rename to zh/plugin/dev/components/runner.mdx index 990fc241f..4894288c4 100644 --- a/zh/plugin/dev/components/event-processor.mdx +++ b/zh/plugin/dev/components/runner.mdx @@ -1,28 +1,45 @@ --- -title: "组件:事件处理器" +title: "组件:Runner" --- -事件处理器(EventProcessor)组件用于处理机器人收到的消息、成员入群等事件。用户安装插件后,可在 LangBot 中创建「插件处理器」并选择该组件,再绑定机器人的事件。 +Runner 组件用于实现 Agent 的运行逻辑,也可以按事件注册处理函数,供「插件处理器」使用。两种写法共用 `RunnerContext`,插件安装后由用户选择组件并配置。 环境准备见[插件开发教程](/zh/plugin/dev/tutor)。 -## 添加事件处理器组件 +## 添加 Runner 组件 -单个插件中可以添加多个事件处理器。请在插件目录执行命令 `lbp comp EventProcessor`,名称填写 `welcome`,描述填写 `Welcome new members`。 +单个插件中可以添加多个事件处理器。请在插件目录执行命令 `lbp comp Runner`,名称填写 `welcome`,描述填写 `Welcome new members`。 ```bash -lbp comp EventProcessor +lbp comp Runner ``` -生成的 `components/event_processor/welcome.yaml` 定义组件信息,`welcome.py` 是处理程序。 +生成的 `components/runner/welcome.yaml` 定义组件信息,`welcome.py` 是处理程序。 -## 清单文件:事件处理器 +## 自定义运行逻辑 + +需要接入模型或实现自己的执行循环时,声明 `spec.usages: [agent]`,在 `run(ctx)` 中返回结果: + +```python +from langbot_plugin.api.definition.components.runner import Runner, RunnerContext, RunnerResult +from langbot_plugin.api.entities.builtin.provider.message import Message + + +class Echo(Runner): + async def run(self, ctx: RunnerContext): + yield RunnerResult.run_completed( + ctx.run_id, + message=Message(role="assistant", content=ctx.input.to_text()), + ) +``` + +## 清单文件:Runner 将 `welcome.yaml` 修改为: ```yaml apiVersion: langbot/v1 -kind: EventProcessor +kind: Runner metadata: name: welcome label: @@ -34,6 +51,7 @@ metadata: zh_Hans: 欢迎新成员,并响应 /hello 消息。 ja_JP: 新しいメンバーを歓迎し、/hello に返信します。 spec: + usages: [event] events: - group.member_joined - message.received @@ -65,14 +83,17 @@ execution: `spec.events` 指定支持的事件,`spec.config` 定义用户可在处理器详情页修改的配置。本例支持成员入群和收到消息,并提供欢迎语、回复开关两个配置项。回复消息需要上面的 `tool_calling` 和 `permissions.tools` 配置。 + +`spec.usages` 决定组件可在哪类处理器中选择:`agent` 用于 Agent 和流水线,`event` 用于插件处理器。可同时声明 `[agent, event]`;包含 `event` 时必须填写 `spec.events`。本例使用事件处理函数,也可以在 `run(ctx)` 中按需调用 `await super().run(ctx)`。 + ## 插件处理 在 `Welcome` 类的 `initialize` 方法中注册事件处理函数。将 `welcome.py` 修改为: ```python -from langbot_plugin.api.definition.components.event_processor import ( - EventProcessor, - EventProcessorContext, +from langbot_plugin.api.definition.components.runner import ( + Runner, + RunnerContext, ) from langbot_plugin.api.entities.builtin.platform.events import ( MemberJoinedEvent, @@ -81,13 +102,13 @@ from langbot_plugin.api.entities.builtin.platform.events import ( from langbot_plugin.api.entities.builtin.platform.message import Plain -class Welcome(EventProcessor): +class Welcome(Runner): async def initialize(self): await super().initialize() @self.handler(MemberJoinedEvent) - async def on_join(ctx: EventProcessorContext): - name = ctx.event.member.nickname or str(ctx.event.member.id) + async def on_join(ctx: RunnerContext): + name = ctx.platform_event.member.nickname or str(ctx.platform_event.member.id) await ctx.log(f"Member joined: {name}") if not ctx.config.get("reply_enabled", True): await ctx.log("Replies are disabled for this processor") @@ -97,9 +118,9 @@ class Welcome(EventProcessor): await ctx.log("Welcome action completed") @self.handler(MessageReceivedEvent) - async def on_message(ctx: EventProcessorContext): + async def on_message(ctx: RunnerContext): text = "".join( - item.text for item in ctx.event.message_chain if isinstance(item, Plain) + item.text for item in ctx.platform_event.message_chain if isinstance(item, Plain) ).strip() if text != "/hello": await ctx.log("Message ignored: expected /hello") @@ -112,7 +133,7 @@ class Welcome(EventProcessor): `on_join` 在成员入群时发送欢迎语,`on_message` 回复 `/hello`。关闭「发送回复」后,只记录日志。 -处理函数中,`ctx.event` 保存当前事件的数据,`ctx.config` 是当前处理器的配置。调用 `ctx.reply()` 回复消息,`ctx.log()` 记录日志。函数执行完毕,本次处理即结束。 +处理函数中,`ctx.platform_event` 保存当前事件的数据,`ctx.config` 是当前处理器的配置。调用 `ctx.reply()` 回复消息,`ctx.log()` 记录日志。函数执行完毕,本次处理即结束。 ## 流式回复 @@ -126,7 +147,7 @@ async with ctx.reply_stream() as reply: 正常退出 `async with` 时完成回复。平台不支持流式、未开启流式回复或当前不是消息事件时,会在结束后发送一条完整消息;发生异常时不发送缓存中的未完成文本。调试模式仅模拟发送。 -此接口沿用 `ctx.reply()` 的权限。AgentRunner 可通过 `self.get_run_api(ctx).reply_stream()` 使用同一接口,模型返回文本仍不会自动发送。 +此接口沿用 `ctx.reply()` 的权限。Runner 可通过 `self.get_run_api(ctx).reply_stream()` 使用同一接口,模型返回文本仍不会自动发送。 ## 事件注册 @@ -168,7 +189,7 @@ async with ctx.reply_stream() as reply: ## 接下来做什么 -- 查看 [EventProcessorDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/EventProcessorDemo) 中的更多示例。 +- 查看 [RunnerDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/RunnerDemo) 中的更多示例。 - 如果要扩展流水线的预处理、模型调用完成等步骤,请使用[事件监听器](/zh/plugin/dev/components/event-listener)。 回复、模型、工具、历史与状态接口见[运行上下文 API](/zh/plugin/dev/apis/agent-run)。 diff --git a/zh/plugin/dev/tutor.mdx b/zh/plugin/dev/tutor.mdx index f5ffd0b95..089977061 100644 --- a/zh/plugin/dev/tutor.mdx +++ b/zh/plugin/dev/tutor.mdx @@ -3,7 +3,7 @@ title: "插件开发教程" --- -要开发处理消息、成员入群等事件的插件,可在完成基础环境准备后,继续阅读[事件处理器教程](/zh/plugin/dev/components/event-processor)。 +要开发处理消息、成员入群等事件的插件,可在完成基础环境准备后,继续阅读[事件处理器教程](/zh/plugin/dev/components/runner)。 ## 架构基础 From df06842ecb6e0c862e7fa49d21e0dbdea95f6012 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Fri, 11 Sep 2026 00:03:42 +0800 Subject: [PATCH 04/22] docs(plugin): reorganize context API reference --- docs.json | 6 +- en/plugin/dev/apis/agent-run.mdx | 326 ++++++++++------- en/plugin/dev/apis/common.mdx | 115 +----- en/plugin/dev/apis/messages.mdx | 2 +- en/plugin/dev/apis/pipeline-events.mdx | 49 +-- en/plugin/dev/apis/platform.mdx | 14 +- en/plugin/dev/components/command.mdx | 3 +- en/plugin/dev/components/event-listener.mdx | 3 +- en/plugin/dev/components/runner.mdx | 8 +- en/plugin/dev/components/tool.mdx | 1 + ja/plugin/dev/apis/agent-run.mdx | 326 ++++++++++------- ja/plugin/dev/apis/common.mdx | 115 +----- ja/plugin/dev/apis/messages.mdx | 2 +- ja/plugin/dev/apis/pipeline-events.mdx | 49 +-- ja/plugin/dev/apis/platform.mdx | 14 +- ja/plugin/dev/components/command.mdx | 3 +- ja/plugin/dev/components/event-listener.mdx | 5 +- ja/plugin/dev/components/runner.mdx | 8 +- ja/plugin/dev/components/tool.mdx | 3 +- scripts/verify-plugin-api-examples.py | 99 +++++- .../dify-agent-discord-telegram-slack.mdx | 4 +- zh/articles/langflow-drag-and-drop-ai-bot.mdx | 4 +- zh/articles/n8n-multi-platform-ai-chatbot.mdx | 2 +- zh/plugin/dev/apis/agent-run.mdx | 328 +++++++++++------- zh/plugin/dev/apis/common.mdx | 119 +------ zh/plugin/dev/apis/messages.mdx | 2 +- zh/plugin/dev/apis/pipeline-events.mdx | 47 +-- zh/plugin/dev/apis/platform.mdx | 14 +- zh/plugin/dev/components/add.mdx | 6 +- zh/plugin/dev/components/command.mdx | 3 +- zh/plugin/dev/components/event-listener.mdx | 5 +- zh/plugin/dev/components/runner.mdx | 16 +- zh/plugin/dev/components/tool.mdx | 3 +- zh/usage/mcp/readme.mdx | 2 +- zh/usage/pipelines/tbox.mdx | 2 +- zh/usage/sandbox/readme.mdx | 2 +- zh/usage/sandbox/runtime-relationships.mdx | 2 +- zh/usage/skills/readme.mdx | 2 +- 38 files changed, 780 insertions(+), 934 deletions(-) diff --git a/docs.json b/docs.json index 96d117643..c86b6afb8 100644 --- a/docs.json +++ b/docs.json @@ -700,8 +700,8 @@ { "group": "Plugin SDK API", "pages": [ - "en/plugin/dev/apis/common", "en/plugin/dev/apis/agent-run", + "en/plugin/dev/apis/common", "en/plugin/dev/apis/platform", "en/plugin/dev/apis/pipeline-events", "en/plugin/dev/apis/messages", @@ -1006,8 +1006,8 @@ { "group": "插件 SDK API", "pages": [ - "zh/plugin/dev/apis/common", "zh/plugin/dev/apis/agent-run", + "zh/plugin/dev/apis/common", "zh/plugin/dev/apis/platform", "zh/plugin/dev/apis/pipeline-events", "zh/plugin/dev/apis/messages", @@ -1307,8 +1307,8 @@ { "group": "プラグイン SDK API", "pages": [ - "ja/plugin/dev/apis/common", "ja/plugin/dev/apis/agent-run", + "ja/plugin/dev/apis/common", "ja/plugin/dev/apis/platform", "ja/plugin/dev/apis/pipeline-events", "ja/plugin/dev/apis/messages", diff --git a/en/plugin/dev/apis/agent-run.mdx b/en/plugin/dev/apis/agent-run.mdx index a9dd55425..261a3feb6 100644 --- a/en/plugin/dev/apis/agent-run.mdx +++ b/en/plugin/dev/apis/agent-run.mdx @@ -1,179 +1,241 @@ --- -title: "Run Context APIs" +title: "Context APIs" --- -Runner provides `ctx.api` bound to the current run ID, context, and resource grants. Custom `run(ctx)` implementations and registered handlers share `RunnerContext`. `self.plugin` still exposes [Common APIs](/en/plugin/dev/apis/common), but does not automatically represent the current run. +A context contains data for one component invocation. Each component receives a different context, so start with the entry point for your component. -## Access +| Component | Handler entry point | Context type | +| --- | --- | --- | +| EventListener | `handler(event_context)` | `EventContext` | +| Command | `subcommand(context)` | `ExecuteContext` | +| Tool | `call(params, session, query_id)` | No separate context object | +| Runner | `run(ctx)` or `handler(ctx)` | `RunnerContext` | +| KnowledgeEngine | `ingest(context)`, `retrieve(context)` | `IngestionContext`, `RetrievalContext` | +| KnowledgeRetriever | `retrieve(context)` | `RetrievalContext` | +| Parser | `parse(context)` | `ParseContext` | +| Page | `handle_api(request)` | `PageRequest` | + +This page covers the data and methods available through these contexts. Use `self.plugin` for models, knowledge bases, storage, and other LangBot capabilities; see [LangBot API](/en/plugin/dev/apis/common). For platform operations, see [Platform API](/en/plugin/dev/apis/platform). + +`EventContext`, `ExecuteContext`, and `Session` also carry `instance_uuid`, `workspace_uuid`, and `placement_generation`. These fields identify the current instance and workspace for execution; plugins must not treat them as authorization data. + +## EventListener + +EventListener handles Pipeline events. Its event handler receives an `EventContext`: ```python -# Inside Runner.run(ctx) or a registered handler -api = ctx.api +event = event_context.event +sender_id = event.sender_id ``` -In the examples below, `api` refers to the current run API. Use `await` for the methods below, except synchronous `get_allowed_*` methods and streaming context/iterator APIs. Do not retain or reuse it across runs. +### Context data -### Configuration and permissions +| Property | Type | Description | +| --- | --- | --- | +| `event` | `BaseEventModel` | Current event object; its actual type matches the registered event type | +| `event_name` | `str` | Current event class name | +| `query_id` | `int` | Current Pipeline request ID | +| `query_uuid` | `str \| None` | Stable identifier for the current Pipeline request | +| `eid` | `int` | Temporary event number inside Plugin Runtime; do not persist it | +| `is_prevent_default` | `bool` | Whether default handling has been stopped; change it with `prevent_default()` | +| `is_prevent_postorder` | `bool` | Whether later plugins have been stopped; change it with `prevent_postorder()` | -`ctx.config` contains the processor or Runner instance configuration; `self.get_plugin_config()` reads plugin-level configuration. Manifest `spec.permissions` declares requested permissions; actual resources also depend on user configuration and the current event. +See [Pipeline Events](/en/plugin/dev/apis/pipeline-events) for event fields and all supported event types. -```yaml -spec: - capabilities: - tool_calling: true - permissions: - tools: [detail, call] +### Context methods + +| Method | Return value | Description | +| --- | --- | --- | +| `await event_context.reply(message_chain, quote_origin=False)` | - | Reply in the conversation for the current request | +| `await event_context.get_bot_uuid()` | `str` | Get the source bot UUID | +| `await event_context.set_query_var(key, value)` | - | Set a variable on the current request | +| `await event_context.get_query_var(key)` | `Any` | Get one request variable | +| `await event_context.get_query_vars()` | `dict[str, Any]` | Get all request variables | +| `await event_context.create_new_conversation()` | `dict[str, Any]` | Clear the current Pipeline conversation so a later request starts a new one | +| `await event_context.list_pipeline_knowledge_bases()` | `list[dict[str, Any]]` | List knowledge bases bound to the current Pipeline Local Agent | +| `await event_context.retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` | `list[dict[str, Any]]` | Search a knowledge base bound to the current Pipeline | +| `event_context.prevent_default()` | - | Stop the default handling of the current event | +| `event_context.prevent_postorder()` | - | Stop later plugins from handling the current event | + +`reply()` accepts a [MessageChain](/en/plugin/dev/apis/messages). Request variables, replies, and Pipeline knowledge-base methods require an associated Pipeline request. The `kb_id` passed to `retrieve_knowledge()` must come from `list_pipeline_knowledge_bases()`. + +`prevent_default()` only affects Pipeline events whose default flow can be stopped. See [Pipeline Events](/en/plugin/dev/apis/pipeline-events#prevent-default-behavior). + +## Command + +A Command subcommand handler receives an `ExecuteContext`: + +```python +query = " ".join(context.crt_params) +session = context.session ``` -This is the minimum declaration for replies and tool calls. Add other entries to `spec.permissions` as needed: +### Context data -| `spec.permissions` | Operations | +| Property | Type | Description | +| --- | --- | --- | +| `session` | `Session` | Session that contains the current message | +| `command_text` | `str` | Complete command text without the command prefix | +| `full_command_text` | `str` | Complete command text including the prefix | +| `command` | `str` | Root command name | +| `crt_command` | `str` | Subcommand currently being executed | +| `params` | `list[str]` | All arguments after the root command | +| `crt_params` | `list[str]` | Arguments not yet consumed by the current subcommand | +| `privilege` | `int` | Command privilege level of the current user | +| `query_id` | `int` | Current Pipeline request ID | +| `query_uuid` | `str \| None` | Stable identifier for the current Pipeline request | + +The SDK calls `context.shift()` before entering the current subcommand. Components do not need to call it directly. + +### Context methods + +| Method | Return value | Description | +| --- | --- | --- | +| `await context.reply(message_chain, quote_origin=False)` | - | Reply in the conversation for the current request | +| `await context.get_bot_uuid()` | `str` | Get the source bot UUID | +| `await context.set_query_var(key, value)` | - | Set a variable on the current request | +| `await context.get_query_var(key)` | `Any` | Get one request variable | +| `await context.get_query_vars()` | `dict[str, Any]` | Get all request variables | +| `await context.create_new_conversation()` | `dict[str, Any]` | Clear the current Pipeline conversation so a later request starts a new one | +| `await context.list_pipeline_knowledge_bases()` | `list[dict[str, Any]]` | List knowledge bases bound to the current Pipeline Local Agent | +| `await context.retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` | `list[dict[str, Any]]` | Search a knowledge base bound to the current Pipeline | + +These request methods behave the same as their EventListener equivalents. See [Command](/en/plugin/dev/components/command) for registration and return values. + +## Tool + +Tool has no separate context object. Its `call()` parameters carry the invocation data: + +```python +conversation_type = session.launcher_type.value +conversation_id = session.launcher_id +sender_id = session.sender_id +``` + +| Parameter | Description | | --- | --- | -| `models` | `invoke`, `stream`, `rerank`, `count_tokens` | -| `tools` | `detail`, `call` | -| `knowledge_bases` | `list`, `retrieve` | -| `history` | `page`, `search` | -| `events` | `get`, `page` | -| `storage` | `plugin`, `workspace` | -| `interactions` | `request` | +| `params` | Arguments generated by the model or caller from the tool JSON Schema | +| `session` | Current session data | +| `query_id` | Current Pipeline request ID; pass it unchanged to LangBot APIs that require request context | -Inspect the resources actually granted: +Common `Session` properties are: -```python -models = api.get_allowed_models() # Each item has model_id and operations -tools = api.get_allowed_tools() # Each item has tool_name and parameters -knowledge_bases = api.get_allowed_knowledge_bases() # Each item has kb_id -``` - -Check context capabilities through `api.ctx.context.available_apis`. Do not call an API whose flag is `False`; a manifest permission alone does not enable it. - -## Replies and logs - -| Runner method | Purpose | +| Property | Description | | --- | --- | -| `await ctx.reply(text)` | Send text to the current event conversation; return the action result. | -| `await ctx.log(text, level="info")` | Record a log at `debug`, `info`, `warning`, or `error` level. | -| `ctx.reply_stream()` | Update one reply through an async context manager. | +| `launcher_type` | Conversation type: `person` or `group` | +| `launcher_id` | Private-chat user or group ID | +| `sender_id` | Current message sender ID | +| `bot_uuid` | Current bot UUID | +| `using_conversation` | Current Pipeline conversation | +| `conversations` | Pipeline conversations stored in this Session | +| `use_prompt_name` | Active prompt name | +| `create_time`, `update_time` | Session creation and update times | -`ctx.reply(text)` is equivalent to `await api.call_tool("event_reply", {"text": text})`. The event must have a reply target and grant `event_reply`. Logs and text returned by an independent Agent are not automatically sent to the platform. +`session` is a data object and has no context methods such as `reply()`. A tool return value goes back to its caller; it is not sent to the chat platform automatically. Use `self.plugin` for LangBot and platform capabilities. If an API requires `session` and `query_id`, pass the values received by the current call. -### Streaming replies +## Runner + +Both custom `run(ctx)` implementations and Runner event handlers receive a `RunnerContext`. It remains valid only while the current invocation is running. ```python -async with api.reply_stream() as reply: - await reply.update("Processing…") - await reply.update("Processing complete.") -result = reply.result +text = ctx.input.to_text() +event_type = ctx.event.event_type ``` -Runner can use `ctx.reply_stream()` to also record the action start and result. +### Context data -`update(text)` takes the **complete text so far**, not a delta, and returns the action result. Normal exit completes the reply. Message events with platform streaming support enabled update the platform message; other replyable events buffer text and send once on successful exit. Errors or cancellation discard buffered partial text; already displayed content is not guaranteed to be retracted. Debug mode simulates platform delivery. +| Property | Type | Description | +| --- | --- | --- | +| `run_id` | `str` | Current run ID | +| `trigger` | `AgentTrigger` | Trigger type, source, and time | +| `event` | `AgentEventContext` | Standard event envelope with the event ID, type, source, time, and raw data | +| `platform_event` | Platform event type | Platform event parsed into its specific event type | +| `conversation` | `ConversationContext \| None` | Current conversation, thread, bot, and workspace identifiers | +| `actor` | `ActorContext \| None` | Actor that caused the event | +| `subject` | `SubjectContext \| None` | Message, group, or other object affected by the event | +| `input` | `AgentInput` | Current text, structured content, and attachments | +| `delivery` | `DeliveryContext` | Output target and streaming, editing, and reaction capabilities | +| `resources` | `AgentResources` | Models, tools, knowledge bases, skills, and storage authorized for this run | +| `context` | `ContextAccess` | Conversation cursors, inline policy, and available context APIs | +| `state` | `AgentRunState` | Scope-specific state snapshot loaded at the start of the run | +| `runtime` | `AgentRuntimeContext` | LangBot version, trace ID, and deadline | +| `config` | `dict[str, Any]` | Current Agent or plugin-processor instance configuration | +| `adapter` | `AdapterContext \| None` | Additional entry-adapter data | +| `metadata` | `dict[str, Any]` | Other metadata supplied by the Host | -This requires `available_apis.reply_stream` and the `event_reply` permission. Model streaming and platform streaming are separate APIs; the plugin explicitly connects them. +Prefer the stable properties in this table. `event.data` and `adapter.extra` carry additional data that has not been promoted to stable fields. -## Models, tools, and knowledge bases +### Replies and logs -See [Common APIs](/en/plugin/dev/apis/common) for model parameters and usage objects. The run API also provides: +| Method | Return value | Description | +| --- | --- | --- | +| `await ctx.get_bot_uuid()` | `str` | Get the source bot UUID; raises an error when no bot is associated | +| `await ctx.reply(message_chain, quote_origin=False)` | `Any` | Reply to the current event with a string or `MessageChain` | +| `ctx.reply_stream()` | Async context manager | Stream full-text updates to one reply | +| `await ctx.log(text, level="info")` | - | Write to this run's log; levels are `debug`, `info`, `warning`, and `error` | -| Method | Result or purpose | -| --- | --- | -| `invoke_llm(llm_model_uuid, messages, ...)` | `Message` | -| `invoke_llm_with_usage(llm_model_uuid, messages, ...)` | `LLMInvokeResult` | -| `invoke_llm_stream(llm_model_uuid, messages, ...)` | Async iterator of `MessageChunk` | -| `invoke_llm_stream_events(llm_model_uuid, messages, ...)` | Async iterator of `LLMStreamEvent` | -| `count_tokens(llm_model_uuid, messages, funcs=None, extra_args=None, timeout=None)` | Input token count, `int` | -| `invoke_rerank(rerank_model_uuid, query, documents, top_k=None, extra_args=None, timeout=30.0)` | List with `index` and `relevance_score` | -| `get_tool_detail(tool_name)` | Tool description and parameter schema, `dict` | -| `call_tool(tool_name, parameters)` | Tool result, `dict`; no `session` or `query_id` | -| `retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` | Retrieval result list | +Each `reply_stream().update(text)` call supplies the complete current text. If the platform does not support streaming, LangBot sends one complete message when the stream finishes. Debug mode only simulates delivery. -All four model invocation methods also accept `remove_think=None`; non-streaming methods accept `timeout=None`. Timeouts are bounded by the remaining run time. Resource lists may be empty; do not assume a first model or knowledge base exists. +### Available tools -```python -allowed = {tool.tool_name for tool in api.get_allowed_tools()} -if "event_get_actor" in allowed: - actor = await api.call_tool("event_get_actor", {}) -``` +| Method | Return value | Description | +| --- | --- | --- | +| `await ctx.get_available_tools()` | `list[dict[str, Any]]` | Get context actions, platform APIs, plugin tools, and MCP tools callable in this invocation | +| `await ctx.call_tool(tool_name, parameters=None)` | `dict[str, Any]` | Call a tool from that list | -LangBot fixes targets for `event_*` tools; `platform_*` tools accept targets according to their schema. Use names and parameters from the granted list; pass `{}` to tools with no parameters. +Each item returned by `get_available_tools()` contains `name`, `description`, `parameters`, `type`, and `operations`. The result is already filtered by the current Agent or plugin-processor configuration and the event's capabilities. `ctx.call_tool()` cannot call a tool absent from this list. -## History and events +### Prompt, history, and events -The current input does not include full history. Pull records within the current run scope as needed; passing another conversation ID does not expand access. +| Method | Return value | Availability field | +| --- | --- | --- | +| `await ctx.get_prompt()` | `list[dict[str, Any]]` | `prompt_get` | +| `await ctx.history_page(conversation_id=None, before_cursor=None, after_cursor=None, limit=50, direction="backward", include_attachments=False)` | `HistoryPage` | `history_page` | +| `await ctx.history_search(query, filters=None, top_k=10)` | `HistorySearchResult` | `history_search` | +| `await ctx.event_get(event_id)` | `AgentEventRecord` | `event_get` | +| `await ctx.event_page(conversation_id=None, event_types=None, before_cursor=None, limit=50)` | `EventPage` | `event_page` | +| `await ctx.steering_pull(mode="all", limit=None)` | `SteeringPullResult` | `steering_pull` | -| Method | Returns | -| --- | --- | -| `get_prompt()` | Effective prompt list; capability flag: `prompt_get` | -| `history_page(conversation_id=None, before_cursor=None, after_cursor=None, limit=50, direction="backward", include_attachments=False)` | `HistoryPage` | -| `history_search(query, filters=None, top_k=10)` | `HistorySearchResult` | -| `event_get(event_id)` | `AgentEventRecord` | -| `event_page(conversation_id=None, event_types=None, before_cursor=None, limit=50)` | `EventPage` | -| `steering_pull(mode="all", limit=None)` | `SteeringPullResult`; consume queued follow-up inputs | +### State -```python -if api.ctx.context.available_apis.history_page: - page = await api.history_page(limit=20) - for item in page.items: - print(item.model_dump()) - if page.has_more and page.prev_cursor: - older = await api.history_page(before_cursor=page.prev_cursor, limit=20) -``` +| Method | Return value | Description | +| --- | --- | --- | +| `await ctx.state_get(scope, key)` | `dict[str, Any]` | Read a state value | +| `await ctx.state_set(scope, key, value)` | `dict[str, Any]` | Store a JSON-serializable state value | +| `await ctx.state_delete(scope, key)` | `dict[str, Any]` | Delete a state value | +| `await ctx.state_list(scope, prefix=None, limit=100)` | `dict[str, Any]` | List state keys in a scope | -Page results are SDK objects: use `.items`, `.has_more`, `.prev_cursor`, and `.next_cursor`, not dictionary indexing. Use `before_cursor` for older records, and `after_cursor` with `direction="forward"` for newer records. History search may only provide text matching. `steering_pull` accepts `all`, `one`, or `one-at-a-time` for Runners supporting follow-up inputs. +`scope` can be `conversation`, `actor`, `subject`, or `runner`. These methods require `ctx.context.available_apis.state` to be `true`. -## State and storage +### Run records -| Method | Returns | -| --- | --- | -| `state_get(scope, key)` | `{"value": ...}` | -| `state_set(scope, key, value)` | `{"success": ...}` | -| `state_delete(scope, key)` | `{"success": ...}` | -| `state_list(scope, prefix=None, limit=100)` | `{"keys": [...], "has_more": ...}` | +| Method | Return value | Availability field | +| --- | --- | --- | +| `await ctx.run_get(run_id=None)` | `AgentRun` | `run_get` | +| `await ctx.run_list(conversation_id=None, statuses=None, before_cursor=None, limit=50)` | `RunPage` | `run_list` | +| `await ctx.run_events_page(run_id=None, before_cursor=None, after_cursor=None, limit=50, direction="forward")` | `RunEventPage` | `run_events_page` | +| `await ctx.run_cancel(run_id=None, reason=None)` | `AgentRun` | `run_cancel` | +| `await ctx.run_append_result(result)` | `AgentRunEvent` | `run_append_result` | +| `await ctx.run_finalize(run_id=None, status=None, reason=None)` | `AgentRun` | `run_finalize` | -State requires `available_apis.state` and a Host-enabled scope: `conversation`, `actor`, `subject`, or `runner`. Values must be JSON-serializable; use a key prefix such as `external.`. LangBot persists state; these methods return dictionaries. +`run_id=None` refers to the current run. The SDK completes a normal event handler automatically. When a custom `run(ctx)` yields `RunnerResult`, do not register the same result again with `run_append_result()`. -```python -if api.ctx.context.available_apis.state: - await api.state_set("conversation", "external.last_topic", "setup") - value = await api.state_get("conversation", "external.last_topic") -``` +### Availability and authorization -Plugin and workspace storage provide `set_plugin_storage`, `get_plugin_storage`, `get_plugin_storage_keys`, `delete_plugin_storage`, and the corresponding `*_workspace_storage*` methods. Parameters match [Common APIs](/en/plugin/dev/apis/common); values are `bytes`. Access requires `api.ctx.resources.storage.plugin_storage` or `workspace_storage` respectively. +Use `ctx.context.available_apis` to check whether prompt, history, event, state, and run-record methods are available. Authorized models, tools, and knowledge bases are in `ctx.resources.models`, `ctx.resources.tools`, and `ctx.resources.knowledge_bases`. -## Run records +During Runner execution, model, tool, knowledge-base, storage, and platform calls through `self.plugin` are automatically associated with the current run and validated against `ctx.resources`. Do not pass `run_id`. After the handler returns, do not use this context or its run-bound calls from background tasks. -The following methods require the matching `available_apis` flag and only access runs within the authorized scope. +`ctx.api` is the lower-level run-scoped proxy. Normal component code should prefer the `ctx` methods listed on this page and use `self.plugin` for LangBot APIs. -| Method | Returns | -| --- | --- | -| `run_get(run_id=None)` | `AgentRun` | -| `run_list(conversation_id=None, statuses=None, before_cursor=None, limit=50)` | `RunPage` | -| `run_events_page(run_id=None, before_cursor=None, after_cursor=None, limit=50, direction="forward")` | `RunEventPage` | -| `run_cancel(run_id=None, reason=None)` | `AgentRun` | -| `run_append_result(result)` | `AgentRunEvent` | -| `run_finalize(run_id=None, status=None, reason=None)` | `AgentRun` | +## Data-only contexts -`run_id=None` selects the current run. Registered handlers finish automatically when they return. Custom `run(ctx)` implementations can yield `RunnerResult`; do not append the same result again through `run_append_result`. +The following components receive task input rather than an EventListener or Runner execution context. Their fields, return values, and complete examples live in the corresponding component guides. -## Structured interactions (Runner) - -To request confirmation or form input, yield `RunnerResult.interaction_requested(ctx.run_id, request)`. `request` accepts an `InteractionRequest` object or dictionary. Required fields are `interaction_id`, `title`, and `fallback_text`; optional fields include `kind` (`form`, `confirmation`, `choice`), `fields`, `actions`, and `expires_at`. - -This requires `spec.capabilities.interactions: true`, `spec.permissions.interactions: [request]`, and Host authorization. Inspect `ctx.delivery.interactions` for surface support; submissions return as `interaction.submitted` events. This is a Runner result event, not a method on `ctx.api`, and is yielded from a custom run(ctx) implementation. - -## Error handling - -```python -from langbot_plugin.api.proxies.runner import PermissionDeniedError -from langbot_plugin.api.entities.builtin.runner.errors import AgentAPIException - -try: - result = await api.call_tool("event_get_actor", {}) -except PermissionDeniedError as exc: - await ctx.log(str(exc), level="warning") -except AgentAPIException as exc: - await ctx.log(f"{exc.error.code}: {exc.error.message}", level="error") -``` - -This example runs inside a Runner. `PermissionDeniedError` means a resource, operation, or capability is not granted. `AgentAPIException.error` contains `code`, `message`, `retryable`, and `details`. A timed-out send may already have succeeded; do not resend solely because `retryable` is true. +| Component entry point | Main data | Component guide | +| --- | --- | --- | +| `KnowledgeEngine.ingest(context: IngestionContext)` | File, target knowledge base, creation settings, and parse result | [KnowledgeEngine](/en/plugin/dev/components/knowledge-engine#document-ingestion) | +| `KnowledgeEngine.retrieve(context: RetrievalContext)` | Query, target knowledge base, retrieval settings, and filters | [KnowledgeEngine](/en/plugin/dev/components/knowledge-engine#knowledge-retrieval) | +| `KnowledgeRetriever.retrieve(context: RetrievalContext)` | Query and external knowledge-base settings | [KnowledgeRetriever](/en/plugin/dev/components/knowledge-retriever#retrieval-context) | +| `Parser.parse(context: ParseContext)` | File content, filename, MIME type, and metadata | [Parser](/en/plugin/dev/components/parser#parse-method) | +| `Page.handle_api(request: PageRequest)` | Endpoint, HTTP method, body, and headers | [Page](/en/plugin/dev/components/page#pagerequest-fields) | diff --git a/en/plugin/dev/apis/common.mdx b/en/plugin/dev/apis/common.mdx index f60294710..e7e8914c2 100644 --- a/en/plugin/dev/apis/common.mdx +++ b/en/plugin/dev/apis/common.mdx @@ -1,119 +1,8 @@ --- -title: "Plugin Common APIs" +title: "LangBot API" --- -LangBot provides a series of APIs for plugins to operate various LangBot modules and control message contexts. - -All components can access these LangBot APIs through `self.plugin`. Runner also provides `ctx.api`, bound to the current invocation and its resource grants; see [Run Context APIs](/en/plugin/dev/apis/agent-run). Operations with an explicit bot and target are covered in [Platform APIs](/en/plugin/dev/apis/platform). - -## Request API - -Operations for the currently processing user request (message). Only available in `EventListener` and `Command` components. Access methods: - -- In event handler methods of `EventListener`: Internal methods of `event_context: context.EventContext` object -- In subcommand handler methods of `Command`: Internal methods of `context: ExecuteContext` object - -### Direct Reply Message - -Directly reply with a message chain to the session where the current request is located. - -For message chain construction methods, please refer to [Message Platform Entities](/en/plugin/dev/apis/messages). - -```python -async def reply( - self, message_chain: platform_message.MessageChain, quote_origin: bool = False -): - """Reply to the message request - - Args: - message_chain (platform.types.MessageChain): LangBot message chain - quote_origin (bool): Whether to quote the original message - """ - -# Usage example -await event_context.reply( - platform_message.MessageChain([ - platform_message.Plain(text="Hello, world!"), - ]), -) -``` - -### Get Bot UUID - -Get the UUID of the bot that originated the current request. - -```python -async def get_bot_uuid(self) -> str: - """Get the bot uuid""" - -# Usage example -bot_uuid = await event_context.get_bot_uuid() -``` - -### Set Request Variables - -Some information in a single request is stored in request variables. When using external Agent platforms like Dify, [these variables are explicitly passed to Agent platforms](/en/deploy/pipelines/readme.html#request-variables). - -```python -async def set_query_var(self, key: str, value: Any): - """Set a query variable""" - -# Usage example -await event_context.set_query_var("key", "value") -``` - -### Get Request Variables - -Get a single request variable. - -```python -async def get_query_var(self, key: str) -> Any: - """Get a query variable""" - -# Usage example -query_var = await event_context.get_query_var("key") -``` - -### Get All Request Variables - -```python -async def get_query_vars(self) -> dict[str, Any]: - """Get all query variables""" - -# Usage example -query_vars = await event_context.get_query_vars() -``` - -### List Pipeline Knowledge Bases - -Get the list of knowledge bases configured for the pipeline used by the current request. - -```python -async def list_pipeline_knowledge_bases(self) -> list[dict[str, Any]]: - """List knowledge bases configured for the current pipeline""" - -# Usage example -knowledge_bases = await event_context.list_pipeline_knowledge_bases() - -# Return example -[ - { - "uuid": "kb_uuid", - "name": "Product Docs", - "description": "Internal product documentation", - } -] -``` - -Only returns knowledge bases bound in the `Local Agent` configuration of the pipeline corresponding to the current request. If the current pipeline has no knowledge bases configured, or does not use `Local Agent`, an empty list is returned. - -### Start a new conversation - -`await event_context.create_new_conversation()` clears the conversation currently used by the Pipeline session, so subsequent processing starts a new one. It returns an empty dictionary and does not delete platform chat history. - -### Retrieve from Pipeline knowledge bases - -`await event_context.retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` returns retrieval results. `kb_id` must come from `list_pipeline_knowledge_bases()`; this request API cannot access knowledge bases outside the Pipeline configuration. +Use `self.plugin` for models, tools, knowledge bases, and storage. For the current event, replies, and conversation state, see [Context APIs](/en/plugin/dev/apis/agent-run). ## LangBot API diff --git a/en/plugin/dev/apis/messages.mdx b/en/plugin/dev/apis/messages.mdx index 2bc79edea..e4a084016 100644 --- a/en/plugin/dev/apis/messages.mdx +++ b/en/plugin/dev/apis/messages.mdx @@ -62,4 +62,4 @@ In addition to the above entities, there are also entities like `Friend` and `Gr ## Platform APIs -Use [Platform APIs](/en/plugin/dev/apis/platform) to query users, manage groups, and call platform-specific actions. Runner call authorized platform tools through [Run Context APIs](/en/plugin/dev/apis/agent-run). +Use [Platform APIs](/en/plugin/dev/apis/platform) to query users, manage groups, and call platform-specific actions. Runner uses the same `self.plugin` entrypoint; use `ctx.reply()` for the current conversation. diff --git a/en/plugin/dev/apis/pipeline-events.mdx b/en/plugin/dev/apis/pipeline-events.mdx index 040615e69..50183854f 100644 --- a/en/plugin/dev/apis/pipeline-events.mdx +++ b/en/plugin/dev/apis/pipeline-events.mdx @@ -248,42 +248,13 @@ class PromptPreProcessing(BaseEventModel): """Existing message records for this conversation, can be modified, langbot_plugin.api.entities.builtin.provider.message.Message or langbot_plugin.api.entities.builtin.provider.message.MessageChunk type""" ``` -## Event Context APIs +## EventListener context -```python -... - @self.handler(events.PersonMessageReceived) - async def handler(event_context: context.EventContext): - ... -``` +Event handlers read the current event through `event_context.event`. See [Context APIs](/en/plugin/dev/apis/agent-run#eventlistener) for the complete `EventContext` properties and methods. -Event processing methods will be passed the `EventContext` object, which contains event context information, and the object has both [Request API](/en/plugin/dev/apis/common) and event context specific APIs. The following is a list of event context specific APIs: +### Prevent default behavior -### Get Original Event Attributes - -`event_context.event` attribute is the original event object, which can get the attributes of the original event. The type is the type of the event being listened to. - -For example, when the event being listened to is `GroupMessageReceived`: - -```python -event = event_context.event -print(event.launcher_type) # Launcher object type (group) -print(event.launcher_id) # Group ID -print(event.sender_id) # Sender account ID -print(event.message_chain) # Message chain -``` - -### Prevent Default Behavior - -```python -def prevent_default(self): - """Prevent default behavior""" - -# Usage example -event_context.prevent_default() -``` - -Calling this method will prevent the default subsequent behavior of this event, and the pipeline will end directly. +Calling `event_context.prevent_default()` stops the default handling of the current event. Only these events support it: Only the following events can prevent default behavior: @@ -297,14 +268,4 @@ Only the following events can prevent default behavior: - NormalMessageResponded -### Prevent Postorder Execution - -```python -def prevent_postorder(self): - """Prevent postorder execution""" - -# Usage example -event_context.prevent_postorder() -``` - -Calling this method will prevent the subsequent plugins from executing this time. \ No newline at end of file +Call `event_context.prevent_postorder()` to stop later plugins from handling the current event. diff --git a/en/plugin/dev/apis/platform.mdx b/en/plugin/dev/apis/platform.mdx index c9fdc73c6..40219c03f 100644 --- a/en/plugin/dev/apis/platform.mdx +++ b/en/plugin/dev/apis/platform.mdx @@ -64,15 +64,15 @@ account = await self.plugin.call_platform_api( Only actions declared by the adapter are allowed; this is not unrestricted pass-through to every platform endpoint. Plugins call the Host through the runtime, rather than accessing `ctx.query.adapter` or `adapter.bot`. -## Calling from an event processor +## Runner -Runner can also call these platform APIs through `self.plugin`, specifying the bot and target explicitly. To use the current event grants and record actions in its run trace, use `ctx.api` or `ctx.reply()`: +During Runner execution, model, tool, knowledge-base, storage, and platform calls through `self.plugin` automatically use the current run and its authorization. Do not pass `run_id`. Concurrent runs remain isolated. Do not continue these run-bound calls after the handler returns. + +Use `await ctx.get_bot_uuid()` for the current bot. Platform calls must match the tool grants for this invocation. Event-target grants cannot access another target. Ungranted actions, including platform-specific actions, are rejected. These calls also use Mock delivery in debug mode. ```python -tools = {tool.tool_name for tool in ctx.api.get_allowed_tools()} -if "event_get_actor" in tools: - actor = await ctx.api.call_tool("event_get_actor", {}) +tools = await ctx.get_available_tools() +if any(tool["name"] == "event_get_actor" for tool in tools): + actor = await ctx.call_tool("event_get_actor") await ctx.log(str(actor)) ``` - -See [Run Context APIs](/en/plugin/dev/apis/agent-run) for access, permissions, and replies. Event debugging mocks platform tools; `self.plugin.call_platform_api()` in regular components performs real calls. diff --git a/en/plugin/dev/components/command.mdx b/en/plugin/dev/components/command.mdx index c9fd77d87..5e6d03787 100644 --- a/en/plugin/dev/components/command.mdx +++ b/en/plugin/dev/components/command.mdx @@ -211,4 +211,5 @@ For specific return values, please refer to the definition of `CommandReturn`: [ You have learned the basic information about command registration and command execution. Next, you can: -- Check out [Plugin Common APIs](/en/plugin/dev/apis/common) +- See the complete [`ExecuteContext` API reference](/en/plugin/dev/apis/agent-run#command). +- Check out [Plugin Common APIs](/en/plugin/dev/apis/common). diff --git a/en/plugin/dev/components/event-listener.mdx b/en/plugin/dev/components/event-listener.mdx index 47d85830a..298cfd1f7 100644 --- a/en/plugin/dev/components/event-listener.mdx +++ b/en/plugin/dev/components/event-listener.mdx @@ -103,5 +103,6 @@ Event registration is implemented through the `@self.handler` decorator, with th You have learned the basic information about event listeners. Next, you can: -- Check the `List of Monitorable Events` and `Event Context APIs` in the documentation [Pipeline Events](/en/plugin/dev/apis/pipeline-events) +- See [Pipeline Events](/en/plugin/dev/apis/pipeline-events) for event types and fields. +- See the complete [`EventContext` API reference](/en/plugin/dev/apis/agent-run#eventlistener). - Check out [Plugin Common APIs](/en/plugin/dev/apis/common) diff --git a/en/plugin/dev/components/runner.mdx b/en/plugin/dev/components/runner.mdx index df269b769..7eed968c3 100644 --- a/en/plugin/dev/components/runner.mdx +++ b/en/plugin/dev/components/runner.mdx @@ -6,6 +6,8 @@ Runner implements Agent execution or typed event handlers for a **Plugin process For environment setup, see the [plugin development tutorial](/en/plugin/dev/tutor). +See [Context APIs](/en/plugin/dev/apis/agent-run#runner) for the complete `RunnerContext` fields and methods. + ## Adding a Runner Component A plugin can contain several event processors. Run `lbp comp Runner` in the plugin directory. Enter `welcome` as the name and `Welcome new members` as the description. @@ -147,7 +149,6 @@ async with ctx.reply_stream() as reply: The reply finishes when the `async with` block exits normally. If streaming is unsupported, disabled, or the event is not a message, the Host sends one complete message at the end. Exceptions discard buffered partial text. Debug runs simulate delivery. -This API uses the same permission as `ctx.reply()`. Runner components can use `self.get_run_api(ctx).reply_stream()`; model output text is still not sent automatically. ## Registering Events @@ -189,7 +190,8 @@ After installing the plugin, bind the Bot's **Member joined** and **Message rece ## Next Steps +- See the [`RunnerContext` API reference](/en/plugin/dev/apis/agent-run#runner). +- See [LangBot API](/en/plugin/dev/apis/common) for models, tools, knowledge bases, and storage. +- See [Platform API](/en/plugin/dev/apis/platform) for messaging and other platform operations. - Explore more examples in [RunnerDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/RunnerDemo). - To extend Pipeline steps such as preprocessing or completion of a model call, use [Event Listener](/en/plugin/dev/components/event-listener). - -See [Run Context APIs](/en/plugin/dev/apis/agent-run) for replies, models, tools, history, and state. diff --git a/en/plugin/dev/components/tool.mdx b/en/plugin/dev/components/tool.mdx index f1ff5ccce..5ac07dc85 100644 --- a/en/plugin/dev/components/tool.mdx +++ b/en/plugin/dev/components/tool.mdx @@ -173,4 +173,5 @@ Now execute the command `lbp run` in the plugin directory to start debugging. Co You have learned the basic usage of tools. Next, you can: +- See how Tool receives [`session` and `query_id`](/en/plugin/dev/apis/agent-run#tool). - Check out [Plugin Common APIs](/en/plugin/dev/apis/common) diff --git a/ja/plugin/dev/apis/agent-run.mdx b/ja/plugin/dev/apis/agent-run.mdx index 480c6ddfa..8cb224bc1 100644 --- a/ja/plugin/dev/apis/agent-run.mdx +++ b/ja/plugin/dev/apis/agent-run.mdx @@ -1,179 +1,241 @@ --- -title: "実行コンテキスト API" +title: "コンテキスト API" --- -Runner の `ctx.api` は現在の実行 ID、コンテキスト、リソースの許可範囲に結び付いています。独自の `run(ctx)` と登録した処理関数は同じ `RunnerContext` を使用します。`self.plugin` から[共通 API](/ja/plugin/dev/apis/common)も利用できますが、現在の実行を自動的に表すものではありません。 +コンテキストには、1 回のコンポーネント呼び出しに関するデータが含まれます。コンポーネントごとに受け取るコンテキストが異なるため、まず対象コンポーネントの入口を確認してください。 -## アクセス方法 +| コンポーネント | ハンドラーの入口 | コンテキスト型 | +| --- | --- | --- | +| EventListener | `handler(event_context)` | `EventContext` | +| Command | `subcommand(context)` | `ExecuteContext` | +| Tool | `call(params, session, query_id)` | 独立したコンテキストオブジェクトなし | +| Runner | `run(ctx)` または `handler(ctx)` | `RunnerContext` | +| KnowledgeEngine | `ingest(context)`、`retrieve(context)` | `IngestionContext`、`RetrievalContext` | +| KnowledgeRetriever | `retrieve(context)` | `RetrievalContext` | +| Parser | `parse(context)` | `ParseContext` | +| Page | `handle_api(request)` | `PageRequest` | + +このページでは、コンテキストのデータとメソッドのみを説明します。モデル、ナレッジベース、ストレージなどの LangBot 機能には `self.plugin` を使用します。詳細は [LangBot API](/ja/plugin/dev/apis/common) を参照してください。プラットフォーム操作は[プラットフォーム API](/ja/plugin/dev/apis/platform)に記載しています。 + +`EventContext`、`ExecuteContext`、`Session` には `instance_uuid`、`workspace_uuid`、`placement_generation` も含まれます。これらは現在の実行に対応するインスタンスとワークスペースを示すためのもので、プラグイン側の認可情報として使用しないでください。 + +## EventListener + +EventListener はパイプラインイベントを処理します。イベントハンドラーは `EventContext` を受け取ります。 ```python -# Inside Runner.run(ctx) or a registered handler -api = ctx.api +event = event_context.event +sender_id = event.sender_id ``` -以下の `api` は現在の実行の API です。 同期の `get_allowed_*` とストリーミングのコンテキスト・イテレーター以外は、`await` で呼び出します。別の実行へ持ち越して再利用しないでください。 +### コンテキストデータ -### 設定と権限 +| プロパティ | 型 | 説明 | +| --- | --- | --- | +| `event` | `BaseEventModel` | 現在のイベントオブジェクト。実際の型は登録したイベント型と一致します | +| `event_name` | `str` | 現在のイベントクラス名 | +| `query_id` | `int` | 現在のパイプラインリクエスト ID | +| `query_uuid` | `str \| None` | 現在のパイプラインリクエストの安定した識別子 | +| `eid` | `int` | Plugin Runtime 内の一時的なイベント番号。永続化しないでください | +| `is_prevent_default` | `bool` | デフォルト処理を停止済みか。変更には `prevent_default()` を使用します | +| `is_prevent_postorder` | `bool` | 後続プラグインを停止済みか。変更には `prevent_postorder()` を使用します | -`ctx.config` はプロセッサーまたは Runner インスタンスの設定、`self.get_plugin_config()` はプラグイン全体の設定です。`spec.permissions` は要求する権限を宣言し、実際に利用できるリソースはユーザー設定と現在のイベントにも依存します。 +イベントのフィールドと購読可能なイベントは[パイプラインイベント](/ja/plugin/dev/apis/pipeline-events)を参照してください。 -```yaml -spec: - capabilities: - tool_calling: true - permissions: - tools: [detail, call] +### コンテキストメソッド + +| メソッド | 戻り値 | 説明 | +| --- | --- | --- | +| `await event_context.reply(message_chain, quote_origin=False)` | - | 現在のリクエストの会話に返信 | +| `await event_context.get_bot_uuid()` | `str` | 送信元 Bot の UUID を取得 | +| `await event_context.set_query_var(key, value)` | - | 現在のリクエストに変数を設定 | +| `await event_context.get_query_var(key)` | `Any` | リクエスト変数を 1 つ取得 | +| `await event_context.get_query_vars()` | `dict[str, Any]` | すべてのリクエスト変数を取得 | +| `await event_context.create_new_conversation()` | `dict[str, Any]` | 現在のパイプライン会話をクリアし、後続リクエストで新しい会話を作成 | +| `await event_context.list_pipeline_knowledge_bases()` | `list[dict[str, Any]]` | 現在のパイプラインの Local Agent に設定されたナレッジベースを取得 | +| `await event_context.retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` | `list[dict[str, Any]]` | 現在のパイプラインに設定されたナレッジベースを検索 | +| `event_context.prevent_default()` | - | 現在のイベントのデフォルト処理を停止 | +| `event_context.prevent_postorder()` | - | 後続プラグインによる現在のイベント処理を停止 | + +`reply()` は [MessageChain](/ja/plugin/dev/apis/messages) を受け取ります。リクエスト変数、返信、パイプラインのナレッジベース API には、関連付けられたパイプラインリクエストが必要です。`retrieve_knowledge()` の `kb_id` は `list_pipeline_knowledge_bases()` の結果から指定します。 + +`prevent_default()` はデフォルトフローを停止できるイベントにのみ作用します。詳細は[パイプラインイベント](/ja/plugin/dev/apis/pipeline-events#デフォルト動作を阻止)を参照してください。 + +## Command + +Command のサブコマンドハンドラーは `ExecuteContext` を受け取ります。 + +```python +query = " ".join(context.crt_params) +session = context.session ``` -返信とツール呼び出しに必要な最小設定です。他の機能を使う場合は `spec.permissions` に追加します: +### コンテキストデータ -| `spec.permissions` | 操作 | +| プロパティ | 型 | 説明 | +| --- | --- | --- | +| `session` | `Session` | 現在のメッセージが属するセッション | +| `command_text` | `str` | コマンド接頭辞を除いた完全なコマンド文字列 | +| `full_command_text` | `str` | 接頭辞を含む完全なコマンド文字列 | +| `command` | `str` | ルートコマンド名 | +| `crt_command` | `str` | 現在実行中のサブコマンド名 | +| `params` | `list[str]` | ルートコマンド以降のすべての引数 | +| `crt_params` | `list[str]` | 現在のサブコマンドで未処理の引数 | +| `privilege` | `int` | 現在のユーザーのコマンド権限レベル | +| `query_id` | `int` | 現在のパイプラインリクエスト ID | +| `query_uuid` | `str \| None` | 現在のパイプラインリクエストの安定した識別子 | + +SDK は現在のサブコマンドに入る前に `context.shift()` を呼び出します。コンポーネントから直接呼び出す必要はありません。 + +### コンテキストメソッド + +| メソッド | 戻り値 | 説明 | +| --- | --- | --- | +| `await context.reply(message_chain, quote_origin=False)` | - | 現在のリクエストの会話に返信 | +| `await context.get_bot_uuid()` | `str` | 送信元 Bot の UUID を取得 | +| `await context.set_query_var(key, value)` | - | 現在のリクエストに変数を設定 | +| `await context.get_query_var(key)` | `Any` | リクエスト変数を 1 つ取得 | +| `await context.get_query_vars()` | `dict[str, Any]` | すべてのリクエスト変数を取得 | +| `await context.create_new_conversation()` | `dict[str, Any]` | 現在のパイプライン会話をクリアし、後続リクエストで新しい会話を作成 | +| `await context.list_pipeline_knowledge_bases()` | `list[dict[str, Any]]` | 現在のパイプラインの Local Agent に設定されたナレッジベースを取得 | +| `await context.retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` | `list[dict[str, Any]]` | 現在のパイプラインに設定されたナレッジベースを検索 | + +これらのリクエストメソッドは EventListener と同じ動作です。登録方法と戻り値は [Command コンポーネント](/ja/plugin/dev/components/command)を参照してください。 + +## Tool + +Tool には独立したコンテキストオブジェクトがありません。`call()` の引数として呼び出し情報を受け取ります。 + +```python +conversation_type = session.launcher_type.value +conversation_id = session.launcher_id +sender_id = session.sender_id +``` + +| 引数 | 説明 | | --- | --- | -| `models` | `invoke`, `stream`, `rerank`, `count_tokens` | -| `tools` | `detail`, `call` | -| `knowledge_bases` | `list`, `retrieve` | -| `history` | `page`, `search` | -| `events` | `get`, `page` | -| `storage` | `plugin`, `workspace` | -| `interactions` | `request` | +| `params` | ツールの JSON Schema に基づいてモデルまたは呼び出し元が生成した引数 | +| `session` | 現在のセッションデータ | +| `query_id` | 現在のパイプラインリクエスト ID。リクエストコンテキストが必要な LangBot API にそのまま渡します | -実際に許可されたリソースを確認します: +`Session` の主なプロパティは次のとおりです。 -```python -models = api.get_allowed_models() # Each item has model_id and operations -tools = api.get_allowed_tools() # Each item has tool_name and parameters -knowledge_bases = api.get_allowed_knowledge_bases() # Each item has kb_id -``` - -コンテキスト API の利用可否は `api.ctx.context.available_apis` で確認できます。`False` の API は呼び出せません。マニフェストへの権限追加だけで有効になるわけではありません。 - -## 返信とログ - -| Runner メソッド | 用途 | +| プロパティ | 説明 | | --- | --- | -| `await ctx.reply(text)` | 現在のイベントの会話へテキストを送信し、操作結果を返します。 | -| `await ctx.log(text, level="info")` | `debug`、`info`、`warning`、`error` のログを記録します。 | -| `ctx.reply_stream()` | 非同期コンテキストマネージャーで同じ返信を更新します。 | +| `launcher_type` | 会話種別:`person` または `group` | +| `launcher_id` | 個人チャットのユーザー ID またはグループ ID | +| `sender_id` | 現在のメッセージ送信者 ID | +| `bot_uuid` | 現在の Bot UUID | +| `using_conversation` | 現在のパイプライン会話 | +| `conversations` | この Session に保存されたパイプライン会話 | +| `use_prompt_name` | 使用中のプロンプト名 | +| `create_time`、`update_time` | Session の作成時刻と更新時刻 | -`ctx.reply(text)` は `await api.call_tool("event_reply", {"text": text})` と同じです。イベントに返信先があり、`event_reply` が許可されている必要があります。ログや独立した Agent の出力テキストは自動送信されません。 +`session` はデータオブジェクトであり、`reply()` などのコンテキストメソッドはありません。ツールの戻り値は呼び出し元へ返され、チャットプラットフォームには自動送信されません。LangBot とプラットフォームの機能には `self.plugin` を使用します。API が `session` と `query_id` を要求する場合は、現在の呼び出しで受け取った値を渡してください。 -### ストリーミング返信 +## Runner + +独自の `run(ctx)` と Runner のイベントハンドラーは、どちらも `RunnerContext` を受け取ります。このコンテキストは現在の実行中だけ有効です。 ```python -async with api.reply_stream() as reply: - await reply.update("Processing…") - await reply.update("Processing complete.") -result = reply.result +text = ctx.input.to_text() +event_type = ctx.event.event_type ``` -Runner では `ctx.reply_stream()` を使うと、操作の開始と結果も記録されます。 +### コンテキストデータ -`update(text)` には差分ではなく**その時点の全文**を渡します。戻り値は操作結果です。正常終了時に返信を完了します。プラットフォームでストリーミング返信が有効なメッセージイベントでは表示を更新し、それ以外の返信可能なイベントでは正常終了時に全文を一度送信します。例外やキャンセルでは未送信のテキストを破棄しますが、表示済みの内容の撤回は保証されません。デバッグ時の送信は Mock です。 +| プロパティ | 型 | 説明 | +| --- | --- | --- | +| `run_id` | `str` | 現在の実行 ID | +| `trigger` | `AgentTrigger` | トリガーの種類、発生元、時刻 | +| `event` | `AgentEventContext` | イベント ID、種類、発生元、時刻、元データを含む標準イベントエンベロープ | +| `platform_event` | プラットフォームイベント型 | イベント型に応じて解析されたプラットフォームイベント | +| `conversation` | `ConversationContext \| None` | 現在の会話、スレッド、Bot、ワークスペースの識別子 | +| `actor` | `ActorContext \| None` | イベントを発生させた主体 | +| `subject` | `SubjectContext \| None` | イベントの対象となるメッセージやグループなど | +| `input` | `AgentInput` | 現在のテキスト、構造化コンテンツ、添付ファイル | +| `delivery` | `DeliveryContext` | 出力先とストリーミング、編集、リアクションの対応状況 | +| `resources` | `AgentResources` | 今回の実行で許可されたモデル、ツール、ナレッジベース、スキル、ストレージ | +| `context` | `ContextAccess` | 会話カーソル、インライン方針、利用可能なコンテキスト API | +| `state` | `AgentRunState` | 実行開始時に読み込まれたスコープ別状態のスナップショット | +| `runtime` | `AgentRuntimeContext` | LangBot バージョン、トレース ID、期限 | +| `config` | `dict[str, Any]` | 現在の Agent またはプラグイン処理インスタンスの設定 | +| `adapter` | `AdapterContext \| None` | エントリーアダプターの追加データ | +| `metadata` | `dict[str, Any]` | ホストが提供するその他のメタデータ | -`available_apis.reply_stream` と `event_reply` 権限が必要です。モデル出力のストリーミングとプラットフォームへの送信は別の API です。 +上表の安定したプロパティを優先してください。`event.data` と `adapter.extra` には、安定したフィールドに含まれない追加データが入ります。 -## モデル、ツール、ナレッジベース +### 返信とログ -モデルの引数と使用量の形式は[共通 API](/ja/plugin/dev/apis/common)を参照してください。実行 API には次のメソッドもあります: +| メソッド | 戻り値 | 説明 | +| --- | --- | --- | +| `await ctx.get_bot_uuid()` | `str` | 送信元 Bot UUID を取得。関連する Bot がなければエラー | +| `await ctx.reply(message_chain, quote_origin=False)` | `Any` | 文字列または `MessageChain` で現在のイベントに返信 | +| `ctx.reply_stream()` | 非同期コンテキストマネージャー | 1 件の返信を完全なテキストで逐次更新 | +| `await ctx.log(text, level="info")` | - | 今回の実行ログへ記録。レベルは `debug`、`info`、`warning`、`error` | -| メソッド | 戻り値・用途 | -| --- | --- | -| `invoke_llm(llm_model_uuid, messages, ...)` | `Message` | -| `invoke_llm_with_usage(llm_model_uuid, messages, ...)` | `LLMInvokeResult` | -| `invoke_llm_stream(llm_model_uuid, messages, ...)` | `MessageChunk` の非同期イテレーター | -| `invoke_llm_stream_events(llm_model_uuid, messages, ...)` | `LLMStreamEvent` の非同期イテレーター | -| `count_tokens(llm_model_uuid, messages, funcs=None, extra_args=None, timeout=None)` | 入力トークン数(`int`) | -| `invoke_rerank(rerank_model_uuid, query, documents, top_k=None, extra_args=None, timeout=30.0)` | `index`、`relevance_score` を含むリスト | -| `get_tool_detail(tool_name)` | ツールの説明と引数 schema(`dict`) | -| `call_tool(tool_name, parameters)` | ツール結果(`dict`)。`session`、`query_id` は不要 | -| `retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` | 検索結果のリスト | +`reply_stream().update(text)` には、その時点の完全なテキストを渡します。プラットフォームがストリーミングに対応しない場合、終了時に完全なメッセージを 1 件送信します。デバッグモードでは送信をシミュレートします。 -4 種類のモデル呼び出しは `remove_think=None`、非ストリーミング呼び出しは `timeout=None` も受け取ります。タイムアウトは実行の残り時間に制限されます。リソース一覧が空の場合もあります。 +### 利用可能なツール -```python -allowed = {tool.tool_name for tool in api.get_allowed_tools()} -if "event_get_actor" in allowed: - actor = await api.call_tool("event_get_actor", {}) -``` +| メソッド | 戻り値 | 説明 | +| --- | --- | --- | +| `await ctx.get_available_tools()` | `list[dict[str, Any]]` | 今回呼び出せるコンテキスト操作、プラットフォーム API、プラグインツール、MCP ツールを取得 | +| `await ctx.call_tool(tool_name, parameters=None)` | `dict[str, Any]` | 上記一覧のツールを呼び出す | -`event_*` ツールの対象は LangBot がイベントから固定し、`platform_*` ツールの対象は schema に従って指定します。許可された一覧の名前と引数を使い、引数なしのツールには `{}` を渡します。 +`get_available_tools()` の各項目には `name`、`description`、`parameters`、`type`、`operations` が含まれます。結果は現在の Agent またはプラグイン処理インスタンスの設定とイベント機能で絞り込まれています。一覧にないツールは `ctx.call_tool()` で呼び出せません。 -## 履歴とイベント +### プロンプト、履歴、イベント -現在の入力には全履歴が含まれません。必要に応じて現在の実行範囲の記録を取得します。別の会話 ID を渡してアクセス範囲を広げることはできません。 +| メソッド | 戻り値 | 利用可否フィールド | +| --- | --- | --- | +| `await ctx.get_prompt()` | `list[dict[str, Any]]` | `prompt_get` | +| `await ctx.history_page(conversation_id=None, before_cursor=None, after_cursor=None, limit=50, direction="backward", include_attachments=False)` | `HistoryPage` | `history_page` | +| `await ctx.history_search(query, filters=None, top_k=10)` | `HistorySearchResult` | `history_search` | +| `await ctx.event_get(event_id)` | `AgentEventRecord` | `event_get` | +| `await ctx.event_page(conversation_id=None, event_types=None, before_cursor=None, limit=50)` | `EventPage` | `event_page` | +| `await ctx.steering_pull(mode="all", limit=None)` | `SteeringPullResult` | `steering_pull` | -| メソッド | 戻り値 | -| --- | --- | -| `get_prompt()` | 適用されたプロンプトのリスト。機能フラグは `prompt_get` | -| `history_page(conversation_id=None, before_cursor=None, after_cursor=None, limit=50, direction="backward", include_attachments=False)` | `HistoryPage` | -| `history_search(query, filters=None, top_k=10)` | `HistorySearchResult` | -| `event_get(event_id)` | `AgentEventRecord` | -| `event_page(conversation_id=None, event_types=None, before_cursor=None, limit=50)` | `EventPage` | -| `steering_pull(mode="all", limit=None)` | `SteeringPullResult`。実行中に追加された入力を取得 | +### 状態 -```python -if api.ctx.context.available_apis.history_page: - page = await api.history_page(limit=20) - for item in page.items: - print(item.model_dump()) - if page.has_more and page.prev_cursor: - older = await api.history_page(before_cursor=page.prev_cursor, limit=20) -``` +| メソッド | 戻り値 | 説明 | +| --- | --- | --- | +| `await ctx.state_get(scope, key)` | `dict[str, Any]` | 状態値を取得 | +| `await ctx.state_set(scope, key, value)` | `dict[str, Any]` | JSON シリアライズ可能な状態値を保存 | +| `await ctx.state_delete(scope, key)` | `dict[str, Any]` | 状態値を削除 | +| `await ctx.state_list(scope, prefix=None, limit=100)` | `dict[str, Any]` | 指定スコープの状態キーを一覧表示 | -ページ結果は SDK オブジェクトです。辞書ではなく `.items`、`.has_more`、`.prev_cursor`、`.next_cursor` を使います。古い記録は `before_cursor`、新しい記録は `after_cursor` と `direction="forward"` で取得します。履歴検索はテキスト一致のみの場合があります。追加入力対応 Runner の `steering_pull` は `all`、`one`、`one-at-a-time` を受け取ります。 +`scope` は `conversation`、`actor`、`subject`、`runner` のいずれかです。これらのメソッドには `ctx.context.available_apis.state` が `true` である必要があります。 -## 状態とストレージ +### 実行記録 -| メソッド | 戻り値 | -| --- | --- | -| `state_get(scope, key)` | `{"value": ...}` | -| `state_set(scope, key, value)` | `{"success": ...}` | -| `state_delete(scope, key)` | `{"success": ...}` | -| `state_list(scope, prefix=None, limit=100)` | `{"keys": [...], "has_more": ...}` | +| メソッド | 戻り値 | 利用可否フィールド | +| --- | --- | --- | +| `await ctx.run_get(run_id=None)` | `AgentRun` | `run_get` | +| `await ctx.run_list(conversation_id=None, statuses=None, before_cursor=None, limit=50)` | `RunPage` | `run_list` | +| `await ctx.run_events_page(run_id=None, before_cursor=None, after_cursor=None, limit=50, direction="forward")` | `RunEventPage` | `run_events_page` | +| `await ctx.run_cancel(run_id=None, reason=None)` | `AgentRun` | `run_cancel` | +| `await ctx.run_append_result(result)` | `AgentRunEvent` | `run_append_result` | +| `await ctx.run_finalize(run_id=None, status=None, reason=None)` | `AgentRun` | `run_finalize` | -状態には `available_apis.state` とホストが許可するスコープ(`conversation`、`actor`、`subject`、`runner`)が必要です。値は JSON 化可能にし、`external.` などのキープレフィックスを推奨します。LangBot が永続化し、API は辞書を返します。 +`run_id=None` は現在の実行を指します。通常のイベントハンドラーは SDK が自動的に完了します。独自の `run(ctx)` が `RunnerResult` を yield する場合、同じ結果を `run_append_result()` で重複登録しないでください。 -```python -if api.ctx.context.available_apis.state: - await api.state_set("conversation", "external.last_topic", "setup") - value = await api.state_get("conversation", "external.last_topic") -``` +### 利用可否と権限 -プラグインおよびワークスペースのストレージは `set_plugin_storage`、`get_plugin_storage`、`get_plugin_storage_keys`、`delete_plugin_storage` と対応する `*_workspace_storage*` を使用します。引数は[共通 API](/ja/plugin/dev/apis/common)と同じで、値は `bytes` です。`api.ctx.resources.storage.plugin_storage` または `workspace_storage` の権限が必要です。 +プロンプト、履歴、イベント、状態、実行記録の API が利用可能かどうかは `ctx.context.available_apis` で確認します。許可されたモデル、ツール、ナレッジベースはそれぞれ `ctx.resources.models`、`ctx.resources.tools`、`ctx.resources.knowledge_bases` にあります。 -## 実行記録 +Runner の実行中に `self.plugin` から呼び出すモデル、ツール、ナレッジベース、ストレージ、プラットフォーム API は、自動的に現在の実行へ関連付けられ、`ctx.resources` に基づいて再検証されます。`run_id` を渡す必要はありません。ハンドラー終了後にバックグラウンドタスクからこのコンテキストや実行に紐づく呼び出しを使用しないでください。 -以下は同名の `available_apis` フラグが必要で、許可された範囲の実行のみ操作できます。 +`ctx.api` は低レベルの実行スコーププロキシです。通常のコンポーネントコードでは、このページに記載した `ctx` メソッドと、LangBot API 用の `self.plugin` を使用してください。 -| メソッド | 戻り値 | -| --- | --- | -| `run_get(run_id=None)` | `AgentRun` | -| `run_list(conversation_id=None, statuses=None, before_cursor=None, limit=50)` | `RunPage` | -| `run_events_page(run_id=None, before_cursor=None, after_cursor=None, limit=50, direction="forward")` | `RunEventPage` | -| `run_cancel(run_id=None, reason=None)` | `AgentRun` | -| `run_append_result(result)` | `AgentRunEvent` | -| `run_finalize(run_id=None, status=None, reason=None)` | `AgentRun` | +## データ専用コンテキスト -`run_id=None` は現在の実行を指します。登録した処理関数は戻ると自動終了します。独自の `run(ctx)` は `yield RunnerResult` で結果を報告できます。同じ結果を `run_append_result` で重複登録しないでください。 +以下のコンポーネントが受け取るものはタスク入力であり、EventListener や Runner の実行コンテキストメソッドはありません。フィールド、戻り値、完全な例は各コンポーネントガイドに記載しています。 -## 構造化インタラクション(Runner) - -確認やフォーム入力には `yield RunnerResult.interaction_requested(ctx.run_id, request)` を使用します。`request` は `InteractionRequest` または辞書で、必須フィールドは `interaction_id`、`title`、`fallback_text` です。`kind`(`form`、`confirmation`、`choice`)、`fields`、`actions`、`expires_at` は任意です。 - -`spec.capabilities.interactions: true` と `spec.permissions.interactions: [request]` の宣言、およびホストの許可が必要です。画面の対応状況は `ctx.delivery.interactions` で確認し、送信結果は `interaction.submitted` イベントとして返ります。これは Runner の結果イベントであり、`ctx.api` のメソッドではありません。独自の run(ctx) から yield します。 - -## エラー処理 - -```python -from langbot_plugin.api.proxies.runner import PermissionDeniedError -from langbot_plugin.api.entities.builtin.runner.errors import AgentAPIException - -try: - result = await api.call_tool("event_get_actor", {}) -except PermissionDeniedError as exc: - await ctx.log(str(exc), level="warning") -except AgentAPIException as exc: - await ctx.log(f"{exc.error.code}: {exc.error.message}", level="error") -``` - -Runner 内の例です。`PermissionDeniedError` はリソース・操作・機能の権限不足を示します。`AgentAPIException.error` は `code`、`message`、`retryable`、`details` を持ちます。送信のタイムアウト時は成功済みの場合もあるため、`retryable` だけで再送しないでください。 +| コンポーネントの入口 | 主なデータ | コンポーネントガイド | +| --- | --- | --- | +| `KnowledgeEngine.ingest(context: IngestionContext)` | ファイル、対象ナレッジベース、作成設定、解析結果 | [KnowledgeEngine](/ja/plugin/dev/components/knowledge-engine#ドキュメント取り込み) | +| `KnowledgeEngine.retrieve(context: RetrievalContext)` | クエリ、対象ナレッジベース、検索設定、フィルター | [KnowledgeEngine](/ja/plugin/dev/components/knowledge-engine#ナレッジ検索) | +| `KnowledgeRetriever.retrieve(context: RetrievalContext)` | クエリと外部ナレッジベース設定 | [KnowledgeRetriever](/ja/plugin/dev/components/knowledge-retriever#検索コンテキスト) | +| `Parser.parse(context: ParseContext)` | ファイル内容、ファイル名、MIME タイプ、メタデータ | [Parser](/ja/plugin/dev/components/parser#解析メソッド) | +| `Page.handle_api(request: PageRequest)` | エンドポイント、HTTP メソッド、本文、ヘッダー | [Page](/ja/plugin/dev/components/page#pagerequest-フィールド) | diff --git a/ja/plugin/dev/apis/common.mdx b/ja/plugin/dev/apis/common.mdx index 2d24166e7..82f650be1 100644 --- a/ja/plugin/dev/apis/common.mdx +++ b/ja/plugin/dev/apis/common.mdx @@ -1,119 +1,8 @@ --- -title: "プラグイン共通API" +title: "LangBot API" --- -LangBotは、プラグインがさまざまなLangBotモジュールを操作し、メッセージコンテキストを制御するための一連のAPIを提供します。 - -すべてのコンポーネントは `self.plugin` から本ページの LangBot API を利用できます。Runner の `ctx.api` は現在の実行とリソースの許可範囲に結び付いています。[実行コンテキスト API](/ja/plugin/dev/apis/agent-run)を参照してください。Bot と宛先を指定する操作は[プラットフォーム API](/ja/plugin/dev/apis/platform)に記載しています。 - -## リクエストAPI - -現在処理中のユーザーリクエスト(メッセージ)に対する操作です。`EventListener`と`Command`コンポーネントでのみ使用可能です。アクセス方法: - -- `EventListener`のイベントハンドラメソッド内: `event_context: context.EventContext`オブジェクトの内部メソッド -- `Command`のサブコマンドハンドラメソッド内: `context: ExecuteContext`オブジェクトの内部メソッド - -### 直接返信メッセージ - -現在のリクエストが存在するセッションにメッセージチェーンで直接返信します。 - -メッセージチェーンの構築方法については、[メッセージプラットフォームエンティティ](/en/plugin/dev/apis/messages)を参照してください。 - -```python -async def reply( - self, message_chain: platform_message.MessageChain, quote_origin: bool = False -): - """メッセージリクエストに返信する - - Args: - message_chain (platform.types.MessageChain): LangBotメッセージチェーン - quote_origin (bool): 元のメッセージを引用するかどうか - """ - -# 使用例 -await event_context.reply( - platform_message.MessageChain([ - platform_message.Plain(text="Hello, world!"), - ]), -) -``` - -### Bot UUIDの取得 - -現在のリクエストを発行したBotのUUIDを取得します。 - -```python -async def get_bot_uuid(self) -> str: - """Bot UUIDを取得する""" - -# 使用例 -bot_uuid = await event_context.get_bot_uuid() -``` - -### リクエスト変数の設定 - -単一のリクエスト内の一部の情報は、リクエスト変数に保存されます。Difyなどの外部Agent プラットフォームを使用する場合、[これらの変数はAgent プラットフォームに明示的に渡されます](/en/deploy/pipelines/readme.html#request-variables)。 - -```python -async def set_query_var(self, key: str, value: Any): - """クエリ変数を設定する""" - -# 使用例 -await event_context.set_query_var("key", "value") -``` - -### リクエスト変数の取得 - -単一のリクエスト変数を取得します。 - -```python -async def get_query_var(self, key: str) -> Any: - """クエリ変数を取得する""" - -# 使用例 -query_var = await event_context.get_query_var("key") -``` - -### すべてのリクエスト変数の取得 - -```python -async def get_query_vars(self) -> dict[str, Any]: - """すべてのクエリ変数を取得する""" - -# 使用例 -query_vars = await event_context.get_query_vars() -``` - -### 現在のパイプラインに設定されたナレッジベース一覧の取得 - -現在のリクエストが使用するパイプラインに設定されたナレッジベースの一覧を取得します。 - -```python -async def list_pipeline_knowledge_bases(self) -> list[dict[str, Any]]: - """List knowledge bases configured for the current pipeline""" - -# 使用例 -knowledge_bases = await event_context.list_pipeline_knowledge_bases() - -# 返り値の例 -[ - { - "uuid": "kb_uuid", - "name": "Product Docs", - "description": "社内製品ドキュメント", - } -] -``` - -現在のリクエストに対応するパイプラインの `Local Agent` 設定にバインドされたナレッジベースのみを返します。現在のパイプラインにナレッジベースが設定されていない場合、または `Local Agent` を使用していない場合は、空のリストが返されます。 - -### 新しい会話を開始 - -`await event_context.create_new_conversation()` は現在のパイプラインセッションが使用している会話を解除し、後続の処理で新しい会話を開始します。空の辞書を返し、プラットフォームのチャット履歴は削除しません。 - -### パイプラインのナレッジベースを検索 - -`await event_context.retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` は検索結果のリストを返します。`kb_id` は `list_pipeline_knowledge_bases()` で取得します。このリクエスト API ではパイプラインに設定されていないナレッジベースにアクセスできません。 +モデル、ツール、ナレッジベース、ストレージには `self.plugin` を使用します。現在のイベント、返信、会話の状態については[コンテキスト API](/ja/plugin/dev/apis/agent-run) を参照してください。 ## LangBot API diff --git a/ja/plugin/dev/apis/messages.mdx b/ja/plugin/dev/apis/messages.mdx index eff0d072d..90d43f35e 100644 --- a/ja/plugin/dev/apis/messages.mdx +++ b/ja/plugin/dev/apis/messages.mdx @@ -62,4 +62,4 @@ msg_chain = MessageChain([ ## プラットフォーム API -ユーザー取得、グループ管理、固有の操作には[プラットフォーム API](/ja/plugin/dev/apis/platform)を使用してください。Runner は[実行コンテキスト API](/ja/plugin/dev/apis/agent-run)から許可されたツールを呼び出します。 +ユーザー取得、グループ管理、固有の操作には[プラットフォーム API](/ja/plugin/dev/apis/platform)を使用してください。Runner も同じ `self.plugin` を使用し、現在の会話には `ctx.reply()` で返信します。 diff --git a/ja/plugin/dev/apis/pipeline-events.mdx b/ja/plugin/dev/apis/pipeline-events.mdx index d7bd79052..97554f7b5 100644 --- a/ja/plugin/dev/apis/pipeline-events.mdx +++ b/ja/plugin/dev/apis/pipeline-events.mdx @@ -248,42 +248,13 @@ class PromptPreProcessing(BaseEventModel): """この会話の既存メッセージレコード、変更可能、langbot_plugin.api.entities.builtin.provider.message.Messageまたはlangbot_plugin.api.entities.builtin.provider.message.MessageChunk型""" ``` -## イベントコンテキストAPI +## EventListener コンテキスト -```python -... - @self.handler(events.PersonMessageReceived) - async def handler(event_context: context.EventContext): - ... -``` +イベントハンドラーは `event_context.event` から現在のイベントを取得します。`EventContext` の完全なプロパティとメソッドは[コンテキスト API](/ja/plugin/dev/apis/agent-run#eventlistener)を参照してください。 -イベント処理メソッドには`EventContext`オブジェクトが渡され、イベントコンテキスト情報が含まれます。このオブジェクトには[リクエストAPI](/ja/plugin/dev/apis/common)とイベントコンテキスト固有のAPIの両方があります。以下は、イベントコンテキスト固有のAPIのリストです: +### デフォルト動作を阻止 -### 元のイベント属性の取得 - -`event_context.event`属性は元のイベントオブジェクトであり、元のイベントの属性を取得できます。型はリッスンされているイベントの型です。 - -例えば、リッスンされているイベントが`GroupMessageReceived`の場合: - -```python -event = event_context.event -print(event.launcher_type) # 起動元オブジェクトタイプ(group) -print(event.launcher_id) # グループID -print(event.sender_id) # 送信者アカウントID -print(event.message_chain) # メッセージチェーン -``` - -### デフォルト動作を防止 - -```python -def prevent_default(self): - """デフォルト動作を防止する""" - -# 使用例 -event_context.prevent_default() -``` - -このメソッドを呼び出すと、このイベントのデフォルトの後続動作が防止され、パイプラインは直接終了します。 +`event_context.prevent_default()` を呼び出すと、現在のイベントのデフォルト処理が停止します。この操作を利用できるイベントは次のとおりです。 以下のイベントのみデフォルト動作を阻止できます: @@ -297,14 +268,4 @@ event_context.prevent_default() - NormalMessageResponded -### 後続実行を防止 - -```python -def prevent_postorder(self): - """後続実行を防止する""" - -# 使用例 -event_context.prevent_postorder() -``` - -このメソッドを呼び出すと、後続のプラグインがこの時点で実行されなくなります。 \ No newline at end of file +`event_context.prevent_postorder()` を呼び出すと、後続プラグインによる現在のイベント処理を停止できます。 diff --git a/ja/plugin/dev/apis/platform.mdx b/ja/plugin/dev/apis/platform.mdx index 7b797d541..6ba881862 100644 --- a/ja/plugin/dev/apis/platform.mdx +++ b/ja/plugin/dev/apis/platform.mdx @@ -64,15 +64,15 @@ account = await self.plugin.call_platform_api( アダプターが宣言した操作のみ呼び出せます。すべてのプラットフォーム API を任意に転送できるわけではありません。プラグインはランタイム経由でホストを呼び出し、`ctx.query.adapter` や `adapter.bot` には直接アクセスしません。 -## イベントプロセッサーからの呼び出し +## Runner -Runner も `self.plugin` から Bot と宛先を指定して本ページの API を利用できます。現在のイベントの許可範囲で操作し、実行記録に残すには `ctx.api` または `ctx.reply()` を使用します: +Runner の実行中、`self.plugin` によるモデル、ツール、ナレッジベース、ストレージ、プラットフォーム呼び出しは現在の実行と権限に自動的に関連付けられます。`run_id` の指定は不要です。同時実行は互いに分離されます。ハンドラー終了後に実行に紐づく API を呼び続けないでください。 + +現在のボットは `await ctx.get_bot_uuid()` で取得します。プラットフォーム呼び出しは今回のツール権限に従い、イベント対象だけが許可されている場合は別の対象を指定できません。プラットフォーム固有の操作を含む未許可の操作は拒否されます。デバッグではこれらの呼び出しも Mock を使用します。 ```python -tools = {tool.tool_name for tool in ctx.api.get_allowed_tools()} -if "event_get_actor" in tools: - actor = await ctx.api.call_tool("event_get_actor", {}) +tools = await ctx.get_available_tools() +if any(tool["name"] == "event_get_actor" for tool in tools): + actor = await ctx.call_tool("event_get_actor") await ctx.log(str(actor)) ``` - -アクセス方法、権限、返信の例は[実行コンテキスト API](/ja/plugin/dev/apis/agent-run)を参照してください。イベントデバッグのプラットフォームツールは Mock ですが、通常のコンポーネントの `self.plugin.call_platform_api()` は実際の呼び出しです。 diff --git a/ja/plugin/dev/components/command.mdx b/ja/plugin/dev/components/command.mdx index bb6b0dc7d..db9523a2e 100644 --- a/ja/plugin/dev/components/command.mdx +++ b/ja/plugin/dev/components/command.mdx @@ -211,4 +211,5 @@ yield CommandReturn( コマンド登録とコマンド実行の基本情報を学びました。次は: -- [プラグイン共通API](/en/plugin/dev/apis/common)を確認してください +- 完全な [`ExecuteContext` API リファレンス](/ja/plugin/dev/apis/agent-run#command)を確認します。 +- [LangBot API](/ja/plugin/dev/apis/common)を確認します。 diff --git a/ja/plugin/dev/components/event-listener.mdx b/ja/plugin/dev/components/event-listener.mdx index 2e7fdd561..e1cd24f8b 100644 --- a/ja/plugin/dev/components/event-listener.mdx +++ b/ja/plugin/dev/components/event-listener.mdx @@ -103,5 +103,6 @@ class EventContext(pydantic.BaseModel): イベントリスナーの基本情報を学びました。次は: -- ドキュメント[パイプラインイベント](/en/plugin/dev/apis/pipeline-events)で`監視可能なイベントのリスト`と`イベントコンテキストAPI`を確認してください -- [プラグイン共通API](/en/plugin/dev/apis/common)を確認してください +- [パイプラインイベント](/ja/plugin/dev/apis/pipeline-events)でイベントの種類とフィールドを確認します。 +- 完全な [`EventContext` API リファレンス](/ja/plugin/dev/apis/agent-run#eventlistener)を確認します。 +- [LangBot API](/ja/plugin/dev/apis/common)を確認します。 diff --git a/ja/plugin/dev/components/runner.mdx b/ja/plugin/dev/components/runner.mdx index b744566e5..51723b417 100644 --- a/ja/plugin/dev/components/runner.mdx +++ b/ja/plugin/dev/components/runner.mdx @@ -6,6 +6,8 @@ Runner は Agent の実行ロジック、または「プラグインプロセッ 環境の準備は[プラグイン開発チュートリアル](/ja/plugin/dev/tutor)を参照してください。 +`RunnerContext` の完全なフィールドとメソッドは[コンテキスト API](/ja/plugin/dev/apis/agent-run#runner)を参照してください。 + ## Runner コンポーネントの追加 1 つのプラグインに複数のイベントプロセッサーを追加できます。プラグインのディレクトリで `lbp comp Runner` を実行し、名前に `welcome`、説明に `Welcome new members` を入力します。 @@ -147,7 +149,6 @@ async with ctx.reply_stream() as reply: `async with` ブロックが正常終了すると返信が完了します。ストリーミングが未対応・無効の場合やメッセージ以外のイベントでは、最後に全文を一度だけ送信します。例外時には、バッファ内の未完成テキストを送信しません。デバッグでは送信をシミュレーションします。 -権限は `ctx.reply()` と共通です。Runner も `self.get_run_api(ctx).reply_stream()` を利用できます。モデルの出力テキストが自動送信されることはありません。 ## イベントの登録 @@ -189,7 +190,8 @@ async with ctx.reply_stream() as reply: ## 次のステップ +- [`RunnerContext` API リファレンス](/ja/plugin/dev/apis/agent-run#runner)を確認します。 +- モデル、ツール、ナレッジベース、ストレージについては [LangBot API](/ja/plugin/dev/apis/common)を確認します。 +- メッセージ送信などについては[プラットフォーム API](/ja/plugin/dev/apis/platform)を確認します。 - [RunnerDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/RunnerDemo)で他の実装例を確認できます。 - パイプラインの前処理やモデル呼び出し完了時などに機能を追加する場合は、[イベントリスナー](/ja/plugin/dev/components/event-listener)を使用してください。 - -返信、モデル、ツール、履歴、状態は[実行コンテキスト API](/ja/plugin/dev/apis/agent-run)を参照してください。 diff --git a/ja/plugin/dev/components/tool.mdx b/ja/plugin/dev/components/tool.mdx index 1d8825a27..8a9ab8c77 100644 --- a/ja/plugin/dev/components/tool.mdx +++ b/ja/plugin/dev/components/tool.mdx @@ -173,4 +173,5 @@ httpx ツールの基本的な使い方を学びました。次は: -- [プラグイン共通API](/en/plugin/dev/apis/common)を確認してください +- Tool が受け取る [`session` と `query_id`](/ja/plugin/dev/apis/agent-run#tool)を確認します。 +- [LangBot API](/ja/plugin/dev/apis/common)を確認します。 diff --git a/scripts/verify-plugin-api-examples.py b/scripts/verify-plugin-api-examples.py index 550c79282..1f984550a 100644 --- a/scripts/verify-plugin-api-examples.py +++ b/scripts/verify-plugin-api-examples.py @@ -22,6 +22,10 @@ from langbot_plugin.api.entities.builtin.runner.errors import ( ) from langbot_plugin.api.proxies.runner import RunnerAPIProxy from langbot_plugin.api.proxies.langbot_api import LangBotAPIProxy +from langbot_plugin.api.proxies.event_context import EventContextProxy +from langbot_plugin.api.proxies.execute_context import ExecuteContextProxy +from langbot_plugin.api.entities.builtin.provider.session import Session +from langbot_plugin.api.proxies.invocation import bind_invocation from langbot_plugin.api.entities.builtin.provider.message import Message ROOT = Path(__file__).resolve().parents[1] @@ -59,6 +63,8 @@ class Host: "prev_cursor": "older", "next_cursor": None, }, + "reply_message": {}, + "set_query_var": {}, "state_set": {"success": True}, "state_get": {"value": "setup"}, } @@ -118,6 +124,65 @@ def snippets(locale, page): return re.findall(r"^```python\n(.*?)^```", source, re.M | re.S) +def verify_context_reference(): + source = (ROOT / "zh/plugin/dev/apis/agent-run.mdx").read_text() + query_methods = { + "reply", + "get_bot_uuid", + "set_query_var", + "get_query_var", + "get_query_vars", + "create_new_conversation", + "list_pipeline_knowledge_bases", + "retrieve_knowledge", + } + runner_methods = { + name + for name, value in RunnerContext.__dict__.items() + if callable(value) and not name.startswith("_") + } + runner_methods.discard("model_post_init") + + for prefix in ("event_context", "context"): + missing = sorted( + name for name in query_methods if f"{prefix}.{name}(" not in source + ) + assert not missing, f"{prefix} methods missing from context reference: {missing}" + + for name in ("prevent_default", "prevent_postorder"): + assert f"event_context.{name}(" in source + + missing = sorted(name for name in runner_methods if f"ctx.{name}(" not in source) + assert not missing, f"RunnerContext methods missing from context reference: {missing}" + assert "`platform_event`" in source + + required_fields = { + "instance_uuid", + "workspace_uuid", + "placement_generation", + "query_id", + "query_uuid", + "eid", + "event_name", + "event", + "is_prevent_default", + "is_prevent_postorder", + "session", + "command_text", + "full_command_text", + "command", + "crt_command", + "params", + "crt_params", + "privilege", + *RunnerContext.model_fields, + } + missing = sorted(name for name in required_fields if f"`{name}`" not in source) + assert not missing, f"Context fields missing from context reference: {missing}" + assert "spec.permissions" not in source + assert "spec.capabilities" not in source + + async def execute(block, namespace): # Examples are function bodies; signature reference blocks may include stubs. function = "async def example():\n" + "\n".join( @@ -131,6 +196,12 @@ async def execute(block, namespace): cls = ( LangBotAPIProxy if owner == "self.plugin" + else EventContextProxy + if owner == "event_context" + else ExecuteContextProxy + if owner == "context" + else RunnerContext + if owner == "ctx" else RunnerAPIProxy if owner == "api" else None @@ -147,6 +218,7 @@ async def execute(block, namespace): async def main(): + verify_context_reference() total = 0 for page in ("common", "agent-run", "platform"): blocks = snippets("zh", page) @@ -154,9 +226,24 @@ async def main(): assert blocks == snippets(locale, page), f"{page}: {locale} code differs" for block in blocks: host = Host() - api = RunnerAPIProxy(context(), host) - ctx = SimpleNamespace(api=api, log=AsyncMock()) + ctx = context() + api = RunnerAPIProxy(ctx, host) + ctx._api = api + ctx._results = asyncio.Queue() + session = Session(launcher_type="group", launcher_id="docs-group", sender_id="docs-user") namespace = { + "event_context": EventContextProxy.model_construct( + query_id=42, + event=SimpleNamespace(sender_id="docs-user"), + plugin_runtime_handler=host, + ), + "context": ExecuteContextProxy.model_construct( + query_id=43, + plugin_runtime_handler=host, + crt_params=["Hello!"], + session=session, + ), + "session": session, "Any": Any, "Message": Message, "api": api, @@ -171,7 +258,13 @@ async def main(): "bot_uuid": "bot", "print": lambda *args: None, } - await execute(block, namespace) + with bind_invocation(host, api): + await execute(block, namespace) + for name, data in host.calls: + if name in {"reply_message", "set_query_var"}: + expected_query = 42 if "event_context" in block else 43 + assert data["query_id"] == expected_query + assert "run_id" not in data if "reply.update" in block: ops = [ data["operation"] diff --git a/zh/articles/dify-agent-discord-telegram-slack.mdx b/zh/articles/dify-agent-discord-telegram-slack.mdx index eefc7b978..558ba941e 100644 --- a/zh/articles/dify-agent-discord-telegram-slack.mdx +++ b/zh/articles/dify-agent-discord-telegram-slack.mdx @@ -76,9 +76,9 @@ Please always maintain a friendly and professional attitude. If you need to use ![AI Capability Config](/images/articles/external/8f4b3820ed5b-14-langbot-pipeline-ai-capability.png) -选择 **Dify Service API** 作为 Runner: +选择 **Dify Service API** 作为运行器: -![Select Dify Runner](/images/articles/external/efa6fcae9c58-15-langbot-runner-dropdown.png) +![选择 Dify 运行器](/images/articles/external/efa6fcae9c58-15-langbot-runner-dropdown.png) 填写配置: - **Base URL**: `https://api.dify.ai/v1` diff --git a/zh/articles/langflow-drag-and-drop-ai-bot.mdx b/zh/articles/langflow-drag-and-drop-ai-bot.mdx index 3ef8137c2..8f832f693 100644 --- a/zh/articles/langflow-drag-and-drop-ai-bot.mdx +++ b/zh/articles/langflow-drag-and-drop-ai-bot.mdx @@ -120,9 +120,9 @@ http://localhost:7860/flow/{flow-id} ![LangBot Pipeline AI Tab](/images/articles/external/3c7d6bc96738-08-langbot-pipeline-ai-tab.png) -配置 **Runner**,选择 **Langflow API**: +配置**运行器**,选择 **Langflow API**: -![LangBot Runner Dropdown](/images/articles/external/ef9bbef78165-09-langbot-runner-dropdown.png) +![LangBot 运行器下拉菜单](/images/articles/external/ef9bbef78165-09-langbot-runner-dropdown.png) 填写 Langflow 配置: diff --git a/zh/articles/n8n-multi-platform-ai-chatbot.mdx b/zh/articles/n8n-multi-platform-ai-chatbot.mdx index 1b082d3f5..ae848000a 100644 --- a/zh/articles/n8n-multi-platform-ai-chatbot.mdx +++ b/zh/articles/n8n-multi-platform-ai-chatbot.mdx @@ -101,7 +101,7 @@ http://localhost:5678/webhook/{your-webhook-id} ![LangBot pipelines page](/images/articles/external/ef2b2f83bd16-04-langbot-pipelines.png) -切换到 **AI** 标签页,在 Runner 下拉菜单中选择 "n8n Workflow API"。配置: +切换到 **AI** 标签页,在运行器下拉菜单中选择 "n8n Workflow API"。配置: - **Webhook URL**:粘贴你的 n8n 生产环境 webhook URL - **Authentication Type**:None(与 n8n webhook 设置保持一致) diff --git a/zh/plugin/dev/apis/agent-run.mdx b/zh/plugin/dev/apis/agent-run.mdx index 1d983089f..7112ccd97 100644 --- a/zh/plugin/dev/apis/agent-run.mdx +++ b/zh/plugin/dev/apis/agent-run.mdx @@ -1,179 +1,243 @@ --- -title: "运行上下文 API" +title: "上下文 API" --- -Runner 的 `ctx.api` 携带当前运行的 ID、上下文与资源授权;自定义 `run(ctx)` 和事件处理函数使用同一种 `RunnerContext`。`self.plugin` 仍可访问[插件通用 API](/zh/plugin/dev/apis/common),但不会自动代表当前运行。 +上下文保存当前一次组件调用的数据。不同组件接收的上下文不同,请先按组件找到对应入口。 -## 访问方式 +| 组件 | 处理函数入口 | 上下文类型 | +| --- | --- | --- | +| EventListener | `handler(event_context)` | `EventContext` | +| Command | `subcommand(context)` | `ExecuteContext` | +| Tool | `call(params, session, query_id)` | 无独立上下文对象 | +| 运行器 | `run(ctx)` 或 `handler(ctx)` | `RunnerContext` | +| KnowledgeEngine | `ingest(context)`、`retrieve(context)` | `IngestionContext`、`RetrievalContext` | +| KnowledgeRetriever | `retrieve(context)` | `RetrievalContext` | +| Parser | `parse(context)` | `ParseContext` | +| Page | `handle_api(request)` | `PageRequest` | + +本页只介绍上下文中的数据和方法。模型、知识库、存储等 LangBot 能力统一通过 `self.plugin` 调用,见 [LangBot API](/zh/plugin/dev/apis/common);平台操作见[平台 API](/zh/plugin/dev/apis/platform)。 + +`EventContext`、`ExecuteContext` 和 `Session` 还会携带 `instance_uuid`、`workspace_uuid` 和 `placement_generation`。这些字段用于标识当前执行所在的实例和工作区,不应作为插件自行鉴权的依据。 + +## EventListener + +EventListener 处理流水线事件。事件处理函数接收 `EventContext`: ```python -# Inside Runner.run(ctx) or a registered handler -api = ctx.api +event = event_context.event +sender_id = event.sender_id ``` -以下示例中的 `api` 均指当前运行的 API。 除 `get_allowed_*` 同步方法和流式上下文/迭代接口外,下文方法均使用 `await` 调用。不要跨运行保存或复用这个对象。 +### 上下文数据 -### 配置与授权 +| 属性 | 类型 | 说明 | +| --- | --- | --- | +| `event` | `BaseEventModel` | 当前事件对象,实际类型与注册的事件类型一致 | +| `event_name` | `str` | 当前事件类名 | +| `query_id` | `int` | 当前流水线请求 ID | +| `query_uuid` | `str \| None` | 当前流水线请求的稳定标识 | +| `eid` | `int` | Plugin Runtime 内的临时事件编号,不应持久化 | +| `is_prevent_default` | `bool` | 是否已阻止默认处理;通过 `prevent_default()` 修改 | +| `is_prevent_postorder` | `bool` | 是否已阻止后续插件;通过 `prevent_postorder()` 修改 | -`ctx.config` 是处理器或 Runner 实例的配置;`self.get_plugin_config()` 读取插件级配置。清单中的 `spec.permissions` 只声明需要的权限,实际可用资源还取决于用户配置和当前事件。 +事件对象的字段和所有可监听事件见[流水线事件](/zh/plugin/dev/apis/pipeline-events)。 -```yaml -spec: - capabilities: - tool_calling: true - permissions: - tools: [detail, call] +### 上下文方法 + +| 方法 | 返回值 | 说明 | +| --- | --- | --- | +| `await event_context.reply(message_chain, quote_origin=False)` | - | 回复当前请求所在会话 | +| `await event_context.get_bot_uuid()` | `str` | 获取当前请求来源机器人 UUID | +| `await event_context.set_query_var(key, value)` | - | 设置当前请求变量 | +| `await event_context.get_query_var(key)` | `Any` | 获取一个请求变量 | +| `await event_context.get_query_vars()` | `dict[str, Any]` | 获取全部请求变量 | +| `await event_context.create_new_conversation()` | `dict[str, Any]` | 清除流水线当前使用的对话,后续请求会创建新对话 | +| `await event_context.list_pipeline_knowledge_bases()` | `list[dict[str, Any]]` | 列出当前流水线 Local Agent 绑定的知识库 | +| `await event_context.retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` | `list[dict[str, Any]]` | 检索当前流水线已绑定的知识库 | +| `event_context.prevent_default()` | - | 阻止当前事件的默认处理 | +| `event_context.prevent_postorder()` | - | 阻止后续插件继续处理当前事件 | + +`reply()` 使用 [MessageChain](/zh/plugin/dev/apis/messages)。请求变量、回复和流水线知识库方法依赖当前请求;没有关联请求的事件不能调用这些方法。`retrieve_knowledge()` 的 `kb_id` 必须来自 `list_pipeline_knowledge_bases()`。 + +`prevent_default()` 只对支持中止默认流程的流水线事件生效,具体事件见[流水线事件](/zh/plugin/dev/apis/pipeline-events#阻止默认行为)。 + +## Command + +Command 的子命令处理函数接收 `ExecuteContext`: + +```python +query = " ".join(context.crt_params) +session = context.session ``` -这是回复消息和调用工具所需的最小配置。需要其他能力时,在 `spec.permissions` 中按需添加: +### 上下文数据 -| `spec.permissions` | 可选操作 | +| 属性 | 类型 | 说明 | +| --- | --- | --- | +| `session` | `Session` | 当前消息所属会话 | +| `command_text` | `str` | 去掉命令前缀后的完整文本 | +| `full_command_text` | `str` | 包含命令前缀的完整文本 | +| `command` | `str` | 根命令名称 | +| `crt_command` | `str` | 当前正在执行的子命令名称 | +| `params` | `list[str]` | 根命令后的全部参数 | +| `crt_params` | `list[str]` | 当前子命令尚未消费的参数 | +| `privilege` | `int` | 当前用户的命令权限级别 | +| `query_id` | `int` | 当前流水线请求 ID | +| `query_uuid` | `str \| None` | 当前流水线请求的稳定标识 | + +`context.shift()` 由 SDK 在进入当前子命令前调用,组件不需要手动调用。 + +### 上下文方法 + +| 方法 | 返回值 | 说明 | +| --- | --- | --- | +| `await context.reply(message_chain, quote_origin=False)` | - | 回复当前请求所在会话 | +| `await context.get_bot_uuid()` | `str` | 获取当前请求来源机器人 UUID | +| `await context.set_query_var(key, value)` | - | 设置当前请求变量 | +| `await context.get_query_var(key)` | `Any` | 获取一个请求变量 | +| `await context.get_query_vars()` | `dict[str, Any]` | 获取全部请求变量 | +| `await context.create_new_conversation()` | `dict[str, Any]` | 清除流水线当前使用的对话,后续请求会创建新对话 | +| `await context.list_pipeline_knowledge_bases()` | `list[dict[str, Any]]` | 列出当前流水线 Local Agent 绑定的知识库 | +| `await context.retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` | `list[dict[str, Any]]` | 检索当前流水线已绑定的知识库 | + +这些请求方法的行为与 EventListener 相同。命令的注册和返回值见 [Command 组件](/zh/plugin/dev/components/command)。 + +## Tool + +Tool 没有独立的上下文对象。调用信息直接传入 `call()`: + +```python +conversation_type = session.launcher_type.value +conversation_id = session.launcher_id +sender_id = session.sender_id +``` + +| 参数 | 说明 | | --- | --- | -| `models` | `invoke`, `stream`, `rerank`, `count_tokens` | -| `tools` | `detail`, `call` | -| `knowledge_bases` | `list`, `retrieve` | -| `history` | `page`, `search` | -| `events` | `get`, `page` | -| `storage` | `plugin`, `workspace` | -| `interactions` | `request` | +| `params` | 模型或调用方根据工具 JSON Schema 生成的参数 | +| `session` | 当前会话数据 | +| `query_id` | 当前流水线请求 ID;调用需要请求上下文的 LangBot API 时原样传入 | -查询实际授权的资源: +`Session` 的常用字段如下: -```python -models = api.get_allowed_models() # Each item has model_id and operations -tools = api.get_allowed_tools() # Each item has tool_name and parameters -knowledge_bases = api.get_allowed_knowledge_bases() # Each item has kb_id -``` - -上下文能力可通过 `api.ctx.context.available_apis` 检查。某项为 `False` 时不要调用对应接口;只在清单中声明权限不会自动启用它。 - -## 回复与日志 - -| Runner 方法 | 用途 | +| 属性 | 说明 | | --- | --- | -| `await ctx.reply(text)` | 向当前事件的会话发送文本,返回动作结果。 | -| `await ctx.log(text, level="info")` | 记录日志;级别为 `debug`、`info`、`warning`、`error`。 | -| `ctx.reply_stream()` | 以异步上下文管理器更新同一条回复。 | +| `launcher_type` | 会话类型:`person` 或 `group` | +| `launcher_id` | 私聊用户或群组 ID | +| `sender_id` | 当前消息发送者 ID | +| `bot_uuid` | 当前机器人 UUID | +| `using_conversation` | 当前流水线会话 | +| `conversations` | 该 Session 保存的流水线会话列表 | +| `use_prompt_name` | 当前使用的提示词名称 | +| `create_time`、`update_time` | Session 的创建时间和更新时间 | -`ctx.reply(text)` 等价于 `await api.call_tool("event_reply", {"text": text})`。需要当前事件具有回复目标,并且 `event_reply` 已被授权。日志和独立 Agent 返回的文本不会自动发送到平台。 +`session` 只保存数据,不提供 `reply()` 等上下文方法。工具的返回值交给调用方,不会自动发送到聊天平台。需要调用 LangBot 或平台能力时使用 `self.plugin`;相关接口要求 `session` 和 `query_id` 时,传入本次调用收到的值。 -### 流式回复 + + +## 运行器 + +SDK `Runner` 类型的自定义 `run(ctx)` 和事件处理函数都接收 `RunnerContext`。该上下文只在本次运行期间有效。 ```python -async with api.reply_stream() as reply: - await reply.update("Processing…") - await reply.update("Processing complete.") -result = reply.result +text = ctx.input.to_text() +event_type = ctx.event.event_type ``` -Runner 也可使用 `ctx.reply_stream()`,它会同时记录动作的开始与结果。 +### 上下文数据 -`update(text)` 接收**当前完整文本**,不是新增片段;返回本次动作结果。正常退出时完成回复。支持流式且开启流式回复的消息事件会更新平台消息;其他可回复事件会缓存文本,在成功结束时发送一次。异常或取消时丢弃缓存中的未完成文本,已显示的内容不保证能撤回。调试模式仅模拟平台发送。 +| 属性 | 类型 | 说明 | +| --- | --- | --- | +| `run_id` | `str` | 本次运行 ID | +| `trigger` | `AgentTrigger` | 触发类型、来源和时间 | +| `event` | `AgentEventContext` | 标准事件信封,包含事件 ID、类型、来源、时间和原始数据 | +| `platform_event` | 平台事件类型 | 按事件类型解析后的平台事件对象 | +| `conversation` | `ConversationContext \| None` | 当前会话、线程、机器人和工作区标识 | +| `actor` | `ActorContext \| None` | 事件发起者 | +| `subject` | `SubjectContext \| None` | 事件所作用的消息、群组等对象 | +| `input` | `AgentInput` | 当前输入的文本、结构化内容和附件 | +| `delivery` | `DeliveryContext` | 当前输出目标及流式、编辑、回应等能力 | +| `resources` | `AgentResources` | 本次运行已授权的模型、工具、知识库、技能和存储 | +| `context` | `ContextAccess` | 会话游标、内联范围和可用上下文 API | +| `state` | `AgentRunState` | 本次运行开始时读取到的各作用域状态快照 | +| `runtime` | `AgentRuntimeContext` | LangBot 版本、追踪 ID 和截止时间 | +| `config` | `dict[str, Any]` | 当前 Agent 或插件处理器实例配置 | +| `adapter` | `AdapterContext \| None` | 入口适配器附加数据 | +| `metadata` | `dict[str, Any]` | 宿主提供的其他元数据 | -此接口需要 `available_apis.reply_stream` 和 `event_reply` 权限。模型的流式输出与平台的流式发送是两个接口,需要插件主动连接。 +优先使用上表中的稳定字段。`event.data` 和 `adapter.extra` 用于承载尚未进入稳定字段的附加信息。 -## 模型、工具与知识库 +### 回复与日志 -模型调用的参数和用量结构见[通用 API](/zh/plugin/dev/apis/common)。运行 API 还提供以下方法: +| 方法 | 返回值 | 说明 | +| --- | --- | --- | +| `await ctx.get_bot_uuid()` | `str` | 获取来源机器人 UUID;无关联机器人时抛出异常 | +| `await ctx.reply(message_chain, quote_origin=False)` | `Any` | 回复当前事件,支持字符串或 `MessageChain` | +| `ctx.reply_stream()` | 异步上下文管理器 | 流式更新同一条回复 | +| `await ctx.log(text, level="info")` | - | 写入本次运行日志;级别支持 `debug`、`info`、`warning`、`error` | -| 方法 | 返回或用途 | -| --- | --- | -| `invoke_llm(llm_model_uuid, messages, ...)` | `Message` | -| `invoke_llm_with_usage(llm_model_uuid, messages, ...)` | `LLMInvokeResult` | -| `invoke_llm_stream(llm_model_uuid, messages, ...)` | 异步迭代 `MessageChunk` | -| `invoke_llm_stream_events(llm_model_uuid, messages, ...)` | 异步迭代 `LLMStreamEvent` | -| `count_tokens(llm_model_uuid, messages, funcs=None, extra_args=None, timeout=None)` | 模型输入 token 数,`int` | -| `invoke_rerank(rerank_model_uuid, query, documents, top_k=None, extra_args=None, timeout=30.0)` | 带 `index`、`relevance_score` 的列表 | -| `get_tool_detail(tool_name)` | 工具说明与参数 schema,`dict` | -| `call_tool(tool_name, parameters)` | 工具结果,`dict`;无需 `session`、`query_id` | -| `retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` | 检索结果列表 | +`reply_stream()` 的 `update(text)` 接收当前完整文本。平台不支持流式时,LangBot 会在结束时发送一条完整消息;调试模式只模拟发送。 -四个模型调用方法还支持 `remove_think=None`;非流式调用支持 `timeout=None`。调用超时受本次运行的剩余时间约束。资源列表可能为空,不能直接假定存在第一个模型或知识库。 +### 可用工具 -```python -allowed = {tool.tool_name for tool in api.get_allowed_tools()} -if "event_get_actor" in allowed: - actor = await api.call_tool("event_get_actor", {}) -``` +| 方法 | 返回值 | 说明 | +| --- | --- | --- | +| `await ctx.get_available_tools()` | `list[dict[str, Any]]` | 获取本次实际允许调用的上下文动作、平台 API、插件工具和 MCP 工具 | +| `await ctx.call_tool(tool_name, parameters=None)` | `dict[str, Any]` | 调用上述列表中的工具 | -`event_*` 工具由 LangBot 固定事件中的目标,`platform_*` 工具允许按 schema 指定目标。使用实际授权列表中的名称与参数;无参数工具传 `{}`。 +`get_available_tools()` 的每一项包含 `name`、`description`、`parameters`、`type` 和 `operations`。返回结果已经按当前 Agent 或插件处理器的配置和事件能力过滤;不在列表中的工具不能通过 `ctx.call_tool()` 调用。 -## 历史与事件 +### 提示词、历史与事件 -当前输入不包含完整历史。按需读取当前运行范围内的记录;不能通过传入其他会话 ID 扩大访问范围。 +| 方法 | 返回值 | 可用性字段 | +| --- | --- | --- | +| `await ctx.get_prompt()` | `list[dict[str, Any]]` | `prompt_get` | +| `await ctx.history_page(conversation_id=None, before_cursor=None, after_cursor=None, limit=50, direction="backward", include_attachments=False)` | `HistoryPage` | `history_page` | +| `await ctx.history_search(query, filters=None, top_k=10)` | `HistorySearchResult` | `history_search` | +| `await ctx.event_get(event_id)` | `AgentEventRecord` | `event_get` | +| `await ctx.event_page(conversation_id=None, event_types=None, before_cursor=None, limit=50)` | `EventPage` | `event_page` | +| `await ctx.steering_pull(mode="all", limit=None)` | `SteeringPullResult` | `steering_pull` | -| 方法 | 返回值 | -| --- | --- | -| `get_prompt()` | 生效的提示词列表;能力标识为 `prompt_get` | -| `history_page(conversation_id=None, before_cursor=None, after_cursor=None, limit=50, direction="backward", include_attachments=False)` | `HistoryPage` | -| `history_search(query, filters=None, top_k=10)` | `HistorySearchResult` | -| `event_get(event_id)` | `AgentEventRecord` | -| `event_page(conversation_id=None, event_types=None, before_cursor=None, limit=50)` | `EventPage` | -| `steering_pull(mode="all", limit=None)` | `SteeringPullResult`;读取运行中排队的追加输入 | +### 状态 -```python -if api.ctx.context.available_apis.history_page: - page = await api.history_page(limit=20) - for item in page.items: - print(item.model_dump()) - if page.has_more and page.prev_cursor: - older = await api.history_page(before_cursor=page.prev_cursor, limit=20) -``` +| 方法 | 返回值 | 说明 | +| --- | --- | --- | +| `await ctx.state_get(scope, key)` | `dict[str, Any]` | 读取状态值 | +| `await ctx.state_set(scope, key, value)` | `dict[str, Any]` | 写入可 JSON 序列化的状态值 | +| `await ctx.state_delete(scope, key)` | `dict[str, Any]` | 删除状态值 | +| `await ctx.state_list(scope, prefix=None, limit=100)` | `dict[str, Any]` | 列出指定作用域的状态键 | -分页结果是 SDK 对象,使用 `.items`、`.has_more`、`.prev_cursor`、`.next_cursor`,不是字典下标。读取旧记录使用 `before_cursor`;读取新记录使用 `after_cursor` 和 `direction="forward"`。历史搜索可能只提供文本匹配。`steering_pull` 的 `mode` 可为 `all`、`one` 或 `one-at-a-time`,用于支持追加输入的 Runner。 +`scope` 支持 `conversation`、`actor`、`subject` 和 `runner`。这些方法要求 `ctx.context.available_apis.state` 为 `true`。 -## 状态与存储 +### 运行记录 -| 方法 | 返回值 | -| --- | --- | -| `state_get(scope, key)` | `{"value": ...}` | -| `state_set(scope, key, value)` | `{"success": ...}` | -| `state_delete(scope, key)` | `{"success": ...}` | -| `state_list(scope, prefix=None, limit=100)` | `{"keys": [...], "has_more": ...}` | +| 方法 | 返回值 | 可用性字段 | +| --- | --- | --- | +| `await ctx.run_get(run_id=None)` | `AgentRun` | `run_get` | +| `await ctx.run_list(conversation_id=None, statuses=None, before_cursor=None, limit=50)` | `RunPage` | `run_list` | +| `await ctx.run_events_page(run_id=None, before_cursor=None, after_cursor=None, limit=50, direction="forward")` | `RunEventPage` | `run_events_page` | +| `await ctx.run_cancel(run_id=None, reason=None)` | `AgentRun` | `run_cancel` | +| `await ctx.run_append_result(result)` | `AgentRunEvent` | `run_append_result` | +| `await ctx.run_finalize(run_id=None, status=None, reason=None)` | `AgentRun` | `run_finalize` | -状态需要 `available_apis.state` 和宿主允许的作用域:`conversation`(当前会话)、`actor`(当前用户)、`subject`(当前群组或对象)、`runner`(Runner 实例)。值必须可 JSON 序列化,建议使用 `external.` 等键前缀。状态由 LangBot 持久化,接口返回字典。 +`run_id=None` 表示当前运行。普通事件处理函数结束后,SDK 会自动完成本次运行;自定义 `run(ctx)` 通过 `yield RunnerResult` 返回结果时,也不需要再调用 `run_append_result()` 重复登记。 -```python -if api.ctx.context.available_apis.state: - await api.state_set("conversation", "external.last_topic", "setup") - value = await api.state_get("conversation", "external.last_topic") -``` +### 可用性与授权 -插件和工作空间存储使用 `set_plugin_storage`、`get_plugin_storage`、`get_plugin_storage_keys`、`delete_plugin_storage`,以及对应的 `*_workspace_storage*` 方法。参数与[通用 API](/zh/plugin/dev/apis/common)一致,值为 `bytes`,分别受 `api.ctx.resources.storage.plugin_storage` 和 `workspace_storage` 授权限制。 +通过 `ctx.context.available_apis` 判断提示词、历史、事件、状态和运行记录接口是否可用。模型、工具和知识库的授权范围分别位于 `ctx.resources.models`、`ctx.resources.tools` 和 `ctx.resources.knowledge_bases`。 -## 运行记录 +运行器执行期间,通过 `self.plugin` 发起的模型、工具、知识库、存储和平台调用会自动关联当前运行,并按 `ctx.resources` 再次校验,无需传入 `run_id`。处理函数结束后,不要继续在后台任务中使用本次上下文或这些运行关联调用。 -以下接口按同名 `available_apis` 标识检查能力,只能操作当前授权范围内的运行。 +`ctx.api` 是底层的运行级代理。普通组件代码应优先使用本页列出的 `ctx` 方法,并通过 `self.plugin` 调用 LangBot API。 -| 方法 | 返回值 | -| --- | --- | -| `run_get(run_id=None)` | `AgentRun` | -| `run_list(conversation_id=None, statuses=None, before_cursor=None, limit=50)` | `RunPage` | -| `run_events_page(run_id=None, before_cursor=None, after_cursor=None, limit=50, direction="forward")` | `RunEventPage` | -| `run_cancel(run_id=None, reason=None)` | `AgentRun` | -| `run_append_result(result)` | `AgentRunEvent` | -| `run_finalize(run_id=None, status=None, reason=None)` | `AgentRun` | +## 数据型上下文 -`run_id=None` 表示当前运行。事件处理函数返回后自动结束;自定义 `run(ctx)` 可通过 `yield RunnerResult` 上报结果,不要再用 `run_append_result` 重复登记。 +以下组件收到的是本次任务的输入数据,不提供 EventListener 或运行器的上下文方法。字段、返回值和完整示例放在对应组件教程中。 -## 结构化交互(Runner) - -需要用户确认或填写表单时,通过 `yield RunnerResult.interaction_requested(ctx.run_id, request)` 请求交互。`request` 可以是 `InteractionRequest` 对象或字典,必填字段为 `interaction_id`、`title`、`fallback_text`;可选字段有 `kind`(`form`、`confirmation`、`choice`)、`fields`、`actions`、`expires_at`。 - -清单需声明 `spec.capabilities.interactions: true` 和 `spec.permissions.interactions: [request]`,并获得宿主授权。界面支持情况见 `ctx.delivery.interactions`;提交结果以 `interaction.submitted` 事件返回。这是 Runner 的结果事件,不是 `ctx.api` 方法,Runner 的普通处理函数不直接提供此接口。 - -## 错误处理 - -```python -from langbot_plugin.api.proxies.runner import PermissionDeniedError -from langbot_plugin.api.entities.builtin.runner.errors import AgentAPIException - -try: - result = await api.call_tool("event_get_actor", {}) -except PermissionDeniedError as exc: - await ctx.log(str(exc), level="warning") -except AgentAPIException as exc: - await ctx.log(f"{exc.error.code}: {exc.error.message}", level="error") -``` - -这是 Runner 中的示例。`PermissionDeniedError` 表示资源、操作或能力未授权;`AgentAPIException.error` 包含 `code`、`message`、`retryable`、`details`。发送消息等动作超时后,结果可能不确定,不应仅依据 `retryable` 自动重发。 +| 组件入口 | 主要数据 | 组件教程 | +| --- | --- | --- | +| `KnowledgeEngine.ingest(context: IngestionContext)` | 文件、目标知识库、创建配置和解析结果 | [KnowledgeEngine](/zh/plugin/dev/components/knowledge-engine#文档摄取) | +| `KnowledgeEngine.retrieve(context: RetrievalContext)` | 查询、目标知识库、检索配置和过滤条件 | [KnowledgeEngine](/zh/plugin/dev/components/knowledge-engine#知识检索) | +| `KnowledgeRetriever.retrieve(context: RetrievalContext)` | 查询和外部知识库配置 | [KnowledgeRetriever](/zh/plugin/dev/components/knowledge-retriever#检索上下文) | +| `Parser.parse(context: ParseContext)` | 文件内容、文件名、MIME 类型和元数据 | [Parser](/zh/plugin/dev/components/parser#解析方法) | +| `Page.handle_api(request: PageRequest)` | 接口路径、HTTP 方法、请求体和请求头 | [Page](/zh/plugin/dev/components/page#pagerequest-字段) | diff --git a/zh/plugin/dev/apis/common.mdx b/zh/plugin/dev/apis/common.mdx index cd8bd3ca8..9c83d2037 100644 --- a/zh/plugin/dev/apis/common.mdx +++ b/zh/plugin/dev/apis/common.mdx @@ -1,126 +1,15 @@ --- -title: "插件通用 API" +title: "LangBot API" --- -LangBot 为插件提供了一系列 API ,用于操作 LangBot 各个模块、控制消息上下文等。 - -所有组件都可通过 `self.plugin` 使用本页的 LangBot API。Runner 的 `ctx.api` 则携带当前运行的上下文和资源授权,见[运行上下文 API](/zh/plugin/dev/apis/agent-run)。指定机器人和目标的平台操作见[平台 API](/zh/plugin/dev/apis/platform)。 - -## 请求 API - -操作当前正在处理的用户请求(消息)。仅在`EventListener`和`Command`组件中可用。访问方式如下: - -- `EventListener` 的各个事件处理器方法中:`event_context: context.EventContext`对象内部方法 -- `Command` 的各个子命令处理器方法中:`context: ExecuteContext`对象内部方法 - -### 直接回复消息 - -直接回复一个消息链到当前请求所在会话。 - -消息链构造方式请参考[消息平台实体](/zh/plugin/dev/apis/messages)。 - -```python -async def reply( - self, message_chain: platform_message.MessageChain, quote_origin: bool = False -): - """Reply to the message request - - Args: - message_chain (platform.types.MessageChain): LangBot message chain - quote_origin (bool): Whether to quote the original message - """ - -# 调用示例 -await event_context.reply( - platform_message.MessageChain([ - platform_message.Plain(text="Hello, world!"), - ]), -) -``` - -### 获取机器人 UUID - -获取当前请求来源机器人 UUID。 - -```python -async def get_bot_uuid(self) -> str: - """Get the bot uuid""" - -# 调用示例 -bot_uuid = await event_context.get_bot_uuid() -``` - -### 设置请求变量 - -单次请求中某些信息会被存储到请求变量中,在使用 Dify 等外部 Agent 平台时,[这些变量会显式传入 Agent 平台](/zh/deploy/pipelines/readme.html#%E8%AF%B7%E6%B1%82%E5%8F%98%E9%87%8F)。 - -```python -async def set_query_var(self, key: str, value: Any): - """Set a query variable""" - -# 调用示例 -await event_context.set_query_var("key", "value") -``` - -### 获取请求变量 - -获取单个请求变量。 - -```python -async def get_query_var(self, key: str) -> Any: - """Get a query variable""" - -# 调用示例 -query_var = await event_context.get_query_var("key") -``` - -### 获取所有请求变量 - -```python -async def get_query_vars(self) -> dict[str, Any]: - """Get all query variables""" - -# 调用示例 -query_vars = await event_context.get_query_vars() -``` - -### 获取当前流水线配置的知识库列表 - -获取当前请求所使用流水线中配置的知识库列表。 - -```python -async def list_pipeline_knowledge_bases(self) -> list[dict[str, Any]]: - """List knowledge bases configured for the current pipeline""" - -# 调用示例 -knowledge_bases = await event_context.list_pipeline_knowledge_bases() - -# 返回示例 -[ - { - "uuid": "kb_uuid", - "name": "Product Docs", - "description": "内部产品文档", - } -] -``` - -仅返回当前请求对应流水线在 `Local Agent` 配置中绑定的知识库。若当前流水线未配置知识库,或未使用 `Local Agent`,则返回空列表。 - -### 开始新对话 - -`await event_context.create_new_conversation()` 清除当前流水线会话正在使用的对话,后续处理会重新建立对话。返回空字典,不会清除平台聊天记录。 - -### 检索当前流水线的知识库 - -`await event_context.retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` 返回检索结果列表。`kb_id` 必须来自 `list_pipeline_knowledge_bases()`;不能用这个请求接口访问流水线未配置的知识库。 +模型、工具、知识库和存储等能力通过 `self.plugin` 访问。当前事件、回复和会话状态的访问方式见[上下文 API](/zh/plugin/dev/apis/agent-run)。 ## LangBot API -这些 API 可在提供 `self.plugin` 的普通组件中调用。访问方式如下: +这些 API 可在所有组件中通过 `self.plugin` 调用。访问方式如下: - 插件根目录`main.py`中:`self` 对象内部方法,这些 API 均由插件类父类 `BasePlugin` 提供。 -- 普通组件类中:`self.plugin` 对象内部方法。 +- 组件类中:`self.plugin` 对象内部方法。 ### 获取插件配置信息 diff --git a/zh/plugin/dev/apis/messages.mdx b/zh/plugin/dev/apis/messages.mdx index 31d053065..5bce63754 100644 --- a/zh/plugin/dev/apis/messages.mdx +++ b/zh/plugin/dev/apis/messages.mdx @@ -62,4 +62,4 @@ msg_chain = MessageChain([ ## 平台 API -查询用户、管理群组和调用平台专有接口,请使用[平台 API](/zh/plugin/dev/apis/platform)。Runner 通过[运行上下文 API](/zh/plugin/dev/apis/agent-run)调用已授权的平台工具。 +查询用户、管理群组和调用平台专有接口,请使用[平台 API](/zh/plugin/dev/apis/platform)。运行器使用相同的 `self.plugin` 入口,当前会话回复使用 `ctx.reply()`。 diff --git a/zh/plugin/dev/apis/pipeline-events.mdx b/zh/plugin/dev/apis/pipeline-events.mdx index 0b3dcd08c..89903dfc1 100644 --- a/zh/plugin/dev/apis/pipeline-events.mdx +++ b/zh/plugin/dev/apis/pipeline-events.mdx @@ -259,42 +259,13 @@ class PromptPreProcessing(BaseEventModel): """此对话现有消息记录,可修改,langbot_plugin.api.entities.builtin.provider.message.Message 或 langbot_plugin.api.entities.builtin.provider.message.MessageChunk 类型""" ``` -## 事件上下文 API +## EventListener 上下文 -```python -... - @self.handler(events.PersonMessageReceived) - async def handler(event_context: context.EventContext): - ... -``` - -事件处理方法将被传入`EventContext`对象,该对象包含事件的上下文信息,该对象同时具有[请求 API](/zh/plugin/dev/apis/common)和事件上下文特定 API,以下为事件上下文特定 API 列表: - -### 获取原事件属性 - -`event_context.event` 属性为原事件对象,可获取原事件的属性。其类型即为监听的事件类型。 - -例如:当监听的事件是`GroupMessageReceived`时: - -```python -event = event_context.event -print(event.launcher_type) # 发起对象类型(group) -print(event.launcher_id) # 群号 -print(event.sender_id) # 发送者账号 ID -print(event.message_chain) # 消息链 -``` +事件处理函数通过 `event_context.event` 读取当前事件。`EventContext` 的完整属性和方法见[上下文 API](/zh/plugin/dev/apis/agent-run#eventlistener)。 ### 阻止默认行为 -```python -def prevent_default(self): - """阻止默认行为""" - -# 调用示例 -event_context.prevent_default() -``` - -调用该方法后,该事件后续默认行为将被阻止,流水线将直接结束。 +调用 `event_context.prevent_default()` 后,当前事件的默认处理将停止。仅以下事件支持此操作: 仅以下事件可阻止默认行为: @@ -308,14 +279,4 @@ event_context.prevent_default() - NormalMessageResponded -### 阻止后序插件执行 - -```python -def prevent_postorder(self): - """阻止后序插件执行""" - -# 调用示例 -event_context.prevent_postorder() -``` - -调用该方法后,注册了该事件的后序插件此次将不被执行。 \ No newline at end of file +调用 `event_context.prevent_postorder()` 可阻止后续插件继续处理当前事件。 diff --git a/zh/plugin/dev/apis/platform.mdx b/zh/plugin/dev/apis/platform.mdx index 98e1c3919..731c38507 100644 --- a/zh/plugin/dev/apis/platform.mdx +++ b/zh/plugin/dev/apis/platform.mdx @@ -64,15 +64,15 @@ account = await self.plugin.call_platform_api( 只能调用适配器已声明的动作,并非任意平台接口都能透传。插件通过运行时调用宿主,不直接访问 `ctx.query.adapter` 或 `adapter.bot`。 -## 在事件处理器中调用 +## 运行器 -Runner 也可以通过 `self.plugin` 调用本页的平台 API,显式指定机器人和目标。若要按当前事件的授权查询或回复,并在运行记录中展示动作,请使用 `ctx.api` 或 `ctx.reply()`: +运行器执行期间,`self.plugin` 的模型、工具、知识库、存储和平台调用自动关联当前运行并执行授权检查,无需传入 `run_id`。同一插件的并发运行互相隔离。不要在处理函数返回后继续调用这些运行关联的 API。 + +通过 `await ctx.get_bot_uuid()` 获取当前机器人。平台调用必须匹配本次工具授权:仅授权当前事件目标时,不能改传其他目标;未授权的动作(包括平台专有动作)会被拒绝。调试中的这些调用同样使用 Mock。 ```python -tools = {tool.tool_name for tool in ctx.api.get_allowed_tools()} -if "event_get_actor" in tools: - actor = await ctx.api.call_tool("event_get_actor", {}) +tools = await ctx.get_available_tools() +if any(tool["name"] == "event_get_actor" for tool in tools): + actor = await ctx.call_tool("event_get_actor") await ctx.log(str(actor)) ``` - -访问方式、权限与回复示例见[运行上下文 API](/zh/plugin/dev/apis/agent-run)。事件调试中的平台工具使用 Mock;普通组件的 `self.plugin.call_platform_api()` 是实际调用。 diff --git a/zh/plugin/dev/components/add.mdx b/zh/plugin/dev/components/add.mdx index 580fd67a0..cc1345b3b 100644 --- a/zh/plugin/dev/components/add.mdx +++ b/zh/plugin/dev/components/add.mdx @@ -4,8 +4,8 @@ title: "添加组件" 插件由一种或多种组件构成,为 LangBot 提供不同的功能。目前支持的组件类型有: -- 事件处理器(Runner):处理机器人收到的消息、成员入群等事件。详见[开发教程](/zh/plugin/dev/components/runner)。 -- Agent 运行器(Runner):为 Agent 提供模型调用、外部 Agent 服务连接等运行方式。 +- 插件处理器(运行器):处理机器人收到的消息、成员入群等事件。详见[开发教程](/zh/plugin/dev/components/runner)。 +- Agent 使用的运行器:为 Agent 提供模型调用、外部 Agent 服务连接等运行方式。 - 事件监听器(EventListener):监听流水线执行期间的事件,对上下文或流水线进行修改。 - 命令(Command):由用户通过`!`(或其他已设置的前缀)开头的命令消息触发。 - 工具(Tool):供 LangBot 内置的 Local Agent 在执行期间由 LLM 调用。 @@ -73,7 +73,7 @@ components: 各个组件的详细使用方式请参考: -- [Runner](./runner) +- [运行器](./runner) - [EventListener](./event-listener) - [Command](./command) - [Tool](./tool) diff --git a/zh/plugin/dev/components/command.mdx b/zh/plugin/dev/components/command.mdx index 816d03c67..7857c7a32 100644 --- a/zh/plugin/dev/components/command.mdx +++ b/zh/plugin/dev/components/command.mdx @@ -211,4 +211,5 @@ yield CommandReturn( 您已经了解了命令注册和命令执行的基本信息,接下来可以: -- 查看[插件通用 API](/zh/plugin/dev/apis/common) +- 查看完整的 [`ExecuteContext` 上下文 API](/zh/plugin/dev/apis/agent-run#command) +- 查看 [LangBot API](/zh/plugin/dev/apis/common) diff --git a/zh/plugin/dev/components/event-listener.mdx b/zh/plugin/dev/components/event-listener.mdx index eea6cee58..5e1847348 100644 --- a/zh/plugin/dev/components/event-listener.mdx +++ b/zh/plugin/dev/components/event-listener.mdx @@ -103,5 +103,6 @@ class EventContext(pydantic.BaseModel): 您已经了解了事件监听器的基本信息,接下来可以: -- 在文档[流水线事件](/zh/plugin/dev/apis/pipeline-events)中查看`可监听的事件列表`和`事件上下文 API` -- 查看[插件通用 API](/zh/plugin/dev/apis/common) +- 在[流水线事件](/zh/plugin/dev/apis/pipeline-events)中查看可监听的事件及字段 +- 查看完整的 [`EventContext` 上下文 API](/zh/plugin/dev/apis/agent-run#eventlistener) +- 查看 [LangBot API](/zh/plugin/dev/apis/common) diff --git a/zh/plugin/dev/components/runner.mdx b/zh/plugin/dev/components/runner.mdx index 4894288c4..681199f66 100644 --- a/zh/plugin/dev/components/runner.mdx +++ b/zh/plugin/dev/components/runner.mdx @@ -1,12 +1,14 @@ --- -title: "组件:Runner" +title: "组件:运行器" --- -Runner 组件用于实现 Agent 的运行逻辑,也可以按事件注册处理函数,供「插件处理器」使用。两种写法共用 `RunnerContext`,插件安装后由用户选择组件并配置。 +运行器组件通过 SDK 的 `Runner` 类型实现 Agent 的运行逻辑,也可以按事件注册处理函数,供「插件处理器」使用。两种写法共用 `RunnerContext`,插件安装后由用户选择组件并配置。 环境准备见[插件开发教程](/zh/plugin/dev/tutor)。 -## 添加 Runner 组件 +`RunnerContext` 的完整字段和方法见[上下文 API](/zh/plugin/dev/apis/agent-run#runner)。 + +## 添加运行器组件 单个插件中可以添加多个事件处理器。请在插件目录执行命令 `lbp comp Runner`,名称填写 `welcome`,描述填写 `Welcome new members`。 @@ -33,7 +35,7 @@ class Echo(Runner): ) ``` -## 清单文件:Runner +## 清单文件:运行器 将 `welcome.yaml` 修改为: @@ -147,7 +149,6 @@ async with ctx.reply_stream() as reply: 正常退出 `async with` 时完成回复。平台不支持流式、未开启流式回复或当前不是消息事件时,会在结束后发送一条完整消息;发生异常时不发送缓存中的未完成文本。调试模式仅模拟发送。 -此接口沿用 `ctx.reply()` 的权限。Runner 可通过 `self.get_run_api(ctx).reply_stream()` 使用同一接口,模型返回文本仍不会自动发送。 ## 事件注册 @@ -189,7 +190,8 @@ async with ctx.reply_stream() as reply: ## 接下来做什么 +- 查看 [`RunnerContext` 上下文 API](/zh/plugin/dev/apis/agent-run#runner)。 +- 查看模型、工具、知识库和存储相关的 [LangBot API](/zh/plugin/dev/apis/common)。 +- 查看消息发送等[平台 API](/zh/plugin/dev/apis/platform)。 - 查看 [RunnerDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/RunnerDemo) 中的更多示例。 - 如果要扩展流水线的预处理、模型调用完成等步骤,请使用[事件监听器](/zh/plugin/dev/components/event-listener)。 - -回复、模型、工具、历史与状态接口见[运行上下文 API](/zh/plugin/dev/apis/agent-run)。 diff --git a/zh/plugin/dev/components/tool.mdx b/zh/plugin/dev/components/tool.mdx index 6900aae31..3d93be191 100644 --- a/zh/plugin/dev/components/tool.mdx +++ b/zh/plugin/dev/components/tool.mdx @@ -174,4 +174,5 @@ httpx 您已经了解了工具的基本用法,接下来可以: -- 查看[插件通用 API](/zh/plugin/dev/apis/common) +- 查看 Tool 的 [`session` 和 `query_id` 说明](/zh/plugin/dev/apis/agent-run#tool) +- 查看 [LangBot API](/zh/plugin/dev/apis/common) diff --git a/zh/usage/mcp/readme.mdx b/zh/usage/mcp/readme.mdx index 2b5beb89d..fed64e4fb 100644 --- a/zh/usage/mcp/readme.mdx +++ b/zh/usage/mcp/readme.mdx @@ -23,7 +23,7 @@ LangBot 支持调用来自 MCP 服务器的工具。添加 MCP 后,启用了 -MCP 工具需要流水线使用**内置 Agent**,并选择**已开启工具使用能力**的模型——见[模型](../models/readme)。Dify、n8n、Langflow、Coze 等外部 Runner 使用各自平台的工具机制。 +MCP 工具需要流水线使用**内置 Agent**,并选择**已开启工具使用能力**的模型——见[模型](../models/readme)。Dify、n8n、Langflow、Coze 等外部运行器使用各自平台的工具机制。 ## 安装 MCP 服务器 diff --git a/zh/usage/pipelines/tbox.mdx b/zh/usage/pipelines/tbox.mdx index 244ac0372..96e1a7e32 100644 --- a/zh/usage/pipelines/tbox.mdx +++ b/zh/usage/pipelines/tbox.mdx @@ -5,7 +5,7 @@ description: "在 LangBot 中使用 TBox,为 AI 机器人对话管线提供支 [TBox(百宝箱)](https://tbox.cn/)是蚂蚁集团推出的面向 AI 开发者的一站式智能体应用开发平台。在平台上,无论您是否拥有编程基础,都可以通过自然语言,基于各种大模型搭建属于您自己的智能体应用,并将其发布到支付宝小程序、web 服务、浏览器插件等生态渠道。 -LangBot 支持接入 TBox 上编排好的智能体,用作流水线 Runner。 +LangBot 支持接入 TBox 上编排好的智能体,用作流水线运行器。 在 TBox 上搭建好智能体后,请到[`开放平台`](https://www.tbox.cn/open/authorized-management)创建 API Key: diff --git a/zh/usage/sandbox/readme.mdx b/zh/usage/sandbox/readme.mdx index 089941c91..18b368100 100644 --- a/zh/usage/sandbox/readme.mdx +++ b/zh/usage/sandbox/readme.mdx @@ -21,7 +21,7 @@ Box Runtime 给内置 Agent 一个统一的执行环境:跑命令、读写文 `exec` 在所选 Box 后端中运行;其余五个直接读写映射到 `/workspace` 的工作区目录。选择 Host 时,命令也直接运行在 Box Runtime 主机上。 -这些工具面向内置 Agent。使用 Dify、n8n、Langflow、Coze 等外部 Runner 时,请使用对应平台自己的工具机制。 +这些工具面向内置 Agent。使用 Dify、n8n、Langflow、Coze 等外部运行器时,请使用对应平台自己的工具机制。 ## 沙箱作用域 diff --git a/zh/usage/sandbox/runtime-relationships.mdx b/zh/usage/sandbox/runtime-relationships.mdx index 090d11883..ac27da2d4 100644 --- a/zh/usage/sandbox/runtime-relationships.mdx +++ b/zh/usage/sandbox/runtime-relationships.mdx @@ -48,5 +48,5 @@ Box API、Agent 工具、会话作用域、生命周期、Skills 和 stdio MCP | 外部 Host Box 连接被拒绝 | LangBot 与 Box Runtime 两侧设置相同的 `LANGBOT_BOX_CONTROL_TOKEN` | -沙箱内置工具和 Skills 当前面向内置 Agent。外部 Runner(Dify、n8n、Langflow、Coze 等)应使用各自平台的工具和执行机制。 +沙箱内置工具和 Skills 当前面向内置 Agent。外部运行器(Dify、n8n、Langflow、Coze 等)应使用各自平台的工具和执行机制。 diff --git a/zh/usage/skills/readme.mdx b/zh/usage/skills/readme.mdx index 1e68b2a1b..f1324a604 100644 --- a/zh/usage/skills/readme.mdx +++ b/zh/usage/skills/readme.mdx @@ -97,5 +97,5 @@ LangBot 当前读取以下三个字段: | `register_skill` 报错 | 路径必须在 `/workspace` 下,目录或两层内子目录需含 `SKILL.md` | -Skills 当前面向内置 Agent,依赖函数调用和沙箱后端。外部 Runner(Dify、n8n、Langflow、Coze 等)请使用各自平台的等价机制。 +Skills 当前面向内置 Agent,依赖函数调用和沙箱后端。外部运行器(Dify、n8n、Langflow、Coze 等)请使用各自平台的等价机制。 From 4b9ba0db6b34ac2d1fa44fae90e737b686bd4334 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Sat, 12 Sep 2026 12:51:16 +0800 Subject: [PATCH 05/22] docs(runner): require explicit usage declarations --- en/plugin/dev/components/runner.mdx | 2 +- ja/plugin/dev/components/runner.mdx | 2 +- zh/plugin/dev/components/runner.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/en/plugin/dev/components/runner.mdx b/en/plugin/dev/components/runner.mdx index 7eed968c3..45bc4507f 100644 --- a/en/plugin/dev/components/runner.mdx +++ b/en/plugin/dev/components/runner.mdx @@ -86,7 +86,7 @@ execution: `spec.events` lists supported events, and `spec.config` defines settings users can edit on the processor detail page. This example handles new members and incoming messages, with a greeting and a reply switch. Sending replies requires the `tool_calling` and `permissions.tools` settings shown above. -`spec.usages` controls where the component can be selected: `agent` for Agents and Pipelines, `event` for Plugin processors. Both can be declared as `[agent, event]`. Event usage requires `spec.events`. The example uses typed handlers; a custom `run(ctx)` can also call `await super().run(ctx)` to dispatch them. +`spec.usages` is required and has no default. `spec.usages` controls where the component can be selected: `agent` for Agents and Pipelines, `event` for Plugin processors. Both can be declared as `[agent, event]`. Event usage requires `spec.events`. The example uses typed handlers; a custom `run(ctx)` can also call `await super().run(ctx)` to dispatch them. ## Plugin Implementation diff --git a/ja/plugin/dev/components/runner.mdx b/ja/plugin/dev/components/runner.mdx index 51723b417..7363bc9f6 100644 --- a/ja/plugin/dev/components/runner.mdx +++ b/ja/plugin/dev/components/runner.mdx @@ -86,7 +86,7 @@ execution: `spec.events` は対応するイベント、`spec.config` は詳細画面でユーザーが変更できる設定です。この例ではメンバー参加とメッセージ受信に対応し、歓迎メッセージと返信の有効・無効を設定できます。返信には、上記の `tool_calling` と `permissions.tools` の指定が必要です。 -`spec.usages` は選択先を指定します。`agent` は Agent とパイプライン、`event` はプラグインプロセッサーです。`[agent, event]` の併記も可能です。`event` には `spec.events` が必要です。以下はイベント処理関数の例です。独自の `run(ctx)` から `await super().run(ctx)` で呼び出すこともできます。 +`spec.usages` は必須で、デフォルト値はありません。`spec.usages` は選択先を指定します。`agent` は Agent とパイプライン、`event` はプラグインプロセッサーです。`[agent, event]` の併記も可能です。`event` には `spec.events` が必要です。以下はイベント処理関数の例です。独自の `run(ctx)` から `await super().run(ctx)` で呼び出すこともできます。 ## プラグインの実装 diff --git a/zh/plugin/dev/components/runner.mdx b/zh/plugin/dev/components/runner.mdx index 681199f66..00f1b3987 100644 --- a/zh/plugin/dev/components/runner.mdx +++ b/zh/plugin/dev/components/runner.mdx @@ -86,7 +86,7 @@ execution: `spec.events` 指定支持的事件,`spec.config` 定义用户可在处理器详情页修改的配置。本例支持成员入群和收到消息,并提供欢迎语、回复开关两个配置项。回复消息需要上面的 `tool_calling` 和 `permissions.tools` 配置。 -`spec.usages` 决定组件可在哪类处理器中选择:`agent` 用于 Agent 和流水线,`event` 用于插件处理器。可同时声明 `[agent, event]`;包含 `event` 时必须填写 `spec.events`。本例使用事件处理函数,也可以在 `run(ctx)` 中按需调用 `await super().run(ctx)`。 +`spec.usages` 必填、无默认值,决定组件可在哪类处理器中选择:`agent` 用于 Agent 和流水线,`event` 用于插件处理器。可同时声明 `[agent, event]`;包含 `event` 时必须填写 `spec.events`。本例使用事件处理函数,也可以在 `run(ctx)` 中按需调用 `await super().run(ctx)`。 ## 插件处理 From 6dcbcf6eac02268d8a70c232d0a7c71035bc60b5 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Tue, 15 Sep 2026 00:28:14 +0800 Subject: [PATCH 06/22] docs(runner): explain processor configurations and bot subscriptions --- en/plugin/dev/apis/agent-run.mdx | 2 +- en/plugin/dev/components/runner.mdx | 6 ++++-- ja/plugin/dev/apis/agent-run.mdx | 4 ++-- ja/plugin/dev/components/runner.mdx | 6 ++++-- zh/plugin/dev/apis/agent-run.mdx | 2 +- zh/plugin/dev/components/runner.mdx | 6 ++++-- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/en/plugin/dev/apis/agent-run.mdx b/en/plugin/dev/apis/agent-run.mdx index 261a3feb6..71e58f753 100644 --- a/en/plugin/dev/apis/agent-run.mdx +++ b/en/plugin/dev/apis/agent-run.mdx @@ -159,7 +159,7 @@ event_type = ctx.event.event_type | `context` | `ContextAccess` | Conversation cursors, inline policy, and available context APIs | | `state` | `AgentRunState` | Scope-specific state snapshot loaded at the start of the run | | `runtime` | `AgentRuntimeContext` | LangBot version, trace ID, and deadline | -| `config` | `dict[str, Any]` | Current Agent or plugin-processor instance configuration | +| `config` | `dict[str, Any]` | Current Agent or plugin-processor configuration | | `adapter` | `AdapterContext \| None` | Additional entry-adapter data | | `metadata` | `dict[str, Any]` | Other metadata supplied by the Host | diff --git a/en/plugin/dev/components/runner.mdx b/en/plugin/dev/components/runner.mdx index 45bc4507f..688a014fb 100644 --- a/en/plugin/dev/components/runner.mdx +++ b/en/plugin/dev/components/runner.mdx @@ -186,12 +186,14 @@ Configure the debug connection following the [plugin development tutorial](/en/p Platform replies use Mock during debugging and send no real messages. Restart `lbp run` after editing component YAML. -After installing the plugin, bind the Bot's **Member joined** and **Message received** events to this processor to handle actual events. +After installing the plugin, add this configuration under **Plugin processor** on the bot detail page and save. You can also select a component, fill in its configuration, and create and bind a new configuration directly there. Declared events are delivered automatically; no per-event routes are needed. + +Plugin subscriptions run independently of Agent/Pipeline routes. An event can trigger multiple processors, and one failure does not prevent the others from running. Bots using the same configuration share settings and runtime state; create separate configurations for different settings. Expose optional behavior, such as replies, through component configuration to avoid duplicate responses. ## Next Steps - See the [`RunnerContext` API reference](/en/plugin/dev/apis/agent-run#runner). - See [LangBot API](/en/plugin/dev/apis/common) for models, tools, knowledge bases, and storage. - See [Platform API](/en/plugin/dev/apis/platform) for messaging and other platform operations. -- Explore more examples in [RunnerDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/RunnerDemo). +- Explore more examples in [RunnerDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/Runner/RunnerDemo). - To extend Pipeline steps such as preprocessing or completion of a model call, use [Event Listener](/en/plugin/dev/components/event-listener). diff --git a/ja/plugin/dev/apis/agent-run.mdx b/ja/plugin/dev/apis/agent-run.mdx index 8cb224bc1..4b3c4ea79 100644 --- a/ja/plugin/dev/apis/agent-run.mdx +++ b/ja/plugin/dev/apis/agent-run.mdx @@ -159,7 +159,7 @@ event_type = ctx.event.event_type | `context` | `ContextAccess` | 会話カーソル、インライン方針、利用可能なコンテキスト API | | `state` | `AgentRunState` | 実行開始時に読み込まれたスコープ別状態のスナップショット | | `runtime` | `AgentRuntimeContext` | LangBot バージョン、トレース ID、期限 | -| `config` | `dict[str, Any]` | 現在の Agent またはプラグイン処理インスタンスの設定 | +| `config` | `dict[str, Any]` | 現在の Agent またはプラグインプロセッサーの設定 | | `adapter` | `AdapterContext \| None` | エントリーアダプターの追加データ | | `metadata` | `dict[str, Any]` | ホストが提供するその他のメタデータ | @@ -183,7 +183,7 @@ event_type = ctx.event.event_type | `await ctx.get_available_tools()` | `list[dict[str, Any]]` | 今回呼び出せるコンテキスト操作、プラットフォーム API、プラグインツール、MCP ツールを取得 | | `await ctx.call_tool(tool_name, parameters=None)` | `dict[str, Any]` | 上記一覧のツールを呼び出す | -`get_available_tools()` の各項目には `name`、`description`、`parameters`、`type`、`operations` が含まれます。結果は現在の Agent またはプラグイン処理インスタンスの設定とイベント機能で絞り込まれています。一覧にないツールは `ctx.call_tool()` で呼び出せません。 +`get_available_tools()` の各項目には `name`、`description`、`parameters`、`type`、`operations` が含まれます。結果は現在の Agent またはプラグインプロセッサーの設定とイベント機能で絞り込まれています。一覧にないツールは `ctx.call_tool()` で呼び出せません。 ### プロンプト、履歴、イベント diff --git a/ja/plugin/dev/components/runner.mdx b/ja/plugin/dev/components/runner.mdx index 7363bc9f6..466365010 100644 --- a/ja/plugin/dev/components/runner.mdx +++ b/ja/plugin/dev/components/runner.mdx @@ -186,12 +186,14 @@ async with ctx.reply_stream() as reply: デバッグ中の返信は Mock で再現され、実際のメッセージは送信されません。コンポーネントの YAML を変更したら `lbp run` を再起動してください。 -インストール後、Bot のメンバー参加とメッセージ受信をこのプロセッサーに紐付けると、実際のイベントを処理できます。 +インストール後、ボット詳細の「プラグインプロセッサー」でこの設定を追加して保存します。この画面でコンポーネントを選択し、設定を入力して新規作成・紐付けすることもできます。宣言されたイベントは自動で配信され、イベントごとのルート設定は不要です。 + +プラグインの購読は Agent/パイプラインのルートと独立して実行されます。同じイベントを複数のプロセッサーが処理でき、一つが失敗しても他の実行を妨げません。同じ設定を使用するボットは設定内容と実行状態を共有します。個別に設定する場合は、新しい設定を作成してください。返信などの任意の動作はコンポーネントの設定として提供し、重複返信を避けてください。 ## 次のステップ - [`RunnerContext` API リファレンス](/ja/plugin/dev/apis/agent-run#runner)を確認します。 - モデル、ツール、ナレッジベース、ストレージについては [LangBot API](/ja/plugin/dev/apis/common)を確認します。 - メッセージ送信などについては[プラットフォーム API](/ja/plugin/dev/apis/platform)を確認します。 -- [RunnerDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/RunnerDemo)で他の実装例を確認できます。 +- [RunnerDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/Runner/RunnerDemo)で他の実装例を確認できます。 - パイプラインの前処理やモデル呼び出し完了時などに機能を追加する場合は、[イベントリスナー](/ja/plugin/dev/components/event-listener)を使用してください。 diff --git a/zh/plugin/dev/apis/agent-run.mdx b/zh/plugin/dev/apis/agent-run.mdx index 7112ccd97..11489ac9e 100644 --- a/zh/plugin/dev/apis/agent-run.mdx +++ b/zh/plugin/dev/apis/agent-run.mdx @@ -161,7 +161,7 @@ event_type = ctx.event.event_type | `context` | `ContextAccess` | 会话游标、内联范围和可用上下文 API | | `state` | `AgentRunState` | 本次运行开始时读取到的各作用域状态快照 | | `runtime` | `AgentRuntimeContext` | LangBot 版本、追踪 ID 和截止时间 | -| `config` | `dict[str, Any]` | 当前 Agent 或插件处理器实例配置 | +| `config` | `dict[str, Any]` | 当前 Agent 或插件处理器配置 | | `adapter` | `AdapterContext \| None` | 入口适配器附加数据 | | `metadata` | `dict[str, Any]` | 宿主提供的其他元数据 | diff --git a/zh/plugin/dev/components/runner.mdx b/zh/plugin/dev/components/runner.mdx index 00f1b3987..f94a824ad 100644 --- a/zh/plugin/dev/components/runner.mdx +++ b/zh/plugin/dev/components/runner.mdx @@ -186,12 +186,14 @@ async with ctx.reply_stream() as reply: 调试中的平台回复使用 Mock,不发送真实消息。修改组件 YAML 后,请重启 `lbp run`。 -安装插件后,在机器人中将「成员加入群组」和「收到消息」绑定到该处理器,即可处理实际事件。 +安装插件后,在机器人详情页的「插件处理器」中添加该配置并保存。也可以直接在这里选择组件、填写配置,创建并绑定新配置。机器人会自动投递组件声明的事件,无需逐条配置事件路由。 + +插件处理器与 Agent/流水线路由独立执行,同一事件可以触发多个插件处理器,某个处理器失败不会阻止其他处理。使用同一配置的机器人会共享设置和运行状态;需要分别设置时,创建不同的配置。可选功能应通过组件配置提供,例如是否回复消息,避免与其他处理器重复回复。 ## 接下来做什么 - 查看 [`RunnerContext` 上下文 API](/zh/plugin/dev/apis/agent-run#runner)。 - 查看模型、工具、知识库和存储相关的 [LangBot API](/zh/plugin/dev/apis/common)。 - 查看消息发送等[平台 API](/zh/plugin/dev/apis/platform)。 -- 查看 [RunnerDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/RunnerDemo) 中的更多示例。 +- 查看 [RunnerDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/Runner/RunnerDemo) 中的更多示例。 - 如果要扩展流水线的预处理、模型调用完成等步骤,请使用[事件监听器](/zh/plugin/dev/components/event-listener)。 From a5c1fb43e0c14c6f992c549a8c436de960371aab Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Thu, 17 Sep 2026 23:04:53 +0800 Subject: [PATCH 07/22] docs: explain runner sandbox APIs and custom select presets --- en/plugin/dev/apis/agent-run.mdx | 20 ++++++++++++++++++++ en/plugin/dev/apis/common.mdx | 14 ++++++++++++++ en/plugin/dev/basic-info.mdx | 3 +++ ja/plugin/dev/apis/agent-run.mdx | 20 ++++++++++++++++++++ ja/plugin/dev/apis/common.mdx | 14 ++++++++++++++ ja/plugin/dev/basic-info.mdx | 3 +++ zh/plugin/dev/apis/agent-run.mdx | 20 ++++++++++++++++++++ zh/plugin/dev/apis/common.mdx | 14 ++++++++++++++ zh/plugin/dev/basic-info.mdx | 5 ++++- 9 files changed, 112 insertions(+), 1 deletion(-) diff --git a/en/plugin/dev/apis/agent-run.mdx b/en/plugin/dev/apis/agent-run.mdx index 71e58f753..58bcf069f 100644 --- a/en/plugin/dev/apis/agent-run.mdx +++ b/en/plugin/dev/apis/agent-run.mdx @@ -161,6 +161,7 @@ event_type = ctx.event.event_type | `runtime` | `AgentRuntimeContext` | LangBot version, trace ID, and deadline | | `config` | `dict[str, Any]` | Current Agent or plugin-processor configuration | | `adapter` | `AdapterContext \| None` | Additional entry-adapter data | +| `variables` | `dict[str, Any]` | Public request variables for Runner templates | | `metadata` | `dict[str, Any]` | Other metadata supplied by the Host | Prefer the stable properties in this table. `event.data` and `adapter.extra` carry additional data that has not been promoted to stable fields. @@ -185,6 +186,25 @@ Each `reply_stream().update(text)` call supplies the complete current text. If t Each item returned by `get_available_tools()` contains `name`, `description`, `parameters`, `type`, and `operations`. The result is already filtered by the current Agent or plugin-processor configuration and the event's capabilities. `ctx.call_tool()` cannot call a tool absent from this list. +### Box and attachments + +The Runner owns enablement, acquisition, and reuse policy. Use the [LangBot Box APIs](/en/plugin/dev/apis/common#box-sandbox) for resources and these context methods for the current run. + +| Method | Returns | Behavior | +| --- | --- | --- | +| `await ctx.bind_box(box_id)` | `BoxBinding` | Bind a Workspace Box; returns `box_id`, `outbox`. A run cannot switch Boxes | +| `await ctx.import_box_attachments(attachment_ids=None)` | `list[BoxFile]` | Import selected input `ref` values, or all inputs; returns sandbox paths | +| `await ctx.export_box_files()` | `list[BoxFile]` | Export this run's outbox as file handles; sends no message | +| `await ctx.reply_files(file_ids)` | `Any` | Reply with exported IDs, subject to event reply authorization | + +`BoxFile` contains `id`, `name`, `type`, `size`, and optional `path`. Original attachment URLs and content remain available. Files are staged only on explicit import. Output handles belong to this run and cannot be sent twice. + +Bind before native sandbox tools or file operations. Runs sharing a Box still have separate attachment directories. `ctx.variables` contains public request variables for computing reuse keys. + +`ctx.delivery.automatic_reply` is `True` for Pipeline execution: return files using `RunnerResult.message_completed(ctx.run_id, message, file_ids=[...])`. Agents and plugin processors explicitly call `ctx.reply_files()`. + +External runners use the same flow through `AgentRunExternalTools`: `langbot_get_box_status`, `langbot_list_boxes`, `langbot_acquire_box`, `langbot_bind_box`, `langbot_import_box_attachments`, and `langbot_export_box_files`. `langbot_reply_files` is available when the run has event reply permission. All calls retain the current run authorization. + ### Prompt, history, and events | Method | Return value | Availability field | diff --git a/en/plugin/dev/apis/common.mdx b/en/plugin/dev/apis/common.mdx index e7e8914c2..4ea0089ac 100644 --- a/en/plugin/dev/apis/common.mdx +++ b/en/plugin/dev/apis/common.mdx @@ -693,3 +693,17 @@ commands = await self.plugin.list_commands() ``` Both methods take no arguments and return the Host-provided plugin manifest list and command list respectively. + +### Box sandbox + +These methods use `self.plugin` and the current Workspace. Runner calls automatically retain invocation authorization. + +| Method | Returns | Behavior | +| --- | --- | --- | +| `await self.plugin.get_box_status()` | `BoxStatus` | `enabled`, `available`, capacity `limit`, `used`, `remaining`, optional managed `required_reuse_key`, and unavailable `reason` | +| `await self.plugin.list_boxes()` | `list[BoxSession]` | Box `id` and `status` snapshot (`idle`, `running`, `closing`) | +| `await self.plugin.acquire_box(reuse_key, options=None)` | `BoxSession` | Reuse the same key or create within capacity; `options` supports `image` | + +Unknown capacity fields are `None`. Zero remaining capacity still permits reuse; Runtime checks creation capacity atomically. The Runner computes reuse keys. When `required_reuse_key` is supplied, use it. Equal keys in different Workspaces never share a Box. + +Use the [Runner context](/en/plugin/dev/apis/agent-run#runner) to bind the Box before importing attachments or executing tools. diff --git a/en/plugin/dev/basic-info.mdx b/en/plugin/dev/basic-info.mdx index e51fc5a3b..b7b8c4df2 100644 --- a/en/plugin/dev/basic-info.mdx +++ b/en/plugin/dev/basic-info.mdx @@ -159,6 +159,7 @@ Dropdown menu. Requires configuring `options`, which represents the dropdown men ```yaml - name: mode type: select + allow_custom: true options: # Dropdown options, supports multilingual. - name: mode1 # Value, displayed as secondary text in the dropdown. label: # Display name, supports multilingual. @@ -171,6 +172,8 @@ Dropdown menu. Requires configuring `options`, which represents the dropdown men ... ``` +Set `allow_custom: true` to add a Custom option alongside the presets. A text input appears only when Custom is selected. The saved value is the selected `name` or the custom string, never a Custom marker. This also applies to a runner’s `spec.config`. + ### type: prompt-editor Prompt editor. Displays a prompt editor from the pipeline configuration page, with the final result represented in OpenAI's `messages` format. diff --git a/ja/plugin/dev/apis/agent-run.mdx b/ja/plugin/dev/apis/agent-run.mdx index 4b3c4ea79..bd49ed182 100644 --- a/ja/plugin/dev/apis/agent-run.mdx +++ b/ja/plugin/dev/apis/agent-run.mdx @@ -161,6 +161,7 @@ event_type = ctx.event.event_type | `runtime` | `AgentRuntimeContext` | LangBot バージョン、トレース ID、期限 | | `config` | `dict[str, Any]` | 現在の Agent またはプラグインプロセッサーの設定 | | `adapter` | `AdapterContext \| None` | エントリーアダプターの追加データ | +| `variables` | `dict[str, Any]` | Runner テンプレートで使える公開リクエスト変数 | | `metadata` | `dict[str, Any]` | ホストが提供するその他のメタデータ | 上表の安定したプロパティを優先してください。`event.data` と `adapter.extra` には、安定したフィールドに含まれない追加データが入ります。 @@ -185,6 +186,25 @@ event_type = ctx.event.event_type `get_available_tools()` の各項目には `name`、`description`、`parameters`、`type`、`operations` が含まれます。結果は現在の Agent またはプラグインプロセッサーの設定とイベント機能で絞り込まれています。一覧にないツールは `ctx.call_tool()` で呼び出せません。 +### Box と添付ファイル + +Box の有効化、取得、再利用方針は Runner が決めます。リソースの取得には [LangBot API](/ja/plugin/dev/apis/common#box-サンドボックス)、実行中の操作には以下を使います。 + +| メソッド | 戻り値 | 説明 | +| --- | --- | --- | +| `await ctx.bind_box(box_id)` | `BoxBinding` | Box を現在の実行にバインドし、`box_id`、`outbox` を返す。同じ実行内では変更不可 | +| `await ctx.import_box_attachments(attachment_ids=None)` | `list[BoxFile]` | 入力の `ref` を指定してインポート。省略時は全件。サンドボックスの `path` を返す | +| `await ctx.export_box_files()` | `list[BoxFile]` | 現在の実行の outbox をファイルハンドルとしてエクスポート。メッセージは送信しない | +| `await ctx.reply_files(file_ids)` | `Any` | エクスポートした ID で返信。返信ツールの権限制限が適用される | + +`BoxFile` は `id`、`name`、`type`、`size`、任意の `path` を含みます。入力の元 URL と内容は保持され、明示的にインポートして初めてファイルパスが生成されます。ハンドルは今回の実行でのみ有効で、重複送信できません。 + +ネイティブツールやファイル操作の前に Box をバインドします。Box を共有しても添付ディレクトリは実行ごとに分かれます。`ctx.variables` の公開リクエスト変数で再利用キーを計算できます。 + +Pipeline では `ctx.delivery.automatic_reply` が `True` になり、`RunnerResult.message_completed(ctx.run_id, message, file_ids=[...])` で添付を返せます。Agent とプラグインプロセッサーは `ctx.reply_files()` で明示的に送信します。 + +外部ランナーも `AgentRunExternalTools` で同じ手順を使用できます:`langbot_get_box_status`、`langbot_list_boxes`、`langbot_acquire_box`、`langbot_bind_box`、`langbot_import_box_attachments`、`langbot_export_box_files`。イベントへの返信権限がある場合は `langbot_reply_files` も利用できます。すべての呼び出しに現在の実行の権限が適用されます。 + ### プロンプト、履歴、イベント | メソッド | 戻り値 | 利用可否フィールド | diff --git a/ja/plugin/dev/apis/common.mdx b/ja/plugin/dev/apis/common.mdx index 82f650be1..5b8284839 100644 --- a/ja/plugin/dev/apis/common.mdx +++ b/ja/plugin/dev/apis/common.mdx @@ -693,3 +693,17 @@ commands = await self.plugin.list_commands() ``` いずれも引数なしで、ホストが提供するプラグインのマニフェスト一覧とコマンド一覧を返します。 + +### Box サンドボックス + +`self.plugin` から現在のワークスペースに対して呼び出します。Runner 内では実行権限が自動的に引き継がれます。 + +| メソッド | 戻り値 | 説明 | +| --- | --- | --- | +| `await self.plugin.get_box_status()` | `BoxStatus` | `enabled`、`available`、上限 `limit`、使用数 `used`、残数 `remaining`、任意の `required_reuse_key`、利用不可理由 `reason` | +| `await self.plugin.list_boxes()` | `list[BoxSession]` | `id` と状態 `status`(`idle`、`running`、`closing`)のスナップショット | +| `await self.plugin.acquire_box(reuse_key, options=None)` | `BoxSession` | 同一キーで再利用、または上限内で作成。`options` は `image` をサポート | + +不明な数量は `None` です。残数がゼロでも既存 Box の再利用は可能です。新規作成時の上限は Runtime が原子的に確認します。キーは Runner が計算しますが、`required_reuse_key` が返された場合はその値を使います。異なるワークスペース間では同じキーでも共有されません。 + +取得後は[Runner コンテキスト](/ja/plugin/dev/apis/agent-run#runner)でバインドしてから、添付のインポートやツール実行を行います。 diff --git a/ja/plugin/dev/basic-info.mdx b/ja/plugin/dev/basic-info.mdx index 00426dfdc..6c9b42a98 100644 --- a/ja/plugin/dev/basic-info.mdx +++ b/ja/plugin/dev/basic-info.mdx @@ -159,6 +159,7 @@ spec: ```yaml - name: mode type: select + allow_custom: true options: # ドロップダウンオプション、多言語サポート。 - name: mode1 # 値、ドロップダウンで小さなテキストとして表示。 label: # 表示名、多言語サポート。 @@ -171,6 +172,8 @@ spec: ... ``` +`allow_custom: true` を設定すると、プリセットに「カスタム」が追加され、その選択時だけ入力欄が表示されます。保存される値は選択した `name` または入力した文字列であり、「カスタム」というマーカーではありません。ランナーの `spec.config` にも使用できます。 + ### type: prompt-editor プロンプトエディター。パイプライン設定ページからプロンプトエディターを表示し、最終結果はOpenAIの`messages`形式で表されます。 diff --git a/zh/plugin/dev/apis/agent-run.mdx b/zh/plugin/dev/apis/agent-run.mdx index 11489ac9e..e04d76f9b 100644 --- a/zh/plugin/dev/apis/agent-run.mdx +++ b/zh/plugin/dev/apis/agent-run.mdx @@ -163,6 +163,7 @@ event_type = ctx.event.event_type | `runtime` | `AgentRuntimeContext` | LangBot 版本、追踪 ID 和截止时间 | | `config` | `dict[str, Any]` | 当前 Agent 或插件处理器配置 | | `adapter` | `AdapterContext \| None` | 入口适配器附加数据 | +| `variables` | `dict[str, Any]` | 公开的请求变量,可用于运行器插值模板 | | `metadata` | `dict[str, Any]` | 宿主提供的其他元数据 | 优先使用上表中的稳定字段。`event.data` 和 `adapter.extra` 用于承载尚未进入稳定字段的附加信息。 @@ -187,6 +188,25 @@ event_type = ctx.event.event_type `get_available_tools()` 的每一项包含 `name`、`description`、`parameters`、`type` 和 `operations`。返回结果已经按当前 Agent 或插件处理器的配置和事件能力过滤;不在列表中的工具不能通过 `ctx.call_tool()` 调用。 +### Box 与附件 + +Box 是否启用、何时创建以及如何复用,由运行器决定。获取状态和申请 Box 使用 [LangBot API](/zh/plugin/dev/apis/common#box-沙箱);当前运行的绑定和附件操作使用下列方法。 + +| 方法 | 返回值 | 说明 | +| --- | --- | --- | +| `await ctx.bind_box(box_id)` | `BoxBinding` | 绑定当前工作区中的 Box,返回 `box_id`、`outbox`;同次运行不能切换 Box | +| `await ctx.import_box_attachments(attachment_ids=None)` | `list[BoxFile]` | 导入 `ctx.input.attachments` 的 `ref`,省略参数则导入全部;返回各文件的沙箱 `path` | +| `await ctx.export_box_files()` | `list[BoxFile]` | 导出本次运行 outbox 中的文件,返回文件句柄,不发送消息 | +| `await ctx.reply_files(file_ids)` | `Any` | 使用导出的 `id` 回复当前事件,受回复工具权限约束 | + +`BoxFile` 包含 `id`、`name`、`type`、`size` 和可选的 `path`。输入的 `url`、`content` 等原始信息仍保留,只有显式导入才产生沙箱文件路径。文件句柄仅在本次运行有效,不能重复发送。 + +先绑定 Box,再使用原生执行、文件工具和附件方法。复用同一 Box 时,各次运行的附件目录仍独立。`ctx.variables` 提供公开的请求变量,运行器可据此计算复用键。 + +通过流水线运行时,`ctx.delivery.automatic_reply` 为 `True`,可以在 `RunnerResult.message_completed(ctx.run_id, message, file_ids=[...])` 返回附件;Agent 和插件处理器使用 `ctx.reply_files()` 显式发送。 + +外部运行器通过 `AgentRunExternalTools` 工具网关使用相同流程:`langbot_get_box_status`、`langbot_list_boxes`、`langbot_acquire_box`、`langbot_bind_box`、`langbot_import_box_attachments`、`langbot_export_box_files`。具有当前事件回复权限时还可使用 `langbot_reply_files`;这些调用均携带当前运行的授权。 + ### 提示词、历史与事件 | 方法 | 返回值 | 可用性字段 | diff --git a/zh/plugin/dev/apis/common.mdx b/zh/plugin/dev/apis/common.mdx index 9c83d2037..87b3682d6 100644 --- a/zh/plugin/dev/apis/common.mdx +++ b/zh/plugin/dev/apis/common.mdx @@ -693,3 +693,17 @@ commands = await self.plugin.list_commands() ``` 两个方法均不需要参数,分别返回宿主提供的插件清单列表和命令列表。 + +### Box 沙箱 + +这些方法通过 `self.plugin` 调用,作用于当前工作区。在运行器内调用时会自动携带本次运行的授权。 + +| 方法 | 返回值 | 说明 | +| --- | --- | --- | +| `await self.plugin.get_box_status()` | `BoxStatus` | `enabled`、`available`、数量上限 `limit`、已用 `used`、剩余 `remaining`、托管环境要求的 `required_reuse_key` 和不可用原因 `reason` | +| `await self.plugin.list_boxes()` | `list[BoxSession]` | 可用 Box 的 `id` 和状态快照 `status`(`idle`、`running`、`closing`) | +| `await self.plugin.acquire_box(reuse_key, options=None)` | `BoxSession` | 同一键复用已有 Box,否则在数量限制内创建;`options` 支持 `image` | + +数量未知时对应字段为 `None`。`remaining=0` 仍可以复用已有 Box;创建时由 Runtime 原子检查额度。运行器自行计算复用键,Host 不解释聊天模板;托管环境给出 `required_reuse_key` 时须使用该值。不同工作区的相同键不会共用 Box。 + +获取 Box 后,通过[运行器上下文](/zh/plugin/dev/apis/agent-run#runner)绑定到本次运行,再导入附件和执行工具。 diff --git a/zh/plugin/dev/basic-info.mdx b/zh/plugin/dev/basic-info.mdx index 8ffc5f20e..1833bf85f 100644 --- a/zh/plugin/dev/basic-info.mdx +++ b/zh/plugin/dev/basic-info.mdx @@ -159,6 +159,7 @@ spec: ```yaml - name: mode type: select + allow_custom: true options: # 下拉框选项,支持多语言。 - name: mode1 # 值,在下拉框中以小字显示。 label: # 显示名称,支持多语言。 @@ -171,6 +172,8 @@ spec: ... ``` +设置 `allow_custom: true` 可在预设之外提供“自定义”选项,仅选择该项时显示输入框。最终保存的是所选 `name` 或自定义字符串,不会保存“自定义”标记。此选项也适用于运行器的 `spec.config`。 + ### type: prompt-editor 提示词编辑器。会展示一个流水线配置页面的提示词编辑器,最终结果表示为 OpenAI 的 `messages` 格式。 @@ -305,4 +308,4 @@ for tool_name in selected_tools: 该教程将指引您逐步完善插件功能。 -- 添加组件:插件组件是插件的核心功能单元,您可以根据需求[添加组件](/zh/plugin/dev/components/add)。 \ No newline at end of file +- 添加组件:插件组件是插件的核心功能单元,您可以根据需求[添加组件](/zh/plugin/dev/components/add)。 From 4130a5bc0161426fd7311faeaa03135737995537 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Sat, 19 Sep 2026 00:03:50 +0800 Subject: [PATCH 08/22] docs(api): explain per-call reasoning levels and compatibility --- en/plugin/dev/apis/common.mdx | 28 +++++++++++++++++++++++++--- zh/plugin/dev/apis/common.mdx | 28 +++++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/en/plugin/dev/apis/common.mdx b/en/plugin/dev/apis/common.mdx index 4ea0089ac..e1196cd9d 100644 --- a/en/plugin/dev/apis/common.mdx +++ b/en/plugin/dev/apis/common.mdx @@ -124,6 +124,8 @@ async def invoke_llm( funcs: list[resource_tool.LLMTool] = [], extra_args: dict[str, Any] = {}, timeout: float | None = None, + *, + reasoning_level: ReasoningLevel | None = None, ) -> provider_message.Message: """Invoke an LLM model""" @@ -137,13 +139,33 @@ llm_message = await self.plugin.invoke_llm( ``` +### Reasoning level + +All model invocation methods above accept the optional keyword-only `reasoning_level` parameter: + +```python +response = await self.plugin.invoke_llm( + llm_model_uuid=model_uuid, + messages=[provider_message.Message(role="user", content="Analyze this problem")], + reasoning_level="medium", +) +``` + +Values are `provider_default`, `disabled`, `enabled`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. Available levels depend on the model. LangBot validates support and translates the level into provider parameters; unsupported levels raise an error. + +- Omitted or `None`: preserves existing model configuration and `extra_args` behavior. +- `provider_default`: adds no explicit reasoning level. +- Other levels: apply to this call only. Do not also supply provider-specific reasoning parameters through `extra_args`. + +Runners read their own configuration and explicitly pass the appropriate level for each call, including fallback models and tool follow-ups. LangBot does not extract reasoning levels from Runner configuration. Existing plugins require no changes; when an explicit option is used with an unsupported Host, the SDK requests an upgrade before invoking the model. + ### Model streaming and usage | Method | Returns | | --- | --- | -| `invoke_llm_with_usage(llm_model_uuid, messages, funcs=[], extra_args={}, timeout=None)` | `LLMInvokeResult` | -| `invoke_llm_stream(llm_model_uuid, messages, funcs=[], extra_args={})` | Async iterator of `MessageChunk` | -| `invoke_llm_stream_events(llm_model_uuid, messages, funcs=[], extra_args={})` | Async iterator of `LLMStreamEvent` | +| `invoke_llm_with_usage(llm_model_uuid, messages, funcs=[], extra_args={}, timeout=None, *, reasoning_level=None)` | `LLMInvokeResult` | +| `invoke_llm_stream(llm_model_uuid, messages, funcs=[], extra_args={}, *, reasoning_level=None)` | Async iterator of `MessageChunk` | +| `invoke_llm_stream_events(llm_model_uuid, messages, funcs=[], extra_args={}, *, reasoning_level=None)` | Async iterator of `LLMStreamEvent` | Parameters match `invoke_llm`. `timeout` is in seconds; non-streaming calls default to 120 seconds. Use `invoke_llm_with_usage` when token usage is needed: diff --git a/zh/plugin/dev/apis/common.mdx b/zh/plugin/dev/apis/common.mdx index 87b3682d6..950558c49 100644 --- a/zh/plugin/dev/apis/common.mdx +++ b/zh/plugin/dev/apis/common.mdx @@ -124,6 +124,8 @@ async def invoke_llm( funcs: list[resource_tool.LLMTool] = [], extra_args: dict[str, Any] = {}, timeout: float | None = None, + *, + reasoning_level: ReasoningLevel | None = None, ) -> provider_message.Message: """Invoke an LLM model""" @@ -137,13 +139,33 @@ llm_message = await self.plugin.invoke_llm( ``` +### 思考等级 + +上述模型调用方法都支持可选的关键字参数 `reasoning_level`: + +```python +response = await self.plugin.invoke_llm( + llm_model_uuid=model_uuid, + messages=[provider_message.Message(role="user", content="分析这个问题")], + reasoning_level="medium", +) +``` + +可选值为 `provider_default`、`disabled`、`enabled`、`minimal`、`low`、`medium`、`high`、`xhigh`、`max`,具体取决于模型支持的等级。LangBot 校验能力并转换为供应商参数,不支持的等级会报错。 + +- 不传或传 `None`:保持原有行为,使用模型配置及 `extra_args`。 +- 传 `provider_default`:不额外指定思考等级。 +- 传其他等级:仅作用于本次调用;不要再通过 `extra_args` 重复设置供应商的思考参数。 + +Runner 应读取自己的配置,在每次模型调用时显式传入对应等级,包括备用模型和工具调用后的续答。LangBot 不会自动读取 Runner 配置中的思考等级。旧插件无需修改;显式使用新参数时,如果 Host 不支持,SDK 会在调用模型前提示升级。 + ### 模型流式输出与用量 | 方法 | 返回值 | | --- | --- | -| `invoke_llm_with_usage(llm_model_uuid, messages, funcs=[], extra_args={}, timeout=None)` | `LLMInvokeResult` | -| `invoke_llm_stream(llm_model_uuid, messages, funcs=[], extra_args={})` | 异步迭代 `MessageChunk` | -| `invoke_llm_stream_events(llm_model_uuid, messages, funcs=[], extra_args={})` | 异步迭代 `LLMStreamEvent` | +| `invoke_llm_with_usage(llm_model_uuid, messages, funcs=[], extra_args={}, timeout=None, *, reasoning_level=None)` | `LLMInvokeResult` | +| `invoke_llm_stream(llm_model_uuid, messages, funcs=[], extra_args={}, *, reasoning_level=None)` | 异步迭代 `MessageChunk` | +| `invoke_llm_stream_events(llm_model_uuid, messages, funcs=[], extra_args={}, *, reasoning_level=None)` | 异步迭代 `LLMStreamEvent` | 参数与 `invoke_llm` 相同。`timeout` 单位为秒,非流式调用默认 120 秒。需要 token 用量时使用 `invoke_llm_with_usage`: From 124557f5604d564b066885f6d88b87d67a6a89f4 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Sun, 20 Sep 2026 18:31:22 +0800 Subject: [PATCH 09/22] docs(plugin): document certified plugin releases (#134) --- docs.json | 3 +++ en/plugin/certified-plugins.mdx | 27 +++++++++++++++++++++++++++ en/plugin/dev/publish/market.mdx | 8 ++++++++ en/plugin/plugin-intro.mdx | 2 +- ja/plugin/certified-plugins.mdx | 27 +++++++++++++++++++++++++++ ja/plugin/dev/publish/market.mdx | 8 ++++++++ ja/plugin/plugin-intro.mdx | 2 +- sitemap-alternates.xml | 21 +++++++++++++++++++++ tests/agent-static-build.test.mjs | 2 +- tests/fumapress-migration.test.mjs | 6 +++--- tests/fumapress-static-build.test.mjs | 5 +++-- tests/locale-static-build.test.mjs | 4 ++-- zh/plugin/certified-plugins.mdx | 27 +++++++++++++++++++++++++++ zh/plugin/dev/publish/market.mdx | 8 ++++++++ zh/plugin/plugin-intro.mdx | 2 +- 15 files changed, 141 insertions(+), 11 deletions(-) create mode 100644 en/plugin/certified-plugins.mdx create mode 100644 ja/plugin/certified-plugins.mdx create mode 100644 zh/plugin/certified-plugins.mdx diff --git a/docs.json b/docs.json index ec40b98fb..92dd1f2a9 100644 --- a/docs.json +++ b/docs.json @@ -670,6 +670,7 @@ { "group": "Plugin Development", "pages": [ + "en/plugin/certified-plugins", "en/plugin/dev/tutor", "en/plugin/dev/basic-info", "en/plugin/dev/directory-structure", @@ -973,6 +974,7 @@ { "group": "插件开发", "pages": [ + "zh/plugin/certified-plugins", "zh/plugin/dev/tutor", "zh/plugin/dev/basic-info", "zh/plugin/dev/directory-structure", @@ -1271,6 +1273,7 @@ { "group": "プラグイン開発", "pages": [ + "ja/plugin/certified-plugins", "ja/plugin/dev/tutor", "ja/plugin/dev/basic-info", "ja/plugin/dev/directory-structure", diff --git a/en/plugin/certified-plugins.mdx b/en/plugin/certified-plugins.mdx new file mode 100644 index 000000000..87babac82 --- /dev/null +++ b/en/plugin/certified-plugins.mdx @@ -0,0 +1,27 @@ +--- +title: "Certified Plugins" +description: "How LangBot identifies reviewed plugin releases and how the allowance works in Cloud." +--- + +A **Certified Plugin** is a specific plugin release that LangBot has reviewed for the certified-plugin requirements. Certification belongs to the exact published version, not to the author account or plugin name alone. + +## What you see + +Certified Plugin releases show a **Certified Plugin** badge in the marketplace and plugin details. A plugin without the badge is still a normal plugin; it is not treated as unsafe simply because it is not certified. + +In LangBot Cloud, Certified Plugins and other plugins use separate allowances. The install screen shows which allowance applies before you install. Existing installations are not removed just because an older release does not carry certification. + +## Using a Certified Plugin + +1. Open the plugin in the marketplace and select a version carrying the Certified Plugin badge. +2. Install or update it from LangBot's plugin management page. +3. Configure it for your workspace as usual. +4. Use the plugin log view for workspace-scoped diagnostic records when available. + +## For plugin authors + +Certification is requested by declaring the supported certified-runtime mode in the plugin manifest and submitting a new immutable release. The declaration is only a request: LangBot Space performs an advisory automated review, a human reviewer makes the publication decision, and certification is issued only for the exact reviewed package. + +Authors should keep configuration, storage, logs, background work, and credentials scoped to the active installation and workspace. Do not rely on mutable process-global state or raw process logs. If a release cannot meet those requirements, publish it as a normal dedicated plugin instead. + +See [publishing to the marketplace](/en/plugin/dev/publish/market) for submission guidance. \ No newline at end of file diff --git a/en/plugin/dev/publish/market.mdx b/en/plugin/dev/publish/market.mdx index 76f1d3166..5b0c3926a 100644 --- a/en/plugin/dev/publish/market.mdx +++ b/en/plugin/dev/publish/market.mdx @@ -78,6 +78,14 @@ Plugin built successfully: dist/tmp/RockChinQ-HelloPlugin-0.1.0.lbpkg You can now check your plugin submission task at [LangBot Plugin Marketplace](https://space.langbot.app/market). Please wait for approval, after which you can view your plugin in the marketplace. +## Request Certified Plugin review + +If your release is designed for the Certified Plugin requirements, declare the supported certified runtime mode in `manifest.yaml` and submit a **new version**. This declaration requests review; it does not grant a badge or change runtime behavior by itself. + +LangBot Space checks the exact submitted package, runs an advisory code review, and requires a human approval before a Certified Plugin badge can be issued. Certification is bound to the reviewed package version and checksum. Keep workspace configuration, storage, credentials, logs, and background work scoped to the active installation. Do not rely on mutable process-global state. + +If the review finds that the release cannot meet these requirements, publish it as a normal dedicated plugin and address the findings in a later immutable version. See [Certified Plugins](/en/plugin/certified-plugins) for user-visible behavior. + If you have any questions, you can send an email to `hello@langbot.app` or contact us through [Community](/en/insight/community). diff --git a/en/plugin/plugin-intro.mdx b/en/plugin/plugin-intro.mdx index add342e22..c49c57d27 100644 --- a/en/plugin/plugin-intro.mdx +++ b/en/plugin/plugin-intro.mdx @@ -4,7 +4,7 @@ title: "LangBot Plugins" LangBot is composed of various internal components such as Large Language Model tools, commands, messaging platform adapters, LLM requesters, and more. To meet extensibility and flexibility requirements, we have implemented a production-grade plugin system. -Each plugin runs in an independent process, managed uniformly by the Plugin Runtime. +Plugins are managed uniformly by the Plugin Runtime. Most plugins use a dedicated runtime; a reviewed release may be shown as a [Certified Plugin](/en/plugin/certified-plugins) and use the Certified Plugin allowance in LangBot Cloud. diff --git a/ja/plugin/certified-plugins.mdx b/ja/plugin/certified-plugins.mdx new file mode 100644 index 000000000..77e61d4c1 --- /dev/null +++ b/ja/plugin/certified-plugins.mdx @@ -0,0 +1,27 @@ +--- +title: "認証プラグイン" +description: "LangBot が審査済みのプラグインリリースを示す方法と、Cloud の利用枠について説明します。" +--- + +**認証プラグイン**は、LangBot の認証要件を満たした特定のプラグインリリースです。認証は作者アカウントやプラグイン名ではなく、公開済みの正確なバージョンに紐付きます。 + +## 表示される内容 + +認証済みリリースには、マーケットプレイスとプラグイン詳細に**認証プラグイン**の表示が出ます。表示がないプラグインも通常のプラグインであり、未認証だからといって安全でないことを意味しません。 + +LangBot Cloud では、認証プラグインとその他のプラグインで利用枠が分かれています。インストール前に、どちらの利用枠を使用するかが表示されます。以前の認証がないバージョンをすでに利用していても、自動的に削除されることはありません。 + +## 認証プラグインを使う + +1. マーケットプレイスでプラグインを開き、認証プラグイン表示のあるバージョンを選びます。 +2. LangBot のプラグイン管理画面からインストールまたは更新します。 +3. ワークスペースに合わせて通常どおり設定します。 +4. 利用できる場合は、プラグインログで現在のワークスペースに関連する診断記録を確認します。 + +## プラグイン作者向け + +作者は manifest で認証ランタイムモードの対応を宣言し、新しい不変リリースを提出できます。宣言は申請に過ぎません。LangBot Space は補助的なコードレビューを行い、人が公開を承認します。認証されるのは審査済みの正確なパッケージだけです。 + +設定、ストレージ、ログ、バックグラウンド処理、資格情報は、現在のインストールとワークスペースに必ず紐付けてください。可変なプロセスグローバル状態や生のプロセスログに依存しないでください。要件を満たせないリリースは、通常の専用プラグインとして公開できます。 + +提出方法は[マーケットプレイスへの公開](/ja/plugin/dev/publish/market)を参照してください。 \ No newline at end of file diff --git a/ja/plugin/dev/publish/market.mdx b/ja/plugin/dev/publish/market.mdx index 959b76ea3..21f71c1b9 100644 --- a/ja/plugin/dev/publish/market.mdx +++ b/ja/plugin/dev/publish/market.mdx @@ -78,6 +78,14 @@ Plugin built successfully: dist/tmp/RockChinQ-HelloPlugin-0.1.0.lbpkg [LangBotプラグインマーケットプレイス](https://space.langbot.app/market)でプラグイン提出タスクを確認できます。承認をお待ちください。承認後、マーケットプレイスでプラグインを表示できます。 +## 認証プラグイン審査を申請する + +認証プラグイン要件に対応するリリースでは、`manifest.yaml` に対応する認証ランタイムモードを宣言し、**新しいバージョン**を提出します。宣言は審査申請であり、表示の付与やランタイム動作の変更を単独で行うものではありません。 + +LangBot Space は提出された正確なパッケージを確認し、補助的なコードレビューを実行したうえで、人による承認後にのみ認証プラグインの表示を発行します。認証は審査済みのパッケージバージョンとチェックサムに紐付きます。ワークスペース設定、ストレージ、資格情報、ログ、バックグラウンド処理は、現在のインストールに必ず紐付けてください。可変なプロセスグローバル状態に依存しないでください。 + +要件を満たせない場合は通常の専用プラグインとして公開し、後続の不変バージョンで修正してください。ユーザー向けの説明は[認証プラグイン](/ja/plugin/certified-plugins)を参照してください。 + ご質問がある場合は、`hello@langbot.app`にメールを送信するか、[コミュニティ](/ja/insight/community)を通じてお問い合わせください。 diff --git a/ja/plugin/plugin-intro.mdx b/ja/plugin/plugin-intro.mdx index 68dd420a5..f23ffd760 100644 --- a/ja/plugin/plugin-intro.mdx +++ b/ja/plugin/plugin-intro.mdx @@ -4,7 +4,7 @@ title: "LangBotプラグイン" LangBotは、大規模言語モデルツール、コマンド、メッセージングプラットフォームアダプター、LLMリクエスターなど、さまざまな内部コンポーネントで構成されています。拡張性と柔軟性の要件を満たすために、本番環境グレードのプラグインシステムを実装しました。 -各プラグインは独立したプロセスで実行され、Plugin Runtimeによって統一的に管理されます。 +プラグインは Plugin Runtime により統一して管理されます。多くのプラグインは専用ランタイムを使用し、審査済みのリリースは[認証プラグイン](/ja/plugin/certified-plugins)として表示され、LangBot Cloud では認証プラグインの利用枠を使用します。 diff --git a/sitemap-alternates.xml b/sitemap-alternates.xml index 88cd9ce3d..86f94335d 100644 --- a/sitemap-alternates.xml +++ b/sitemap-alternates.xml @@ -1911,6 +1911,27 @@ + + https://langbot.app/docs/en/plugin/certified-plugins + + + + + + + https://langbot.app/docs/zh/plugin/certified-plugins + + + + + + + https://langbot.app/docs/ja/plugin/certified-plugins + + + + + https://langbot.app/docs/en/plugin/compatibility diff --git a/tests/agent-static-build.test.mjs b/tests/agent-static-build.test.mjs index 0abc55a5a..9ee1c33f0 100644 --- a/tests/agent-static-build.test.mjs +++ b/tests/agent-static-build.test.mjs @@ -22,7 +22,7 @@ function links(node, result = []) { } test("every sitemap page has meaningful canonical Markdown and HTML discovery", async () => { - assert.equal(urls.length, 482); + assert.equal(urls.length, 485); for (const url of urls) { const route = relative(url); const text = await read(`${route}.md`); diff --git a/tests/fumapress-migration.test.mjs b/tests/fumapress-migration.test.mjs index 7738db42c..a99888ae5 100644 --- a/tests/fumapress-migration.test.mjs +++ b/tests/fumapress-migration.test.mjs @@ -170,8 +170,8 @@ test("MDX compatibility normalization removes internal .html route suffixes", () test("canonical MDX and nested OpenAPI sources are discovered", async () => { const docs = JSON.parse(await readFile(path.join(root, "docs.json"), "utf8")); const pages = await collectMdxDocuments(root); - assert.equal(pages.length, 305); - assert.deepEqual(Object.fromEntries([...pages.reduce((acc, page) => acc.set(page.split("/", 1)[0], (acc.get(page.split("/", 1)[0]) ?? 0) + 1), new Map()).entries()]), { en: 97, zh: 111, ja: 97 }); + assert.equal(pages.length, 308); + assert.deepEqual(Object.fromEntries([...pages.reduce((acc, page) => acc.set(page.split("/", 1)[0], (acc.get(page.split("/", 1)[0]) ?? 0) + 1), new Map()).entries()]), { en: 98, zh: 112, ja: 98 }); assert.deepEqual(collectOpenApiSources(docs), [ { locale: "en", source: "openapi/service-api-en.json", directory: "en/api-reference" }, { locale: "zh", source: "openapi/service-api-zh.json", directory: "zh/api-reference" }, @@ -216,7 +216,7 @@ test("prebuild is deterministic and preserves local assets and SEO files", async const first = await prepareFumapress({ root, outRoot: temp }); const second = await prepareFumapress({ root, outRoot: temp }); assert.deepEqual(first, second); - assert.equal(first.documents, 305); + assert.equal(first.documents, 308); assert.equal(first.fallbackDefaults, 0); assert.equal(first.localeOnlyDocuments, 14); assert.equal(first.redirects, 103); diff --git a/tests/fumapress-static-build.test.mjs b/tests/fumapress-static-build.test.mjs index 6f46f0495..a371ca471 100644 --- a/tests/fumapress-static-build.test.mjs +++ b/tests/fumapress-static-build.test.mjs @@ -43,9 +43,9 @@ function staticHtmlForUrlPath(urlPath) { return path.join(publicRoot, ...parts, "index.html"); } -test("all 305 canonical localized documents have static HTML", async () => { +test("all 308 canonical localized documents have static HTML", async () => { const documents = await collectMdxDocuments(root); - assert.equal(documents.length, 305); + assert.equal(documents.length, 308); const missing = []; for (const document of documents) { try { @@ -202,6 +202,7 @@ test("canonical sitemap routes match the legacy Mintlify directory structure", a // New guides added after migration extend, rather than replace, the legacy routes. for (const locale of ["en", "zh", "ja"]) { legacy.add(`/${locale}/usage/platforms/mattermost`); + legacy.add(`/${locale}/plugin/certified-plugins`); } const sitemap = await readFile(path.join(publicRoot, "sitemap.xml"), "utf8"); const actual = new Set([...sitemap.matchAll(/https:\/\/langbot\.app\/docs(\/[^<]+)<\/loc>/g)] diff --git a/tests/locale-static-build.test.mjs b/tests/locale-static-build.test.mjs index 96cb1900d..3cd370884 100644 --- a/tests/locale-static-build.test.mjs +++ b/tests/locale-static-build.test.mjs @@ -35,6 +35,6 @@ test("every localized HTML payload includes the preference client provider", asy } } for (const locale of ["en", "zh", "ja"]) await visit(path.join(root, locale)); - // 482 canonical content pages plus one framework 404 page per locale. - assert.equal(count, 485); + // 485 canonical content pages plus one framework 404 page per locale. + assert.equal(count, 488); }); diff --git a/zh/plugin/certified-plugins.mdx b/zh/plugin/certified-plugins.mdx new file mode 100644 index 000000000..af406e372 --- /dev/null +++ b/zh/plugin/certified-plugins.mdx @@ -0,0 +1,27 @@ +--- +title: "认证插件" +description: "了解 LangBot 如何标识已审核的插件版本,以及 Cloud 中的额度使用方式。" +--- + +**认证插件**是通过 LangBot 审核要求的某个具体插件版本。认证绑定到已发布的精确版本,而不是作者账号或插件名称。 + +## 你会看到什么 + +认证版本会在扩展市场和插件详情中显示**认证插件**标识。没有该标识的插件仍是正常插件;未认证不等于不安全。 + +在 LangBot Cloud 中,认证插件与其他插件使用不同的额度。安装前,界面会明确提示本次安装使用哪一类额度。已有安装不会因为旧版本没有认证而被自动删除。 + +## 使用认证插件 + +1. 在市场打开插件,选择带有认证插件标识的版本。 +2. 从 LangBot 的插件管理页面安装或更新。 +3. 按工作区正常配置并使用。 +4. 可用时,通过插件日志查看与当前工作区相关的诊断记录。 + +## 给插件作者 + +作者可在 manifest 中声明希望支持的认证运行模式,并提交新的不可变版本。声明只是申请:LangBot Space 会先进行辅助代码审查,再由人工审核发布;只有经过审核的精确插件包才会获得认证。 + +请让配置、存储、日志、后台工作和凭据都与当前安装和工作区绑定。不要依赖可变的进程全局状态或原始进程日志。无法满足这些要求的版本,仍可作为普通专属插件发布。 + +提交说明请参阅[发布到插件市场](/zh/plugin/dev/publish/market)。 \ No newline at end of file diff --git a/zh/plugin/dev/publish/market.mdx b/zh/plugin/dev/publish/market.mdx index 5b0de4aa2..fd5f8ce3b 100644 --- a/zh/plugin/dev/publish/market.mdx +++ b/zh/plugin/dev/publish/market.mdx @@ -78,6 +78,14 @@ Plugin built successfully: dist/tmp/RockChinQ-HelloPlugin-0.1.0.lbpkg 您现在可以在[LangBot 插件市场](https://space.langbot.app/market)查看您的插件提交任务。请等待审核通过后,您可以在插件市场查看您的插件。 +## 申请认证插件审核 + +如果该版本准备满足认证插件要求,请在 `manifest.yaml` 中声明支持的认证运行模式,并提交一个**新版本**。声明只是申请,不会自行获得标识,也不会自行改变运行行为。 + +LangBot Space 会检查本次提交的精确插件包,进行辅助代码审查,并在人工批准后才签发认证插件标识。认证绑定到已审核的插件版本与校验和。请让工作区配置、存储、凭据、日志和后台工作都与当前安装绑定,不要依赖可变的进程全局状态。 + +如果审核发现该版本无法满足要求,请先作为普通专属插件发布,并在后续不可变版本中修复问题。用户侧说明见[认证插件](/zh/plugin/certified-plugins)。 + 有任何问题,您可以发送邮件到`hello@langbot.app`或通过[社区](/zh/insight/community)联系我们。 diff --git a/zh/plugin/plugin-intro.mdx b/zh/plugin/plugin-intro.mdx index 23bc67feb..c6d29f052 100644 --- a/zh/plugin/plugin-intro.mdx +++ b/zh/plugin/plugin-intro.mdx @@ -4,7 +4,7 @@ title: "LangBot 插件" LangBot 内部由多种组件组成,如大模型工具、命令、消息平台适配器、大模型请求器等。为了满足扩展性和灵活性的需求,我们实现了一套生产级的插件系统。 -每个插件被运行在独立的进程中,由 Plugin Runtime 统一管理。 +插件由 Plugin Runtime 统一管理。大多数插件使用专属运行时;通过审核的版本会显示为[认证插件](/zh/plugin/certified-plugins),并在 LangBot Cloud 中使用认证插件额度。 From 17b69234497d582371c8bb5902f9bf7321d7764e Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Mon, 21 Sep 2026 10:52:23 +0000 Subject: [PATCH 10/22] design: use clean branded documentation social preview --- docs.json | 8 +++++++- images/social/docs-v2.png | Bin 0 -> 34716 bytes package.json | 2 +- press.config.tsx | 3 +++ tests/social-preview.test.mjs | 20 ++++++++++++++++++++ 5 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 images/social/docs-v2.png create mode 100644 tests/social-preview.test.mjs diff --git a/docs.json b/docs.json index 92dd1f2a9..6a19e6760 100644 --- a/docs.json +++ b/docs.json @@ -20,7 +20,13 @@ "metatags": { "og:site_name": "LangBot Docs", "twitter:card": "summary_large_image", - "og:image": "https://langbot.app/og-image.png" + "og:image": "https://langbot.app/docs/images/social/docs-v2.png", + "og:image:width": "1200", + "og:image:height": "630", + "og:image:type": "image/png", + "og:image:alt": "LangBot Docs — Build. Connect. Extend.", + "twitter:image": "https://langbot.app/docs/images/social/docs-v2.png", + "twitter:image:alt": "LangBot Docs — Build. Connect. Extend." } }, "navbar": { diff --git a/images/social/docs-v2.png b/images/social/docs-v2.png new file mode 100644 index 0000000000000000000000000000000000000000..acbe3d670bade82b7b50b6e65cb9ae3a329d6d20 GIT binary patch literal 34716 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 literal 0 HcmV?d00001 diff --git a/package.json b/package.json index cc8c06860..5c9c24b81 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "generate:sitemap-alternates": "node scripts/generate-alternate-sitemap.mjs", "sync:articles": "node scripts/sync-blog-articles.mjs", "check:articles": "node scripts/sync-blog-articles.mjs --check", - "test": "node --test tests/locale-preference.test.mjs tests/agent-docs.test.mjs tests/blog-articles.test.mjs tests/fumapress-migration.test.mjs tests/seo.test.mjs && python3 -m unittest discover -s tests -p 'test_*.py'", + "test": "node --test tests/social-preview.test.mjs tests/locale-preference.test.mjs tests/agent-docs.test.mjs tests/blog-articles.test.mjs tests/fumapress-migration.test.mjs tests/seo.test.mjs && python3 -m unittest discover -s tests -p 'test_*.py'", "finalize:base-path": "node scripts/finalize-base-path.mjs" }, "dependencies": { diff --git a/press.config.tsx b/press.config.tsx index a68713a02..54ede693a 100644 --- a/press.config.tsx +++ b/press.config.tsx @@ -404,6 +404,9 @@ export default defineConfig({ // One registration is intentional: its adapter and loader plugin handle all // OpenAPI virtual pages, regardless of which content source generated them. .plugins( + // Replace the recommended generic Takumi artwork with our shared static + // brand card. Keeping its plugin name suppresses duplicate og:image tags. + { name: "core:takumi" }, { name: "langbot:locale-preference", init() { diff --git a/tests/social-preview.test.mjs b/tests/social-preview.test.mjs new file mode 100644 index 000000000..b9b41a5b7 --- /dev/null +++ b/tests/social-preview.test.mjs @@ -0,0 +1,20 @@ +import assert from "node:assert/strict"; +import { readFileSync, existsSync } from "node:fs"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +test("share metadata references the branded versioned preview", () => { + for (const file of ['docs.json']) { + assert.ok(readFileSync(path.join(root, file), "utf8").includes('/images/social/docs-v2.png'), file); + } +}); +test("sharing PNG is a small 1200 by 630 image", () => { + const file = path.join(root, 'images/social/docs-v2.png'); + assert.ok(existsSync(file), "branded image is present"); + const png = readFileSync(file); + assert.equal(png.subarray(1, 4).toString(), "PNG"); + assert.equal(png.readUInt32BE(16), 1200); + assert.equal(png.readUInt32BE(20), 630); + assert.ok(png.length < 200_000); +}); From f6b6a635c2b7be52341c10cda786cd5ff4c9e550 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Mon, 28 Sep 2026 13:25:36 +0800 Subject: [PATCH 11/22] docs: stateless certified plugins Comprehensive documentation for stateless singleton component model supporting SDK 0.7.0 and certificate v2 envelope. --- en/plugin/certified-plugins.mdx | 61 ++++++++++++++++++++++-------- en/plugin/dev/basic-info.mdx | 3 ++ en/plugin/dev/components/page.mdx | 21 +++++------ en/plugin/dev/publish/market.mdx | 2 +- en/plugin/dev/style.mdx | 2 + ja/plugin/certified-plugins.mdx | 41 ++++++++++++-------- ja/plugin/dev/basic-info.mdx | 3 ++ ja/plugin/dev/components/page.mdx | 21 +++++------ ja/plugin/dev/publish/market.mdx | 2 +- ja/plugin/dev/style.mdx | 2 + zh/plugin/certified-plugins.mdx | 63 +++++++++++++++++++++++-------- zh/plugin/dev/basic-info.mdx | 3 ++ zh/plugin/dev/components/page.mdx | 21 +++++------ zh/plugin/dev/publish/market.mdx | 2 +- zh/plugin/dev/style.mdx | 2 + 15 files changed, 164 insertions(+), 85 deletions(-) diff --git a/en/plugin/certified-plugins.mdx b/en/plugin/certified-plugins.mdx index 87babac82..44d0324a1 100644 --- a/en/plugin/certified-plugins.mdx +++ b/en/plugin/certified-plugins.mdx @@ -1,27 +1,58 @@ --- -title: "Certified Plugins" -description: "How LangBot identifies reviewed plugin releases and how the allowance works in Cloud." +title: "Certified plugins and stateless components" +description: "Why certified plugins share workers and how to implement stateless-v1 components." --- -A **Certified Plugin** is a specific plugin release that LangBot has reviewed for the certified-plugin requirements. Certification belongs to the exact published version, not to the author account or plugin name alone. +A **Certified Plugin** badge grants a specific immutable plugin artifact eligibility for cross-tenant shared execution. Installations of the same artifact digest may share one Worker, one `BasePlugin` object and one instance of each component. Each installation contributes only lightweight configuration, authority and lifecycle context. “Each component” means one instance for every declared component manifest across EventListener, Tool, Command, KnowledgeEngine, Parser, Page and Runner. -## What you see +## Declaration -Certified Plugin releases show a **Certified Plugin** badge in the marketplace and plugin details. A plugin without the badge is still a normal plugin; it is not treated as unsafe simply because it is not certified. +```yaml +execution: + python: + path: main.py + attr: Plugin + sharedRuntime: shared-runtime-v1 + componentModel: stateless-v1 +``` -In LangBot Cloud, Certified Plugins and other plugins use separate allowances. The install screen shows which allowance applies before you install. Existing installations are not removed just because an older release does not carry certification. +Both fields are required. The declaration is an application, not approval: advisory source review, explicit human approval and a signature over the exact artifact remain mandatory. -## Using a Certified Plugin +## Stateless component contract -1. Open the plugin in the marketplace and select a version carrying the Certified Plugin badge. -2. Install or update it from LangBot's plugin management page. -3. Configure it for your workspace as usual. -4. Use the plugin log view for workspace-scoped diagnostic records when available. +- Plugin and component `initialize()` methods are process-scoped and run once per Worker. +- Component objects must be re-entrant because calls from multiple Workspaces can overlap. +- On `BasePlugin`, read installation configuration with `self.get_config()`. In a component, use `self.get_plugin_config()` or `self.plugin.get_config()`. +- Never retain configuration, credentials, Workspace/installation identity or request data in instance fields or module globals. +- Do not capture tenant configuration in `initialize()`. +- Use SDK Host APIs for storage, files, credentials, logs, models and other platform capabilities. Authority comes from the task-local invocation context. +- Durable tenant state belongs in Host services. Process caches may contain artifact-level public data only, or must use the complete installation binding as their key and support revocation cleanup. +- Tenant-bearing detached tasks and threads are forbidden in `stateless-v1`; the SDK does not yet expose a plugin-facing ownership/revocation API. -## For plugin authors +The contract intentionally preserves serverless-ready semantics, but current releases still run long-lived Workers and do not offer a serverless deployment mode. -Certification is requested by declaring the supported certified-runtime mode in the plugin manifest and submitting a new immutable release. The declaration is only a request: LangBot Space performs an advisory automated review, a human reviewer makes the publication decision, and certification is issued only for the exact reviewed package. +## Configuration example -Authors should keep configuration, storage, logs, background work, and credentials scoped to the active installation and workspace. Do not rely on mutable process-global state or raw process logs. If a release cannot meet those requirements, publish it as a normal dedicated plugin instead. +```python +class LookupTool(Tool): + async def initialize(self): + self.transport = StatelessHTTPTransport() -See [publishing to the marketplace](/en/plugin/dev/publish/market) for submission guidance. \ No newline at end of file + async def call(self, params, **kwargs): + config = self.get_plugin_config() + return await self.transport.lookup(config["endpoint"], params) +``` + +Do not retain tenant data during process initialization: + +```python +async def initialize(self): + self.endpoint = self.plugin.config["endpoint"] + self.api_key = self.plugin.config["api_key"] +``` + +Shared initialization has no tenant config, so this reads empty/stale data or fails. Any attempt to retain invocation config on the shared object blocks certification. + +## Legacy compatibility + +Legacy source and unsigned packages remain compatible on dedicated Workers. In OSS, packages without a trusted `stateless-v1` v2 certificate stay dedicated according to admission policy. Cloud can reject packages that do not meet its certificate policy before installation; existing installations are not proof of new eligibility. Changing only the manifest does not satisfy certification review. diff --git a/en/plugin/dev/basic-info.mdx b/en/plugin/dev/basic-info.mdx index b7b8c4df2..cc7cb99c2 100644 --- a/en/plugin/dev/basic-info.mdx +++ b/en/plugin/dev/basic-info.mdx @@ -28,6 +28,9 @@ execution: python: path: main.py # Do not modify attr: HelloPlugin # Do not modify + # Add only after migrating to the stateless singleton certification contract: + # sharedRuntime: shared-runtime-v1 + # componentModel: stateless-v1 ``` Plugin multilingual support follows the [RFC 4646](https://datatracker.ietf.org/doc/html/rfc4646) standard. Currently supported languages include: diff --git a/en/plugin/dev/components/page.mdx b/en/plugin/dev/components/page.mdx index af4b178b1..f95516b59 100644 --- a/en/plugin/dev/components/page.mdx +++ b/en/plugin/dev/components/page.mdx @@ -79,10 +79,9 @@ class DashboardPage(Page): # request.body: Request body (parsed JSON, or None) if request.endpoint == '/stats' and request.method == 'GET': - # Access shared plugin state via self.plugin - return PageResponse.ok({ - 'total': len(self.plugin.entries), - }) + # Read installation-scoped data from a Host storage API. + entries = await self.load_entries_for_current_installation() + return PageResponse.ok({'total': len(entries)}) return PageResponse.fail(f'Unknown endpoint: {request.endpoint}') ``` @@ -200,24 +199,22 @@ from langbot_plugin.api.definition.components.page import Page, PageRequest, Pag class ManagerPage(Page): async def handle_api(self, request: PageRequest) -> PageResponse: - plugin = self.plugin - if request.endpoint == '/entries' and request.method == 'GET': - return PageResponse.ok({'entries': plugin.entries}) + entries = await self.load_entries_for_current_installation() + return PageResponse.ok({'entries': entries}) if request.endpoint == '/entries' and request.method == 'POST': question = (request.body or {}).get('question', '').strip() answer = (request.body or {}).get('answer', '').strip() if not question or not answer: return PageResponse.fail('question and answer are required') - entry = plugin.add_entry(question, answer) - await plugin.persist() + entry = {'question': question, 'answer': answer} + await self.save_entry_for_current_installation(entry) return PageResponse.ok({'entry': entry}) if request.endpoint == '/entries' and request.method == 'DELETE': entry_id = (request.body or {}).get('id', '') - if plugin.delete_entry(entry_id): - await plugin.persist() + if await self.delete_entry_for_current_installation(entry_id): return PageResponse.ok({'deleted': entry_id}) return PageResponse.fail('entry not found') @@ -242,6 +239,6 @@ Full example code is available at [FAQManager plugin](https://github.com/langbot ## Notes - Pages run inside a `sandbox="allow-scripts allow-forms"` iframe and cannot open popups or navigate the parent page -- Access shared plugin state via `self.plugin` — Page and Tool components can share data +- Certified Pages must keep tenant data in installation-scoped Host storage. `self.plugin` is process-wide and must not hold tenant entries. The storage helper names above are illustrative; use the current SDK Host storage APIs available to your component. - Use `PageResponse.ok()` and `PageResponse.fail()` to construct responses for consistent formatting - The Page SDK `