Add 'docs/' from commit '51dddcb3f891d60bc38645e88b2dbd515344da81'

git-subtree-dir: docs
git-subtree-mainline: 0582d1b3d4
git-subtree-split: 51dddcb3f8
This commit is contained in:
RockChinQ
2026-10-27 08:04:35 +00:00
847 changed files with 87063 additions and 0 deletions
+263
View File
@@ -0,0 +1,263 @@
---
title: "Context APIs"
---
A context contains data for one component invocation. Each component receives a different context, so start with the entry point for your component.
| 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.
See [LLM request configuration](/en/plugin/dev/apis/llm-requests) for retries, timeouts, numeric reasoning budgets, prompt caching, headers and stream cancellation.
## EventListener
EventListener handles Pipeline events. Its event handler receives an `EventContext`:
```python
event = event_context.event
sender_id = event.sender_id
```
### Context data
| 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()` |
See [Pipeline Events](/en/plugin/dev/apis/pipeline-events) for event fields and all supported event types.
### 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
```
### Context data
| 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 |
| --- | --- |
| `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 |
Common `Session` properties are:
| Property | Description |
| --- | --- |
| `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 |
`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.
## Runner
Both custom `run(ctx)` implementations and Runner event handlers receive a `RunnerContext`. It remains valid only while the current invocation is running.
```python
text = ctx.input.to_text()
event_type = ctx.event.event_type
```
### Context data
| 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 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.
### Replies and logs
| 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` |
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.
### Available tools
| 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 |
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 |
| --- | --- | --- |
| `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` |
### State
| 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 |
`scope` can be `conversation`, `actor`, `subject`, or `runner`. These methods require `ctx.context.available_apis.state` to be `true`.
### Run records
| 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` |
`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()`.
### Availability and authorization
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`.
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.
`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.
## Data-only contexts
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.
| 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) |
+733
View File
@@ -0,0 +1,733 @@
---
title: "LangBot API"
---
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).
See [LLM request configuration](/en/plugin/dev/apis/llm-requests) for retries, timeouts, numeric reasoning budgets, prompt caching, headers and stream cancellation.
## 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 regular plugin component classes: Internal methods of the `self.plugin` object.
### Get Plugin Configuration
Plugin configuration format can be written in `manifest.yaml`, and users need to fill it out according to the plugin configuration format in LangBot's plugin management. Plugin code can then call this API to get plugin configuration information.
```python
def get_config(self) -> dict[str, typing.Any]:
"""Get the config of the plugin."""
# Usage example
config = self.plugin.get_config()
```
### Get LangBot Version
Get the LangBot version number, returned as a string in format `v<major>.<minor>.<patch>`.
```python
async def get_langbot_version(self) -> str:
"""Get the langbot version"""
# Usage example
langbot_version = await self.plugin.get_langbot_version()
```
### Get Configured Bot List
Returns a list of all bot UUIDs.
```python
async def get_bots(self) -> list[str]:
"""Get all bots"""
# Usage example
bots = await self.plugin.get_bots()
```
### Get Bot Information
Get bot information.
```python
async def get_bot_info(self, bot_uuid: str) -> dict[str, Any]:
"""Get a bot info"""
# Usage example
bot_info = await self.plugin.get_bot_info("de639861-be05-4018-859b-c2e2d3e0d603")
# Return example
{
"uuid": "de639861-be05-4018-859b-c2e2d3e0d603",
"name": "aiocqhttp",
"description": "Migrated from LangBot v3",
"adapter": "aiocqhttp",
"enable": true,
"use_pipeline_name": "ChatPipeline",
"use_pipeline_uuid": "c30a1dca-e91c-452b-83ec-84d635a30028",
"created_at": "2025-05-10T13:53:08",
"updated_at": "2025-08-12T11:27:30",
"adapter_runtime_values": { # Present if the bot is currently running
"bot_account_id": 960164003 # Bot account ID
}
}
```
### Send Proactive Message
Send proactive messages through bot UUID and target session ID.
For message chain construction methods, please refer to [Message Platform Entities](/en/plugin/dev/apis/messages).
```python
async def send_message(
self,
bot_uuid: str,
target_type: str,
target_id: str,
message_chain: platform_message.MessageChain,
) -> None:
"""Send a message to a session"""
# Usage example
await self.plugin.send_message(
bot_uuid="de639861-be05-4018-859b-c2e2d3e0d603",
target_type="person",
target_id="1010553892",
message_chain=platform_message.MessageChain([platform_message.Plain(text="Hello, world!")]),
)
```
### Get Configured LLM Model List
Returns a list of UUIDs for all configured LLM models.
```python
async def get_llm_models(self) -> list[str]:
"""Get all LLM models"""
# Usage example
llm_models = await self.plugin.get_llm_models()
```
### Invoke LLM Model
Invoke an LLM model, returns an LLM message. Non-streaming.
```python
async def invoke_llm(
self,
llm_model_uuid: str,
messages: list[provider_message.Message],
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"""
# Usage example
llm_message = await self.plugin.invoke_llm(
llm_model_uuid="llm_model_uuid",
messages=[provider_message.Message(role="user", content="Hello, world!")],
funcs=[],
extra_args={},
)
```
### 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, *, 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:
```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.
```python
async def list_parsers(self, mime_type: str | None = None) -> list[dict[str, Any]]:
"""List available Parser plugins"""
# Usage example
parsers = await self.plugin.list_parsers(mime_type="application/pdf")
# Each item includes plugin_id, plugin_author, plugin_name, name, description, supported_mime_types
```
### List All Available Tools
List all available tools in the current LangBot instance (including plugin tools and MCP tools).
```python
async def list_tools(self) -> list[dict[str, Any]]:
"""List all available tools
Returns:
A list of tool dicts, each containing:
- name: Tool name
- label: Display label (i18n)
- description: Tool description (i18n)
- icon: Tool icon
- spec: Tool specification (includes llm_prompt and parameters)
"""
# Example
tools = await self.plugin.list_tools()
for tool in tools:
print(f"Tool: {tool['name']}")
```
### Get Tool Detail
Get detailed information about a specific tool.
```python
async def get_tool_detail(self, tool_name: str) -> dict[str, Any]:
"""Get detailed information about a specific tool
Args:
tool_name: Tool name (metadata.name, e.g. "get_weather_alerts")
Returns:
Tool detail dict containing name, label, description, spec (with parameters and llm_prompt)
"""
# Example
detail = await self.plugin.get_tool_detail("get_weather_alerts")
print(detail['spec']['parameters'])
```
### Call Tool
Call a specific tool.
```python
async def call_tool(
self,
tool_name: str,
parameters: dict[str, Any],
session: dict[str, Any],
query_id: int,
) -> dict[str, Any]:
"""Call a specific tool
Args:
tool_name: Tool name (metadata.name)
parameters: Tool parameters
session: Session info
query_id: Query ID
Returns:
Tool response dict
"""
# Example
result = await self.plugin.call_tool(
tool_name="get_weather_alerts",
parameters={"state": "CA"},
session={},
query_id=0,
)
print(result)
```
<Tip>
Tool names use `metadata.name` (e.g. `echo_tool`, `get_weather_alerts`), without author or plugin name prefix.
</Tip>
### Set Plugin Persistent Data
Persistently store plugin data. Data stored through this interface can only be accessed by this plugin. Values need to be converted to bytes manually.
```python
async def set_plugin_storage(self, key: str, value: bytes) -> None:
"""Set a plugin storage value"""
# Usage example
await self.plugin.set_plugin_storage("key", b"value")
```
### Get Plugin Persistent Data
```python
async def get_plugin_storage(self, key: str) -> bytes:
"""Get a plugin storage value"""
# Usage example
plugin_storage = await self.plugin.get_plugin_storage("key")
```
### Get All Plugin Persistent Data Keys
```python
async def get_plugin_storage_keys(self) -> list[str]:
"""Get all plugin storage keys"""
# Usage example
plugin_storage_keys = await self.plugin.get_plugin_storage_keys()
```
### Delete Plugin Persistent Data
```python
async def delete_plugin_storage(self, key: str) -> None:
"""Delete a plugin storage value"""
# Usage example
await self.plugin.delete_plugin_storage("key")
```
### Get Workspace Persistent Data
Data stored through this interface can be accessed by all plugins. Values need to be converted to bytes manually.
```python
async def set_workspace_storage(self, key: str, value: bytes) -> None:
"""Set a workspace storage value"""
# Usage example
await self.plugin.set_workspace_storage("key", b"value")
```
### Get Workspace Persistent Data
```python
async def get_workspace_storage(self, key: str) -> bytes:
"""Get a workspace storage value"""
# Usage example
workspace_storage = await self.plugin.get_workspace_storage("key")
```
### Get All Workspace Persistent Data Keys
```python
async def get_workspace_storage_keys(self) -> list[str]:
"""Get all workspace storage keys"""
# Usage example
workspace_storage_keys = await self.plugin.get_workspace_storage_keys()
```
### Delete Workspace Persistent Data
```python
async def delete_workspace_storage(self, key: str) -> None:
"""Delete a workspace storage value"""
# Usage example
await self.plugin.delete_workspace_storage("key")
```
### Get Plugin File-typed Config Field Data
```python
async def get_config_file(self, file_key: str) -> bytes:
"""Get a config file value"""
# Usage example
file_bytes = await self.plugin.get_config_file("key")
```
Use this in conjunction with configuration fields of type [``file or array[file``](/en/plugin/dev/basic-info.html#type-file).
## Knowledge Base API
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
List all available knowledge bases in the LangBot instance.
```python
async def list_knowledge_bases(self) -> list[dict[str, Any]]:
"""List all knowledge bases
Returns:
List of knowledge base dicts, each containing:
- uuid: Knowledge base UUID
- name: Knowledge base name
- description: Knowledge base description
"""
# Usage example
knowledge_bases = await self.plugin.list_knowledge_bases()
for kb in knowledge_bases:
print(f"KB: {kb['name']} ({kb['uuid']})")
```
### Retrieve from Knowledge Base
Retrieve relevant documents from any knowledge base.
```python
async def retrieve_knowledge(
self,
kb_id: str,
query_text: str,
top_k: int = 5,
filters: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
"""Retrieve from a knowledge base
Args:
kb_id: Knowledge base UUID (from list_knowledge_bases)
query_text: Search query text
top_k: Number of results to return (default: 5)
filters: Optional metadata filters for retrieval
Returns:
List of retrieval result entries
"""
# Usage example
results = await self.plugin.retrieve_knowledge(
kb_id="kb-uuid-here",
query_text="How to configure the system?",
top_k=3,
)
for entry in results:
print(entry)
```
<Tip>
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.
</Tip>
## RAG API
These APIs are available for `KnowledgeEngine` components to access the LangBot host's embedding models, vector database, and file storage. Access method:
- In `KnowledgeEngine` component classes: Internal methods of the `self.plugin` object.
### Invoke Embedding Model
Generate text embeddings using the host's configured embedding model.
```python
async def invoke_embedding(
self,
embedding_model_uuid: str,
texts: list[str],
) -> list[list[float]]:
"""Generate embeddings using host's embedding model
Args:
embedding_model_uuid: Embedding model UUID
texts: List of texts to embed
Returns:
List of embedding vectors, one per input text
"""
# Usage example
vectors = await self.plugin.invoke_embedding("model_uuid", ["Hello", "World"])
```
### Vector Upsert
Upsert vectors to the host's vector database.
```python
async def vector_upsert(
self,
collection_id: str,
vectors: list[list[float]],
ids: list[str],
metadata: list[dict[str, Any]] | None = None,
documents: list[str] | None = None,
) -> None:
"""Upsert vectors
Args:
collection_id: Target collection ID
vectors: List of vectors
ids: List of unique IDs for vectors
metadata: Optional list of metadata dicts
documents: Optional raw text documents. Required for full-text
and hybrid search in backends that support them.
"""
# Usage example
await self.plugin.vector_upsert(
collection_id="kb_uuid",
vectors=[[0.1, 0.2, ...], [0.3, 0.4, ...]],
ids=["chunk_0", "chunk_1"],
metadata=[{"document_id": "doc1"}, {"document_id": "doc1"}],
documents=["chunk text 0", "chunk text 1"],
)
```
### Vector Search
Search similar vectors in the host's vector database.
```python
async def vector_search(
self,
collection_id: str,
query_vector: list[float],
top_k: int = 5,
filters: dict[str, Any] | None = None,
search_type: str = "vector",
query_text: str = "",
) -> list[dict[str, Any]]:
"""Vector search
Args:
collection_id: Target collection ID
query_vector: Query vector for similarity search
top_k: Number of results to return
filters: Optional metadata filters
search_type: One of 'vector', 'full_text', 'hybrid'
query_text: Raw query text, used for full_text and hybrid search
Returns:
List of search results (dict with id, score, metadata, etc.)
"""
# Usage example
results = await self.plugin.vector_search(
collection_id="kb_uuid",
query_vector=[0.1, 0.2, ...],
top_k=5,
search_type="hybrid",
query_text="search query",
)
# Return format: [{"id": "chunk_0", "score": 0.123, "metadata": {"document_id": "doc1", ...}}, ...]
```
<Info>
Each result returned by `vector_search` is a dict containing `id` (vector ID), `score` (distance score), and `metadata` (metadata provided during upsert). If you need text content in retrieval results, store the text in metadata during ingestion.
</Info>
### Vector Delete
Delete vectors from the host's vector database.
```python
async def vector_delete(
self,
collection_id: str,
file_ids: list[str] | None = None,
filters: dict[str, Any] | None = None,
) -> int:
"""Vector delete
Args:
collection_id: Target collection ID
file_ids: File IDs whose vectors should be deleted
filters: Optional metadata filters for deletion
Returns:
Number of deleted items
"""
# Usage example
deleted = await self.plugin.vector_delete(
collection_id="kb_uuid",
file_ids=["doc_001"],
)
```
<Info>
The `filters` parameter supports Chroma-style `where` syntax for metadata filtering. Multiple top-level keys are AND-ed. Supported operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`.
```python
# Implicit $eq
results = await self.plugin.vector_search(
collection_id="kb_uuid",
query_vector=[0.1, 0.2, ...],
filters={"file_id": "abc"},
)
# Comparison operator
results = await self.plugin.vector_search(
collection_id="kb_uuid",
query_vector=[0.1, 0.2, ...],
filters={"created_at": {"$gte": 1700000000}},
)
# In-list operator
results = await self.plugin.vector_search(
collection_id="kb_uuid",
query_vector=[0.1, 0.2, ...],
filters={"file_type": {"$in": ["pdf", "docx"]}},
)
# Delete by filter
deleted = await self.plugin.vector_delete(
collection_id="kb_uuid",
filters={"file_type": {"$eq": "pdf"}},
)
```
**Note:** Chroma, Qdrant, and SeekDB store full metadata and can filter on any field. Milvus and pgvector only store `text`, `file_id`, and `chunk_uuid` — filters on other fields will be silently ignored.
</Info>
Get uploaded file content from the host's storage.
```python
async def get_knowledge_file_stream(self, storage_path: str) -> bytes:
"""Get file content
Args:
storage_path: File storage path (from FileObject.storage_path)
Returns:
File content as 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.
### 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.
+134
View File
@@ -0,0 +1,134 @@
---
title: "LLM request configuration"
description: "Retries, deadlines, reasoning budgets, caching and stream cancellation for plugins and Runners"
---
These options require a Core build containing [Core #2661](https://github.com/langbot-app/LangBot/pull/2661). A merged PR does not imply a published release. Pass them through the existing `extra_args`; plugin and Runner signatures, message types and authorization remain unchanged. See [LangBot API](/en/plugin/dev/apis/common) and [Context APIs](/en/plugin/dev/apis/agent-run).
## Retries and timeouts
The following non-streaming example configures the request and reads complete 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!")],
timeout=110,
extra_args={
"request_policy": {
"retries": 1,
"total_timeout": 90,
"first_event_timeout": 30,
"stream_idle_timeout": 30,
"connect_timeout": 10,
"read_timeout": 60,
},
},
)
print(result.message.content)
if result.usage is not None:
print(result.usage.model_dump())
```
Runner `ctx.api.invoke_llm*` accepts the same `extra_args`, subject to run authorization and remaining time. Calls through `self.plugin` also inherit these restrictions inside a run context.
`timeout=` is the SDK/RPC waiting limit (120 seconds by default for non-streaming calls). Core's `request_policy` controls the inner request. Keep its total budget within the enclosing RPC/Runner deadline; raising it cannot extend that deadline. Streaming methods have no `timeout=` argument and remain bounded by SDK/Runner waiting limits.
| `request_policy` field | Default | Meaning |
| --- | --- | --- |
| `retries` | `0` | Additional attempts after the first, integer 0–5 |
| `total_timeout` | `300` | Whole invocation budget, including backoff |
| `first_event_timeout` | `120` | Wait for the first upstream event, not the first visible text |
| `stream_idle_timeout` | `120` | Idle time between stream events |
| `connect_timeout` | `10` | Connection timeout |
| `read_timeout` | `120` | Transport read timeout |
| `write_timeout` | `30` | Transport write timeout |
| `pool_timeout` | `10` | Connection pool acquisition timeout |
| `backoff_base` / `backoff_max` | `0.5` / `8` | Exponential backoff base and cap, with full jitter |
| `attribution` | `True` | Automatic application attribution headers |
| `conversation_header` | `None` | Generated session header name, described below |
Times are seconds; timeouts must be finite and positive. Unknown policy fields are rejected. Precedence: instance `llm_request_policy`, model `extra_args`, then call `extra_args`. Within one layer, `request_policy.retries` wins over legacy `num_retries` / `max_retries`. A call-level legacy value still overrides model policy; `0` disables retries. Legacy numeric `extra_args["timeout"]` sets read, first-event and stream-idle limits without extending the total budget.
Authentication, invalid-request and exhausted-quota failures are not retried. Retries stop after any upstream event, including reasoning or usage. `Retry-After` is bounded by the remaining budget. Core disables LiteLLM and provider SDK retries for managed calls; gateways may independently retry. This adds no tool replay, model fallback or embedding/rerank retries. Legacy third-party requesters still own their transport configuration.
## Reasoning budgets
Existing `reasoning_level` behavior is unchanged. For a numeric budget, pass this object as `extra_args` or save it in model advanced parameters. Omitting `level` means `enabled`:
```json
{
"reasoning_config": {"budget_tokens": 2048},
"max_tokens": 4096
}
```
Do not combine a budget with an effort level such as `medium`, `disabled`, or native reasoning parameters. Direct Anthropic calls require explicit `max_tokens` greater than the budget; Core never raises that limit automatically.
Precedence: persisted model `reasoning_config`, model `extra_args.reasoning_config`, call `extra_args.reasoning_config`, then an explicit Runner level override. Each object replaces the previous one; an effort override clears a lower-priority budget.
Numeric budgets map to native Anthropic, Gemini 2.5, supported Qwen models and OpenRouter routes. Direct OpenAI/Codex and adaptive-only Claude models retain effort control. Check the model's `reasoning_capabilities.budget_tokens` for support and bounds. Unsupported budgets fail before HTTP. Providers/gateways may impose stricter limits; OpenRouter may translate a numeric budget into effort.
## Prompt caching
`extra_args.prompt_caching` is consumed by Core, not forwarded unchanged. Call configuration replaces model configuration. The default `{"mode": "provider_default"}` adds no fields. This is not a local response cache.
| Configuration | Behavior |
| --- | --- |
| `{"mode": "openai", "retention": "in_memory"}` | Generates a model-and-conversation-scoped `prompt_cache_key` when conversation context exists; optional `retention` also accepts `24h` if supported by the target |
| `{"mode": "anthropic", "ttl": "5m"}` | Adds an ephemeral breakpoint to the last text block of the leading system/developer prefix; `ttl` accepts `5m` or `1h` |
OpenAI mode applies to compatible routes and OpenRouter OpenAI models supporting these options. Explicit native `prompt_cache_key`, `prompt_cache_retention` and `prompt_cache_options` win. Anthropic mode supports native Claude and OpenRouter Claude. Existing native cache markers are preserved without adding duplicates. Core does not reorder tools/history or mark user messages, and inserts nothing without an eligible prefix.
Hints do not guarantee hits. Inspect `usage.model_dump()` for provider fields such as `prompt_tokens_details.cached_tokens` or `cache_read_input_tokens`; missing counts are not zero. Minimum prefix lengths, retention eligibility and pricing are provider-controlled. Codex's restricted Responses body does not accept this configuration object; existing server caching remains. General Responses migration and Gemini explicit cache-resource management are outside this change.
## Application and session headers
Direct OpenRouter calls receive `HTTP-Referer: https://langbot.app`, `X-OpenRouter-Title: LangBot`, and, with conversation context, `x-session-id: lb-<opaque scoped hash>`. Custom gateway addresses do not automatically inherit these headers. `request_policy.attribution=False` disables only automatic application attribution. Explicit `extra_headers` remain and take precedence case-insensitively.
A gateway can use an explicitly configured session header:
```json
{
"request_policy": {"conversation_header": "x-litellm-session-id"},
"extra_headers": {
"HTTP-Referer": "https://langbot.app",
"X-OpenRouter-Title": "LangBot"
}
}
```
Supported names: `x-session-id`, `x-litellm-session-id`, `x-langbot-conversation-id`. The last is a custom convention, not a universal provider standard, and provides no automatic routing or caching benefit. Generated identifiers are scoped by workspace/bot/pipeline/session and contain no raw session ID or prompt. They are pseudonymous, not a guarantee of anonymization, and are omitted without context. New API and other gateways may filter or rewrite headers; verify forwarding rules and actual outbound requests.
## Stream cancellation and compatibility
Use `aclosing` when leaving a stream early. Allow task cancellation exceptions to propagate:
```python
from contextlib import aclosing
from langbot_plugin.api.entities.builtin.provider.message import Message
async with aclosing(self.plugin.invoke_llm_stream_events(
llm_model_uuid=model_uuid,
messages=[Message(role="user", content="Hello!")],
extra_args={"request_policy": {"retries": 0, "total_timeout": 90}},
)) as events:
async for event in events:
if event.usage is not None:
print(event.usage.model_dump())
if event.chunk is not None and event.chunk.content:
print(event.chunk.content)
break
```
This example intentionally stops after the first nonempty content chunk and may miss final usage. For a complete response, remove `break`; a final event can have usage without a chunk. `invoke_llm` still returns `Message`, and `invoke_llm_stream` still yields `MessageChunk`. Choose `invoke_llm_with_usage` or `invoke_llm_stream_events` for accounting. Usage may be `None`; nested cache/reasoning counts and provider-specific fields are preserved.
SDK ≤0.7.11 has a proxy-layer defect that delays upstream cancellation on early close. Deterministic close requires **langbot-plugin ≥0.7.12**, which publishes the fix from [SDK #177](https://github.com/langbot-app/langbot-plugin-sdk/pull/177). A Core dependency pin does not automatically upgrade a separately deployed Runtime or plugin development environment; upgrade those executing environments too. Ordinary signatures, payloads and return types remain compatible. Total deadlines include consumer time, but cannot interrupt a consumer while the generator is suspended at `yield`; expiration is enforced on the next pull.
## Diagnostics
Core monitoring records expose `details.timing`, complete `usage` and structured `error`; legacy SDK exception transport is unchanged. Structured errors include `code`, `status_code`, `retryable`, `retry_after`, `request_id` and `phase`. Do not assume these fields exist on legacy exceptions received by plugins.
Millisecond timings include `preparation_ms`, `transport_setup_ms`, `first_event_ms`, `first_reasoning_ms`, `first_content_ms`, `first_visible_chunk_ms`, `total_ms`, plus `attempts` and `retry_wait_ms`. The first event may be reasoning/control, and the first visible chunk may include reasoning. `request_started_ms` marks the SDK call, not a packet leaving the socket. Missing fields mean unobserved, not zero. Monitoring retains partial usage received before cancellation but cannot reconstruct missing upstream accounting.
+65
View File
@@ -0,0 +1,65 @@
---
title: "Message Platform Entities"
---
LangBot supports multiple messaging platforms, and each platform has different `message entity` formats. To abstract away these differences, we have implemented a unified standard. Plugin developers only need to master and use the message entities described on this page in plugins, and LangBot's internal message processing logic can automatically handle message parsing and conversion.
## Message Chain
`Messages` on messaging platforms are different from messages used for AI interaction. Messages on messaging platforms are described in the form of `message chains`. Each independent message displayed on QQ is a message chain, which can contain various `message chain components` such as `text`, `images`, `@mentions`, etc. For example:
![](/images/zh/plugin/dev/apis/plugin_dev_messages_01.png)
This is a message chain containing one Plain component (Hello World) and one Image component (Usagi).
The definitions of message chains and message chain components are located in `langbot_plugin.api.entities.builtin.platform.message`.
### Constructing Message Chains
Please first import the `langbot_plugin.api.entities.builtin.platform.message` package to use the message components within it.
```python
from langbot_plugin.api.entities.builtin.platform.message import *
# Build a message containing text "Hello LangBot" and an image (from URL)
msg_chain = MessageChain([
Plain(text="Hello LangBot"),
Image(url='https://qchatgpt.rockchin.top/langbot-logo.png')
])
# Build a message containing @everyone and text "Hello LangBot"
msg_chain = MessageChain([
AtAll(),
Plain(text="Hello LangBot")
])
# Build a message containing @specific member and text "Hello LangBot"
msg_chain = MessageChain([
At(target=123456),
Plain(text="Hello LangBot")
])
```
Currently supported message chain components:
- `Source` - Source message chain information. Messages received from messaging platforms will include this component at the front of the message chain to record message information
- `Plain` - Plain text message
- `Quote` - Quote message
- `Image` - Image message
- `AtAll` - @everyone message
- `At` - @specific member message
- `Voice` - Voice message
- Need to check messaging platform support
- `Forward` - Forward message
- Not supported on many platforms, not recommended
- `File` - File message
When creating components, named parameters must be used. For specific usage, you can check the definitions in the source code `langbot_plugin.api.entities.builtin.platform.message`.
## 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.builtin.platform.entities`.
## Platform APIs
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.
+271
View File
@@ -0,0 +1,271 @@
---
title: "Pipeline Events and APIs"
---
LangBot plugins can register and handle pipeline events. For usage instructions, see [Component: Event Listener](/en/plugin/dev/components/event-listener).
## List of Events
Some events have `settable attributes`, which can be modified by plugin code and used in subsequent LangBot processing.
### \*MessageReceived
Triggered when any message is received in group chat or private chat.
```python
class PersonMessageReceived(BaseEventModel):
"""When any private chat message is received"""
event_name: str = "PersonMessageReceived"
launcher_type: str
"""Launcher object type (person)"""
launcher_id: typing.Union[int, str]
"""Sender ID"""
sender_id: typing.Union[int, str]
"""Sender ID, same as launcher_id in private chat"""
message_event: platform_events.PersonMessage
"""Original message event object. Contains sender information."""
message_chain: platform_message.MessageChain = pydantic.Field(
serialization_alias="message_chain"
)
"""Message chain"""
class GroupMessageReceived(BaseEventModel):
"""When any group chat message is received"""
event_name: str = "GroupMessageReceived"
launcher_type: str
"""Launcher object type (group)"""
launcher_id: typing.Union[int, str]
"""Group ID"""
sender_id: typing.Union[int, str]
"""Sender ID"""
message_event: platform_events.GroupMessage
"""Original message event object. Contains group and sender information."""
message_chain: platform_message.MessageChain = pydantic.Field(
serialization_alias="message_chain"
)
"""Message chain"""
```
### \*NormalMessageReceived
Triggered when a group chat or private chat message is received and determined to be a message that needs to be processed by LLM (non-command message).
```python
class PersonNormalMessageReceived(BaseEventModel):
"""Triggered when a private chat normal message that should be processed is determined"""
event_name: str = "PersonNormalMessageReceived"
launcher_type: str
"""Launcher object type (person)"""
launcher_id: typing.Union[int, str]
"""Launcher object ID"""
sender_id: typing.Union[int, str]
"""Sender ID, same as launcher_id in private chat"""
text_message: str
"""Message text"""
message_event: platform_events.PersonMessage
"""Original message event object. Contains sender information."""
message_chain: platform_message.MessageChain = pydantic.Field(
serialization_alias="message_chain"
)
"""Message chain"""
# ========== Settable Attributes ==========
user_message_alter: typing.Optional[provider_message.ContentElement] = None
"""Modified message text, langbot_plugin.api.entities.builtin.provider.message.ContentElement type"""
reply_message_chain: typing.Optional[platform_message.MessageChain] = None
"""Direct reply message chain, only effective when preventing default behavior"""
class GroupNormalMessageReceived(BaseEventModel):
"""Triggered when a group chat normal message that should be processed is determined"""
event_name: str = "GroupNormalMessageReceived"
launcher_type: str
"""Launcher object type (group)"""
launcher_id: typing.Union[int, str]
"""Group ID"""
sender_id: typing.Union[int, str]
"""Sender ID"""
text_message: str
"""Message text"""
message_event: platform_events.GroupMessage
"""Original message event object. Contains group and sender information."""
message_chain: platform_message.MessageChain = pydantic.Field(
serialization_alias="message_chain"
)
"""Message chain"""
# ========== Settable Attributes ==========
user_message_alter: typing.Optional[provider_message.ContentElement] = None
"""Modified message text, langbot_plugin.api.entities.builtin.provider.message.ContentElement type"""
reply_message_chain: typing.Optional[platform_message.MessageChain] = None
"""Direct reply message chain, only effective when preventing default behavior"""
```
### \*CommandSent
<Warning>
No longer recommended for use, please use [Component: Command](/en/plugin/dev/components/command) instead.
</Warning>
Triggered when a group chat or private chat command is received.
```python
class PersonCommandSent(BaseEventModel):
"""Triggered when a private chat command that should be processed is determined"""
event_name: str = "PersonCommandSent"
launcher_type: str
launcher_id: typing.Union[int, str]
sender_id: typing.Union[int, str]
command: str
"""Command text"""
params: list[str]
"""Command parameters"""
text_message: str
"""Message text"""
is_admin: bool
"""Whether it's an administrator"""
class GroupCommandSent(BaseEventModel):
"""Triggered when a group chat command that should be processed is determined"""
event_name: str = "GroupCommandSent"
launcher_type: str
launcher_id: typing.Union[int, str]
sender_id: typing.Union[int, str]
command: str
"""Command text"""
params: list[str]
"""Command parameters"""
text_message: str
"""Message text"""
is_admin: bool
"""Whether it's an administrator"""
```
### NormalMessageResponded
Triggered when a message receives an LLM response.
```python
class NormalMessageResponded(BaseEventModel):
"""Triggered when replying to a normal message"""
event_name: str = "NormalMessageResponded"
launcher_type: str
launcher_id: typing.Union[int, str]
sender_id: typing.Union[int, str]
session: provider_session.Session
"""Session object"""
prefix: str
"""Reply message prefix"""
response_text: str
"""Reply message text"""
finish_reason: str
"""Response end reason"""
funcs_called: list[str]
"""List of called functions"""
# ========== Settable Attributes ==========
reply_message_chain: typing.Optional[platform_message.MessageChain] = None
"""Reply message component list, only effective when preventing default behavior"""
```
### PromptPreProcessing
Triggered when building the LLM response context (prompt).
```python
class PromptPreProcessing(BaseEventModel):
"""Triggered when preprocessing prompt in session"""
event_name: str = "PromptPreProcessing"
session_name: str
"""Session name, format is person_1234567890 or group_1234567890"""
# ========== Settable Attributes ==========
default_prompt: list[typing.Union[provider_message.Message, provider_message.MessageChunk]]
"""Scenario preset (system prompt) for this conversation, can be modified, langbot_plugin.api.entities.builtin.provider.message.Message or langbot_plugin.api.entities.builtin.provider.message.MessageChunk type"""
prompt: list[typing.Union[provider_message.Message, provider_message.MessageChunk]]
"""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"""
```
## EventListener context
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.
### Prevent default behavior
Calling `event_context.prevent_default()` stops the default handling of the current event. Only these events support it:
<Info>
Only the following events can prevent default behavior:
- PersonMessageReceived
- GroupMessageReceived
- PersonNormalMessageReceived
- GroupNormalMessageReceived
- PersonCommandSent
- GroupCommandSent
- NormalMessageResponded
</Info>
Call `event_context.prevent_postorder()` to stop later plugins from handling the current event.
+78
View File
@@ -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`.
## Runner
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 = 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))
```
+13
View File
@@ -0,0 +1,13 @@
---
title: "Plugin Technical Details"
---
## Architecture
<img width="600" src="/images/zh/plugin/dev/plugin_system_arch.png" />
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.
Plugin Runtime is used to manage plugin lifecycles and coordinate interactions between LangBot and plugins. It has two operating modes: `stdio` and `websocket`. When LangBot is started directly by users (not running in a container), it uses `stdio` mode, which is common for personal users or lightweight environments. When LangBot runs in a container, it uses `websocket` mode, designed specifically for production environments.
Plugin Runtime automatically starts each installed plugin and interacts through stdio. In plugin development scenarios, developers can use the `lbp` command-line tool to start plugins and connect to the running Runtime via WebSocket for debugging.
+313
View File
@@ -0,0 +1,313 @@
---
title: "Completing Plugin Configuration Information"
---
## Modifying the Manifest File
The `manifest.yaml` file in the plugin directory declares the basic information about the plugin, which will be displayed in the LangBot UI or plugin marketplace interfaces.
```yaml
apiVersion: v1 # Do not modify
kind: Plugin # Do not modify
metadata:
author: RockChinQ # Author, must match regex ^[a-zA-Z0-9_-]+$
name: HelloPlugin # Plugin name, used to distinguish plugins, must match regex ^[a-zA-Z0-9-]+$
repository: 'https://github.com/langbot-app/HelloPlugin' # Plugin repository URL
version: 0.1.0 # Plugin version
description:
en_US: 'Hello LangBot Plugin' # Plugin description, multilingual
zh_Hans: 'Hello LangBot Plugin'
label:
en_US: HelloPlugin # Plugin label, displayed in the interface, multilingual
zh_Hans: HelloPlugin
icon: assets/icon.svg # Plugin icon, defaults to assets/icon.svg, can be replaced with various image formats
spec:
config: [] # Plugin configuration item format
components: {} # Plugin component list, no need to modify manually
execution:
python:
path: main.py # Do not modify
attr: HelloPlugin # Do not modify
```
To certify your plugin and let several installations share one runtime, also declare `sharedRuntime` and `componentModel` under `execution`; see [Certified plugins and shared runtime](/en/plugin/certified-plugins).
Plugin multilingual support follows the [RFC 4646](https://datatracker.ietf.org/doc/html/rfc4646) standard. Currently supported languages include:
- `en_US` English (required)
- `zh_Hans` Simplified Chinese
- `zh_Hant` Traditional Chinese
- `ja_JP` Japanese
- `vi_VN` Vietnamese
- `th_TH` Thai
- `es_ES` Spanish
## Plugin Configuration Item Format
In the `manifest.yaml` file, fields declared in `spec.config` will be rendered by LangBot as configuration item forms for users to fill out. Plugins can retrieve user-filled configuration items through APIs provided by the SDK (see later sections).
For example:
```yaml
spec:
config:
- name: github_token # Required; configuration item name, used for retrieval in the plugin
type: string # Required; configuration item type, supports string, integer, float, boolean, select, prompt-editor, llm-model-selector, bot-selector, tools-selector, etc.
label: # Required; configuration item display name, supports multilingual. Language codes follow RFC 4646 standard.
en_US: Github Token
zh_Hans: Github Token
description: # Configuration item description, supports multilingual. Optional.
en_US: Image downloading requires a Github token
zh_Hans: 如果不填的话,图片可能会下载失败
default: '' # Configuration item default value, optional.
required: false # Whether the configuration item is required, optional.
show_if: # Conditional rendering (shows when conditions are met), optional.
field: mode
operator: eq
value: mode1
- name: mode
type: select
...
```
The supported types and fields for configuration items are as follows:
### Conditional Rendering (`show_if`)
All types of configuration items support conditional rendering using the `show_if` field. You can decide whether the current field is rendered based on the value of another field in the same (or an associated) form.
```yaml
- name: advanced_setting
type: string
show_if:
field: mode # The name of the dependent field
operator: eq # The operator, supports 'eq' (equals), 'neq' (not equals), 'in' (in a list)
value: advanced # The value to check against
```
> **Note:** Cross-form cascading relationships (e.g. `creation_schema` values reading from `retrieval_schema` values and vice versa in a Knowledge Engine) are supported in the latest LangBot frontend.
### type: string
String type. Optionally supports `options` for preset values — when provided, a dropdown button appears next to the input allowing users to quickly fill in a preset value. Users can still type freely.
```yaml
- name: api_key
type: string
...
```
With preset options:
```yaml
- name: api_url
type: string
options: # Optional preset values, displayed as a dropdown button next to the input.
- name: "https://api.openai.com/v1" # The value to fill in when selected.
label: # Display name, supports multilingual.
en_US: OpenAI Official
zh_Hans: OpenAI 官方
- name: "https://api.deepseek.com/v1"
label:
en_US: DeepSeek
zh_Hans: DeepSeek
...
```
### type: array[string]
String array.
```yaml
- name: tags
type: array[string]
...
```
### type: integer
Integer type.
```yaml
- name: progress
type: integer
...
```
### type: float
Float type.
```yaml
- name: temperature
type: float
...
```
### type: boolean
Boolean type.
```yaml
- name: is_enabled
type: boolean
...
```
### type: select
Dropdown menu. Requires configuring `options`, which represents the dropdown menu options. Each option displays its `label` as the primary text, with the `name` (value) shown in smaller text below.
```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.
en_US: Mode 1
zh_Hans: 模式 1
- name: mode2
label: # Display name, supports multilingual.
en_US: Mode 2
zh_Hans: 模式 2
...
```
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.
```yaml
- name: prompt
type: prompt-editor
...
```
### type: text
Large text input. Rendered as a textarea in the frontend for user input, and passed to the plugin as a string.
```yaml
- name: prompt
type: text
...
```
### type: file
File upload. Supports file uploads up to 10MB, passed to the plugin in the format `{"file_key": "xxxxx.xxx", "mimetype": "xxxxx"}`. The plugin can use the `get_config_file` API to retrieve the file content.
```yaml
- name: config_file
type: file
accept: 'application/json' # Optional, specify accepted file MIME types
...
```
Retrieving the file in the plugin:
```python
# Get file information from config
file_config = self.get_config()['config_file']
file_key = file_config['file_key']
mimetype = file_config['mimetype']
# Example output: {'file_input': {'file_key': 'plugin_config_d2234fd802054faf80babe0679a97fa9.json', 'mimetype': 'application/json'}}
print(file_config)
# Get file content
file_bytes = await self.get_config_file(file_key)
```
<Info>
Common MIME type and file extension reference: [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types)
</Info>
### type: array[file]
Multiple file upload. Similar to the file type, but supports uploading multiple files, passed to the plugin in the format `[{"file_key": "xxxxx.xxx", "mimetype": "xxxxx"}]`.
```yaml
- name: resource_files
type: array[file]
accept: 'image/*' # Optional, specify accepted file MIME types
...
```
Retrieving files in the plugin:
```python
# Get file list from config
files_config = self.get_config()['resource_files']
for file_config in files_config:
file_key = file_config['file_key']
mimetype = file_config['mimetype']
# Get file content
file_bytes = await self.get_config_file(file_key)
```
### type: llm-model-selector
LLM model selector. Displays an LLM model selector where you can choose configured LLM models, with the final result represented as the LLM model UUID.
```yaml
- name: model
type: llm-model-selector
...
```
### type: bot-selector
Bot selector. Displays a Bot selector where you can choose configured Bots, with the final result represented as the Bot UUID.
```yaml
- name: bot
type: bot-selector
...
```
Using the Bot UUID in the plugin:
```python
# Get Bot UUID from config
bot_uuid = self.get_config()['bot']
print(bot_uuid) # Output: '550e8400-e29b-41d4-a716-446655440000'
```
### type: tools-selector
Tool selector. Displays a multi-select tool selector where you can choose registered tools (plugin tools and MCP tools), with the final result represented as an array of tool names.
```yaml
- name: selected_tools
type: tools-selector
...
```
Using the selected tool names in the plugin:
```python
# Get selected tool names from config
selected_tools = self.get_config()['selected_tools']
print(selected_tools) # Output: ['get_weather_alerts', 'web_search']
# Call selected tools
for tool_name in selected_tools:
result = await self.plugin.call_tool(
tool_name=tool_name,
parameters={"query": "hello"},
session={},
query_id=0,
)
```
## What's Next
This tutorial will guide you through step-by-step completion of plugin functionality.
- Adding Components: Plugin components are the core functional units of plugins. You can [add components](/en/plugin/dev/components/add) based on your needs.
+81
View File
@@ -0,0 +1,81 @@
---
title: "Adding Components"
---
Plugins consist of one or more components that provide different functionalities to LangBot. Currently supported component types include:
- 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.
- Knowledge Engines (KnowledgeEngine): Provide knowledge base indexing and retrieval capabilities for LangBot, managing document ingestion, retrieval, and deletion.
- Parsers (Parser): Parse binary files such as PDF, Word, etc. into structured text before document ingestion, for use by Knowledge Engines.
- Pages (Page): Register custom visual pages in the LangBot WebUI sidebar, providing admin panels, data displays, and interactive interfaces.
## Adding Components
Execute the command in the plugin directory:
```bash
lbp comp <component_type>
```
For example, to add an event listener:
```bash
lbp comp EventListener
```
Follow the prompts to enter the component configuration (if any).
```bash
➜ HelloPlugin > lbp comp EventListener
Generating component EventListener...
Component EventListener generated successfully.
组件 EventListener 生成成功。
➜ HelloPlugin >
```
<Info title="">
The CLI will generate a `components` directory in the plugin directory and create corresponding component directories within it.
```
.
├── assets
│ └── icon.svg
├── components
│ ├── __init__.py
│ └── event_listener
│ ├── __init__.py
│ ├── default.py
│ └── default.yaml
├── main.py
├── manifest.yaml
├── README.md
└── requirements.txt
```
At the same time, component discovery configuration will be added to the plugin's `manifest.yaml`:
```yaml
...
components:
EventListener:
fromDirs:
- path: components/event_listener/
...
```
To remove components, you can delete the corresponding information manually.
</Info>
## Component Types
For detailed usage of each component, please refer to:
- [Runner](./runner)
- [EventListener](./event-listener)
- [Command](./command)
- [Tool](./tool)
- [KnowledgeEngine](./knowledge-engine)
- [Parser](./parser)
- [Page](./page)
+226
View File
@@ -0,0 +1,226 @@
---
title: "Component: Command"
---
Command components are triggered by user command messages starting with `!` (or other configured prefixes). Here's an example of triggering the `!help` command:
<img width="600" src="/images/zh/plugin/dev/components/command_use.png" />
## Adding Command Components
A single plugin can contain any number of commands. Execute the command `lbp comp Command` in the plugin directory and follow the prompts to enter the command configuration.
```bash
➜ HelloPlugin > lbp comp Command
Generating component Command...
Command name: info
Command description: Show information of the query
Component Command generated successfully.
组件 Command 生成成功。
```
This will generate `info.yaml` and `info.py` files in the `components/commands/` directory. The `.yaml` file defines the basic information for the `!info` command, and the `.py` file is the command handler:
```bash
➜ HelloPlugin > tree
...
├── components
│ ├── __init__.py
│ ├── commands
│ │ ├── __init__.py
│ │ ├── info.py
│ │ └── info.yaml
...
```
## Manifest File: Command Component
```yaml
apiVersion: v1 # Do not modify
kind: Command # Do not modify
metadata:
name: info # Command name, users will trigger this command with !info
label: # Command display name, shown in LangBot UI, supports multilingual
en_US: Info
zh_Hans: Info
description: # Command description, shown in LangBot UI, supports multilingual. Optional.
en_US: 'Show information of the query'
zh_Hans: '发送此次消息的详细信息'
spec:
execution:
python:
path: info.py # Command handler, do not modify
attr: Info # Command handler class name, matches the class name in info.py
```
## Plugin Processing
The following code is generated by default (`components/command/<command_name>.py`). You need to register and implement subcommand processing logic in the `initialize` method of the `Info` class.
```python
# Auto generated by LangBot Plugin SDK.
# Please refer to https://langbot.app/docs/en/plugin/dev/tutor.html for more details.
from __future__ import annotations
from typing import Any, AsyncGenerator
from langbot_plugin.api.definition.components.command.command import Command, Subcommand
from langbot_plugin.api.entities.builtin.command.context import ExecuteContext, CommandReturn
class Info(Command):
async def initialize(self):
await super().initialize()
"Fill with your code here"
```
Adding subcommands:
```python
...
class Info(Command):
async def initialize(self):
await super().initialize()
@self.subcommand(
name="", # Empty string represents root command
help="Show information of the query", # Command help information
usage="info", # Command usage example, displayed in command help
aliases=["i"], # Command aliases
)
async def send(self, context: ExecuteContext) -> AsyncGenerator[CommandReturn, None]:
print(context)
reply_text = f"Query ID: {context.query_id}\n"
reply_text += f"command: {context.command}\n"
reply_text += f"command_text: {context.command_text}\n"
reply_text += f"params: {context.params}\n"
reply_text += f"crt_params: {context.crt_params}\n"
reply_text += f"privilege: {context.privilege}\n"
reply_text += f"session: {context.session.launcher_type.value}_{context.session.launcher_id}\n"
yield CommandReturn(
text=reply_text,
)
@self.subcommand(
name="field",
help="Show information of the field",
usage="info field",
aliases=["f"],
)
async def field(self, context: ExecuteContext) -> AsyncGenerator[CommandReturn, None]:
print(context)
field_name = context.crt_params[0]
field_value = getattr(context, field_name)
yield CommandReturn(
text=f"{field_name}: {field_value}",
)
```
In this code, the `send` function is registered as a subcommand through the `@self.subcommand` decorator, prints the command context (ExecuteContext) information, and concatenates it into a reply message.
### Subcommand Registration
`name` is the subcommand name. Leaving it empty means handling the main command. If not empty, it matches the second parameter as the command. For example:
- `!info` matches the subcommand with name=""
- `!info field` matches the subcommand with name="field"
- `!info field value` matches the subcommand with name="field", with `value` as the subcommand parameter
- Specialy, `name="*"` matches all unmatched first-level subcommands, and passes each section after `info` as parameters, for example `!info 123`, `!info abc`. You can get `['123']` or `['abc']` from `context.crt_params`, depending on the user's input.
In subcommand functions, you can read command parameters through the `context` variable.
The effect of the above command is shown in the image:
<img width="600" src="/images/zh/plugin/dev/components/info_command_demo.png" />
### Command Context
```python
class ExecuteContext(pydantic.BaseModel):
"""Single command execution context"""
query_id: int
"""Request ID"""
session: provider_session.Session
"""Session object for this message"""
command_text: str
"""Complete command text"""
command: str
"""Command name"""
crt_command: str
"""Current command
In multi-level commands, crt_command is the current command, command is the root command.
Example: !plugin on Webwlkr
When processing plugin, command is plugin, crt_command is plugin
When processing on, command is plugin, crt_command is on
"""
params: list[str]
"""Command parameters
Parameter list after splitting the entire command by spaces
"""
crt_params: list[str]
"""Current command parameters
In multi-level commands, crt_params are current command parameters, params are root command parameters.
Example: !plugin on Webwlkr
When processing plugin, params is ['on', 'Webwlkr'], crt_params is ['on', 'Webwlkr']
When processing on, params is ['on', 'Webwlkr'], crt_params is ['Webwlkr']
"""
privilege: int
"""Session privilege level"""
...
```
### Command Return Values
Command return values (CommandReturn) currently support returning text, images (image_url, image links), and errors.
```python
yield CommandReturn(
text=reply_text,
)
yield CommandReturn(
image_url=image_url,
)
```
Command processing supports returning multiple messages, so please use `yield` statements to return messages.
For specific return values, please refer to the definition of `CommandReturn`: [langbot_plugin.api.entities.builtin.command.context.CommandReturn](https://github.com/langbot-app/langbot-plugin-sdk/tree/main/src/langbot_plugin/api/entities/builtin/command/context.py)
## One installation, many usages
A single command is triggered **concurrently by several conversations**:
- Read config per call; never read it in `initialize()` and cache it on the instance.
- Never leave command arguments, return values or conversation data on instance fields or module-level variables; `ExecuteContext` is valid only for the current execution.
- Concurrent triggering means instance fields get interleaved writes — do not use them to hold "current conversation" style state. Data that must survive calls belongs in Host storage, keyed with the conversation identity.
- If you keep per-installation process-local caches, release them in `on_installation_revoked(binding)`.
See [Certified plugins and shared runtime](/en/plugin/certified-plugins) for the full specification.
## What's Next
You have learned the basic information about command registration and command execution. Next, you can:
- See the complete [`ExecuteContext` API reference](/en/plugin/dev/apis/agent-run#command).
- Check out [Plugin Common APIs](/en/plugin/dev/apis/common).
@@ -0,0 +1,119 @@
---
title: "Component: Event Listener"
---
<Info>
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).
</Info>
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
Execute the command `lbp comp EventListener` in the plugin directory. Creating an event listener component doesn't require any configuration.
```bash
➜ HelloPlugin > lbp comp EventListener
Generating component EventListener...
Component EventListener generated successfully.
组件 EventListener 生成成功。
```
This will generate `default.yaml` and `default.py` files in the `components/event_listener/` directory. The `.yaml` file defines the basic information for the event listener, and the `.py` file is the event listener handler:
```bash
➜ HelloPlugin > tree
...
├── components
│ ├── __init__.py
│ └── event_listener
│ ├── __init__.py
│ ├── default.py
│ └── default.yaml
...
```
## Manifest File: Event Listener
Since each plugin can only have one event listener, no modifications are needed in the manifest file.
## Plugin Processing
The following code is generated by default (components/event_listener/default.py). You need to register and implement event processing logic in the `initialize` method of the `DefaultEventListener` class.
```python
# Auto generated by LangBot Plugin SDK.
# Please refer to https://langbot.app/docs/en/plugin/dev/tutor.html for more details.
from __future__ import annotations
from langbot_plugin.api.definition.components.common.event_listener import EventListener
from langbot_plugin.api.entities import events, context
class DefaultEventListener(EventListener):
async def initialize(self):
await super().initialize()
"Fill with your code here"
```
Adding listeners for specific events:
```python
...
class DefaultEventListener(EventListener):
async def initialize(self):
await super().initialize()
@self.handler(events.PersonMessageReceived)
async def handler(event_context: context.EventContext):
print("Hello LangBot Plugin!")
print(event_context)
await event_context.reply(
platform_message.MessageChain([
platform_message.Plain(text=f"Hello from Nahida Plugin!"),
])
)
```
This code registers a listener for the `PersonMessageReceived` (receiving any message from private chat) event, prints the event context (EventContext) information when the event is triggered, and calls the event context API to reply with a message.
EventContext stores common context information for this event trigger, while EventContext.event is an object of PersonMessageReceived (or other corresponding event types), storing detailed information about the event.
```python
# Summary of EventContext definition
class EventContext(pydantic.BaseModel):
"""Event context, stores information for this event execution"""
...
event: pydantic.SerializeAsAny[BaseEventModel]
"""Event object, specific type is the type specified when registering the handler, can be found in events.py"""
...
```
## Event Registration
Event registration is implemented through the `@self.handler` decorator, with the decorator parameter being the event type. All monitorable events are defined in `langbot_plugin.api.entities.builtin.events`.
## One installation, many usages
The same event listener is called **concurrently by several Workspaces and several events**:
- Never accumulate state per event on the instance (counters, last event, conversation maps); concurrent callbacks will overwrite each other.
- A callback takes its data only from the current `EventContext`; never reuse a context or its fields across calls.
- Data that must survive calls belongs in Host storage, keyed with the installation identity and the event-related identity (Workspace, conversation, and so on).
- If you keep per-installation process-local state, release it in `on_installation_revoked(binding)`.
See [Certified plugins and shared runtime](/en/plugin/certified-plugins) for the full specification.
## What's Next
You have learned the basic information about event listeners. Next, you can:
- 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)
@@ -0,0 +1,490 @@
---
title: "Component: Knowledge Engine"
---
The Knowledge Engine component allows plugins to provide full knowledge base indexing and retrieval capabilities for LangBot. When users create a knowledge base in LangBot, they can choose a Knowledge Engine provided by a plugin to manage document ingestion, retrieval, and deletion. Plugins can also bridge advanced knowledge base services such as Dify, RAGFlow, FastGPT, and more.
## Adding a Knowledge Engine Component
A single plugin can add any number of Knowledge Engines. Execute the command `lbp comp KnowledgeEngine` in the plugin directory and follow the prompts to enter the Knowledge Engine configuration.
```bash
➜ MyRAGPlugin > lbp comp KnowledgeEngine
Generating component KnowledgeEngine...
Knowledge Engine name: simple_rag
Knowledge Engine description: A simple Knowledge Engine with built-in chunking and retrieval
Component KnowledgeEngine generated successfully.
```
This will generate `simple_rag.yaml` and `simple_rag.py` files in the `components/knowledge_engine/` directory. The `.yaml` file defines the Knowledge Engine's basic information and configuration schemas, and the `.py` file is the handler for this engine:
```bash
➜ MyRAGPlugin > tree
...
├── components
│ ├── __init__.py
│ └── knowledge_engine
│ ├── __init__.py
│ ├── simple_rag.py
│ └── simple_rag.yaml
...
```
## Manifest File: Knowledge Engine
```yaml
apiVersion: v1 # Do not modify
kind: KnowledgeEngine # Do not modify
metadata:
name: simple_rag # Knowledge Engine name, used to identify this engine
label:
en_US: Simple Knowledge Engine # Engine display name, shown in LangBot's UI, supports multiple languages
zh_Hans: 简易 RAG 引擎
ja_JP: シンプル RAG エンジン
description:
en_US: 'A simple Knowledge Engine with built-in chunking and retrieval'
zh_Hans: '内置分块和检索能力的简易 RAG 引擎'
ja_JP: 'チャンキングと検索を内蔵したシンプルなRAGエンジン'
spec:
creation_schema: # Configuration parameters users fill in when creating a knowledge base
- name: chunk_size
label:
en_US: Chunk Size
zh_Hans: 分块大小
type: integer
required: false
default: 500
- name: chunk_overlap
label:
en_US: Chunk Overlap
zh_Hans: 分块重叠
type: integer
required: false
default: 50
retrieval_schema: # Configurable parameters during retrieval
- name: score_threshold
label:
en_US: Score Threshold
zh_Hans: 分数阈值
type: float
required: false
default: 0.5
execution:
python:
path: simple_rag.py # Engine handler, do not modify
attr: SimpleRag # Class name of the engine handler, consistent with the class name in simple_rag.py
```
For configuration item format reference, see: [Plugin Manifest Configuration Format](/en/plugin/dev/basic-info)
### creation_schema and retrieval_schema
Unlike the old `KnowledgeRetriever` component which used a single `spec.config`, KnowledgeEngine uses two separate schemas:
- **creation_schema**: Parameters users fill in when creating a knowledge base. These are passed to the plugin via `creation_settings` during ingestion and retrieval.
- **retrieval_schema**: Parameters users can adjust when querying the knowledge base. These are passed to the plugin via `retrieval_settings`.
## Capability Declaration
KnowledgeEngine can declare its supported capabilities. LangBot uses these capability declarations to determine UI behavior and available operations:
```python
from langbot_plugin.api.definition.components.knowledge_engine.engine import KnowledgeEngine, KnowledgeEngineCapability
class SimpleRag(KnowledgeEngine):
@classmethod
def get_capabilities(cls) -> list[str]:
return [
KnowledgeEngineCapability.DOC_INGESTION, # Supports document upload and processing
]
```
Available capability constants:
| Capability | Description |
|-----------|-------------|
| `DOC_INGESTION` | Supports document upload and processing. When declared, LangBot shows a "Documents" tab in the knowledge base details. If the plugin is used to bridge other knowledge base services or does not require users to upload documents, this capability declaration can be omitted. |
| `DOC_PARSING` | Supports native document parsing (file-to-text extraction). When declared, users can choose to use the Knowledge Engine's built-in parsing when uploading documents, without needing an external [Parser plugin](./parser). If not declared, an external Parser plugin must be installed to upload documents. |
<Info>
Other retrieval behaviors (such as reranking, hybrid search, etc.) are controlled by the plugin's `retrieval_schema` and do not need capability flags.
</Info>
## Plugin Handler
The following code will be generated by default (`components/knowledge_engine/<engine_name>.py`). You need to implement the three core methods: `ingest`, `retrieve`, and `delete_document`. Complete code can be found in the `SimpleKnowledgeEngine` example in [langbot-plugin-demo](https://github.com/langbot-app/langbot-plugin-demo).
```python
from langbot_plugin.api.definition.components.knowledge_engine.engine import KnowledgeEngine, KnowledgeEngineCapability
from langbot_plugin.api.entities.builtin.rag.models import (
IngestionContext,
IngestionResult,
)
from langbot_plugin.api.entities.builtin.rag.context import (
RetrievalContext,
RetrievalResponse,
RetrievalResultEntry,
)
from langbot_plugin.api.entities.builtin.rag.enums import DocumentStatus
from langbot_plugin.api.entities.builtin.provider.message import ContentElement
class SimpleRag(KnowledgeEngine):
@classmethod
def get_capabilities(cls) -> list[str]:
return [KnowledgeEngineCapability.DOC_INGESTION]
async def on_knowledge_base_create(self, kb_id: str, config: dict) -> None:
"""Callback when a knowledge base is created, can be used to initialize resources"""
pass
async def on_knowledge_base_delete(self, kb_id: str) -> None:
"""Callback when a knowledge base is deleted, can be used to clean up resources"""
pass
async def ingest(self, context: IngestionContext) -> IngestionResult:
"""Ingest a document into the knowledge base"""
# 1. Get file content
file_bytes = await self.plugin.get_knowledge_file_stream(context.file_object.storage_path)
# 2. Parse document and split into chunks
text = file_bytes.decode('utf-8')
chunk_size = context.creation_settings.get('chunk_size', 500)
chunk_overlap = context.creation_settings.get('chunk_overlap', 50)
chunks = self._split_text(text, chunk_size, chunk_overlap)
# 3. Generate embeddings using host embedding model
embedding_model_uuid = context.creation_settings.get('embedding_model_uuid', '')
vectors = await self.plugin.invoke_embedding(embedding_model_uuid, chunks)
# 4. Write to host vector database
collection_id = context.get_collection_id()
ids = [f"{context.file_object.metadata.document_id}_{i}" for i in range(len(chunks))]
metadata = [{"document_id": context.file_object.metadata.document_id, "chunk_index": i, "text": chunks[i]} for i in range(len(chunks))]
await self.plugin.vector_upsert(collection_id, vectors, ids, metadata)
return IngestionResult(
document_id=context.file_object.metadata.document_id,
status=DocumentStatus.COMPLETED,
chunks_created=len(chunks),
)
async def retrieve(self, context: RetrievalContext) -> RetrievalResponse:
"""Retrieve relevant content from the knowledge base"""
# 1. Generate query vector
embedding_model_uuid = context.creation_settings.get('embedding_model_uuid', '')
query_vectors = await self.plugin.invoke_embedding(embedding_model_uuid, [context.query])
query_vector = query_vectors[0]
# 2. Vector search
collection_id = context.get_collection_id()
results = await self.plugin.vector_search(collection_id, query_vector, top_k=context.retrieval_settings.get('top_k', 5))
# 3. Convert to retrieval results
entries = []
for r in results:
entry = RetrievalResultEntry(
id=r.get('id', ''),
content=[ContentElement.from_text(r.get('metadata', {}).get('text', ''))],
metadata=r.get('metadata', {}),
distance=r.get('score', 0.0),
)
entries.append(entry)
return RetrievalResponse(
results=entries,
total_found=len(entries),
metadata={},
)
async def delete_document(self, kb_id: str, document_id: str) -> bool:
"""Delete a document and its associated data from the knowledge base"""
collection_id = kb_id
deleted = await self.plugin.vector_delete(collection_id, file_ids=[document_id])
return deleted > 0
```
### Lifecycle Hooks
KnowledgeEngine provides two lifecycle hooks that are called when knowledge bases are created and deleted:
```python
async def on_knowledge_base_create(self, kb_id: str, config: dict) -> None:
"""Callback when a knowledge base using this engine is created
Args:
kb_id: Knowledge base UUID
config: User-provided configuration (fields from creation_schema)
"""
async def on_knowledge_base_delete(self, kb_id: str) -> None:
"""Callback when a knowledge base using this engine is deleted
Args:
kb_id: Knowledge base UUID
"""
```
### Document Ingestion
The `ingest` method is called when a user uploads a document to the knowledge base:
```python
async def ingest(self, context: IngestionContext) -> IngestionResult:
```
**IngestionContext** contains the following information:
```python
class IngestionContext(pydantic.BaseModel):
file_object: FileObject # File object to ingest
knowledge_base_id: str # Target knowledge base ID
collection_id: str | None # Vector collection ID (falls back to knowledge_base_id)
creation_settings: dict # Configuration from knowledge base creation
parsed_content: ParseResult | None # Pre-parsed content from external Parser plugin (if any)
```
**FileObject** contains file metadata:
```python
class FileObject(pydantic.BaseModel):
metadata: FileMetadata # Contains filename, file_size, mime_type, document_id, knowledge_base_id, etc.
storage_path: str # File path in the storage system
```
**IngestionResult** should return the ingestion result:
```python
class IngestionResult(pydantic.BaseModel):
document_id: str # Document ID
status: DocumentStatus # Processing status: COMPLETED / FAILED
chunks_created: int = 0 # Number of chunks created
error_message: str | None = None # Error message on failure
metadata: dict = {} # Additional metadata
```
### Knowledge Retrieval
The `retrieve` method is called when the knowledge base is queried:
```python
async def retrieve(self, context: RetrievalContext) -> RetrievalResponse:
```
**RetrievalContext** contains the following information:
```python
class RetrievalContext(pydantic.BaseModel):
query: str # Query text
knowledge_base_id: str | None # Knowledge base ID
collection_id: str | None # Vector collection ID
retrieval_settings: dict # Retrieval configuration (fields from retrieval_schema)
creation_settings: dict # Knowledge base creation configuration
filters: dict # Metadata filter conditions (Chroma-style where syntax)
```
<Info>
The LangBot host automatically injects a default `top_k` value (default 5) into `retrieval_settings` before passing it to the plugin. Plugins can access it via `context.retrieval_settings.get('top_k', 5)`.
The `filters` field contains Chroma-style `where` filter conditions extracted from `retrieval_settings`. When the caller provides `filters` in the retrieval settings (e.g., filtering by time range, file type, or custom metadata fields), the host populates this field so plugins can apply them during retrieval. If no filters are provided, this field is an empty dict.
</Info>
**RetrievalResponse** should return the retrieval results:
```python
class RetrievalResponse(pydantic.BaseModel):
results: list[RetrievalResultEntry] # List of retrieval results
total_found: int # Total number of matches
metadata: dict # Additional metadata
```
**RetrievalResultEntry** represents a single retrieval result:
```python
class RetrievalResultEntry(pydantic.BaseModel):
id: str # Result ID
content: list[ContentElement] # Content, create using ContentElement.from_text()
metadata: dict # Metadata
distance: float # Distance score (smaller is more relevant)
score: float | None = None # Similarity score (larger is more relevant)
```
### Document Deletion
The `delete_document` method is called when a user deletes a document from the knowledge base:
```python
async def delete_document(self, kb_id: str, document_id: str) -> bool:
"""Delete a document and its associated data
Args:
kb_id: Knowledge base ID
document_id: Document ID
Returns:
Whether the deletion was successful
"""
```
## Host RAG APIs
KnowledgeEngine components can call LangBot host-provided RAG APIs via `self.plugin`, including embedding model invocation, vector database operations, and file retrieval.
### Invoke Embedding Model
```python
async def invoke_embedding(
self,
embedding_model_uuid: str,
texts: list[str],
) -> list[list[float]]:
"""Generate embeddings using host's embedding model
Args:
embedding_model_uuid: Embedding model UUID
texts: List of texts to embed
Returns:
List of embedding vectors, one per input text
"""
# Usage example
vectors = await self.plugin.invoke_embedding("model_uuid", ["Hello", "World"])
```
### Vector Upsert
```python
async def vector_upsert(
self,
collection_id: str,
vectors: list[list[float]],
ids: list[str],
metadata: list[dict] | None = None,
) -> None:
"""Upsert vectors to host's vector database
Args:
collection_id: Target collection ID
vectors: List of vectors
ids: List of unique IDs for vectors
metadata: Optional list of metadata dicts
"""
# Usage example
await self.plugin.vector_upsert(
collection_id="kb_uuid",
vectors=[[0.1, 0.2, ...], [0.3, 0.4, ...]],
ids=["chunk_0", "chunk_1"],
metadata=[{"document_id": "doc1"}, {"document_id": "doc1"}],
)
```
### Vector Search
```python
async def vector_search(
self,
collection_id: str,
query_vector: list[float],
top_k: int = 5,
filters: dict | None = None,
) -> list[dict]:
"""Search similar vectors in host's vector database
Args:
collection_id: Target collection ID
query_vector: Query vector for similarity search
top_k: Number of results to return
filters: Optional metadata filters
Returns:
List of search results (dict with id, score, metadata, etc.)
"""
# Usage example
results = await self.plugin.vector_search(
collection_id="kb_uuid",
query_vector=[0.1, 0.2, ...],
top_k=5,
)
# Return format: [{"id": "chunk_0", "score": 0.123, "metadata": {"document_id": "doc1", ...}}, ...]
```
<Info>
Each result returned by `vector_search` is a dict containing `id` (vector ID), `score` (distance score), and `metadata` (metadata provided during upsert). If you need text content in retrieval results, store the text in metadata during ingestion.
</Info>
### Vector Delete
```python
async def vector_delete(
self,
collection_id: str,
file_ids: list[str] | None = None,
filters: dict | None = None,
) -> int:
"""Delete vectors from host's vector database
Args:
collection_id: Target collection ID
file_ids: File IDs whose vectors should be deleted
filters: Optional metadata filters for deletion
Returns:
Number of deleted items
"""
# Usage example
deleted = await self.plugin.vector_delete(
collection_id="kb_uuid",
file_ids=["doc_001"],
)
```
<Info>
The `filters` parameter supports Chroma-style `where` syntax for metadata filtering. Multiple top-level keys are AND-ed. Supported operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`. Example: `{"file_id": {"$eq": "abc"}}`.
**Note:** Chroma, Qdrant, and SeekDB store full metadata and can filter on any field. Milvus and pgvector only store `text`, `file_id`, and `chunk_uuid` — filters on other fields will be silently ignored.
</Info>
```python
async def get_knowledge_file_stream(self, storage_path: str) -> bytes:
"""Get file content from host storage
Args:
storage_path: File storage path (from FileObject.storage_path)
Returns:
File content as bytes
"""
# Usage example
file_bytes = await self.plugin.get_knowledge_file_stream(context.file_object.storage_path)
```
## One installation, many usages
A single knowledge engine is used by **several knowledge bases** at once, and the platform storage key `(instance, Workspace, owner type, plugin, key)` carries no knowledge-base dimension:
- Persistence and cache keys must include the **knowledge-base ID**, otherwise vectors and document state of different knowledge bases overwrite each other.
- Locks and reconciliation state must be keyed by `(installation, knowledge base)`; **a failure while ingesting one knowledge base must not block ingestion or retrieval of the others under the same installation**.
- Only a dispatched mutation with an unknown outcome should enter a state that needs manual reconciliation; deterministic validation errors and pre-dispatch cancellation must not affect later calls.
- Telemetry and diagnostic failures must not affect the work: never let a log or report failure interrupt ingestion or retrieval, and never write tenant data into a process-wide singleton.
- If you keep per-installation process-local caches or state, release them in `on_installation_revoked(binding)`.
See [Certified plugins and shared runtime](/en/plugin/certified-plugins) for the full specification.
## Testing the Knowledge Engine
After creation, execute the command `lbp run` in the plugin directory to start debugging. Then in LangBot:
1. Go to the "Knowledge Base" page
2. Click "Create Knowledge Base"
3. Select the Knowledge Engine provided by your plugin and fill in the configuration based on the engine's `creation_schema`
4. After creation, upload documents to test ingestion capabilities (if the engine declares `DOC_INGESTION` capability)
5. Bind the knowledge base to a pipeline and test retrieval capabilities
@@ -0,0 +1,278 @@
---
title: "Component: Knowledge Retriever"
---
The Knowledge Retriever component allows plugins to provide external knowledge base retrieval capabilities for LangBot. When users create an external knowledge base in LangBot, they can choose a knowledge retriever provided by a plugin to retrieve knowledge.
<img width="600" src="/images/zh/plugin/dev/components/knowledge_retriever_use.png" />
## Adding a Knowledge Retriever Component
A single plugin can add any number of knowledge retrievers. Execute the command `lbp comp KnowledgeRetriever` in the plugin directory and follow the prompts to enter the knowledge retriever configuration.
```bash
➜ FastGPTRetriever > lbp comp KnowledgeRetriever
Generating component KnowledgeRetriever...
KnowledgeRetriever name: fastgpt
KnowledgeRetriever description: Retrieve knowledge from FastGPT knowledge bases
Component KnowledgeRetriever generated successfully.
```
This will generate `fastgpt.yaml` and `fastgpt.py` files in the `components/knowledge_retriever/` directory. The `.yaml` file defines the basic information and configuration parameters of the knowledge retriever, and the `.py` file is the handler for this retriever:
```bash
➜ FastGPTRetriever > tree
...
├── components
│ ├── __init__.py
│ └── knowledge_retriever
│ ├── __init__.py
│ ├── fastgpt.py
│ └── fastgpt.yaml
...
```
## Manifest File: Knowledge Retriever
```yaml
apiVersion: v1 # Do not modify
kind: KnowledgeRetriever # Do not modify
metadata:
name: fastgpt # Knowledge retriever name, used to identify this retriever
label:
en_US: FastGPT Knowledge Base # Retriever display name, shown in LangBot's UI, supports multiple languages
zh_Hans: FastGPT 知识库
ja_JP: FastGPT ナレッジベース
description:
en_US: 'Retrieve knowledge from FastGPT knowledge bases' # Retriever description, shown in LangBot's UI, supports multiple languages. Optional
zh_Hans: '从 FastGPT 知识库中检索知识'
ja_JP: 'FastGPT ナレッジベースから知識を取得'
icon: assets/icon.svg # Retriever icon, displayed on the external knowledge base configuration page
spec:
config: # Retriever configuration parameters, users need to fill in these parameters when creating an external knowledge base
- name: api_base_url # Parameter name
type: string # Parameter type: string, number, boolean, select
label:
en_US: API Base URL
zh_Hans: API 基础地址
ja_JP: API ベース URL
description:
en_US: 'Base URL for FastGPT API'
zh_Hans: 'FastGPT API 基础地址'
ja_JP: 'FastGPT API ベース URL'
default: 'http://localhost:3000' # Default parameter value
required: true # Whether required
- name: api_key
type: string
label:
en_US: FastGPT API Key
zh_Hans: FastGPT API Key
ja_JP: FastGPT API キー
description:
en_US: 'API key from your FastGPT instance'
zh_Hans: '从您的 FastGPT 实例获取的 API Key'
ja_JP: 'FastGPT インスタンスから取得した API Key'
default: ''
required: true
- name: search_mode # Example of select type parameter
type: select
label:
en_US: Search Mode
zh_Hans: 搜索模式
ja_JP: 検索モード
description:
en_US: 'The search method to use'
zh_Hans: '使用的搜索方法'
ja_JP: '使用する検索方法'
default: 'embedding'
options: # Select type parameters need to define an option list
- name: 'embedding'
label:
en_US: 'Embedding Search'
zh_Hans: '向量搜索'
ja_JP: '埋め込み検索'
- name: 'fullTextRecall'
label:
en_US: 'Full-Text Recall'
zh_Hans: '全文检索'
ja_JP: '全文検索'
- name: using_rerank # Example of boolean type parameter
type: boolean
label:
en_US: Use Re-ranking
zh_Hans: 使用重排序
ja_JP: リランキングを使用
description:
en_US: 'Whether to use re-ranking'
zh_Hans: '是否使用重排序'
ja_JP: 'リランキングを使用するかどうか'
default: false
required: false
execution:
python:
path: fastgpt.py # Retriever handler, do not modify
attr: FastGPT # Class name of the retriever handler, consistent with the class name in fastgpt.py
```
For configuration item format reference, see: [Plugin Manifest Configuration Format](/en/plugin/dev/basic-info)
## Plugin Handler
The following code will be generated by default (`components/knowledge_retriever/<retriever_name>.py`). You need to implement the knowledge retrieval logic in the `retrieve` method of the `FastGPT` class. Complete code can be found in [langbot-plugin-demo](https://github.com/langbot-app/langbot-plugin-demo).
```python
# Auto generated by LangBot Plugin SDK.
# Please refer to https://langbot.app/docs/en/plugin/dev/tutor.html for more details.
...
class FastGPT(KnowledgeRetriever):
async def retrieve(self, context: RetrievalContext) -> list[RetrievalResultEntry]:
"""Retrieve knowledge from FastGPT knowledge base"""
# 1. Get configuration parameters
api_base_url = self.config.get('api_base_url', 'http://localhost:3000')
api_key = self.config.get('api_key')
dataset_id = self.config.get('dataset_id')
search_mode = self.config.get('search_mode', 'embedding')
using_rerank = self.config.get('using_rerank', False)
# 2. Parameter validation
if not api_key or not dataset_id:
logger.error("Missing required configuration: api_key or dataset_id")
return []
# 3. Build API request
url = f"{api_base_url.rstrip('/')}/api/core/dataset/searchTest"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
payload = {
"datasetId": dataset_id,
"text": context.query, # Get query text from RetrievalContext
"searchMode": search_mode,
"usingReRank": using_rerank,
}
try:
# 4. Call external API
async with httpx.AsyncClient() as client:
response = await client.post(url, json=payload, headers=headers, timeout=30.0)
response.raise_for_status()
result = response.json()
# 5. Parse response and convert to RetrievalResultEntry
results = []
for record in result.get('data', []):
# Combine main data and auxiliary data as content
content_text = '\n'.join([
record.get('q', ''),
record.get('a', '')
]).strip()
# Create retrieval result entry
entry = RetrievalResultEntry(
id=record.get('id', ''),
content=[ContentElement.from_text(content_text)],
metadata={
'dataset_id': record.get('datasetId', ''),
'source_name': record.get('sourceName', ''),
'score': record.get('score', 0.0),
},
# Convert similarity score to distance (higher score = smaller distance)
distance=1.0 - float(record.get('score', 0.0)),
)
results.append(entry)
logger.info(f"Retrieved {len(results)} chunks from FastGPT dataset {dataset_id}")
return results
except httpx.HTTPStatusError as e:
logger.error(f"HTTP error from FastGPT API: {e.response.status_code} - {e.response.text}")
return []
except httpx.RequestError as e:
logger.error(f"Request error when calling FastGPT API: {str(e)}")
return []
except Exception as e:
traceback.print_exc()
logger.error(f"Unexpected error during retrieval: {str(e)}")
return []
```
### Retrieval Context
`RetrievalContext` contains context information for this retrieval:
```python
class RetrievalContext(pydantic.BaseModel):
"""Knowledge retrieval context"""
query: str
"""Query text, the user's retrieval question"""
```
### Retrieval Results
Retrieval results need to be converted to a list of `RetrievalResultEntry` objects, each representing a retrieved knowledge chunk:
```python
class RetrievalResultEntry(pydantic.BaseModel):
"""Single retrieval result entry"""
id: str
"""Result ID, uniquely identifies this entry"""
content: list[ContentElement]
"""Result content, create text content using ContentElement.from_text()"""
metadata: dict[str, Any]
"""Result metadata, can contain any key-value pairs, such as source, score, etc."""
distance: float
"""Distance score, indicates relevance to the query (smaller is more relevant)
Usually calculated as 1.0 - similarity_score"""
```
### Getting Configuration Parameters
Get user-configured parameter values using `self.config.get(parameter_name, default_value)`:
```python
# Get string parameter
api_base_url = self.config.get('api_base_url', 'http://localhost:3000')
# Get number parameters
limit = self.config.get('limit', 5000)
similarity = self.config.get('similarity', 0.0)
# Get boolean parameter
using_rerank = self.config.get('using_rerank', False)
# Get select parameter
search_mode = self.config.get('search_mode', 'embedding')
```
## One installation, many usages
A single knowledge retriever is used by **several knowledge bases**, and the platform storage key `(instance, Workspace, owner type, plugin, key)` carries no knowledge-base dimension:
- Any cache, index or intermediate result used for retrieval must be isolated by **knowledge-base ID**, with the ID in the key; never keep it on the instance.
- Key retrieval locks and reconciliation state by `(installation, knowledge base)`; a retrieval failure in one knowledge base must not make retrieval unavailable for the others under the same installation.
- `RetrievalContext` is valid only for a single retrieval; never store it or its results on instance fields for reuse across calls.
- Knowledge-base data that must survive calls belongs in Host storage, with a key that includes both the installation identity and the knowledge-base ID.
- If you cache connections, indexes or any other process-local state per installation, release them in `on_installation_revoked(binding)`.
See [Certified plugins and shared runtime](/en/plugin/certified-plugins) for the full specification.
## Testing the Retriever
After creation, execute the command `lbp run` in the plugin directory to start debugging. Then in LangBot:
1. Go to the "Knowledge Base" page
2. Add an external knowledge base
3. Select the knowledge retriever provided by your plugin and fill in the configuration
<img width="600" src="/images/zh/plugin/dev/components/knowledge_retriever_config.png" />
After saving, you can select this knowledge base in the LangBot pipeline.
+255
View File
@@ -0,0 +1,255 @@
---
title: "Component: Page"
---
The Page component allows plugins to register custom visual pages in the LangBot WebUI sidebar. Pages run inside an iframe sandbox, communicate with the host via the Page SDK, and can call the plugin's backend API.
## Use Cases
- **Admin panels**: Provide visual configuration and data management interfaces (e.g., FAQ management, analytics dashboards)
- **Data displays**: Show runtime statistics, logs, charts, etc.
- **Interactive tools**: Provide forms, editors, and other interactive interfaces
<img width="600" src="/images/zh/plugin/dev/components/plugin-page-demo.png" />
## Adding a Page Component
Run the following command in the plugin directory:
```bash
lbp comp Page
```
After entering the page name, the CLI will generate page files under `components/pages/`:
```bash
├── components
│ └── pages
│ └── dashboard
│ ├── dashboard.yaml # Page manifest
│ ├── dashboard.py # Backend handler
│ ├── index.html # Page entry point
│ └── i18n # Translation files (optional)
│ ├── en_US.json
│ └── zh_Hans.json
```
The plugin's `manifest.yaml` will also be updated with the component discovery config:
```yaml
spec:
components:
Page:
fromDirs:
- path: components/pages/
maxDepth: 2
```
## Manifest File: Page
```yaml
apiVersion: v1 # Do not modify
kind: Page # Do not modify
metadata:
name: dashboard # Unique page ID within the plugin
label:
en_US: Dashboard # Display name shown in the WebUI sidebar, supports i18n
zh_Hans: 仪表盘
spec:
path: index.html # HTML entry file, relative to this YAML file's directory
execution:
python:
path: dashboard.py # Backend handler file
attr: DashboardPage # Handler class name
```
## Backend Handler
The Page component's backend handler extends the `Page` base class and implements `handle_api` to process API requests from the frontend page.
```python
from langbot_plugin.api.definition.components.page import Page, PageRequest, PageResponse
class DashboardPage(Page):
async def handle_api(self, request: PageRequest) -> PageResponse:
# request.endpoint: API endpoint path, e.g. '/stats'
# request.method: HTTP method (GET, POST, PUT, DELETE)
# request.body: Request body (parsed JSON, or None)
if request.endpoint == '/stats' and request.method == 'GET':
# 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}')
```
### PageRequest Fields
| Field | Type | Description |
|-------|------|-------------|
| `endpoint` | `str` | API endpoint path (e.g. `'/entries'`) |
| `method` | `str` | HTTP method (`GET`, `POST`, `PUT`, `DELETE`) |
| `body` | `Any` | Request body (parsed JSON, or `None`) |
### PageResponse Construction
| Method | Description |
|--------|-------------|
| `PageResponse.ok(data)` | Success response. `data` can be any JSON-serializable value |
| `PageResponse.fail(error)` | Error response. `error` is a human-readable error string |
## Frontend Page Development
Include the Page SDK in your HTML file to communicate with the plugin backend:
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
background: var(--langbot-bg, #ffffff);
color: var(--langbot-text, #0a0a0a);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
padding: 24px;
}
</style>
</head>
<body>
<h1 data-i18n="title">Dashboard</h1>
<p id="stats"></p>
<script src="/api/v1/plugins/_sdk/page-sdk.js"></script>
<script>
langbot.onReady(async function(ctx) {
// Call the backend API
var data = await langbot.api('/stats', null, 'GET');
document.getElementById('stats').textContent = 'Total: ' + data.total;
});
</script>
</body>
</html>
```
### Page SDK API
| Method | Description |
|--------|-------------|
| `langbot.onReady(callback)` | Fires when SDK is ready. `callback` receives `ctx` with `theme` and `language` |
| `langbot.api(endpoint, body?, method?)` | Calls the plugin's `handle_api`. Returns a Promise |
| `langbot.t(key, fallback?)` | Gets a translated string |
| `langbot.onThemeChange(callback)` | Fires when the theme changes |
| `langbot.onLanguageChange(callback)` | Fires when the language changes |
| `langbot.applyI18n()` | Manually re-apply `data-i18n` translations |
### Dark Mode
The SDK automatically sets CSS custom properties on the page. Use them directly:
| CSS Variable | Purpose |
|-------------|---------|
| `--langbot-bg` | Page background |
| `--langbot-bg-card` | Card background |
| `--langbot-text` | Primary text color |
| `--langbot-text-muted` | Secondary text color |
| `--langbot-border` | Border color |
| `--langbot-accent` | Accent color |
### Page i18n
Create an `i18n/` directory inside your page directory with JSON translation files:
```
pages/dashboard/
├── index.html
└── i18n/
├── en_US.json
└── zh_Hans.json
```
Translation files are flat JSON key-value pairs:
```json
{
"title": "Dashboard",
"totalEntries": "Total Entries"
}
```
Add the `data-i18n` attribute to HTML elements for automatic translation:
```html
<h1 data-i18n="title">Dashboard</h1>
```
## Full Example: FAQ Manager
Here is a complete Page component example that implements CRUD operations for FAQ entries.
**Backend handler** (`components/pages/manager/manager.py`):
```python
from langbot_plugin.api.definition.components.page import Page, PageRequest, PageResponse
class ManagerPage(Page):
async def handle_api(self, request: PageRequest) -> PageResponse:
if request.endpoint == '/entries' and request.method == 'GET':
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 = {'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 await self.delete_entry_for_current_installation(entry_id):
return PageResponse.ok({'deleted': entry_id})
return PageResponse.fail('entry not found')
return PageResponse.fail(f'Unknown: {request.method} {request.endpoint}')
```
**Frontend page** (`components/pages/manager/index.html`) calls the backend via `langbot.api()`:
```javascript
// Load entries
var data = await langbot.api('/entries', null, 'GET');
// Add entry
await langbot.api('/entries', { question: '...', answer: '...' }, 'POST');
// Delete entry
await langbot.api('/entries', { id: '...' }, 'DELETE');
```
Full example code is available at [FAQManager plugin](https://github.com/langbot-app/langbot-plugin-demo/tree/main/FAQManager).
## One installation, many usages
The same Page is called by **many page requests** (different requests and different Workspaces under one installation):
- Never leave the previous request's data on the instance; take the endpoint, method, body and current installation information from the current `PageRequest` / binding every time.
- Page data always lives in installation-scoped Host storage; `self.plugin` is a process-level shared object and must not hold tenant data.
- Requests from different Workspaces share the same Page instance, so instance fields get interleaved; data that must survive requests belongs in Host storage.
- If you keep per-installation process-local caches, release them in `on_installation_revoked(binding)`.
See [Certified plugins and shared runtime](/en/plugin/certified-plugins) for the full specification.
## Notes
- Pages run inside a `sandbox="allow-scripts allow-forms"` iframe and cannot open popups or navigate the parent page
- 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 `<script>` tag must be placed before any code that uses the `langbot` object
+221
View File
@@ -0,0 +1,221 @@
---
title: "Component: Parser"
---
The Parser component allows plugins to provide document parsing capabilities for LangBot. When a user uploads a document to a knowledge base, LangBot invokes the parser before the Knowledge Engine's `ingest` step, extracting structured text from binary files such as PDF, Word, Markdown, etc.
Relationship between Parser and KnowledgeEngine:
- **Parser** is responsible for converting files to text (file → text)
- **KnowledgeEngine** is responsible for indexing and retrieving text (text → chunks → vectors)
If the Knowledge Engine already has native document parsing capabilities (declared `DOC_PARSING` capability), users can choose to use the Knowledge Engine's built-in parsing or an external Parser plugin.
## Adding a Parser Component
A single plugin can add any number of parsers. Execute the command `lbp comp Parser` in the plugin directory and follow the prompts to enter the parser configuration.
```bash
➜ MyParserPlugin > lbp comp Parser
Generating component Parser...
Parser name: pdf_parser
Parser description: A PDF document parser
Component Parser generated successfully.
```
This will generate `pdf_parser.yaml` and `pdf_parser.py` files in the `components/parser/` directory. The `.yaml` file defines the parser's basic information and supported MIME types, and the `.py` file is the handler for this parser:
```bash
➜ MyParserPlugin > tree
...
├── components
│ ├── __init__.py
│ └── parser
│ ├── __init__.py
│ ├── pdf_parser.py
│ └── pdf_parser.yaml
...
```
## Manifest File: Parser
```yaml
apiVersion: v1 # Do not modify
kind: Parser # Do not modify
metadata:
name: pdf_parser # Parser name, used to identify this parser
label:
en_US: PDF Parser # Parser display name, shown in LangBot's UI, supports multiple languages
zh_Hans: PDF 解析器
description:
en_US: 'A PDF document parser'
zh_Hans: 'PDF 文档解析器'
spec:
supported_mime_types: # Declare supported file MIME types
- application/pdf
execution:
python:
path: pdf_parser.py # Parser handler, do not modify
attr: PdfParser # Class name of the parser handler, consistent with the class name in pdf_parser.py
```
### supported_mime_types
`supported_mime_types` declares the file types this parser supports. Common MIME types:
| MIME Type | Description |
|----------|-------------|
| `application/pdf` | PDF documents |
| `application/vnd.openxmlformats-officedocument.wordprocessingml.document` | Word documents (.docx) |
| `text/markdown` | Markdown files |
| `text/plain` | Plain text files |
| `text/html` | HTML files |
## Plugin Handler
The following code will be generated by default (`components/parser/<parser_name>.py`). You need to implement the `parse` method.
```python
from langbot_plugin.api.definition.components.parser.parser import Parser
from langbot_plugin.api.entities.builtin.rag.models import (
ParseContext,
ParseResult,
TextSection,
)
class PdfParser(Parser):
"""Parser component for extracting text from files."""
async def parse(self, context: ParseContext) -> ParseResult:
"""Parse a file and extract structured text.
Args:
context: Contains file_content (bytes), mime_type, filename, and metadata.
Returns:
ParseResult with extracted text and optional structured sections.
"""
# TODO: Implement parsing logic
text = context.file_content.decode("utf-8", errors="replace")
return ParseResult(
text=text,
sections=[
TextSection(
content=text,
heading=context.filename,
level=0,
),
],
metadata={
"filename": context.filename,
"mime_type": context.mime_type,
},
)
```
### Parse Method
The `parse` method is called when a document is uploaded to a knowledge base (before Knowledge Engine ingestion):
```python
async def parse(self, context: ParseContext) -> ParseResult:
```
**ParseContext** contains the following information:
```python
class ParseContext(pydantic.BaseModel):
file_content: bytes # Raw file bytes (read by LangBot from storage)
mime_type: str # Detected MIME type of the file
filename: str # Original filename
metadata: dict[str, Any] # Extra metadata from FileObject
```
**ParseResult** should return the parsing result:
```python
class ParseResult(pydantic.BaseModel):
text: str # Full extracted plain text
sections: list[TextSection] = [] # Structured sections (optional)
metadata: dict[str, Any] = {} # Parsing metadata (e.g., page_count, language)
```
**TextSection** represents a section of text extracted from the document:
```python
class TextSection(pydantic.BaseModel):
content: str # Section text content
heading: str | None = None # Section heading
level: int = 0 # Nesting level
page: int | None = None # Source page number (for PDF, etc.)
metadata: dict[str, Any] = {} # Additional section metadata
```
## Integration with KnowledgeEngine
When a user uploads a document, LangBot determines the parsing flow as follows:
- If the user selects an external Parser plugin, LangBot first calls the Parser's `parse` method, then passes the result to the Knowledge Engine's `ingest` method via `IngestionContext.parsed_content`.
- If the Knowledge Engine declares `DOC_PARSING` capability and the user does not select an external parser, the Knowledge Engine handles document parsing on its own.
KnowledgeEngine can check `IngestionContext.parsed_content` to determine whether pre-parsed content is available:
```python
async def ingest(self, context: IngestionContext) -> IngestionResult:
if context.parsed_content:
# Use pre-parsed content from external Parser
text = context.parsed_content.text
sections = context.parsed_content.sections
else:
# Parse the document internally
file_bytes = await self.plugin.get_knowledge_file_stream(context.file_object.storage_path)
text = file_bytes.decode('utf-8')
...
```
## Cross-Plugin Parser Invocation
Before invoking a parser from another plugin, you can use `self.plugin.list_parsers` to discover the parsers currently available on the host:
```python
parsers = await self.plugin.list_parsers(mime_type="application/pdf")
# Each item includes plugin_id, plugin_author, plugin_name, name, description, supported_mime_types
```
If the list is empty, no connected Parser plugin currently supports that MIME type.
After obtaining `plugin_author` and `plugin_name`, you can call `self.plugin.invoke_parser`:
```python
parser = parsers[0]
result = await self.plugin.invoke_parser(
plugin_author=parser["plugin_author"],
plugin_name=parser["plugin_name"],
storage_path=context.file_object.storage_path,
mime_type=context.file_object.metadata.mime_type,
filename=context.file_object.metadata.filename,
metadata={},
)
# result is a dict containing text, sections, metadata
```
## One installation, many usages
A single Parser is used by **several knowledge bases** (different knowledge bases of one installation upload files of different MIME types), and the platform storage key carries no knowledge-base dimension:
- Parse caches and temporary files must be isolated by **knowledge-base ID**, with the ID in the key; never cache them on the instance.
- `ParseContext` and intermediate parse results are valid only for a single parse; never store them on instance fields or module-level variables.
- A parse failure affects only that upload of the current knowledge base; it must not make parsing unavailable for the other knowledge bases.
- If you cache models, parsers or temporary directories per installation, release them in `on_installation_revoked(binding)` and make sure temporary files can be cleaned up.
See [Certified plugins and shared runtime](/en/plugin/certified-plugins) for the full specification.
## Testing the Parser
After creation, execute the command `lbp run` in the plugin directory to start debugging. Then in LangBot:
1. Go to the "Knowledge Base" page
2. Select a knowledge base and enter document management
3. When uploading a file, select your plugin's parser in the parser selector
4. After uploading, verify that the document is correctly ingested
+211
View File
@@ -0,0 +1,211 @@
---
title: "Component: Runner"
---
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).
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.
```bash
lbp comp Runner
```
The generated `components/runner/welcome.yaml` defines the component, and `welcome.py` contains its implementation.
## 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: Runner
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:
usages: [event]
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.
`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
Register handlers in the `Welcome` class's `initialize` method. Replace `welcome.py` with:
```python
from langbot_plugin.api.definition.components.runner import (
Runner,
RunnerContext,
)
from langbot_plugin.api.entities.builtin.platform.events import (
MemberJoinedEvent,
MessageReceivedEvent,
)
from langbot_plugin.api.entities.builtin.platform.message import Plain
class Welcome(Runner):
async def initialize(self):
await super().initialize()
@self.handler(MemberJoinedEvent)
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")
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: RunnerContext):
text = "".join(
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")
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.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
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.
## 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, 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.
## One installation, many usages
A Workspace installs each plugin once, but that installation is reused by **several processors** (Agents and Pipelines). A Runner must keep their data apart on its own:
- `ctx.state.*` is already per-processor: scopes carry `binding_identity` (derived from the Agent UUID), so different processors of the same installation never overwrite each other.
- The platform storage key is `(instance, Workspace, owner type, plugin, key)` and carries **no processor dimension**; any key you write into Host storage or caches yourself must therefore also include the processor or conversation identity.
- `RunnerContext` (`ctx`) is valid only for a single run. Never keep it or its fields on the instance for reuse across calls; persist session state that must survive calls via `ctx.state_get()` / `ctx.state_set()`.
- Several processors share the same Runner instance, so never accumulate the current run's data on instance fields. If your runner must hold cross-call state in-process (for example a long-lived external session process), do not declare shared runtime — stay dedicated.
- If you keep per-installation process-local caches, release them in `on_installation_revoked(binding)`.
See [Certified plugins and shared runtime](/en/plugin/certified-plugins) for the full specification.
## 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/Runner/RunnerDemo).
- To extend Pipeline steps such as preprocessing or completion of a model call, use [Event Listener](/en/plugin/dev/components/event-listener).
+188
View File
@@ -0,0 +1,188 @@
---
title: "Component: Tool"
---
LangBot's built-in Local Agent calls tools to interact with the outside world during execution. Currently, adding tools is supported through both plugins and MCP.
## Adding Tool Components
A single plugin can contain any number of tools. Execute the command `lbp comp Tool` in the plugin directory and follow the prompts to enter the tool configuration.
```bash
➜ HelloPlugin > lbp comp Tool
Generating component Tool...
Tool name: get_weather_alerts
Tool description: Get weather alerts for a US state.
Component Tool generated successfully.
组件 Tool 生成成功。
```
This will generate `get_weather_alerts.yaml` and `get_weather_alerts.py` files in the `components/tools/` directory. The `.yaml` file defines the basic information for the tool, and the `.py` file is the tool handler:
```bash
➜ HelloPlugin > tree
...
├── components
│ ├── __init__.py
│ └── tools
│ ├── __init__.py
│ ├── get_weather_alerts.py
│ └── get_weather_alerts.yaml
...
```
## Manifest File: Tool
```yaml
apiVersion: v1 # Do not modify
kind: Tool # Do not modify
metadata:
name: get_weather_alerts # Tool name, for LLM recognition
label:
en_US: GetWeatherAlerts # Tool display name, shown in LangBot UI, supports multilingual
zh_Hans: GetWeatherAlerts
description:
en_US: 'Get weather alerts for a US state.' # Tool description, shown in LangBot UI, supports multilingual. Optional
zh_Hans: '获取美国某个州的天气预警'
spec:
parameters: [] # Tool parameters, values generated by LLM based on conversation context
llm_prompt: 'Get weather alerts for a US state.' # Tool prompt, for LLM to determine whether to call this tool
execution:
python:
path: get_weather_alerts.py # Tool handler, do not modify
attr: GetWeatherAlerts # Tool handler class name, matches the class name in get_weather_alerts.py
```
## Plugin Processing
The following code is generated by default (`components/tools/<tool_name>.py`). You need to implement the calling and return logic for this tool in the `call` method of the `GetWeatherAlerts` class.
```python
# Auto generated by LangBot Plugin SDK.
# Please refer to https://langbot.app/docs/en/plugin/dev/tutor.html for more details.
from __future__ import annotations
from typing import Any
from langbot_plugin.api.definition.components.tool.tool import Tool
from langbot_plugin.api.entities.builtin.provider import session as provider_session
class GetWeatherAlerts(Tool):
async def call(self, params: dict[str, Any], session: provider_session.Session, query_id: int) -> str:
"""Fill your tool code here"""
return {}
```
In the `call` method, implement this tool:
<Info>
The weather retrieval tool example comes from MCP's [Server Writing Example](https://modelcontextprotocol.io/quickstart/server).
</Info>
```python
from __future__ import annotations
from typing import Any
from langbot_plugin.api.definition.components.tool.tool import Tool
from langbot_plugin.api.entities.builtin.provider import session as provider_session
import httpx
# Constants
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"
async def make_nws_request(url: str) -> dict[str, Any] | None:
"""Make a request to the NWS API with proper error handling."""
headers = {
"User-Agent": USER_AGENT,
"Accept": "application/geo+json"
}
async with httpx.AsyncClient() as client:
try:
response = await client.get(url, headers=headers, timeout=30.0)
response.raise_for_status()
return response.json()
except Exception:
return None
def format_alert(feature: dict) -> str:
"""Format an alert feature into a readable string."""
props = feature["properties"]
return f"""
Event: {props.get('event', 'Unknown')}
Area: {props.get('areaDesc', 'Unknown')}
Severity: {props.get('severity', 'Unknown')}
Description: {props.get('description', 'No description available')}
Instructions: {props.get('instruction', 'No specific instructions provided')}
"""
class GetWeatherAlerts(Tool):
async def call(self, params: dict[str, Any], session: provider_session.Session, query_id: int) -> str:
"""Fill your tool code here"""
state = params.get("state", "CA")
url = f"{NWS_API_BASE}/alerts/active/area/{state}"
data = await make_nws_request(url)
if not data or "features" not in data:
return "Unable to fetch alerts or no alerts found."
if not data["features"]:
return "No active alerts for this state."
alerts = [format_alert(feature) for feature in data["features"]]
return "\n---\n".join(alerts)
```
In this tool, we need to get the `state` parameter from `params` and call the NWS API to get weather alerts for that state, so we need to define the `state` parameter in the `parameters` of the manifest file.
The `parameters` format follows [JSON Schema](https://json-schema.org/docs), is supported by [OpenAI Function Calling feature](https://platform.openai.com/docs/guides/function-calling#defining-functions), and its root type is fixed as `type: object`. Please add parameters in `properties` and add required parameter descriptions in `required`.
```yaml
...
spec:
parameters:
type: object # Do not modify
properties: # Do not modify, add parameters here
state:
type: string
description: 'Two-letter US state code (e.g. CA, NY)'
required: # Add required parameter descriptions here
- state
...
```
This tool also uses the `httpx` library, so we need to add the `httpx` dependency in `requirements.txt` in the plugin directory.
```txt
# HelloPlugin/requirements.txt
langbot-plugin # Already exists by default
httpx
```
Now execute the command `lbp run` in the plugin directory to start debugging. Configure a model that supports tool calling in LangBot and select to use that model on the corresponding pipeline to use this tool.
<img width="600" src="/images/zh/plugin/dev/components/tool_use_in_chat.png" />
## One installation, many usages
A single Tool is called by **several processors and conversations** (one event can trigger several plugin processors):
- Read config per call (`self.get_plugin_config()` or `ctx.config`); never read it in `initialize()` and cache it on the instance.
- Never leave call arguments, results or conversation data on instance fields or module-level variables; take the conversation identity from the current call's `session` / `query_id`.
- Concurrent calls share the same instance, so instance fields get interleaved writes. Data that must survive calls belongs in Host storage, keyed with the corresponding processor or conversation identity.
- If you keep per-installation process-local caches, release them in `on_installation_revoked(binding)`.
See [Certified plugins and shared runtime](/en/plugin/certified-plugins) for the full specification.
## What's Next
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)
+190
View File
@@ -0,0 +1,190 @@
---
title: "Plugin Directory Structure"
---
This document introduces the standard directory structure and file organization specifications for LangBot plugins, helping developers create compliant plugins.
## Basic Directory Structure
A standard LangBot plugin should follow this directory structure:
```
MyPlugin/
├── manifest.yaml # Plugin manifest file (required)
├── main.py # Plugin main entry file (required)
├── README.md # English plugin documentation (required)
├── readme/ # Multilingual README directory (optional)
│ ├── README_zh_Hans.md # Simplified Chinese documentation
│ ├── README_ja_JP.md # Japanese documentation
│ └── README_zh_Hant.md # Traditional Chinese documentation (optional)
├── assets/ # Resource files directory
│ ├── icon.svg # Plugin icon (recommended)
│ └── ... # Other resource files
├── components/ # Components directory
│ ├── event_listener/ # Event listener components
│ ├── commands/ # Command components
│ └── tools/ # Tool components
├── requirements.txt # Python dependencies (optional)
└── config/ # Configuration directory (optional)
```
## Multilingual README Specification
### File Location Specification
LangBot plugins support multilingual README documentation to provide localized plugin descriptions for users in different languages.
**Important Specifications:**
1. **`Root README.md (Required)`**
- Must be written in **English**
- Serves as the plugin's default documentation
- Used as fallback when requested language version doesn't exist
2. **`readme/ Directory (Optional)`**
- Used to store **non-English** README documents
- File naming format: `README_{language_code}.md`
### Supported Language Codes
According to [RFC 4646](https://datatracker.ietf.org/doc/html/rfc4646) standard, LangBot currently supports the following language codes:
| Language | Language Code | Filename | Location |
| ------------------- | --------------- | ------------------- | ------------------------- |
| English | `en` or `en_US` | `README.md` | **Plugin root directory** |
| Simplified Chinese | `zh_Hans` | `README_zh_Hans.md` | `readme/` directory |
| Traditional Chinese | `zh_Hant` | `README_zh_Hant.md` | `readme/` directory |
| Japanese | `ja_JP` | `README_ja_JP.md` | `readme/` directory |
| Vietnamese | `vi_VN` | `README_vi_VN.md` | `readme/` directory |
| Thai | `th_TH` | `README_th_TH.md` | `readme/` directory |
| Spanish | `es_ES` | `README_es_ES.md` | `readme/` directory |
### Example Directory Structure
```
MyPlugin/
├── README.md # ✅ English version (required, in root)
└── readme/ # ✅ Multilingual directory
├── README_zh_Hans.md # ✅ Simplified Chinese
├── README_ja_JP.md # ✅ Japanese
└── README_zh_Hant.md # ✅ Traditional Chinese
```
### Incorrect Examples
❌ **Wrong: Placing English README in readme/ directory**
```
MyPlugin/
├── readme/
│ ├── README_en.md # ❌ Wrong: English should not be in readme/
│ └── README_zh_Hans.md
```
❌ **Wrong: Root README.md contains non-English content**
```markdown
# MyPlugin
This is a plugin...
这是一个插件... # ❌ Wrong: Root README.md should only contain English
```
### README Language Fallback Logic
When a user requests a README in a specific language, LangBot will search in the following order:
1. Try to read `readme/README_{language_code}.md`
2. If not found, fall back to root `README.md` (English version)
**Examples:**
- User requests Simplified Chinese (`zh_Hans`)
- → Look for `readme/README_zh_Hans.md`
- → If not found, return `README.md` (English version)
- User requests Japanese (`ja_JP`)
- → Look for `readme/README_ja_JP.md`
- → If not found, return `README.md` (English version)
## Resource Files Directory
### assets/ Directory
The `assets/` directory is used to store plugin static resource files.
**Recommended Structure:**
```
assets/
├── icon.svg # Plugin icon (SVG format recommended)
├── example.png # Example image
├── screenshot1.png # Screenshot
└── logo.png # Logo image
```
**Icon Specifications:**
- Recommended to use `icon.svg` as plugin icon
- Supported formats: `.svg`, `.png`, `.jpg`, `.jpeg`, `.gif`
- Recommended size: At least 256x256 pixels
- Reference in `manifest.yaml`: `icon: assets/icon.svg`
### Referencing Resources in README
In README documentation, you can use relative paths to reference images in the assets/ directory:
```markdown
# MyPlugin
![Example](./assets/example.png)
![Screenshot](./assets/screenshot1.png)
```
**Note:** After uploading plugins to LangBot Space, resource files will be automatically processed and hosted, and images will display correctly when users view the README.
## Components Directory
Plugin functionality is implemented through components, which should be organized by type in the `components/` directory:
```
components/
├── event_listener/ # Event listeners
│ ├── on_message.py
│ └── on_message.yaml
├── commands/ # Commands
│ ├── hello.py
│ └── hello.yaml
└── tools/ # Tools
├── search.py
└── search.yaml
```
For detailed component development specifications, please refer to: [Adding Components](/en/plugin/dev/components/add)
## Configuration Files
### manifest.yaml
The plugin manifest file `manifest.yaml` is the core configuration file, containing plugin metadata, configuration items, component lists, and other information.
For detailed instructions, please refer to: [Complete Plugin Configuration Information](/en/plugin/dev/basic-info)
### requirements.txt
If the plugin requires additional Python dependencies, create a `requirements.txt` file in the root directory:
```
requests>=2.28.0
beautifulsoup4>=4.11.0
pillow>=9.0.0
```
**Note:** LangBot will automatically install dependencies listed in `requirements.txt` when installing the plugin.
## Related Documentation
- [Basic Tutorial](/en/plugin/dev/tutor) - Learn how to create your first plugin
- [Complete Plugin Configuration Information](/en/plugin/dev/basic-info) - Configure manifest.yaml
- [Adding Components](/en/plugin/dev/components/add) - Develop plugin components
- [Publish to Marketplace](/en/plugin/dev/publish/market) - Distribute your plugin
+27
View File
@@ -0,0 +1,27 @@
---
title: "Migration Guide"
---
This tutorial highlights certain considerations. Please read the current version plugin tutorial first, then use this document as a reference for plugin migration.
## Migrating from 3.x Supported Asynchronous Plugins
<Info title="Plugin characteristics:">
- Main time period: 2024.2 ~ 2025.8
- Registration method: Using `@handler` decorators to register event listeners and `@llm_func` decorators to register content functions (tools) in the plugin class in `main.py`; all handler functions are asynchronous functions (async def)
</Info>
- Plugin and component registration method: Changed from the previous single plugin class decorator registration method to manifest file registration method. See [Manifest File](/en/plugin/dev/basic-info); and separated different types of components independently. For details, see [Adding Components](/en/plugin/dev/components/add).
- Message chain entities: Now refactored. The construction method of each message chain element now requires explicit passing of **named parameters**. See [Message Platform Entities](/en/plugin/dev/apis/messages).
- Event listeners: Provides backward compatibility, but no longer provides `query: Query` object in context. For details, see [Pipeline Events](/en/plugin/dev/apis/pipeline-events).
- Command components: Now refactored. See [Command Components](/en/plugin/dev/components/command).
## Migrating from 2.x and 3.x Supported Synchronous Plugins
<Info title="Plugin characteristics:">
- Main time period: 2023.1 ~ 2024.2
- Registration method: Using `@on` decorators to register event listeners and `@func` decorators to register content functions (tools) in the plugin class in `main.py`; all handler functions are synchronous functions (def)
</Info>
- Synchronous to asynchronous: Plugin runtime has now changed to fully asynchronous (based on asyncio). Please change all plugin code involving IO operations to asynchronous writing.
- For detailed content, please refer to the above 3.x asynchronous plugin migration guide.
+40
View File
@@ -0,0 +1,40 @@
---
title: "Distribute Plugins via GitHub"
---
You can upload your plugin to GitHub, create a Release for the repository, and upload the `.lbpkg` file, allowing other users to install it in LangBot.
## Build Plugin
Execute the following command in the plugin directory:
```bash
lbp build
```
This will generate a `.lbpkg` file in the `dist` directory.
## Create Release
Upload your plugin to GitHub, then go to the Release page of your plugin repository, click to create a Release, and upload the `.lbpkg` file you just built.
<img width="600" src="/images/zh/plugin/dev/dist/github_release.png" />
## Auto-create Release (GitHub Actions)
Plugins created by `lbp init` come with a built-in GitHub Actions workflow (`.github/workflows/release.yml`), so you don't have to build and upload manually.
After pushing your plugin repository to GitHub, whenever you bump the `version` field in `manifest.yaml` and push to the `main` or `master` branch, the workflow will automatically:
1. Install the `lbp` CLI and run `lbp build` to package the plugin;
2. Create a GitHub Release tagged `v<version>` and upload the generated `.lbpkg` file.
If a Release for that version already exists, the workflow skips it to avoid duplicate publishing. You can also trigger it manually from the repository's Actions page (`workflow_dispatch`).
<Info title="">
The workflow relies on the repository's default `GITHUB_TOKEN` and requires the `contents: write` permission (already declared in the template). Make sure Actions is enabled for the repository, and that the `metadata.author`, `metadata.name`, and `metadata.version` fields in `manifest.yaml` are correct — the Release name and artifact filename are composed from these fields (`<author>-<name>-<version>.lbpkg`).
</Info>
## Install Plugin
In LangBot's plugin management page, click the top-right corner to select the GitHub installation method.
+85
View File
@@ -0,0 +1,85 @@
---
title: "Publish to Plugin Marketplace"
---
Publish plugins to [LangBot Plugin Marketplace](https://space.langbot.app/market) via CLI.
## Create Account on LangBot Space
LangBot Space is LangBot's centralized service platform. Please visit [LangBot Space](https://space.langbot.app), click the top-right corner to log in with GitHub and create an account.
<img width="600" src="/images/zh/plugin/dev/login_space.png" />
This will redirect to GitHub authorization page, please complete authorization as prompted.
## Log in to LangBot Space Using CLI
### Option 1: Personal Access Token (Recommended)
1. Log in to [LangBot Space](https://space.langbot.app), click your avatar to open **Account Settings**, find **Personal Access Tokens** under the **Account** tab, and create a new token.
2. Copy the generated token (shown only once), then run:
```bash
➜ HelloPlugin > lbp login --token lbpat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
==================================================
✅ Logged in with personal access token!
Token saved to: ~/.langbot/cli/config.json
==================================================
```
### Option 2: Device Code Verification
Execute command `lbp login` in any directory:
```bash
➜ HelloPlugin > lbp login
Starting LangBot CLI login process...
Generating device code...
==================================================
Please copy the user code and complete verification in your browser:
User Code: 6CD64D56
Verification URL: https://space.langbot.app/auth/device
Device code expires in: 120 seconds
==================================================
Waiting for verification...
```
Please click `Verification URL` to redirect to LangBot Space, enter `User Code` to complete login.
<img width="600" src="/images/zh/plugin/dev/space_device_flow.png" />
```bash
==================================================
✅ Login successful!
Access token saved to: ~/.langbot/cli/config.json
Token type: Bearer
Expires in: 21600 seconds
==================================================
```
After successful login with either method, you can continue using CLI to upload plugins.
## Upload Plugin
Please execute `lbp publish` in your plugin directory:
```bash
➜ HelloPlugin > lbp publish
Building plugin to dist/tmp...
- Skipping ignored directory: dist
- Skipping ignored directory: __pycache__
...
Plugin built successfully: dist/tmp/RockChinQ-HelloPlugin-0.1.0.lbpkg
✅ Plugin published successfully. You can check it on https://https://space.langbot.app/market
```
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.
<img width="600" src="/images/zh/plugin/dev/plugin_submission.png" />
Publishing does not involve certification: plugins run in dedicated mode by default. If this release should also be certified, see [Certified plugins and shared runtime](/en/plugin/certified-plugins).
If you have any questions, you can send an email to `[email protected]` or contact us through [Community](/en/insight/community).
+19
View File
@@ -0,0 +1,19 @@
---
title: "Code Style Guide"
---
## Logging
In plugins, please use the `logging` module to record logs, do not use `print` statements (this will cause logs to not be output in the container environment).
```python
import logging
logger = logging.getLogger(__name__) # Import logging module at the beginning of any Python file and define logger object
logger.info('This is an info message') # Use logger.info to record info level logs
logger.warning('This is a warning message')
logger.error('This is an error message')
```
For Certified Plugins, raw process logs cannot be safely attributed to one Workspace and are operator-only. Runner code may use `RunnerContext.log()` for installation-scoped tenant-visible logs. Other component kinds do not yet have a generic tenant-visible logging API; their raw process logs are operator-only. Never emit tenant configuration, credentials or request data from module-global background work.
+148
View File
@@ -0,0 +1,148 @@
---
title: "Plugin Development Tutorial"
---
<Info>
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).
</Info>
## 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.
<img width="600" src="/images/zh/plugin/dev/plugin_system_arch.png" />
Plugin Runtime is used to manage plugin lifecycles and coordinate interactions between LangBot and plugins. It has two operating modes: `stdio` and `websocket`. When LangBot is started directly by users (not running in a container), it uses `stdio` mode, which is common for personal users or lightweight environments. When LangBot runs in a container, it uses `websocket` mode, designed specifically for production environments.
Plugin Runtime automatically starts each installed plugin and interacts through stdio. In plugin development scenarios, developers can use the `lbp` command-line tool to start plugins and connect to the running Runtime via WebSocket for debugging.
### Plugin Structure
A plugin's directory structure is similar to the following:
```bash
➜ HelloPlugin > tree
.
├── assets
│ └── icon.svg # Plugin icon, displayed on the plugin marketplace page
├── components # Plugin component directory, stores various component code and manifest files
│ ├── __init__.py
│ ├── commands
│ │ ├── __init__.py
│ │ ├── info.py
│ │ └── info.yaml
│ ├── event_listener
│ │ ├── __init__.py
│ │ ├── default.py
│ │ └── default.yaml
│ └── tools
│ ├── __init__.py
│ ├── get_weather_alerts.py
│ └── get_weather_alerts.yaml
├── main.py # Plugin main program, listens to plugin lifecycle
├── manifest.yaml # Plugin manifest file, describes plugin metadata
├── README.md # Plugin documentation file, describes plugin functionality and usage, displayed on the plugin marketplace page
├── readme
│ └── README_zh_Hans.md # Simplified Chinese documentation, for multi-language docs
├── requirements.txt # Plugin dependencies file
└── .github
└── workflows
└── release.yml # GitHub Actions workflow, automatically builds and publishes a Release when the version is bumped
```
The plugin class in main.py is common code for each plugin, initialized when the plugin starts and passed context information (configuration, etc.). You can implement initialization and shutdown logic for the plugin here. The plugin class inherits from `langbot_plugin.api.definition.plugin.BasePlugin` and provides [LangBot Global APIs](/en/plugin/dev/apis/common).
Each component is a core functional module of your plugin, which can be [added and removed](/en/plugin/dev/components/add) as needed. Different components are called in different situations, making it easy to extend plugin functionality later. Each component directly inherits from the various component base classes under the `langbot_plugin.api.definition.components` package and contains a `plugin: BasePlugin` object, allowing you to directly call the plugin's APIs.
<Info title="">
- For detailed API documentation, please refer to [API Reference](/en/plugin/dev/apis/common).
Please continue reading this tutorial to understand the plugin development process.
</Info>
## AI-Assisted Development
- [skills-LangBotplugin](https://github.com/TyperBody/skills-LangBotplugin) - Skills contributed by community member [@TyperBody](https://github.com/TyperBody), use AI to quickly develop plugins, welcome to try
- *langbotplugin* This skill provides automated plugin generation tools
- *langbotplugindebug* This skill provides plugin debugging tools
## Installing CLI
Please ensure you have Python 3.10 or higher installed, and have installed the [uv package manager](https://docs.astral.sh/uv/).
Execute the command in any empty directory to install LangBot CLI and SDK:
```bash
pip install -U langbot_plugin
```
## Initializing Plugin Directory
Assuming your plugin name is `HelloPlugin`, create a directory `HelloPlugin` in any directory, enter that directory, and execute the command to initialize the plugin:
```bash
lbp init
```
Follow the prompts to enter `Author`, `Description`, and other information.
<Info title="">
You can also use the `lbp init HelloPlugin` command to initialize the plugin in the subdirectory `HelloPlugin`.
<img width="600" src="/images/zh/plugin/dev/create_plugin.png" />
</Info>
This operation will generate the initial files for the plugin. You can now open the `HelloPlugin` directory in your favorite editor and start writing plugin code.
<Info title="">
If you get a "command not found" error for `lbp`, it may be because you have not properly set the `PATH` environment variable.
You can use `python -m langbot_plugin.cli.__init__` instead of the `lbp` command.
For example:
```bash
python -m langbot_plugin.cli.__init__ init HelloPlugin
cd HelloPlugin
```
</Info>
## Starting Debug Mode
You need to first deploy and start LangBot, ensuring that Plugin Runtime is running and listening on port `5401`.
<Info title="Runtime has two startup modes:">
- `Stdio` mode: When you start LangBot using source code without the startup parameter `--standalone-runtime`, LangBot will automatically start Plugin Runtime as a subprocess and communicate with Plugin Runtime through `stdio` (standard input/output streams). In this case, Runtime will load plugins from the `data/plugins` directory in the LangBot root directory and listen on port `5401` of the LangBot host as a debug port.
- `WebSocket` mode:
- Production environment: When you start LangBot using the official `docker-compose.yaml`, Plugin Runtime will run in a separate container, and LangBot will communicate with Plugin Runtime in WebSocket mode due to the startup parameter `--standalone-runtime`. The 5401 port of the Runtime container will be mapped to the 5401 port of the host as default.
- Development environment: If you start LangBot through source code with the startup parameter `--standalone-runtime`, LangBot will connect to the already started Plugin Runtime according to the `plugin.runtime_ws_url` address (port usually 5400) configured in `data/config.yaml`. You need to start the standalone Plugin Runtime yourself, see [Developing Plugin Runtime](/en/develop/plugin-runtime).
Whether in `stdio` or `websocket` mode, Plugin Runtime will listen on port `5401` of its host as a debug port for plugin debugging connections.
When developing plugins, we recommend starting LangBot according to the [Development Configuration](/en/develop/dev-config) method, which will start Plugin Runtime in Stdio mode, making plugin development easier.
</Info>
Copy `.env.example` to `.env`. In LangBot WebUI, open **Extensions → Debug Info** and copy the current Workspace's Debug URL and Debug Key into `DEBUG_RUNTIME_WS_URL` and `PLUGIN_DEBUG_KEY`. Every Workspace has a different key, and it rotates every two hours; copy it again after expiry.
```bash
cp .env.example .env
```
Start plugin debugging, and you will see the plugin output:
```bash
lbp run
```
<img width="600" src="/images/zh/plugin/dev/launch_debug_plugin.png" />
And you can see this plugin has been loaded in LangBot's WebUI.
<img width="600" src="/images/zh/plugin/dev/debug_plugin_loaded.png" />
## What's Next
This tutorial will guide you through step-by-step completion of plugin functionality.
- Modify Plugin Information: The plugin has been created with basic plugin information. Please [complete the plugin information](/en/plugin/dev/basic-info).
- Add Components: Plugin components are the core functional units of plugins. You can [add components](/en/plugin/dev/components/add) based on your needs.